Artigo Técnico

Certificados de Teste Autoassinados em Delphi com CryptoAPI

A função PLCreateSelfSignedCertificate do PDFlibPas 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 PDFlibPas 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=PDFlibPas 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

// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as 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

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 PDFlibPas. 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

// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES;   // same constant, both call sites

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

// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
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 PDFlibPas 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

É 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 PDFlibPas, 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 PDFlibPas 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 PDFlibPas 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