Artigo Técnico

Certificados de Teste Autoassinados em Delphi com CryptoAPI

A função PLCreateSelfSignedCertificate do PDF Library for Delphi constrói um certificado RSA/SHA-256 autoassinado e exporta-o, chave privada incluída, diretamente para um ficheiro PFX protegido por palavra-passe, usando apenas a CryptoAPI Win32 já instalada em qualquer máquina Windows. Sem ferramenta externa, sem autoridade certificadora, sem passo manual de makecert ou OpenSSL: uma chamada de função, um certificado suficientemente bom para conduzir um teste de assinatura

O cenário que torna esta função digna de existir é quase sempre um pipeline de CI. Um teste rápido de assinatura precisa de um PFX real com uma chave privada real por trás, e incluir um no repositório é, por si só, um problema de segurança, já que uma chave privada submetida a commit é uma chave privada exposta a partir do momento em que esse commit chega. Chamar o makecert.exe ou uma invocação OpenSSL a partir de um script de compilação também funciona, mas nesse caso o pipeline passa a depender de uma ferramenta que tem de estar instalada, ser encontrada no PATH, e manter-se consistente em versão em cada agente de compilação. Gerar o certificado dentro do mesmo processo que corre o teste, com as mesmas chamadas CryptoAPI Win32 que o Windows já distribui, elimina essa dependência por completo

O que produz efetivamente o PLCreateSelfSignedCertificate?

O PLCreateSelfSignedCertificate produz um ficheiro PFX protegido por palavra-passe que contém um certificado RSA autoassinado e a sua chave privada, assinado com sha256RSA, controlado por cinco parâmetros: SubjectName, PFXFileName, PFXPassword, ValidDays, e KeyBits, e devolve uma simples flag Boolean de sucesso. O SubjectName aceita uma cadeia X.500 completa como 'CN=Alice, O=Example', e um nome simples sem qualquer sinal = recebe automaticamente o prefixo CN=. O ValidDays abaixo de 1 recorre por predefinição a 365, e o KeyBits fora do intervalo de 1024 a 16384 recorre por predefinição a 2048. O PDF Library for Delphi distribui esta função desde a v3.224.0, acessível não só a partir da unidade Delphi mas também através das superfícies DLL e ActiveX, e o seu próprio comentário de documentação é direto quanto a onde deixa de ser útil: qualquer visualizador comum marca um certificado autoassinado como não fiável a menos que alguém o instale explicitamente, pelo que se deve tratar o que produz como um certificado para exercitar um caminho de código, não uma assinatura em que se deva pedir a alguém fora da equipa que confie

var
  Success: Boolean;
begin
  Success := PLCreateSelfSignedCertificate(
    'CN=PDF Library for Delphi CI Test, O=Example Corp',
    'ci-test-signer.pfx',
    'a-strong-throwaway-password',
    365,     // ValidDays
    2048);   // KeyBits
  if not Success then
    raise Exception.Create('Self-signed certificate generation failed');
end;

Porque é que o CryptGenKey codifica o comprimento da chave no parâmetro de flags?

O CryptGenKey comprime duas configurações sem relação entre si num único parâmetro dwFlags. A palavra baixa transporta flags de comportamento, entre elas CRYPT_EXPORTABLE, enquanto a palavra alta, para uma chave de troca de chaves RSA, transporta o comprimento de chave pedido em bits. Passar 2048 como se fosse apenas mais uma flag coloca-o na palavra baixa, onde não corresponde a nenhuma flag de comportamento que a CryptoAPI defina, pelo que a chamada gera uma chave no comprimento predefinido para que o provedor recorre, em vez do comprimento que o chamador pensava ter pedido. Obter uma verdadeira chave RSA de 2048 bits significa primeiro deslocar o número para a palavra alta

