Intermediate

Quem você confia quando confia em um certificado? O incidente da eMudhra explicado

On this page

Em outubro de 2026, um pesquisador descobriu que oito certificados intermediários emitidos pela eMudhra (uma autoridade certificadora, uma das ~140 organizações cuja assinatura seu navegador e sistema operacional aceitam como prova de que um site é quem afirma ser) tinham chaves privadas que qualquer pessoa poderia recuperar do certificado público. Uma delas era o número 4. Sim, um dígito. Quatro. Não um segredo de 384 bits, não um gerador aleatório fraco, apenas o número que você conta depois do três. Os navegadores bloquearam os certificados em algumas horas. Se você executa qualquer coisa que não seja um navegador, como um serviço Python, um binário Go, um worker Node ou o curl no seu pipeline de CI (integração contínua), você provavelmente continuou confiando neles por dias, e pode ainda estar confiando neles agora. E enquanto você envia seu agente de codificação para resolver isso, vale a pena entender qual problema ele está realmente resolvendo, porque este se esconde em mais lugares do que um arquivo de lock.

Esta é uma visão geral do que uma conexão TLS (Transport Layer Security) confia, o que deu errado na eMudhra, por que revogar os certificados não ajudou a maioria de nós e o que você deve verificar em seus próprios containers. Você não precisa de nenhum conhecimento em criptografia para acompanhar; se você sabe o que o cadeado no seu navegador significa, você tem o necessário. Os exemplos são em Python, porque foi onde encontrei o problema, mas o problema em si é sobre quais arquivos seu runtime confia, então se você escreve em Go, Node, Java ou Ruby, deve ser capaz de mapear cada etapa para sua própria stack. Algumas partes desta história já estão bem explicadas em outros lugares, então as manterei curtas e deixarei dicas de leitura ao longo do caminho para quem quiser ir além do que um post de blog permite.

Afinal, no que o TLS confia?

O TLS oferece duas coisas a uma conexão. Ele criptografa, para que ninguém no caminho possa ler o que você envia, e ele autentica, para que você saiba que a parte para a qual você está criptografando é o servidor que você pretendia alcançar. Geralmente pensamos na primeira, mas é na segunda que esta história reside. Criptografar para a parte errada é pior do que não criptografar, porque parece seguro.

A autenticação depende do certificado que o servidor envia durante o handshake. Seu cliente verifica esse certificado antes de concordar com as chaves da sessão. Se a verificação for bem-sucedida, o cadeado fecha. Se ela for bem-sucedida quando não deveria, o cadeado fecha do mesmo jeito.

O Passo 3 é a história toda. Tudo depois dele assume que o certificado era genuíno.

O handshake em si é bem documentado, então não vou replicá-lo aqui.

Leitura adicional

What happens in a TLS handshake? é o guia didático da Cloudflare sobre os cinco passos acima.

Transport Layer Security no MDN (Mozilla Developer Network) é a referência, com o histórico de versões.

RFC 8446 é o próprio TLS 1.3, se você preferir a fonte da verdade. RFC significa Request for Comments, a série onde os padrões da internet são publicados.

Como um certificado raiz se torna algo em que sua conexão confia?

Um certificado é um pequeno documento assinado: um nome, uma chave pública, uma janela de validade e uma assinatura de quem o emitiu. O próprio certificado do servidor, a folha, é assinado por uma autoridade certificadora (CA) intermediária. A intermediária é assinada por uma raiz. A raiz assina a si mesma, o que parece trapaça, e é, de propósito.

Seu cliente percorre essa cadeia para cima. Em cada etapa, ele pega a chave pública do emissor e verifica a assinatura no certificado abaixo. O percurso termina quando ele atinge um certificado que já está em seu armazenamento de confiança, um arquivo com aproximadamente 140 raízes que veio com seu navegador, seu sistema operacional ou o runtime da sua linguagem. Estar nesse arquivo é o que "confiado" significa. Não há mais nenhuma autoridade a quem perguntar.

A folha é confiada porque a intermediária a garante, e a intermediária é confiada porque sua raiz está no armazenamento.

Note uma coisa na imagem: nada na cadeia diz por quais nomes de host a intermediária tem permissão para garantir. Um certificado de CA não é limitado a um domínio. Qualquer intermediária sob qualquer raiz confiável pode assinar uma folha para qualquer nome de host na internet, e a cadeia será verificada. Se você quiser ver uma cadeia real, a Let’s Encrypt publica as dela, com cross-signs e tudo mais.

