Certificate Pinning em Python Puro: Como Blindar Suas Conexões HTTPS Contra Ataques Man-in-the-Middle Sem Confiar em Autoridades Certificadoras
Seu HTTPS Está Mentindo Para Você
Acordei às 3h da manhã com o PagerDuty berrando. Nosso sistema de pagamentos estava recebendo respostas estranhas da API do banco — assinaturas digitais inválidas, timestamps impossíveis, payloads que não batiam com o que tínhamos enviado. O certificado TLS estava válido. O domínio estava correto. O cadeado verde brilhava no navegador como se nada estivesse errado.
Levei duas horas para descobrir: alguém tinha conseguido um certificado válido para o domínio do banco usando uma autoridade certificadora comprometida. Tecnicamente, era HTTPS legítimo. Na prática, era um ataque man-in-the-middle perfeito.
Esse dia me ensinou uma lição brutal: HTTPS não é sinônimo de segurança. HTTPS só garante que você está falando com alguém que tem um certificado válido. Não garante que esse alguém é quem você pensa que é.
É aí que entra o certificate pinning — a técnica que transforma “esse certificado é válido” em “esse certificado é EXATAMENTE o que eu espero”. E hoje vou te mostrar como implementar isso em Python puro, sem bibliotecas externas, sem OpenSSL wrapper, sem mágica.

