Fundamentos de PKI: O Alicerce da Segurança Digital
A Infraestrutura de Chave Pública (PKI — Public Key Infrastructure) é o sistema que permite comunicação segura e verificação de identidade em ambientes digitais descentralizados. Diferente de sistemas de criptografia simétrica, onde uma chave única é compartilhada entre as partes, a PKI utiliza um par de chaves: uma pública (que pode ser divulgada) e uma privada (que deve ser guardada com rigor). Essa abordagem resolve dois problemas críticos: autenticação (saber que você é realmente quem diz ser) e confidencialidade (garantir que apenas o destinatário leia a mensagem).
O desafio central da PKI é garantir que a chave pública que você recebe realmente pertence à pessoa ou entidade que você acredita. Aqui entra o certificado digital, um documento que vincula uma chave pública a uma identidade. Porém, um certificado por si só não é suficiente — você precisa confiar em quem emitiu esse certificado. É aí que nascem as Autoridades Certificadoras (CAs) e toda a cadeia de confiança que exploraremos a seguir.
Certificados X.509 e o Papel das Autoridades Certificadoras
O padrão X.509 define a estrutura de um certificado digital. Ele contém informações como o nome do titular, a chave pública, datas de validade, algoritmos criptográficos utilizados e, crucialmente, uma assinatura digital da Autoridade Certificadora emissora. A assinatura digital funciona assim: a CA aplica sua chave privada aos dados do certificado, criando uma prova matemática de que a CA realmente validou e emitiu aquele certificado. Qualquer pessoa pode verificar essa assinatura usando a chave pública da CA.
Estrutura de um Certificado X.509
Um certificado X.509 (versão 3) é uma estrutura ASN.1 codificada, tipicamente em DER ou PEM. Vamos examinar um certificado real em Python usando a biblioteca cryptography:
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timedelta
# Carregar um certificado de um arquivo PEM
with open("certificado.pem", "rb") as f:
cert_data = f.read()
cert = x509.load_pem_x509_certificate(cert_data)
# Extrair informações principais
print(f"Sujeito: {cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value}")
print(f"Emissor: {cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value}")
print(f"Válido de: {cert.not_valid_before}")
print(f"Válido até: {cert.not_valid_after}")
print(f"Número de série: {cert.serial_number}")
# Verificar extensões críticas
for ext in cert.extensions:
print(f"Extensão {ext.oid._name}: crítica={ext.critical}")
if ext.oid == ExtensionOID.BASIC_CONSTRAINTS:
bc = ext.value
print(f" É CA: {bc.ca}")
print(f" Path length: {bc.path_length}")
A Autoridade Certificadora (CA)
Uma Autoridade Certificadora é uma entidade confiável que valida a identidade dos solicitantes e emite certificados em seu nome. Existem dois tipos principais: a CA raiz (root CA), que é auto-assinada e está no topo da cadeia de confiança, e CAs intermediárias, que são assinadas pela raiz ou por outras intermediárias. A separação em múltiplas camadas existe por razões de segurança: a chave privada da raiz raramente é usada e pode ser mantida offline, enquanto as intermediárias fazem o trabalho diário.
Quando você cria um certificado de servidor (por exemplo, para um site HTTPS), você não solicita diretamente à raiz — você envia um CSR (Certificate Signing Request) para uma CA intermediária. Essa CA valida seus dados e emite o certificado, assinando-o com sua chave privada.
Vamos criar um certificado auto-assinado (simulando uma CA raiz) e depois um certificado assinado por ele:
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timedelta
import ipaddress
# 1. Criar a chave privada da CA raiz
ca_private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# 2. Criar o certificado auto-assinado da CA raiz
ca_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "BR"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "São Paulo"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Minha Autoridade Certificadora"),
x509.NameAttribute(NameOID.COMMON_NAME, "Root CA"),
])
ca_cert = x509.CertificateBuilder().subject_name(
ca_subject
).issuer_name(
ca_subject # Auto-assinado: sujeito = emissor
).public_key(
ca_private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
).not_valid_after(
datetime.utcnow() + timedelta(days=3650) # 10 anos
).add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
).add_extension(
x509.KeyUsage(
digital_signature=True,
key_cert_sign=True,
crl_sign=True,
key_encipherment=False,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
).sign(ca_private_key, hashes.SHA256())
# Salvar a CA raiz
with open("ca_root.pem", "wb") as f:
f.write(ca_cert.public_bytes(serialization.Encoding.PEM))
# 3. Agora criar um certificado de servidor assinado pela CA raiz
server_private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
server_subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "BR"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Minha Empresa"),
x509.NameAttribute(NameOID.COMMON_NAME, "www.exemplo.com.br"),
])
server_cert = x509.CertificateBuilder().subject_name(
server_subject
).issuer_name(
ca_subject # Emissor é a CA raiz
).public_key(
server_private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
).not_valid_after(
datetime.utcnow() + timedelta(days=365)
).add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
).add_extension(
x509.SubjectAlternativeName([
x509.DNSName("www.exemplo.com.br"),
x509.DNSName("exemplo.com.br"),
]),
critical=False,
).add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
).sign(ca_private_key, hashes.SHA256()) # Assinado com a chave privada da CA
# Salvar o certificado do servidor
with open("server.pem", "wb") as f:
f.write(server_cert.public_bytes(serialization.Encoding.PEM))
with open("server_key.pem", "wb") as f:
f.write(server_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
))
Chain of Trust: A Cadeia de Confiança
A cadeia de confiança (chain of trust) é o caminho que conecta o certificado de um servidor até uma CA raiz que você já confia. Quando seu navegador recebe o certificado HTTPS de um servidor, ele não confia automaticamente — ele valida a assinatura usando a chave pública do emissor (que está em outro certificado). Se aquele emissor não for uma raiz conhecida, o navegador busca o certificado do próprio emissor e repete o processo, subindo a cadeia até encontrar uma raiz em sua loja de confiança (trusted root store).
Como a Validação Funciona
Cada elo da cadeia é verificado em três passos: primeiro, confirma-se que a assinatura digital é válida (a CA realmente assinou este certificado); segundo, verifica-se que o certificado ainda não expirou; terceiro, confirma-se que o certificado não foi revogado (abordaremos revogação em breve).
Vamos escrever um código que simula uma validação completa de cadeia:
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.x509.oid import NameOID, ExtensionOID
from datetime import datetime
def validar_cadeia_certificados(caminho_cert_servidor, caminho_ca_intermediaria, caminho_ca_raiz):
"""
Valida uma cadeia de certificados do servidor até a raiz.
"""
# Carregar todos os certificados
with open(caminho_cert_servidor, "rb") as f:
cert_servidor = x509.load_pem_x509_certificate(f.read())
with open(caminho_ca_intermediaria, "rb") as f:
cert_intermediaria = x509.load_pem_x509_certificate(f.read())
with open(caminho_ca_raiz, "rb") as f:
cert_raiz = x509.load_pem_x509_certificate(f.read())
cadeia = [cert_servidor, cert_intermediaria, cert_raiz]
print("=" * 60)
print("VALIDAÇÃO DE CADEIA DE CERTIFICADOS")
print("=" * 60)
for i, cert in enumerate(cadeia):
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
print(f"\n[{i}] Certificado: {cn}")
# Verificar validade temporal
agora = datetime.utcnow()
if agora < cert.not_valid_before or agora > cert.not_valid_after:
print(f" ❌ EXPIRADO ou ainda não válido")
return False
else:
print(f" ✓ Válido de {cert.not_valid_before} até {cert.not_valid_after}")
# Verificar se é auto-assinado (raiz)
if cert.issuer == cert.subject:
print(f" ✓ Auto-assinado (CA Raiz)")
# Para a raiz, apenas verificamos auto-assinatura
try:
cert.public_key().verify(
cert.signature,
cert.tbs_certificate_bytes,
cert.signature_algorithm_oid._name,
)
print(f" ✓ Assinatura auto-verificada com sucesso")
except Exception as e:
print(f" ❌ Falha na verificação de assinatura: {e}")
return False
else:
# Para certificados intermediários e servidores, verificar assinatura
emissor_cn = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
print(f" Emissor: {emissor_cn}")
# O próximo certificado na cadeia deve ser o do emissor
if i < len(cadeia) - 1:
cert_emissor = cadeia[i + 1]
try:
# Usar a chave pública do emissor para verificar a assinatura
cert_emissor.public_key().verify(
cert.signature,
cert.tbs_certificate_bytes,
cert.signature_algorithm_oid._name,
)
print(f" ✓ Assinatura verificada pelo certificado do emissor")
except Exception as e:
print(f" ❌ Falha na verificação de assinatura: {e}")
return False
# Verificar extensão BasicConstraints (apenas CAs devem ter ca=True)
try:
bc = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS).value
print(f" ✓ BasicConstraints: CA={bc.ca}, PathLength={bc.path_length}")
except x509.ExtensionNotFound:
print(f" ⚠ BasicConstraints não encontrado")
print("\n" + "=" * 60)
print("✓ CADEIA VÁLIDA!")
print("=" * 60)
return True
# Uso
validar_cadeia_certificados("server.pem", "ca_intermediaria.pem", "ca_root.pem")
Revogação de Certificados: CRL e OCSP
Certificados têm data de expiração, mas às vezes precisam ser revogados antes disso — quando uma chave privada é comprometida, quando um funcionário deixa a empresa, ou quando as informações no certificado mudam. Existem dois mecanismos principais para verificar se um certificado foi revogado: CRL (Certificate Revocation List) e OCSP (Online Certificate Status Protocol).
CRL — Certificate Revocation List
A CRL é uma lista assinada pela CA contendo os números de série de todos os certificados que foram revogados. Um cliente pode baixar a CRL periodicamente e verificar se o certificado em questão está nela. O problema é que a CRL pode ser grande e desatualizada — você pode baixar ela uma vez por dia, mas um certificado pode ter sido revogado há poucos minutos.
Vamos criar uma CRL e verificar se um certificado está nela:
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.x509.oid import NameOID, ExtensionOID
from datetime import datetime, timedelta
# Supondo que temos:
# - ca_private_key: chave privada da CA
# - ca_cert: certificado da CA
# - server_cert: certificado que queremos revogar
def criar_crl(ca_private_key, ca_cert, certificados_revogados, arquivo_saida):
"""
Cria uma CRL assinada pela CA.
certificados_revogados: lista de tuplas (numero_serie, datetime_revogacao, razao)
"""
builder = x509.CertificateRevocationListBuilder()
builder = builder.issuer_name(ca_cert.subject)
builder = builder.last_update(datetime.utcnow())
builder = builder.next_update(datetime.utcnow() + timedelta(days=7))
for serial, revoke_date, reason in certificados_revogados:
revoked_cert = x509.RevokedCertificateBuilder().serial_number(
serial
).revocation_date(
revoke_date
).add_extension(
x509.CRLReason(x509.ReasonFlags.key_compromise),
critical=False
).build()
builder = builder.add_revoked_certificate(revoked_cert)
# Adicionar extensões à CRL
builder = builder.add_extension(
x509.CRLNumber(1), # Número de versão da CRL
critical=False
)
crl = builder.sign(ca_private_key, hashes.SHA256())
with open(arquivo_saida, "wb") as f:
f.write(crl.public_bytes(serialization.Encoding.PEM))
return crl
def verificar_revogacao_crl(cert_a_verificar, arquivo_crl):
"""
Verifica se um certificado está em uma CRL.
Retorna True se revogado, False se válido.
"""
with open(arquivo_crl, "rb") as f:
crl = x509.load_pem_x509_crl(f.read())
for revoked_cert in crl:
if revoked_cert.serial_number == cert_a_verificar.serial_number:
print(f"❌ Certificado REVOGADO em {revoked_cert.revocation_date}")
return True
print(f"✓ Certificado NÃO está na CRL")
return False
# Exemplo de uso
# certificados_para_revogar = [
# (123456, datetime.utcnow() - timedelta(hours=2), "key_compromise"),
# ]
# crl = criar_crl(ca_private_key, ca_cert, certificados_para_revogar, "crl.pem")
# revogado = verificar_revogacao_crl(server_cert, "crl.pem")
OCSP — Online Certificate Status Protocol
OCSP é um protocolo que permite consultar o status de um certificado em tempo real. Um cliente envia uma requisição para o OCSP responder (um servidor mantido pela CA) perguntando: "Este certificado foi revogado?" O responder retorna uma resposta assinada (Good, Revoked ou Unknown). A vantagem é que a informação é atual; a desvantagem é que requer conectividade de rede e há implicações de privacidade (a CA sabe quais certificados você está verificando).
Vamos escrever um exemplo de verificação OCSP usando a biblioteca requests e pyOpenSSL:
import requests
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.backends import default_backend
def extrair_ocsp_responder_url(cert):
"""
Extrai a URL do OCSP responder do certificado.
"""
try:
aia = cert.extensions.get_extension_for_oid(
ExtensionOID.AUTHORITY_INFORMATION_ACCESS
).value
for desc in aia:
if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
return desc.access_location.value
except x509.ExtensionNotFound:
return None
return None
def verificar_status_ocsp(cert_a_verificar, issuer_cert):
"""
Faz uma requisição OCSP para verificar o status do certificado.
"""
ocsp_url = extrair_ocsp_responder_url(cert_a_verificar)
if not ocsp_url:
print("⚠ Nenhuma URL de OCSP responder encontrada no certificado")
return None
print(f"Consultando OCSP responder: {ocsp_url}")
# Montar a requisição OCSP
from ocspbuilder import OCSPRequestBuilder
builder = OCSPRequestBuilder()
builder = builder.add_certificate(cert_a_verificar, issuer_cert)
ocsp_request = builder.build()
# Enviar a requisição
try:
response = requests.post(
ocsp_url,
data=ocsp_request.dump(),
headers={"Content-Type": "application/ocsp-request"},
timeout=5
)
response.raise_for_status()
except Exception as e:
print(f"❌ Erro ao consultar OCSP responder: {e}")
return None
# Analisar a resposta
from ocspbuilder import OCSPResponseBuilder
try:
ocsp_response = x509.ocsp.load_der_ocsp_response(response.content)
status = ocsp_response.certificate_status
if status == x509.ocsp.OCSPCertStatus.GOOD:
print(f"✓ Certificado está VÁLIDO (status: GOOD)")
return True
elif status == x509.ocsp.OCSPCertStatus.REVOKED:
print(f"❌ Certificado foi REVOGADO em {ocsp_response.revocation_time}")
return False
else:
print(f"⚠ Status desconhecido")
return None
except Exception as e:
print(f"❌ Erro ao processar resposta OCSP: {e}")
return None
# Uso (exemplo com um certificado real):
# with open("server.pem", "rb") as f:
# cert = x509.load_pem_x509_certificate(f.read())
# with open("ca_intermediaria.pem", "rb") as f:
# issuer = x509.load_pem_x509_certificate(f.read())
# verificar_status_ocsp(cert, issuer)
Comparação: CRL vs OCSP
A CRL é mais simples de implementar mas menos eficiente; você mantém a lista localmente. OCSP é mais preciso e menos invasivo em largura de banda, mas requer conectividade e tem preocupações com privacidade. Na prática, navegadores modernos usam ambos: verificam uma CRL em cache localmente como primeiro passo, e se a CRL estiver muito antiga, fazem uma consulta OCSP.
Conclusão
Você aprendeu que a PKI é um sistema de confiança baseado em cadeias de certificados assinados digitalmente. Primeiro, entendeu que o certificado X.509 é o documento que vincula uma chave pública a uma identidade, assinado por uma autoridade certificadora para garantir sua autenticidade. Segundo, compreendeu que a segurança depende de uma chain of trust bem definida, onde cada certificado é verificado usando a chave pública do seu emissor, subindo até uma raiz confiável. Terceiro, reconheceu que a revogação de certificados é crítica — você não pode confiar em um certificado apenas por estar assinado; é preciso verificar via CRL (revogação em lote) ou OCSP (status em tempo real) se ele foi revogado.
Esses três conceitos — certificados assinados, cadeia de validação, e revogação — são os pilares de toda segurança HTTPS, emails criptografados, e assinatura digital.