Leitura adicional

How Let’s Encrypt works explica como uma CA decide assinar sua folha inicialmente, incluindo a validação de domínio. É o melhor tutorial curto sobre o lado da emissão que eu conheço.

RFC 5280, seção 6 é o algoritmo de validação que seu cliente executa, passo a passo.

A Política de Root Store da Mozilla é aquilo com que uma CA deve estar em conformidade para ser uma dessas ~140 raízes. Surpreendentemente legível.

Onde reside a confiança?

"The trust store" parece ser uma coisa só. Em uma máquina real, são vários arquivos que por acaso concordam na maioria das vezes. Seu navegador tem o seu próprio. O sistema operacional tem um; no Debian, é o pacote ca-certificates. O módulo ssl do Python lê o arquivo do sistema operacional por padrão, mas httpx e requests leem, em vez disso, um pacote Python chamado certifi. pip carrega uma cópia privada de certifi. uv, escrito em Rust, agrupa sua própria lista. grpc compila um roots.pem em seu wheel. Java tem cacerts, Node tem suas próprias raízes agrupadas, e assim por diante.

Um container Python que eu analisei. Cinco cópias da mesma raiz, cada uma com seu próprio ciclo de lançamento.

Todas elas descendem do programa de raízes da Mozilla, então, em um dia tranquilo, elas contêm as mesmas raízes. Em um dia agitado, cada uma é uma correção separada com um proprietário separado. Corrigir certifi corrige a primeira linha daquela imagem e nada mais. Mantenha esta imagem em mente – voltaremos a ela.

A chave era 4

As intermediárias nesta história usavam chaves de curva elíptica (EC) na curva P-384. Uma chave privada EC é um número, vamos chamá-lo de d, escolhido aleatoriamente entre cerca de 2^384 valores possíveis. A chave pública é Q = d · G, onde G é um ponto fixo na curva. Ir de d para Q é barato. Voltar de Q para d

é inviável, desde que d seja aleatório. É toda a matemática de curvas que este post precisa. Essa garantia tem uma precondição que a matemática não pode verificar por você: d tem que ser realmente aleatório. Em 30 de abril de 2025, a eMudhra realizou uma cerimônia de geração de chaves para seus novos intermediários emSign. De acordo com a própria conta da empresa no rastreador de bugs da Mozilla, uma falha no código da cerimônia produziu um segundo conjunto de chaves que eram mais fracas e, como os rótulos das chaves eram parecidos, o conjunto fraco acabou sendo usado para emitir os intermediários e para solicitar cross-signs da Sectigo.

Mais fracas é uma palavra gentil. Os 376 bits superiores de cada chave privada eram zero. Todo o segredo residia no último byte, então d era inferior a 256 para todas as oito chaves. Para um dos intermediários TLS, era exatamente 4. Como a chave pública está impressa no certificado, qualquer pessoa pode multiplicar G por 1, 2, 3, 4 e comparar.

Nada foi roubado e nenhum módulo de segurança de hardware foi invadido. O segredo era apenas pequeno o suficiente para ser adivinhado.

A recuperação cabe em algumas linhas. Este é o formato da verificação que a Mozilla executou para recuperar as próprias chaves, em duas horas após o relatório, escrita contra a chave pública encontrada no certificado:

from cryptography.hazmat.primitives.asymmetric import ec

target = intermediate_cert.public_key().public_numbers()
for d in range(1, 256):
    candidate = ec.derive_private_key(d, ec.SECP384R1()).public_key()
    if candidate.public_numbers() == target:
        print("private key is", d)  # prints 4 for EM OV TLS CA - G3C
        break

Com d em mãos, um invasor pode assinar qualquer coisa que o intermediário pudesse assinar, o que significa um certificado folha para qualquer hostname na terra, encadeando perfeitamente a uma raiz no seu store.

Leitura adicional

A relatively easy to understand primer on elliptic curve cryptography é o que eu recomendaria para a matemática que pulei.

A CA private key was the number 4 detalha a recuperação com um script funcional e até uma demo em mitmproxy, se você quiser vê-la de ponta a ponta.

emudhra.we-have-a-problem.com é o site de divulgação do pesquisador, com provas assinadas para todas as oito chaves e endpoints de teste que você pode curl.

O que é eMudhra?

