Por que validar não é só "recebeu, confia"
Quando o seu servidor recebe um certificado de cliente — via mTLS ou enviado pela aplicação —, ter o certificado em mãos não significa nada por si só. Qualquer um pode gerar um certificado dizendo "sou o CPF 123". O que dá valor a um e-CPF é a cadeia de confiança: ele foi assinado por uma AC intermediária, que foi assinada por outra, até chegar na AC-Raiz da ICP-Brasil, na qual você — e a legislação brasileira — confia.
Validar corretamente significa responder a quatro perguntas, nesta ordem:
- O certificado encadeia até uma AC-Raiz da ICP-Brasil em que eu confio?
- Todos os certificados da cadeia estão dentro da validade (nem expirados, nem futuros)?
- Nenhum deles foi revogado?
- Quais são os dados do titular (CPF/CNPJ, nome) que posso confiar?
Pular qualquer uma dessas etapas abre um buraco de segurança. O .NET oferece as ferramentas para todas — mas você precisa configurá-las certo, porque os padrões não conhecem a ICP-Brasil.
Montando o repositório de ACs confiáveis
O .NET, por padrão, valida contra as âncoras de confiança do sistema operacional. A AC-Raiz da ICP-Brasil não está nesse conjunto na maioria dos servidores. Então o primeiro passo é carregar, você mesmo, os certificados da raiz e das intermediárias que quer aceitar.
using System.Security.Cryptography.X509Certificates;
// carrega cada .cer/.crt baixado do repositório do ITI
var acsConfiaveis = new X509Certificate2Collection();
foreach (var arquivo in Directory.GetFiles("acs-icp-brasil", "*.cer"))
{
acsConfiaveis.Add(new X509Certificate2(arquivo));
}
Validando com X509Chain
O X509Chain é a classe do .NET que monta e valida a cadeia. O ponto-chave é configurar o ChainPolicy para usar suas ACs como âncoras de confiança, em vez das do sistema:
public bool ValidarCadeia(X509Certificate2 certCliente,
X509Certificate2Collection acsConfiaveis)
{
using var chain = new X509Chain();
// não usar o trust store do SO; usar só as ACs da ICP-Brasil
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.AddRange(acsConfiaveis);
// incluir as intermediárias para o .NET conseguir montar a cadeia
chain.ChainPolicy.ExtraStore.AddRange(acsConfiaveis);
// revogação: ver a próxima seção
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
bool ok = chain.Build(certCliente);
if (!ok)
{
foreach (var status in chain.ChainStatus)
Console.WriteLine($"Falha: {status.StatusInformation}");
}
return ok;
}
O chain.Build() retorna true só se a cadeia inteira for válida sob a política definida. Quando falha, o ChainStatus diz exatamente por quê — e é aí que você vai passar boa parte do tempo de depuração.
Checando revogação (CRL / OCSP)
Um certificado pode ser válido na cadeia e ainda assim ter sido revogado — por exemplo, quando alguém perde o token A3 e comunica a AC. Ignorar revogação significa aceitar certificados que deveriam estar bloqueados.
No código acima, RevocationMode = Online faz o .NET consultar as listas de revogação (CRL) ou OCSP publicadas pelas ACs. Parece automático, mas há armadilhas de produção:
- A verificação online depende de os servidores de CRL/OCSP da ICP-Brasil estarem acessíveis no momento — se cair, sua autenticação cai junto, a menos que você trate o timeout.
- As CRLs têm validade e precisam ser re-baixadas periodicamente. Cache mal configurado leva a aceitar um certificado revogado ou a rejeitar um válido.
- Nem toda AC expõe OCSP; algumas só CRL. Você precisa cobrir os dois casos.
Extraindo CPF, CNPJ e nome do titular
Cadeia validada, falta o que você realmente queria: quem é essa pessoa/empresa? E aqui a ICP-Brasil complica de propósito. O CPF, o CNPJ, a data de nascimento e outros dados não ficam nos campos padrão do X.509 — ficam em extensões com OIDs proprietários (como o 2.16.76.1.3.*), codificados num formato específico.
// OID da ICP-Brasil que carrega os dados da pessoa física
const string OID_PESSOA_FISICA = "2.16.76.1.3.1";
var extensao = certCliente.Extensions
.Cast<X509Extension>()
.FirstOrDefault(e => e.Oid?.Value == OID_PESSOA_FISICA);
if (extensao != null)
{
byte[] raw = extensao.RawData;
// raw é uma string ASN.1 com campos concatenados por posição:
// data de nascimento, CPF, RG, órgão emissor... tudo em offsets fixos.
// você precisa fatiar por posição e decodificar cada pedaço.
var dados = ParseCamposPessoaFisica(raw);
}
O ParseCamposPessoaFisica() esconde a real dificuldade: os campos vêm concatenados por posição numa string, com tamanhos fixos que você tem de conhecer de antemão. E muda entre e-CPF e e-CNPJ (OID 2.16.76.1.3.3 para o CNPJ), entre versões de certificado e entre ACs. É o tipo de código que funciona nos testes e quebra com o certificado de um cliente específico seis meses depois.
Armadilhas comuns
certificado legítimo
ou vazio
O que precisa de manutenção contínua
Escrever a validação uma vez, para os certificados que você tem hoje, é a parte visível — e a menor. O que fica é o sistema vivo por trás:
- Sincronizar a cadeia de ACs conforme a ICP-Brasil adiciona e renova autoridades, senão usuários legítimos começam a falhar sem aviso.
- Manter CRLs atualizadas e tratar indisponibilidade dos endpoints de revogação sem derrubar o login.
- Cobrir todas as variações de parsing — e-CPF, e-CNPJ, A1, A3, versões antigas e novas — à medida que aparecem clientes com certificados diferentes.
- Acompanhar mudanças normativas do ITI que afetam formato e OIDs.
É trabalho de manutenção permanente, sensível a segurança, sobre uma norma que não é sua e muda no ritmo dela. Funciona — mas rouba tempo de engenharia do produto que você de fato quer entregar.
Deixe a validação ICP-Brasil com quem só faz isso
Com o IHub-Auth você não monta X509Chain, não persegue CRL, não faz parsing de OID proprietário. A cadeia ICP-Brasil completa fica atualizada do nosso lado, a validação acontece via verificação server-to-server, e o seu backend recebe CPF, CNPJ e nome do titular já prontos. Você integra em minutos e volta a cuidar do seu produto.