Guia Completo de PKI e Certificados X.509: CA, Chain of Trust, CRL e OCSP Já leu

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

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.

Referências


Artigos relacionados