eMudhra Limited é uma autoridade certificadora listada em bolsa que é licenciada por seu regulador nacional desde 2008 para emitir certificados de assinatura digital. Para a web pública, ela opera a marca emSign, com raízes aceitas pelos programas de raízes da Mozilla, Microsoft e Apple. Suas raízes entraram no certifi com a versão 2019.3.9, e é por isso que a faixa afetada do aviso começa ali.

Se você gosta de post-mortems, aqui está a versão curta deste, tudo vindo de do bug da Mozilla:

Oito dias antes da divulgação, a Mozilla havia encerrado a discussão pública sobre a aprovação de quatro novas raízes da eMudhra. O timing é tudo.

Revogado no sábado, confiado pelo seu script na terça-feira

A revogação é a resposta da infraestrutura de chaves públicas (PKI) a uma chave comprometida. A CA publica "este certificado não é mais válido", através de um CRL ou OCSP (Online Certificate Status Protocol), e espera-se que os clientes verifiquem. Os navegadores verificam. O Firefox consome OneCRL e CRLite, o Chrome consome CRLSets, e ambos são enviados aos usuários em questão de horas. Os navegadores também impõem a Certificate Transparency: um certificado folha deve carregar a prova de que foi registrado publicamente, e esses intermediários nunca registraram um único certificado. Qualquer uma dessas verificações sozinha teria rejeitado uma folha forjada.

Quase nada fora de um navegador faz qualquer uma das duas. Os padrões do ssl do Python, crypto/x509 do Go, Node, Java, curl e do OpenSSL constroem a cadeia, procuram a raiz em seu arquivo e param. Sem CRL (baixar a lista de certificados revogados da CA), sem OCSP (perguntar à CA se este certificado específico ainda é válido), sem verificação de transparência (procurar a prova de que o certificado foi registrado publicamente). Isso não é preguiça, a propósito: verificações de revogação online são lentas, vazam quais sites você visita e falham abertas quando o respondedor está fora do ar, que é por isso que a Let’s Encrypt parou de servir OCSP completamente em 2025. Para eles, a revogação da eMudhra em 18 de outubro não mudou nada. Uma folha forjada assinada com d = 4 validou na segunda-feira exatamente como validou na sexta-feira, e continuaria validando até que o próprio arquivo fosse alterado.

Mesmo certificado forjado, mesma raiz. O navegador tem dois motivos independentes para recusá-lo; o runtime não tem nenhum.

Foi explorado? Nada forjado a partir dessas chaves apareceu nos logs de Certificate Transparency ou foi reportado em uso. Essa é uma garantia mais fraca do que parece, já que um atacante visando clientes que não sejam navegadores não tem motivo para registrar nada. O atacante também ainda precisa estar no caminho, com uma resposta de DNS (Domain Name System) sequestrada, um Wi-Fi hostil ou um proxy comprometido. Então seu cadeado fecha para eles.

Leitura adicional

CRLite: all of the Web PKI’s revocations, compressed explica como o Firefox envia cada revogação para cada usuário em alguns centenas de kilobytes.

CRLSets é o equivalente do Chromium, e a página é honesta sobre o que não cobre.

certificate.transparency.dev é o ponto de partida para entender como os logs de Certificate Transparency funcionam e por que os navegadores os exigem.

Duas maneiras de corrigir

Se a confiança do seu cliente for um arquivo PEM plano (Privacy-Enhanced Mail, o formato de texto simples que bundles de certificados ainda usam), não há como dizer "pare de confiar nestes oito intermediários". O arquivo lista raízes e nada mais. A única alavanca que você tem é remover uma raiz, e tudo abaixo dela vai junto. É isso que certifi, Debian e Red Hat com seus pacotes ca-certificates, e uv fez: emSign ECC Root CA – G3 e a root da Comodo AAA sumiram do bundle, então uma cadeia forjada não tem âncora para pousar. A primeira tentativa do certifi, 18.10.2026, removeu toda root da emSign; 19.10.2026 recolocou as não afetadas, e é por isso que essa é a versão para fixar. O próprio Python lançou 3.13.17, 3.14.9 e 3.15.1 apenas para atualizar a cópia do certifi dentro do pip incluso.

Um verificador que é código em vez de um arquivo pode ser cirúrgico. O Go adicionou uma denylist das oito chaves públicas dentro do crypto/x509; a root continua confiável, mas qualquer cadeia que passe por um desses intermediários falha. O Node fez o mesmo e atualizou suas roots. A diferença é o dano colateral. Remover a root invalida aproximadamente 30.000 certificados legítimos que agora precisam ser reemitidos sob outra CA. Uma denylist invalida exatamente as oito chaves que foram quebradas.