O Problema das Autoridades Certificadoras (CAs)
Para entender por que certificate pinning é necessário, você precisa entender o modelo de confiança do HTTPS — e por que ele é fundamentalmente quebrado.
Quando você acessa https://banco.com.br, seu navegador verifica se o certificado apresentado foi assinado por uma das centenas de autoridades certificadoras (CAs) que ele confia por padrão. DigiCert, Let’s Encrypt, GlobalSign, Sectigo… são mais de 400 CAs raiz que podem emitir certificados válidos para QUALQUER domínio.
Isso significa que se qualquer uma dessas 400+ organizações for comprometida, coagida por um governo, ou simplesmente cometer um erro operacional (como o caso da Symantec em 2017), alguém pode obter um certificado “válido” para o seu banco, sua API, seu serviço crítico.
O modelo de confiança do HTTPS não é “eu confio neste servidor específico”. É “eu confio em qualquer uma dessas 400 entidades para atestar a identidade deste servidor”. É um modelo de confiança distribuída, e como qualquer sistema distribuído, é tão forte quanto seu elo mais fraco.
Ataques Reais que Exploraram CAs
- DigiNotar (2011): CA holandesa comprometida emitiu certificados fraudulentos para Google, Microsoft e outros
- Comodo/Symantec (2015-2017): CAs emitiram certificados indevidos, resultando em desconfiança gradual pelos navegadores
- Turkish Government (2013): Tentou criar uma CA raiz para interceptar tráfego HTTPS nacional
- Superfish (2015): Lenovo pré-instalou CA raiz em laptops para injetar anúncios em conexões HTTPS
Certificate pinning resolve isso dizendo: “não me importa se você tem um certificado válido assinado por uma CA confiável. Eu só aceito ESTE certificado específico (ou esta chave pública específica).”
O Que É Certificate Pinning, Afinal?
Certificate pinning (ou “fixação de certificado”) é a prática de associar um host específico a um certificado ou chave pública conhecida, em vez de confiar em qualquer certificado válido emitido por uma CA confiável.
Existem três níveis de pinning:
- Certificate Pinning: Você fixa o certificado completo (incluindo validade, serial number, etc.)
- Public Key Pinning: Você fixa apenas a chave pública (mais flexível, permite renovação sem quebrar)
- SPKI Pinning: Você fixa o SubjectPublicKeyInfo (o hash da chave pública em formato DER) — é o que vamos implementar
O SPKI pinning é o equilíbrio ideal: específico o suficiente para detectar certificados fraudulentos, flexível o suficiente para permitir renovação legítima (você pode gerar um novo certificado com a mesma chave privada).
Implementando Certificate Pinning em Python Puro
Vamos construir um cliente HTTP que valida não só se o certificado é válido, mas se ele é EXATAMENTE o certificado que esperamos. Zero dependências externas — só a biblioteca padrão do Python.
Passo 1: Extraindo o Fingerprint do Certificado
Primeiro, precisamos de uma função que se conecte a um servidor, extraia o certificado TLS e calcule o hash SHA-256 da chave pública (SPKI fingerprint):
import ssl
import socket
import hashlib
import base64
from urllib.parse import urlparse
def get_spki_fingerprint(hostname: str, port: int = 443) -> str:
"""
Conecta ao servidor, extrai o certificado TLS e retorna
o hash SHA-256 da chave pública em formato base64.
"""
# Cria contexto SSL padrão (valida certificado normalmente)
context = ssl.create_default_context()
# Conecta e faz o handshake TLS
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
# Pega o certificado em formato DER (binário)
cert_der = ssock.getpeercert(binary_form=True)
# Extrai a chave pública do certificado DER
# O certificado DER tem estrutura ASN.1:
# Certificate ::= SEQUENCE {
# tbsCertificate TBSCertificate,
# signatureAlgorithm AlgorithmIdentifier,
# signatureValue BIT STRING
# }
# TBSCertificate contém SubjectPublicKeyInfo
# Para extrair a SPKI, precisamos parsear o ASN.1 DER
# Isso é complexo, então vamos usar uma abordagem mais simples:
# hash do certificado completo (menos flexível, mas funcional)
cert_hash = hashlib.sha256(cert_der).digest()
return base64.b64encode(cert_hash).decode('ascii')
# Exemplo de uso:
fingerprint = get_spki_fingerprint('automente.com.br')
print(f"SPKI Fingerprint: {fingerprint}")
Esse código funciona, mas tem uma limitação: estamos fazendo hash do certificado completo, não apenas da chave pública. Isso significa que quando o certificado for renovado (mesmo com a mesma chave), o fingerprint vai mudar.
Para um SPKI pinning verdadeiro, precisaríamos parsear o ASN.1 DER do certificado e extrair apenas a estrutura SubjectPublicKeyInfo. Isso é possível em Python puro, mas envolve parsing ASN.1 manual — vou mostrar uma versão simplificada que funciona para a maioria dos casos:
Passo 2: Parsing ASN.1 para Extrair a Chave Pública
def extract_spki_from_der(der_bytes: bytes) -> bytes:
"""
Extrai a estrutura SubjectPublicKeyInfo de um certificado DER.
Implementação simplificada de parsing ASN.1.
"""
# Certificate é uma SEQUENCE
if der_bytes[0] != 0x30: # SEQUENCE tag
raise ValueError("Certificado DER inválido")
# Pula o header da SEQUENCE principal
offset = 2
if der_bytes[1] & 0x80: # Long form length
length_bytes = der_bytes[1] & 0x7F
offset = 2 + length_bytes
# TBSCertificate é a primeira SEQUENCE dentro de Certificate
if der_bytes[offset] != 0x30:
raise ValueError("TBSCertificate não encontrado")
tbs_start = offset
offset += 2
if der_bytes[tbs_start + 1] & 0x80:
length_bytes = der_bytes[tbs_start + 1] & 0x7F
offset = tbs_start + 2 + length_bytes
# Dentro de TBSCertificate, precisamos pular:
# version, serialNumber, signature, issuer, validity, subject
# até chegar em subjectPublicKeyInfo
# Pula version (INTEGER, opcional)
if der_bytes[offset] == 0xA0: # EXPLICIT tag para version
offset += 2 + der_bytes[offset + 1]
# Pula serialNumber (INTEGER)
if der_bytes[offset] == 0x02:
offset += 2 + der_bytes[offset + 1]
# Pula signature (SEQUENCE)
if der_bytes[offset] == 0x30:
offset += 2 + der_bytes[offset + 1]
# Pula issuer (SEQUENCE)
if der_bytes[offset] == 0x30:
offset += 2 + der_bytes[offset + 1]
# Pula validity (SEQUENCE)
if der_bytes[offset] == 0x30:
offset += 2 + der_bytes[offset + 1]
# Pula subject (SEQUENCE)
if der_bytes[offset] == 0x30:
offset += 2 + der_bytes[offset + 1]
# Agora estamos em subjectPublicKeyInfo (SEQUENCE)
if der_bytes[offset] != 0x30:
raise ValueError("SubjectPublicKeyInfo não encontrado")
spki_start = offset
# Calcula o tamanho total da SPKI
length = der_bytes[offset + 1]
if length & 0x80:
length_bytes = length & 0x7F
length = int.from_bytes(der_bytes[offset + 2:offset + 2 + length_bytes], 'big')
spki_end = offset + 2 + length_bytes + length
else:
spki_end = offset + 2 + length
return der_bytes[spki_start:spki_end]
def get_spki_fingerprint_v2(hostname: str, port: int = 443) -> str:
"""Versão aprimorada que faz hash apenas da SPKI"""
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert_der = ssock.getpeercert(binary_form=True)
spki = extract_spki_from_der(cert_der)
spki_hash = hashlib.sha256(spki).digest()
return base64.b64encode(spki_hash).decode('ascii')

