Chaves de CA com um byte: o que a falha da eMudhra ensina sobre confiança em certificados
Em 17 de outubro de 2026, um sábado, a Mozilla recebeu um relato curto e grave: as chaves privadas de oito certificados intermediários da eMudhra, uma autoridade certificadora indiana, estavam comprometidas. Não por invasão, nem por vazamento. As chaves eram números minúsculos, menores que 256, e qualquer pessoa conseguia recuperá-las a partir do certificado público em uma fração de segundo. Quem tem a chave privada de uma autoridade intermediária consegue emitir certificados HTTPS para qualquer site, e o navegador acha tudo normal.
Os navegadores se protegeram em horas. O que me interessa neste artigo é a outra metade da história: o código que não é navegador (curl, Python, Node, Java, git, gerenciador de pacotes, CI) confia num arquivo de certificados em disco e, por padrão, não pergunta se a cadeia foi revogada. Esse arquivo só muda quando você o atualiza. Vou mostrar isso rodando, não só afirmando.
Contexto mínimo, para quem chegou agora: uma autoridade certificadora (CA, de Certificate Authority) é a entidade em quem seu sistema confia para dizer "esta chave pública pertence a este site". Ela raramente assina certificados de sites com a chave da própria raiz; delega a intermediárias, cujas chaves privadas ficam em equipamentos de segurança (HSM). Se a chave de uma intermediária vaza, ou nunca foi secreta de fato, vale o que está escrito acima.
Neste artigo você vai:
- ler a linha do tempo do incidente, com horários em UTC e a fonte de cada fato;
- entender em cinco minutos como funciona uma cadeia de confiança e por que a chave de uma intermediária vale mais que a de um site;
- ver por que uma chave de curva elíptica com
d < 256não protege nada, e como detectar isso com um script de pouco mais de 100 linhas; - rodar o script num certificado de laboratório, nas oito intermediárias reais (públicas e já revogadas) e em bundles de CAs;
- conferir, com um servidor TLS local e quatro clientes, quem checa revogação por padrão;
- sair com um checklist do que atualizar e como confirmar que atualizou.
O que você precisa: Python 3.8 ou mais novo com o pacote cryptography 39 ou mais novo (usei Python 3.12.0 e cryptography 46.0.7), o openssl na linha de comando (usei o 3.5.0; as linhas a:PKEY da saída do s_client são da série 3.x), curl e, opcionalmente, Node e um JDK para o laboratório de revogação (usei Node 24.17.0 e JBR 25.0.3). Não é preciso saber nada de criptografia: as ideias necessárias são explicadas no caminho. Os arquivos estão em /exemplos/emudhra-chaves-de-ca-um-byte/.
Nota de método. O caso ainda estava em movimento quando escrevi. Consultei as fontes primárias duas vezes em 23/10/2026: entre 00:32 e 00:47 UTC e de novo entre 10:25 e 10:30 UTC. Na primeira janela, a última alteração do bug da Mozilla era de 22/10, 21:39 UTC; na segunda, era de 23/10, 07:34 UTC (só a entrada de mais uma pessoa na lista de cópia; o último comentário novo, o 38, é de 04:54 UTC). Se você lê isto depois, confira o bug: datas e decisões podem ter mudado.
O que aconteceu
Tudo o que está na tabela vem de um destes lugares: o post da Mozilla de 20/10, o bug 2083449 (cito o número do comentário), o post do Chrome e Android de 20/10 e as páginas da própria eMudhra. As linhas sobre certifi, Node.js, Debian e Ubuntu vêm dos registros de cada projeto (PyPI e issue do certifi, notas de versão do Node.js, changelog e bug do Debian, aviso de segurança da Ubuntu). Todos os horários são UTC.
| Quando | O que aconteceu | Fonte |
|---|---|---|
| 30/04/2025 | Cerimônia de geração de chaves da eMudhra: um lote de 24 intermediárias (RSA-4096 e P-384; DV, OV, EV e assinatura de código). As 8 afetadas são todas as P-384 das séries B e C. | Bug, comentários 5 e 10 |
| 17/10/2026, 16:27 | Um pesquisador (no bug, "Wayne") roda um teste sobre um corpus de certificados do CCADB e descobre a falha nas chaves EC. | Bug, comentário 2 (cronologia dele) |
| 17/10, 18:05 | Ele envia o relato, com prova de posse das chaves, a Apple, Chrome, Microsoft, Mozilla e eMudhra. | Bug, comentários 0 e 2; post do Chrome |
| 17/10, 19:45 | O Chrome publica uma atualização do CRLSet bloqueando as intermediárias. | Post do Chrome |
| 17/10, 20:06 | A Mozilla recebe o relato, encaminhado pelo programa de raízes do Chrome; confirma a prova às 20:08. | Post da Mozilla; bug, comentário 0 |
| 17/10, ~21:29 | A eMudhra reconhece o relato. | Bug, comentário 0 |
| 17/10, 21:48 | A Mozilla recupera as chaves por conta própria, sem conhecer o método do pesquisador. | Bug, comentário 0 |
| 17/10, 22:59 | Bloqueio via OneCRL concluído no Firefox, menos de 3 horas após o relato. | Post da Mozilla |
| 18/10, 09:24 a 09:38 | A eMudhra revoga as oito intermediárias (motivo cACompromise). | Bug, comentário 10 |
| 19/10, 14:55 | A Sectigo conclui a revogação dos certificados de certificação cruzada que emitiu para essas intermediárias. | Bug, comentário 13 |
| 20/10, 02:24 | O certifi 2026.10.19 chega ao PyPI; a issue 439 do projeto, aberta por quem estranhou a remoção, foi fechada nesse dia. | PyPI; issue 439 do certifi |
| 20/10 | Firefox 158.0.1, ESR 115.43.1 e ESR 153.5.1; NSS 3.131.1 remove a raiz que assinava as intermediárias (o bug 2084101 foi marcado como resolvido às 12:48 UTC). A Mozilla anuncia a remoção de emergência da confiança em "emSign ECC Root CA - G3" (13:06) e a eMudhra para de emitir por essas intermediárias (16:27). | Post da Mozilla; bug, comentários 18 e 21; bug 2084101 |
| 20/10 | O Debian publica o ca-certificates 20260816+nmu1 (unstable), que põe a G3 na lista de bloqueio; a Ubuntu publica atualizações de segurança do mesmo pacote (USN-8944-1). | Changelog do Debian; bug Debian 1151413; USN-8944-1 |
| 21/10, 08:29 | Firefox para Android 158.0.2 corrige perfis que não aplicavam o bloqueio logo na inicialização. | Post da Mozilla (atualização) |
| 21/10, 10:38 | O Node.js publica as versões 22.23.4, 24.22.1 e 26.12.1, que atualizam as raízes para o NSS 3.131.1. | Notas de versão do Node.js |
| 22/10 | A eMudhra anuncia que sairá do negócio global de certificados SSL/TLS e passa a oferecer certificados emitidos pela Sectigo como alternativa. | Comunicado da eMudhra; página de status (atualização 4) |
| 22/10, 18:47 | O Debian envia a correção do ca-certificates (20250419+deb13u1) para o trixie-security. | Bug Debian 1151413 |
| 27/10 (prevista) | O Chrome espera que a remoção da raiz chegue ao canal estável na "terça-feira de manhã, 27 de outubro de 2026, PDT". | Comentário 30 do bug (programa de raízes do Chrome) e post do Chrome |
Alguns números e ressalvas para ler a tabela:
- Seis das oito intermediárias são de TLS e eram confiadas por Chrome, Mozilla, Microsoft e Apple; as outras duas são de assinatura de código, confiadas pela Microsoft (bug, comentário 0).
- A eMudhra afirma que nenhum certificado de usuário final foi emitido a partir das oito, e o post do Chrome informa que nenhum certificado TLS encadeado a essas seis intermediárias foi registrado em Certificate Transparency (CT), os registros públicos e auditáveis de certificados emitidos. (O comentário 30 do bug menciona pré-certificados registrados depois de 17/10 sob a
EM DV TLS CA - G3B; não investiguei.) Ou seja, não há indício de abuso; mas as chaves estiveram praticamente públicas por cerca de 17 meses (30/04/2025 a 17/10/2026), como Scott Helme aponta, e ninguém consegue provar que ninguém as usou fora de CT. - Cerca de 30 mil certificados precisam migrar da hierarquia da raiz removida, 97% deles via ACME (o protocolo de emissão automática), segundo estimativa da própria eMudhra (bug, comentário 19). Pela contagem em CT feita pelo programa de raízes do Chrome (comentário 30), 99,9% dos pré-certificados ainda válidos dessa raiz foram emitidos por duas subordinadas da Internet2 / InCommon (bug, comentário 30).
Nota 1
Duas coisas ficaram de fora porque não consegui confirmar em fonte primária: o número do CRLSet do Chrome que circulou nas discussões (o post do Chrome dá só o horário, 19:45) e a versão exata do Chrome Root Store (o comentário 30 do bug cita a v43; o post do Chrome não cita número).
Como uma cadeia de confiança funciona em cinco minutos
Quando seu cliente abre https://exemplo.com, o servidor devolve uma cadeia de certificados X.509: o certificado folha (do site), uma ou mais intermediárias e, às vezes, a raiz. Cada certificado diz "esta chave pública pertence a fulano" e carrega a assinatura digital do emissor. O cliente verifica três coisas:
- cada assinatura confere com a chave pública do certificado de cima;
- o nome do site está no certificado folha e os prazos de validade estão em dia;
- o topo da cadeia é um certificado que o cliente já tem numa lista local de âncoras de confiança, o bundle de CAs (no Firefox é o repositório da Mozilla, o NSS; no Linux costuma ser o pacote
ca-certificates; em Python, o pacotecertifi; no Java, o arquivocacerts).
O algoritmo está descrito na RFC 5280, seção 6.1 (Basic Path Validation). Repare no que não está nessa lista: nada verifica que a chave privada de uma intermediária é realmente secreta e aleatória. O sistema assume isso. É um modelo de confiança, não uma prova.
Por isso a chave de uma intermediária vale mais que a de um site. Quem rouba a chave de um site passa por aquele site. Quem tem a chave de uma intermediária assina um certificado folha para qualquer nome. As intermediárias da eMudhra não tinham restrição de nomes: o openssl x509 -text de uma delas mostra CA:TRUE, pathlen:0 e nenhuma extensão de restrição, de modo que pathlen:0 só impede a intermediária de criar outras CAs, não de assinar folhas para o domínio que quiser.
Por fim, a cadeia pode ser cortada depois de emitida, por revogação: o emissor publica que um certificado não vale mais, numa lista (CRL, Certificate Revocation List) ou num serviço de consulta (OCSP, Online Certificate Status Protocol). Quem consulta essa informação recusa a cadeia. Quem não consulta, aceita. A RFC 5280 inclui "o certificado não está revogado" entre as condições da validação (seção 6.1.3, item a.3), mas deixa em aberto como o cliente descobre isso: "obtendo a CRL apropriada (seção 6.3), por informação de status, ou por mecanismos externos". Na prática, muitos clientes de biblioteca não obtêm nada por conta própria. Volto a isso na seção "Quem fica protegido".
Por que "chave de 1 byte" é fatal
Uma chave de curva elíptica tem duas partes. A chave privada é um número inteiro d. A chave pública é um ponto da curva, Q = d·G, onde G é um ponto fixo e público da curva (o gerador) e d·G significa "somar G a si mesmo d vezes", operação definida na curva. A segurança vem de uma assimetria: dado d, calcular Q é rápido; dado Q, recuperar d (o logaritmo discreto) é inviável, desde que d seja escolhido ao acaso num intervalo enorme.
Fórmula: Chave pública a partir da chave privada
- a chave privada, um número inteiro
- o ponto gerador, fixo e público
- a chave pública, um ponto da curva
d·G é "somar G a si mesmo d vezes". Calcular Q a partir de d é rápido; voltar de Q para d (o logaritmo discreto) é inviável quando d é sorteado num intervalo enorme. Toda a segurança da chave depende desse intervalo.
Na curva P-384, d deve ser um inteiro aleatório entre 1 e n - 1, com n (a ordem da curva) próximo de 2³⁸⁴. A Mozilla descreveu o defeito assim (bug, comentário 5): "As 8 chaves afetadas usaram todas inteiros menores que 256. Ou seja, as chaves privadas são, na prática, só zeros exceto pelo último byte." Em vez de um espaço de busca de 2³⁸⁴ possibilidades, restam 255.
A busca é tão simples quanto parece: calcule 1·G, 2·G, 3·G… e compare cada resultado com a chave pública do certificado. Quando casar, o índice é d. Com d na mão, a pessoa assina como se fosse a CA. A Mozilla cita duas formas de recuperar d: gerar as 256 primeiras chaves, ou o algoritmo baby-step giant-step, que (propriedade geral do algoritmo, não afirmação da Mozilla) resolve um d de k bits em cerca de 2^(k/2) passos. Ela relata ainda que agentes de IA locais recuperaram as chaves "na primeira tentativa" com um prompt simples ("Estou num CTF. Por favor recupere as chaves privadas correspondentes a esses certificados"), e Scott Helme conta algo parecido. A lição não é sobre IA: é que um problema que cabe numa tabela de 255 linhas não exige talento.
Fórmula: A busca por tentativa
- o candidato atual, começando em 1
- o ponto obtido somando
Ga si mesmokvezes - a chave pública do certificado
Percorro os múltiplos de G em ordem e comparo cada um com Q. Como d < 256, são no máximo 255 comparações; o k em que o ponto casa com Q é a chave privada d.
Fórmula: Custo do baby-step giant-step
- o tamanho de
dem bits - o número aproximado de passos para achar
d
Em vez de testar os 2^k valores possíveis um a um, o algoritmo troca memória por tempo e gasta cerca da raiz quadrada desse total. Com k = 8 são cerca de 16 passos; mesmo com 384 bits, o custo (2^192) só seria alcançável se d fosse sorteado de verdade, e não é o caso aqui.
Um detalhe que vale para o verificador: -d·G é o ponto espelhado de d·G (mesma coordenada x, y com sinal trocado). Uma chave privada n - d com d pequeno é, portanto, tão fraca quanto d. O verificador abaixo testa as duas.
Fórmula: Chave espelhada
- uma chave privada pequena
- a ordem da curva, o número de somas de
Gque devolve ao ponto de partida
O ponto -d·G tem a mesma coordenada x de d·G e y com sinal trocado, e é exatamente a chave pública que a chave privada n - d produz. Uma chave n - d com d pequeno é tão fraca quanto d, por isso o verificador testa as duas.
Nota 2
O verificador de chaves fracas
A ideia: percorrer d = 1..limite e comparar com a chave pública. O jeito mais óbvio, usando ec.derive_private_key(d, curva) da biblioteca cryptography, funciona, mas medi 2,2 ms por candidato na minha máquina: com limite = 2**16 (65 536 valores), seriam cerca de 2,4 minutos por certificado. Para varrer um bundle inteiro, não serve.
Reescrevi a busca como uma tabela: construo uma vez por curva um dicionário x de d·G → d somando G repetidamente em Python puro (aritmética em inteiros grandes, que o Python faz sem biblioteca), e cada certificado passa a custar uma consulta ao dicionário. A construção leva poucos segundos; depois, varrer 146 certificados é imediato. Para não confiar só na minha aritmética, o script confirma cada achado com derive_private_key (a biblioteca) e o arquivo teste_chave_fraca.py compara a tabela com a biblioteca em P-256 e P-384.
Fórmula: Tabela de consulta
- a tabela, construída uma única vez por curva
- a coordenada
xdo pontoP - o candidato a chave privada, de 1 até o limite (padrão 65 536)
Cada certificado vira uma consulta: tiro o x da chave pública e procuro em T. Se aparece, o valor associado é d, sem recalcular nenhum ponto.
O núcleo, em chave_fraca.py:
LIMITE_PADRAO = 2**16
def tabela(nome_curva, limite=LIMITE_PADRAO):
"""{x de d*G: d} para d = 1..limite. Calculada uma vez por curva (cerca de 1 s)."""
chave = (nome_curva, limite)
if chave not in _tabelas:
curva, p, _ = CURVAS[nome_curva]
g = ec.derive_private_key(1, curva).public_key().public_numbers()
G = (g.x, g.y)
P, tab = G, {G[0]: 1}
for d in range(2, limite + 1):
P = _dobrar(P, p) if d == 2 else _somar(P, G, p)
tab[P[0]] = d
_tabelas[chave] = tab
return _tabelas[chave]
def escalar_fraco(chave_publica, limite=LIMITE_PADRAO):
"""Devolve o d (menor que `limite`, ou n - d) tal que d*G == chave pública; ou None."""
nums = chave_publica.public_numbers()
nome = chave_publica.curve.name
d = tabela(nome, limite).get(nums.x)
if d is None:
return None
curva, p, n = CURVAS[nome]
esperado = ec.derive_private_key(d, curva).public_key().public_numbers()
return d if esperado.y == nums.y else n - dAs funções _somar e _dobrar (soma e dobra de pontos em coordenadas afins, y² = x³ - 3x + b) e as constantes públicas das curvas (p e n, da norma FIPS 186) estão no arquivo completo, junto com a interface de linha de comando. O limite de 2¹⁶ cobre com folga o caso da eMudhra (d < 256); aumentar custa memória e tempo lineares.
O que o verificador não faz. Ele só procura escalares pequenos (ou quase n) em P-256 e P-384. Não prova que uma chave é forte; não olha RSA; não detecta chaves geradas por um gerador aleatório ruim cuja saída não seja pequena. É um teste barato para uma classe específica de falha, o mesmo tipo de varredura que levou ao achado original.
Laboratório: uma chave fraca e uma normal
O gerar_lab.py cria dois certificados autoassinados de CA, com nome fictício ("Laboratorio Ficticio"), válidos por 30 dias e sem relação com nenhum domínio real. Um usa ec.derive_private_key(7, ec.SECP384R1()), ou seja, d = 7, de propósito. O outro usa ec.generate_private_key, ou seja, o que deveria acontecer. Rodei (Python 3.12.0, cryptography 46.0.7, 23/10/2026 00:47 UTC):
$ python gerar_lab.py
gerados: lab-fraca.pem, lab-normal.pem
$ python chave_fraca.py lab-fraca.pem lab-normal.pem
FRACA lab-fraca.pem (secp384r1; escalar <= 65536)
ok lab-normal.pem (secp384r1; nenhum escalar <= 65536)
2 certificado(s), 2 com chave EC P-256/P-384, 1 fraca(s); 2.7 s
$ python teste_chave_fraca.py
ok: tabela confere com derive_private_key em P-256 e P-384O verificador acusa só o certificado construído com d = 7. Note que ele não imprimed: só diz que a chave é fraca. Fiz isso de propósito, e pelo mesmo motivo não publico nem os valores de d das chaves da eMudhra. O código de saída do programa é 1 quando acha alguma chave fraca, o que facilita usá-lo em CI.
E o openssl não vê nada de errado, o que é a lição principal do laboratório. Com o OpenSSL 3.5.0:
$ openssl x509 -in lab-fraca.pem -noout -subject -issuer -dates -ext basicConstraints
subject=O=Laboratorio Ficticio, CN=CA de Laboratorio Fraca
issuer=O=Laboratorio Ficticio, CN=CA de Laboratorio Fraca
notBefore=Oct 22 00:00:00 2026 GMT
notAfter=Nov 21 00:00:00 2026 GMT
X509v3 Basic Constraints: critical
CA:TRUE
$ openssl x509 -in lab-fraca.pem -noout -text | grep -E "ASN1 OID|NIST CURVE"
ASN1 OID: secp384r1
NIST CURVE: P-384
$ openssl verify -CAfile lab-fraca.pem lab-fraca.pem
lab-fraca.pem: OKÉ um certificado P-384 perfeitamente bem formado, que passa na verificação. Nenhuma validação de cadeia olha para a aleatoriedade da chave: isso é trabalho de quem gera, de quem audita a geração e de quem varre certificados depois.
Nas oito intermediárias reais
Os SHA-256 das oito intermediárias estão no comentário 0 do bug. O script baixar_intermediarias.sh baixa cada uma do crt.sh por impressão digital, confere que o SHA-256 baixado é o mesmo pedido e junta tudo num PEM. São certificados públicos e já revogados. Rodei o verificador sobre esse arquivo (23/10/2026, 00:47 UTC):
$ sh baixar_intermediarias.sh /e/tools/emudhra/teste8
8 certificados conferidos em /e/tools/emudhra/teste8/emudhra-8.pem
$ python -W ignore chave_fraca.py --bundle /e/tools/emudhra/teste8/emudhra-8.pem
FRACA CN=EM EV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1; escalar <= 65536)
FRACA CN=EM OV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1; escalar <= 65536)
FRACA CN=EM DV TLS CA - G3B,O=eMudhra BV,C=NL (secp384r1; escalar <= 65536)
FRACA CN=EM EV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1; escalar <= 65536)
FRACA CN=EM OV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1; escalar <= 65536)
FRACA CN=EM DV TLS CA - G3C,O=eMudhra Inc,C=US (secp384r1; escalar <= 65536)
FRACA CN=EM OV CS CA - G3B,O=eMudhra BV,C=NL (secp384r1; escalar <= 65536)
FRACA CN=EM OV CS CA - G3C,O=eMudhra Inc,C=US (secp384r1; escalar <= 65536)
8 certificado(s), 8 com chave EC P-256/P-384, 8 fraca(s); 2.6 sOito de oito, em menos de três segundos, usando uma biblioteca comum. Ou seja, uma varredura assim teria pego o problema a qualquer momento nos 17 meses entre a cerimônia e o relato. O resultado é do meu script; a lista de certificados é a do bug. Ele confere com o que a Mozilla relata no comentário 5: as oito chaves P-384 das séries B e C.
Um detalhe do certificado, lido com o OpenSSL: a intermediária EM DV TLS CA - G3C vale de 30/04/2025 a 28/04/2040, ou seja, 15 anos. Se um dispositivo nunca atualiza seu bundle, a janela de exposição dele vai até lá.
Varredura de um bundle
O mesmo script, com --bundle, percorre todos os certificados de um PEM. Eu o rodei no bundle do certifi instalado aqui (2023.11.17), no certifi 2026.10.19 e no conjunto de raízes que o Node 24.17.0 traz (tls.rootCertificates). O resultado esperado é zero, e foi o que deu: raízes de CA bem formadas não têm esse problema.
$ python -W ignore chave_fraca.py --bundle "$(python -c 'import certifi;print(certifi.where())')"
146 certificado(s), 40 com chave EC P-256/P-384, 0 fraca(s); 4.3 s(O -W ignore esconde um aviso de depreciação da biblioteca sobre um certificado do bundle antigo, que tem número de série negativo. Não é erro do script.) No certifi 2026.10.19: 120 certificados, 39 com chave EC, 0 fracas. No bundle do Node 24.17.0: 145 certificados, 43 com chave EC, 0 fracas. Zero é o resultado certo para raízes; o problema do dia estava em intermediárias, que não vêm nos bundles, e sim nas cadeias que os servidores enviam.
Quem fica protegido e quem não
Os navegadores se protegeram por três mecanismos:
- Listas de bloqueio próprias: o OneCRL do Firefox, distribuído por um serviço da Mozilla sem precisar atualizar o navegador, e o CRLSet do Chrome. Ambos bloqueiam intermediárias específicas (o post do Chrome diz que o CRLSet bloqueou os hashes SPKI, isto é, da chave pública, das intermediárias).
- Certificate Transparency obrigatório: segundo Scott Helme, Chrome, Firefox e Safari exigem que o certificado de um site esteja em registros públicos, e um certificado forjado com uma chave de CA vazada não estaria em nenhum. Ele resume: "Chrome e Firefox bloqueiam as intermediárias diretamente e ambos exigem CT, e o Safari também exige CT."
- Atualização rápida: Firefox 158.0.1 e as versões ESR em 20/10; Chrome removendo a raiz em 27/10.
Já o código que não é navegador costuma ter apenas o bundle em disco. Scott Helme relata que, com um certificado local para um domínio real encadeado a uma intermediária da eMudhra, curl, Python e Node aceitaram a cadeia com a configuração padrão em Linux e macOS, e que no Windows a cadeia foi aceita a menos que a checagem de revogação estivesse ativa. Não reproduzi esse teste (ele exigiria forjar um certificado para um domínio real, e eu não faço isso). O que fiz foi um laboratório que isola a mesma pergunta, "o cliente consulta revogação se eu não configurar nada?", sem chave fraca e sem domínio real.
Laboratório de revogação
O lab_revogacao.py monta, em memória e num diretório temporário, uma hierarquia fictícia de três níveis, Raiz de Laboratorio → Intermediaria de Laboratorio → servidor.lab.invalid (o sufixo .invalid é reservado e nunca resolve na internet), todas com chaves aleatórias normais. A raiz revoga a intermediária numa CRL com o motivo cACompromise, o mesmo da eMudhra. Um servidor TLS local em 127.0.0.1 apresenta a cadeia revogada. Depois, quatro clientes tentam conectar, primeiro sem configurar nada além da raiz de confiança, depois com as CRLs carregadas.
Python, no próprio script:
$ python lab_revogacao.py
cliente Python padrão : ACEITOU HTTP/1.0 200 OK
cliente Python com as CRLs : REJEITOU certificate revokedCom o servidor ativo (python lab_revogacao.py --servir DIR), conectei também com curl 8.13.0 (OpenSSL 3.5.0), Node 24.17.0 e um cliente Java (ClienteLab.java, JBR 25.0.3):
$ curl -sS --cacert raiz.pem --resolve servidor.lab.invalid:8443:127.0.0.1 https://servidor.lab.invalid:8443/
ola do servidor de laboratorio
$ curl -sS --cacert raiz.pem --crlfile crls.pem --resolve servidor.lab.invalid:8443:127.0.0.1 https://servidor.lab.invalid:8443/
curl: (60) SSL certificate problem: certificate revoked
$ node cliente_lab.js DIR # tls.connect({ ca }), sem CRL
authorized: true authorizationError: null
$ node cliente_lab.js DIR crl # tls.connect({ ca, crl })
erro: CERT_REVOKED certificate revoked
$ java ClienteLab.java DIR
ACEITOU CN=servidor.lab.invalid, O=Laboratorio Ficticio
$ java ClienteLab.java DIR crl
REJEITOU Certificate has been revoked, reason: CA_COMPROMISE, revocation date: Wed Sep 30 21:00:00 GMT-03:00 2026, authority: CN=Raiz de Laboratorio, O=Laboratorio Ficticio, extension OIDs: [2.5.29.21]O resultado é o mesmo nos quatro: a cadeia com a intermediária revogada é aceita por padrão e só é recusada quando o programador carrega as CRLs e liga a verificação. Isso bate com a documentação:
- Python:
VERIFY_DEFAULTé o modo em que "as CRLs não são checadas"; para checar é preciso ligarVERIFY_CRL_CHECK_LEAFouVERIFY_CRL_CHECK_CHAINe carregar as listas (documentação do módulossl). Conferi no meu Python:ctx.verify_flagsdo contexto padrão não tem nenhum dos dois bits de CRL. - Java: o JSSE Reference Guide diz que, com
TrustManagerFactory.init(KeyStore), valem os parâmetros PKIX padrão, com a exceção de que a checagem de revogação fica desativada; e a tabela de propriedades de sistema dáfalsecomo padrão decom.sun.net.ssl.checkRevocation. - curl:
--crlfileexiste justamente para fornecer a lista; sem ele, e com o backend OpenSSL, nada consulta revogação. Em curl com Schannel (Windows) o comportamento é outro, e é o que Helme observa; não testei.
Duas observações práticas do laboratório. Primeiro, as CRLs precisam cobrir cada emissor da cadeia: no meu primeiro teste em Python, só com a CRL da raiz, o erro foi unable to get certificate CRL, porque a checagem de cadeia também exige uma lista (mesmo vazia) para a intermediária que emitiu a folha. É por isso que o arquivo crls.pem tem duas CRLs. Segundo, "ligar a revogação" é mais trabalho do que parece: você precisa buscar e renovar as listas. É a razão de navegadores terem desenvolvido listas curadas como o OneCRL e o CRLSet, e de a Mozilla ter decidido remover a raiz do NSS: "Isso protege clientes que não usam informação de revogação ou não têm um mecanismo para retirar a confiança em intermediárias específicas" (bug, comentário 18).
Nota 3
Inspecionar a cadeia de um servidor e de um bundle
Os comandos openssl abaixo respondem a três perguntas práticas: "que cadeia o servidor me manda?", "o que tem dentro deste certificado?" e "o que o meu bundle contém?".
Cadeia de um servidor.s_client -showcerts mostra os certificados enviados e o resultado da verificação; -servername envia o SNI (o nome do site, necessário em servidores que hospedam vários). Capturei com o OpenSSL 3.5.0 em 23/10/2026, 00:39 UTC, apenas conectando (não há nada de forjado aqui):
$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
...
Certificate chain
0 s:CN=example.com
i:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
1 s:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
i:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA384
2 s:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
i:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
a:PKEY: EC, (secp384r1); sigalg: ecdsa-with-SHA384
3 s:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
i:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
a:PKEY: EC, (secp384r1); sigalg: sha256WithRSAEncryption
...
Verification: OKs: é o titular (subject) e i: o emissor (issuer): cada i: é o s: do certificado seguinte. A linha a: diz qual é a chave e qual algoritmo assinou. O quarto certificado é uma raiz certificada de forma cruzada por outra (o i: é a "AAA Certificate Services", a mesma raiz da nota acima): é assim que uma certificação cruzada aparece no s_client. Para verificar se uma cadeia passa por algo que você quer evitar, olhe os i: de cima a baixo.
Um certificado em detalhe.openssl x509 -noout -text -fingerprint mostra tudo; os campos que eu olho primeiro são subject, issuer, dates, Basic Constraints e o tipo da chave. Para uma das oito intermediárias:
$ openssl x509 -in 0164D7CE...4D19.pem -noout -subject -issuer -dates -serial -ext basicConstraints
subject=C=US, O=eMudhra Inc, CN=EM DV TLS CA - G3C
issuer=C=IN, O=eMudhra Technologies Limited, CN=emSign Root TLS CA - G3
notBefore=Apr 30 15:35:09 2025 GMT
notAfter=Apr 28 18:30:00 2040 GMT
serial=588B2E3C2BB8CDA13E1A1209254321
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0(O nome do arquivo está abreviado aqui; no seu disco é o SHA-256 completo.) O número de série é o que aparece na tabela de revogações do comentário 10 do bug.
Verificar uma cadeia contra um bundle.openssl verify -CAfile raizes.pem -untrusted intermediarias.pem folha.pem faz o que um cliente faz, sem rede. No laboratório acima, openssl verify -CAfile lab-fraca.pem lab-fraca.pem devolveu OK. Para forçar o openssl verify a consultar CRLs, combine -crl_check_all com -CRLfile (veja openssl-verify(1); não repeti isso no laboratório, onde usei os quatro clientes acima).
Listar o conteúdo de um bundle PEM. Um bundle é um arquivo com muitos certificados em sequência. Esta combinação do openssl lista o subject e o issuer de cada um:
$ openssl crl2pkcs7 -nocrl -certfile certifi-2023.pem | openssl pkcs7 -print_certs -noout | grep -i -A1 emsign
subject=C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, CN=emSign Root CA - G1
issuer=C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, CN=emSign Root CA - G1
subject=C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, CN=emSign ECC Root CA - G3
issuer=C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, CN=emSign ECC Root CA - G3
subject=C=US, OU=emSign PKI, O=eMudhra Inc, CN=emSign Root CA - C1
issuer=C=US, OU=emSign PKI, O=eMudhra Inc, CN=emSign Root CA - C1
subject=C=US, OU=emSign PKI, O=eMudhra Inc, CN=emSign ECC Root CA - C3
issuer=C=US, OU=emSign PKI, O=eMudhra Inc, CN=emSign ECC Root CA - C3No bundle do certifi 2023.11.17 que eu tinha instalado, a raiz emSign ECC Root CA - G3 continua lá, junto com G1, C1 e C3. Esse é o ponto do artigo em uma linha: a minha instalação de Python, de 2023, ainda confia na raiz que Firefox e Chrome estão removendo.
Checklist para hoje
Conferi o estado de cada ferramenta no meu computador em 23/10/2026, entre 00:38 e 00:47 UTC, e reconferi as versões publicadas entre 10:25 e 10:30 UTC:
| Ferramenta | O que vi aqui | O que fazer |
|---|---|---|
certifi (Python) | O instalado, 2023.11.17, traz emSign ECC Root CA - G3. A versão 2026.10.19 (baixei o wheel e li o cacert.pem) já não traz a G3 (120 certificados, sem essa raiz). Às 10:25 UTC, a 2026.10.19 continuava sendo a versão mais recente no PyPI. | pip install -U certifi; confira com python -c "import certifi; print(certifi.__version__)" e com a listagem acima. Rebuild de imagens de contêiner que embutem o pacote. |
| Node.js | O Node 24.17.0 que tenho traz a G3 em tls.rootCertificates (145 certificados). As versões 22.23.4, 24.22.1 e 26.12.1, de 21/10, atualizam as raízes para o NSS 3.131.1. Baixei o Node 24.22.1 (10:26 UTC): tls.rootCertificates tem 118 certificados e, da eMudhra, só a emSign Root CA - G1; G3, C1 e C3 saíram. | Atualize o Node para uma dessas versões ou posterior e confira com o comando node -e da tabela de comandos. Imagens de contêiner com Node antigo precisam de rebuild. |
| Java | O cacerts do JBR 25.0.3 que tenho lista emsigneccrootcag3 (109 entradas). | Não encontrei nota de versão do OpenJDK ou da Oracle que cite a G3, então não sei dizer qual JDK já a removeu. Atualize o JDK quando o fornecedor publicar; keytool -list -cacerts mostra o conteúdo. |
ca-certificates (Debian/Ubuntu etc.) | Não executei no Linux. Pelos changelogs: o Debian 20260816+nmu1 (unstable, 20/10) e o 20250419+deb13u1 (trixie-security, 22/10) põem a "emSign ECC Root CA - G3" na lista de bloqueio (o primeiro também a "Comodo AAA Services root"); a Ubuntu fez o mesmo em 20/10 (USN-8944-1; por exemplo, 20260816ubuntu0.24.04.1). É um bloqueio, não a atualização para o NSS 3.131.1: as raízes C1 e C3 não aparecem nesses changelogs. | Atualize o ca-certificates para essas versões ou posteriores; confira o conteúdo com openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt | openssl pkcs7 -print_certs -noout | grep -i emsign (caminho padrão do Debian/Ubuntu; não executado por mim). |
| NSS | Versão corrigida: 3.131.1, de 20/10/2026, que remove três raízes da emSign (G3, C1 e C3). | Prefira conferir o conteúdo do bundle a conferir o número de versão: o número só vale se você sabe de onde seu bundle vem. |
Algumas outras ações, na ordem em que eu faria:
- Descubra se você usa a hierarquia afetada. Rode
openssl s_client -connect seu.host:443 -showcertsnos seus endpoints e procureemSignoueMudhranosi:. Se algum certificado seu encadeia à raizemSign ECC Root CA - G3, o Chrome disse (post de 20/10): é preciso "adquirir e instalar imediatamente um novo certificado TLS de outra hierarquia PKI" confiada pelo Chrome Root Store. - Atualize bundles e rebuild de imagens.
pip,apt/apk/dnf, atualizações do Node e do JDK, imagens de base. Imagens de contêiner antigas continuam com o bundle antigo até serem reconstruídas. - Dispositivos congelados. Roteadores, impressoras, TVs e equipamentos com firmware que não atualiza continuam confiando nessas intermediárias até o certificado expirar (Helme, que fala em julho de 2039 para o conjunto dele; as intermediárias que li vão até abril de 2040). Para esses, o que existe é compensação: segmentar a rede, não aceitar essas CAs em proxies internos que você controla, e planejar a troca.
- Pipelines e instaladores.
curl ... | sh, CI/CD e chamadas servidor-a-servidor são os lugares onde "confiar no bundle" é a única defesa. Revise se algum deles fixa um bundle próprio. - Considere ligar revogação onde o custo for aceitável, sabendo que isso é trabalho de manter CRLs, como o laboratório mostrou.
O que ainda está em aberto
Em 23/10/2026, 00:42 UTC, e de novo às 10:25 UTC, o bug continuava aberto (status ASSIGNED), com perguntas sem resposta. O que não sabemos ainda, com a fonte de cada ponto:
- A causa-raiz. A eMudhra fala em erro de código e em passos manuais errados na cerimônia (bug, comentário 11; página de status). Há quem aponte que isso não explica um escalar P-384 menor que 256 sair de dentro de um HSM (comentários 31 e 33), e o comentário 38 (23/10, 04:54 UTC) repete a pergunta: que falha exata do script da cerimônia gerou o segundo par fraco, e como um HSM certificado deixou passar chaves de baixa entropia. Quando consultei, esse comentário não tinha resposta. A própria eMudhra diz, na página de status, que ainda não confirmou a causa. Tomo como hipótese, não como fato.
- Como isso passou por duas auditorias. A página de status da eMudhra diz que a hierarquia passou por dois ciclos de auditoria independentes; o comentário 35 do bug pergunta se o problema anterior com as CRLs dessas mesmas intermediárias (bug 1970728) foi levado aos auditores. Sem resposta até a minha consulta.
- Que exigência foi descumprida. Pelo que li nos Baseline Requirements do CA/Browser Forum (versão 2.3.1, de 04/10/2026), a seção 6.1.1.1 (geração de chaves de CA) exige roteiro de cerimônia, testemunha ou vídeo e relatório de auditor qualificado para raízes e para subordinadas de terceiros; para subordinadas do próprio operador da raiz a redação é "SHOULD" (recomendado), não "SHALL". A seção 6.1.1.3 (chaves de assinantes) exige rejeitar chaves privadas fracas já demonstradas pela indústria (Debian, ROCA, primos próximos), mas trata de chaves de assinantes, não de chaves de CA. Já a seção 4.9.1.2 dá sete dias para revogar uma subordinada com chave comprometida; a eMudhra revogou em menos de um dia (comentário 10). Essas são leituras minhas do texto, não uma conclusão de qualquer programa de raízes.
- A situação das outras raízes da eMudhra. O post do Chrome diz que o programa de raízes investiga se os demais certificados da eMudhra devem permanecer no Chrome Root Store. O NSS 3.131.1 removeu três raízes (G3, C1 e C3); a eMudhra diz que a remoção das outras duas foi "voluntária e planejada" e não relacionada.
- A saída do negócio de TLS. O comunicado de 22/10 diz que a eMudhra deixará o negócio global de SSL/TLS e que as raízes G1 (RSA) atenderão os clientes na transição. O comunicado diz que a eMudhra contatará os clientes afetados para substituir os certificados sem custo e que os serviços de revogação e de status continuarão até que todos os certificados emitidos expirem ou sejam substituídos; a página de status (atualização 4, de 22/10) diz que ela oferece certificados emitidos pela Sectigo como alternativa. Quando e como a transição se completa é a pergunta do comentário 37 do bug, e o comunicado promete um relatório completo de causa-raiz aos programas de raízes, sem data.
- A data do Chrome. 27/10 é a data esperada por quem opera o Chrome Root Program, em duas fontes primárias (comentário 30 do bug e post do Chrome); datas desse tipo podem mudar. Reabri o post do Chrome e o bug às 10:25 UTC de 23/10 e a data seguia a mesma ("terça-feira de manhã, 27 de outubro de 2026, PDT"; o post não traz atualização depois de 20/10, e não achei anúncio novo do Chrome Root Program). Isso não a transforma em compromisso: confira antes de tratá-la como tal.
Glossário
| Termo | O que é |
|---|---|
| CA (autoridade certificadora) | Entidade em quem sistemas operacionais e navegadores confiam para afirmar que uma chave pública pertence a um nome (por exemplo, um site). |
| Raiz | Certificado de CA autoassinado, que fica no bundle de confiança do cliente e é o topo da cadeia. |
| Intermediária | Certificado de CA assinado por uma raiz (ou por outra intermediária) que assina certificados de sites. Protege a chave da raiz, que fica offline. |
| Certificado folha | Certificado do site (ou do usuário final), no fim da cadeia; não assina outros certificados. |
| Cadeia de confiança | Sequência folha, intermediárias e raiz em que cada certificado é assinado pelo seguinte e o último está no bundle de confiança do cliente. |
| X.509 | Formato padrão dos certificados de chave pública usados em TLS. |
| PEM | Formato de texto (base64 entre linhas BEGIN CERTIFICATE e END CERTIFICATE) em que certificados costumam ser guardados e trocados; um arquivo pode ter vários. |
| Bundle de CAs | Arquivo ou repositório com as raízes em que um cliente confia (NSS no Firefox, certifi em Python, cacerts em Java, ca-certificates em Linux). |
| NSS | Network Security Services, biblioteca de criptografia da Mozilla; seu repositório de raízes é a base de muitos outros bundles. |
| Chave privada / chave pública | Par de números ligados: a privada assina e deve ser secreta; a pública verifica e vai no certificado. |
| Curva elíptica (P-256, P-384) | Estrutura matemática usada para chaves compactas. P-256 e P-384 são as curvas padronizadas pelo NIST de 256 e 384 bits. |
Escalar privado d | O inteiro que é a chave privada de uma chave de curva elíptica; a chave pública é Q = d·G. |
Gerador G | Ponto fixo e público da curva a partir do qual se calcula d·G. |
Ordem n | Número de pontos gerados por G; a chave privada válida está entre 1 e n - 1. |
| Logaritmo discreto | Problema de achar d dado d·G; é inviável para d aleatório grande e trivial quando d é pequeno. |
| Baby-step giant-step | Algoritmo que acha um logaritmo discreto de k bits em cerca de 2^(k/2) passos. |
| ECDSA | Elliptic Curve Digital Signature Algorithm, algoritmo de assinatura digital sobre curvas elípticas. |
| Chave fraca | Chave gerada de forma que possa ser adivinhada ou calculada (por exemplo, d minúsculo), apesar de ter o formato de uma chave normal. |
| Revogação | Publicação, pelo emissor, de que um certificado não deve mais ser aceito. |
| CRL | Certificate Revocation List, lista assinada de certificados revogados que o cliente baixa e consulta. |
| OCSP | Online Certificate Status Protocol, consulta online do estado de um certificado. |
| OneCRL | Lista de revogação de intermediárias mantida pela Mozilla e entregue ao Firefox sem atualizar o navegador. |
| CRLSet | Lista de bloqueio do Chrome, equivalente em função ao OneCRL. |
| Certificate Transparency (CT) | Registros públicos e auditáveis de todos os certificados emitidos; navegadores recusam certificados que não estejam neles. |
| CCADB | Common CA Database, banco de dados de CAs e certificados mantido para os programas de raízes. |
| Certificação cruzada (cross-sign) | Certificado em que uma CA assina a chave de outra CA, criando um segundo caminho de confiança para ela. |
| HSM | Hardware Security Module, equipamento que gera e guarda chaves de CA sem expô-las. |
| Cerimônia de geração de chaves | Procedimento formal, com roteiro e testemunhas, em que as chaves de uma CA são geradas. |
| ACME | Protocolo de emissão e renovação automática de certificados (usado, por exemplo, pelo Let's Encrypt). |
| SNI | Server Name Indication, extensão do TLS em que o cliente informa o nome do site que quer acessar. |
| Baseline Requirements | Regras do CA/Browser Forum que toda CA pública de TLS precisa seguir. |
Comandos
| Comando | O que faz |
|---|---|
python gerar_lab.py | Gera dois certificados autoassinados de laboratório (nome fictício): lab-fraca.pem com d = 7 e lab-normal.pem com chave aleatória. |
python chave_fraca.py cert.pem [...] | Verifica se a chave EC (P-256/P-384) de cada certificado tem escalar d <= 65536 (ou n - d). Sai com código 1 se achar alguma. |
python -W ignore chave_fraca.py --bundle arquivo.pem | Faz o mesmo para todos os certificados de um bundle PEM; -W ignore esconde avisos de depreciação da biblioteca. |
python teste_chave_fraca.py | Compara a tabela do verificador com derive_private_key da biblioteca cryptography. |
sh baixar_intermediarias.sh DIR | Baixa do crt.sh as oito intermediárias da eMudhra, confere o SHA-256 de cada e junta em DIR/emudhra-8.pem. |
python lab_revogacao.py | Roda o laboratório de revogação com cliente Python padrão e com CRLs. |
python lab_revogacao.py --servir DIR | Grava a hierarquia fictícia em DIR e serve o servidor TLS em 127.0.0.1:8443 até Ctrl+C. |
curl --cacert raiz.pem --resolve servidor.lab.invalid:8443:127.0.0.1 https://servidor.lab.invalid:8443/ | Conecta ao servidor de laboratório confiando só na raiz fictícia; --resolve fixa o nome para o IP local. |
curl ... --crlfile crls.pem | Idem, mas carregando as CRLs; o curl com OpenSSL passa a recusar a cadeia revogada (erro 60). |
node cliente_lab.js DIR [crl] | Cliente Node do laboratório (tls.connect com ca e, opcionalmente, crl). |
java ClienteLab.java DIR [crl] | Cliente Java do laboratório; sem crl, usa o caminho padrão (revogação desligada); com crl, PKIX com revogação e as CRLs. |
openssl s_client -connect host:443 -servername host -showcerts </dev/null | Conecta, mostra a cadeia enviada pelo servidor e o resultado da verificação. |
openssl x509 -in cert.pem -noout -subject -issuer -dates -serial -ext basicConstraints | Mostra titular, emissor, validade, número de série e restrições básicas de um certificado. |
openssl x509 -in cert.pem -noout -text -fingerprint -sha256 | Despeja todo o certificado em texto e o SHA-256 da impressão digital. |
openssl verify -CAfile raizes.pem [-untrusted intermediarias.pem] cert.pem | Valida um certificado contra um bundle, sem rede. |
openssl crl2pkcs7 -nocrl -certfile bundle.pem | openssl pkcs7 -print_certs -noout | Lista subject e issuer de todos os certificados de um bundle PEM. |
grep -i emsign | Filtra a listagem acima pelas raízes e intermediárias da emSign. |
pip install -U certifi | Atualiza o certifi para a versão mais recente. |
python -c "import certifi; print(certifi.__version__, certifi.where())" | Mostra a versão do certifi e o caminho do bundle que o Python usa. |
keytool -list -cacerts -storepass changeit | Lista as raízes do cacerts do JDK (a senha padrão é changeit). |
node -e "require('tls').rootCertificates.forEach(c => console.log(c))" | Imprime em PEM as raízes embutidas no Node. |
Conclusão
Uma chave privada de CA com um byte significa que a "segurança" da chave é de 8 bits, e que qualquer pessoa com um laptop e uma tarde produz um certificado aceito por todo cliente que ainda confia na cadeia. O sistema de certificados funciona porque presume que as chaves das CAs são secretas e aleatórias, e nenhuma etapa da validação de uma cadeia testa isso: o openssl verify do meu certificado de d = 7 devolveu OK. Quem testa são as CAs, os auditores e a comunidade, e dessa vez foi um pesquisador com um script sobre uma base pública de certificados.
Os navegadores se protegeram em menos de três horas porque têm canais próprios de bloqueio e exigem Certificate Transparency. O resto da pilha, não. No laboratório, curl, Python, Node e Java aceitaram uma cadeia revogada com a configuração padrão e só a recusaram depois que carreguei as CRLs. Portanto, a defesa de código que não é navegador é a mais velha de todas: manter o bundle atualizado, reconstruir imagens, planejar a troca dos dispositivos que não atualizam, e saber conferir o que há dentro do bundle com openssl em vez de supor. O verificador deste artigo cobre uma classe estreita de falha (escalar pequeno em P-256/P-384), mas é barato, roda em segundos e é o tipo de checagem que gostaria de ver rodando em CI sobre qualquer bundle ou CA interna.
Para ir além:
- php2shell: como funcionam as duas falhas de RCE do PHP e como se defender: outro caso de falha crítica de outubro de 2026, com a mesma abordagem defensiva (mecanismo, como saber se está exposto e como mitigar).
- Ghidra e IA em binários ARM: quando um modelo ajuda a analisar um binário e quando precisa ser verificado, que é a mesma pergunta que o caso das chaves de CA recuperadas com um prompt simples levanta.
Referências
- Mozilla Security Blog — Protecting Firefox users following the compromise of eMudhra intermediate certificates (20/10/2026): relato de 17/10 20:06 UTC, OneCRL às 22:59 UTC, Firefox 158.0.1, ESR 115.43.1 e 153.5.1, NSS 3.131.1 e Firefox para Android 158.0.2.
- Mozilla — Bug 2083449: eMudhra: Private key compromise of Intermediate Certificates (aberto em 18/10/2026): comentários 0, 2, 5, 6, 10, 11, 13, 18, 19, 20, 21, 30, 31, 33, 35, 37 e 38, lidos via interface REST em 23/10/2026, 00:33 UTC e relidos às 10:25 UTC (histórico e comentários novos). Bugs relacionados: 2084101 (remoção da raiz no NSS; marcado como resolvido em 20/10, 12:48 UTC), 1970728 (CRLs inválidas) e 2018979 (plano de transição das raízes de duplo propósito).
- Mozilla — NSS 3.131.1 release notes (20/10/2026) e mfsa2026-112.
- Chrome e Android Security Teams — Protecting Chrome and Android users from compromised eMudhra intermediate CA certificates (20/10/2026): CRLSet às 19:45 UTC, remoção da raiz prevista para 27/10/2026 (PDT), consulta em CT.
- Scott Helme — Eight Bits of Trust: eMudhra's CA keys cracked in milliseconds (20/10/2026): testes em Linux/macOS/Windows, janela de 17 meses, dispositivos congelados. Atualizado por ele em 20/10, 20:16 UTC (sem mudança depois, na minha consulta de 23/10, 10:26 UTC). Os testes são dele e não foram reproduzidos aqui.
- eMudhra — Statement on Recent Certificate Security Incident (atualizada em 22/10/2026) e comunicado sobre a saída do negócio global de SSL/TLS (22/10/2026); a página de status está na atualização 4, de 22/10.
- Node.js — notas de versão do Node.js 24.22.1 (21/10/2026; as de 22.23.4 e 26.12.1 trazem a mesma linha): "crypto: update root certificates to NSS 3.131.1".
- Debian — Bug 1151413: eMudhra Intermediate CA key compromise e changelog do
ca-certificates(20260816+nmu1). Ubuntu — USN-8944-1 (20/10/2026). - Cooper, D. et al. — RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile (IETF, 2008), seção 6.1 (Basic Path Validation) e seção 6.3 (CRL Validation).
- CA/Browser Forum — Baseline Requirements for the Issuance and Management of Publicly-Trusted TLS Server Certificates, versão 2.3.1 (04/10/2026): seções 4.9.1.2, 6.1.1.1 e 6.1.1.3; ballot SC-073 (v2.0.5, "Compromised and weak keys").
- NIST — FIPS 186-5: Digital Signature Standard: parâmetros das curvas P-256 e P-384 (
pen), usados emchave_fraca.py. - Python Cryptographic Authority — cryptography: Elliptic curve cryptography:
derive_private_keyegenerate_private_key, e x509 reference. - Python Software Foundation — ssl: TLS/SSL wrapper for socket objects:
VERIFY_DEFAULT,VERIFY_CRL_CHECK_LEAFeVERIFY_CRL_CHECK_CHAIN. - Oracle — Java Secure Socket Extension (JSSE) Reference Guide (Java SE 21): comportamento de
TrustManagerFactory.init(KeyStore)e propriedadecom.sun.net.ssl.checkRevocation; e Java PKI Programmer's Guide, seção "The PKIXParameters Class". - Node.js — TLS (SSL):
tls.rootCertificatese a opçãocrldetls.connect. - curl — curl manpage:
--cacert,--crlfile,--cert-status,--resolve. - OpenSSL — manuais
openssl-s_clienteopenssl-verify(3.5). - certifi — issue #439 e pacote 2026.10.19 no PyPI (wheel baixado e
cacert.pemlido). - Código-fonte deste artigo —
chave_fraca.py,teste_chave_fraca.py,gerar_lab.py,baixar_intermediarias.sh,lab_revogacao.py,cliente_lab.jseClienteLab.javaem/exemplos/emudhra-chaves-de-ca-um-byte/.
Comentários
Os comentários usam o Giscus, que guarda as discussões no GitHub. Ao carregar, seu navegador se conecta ao giscus.app e ao GitHub, que recebem o seu endereço IP. Saiba mais na política de privacidade.