Resposta curta. Os navegadores abandonaram a fixação via cabeçalho (HPKP): o Chrome a removeu na versão 72 (janeiro de 2019) e o Firefox, na versão 72 (janeiro de 2020); hoje, o Certificate Transparency cobre a maior parte do que o HPKP deveria detectar. A fixação continua viva em aplicativos móveis e clientes de API que se comunicam com servidores que você controla. Se for fixar, fixe o hash SHA-256 de uma chave pública (SPKI), e não o de um certificado; fixe chaves de CAs que você vai continuar usando (uma intermediária ou uma raiz) em vez da sua chave folha, que a maioria dos clientes ACME troca a cada renovação; sempre inclua no aplicativo pelo menos um pin de backup de outra CA ou de uma chave offline; e use o mecanismo da plataforma: a configuração de segurança de rede (Network Security Configuration) do Android, com uma expiração, ou o NSPinnedDomains do iOS. Tanto o Google quanto a Apple desaconselham a fixação, a menos que você tenha um motivo concreto.
O que é um pin
A validação comum de certificados aceita qualquer cadeia que leve a qualquer raiz do trust store. Um pin acrescenta mais uma condição: pelo menos uma chave da cadeia validada precisa estar na sua lista. A lista contém hashes do SubjectPublicKeyInfo (SPKI): o identificador do algoritmo e a chave pública, codificados em DER. Calcular o hash da chave, e não do certificado, faz com que um certificado renovado com a mesma chave continue correspondendo ao pin, e com que um pin na chave de uma CA corresponda a todos os certificados que essa CA emite.
Os pins são calculados a partir dos certificados com o OpenSSL. Para a cadeia que o letsencrypt.org servia em 20 de outubro de 2026 (eclipse-temurin:25.0.4.1_1-jdk-noble, OpenSSL 3.0.13):
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -showcerts </dev/null 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/{n++; f=sprintf("c%d.pem", n); p=1} p{print >> f} /-----END CERTIFICATE-----/{p=0; close(f)}'
pin() { openssl x509 -in "$1" -noout -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64; }
for f in c*.pem; do printf '%s %s\n' "$(pin "$f")" "$(openssl x509 -in "$f" -noout -subject | sed 's/^subject=//')"; done
x8ltqh+zNPYG7RZS+IjFymndoy6YAWLPaSkptqebzfc= CN = letsencrypt.org
s/tdAOmUzd8syaTuqfgGvFcn6DzA5Cmb+Vby1ST+U3Y= C = US, O = Let's Encrypt, CN = YE2
sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w= C = US, O = ISRG, CN = Root YE
diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI= C = US, O = Internet Security Research Group, CN = ISRG Root X2
Os quatro candidatos se comportam de forma muito diferente ao longo do tempo, e escolher entre eles é a maior parte do trabalho. O gerador de pins SPKI calcula os mesmos valores no navegador e gera trechos de configuração.
Por que os navegadores desistiram: HPKP
O HTTP Public Key Pinning (RFC 7469, 2015) permitia que um site enviasse um cabeçalho Public-Key-Pins com hashes de chaves e um max-age; a partir daí, durante todo esse período, o navegador recusava qualquer cadeia que não tivesse uma dessas chaves. Dois modos de falha acabaram com ele:
- Quedas autoinfligidas. Uma chave perdida, uma troca de CA ou um erro de digitação deixava sem acesso todo visitante que voltasse ao site até o
max-ageacabar, sem nenhuma forma de desfazer isso pelo servidor. - Fixação hostil. Um atacante que controlasse um site por pouco tempo (ou a emissão de certificados dele) podia enviar pins de chaves que só ele tinha, transformando um comprometimento breve em uma longa negação de serviço.
O registro de remoção do Chrome cita a “adoção muito baixa” e os “riscos de negação de serviço e de fixação hostil” (chromestatus); o suporte foi removido no Chrome 72 para desktop e Android, e o Firefox fez o mesmo na versão 72. Os navegadores ainda trazem uma pequena lista de pins embutida para os domínios dos próprios fabricantes, mas nenhum site consegue se incluir nela.
O que o substituiu foi a detecção, e não a prevenção. Desde 2018, o Chrome e as plataformas da Apple só aceitam certificados TLS publicamente confiáveis acompanhados de Signed Certificate Timestamps de logs de Certificate Transparency, de modo que todo certificado para o seu domínio é público e pode ser monitorado; os registros CAA dizem às CAs quais delas podem emitir certificados para o seu domínio. Juntos, eles tornam visível em poucas horas a emissão indevida que era o alvo do HPKP, sem o risco de bloqueio.
O que fixar
| Pin | Sobrevive a | Quebra quando |
|---|---|---|
| Chave folha | Renovações que mantêm a chave | Cada renovação com chave nova, que é o padrão no certbot |
| Intermediária emissora (YE2) | Renovações do certificado folha e trocas de chave | A CA faz a rotação das intermediárias; a Let’s Encrypt mantém várias em rotação e outras de reserva, a partir das quais “pode começar a emitir … a qualquer momento, sem aviso” |
| Raiz (ISRG Root X2) | Rotação de intermediárias | Você troca de CA, ou a CA deixa de ser confiável |
| Chave que você guarda offline | Qualquer coisa, enquanto não for usada | Só se você a perder |
A linha da chave folha merece destaque, porque é nela que o comportamento padrão vira armadilha. O certbot --help all (v5.8.0) descreve --reuse-key como desativado por padrão e diz que “não reutilizar chaves privadas é o comportamento padrão do Certbot”. Portanto, um pin na chave folha quebra na próxima renovação, e as renovações ficam cada vez mais próximas: o CA/Browser Forum limita os certificados TLS a 200 dias desde março de 2026, a 100 dias a partir de março de 2027 e a 47 dias a partir de março de 2029, e o padrão da Let’s Encrypt passa a 64 dias em fevereiro de 2027.
A linha da intermediária é a mais sutil. O certificado do letsencrypt.org foi emitido pela YE2, mas a Let’s Encrypt lista YE1 e YE2 (ECDSA) e YR1 e YR2 (RSA) em rotação ativa, com YE3 e YR3 de reserva (cadeias de confiança). Um conjunto de pins só com a YE2 falha no próximo certificado emitido pela YE1.
Um pin na raiz sobrevive a tudo isso e só falha quando você troca de CA. Isso acontece com mais frequência do que se imagina: quando o Chrome deixou de confiar em certificados emitidos pela Entrust cujo SCT mais antigo fosse posterior a 11 de novembro de 2024 (Google Security Blog), todos os clientes da Entrust tiveram de migrar para outra CA em questão de meses. Um aplicativo com pins nas raízes da Entrust e sem backup teria precisado de uma atualização antes disso.
O meio-termo habitual: fixe a raiz (ou as duas raízes) da sua CA atual, mais a raiz de uma segunda CA para a qual você poderia migrar e, opcionalmente, uma chave que você gerou e mantém offline para um certificado de emergência. Com Let’s Encrypt como CA atual e Google Trust Services como alternativa, a partir do pacote ca-certificates 20260816 do Debian:
C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M= subject=C=US, O=Internet Security Research Group, CN=ISRG Root X1 notAfter=Jun 4 11:04:38 2035 GMT
diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI= subject=C=US, O=Internet Security Research Group, CN=ISRG Root X2 notAfter=Sep 17 16:00:00 2040 GMT
mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c= subject=C=US, O=Google Trust Services LLC, CN=GTS Root R4 notAfter=Jun 22 00:00:00 2036 GMT
hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc= subject=C=US, O=Google Trust Services LLC, CN=GTS Root R1 notAfter=Jun 22 00:00:00 2036 GMT
O pin da ISRG Root X2 corresponde ao último certificado que o letsencrypt.org envia (a X2 com assinatura cruzada da X1) porque é a mesma chave da X2 autoassinada que está nos trust stores. Fixar a raiz de outra CA que você ainda não usa não custa nada e é a sua rota de saída.
Android: configuração de segurança de rede
O Android 7.0 e as versões posteriores leem os pins de res/xml/network_security_config.xml, referenciado por android:networkSecurityConfig no manifesto (documentação):
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-10-01">
<!-- ISRG Root X2 and X1: the current CA, ECDSA and RSA chains -->
<pin digest="SHA-256">diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</pin>
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=</pin>
<!-- GTS Root R4 and R1: the CA we would switch to -->
<pin digest="SHA-256">mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=</pin>
<pin digest="SHA-256">hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=</pin>
</pin-set>
</domain-config>
</network-security-config>
As regras que importam, segundo a documentação:
- Uma cadeia é aceita quando “contém pelo menos uma das chaves públicas fixadas”; o único digest aceito é
SHA-256. - Depois da data de
expiration(yyyy-MM-dd), a fixação deixa de ser aplicada. Isso mantém instalações antigas funcionando depois que você já se esqueceu delas; a documentação também avisa que isso “pode permitir que atacantes contornem seus certificados fixados” em dispositivos com aplicativos desatualizados. Escolha uma data cerca de um ciclo de lançamento depois da data em que você vai atualizar os pins da próxima vez. - Âncoras de confiança em
<debug-overrides>dispensam os pins quando o aplicativo é depurável, que é como você inspeciona o tráfego durante o desenvolvimento;overridePins="true"em um elemento<certificates>faz o mesmo nos builds de release e não deve ir para produção. - O Android 16 adicionou verificações de Certificate Transparency ao mesmo arquivo (
<certificateTransparency enabled="true"/>), e a partir do Android 17 elas vêm ativadas por padrão nos aplicativos que têm essa versão como alvo, exceto em conexões que usam âncoras de confiança personalizadas. Para muitos aplicativos, essa é justamente a proteção que eles queriam obter com a fixação.
A orientação do próprio Google é categórica: a fixação “não é recomendada para aplicativos Android” e, se você a usar, inclua “vários pins de backup, incluindo pelo menos uma chave totalmente sob seu controle, e um prazo de expiração suficientemente curto” (segurança com protocolos de rede).
OkHttp
O CertificatePinner do OkHttp funciona no Android e na JVM, onde a configuração de segurança de rede não existe. O teste fixa o letsencrypt.org na ISRG Root X2, com a X1 como backup, e depois em uma chave que nenhuma CA certificou, que é o que um aplicativo vê depois que o servidor migra para uma CA não fixada:
// OkHttp CertificatePinner against letsencrypt.org: a correct CA-key pin, a backup pin, and a wrong pin.
import java.io.IOException;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class Pin {
static void get(String label, CertificatePinner pinner) {
OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build();
Request request = new Request.Builder().url("https://letsencrypt.org/robots.txt").build();
try (Response response = client.newCall(request).execute()) {
System.out.println(label + ": HTTP " + response.code());
} catch (IOException e) {
System.out.println(label + ": " + e.getClass().getName() + ": " + e.getMessage());
}
}
public static void main(String[] args) {
// ISRG Root X2 key, plus a backup pin for ISRG Root X1 (RSA): either satisfies the pin set.
get("pinned to ISRG Root X2 (+ X1 backup)", new CertificatePinner.Builder()
.add("letsencrypt.org", "sha256/" + args[0])
.add("letsencrypt.org", "sha256/" + args[1])
.build());
// A key that is not in the chain: what a client sees after the site moves to another CA.
get("pinned to an unrelated key", new CertificatePinner.Builder()
.add("letsencrypt.org", "sha256/" + args[2])
.build());
}
}
pinned to ISRG Root X2 (+ X1 backup): HTTP 200
pinned to an unrelated key: javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/x8ltqh+zNPYG7RZS+IjFymndoy6YAWLPaSkptqebzfc=: CN=letsencrypt.org
sha256/s/tdAOmUzd8syaTuqfgGvFcn6DzA5Cmb+Vby1ST+U3Y=: CN=YE2, O=Let's Encrypt, C=US
sha256/sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=: CN=Root YE, O=ISRG, C=US
sha256/diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=: CN=ISRG Root X2, O=Internet Security Research Group, C=US
Pinned certificates for letsencrypt.org:
sha256/VIJvW/e8yv/LvjcOADWb4C9aNclxEiQeW0tr4SvazsA=
A mensagem de falha lista os pins da cadeia que o OkHttp validou, terminando na âncora de confiança, e o próprio comentário de documentação do OkHttp sugere exatamente isso como forma de configurar: comece com um pin quebrado em uma rede confiável e copie os valores esperados da exceção. Os pins coincidem com a saída do OpenSSL acima. O mesmo comentário traz um aviso que vale repetir para quem aprova a mudança: “Não use a fixação de certificados sem a bênção do administrador de TLS do seu servidor!” Além disso, o CertificatePinner só é executado depois da validação normal, então não consegue tornar aceitável um certificado autoassinado.
Uma armadilha de depuração em que caímos: o contêiner não tinha rota IPv6 e, sem -Djava.net.preferIPv4Stack=true, a segunda chamada retornava java.net.SocketException: Network is unreachable. O OkHttp 5 tenta os endereços em paralelo (“fast fallback”), e a falha de fixação no endereço IPv4 acabava como uma exceção suprimida por trás do erro de conexão IPv6; com fastFallback(false), a falha de fixação passava a ser a exceção principal. Quando a fixação quebrar em produção, registre no log também o resultado de getSuppressed().
iOS e macOS: NSPinnedDomains
Desde o iOS 14 e o macOS 11, o App Transport Security fixa as chaves declaradas no Info.plist. O artigo da Apple “Identity Pinning” descreve duas listas por domínio: NSPinnedCAIdentities (a chave precisa aparecer em uma intermediária ou na raiz da cadeia) e NSPinnedLeafIdentities (a chave do próprio servidor), e cada entrada é um valor SPKI-SHA256-BASE64 calculado com o mesmo pipeline do OpenSSL:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>letsencrypt.org</key>
<dict>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</string>
</dict>
</array>
</dict>
</dict>
</dict>
Verificamos o comportamento nesta máquina (macOS 27.0.1, Apple M4 Max, Swift 6.4): uma ferramenta de linha de comando que busca https://letsencrypt.org/robots.txt com URLSession, compilada três vezes com swiftc main.swift -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist: sem pins, com o pin da X2 acima e com a chave não relacionada do teste do OkHttp:
HTTP 200
HTTP 200
error: NSURLErrorDomain -1200: A TLS error caused the secure connection to fail.
O aplicativo recebe o genérico NSURLErrorSecureConnectionFailed (-1200), e não um código específico de fixação, então registre com qual conjunto de pins cada versão do aplicativo foi publicada; sem isso, uma divergência de pins depois de uma mudança no servidor se parece com qualquer outro erro de TLS nos relatórios de falhas. NSIncludesSubdomains estende um pin a apenas um nível de subdomínios (math.example.org, e não advanced.math.example.org). A recomendação da Apple coincide com a do Google: “Na maioria dos casos, a fixação não é necessária e deve ser evitada” e, se você fixar, fixe chaves de CA, e mais de uma.
Rotação de pins sem bloquear ninguém
Um conjunto de pins é uma promessa sobre o futuro, e o aplicativo que o contém pode passar um ano sem ser atualizado. Uma rotação que funciona:
- Publique o novo pin antes de precisar dele. Adicione a raiz da próxima CA ou a próxima chave ao conjunto de pins em uma versão e espere até que as versões sem ele correspondam a uma parcela de usuários que você possa se dar ao luxo de deixar sem acesso.
- Só então mude o servidor. Troque a CA ou a chave; os pins antigos continuam correspondendo nas versões antigas do aplicativo, desde que contenham alguma chave da nova cadeia.
- Remova os pins aposentados em uma versão posterior.
- Mantenha um caminho de emergência. Uma chave offline que está fixada, mas nunca foi implantada, permite obter para ela, em uma emergência, um certificado de qualquer CA. Alguns aplicativos também buscam atualizações de pins em uma configuração remota assinada; nesse caso, esse arquivo precisa da própria chave de assinatura, guardada fora da infraestrutura de TLS.
- Defina expirações no Android e trate-as como prazo final para a próxima rotação, e não como formalidade.
- Teste a falha. Rode o aplicativo através de um proxy com um certificado do seu trust store, mas fora do conjunto de pins, e confirme que ele recusa a conexão (fail closed) e exibe uma mensagem útil.
Proxies corporativos de inspeção de TLS quebram aplicativos com fixação pela própria forma como funcionam: os certificados deles se encadeiam a uma raiz corporativa que não está fixada. No Android, CAs instaladas pelo usuário não são confiáveis para aplicativos que têm como alvo o nível de API 24 ou superior, a menos que a configuração as adicione, e os pins continuam valendo para elas, a menos que overridePins esteja definido.
Erros comuns
- Fixar só a chave folha com um cliente ACME que troca as chaves. A próxima renovação causa uma queda.
- Fixar uma única intermediária de uma CA que alterna entre várias. Funciona até a CA escolher outra.
- Nenhum pin de backup, ou um backup da mesma CA. Quando essa CA deixa de ser confiável, os dois pins falham juntos.
- Fixar o hash do certificado em vez do SPKI. Toda renovação o quebra, mesmo com a mesma chave.
- Gerenciadores de confiança (trust managers) escritos à mão. Substituir a validação da plataforma para “adicionar fixação” muitas vezes remove, sem querer, as verificações de nome de host ou da cadeia. Use a configuração de segurança de rede, o
CertificatePinnerou oNSPinnedDomains, que fixam por cima da validação normal. - Esperar que os pins protejam contra um dispositivo comprometido. Em um dispositivo com root ou jailbreak, o usuário (ou um malware) pode remover a fixação com um patch. A fixação protege os usuários contra a rede, e não a sua API contra os seus usuários.
Fontes
- RFC 7469: Public Key Pinning Extension for HTTP e o registro de remoção do Chrome
- Android: Configuração de segurança de rede (pins, expiração, substituições, Certificate Transparency) e Segurança com protocolos de rede
- Apple: Identity Pinning: como configurar os certificados de servidor do seu aplicativo e política de Certificate Transparency
- Código-fonte e comentário de documentação do
CertificatePinnerdo OkHttp - Let’s Encrypt: Cadeias de confiança e Redução da vida útil dos certificados para 45 dias
- Baseline Requirements do CA/Browser Forum 2.3.1, §6.3.2 (períodos de validade)
- Google Security Blog: fim da confiança nos certificados da Entrust (junho de 2024)