Um arquivo PEM só pode perder uma root. Um verificador com código pode perder uma chave. Ambos terminam em "rejeitado"; eles diferem em quem mais sai prejudicado.

Leitura adicional

GHSA-6q48-6c7w-9gw2 é o aviso do certifi: intervalo afetado, as duas versões, e por que você quer a segunda.

DSA-6572-1 é o Debian Security Advisory (DSA) para ca-certificates, com as versões exatas dos pacotes por release.

GO-2026-6809 descreve a denylist do Go e as versões corrigidas.

Como desenvolvedor, o que isso significa para mim?

Volte para a imagem do container. Tudo o que eu tive que fazer nos serviços em que trabalho vem dela, e suspeito que a sua lista seja semelhante.

Conte seus trust stores. Faça um grep em suas imagens por cacert.pem, roots.pem e /etc/ssl/certs, e lembre-se dos binários que não usam nenhum deles: uv, programas Go e Node enviam suas próprias listas dentro do executável. Um container Python que verifiquei tinha cinco stores. Uma atualização do lock file resolve um deles.

Atualize o certifi para 2026.10.19 e reconstrua. Se o Dependabot abriu o PR para você, ótimo, mas não conte com isso: o aviso do certifi não possui um identificador CVE (Common Vulnerabilities and Exposures), e em um repositório que acompanho o alerta só apareceu dias após a correção ter sido lançada. Uma atualização do lock file também não faz nada para um container que já está em execução. Você precisa que a imagem seja reconstruída e o deployment reiniciado.

Verifique como a sua imagem base obtém o store do sistema operacional. Se você fixou o python:3.13.13-slim-bookworm, você congelou o Debian também: imagens oficiais apenas reconstroem as tags que ainda são atuais, então a correção do Debian nunca chegará a essa. Você tem que mudar para 3.13.17. Tags flutuantes também não são seguras, apenas de uma forma diferente. As imagens do ghcr.io/astral-sh/uv:python3.13-bookworm-slim pararam de ser construídas em fevereiro de 2026 quando a Astral mudou para o Debian Trixie, e em outubro eles ainda estavam entregando o uv 0.9 e um trust store com a raiz comprometida. Uma tag que ninguém mais constrói parece exatamente com uma tag que está atualizada.

Não esqueça os stores de build-time. uvfaz o download de seus wheels com sua própria lista de raízes; 0.13.4 é a versão que removeu a raiz. Se o seu arquivo de lock contém hashes, um índice forjado poderia, no máximo, quebrar sua build, e não trocar um pacote, mas você ainda deve fazer a atualização.

E aqueles sem versão lançada ainda. grpc agrupa um roots.pem em seu wheel e, no momento da escrita, havia atualizado no master sem publicar uma versão. Se o seu serviço utiliza gRPC para o mundo externo, GRPC_DEFAULT_SSL_ROOTS_FILE_PATH permite que você o aponte para a loja do sistema operacional enquanto isso.

Finalmente, teste. O site de divulgação do pesquisador hospeda endpoints cujos certificados encadeiam através das intermediárias comprometidas. Um curl de dentro do seu container que falhe com um erro de certificado é o que você deseja ver.

Leitura adicional

O guia de Docker do uv lista quais tags de imagem ainda são construídas e como copiar apenas o binário uv para uma imagem base que você controla.

docker-library/official-images documenta como as tags de imagens oficiais são definidas e quando são reconstruídas.

Variáveis de ambiente do gRPC cobre GRPC_DEFAULT_SSL_ROOTS_FILE_PATH e os outros controles que alteram onde o gRPC procura por raízes.

Conclusion

A matemática da curva estava correta. A cerimônia de chaves não estava, e nenhuma verificação posterior poderia dizer, porque a aleatoriedade é uma pré-condição de todo o sistema, não uma propriedade que você pode verificar depois. A revogação acabou protegendo os navegadores e quase mais ninguém, porque a confiança para o resto de nós é um arquivo, e arquivos só mudam quando alguém reconstrói a imagem. A correção é entediante, o que é bom: conte seus stores, atualize o que tem um release, reconstrua o que fixa uma tag, faça o redeploy e vá verificar o que mais em seus containers parou silenciosamente de receber atualizações.

We want to work with you. Check out our Services page!

We want to work with you

The engineers who write here are the same ones who join client teams. Let’s build something that grows with your business.

Book a call