Diagrama da PDF Library for Delphi do parâmetro dwFlags de CryptGenKey a dividir-se numa palavra alta que transporta KeyBits shl 16 como comprimento da chave RSA e numa palavra baixa que transporta flags de comportamento como CRYPT_EXPORTABLE
Um 2048 isolado cai na metade dos sinalizadores de comportamento, pelo que o fornecedor recua silenciosamente para o seu comprimento de chave predefinido
// O comprimento da chave reside nos 16 bits superiores das flags do CryptGenKey;
// a palavra baixa transporta flags de comportamento como CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
    (Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
  Exit;

O que acontece se se esquecer o CRYPT_EXPORTABLE?

Remova-se CRYPT_EXPORTABLE desse mesmo valor de flags e o CryptGenKey continua a ter sucesso, mas marca a chave privada gerada como não exportável ao nível do CSP. Tudo a jusante continua também a reportar sucesso: o CertCreateSelfSignCertificate devolve um contexto de certificado válido, e o PFXExportCertStoreEx, mesmo chamado com EXPORT_PRIVATE_KEYS, tem sucesso de qualquer forma e escreve um ficheiro PFX que abre, interpreta, e parece inteiramente normal. O que não contém é a chave privada, porque o CSP recusou deixá-la sair do contentor de chaves, e o PFXExportCertStoreEx nunca trata essa recusa como motivo para falhar a exportação inteira

A falha só se manifesta mais tarde, e num sítio completamente diferente: uma chamada de assinatura abre esse PFX, encontra um certificado sem qualquer chave privada associada, e reporta exatamente o erro que se obteria de um PFX corrompido ou errado, não de uma flag em falta três camadas a montante. Quem depure apenas a partir do lado da assinatura pode gastar uma tarde inteira no ficheiro errado antes de perceber que o erro real é um único bit em falta no momento de geração da chave, numa chamada de função completamente diferente, possivelmente num script de compilação completamente diferente

Diagrama da PDF Library for Delphi da cascata de falhas silenciosas desencadeada pela omissão de CRYPT_EXPORTABLE, em que a geração de chaves, a criação do certificado e a exportação PFX reportam sucesso até uma chamada de assinatura posterior não encontrar chave privada dentro do PFX
Omitir CRYPT_EXPORTABLE produz um PFX de aparência normal cuja chave privada em falta só vem à superfície durante a assinatura

Porque é que o ProvType tem de corresponder entre CryptAcquireContextW e o certificado?

O ProvType tem de corresponder porque o CertCreateSelfSignCertificate resolve a chave privada do novo certificado através de um registo CRYPT_KEY_PROV_INFO, e um campo desse registo, ProvType, tem de nomear exatamente o mesmo valor de tipo de CSP passado a CryptAcquireContextW quando o contentor de chaves foi aberto, PROV_RSA_AES, numericamente 24, na implementação do PDF Library for Delphi. Se se definir ProvType como zero, ou como qualquer constante de provedor diferente daquela a que o contentor efetivamente pertence, o certificado ainda pode ser criado, mas a sua ligação registada de volta à chave privada deixa de se resolver para o contentor que a detém, o que se manifesta mais tarde como uma falha de assinatura ou de exportação que nada tem a ver com o conteúdo criptográfico real do certificado

// O tipo de provedor usado para abrir o contentor de chaves tem de corresponder ao
// tipo de provedor registado na informação de key-provider do certificado.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... gera a chave, constrói o blob do nome do sujeito e depois:
KeyProvInfo.ProvType := PROV_RSA_AES;   // mesma constante, em ambos os locais de chamada

Juntando tudo: de um contentor GUID a um PFX protegido por palavra-passe

A cadeia de chamadas dentro de PLCreateSelfSignedCertificate segue uma linha direta: abre um contentor de chaves novo, nomeado a partir de um GUID recém-gerado, para que execuções de CI concorrentes nunca colidam sobre nomes de contentor, gera o par de chaves RSA dentro dele com as duas flags acima abordadas, codifica SubjectName num blob de nome X.500 através de CertStrToNameW, e chama CertCreateSelfSignCertificate com uma janela de validade calculada a partir de ValidDays e entregue como uma simples estrutura no formato SYSTEMTIME. O contexto de certificado resultante entra num repositório de certificados em memória aberto com CertOpenStore e CERT_STORE_PROV_MEMORY, apenas para que o PFXExportCertStoreEx tenha um repositório a partir do qual exportar, já que essa API funciona contra um handle de repositório em vez de um mero contexto de certificado

// Cada chamada abre um contentor descartável nomeado a partir de um GUID novo:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... gera a chave, autoassina o certificado, exporta o PFX ...
// depois apaga o contentor assim que o PFX detiver a sua própria cópia da chave:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_DELETEKEYSET);

O próprio PFXExportCertStoreEx segue a convenção comum de duas passagens do Win32: chamar-se uma vez com um buffer de comprimento zero para saber quantos bytes o PFX precisa, alocar essa quantidade, e chamar-se de novo para preencher o buffer. Assim que os bytes estão no disco, o PDF Library for Delphi apaga o contentor de chaves descartável com CRYPT_DELETEKEYSET em vez de o deixar para trás, porque o PFX já transporta a sua própria cópia de cada byte do material de chave que o contentor detinha. Se se saltar essa limpeza, cada chamada a PLCreateSelfSignedCertificate deixa para trás um contentor de chaves órfão, nomeado por GUID, no perfil do utilizador chamador, exatamente o tipo de fuga que um agente de CI que corra esta função em cada compilação vai acumular ao longo de meses antes de alguém reparar

Diagrama da PDF Library for Delphi da cadeia de chamadas de PLCreateSelfSignedCertificate, de um contentor de chaves com nome GUID através de CryptGenKey, CertStrToNameW e CertCreateSelfSignCertificate com ProvType correspondente, até uma exportação PFX em duas passagens, seguida da eliminação do contentor
Um contentor GUID descartável alimenta uma cadeia CryptoAPI direta que termina no PFX e é eliminada imediatamente depois

É seguro usar um certificado autoassinado para assinatura em produção?

Não: um certificado autoassinado é seguro para exercitar um caminho de código de assinatura e inseguro para uma assinatura em que se espera que alguém fora da equipa confie, porque nada o liga de volta a uma raiz em que o software de uma parte confiante já confie. O passo seguinte natural para um PFX assim é uma chamada de assinatura real, abordada em construir uma bancada de conformidade e assinatura em Delphi com o PDF Library for Delphi, onde um PFX construído desta forma conduz a metade de assinatura de um pipeline que também corre preflight PDF/A e auditorias de ByteRange. A assinatura, contudo, é apenas metade do que rodeia um certificado, e a outra metade é exatamente onde uma folha autoassinada deve falhar: assinatura e validação PAdES em Delphi com o PDF Library for Delphi aborda as verificações de cadeia de confiança que um validador de conformidade corre, e um validador que percorre a cadeia de volta até uma raiz de confiança não tem qualquer motivo para confiar num certificado que esta função inventou há cinco minutos a partir do nada

O PLCreateSelfSignedCertificate é uma função entre as APIs de certificados e assinatura na biblioteca de PDF PDF Library for Delphi para Delphi e C++Builder, e existe exatamente para a lacuna aqui descrita: um teste de assinatura que precisa de um par de chaves real por trás e de nada externo para o gerar