Passo 3: Validando Conexões com Pinning
Agora que temos a função para extrair o fingerprint, vamos criar um cliente HTTP que valida o pin antes de confiar na conexão:
import http.client
import json
class PinnedHTTPSClient:
"""Cliente HTTPS com certificate pinning"""
def __init__(self, hostname: str, expected_fingerprint: str, port: int = 443):
self.hostname = hostname
self.port = port
self.expected_fingerprint = expected_fingerprint
def validate_certificate(self):
"""Valida se o certificado atual corresponde ao fingerprint esperado"""
actual_fingerprint = get_spki_fingerprint_v2(self.hostname, self.port)
if actual_fingerprint != self.expected_fingerprint:
raise ssl.SSLCertVerificationError(
f"Certificate pinning violation!\n"
f"Expected: {self.expected_fingerprint}\n"
f"Actual: {actual_fingerprint}\n"
f"Possible MITM attack detected!"
)
return True
def request(self, method: str, path: str, headers: dict = None, body: str = None):
"""Faz uma requisição HTTPS com validação de pinning"""
# Valida o certificado ANTES de enviar dados sensíveis
self.validate_certificate()
# Agora faz a requisição normalmente
context = ssl.create_default_context()
conn = http.client.HTTPSConnection(
self.hostname,
self.port,
context=context,
timeout=10
)
try:
conn.request(method, path, body=body, headers=headers or {})
response = conn.getresponse()
return {
'status': response.status,
'headers': dict(response.getheaders()),
'body': response.read().decode('utf-8')
}
finally:
conn.close()
# Exemplo de uso:
BANCO_FINGERPRINT = "a1b2c3d4e5f6..." # Obtido previamente de forma segura
client = PinnedHTTPSClient('api.banco.com.br', BANCO_FINGERPRINT)
try:
response = client.request('POST', '/v1/pagamentos',
headers={'Content-Type': 'application/json'},
body=json.dumps({'valor': 100.00}))
print(f"Pagamento processado: {response['status']}")
except ssl.SSLCertVerificationError as e:
print(f"ATAQUE DETECTADO: {e}")
# Alerta de segurança, não prosseguir!
O Box do Perrengue: Quando o Certificado Muda
Perrengue Real: Implementei certificate pinning na API de pagamentos da empresa. Funcionou perfeitamente por 8 meses. Aí o time de DevOps renovou o certificado (como faz todo ano) e o sistema inteiro parou de funcionar às 2h da manhã de um sábado de Black Friday.
O problema? Tínhamos fixado o fingerprint do certificado, não da chave pública. Quando o certificado foi renovado, mesmo usando a mesma chave privada, o fingerprint mudou.
Lição aprendida: Sempre use SPKI pinning (chave pública), não certificate pinning (certificado completo). E tenha um processo de rotação de pins ANTES de renovar certificados. Nós não tínhamos. Pagamos o preço.
Gerenciando Múltiplos Pins (Backup Keys)
Em produção, você nunca deve ter apenas um pin. O padrão é ter pelo menos dois: um pin ativo e um pin de backup. Quando você precisa renovar o certificado, gera um novo par de chaves, cria o pin de backup ANTES de fazer o deploy, e só então renova o certificado.
class MultiPinHTTPSClient:
"""Cliente com suporte a múltiplos pins (ativo + backup)"""
def __init__(self, hostname: str, pins: list[str], port: int = 443):
"""
pins: Lista de fingerprints válidos (SPKI hashes)
Ex: ['pin_ativo_base64', 'pin_backup_base64']
"""
self.hostname = hostname
self.port = port
self.valid_pins = set(pins)
if len(self.valid_pins) < 2:
raise ValueError("Configure pelo menos 2 pins (ativo + backup)")
def validate_certificate(self) -> str:
"""Valida e retorna qual pin foi usado"""
actual_fingerprint = get_spki_fingerprint_v2(self.hostname, self.port)
if actual_fingerprint not in self.valid_pins:
raise ssl.SSLCertVerificationError(
f"Certificate pinning violation!\n"
f"Actual: {actual_fingerprint}\n"
f"Valid pins: {self.valid_pins}\n"
)
return actual_fingerprint
def rotate_pin(self, old_pin: str, new_pin: str):
"""Remove pin antigo e adiciona novo (para rotação segura)"""
if old_pin not in self.valid_pins:
raise ValueError(f"Pin antigo não encontrado: {old_pin}")
self.valid_pins.remove(old_pin)
self.valid_pins.add(new_pin)
# Em produção, persista isso em um keystore seguro
print(f"Pin rotated successfully")
print(f"Remaining pins: {self.valid_pins}")
# Uso com backup key:
client = MultiPinHTTPSClient(
'api.banco.com.br',
pins=[
'a1b2c3d4...', # Pin ativo (certificado atual)
'e5f6g7h8...' # Pin backup (próxima chave pública)
]
)
# Quando for renovar o certificado:
# 1. Gere novo par de chaves
# 2. Calcule o fingerprint da nova chave pública
# 3. Adicione como pin de backup
# 4. Só então renove o certificado
# 5. Remova o pin antigo depois que todos os clientes atualizaram
Casos de Uso Reais: Onde Pinning Faz Diferença
Certificate pinning não é para todo mundo. Adiciona complexidade operacional e risco de lockout se mal gerenciado. Mas existem cenários onde o custo vale a pena:
1. APIs Financeiras e de Pagamento
Se você integra com APIs de banco, gateways de pagamento (Stripe, PayPal, PagSeguro), ou qualquer sistema que processa dinheiro, certificate pinning é uma camada extra de defesa. Um atacante com um certificado fraudulento poderia redirecionar pagamentos ou roubar dados de cartão.
Apps mobile de bancos usam pinning extensivamente. O app do Nubank, por exemplo, faz pinning dos certificados dos servidores da API. Se alguém tentar um MITM, o app simplesmente não conecta.
2. Comunicação Entre Microserviços
Em arquiteturas de microserviços, você tem dezenas de serviços se comunicando internamente. Se um atacante comprometer a rede interna (ou conseguir um certificado para service-a.internal), pode interceptar tráfego entre serviços.
Com pinning, cada serviço só aceita certificados de serviços específicos que ele conhece. É uma forma de mutual authentication sem a complexidade de mTLS completo.
3. IoT e Dispositivos Embarcados
Dispositivos IoT frequentemente se conectam a um único servidor backend. Fazer pinning do certificado desse servidor elimina a necessidade de manter um bundle de CAs confiáveis no dispositivo (que ocupa espaço e precisa ser atualizado).
Muitos dispositivos IoT usam pinning hardcoded no firmware. É uma abordagem válida, desde que você tenha um mecanismo de atualização OTA seguro para trocar os pins se necessário.
Quando NÃO Usar Certificate Pinning
Pinning tem custos reais. Antes de implementar, considere:
- Alta rotatividade de certificados: Se você usa Let’s Encrypt com renovação automática a cada 90 dias, pinning de certificado completo vai te dar dor de cabeça constante
- Múltiplos ambientes: Dev, staging, produção com certificados diferentes? Você vai precisar gerenciar pins diferentes para cada ambiente
- CDNs e load balancers: Se você usa Cloudflare, AWS CloudFront ou similar, o certificado que o cliente vê é da CDN, não do seu servidor. Pinning vai falhar
- Falta de processo de rotação: Se você não tem um processo documentado para renovar pins antes de renovar certificados, não use pinning. Você vai se trancar para fora
Para a maioria dos sites web convencionais, HTTPS padrão com CAs confiáveis é suficiente. Pinning é para casos específicos onde você precisa de garantias extras de identidade.
Alternativas Modernas ao Certificate Pinning
Se pinning parece complexo demais para seu caso de uso, existem alternativas que oferecem benefícios similares:
CT (Certificate Transparency) Logs
Certificate Transparency é um sistema de logs públicos onde todas as CAs são obrigadas a registrar certificados emitidos. Você pode monitorar esses logs e receber alertas se alguém emitir um certificado para seu domínio sem autorização.
Não previne ataques em tempo real, mas permite detecção rápida. Serviços como crt.sh e Cert Spotter fazem esse monitoramento para você.
DANE (DNS-Based Authentication of Named Entities)
DANE usa DNSSEC para publicar fingerprints de certificados diretamente no DNS. É como pinning, mas gerenciado via DNS em vez de hardcoded no cliente. Mais flexível, mas depende de DNSSEC estar corretamente implementado.
mTLS (Mutual TLS)
Em vez de só o servidor apresentar certificado, o cliente também apresenta um. Ambos validam a identidade um do outro. É mais seguro que pinning unilateral, mas exige infraestrutura de PKI para emitir e gerenciar certificados de cliente.
Para comunicação service-to-service em ambientes controlados (Kubernetes com service mesh, por exemplo), mTLS é frequentemente a escolha certa.
Conclusão: Confiança É Uma Escolha, Não Um Default
A lição que aprendi naquela madrugada de PagerDuty foi dura, mas valiosa: segurança não é sobre ter todas as proteções ativadas por padrão. É sobre fazer escolhas conscientes de onde confiar e como validar.
Certificate pinning em Python puro te dá controle total sobre essa decisão. Você não depende de bibliotecas externas, não precisa entender a API do OpenSSL, não precisa configurar contextos SSL complexos. É só sockets, TLS padrão, e um hash SHA-256.
Mas lembre-se: com grande poder vem grande responsabilidade operacional. Se você vai usar pinning, tenha um processo de rotação de pins. Documente. Teste. E monitore. Porque a alternativa — descobrir às 3h da manhã que seu HTTPS estava mentindo — é muito pior.
Agora me conta: qual automação de segurança você quer ver aqui no AutoMente? Implementação de mTLS? Monitoramento de Certificate Transparency? Detecção de anomalias em logs de acesso? Deixa nos comentários que eu transformo em código.
