A função PLCreateSelfSignedCertificate do PDFlibPas constrói um certificado RSA/SHA-256 autoassinado e o exporta, chave privada incluída, direto para um arquivo PFX protegido por senha, usando nada além da CryptoAPI Win32 já instalada em toda máquina Windows. Nenhuma ferramenta externa, nenhuma autoridade certificadora, nenhuma etapa manual de makecert ou OpenSSL: uma chamada de função, um certificado bom o bastante para conduzir um teste de assinatura
O cenário que torna essa função valiosa é quase sempre um pipeline de CI. Um teste de fumaça de assinatura precisa de um PFX real com uma chave privada real por trás dele, e registrar um no repositório é seu próprio problema de segurança, já que uma chave privada commitada é uma chave privada vazada a partir do momento em que esse commit chega. Chamar makecert.exe ou uma invocação OpenSSL a partir de um script de build também funciona, mas então o pipeline depende de uma ferramenta que precisa ser instalada, encontrada no PATH, e mantida consistente em versão em todo agente de build. Gerar o certificado dentro do mesmo processo que roda o teste, com as mesmas chamadas Win32 CryptoAPI que o Windows já distribui, remove essa dependência por completo
O que o PLCreateSelfSignedCertificate de fato produz?
PLCreateSelfSignedCertificate produz um arquivo PFX protegido por senha contendo um certificado RSA autoassinado e sua chave privada, assinado com sha256RSA, conduzido por cinco parâmetros: SubjectName, PFXFileName, PFXPassword, ValidDays, e KeyBits, e retorna uma simples flag booleana de sucesso. SubjectName aceita uma string X.500 completa como 'CN=Alice, O=Example', e um nome simples sem nenhum sinal = nele recebe automaticamente o prefixo CN=. ValidDays abaixo de 1 recai para 365, e KeyBits fora da faixa de 1024 a 16384 recai para 2048. O PDFlibPas distribui essa função desde a v3.224.0, alcançável não apenas da unit Delphi, mas também através das superfícies DLL e ActiveX, e seu próprio comentário de documentação é direto sobre onde ela para de ser útil: todo visualizador comum marca um certificado autoassinado como não confiável, a menos que alguém o instale explicitamente, então trate o que ela produz como um certificado para exercitar um caminho de código, não uma assinatura em que qualquer um fora de sua equipe deveria ser pedido para confiar
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;
Por que o CryptGenKey codifica o comprimento da chave no parâmetro de flags?
CryptGenKey empacota duas configurações não relacionadas em um único parâmetro dwFlags. A palavra baixa carrega flags de comportamento, CRYPT_EXPORTABLE entre elas, enquanto a palavra alta, para uma chave de troca de chave RSA, carrega o comprimento de chave solicitado em bits. Passar 2048 como se fosse apenas outra flag o coloca na palavra baixa, em vez disso, onde não corresponde a nenhuma flag de comportamento que a CryptoAPI define, de modo que a chamada gera uma chave em qualquer comprimento padrão para o qual o provedor recai, em vez do comprimento que quem chama achava ter pedido. Obter uma chave RSA de fato de 2048 bits significa deslocar o número para a palavra alta primeiro
// 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 você esquecer o CRYPT_EXPORTABLE?
Retire CRYPT_EXPORTABLE desse mesmo valor de flags e CryptGenKey ainda tem sucesso, mas marca a chave privada gerada como não exportável no nível do CSP. Tudo rio abaixo também continua reportando sucesso: CertCreateSelfSignCertificate retorna um contexto de certificado válido, e PFXExportCertStoreEx, mesmo chamado com EXPORT_PRIVATE_KEYS, tem sucesso de qualquer forma e escreve um arquivo PFX que abre, analisa, e parece completamente comum. O que ele não contém é a chave privada, porque o CSP se recusou a deixá-la sair do contêiner de chave, e PFXExportCertStoreEx nunca trata essa recusa como motivo para falhar toda a exportação
A falha só aparece depois, e em outro lugar completamente diferente: uma chamada de assinatura abre esse PFX, encontra um certificado sem nenhuma chave privada anexada, e reporta exatamente o erro que você obteria de um PFX corrompido ou errado, não de uma flag ausente três camadas rio acima. Qualquer um depurando apenas pelo lado da assinatura pode queimar uma tarde inteira no arquivo errado antes de perceber que o bug real é um único bit ausente no momento da geração de chave, em uma chamada de função completamente diferente, possivelmente em um script de build completamente diferente
Por que o ProvType precisa corresponder entre CryptAcquireContextW e o certificado?
ProvType precisa corresponder porque CertCreateSelfSignCertificate resolve a chave privada do novo certificado por meio de um registro CRYPT_KEY_PROV_INFO, e um campo nesse registro, ProvType, precisa nomear exatamente o mesmo valor de tipo de CSP passado a CryptAcquireContextW quando o contêiner de chave foi aberto, PROV_RSA_AES, numericamente 24, na implementação do PDFlibPas. Defina ProvType como zero, ou como qualquer constante de provedor diferente daquela a que o contêiner de fato pertence, e o certificado ainda pode ser criado, mas seu link registrado de volta para a chave privada não se resolve mais para o contêiner que a detém, o que aparece depois como uma falha de assinatura ou exportação que não tem nada 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: do contêiner GUID ao PFX protegido por senha
A cadeia de chamadas dentro de PLCreateSelfSignedCertificate segue uma linha reta, abrindo um contêiner de chave novo nomeado a partir de um GUID recém-gerado, para que execuções de CI concorrentes nunca colidam em nomes de contêiner, gerando o par de chaves RSA dentro dele com as duas flags cobertas acima, codificando SubjectName em um blob de nome X.500 por meio de CertStrToNameW, e chamando CertCreateSelfSignCertificate com uma janela de validade calculada a partir de ValidDays e entregue como uma estrutura simples no formato SYSTEMTIME. O contexto de certificado resultante vai para um armazenamento de certificado em memória aberto com CertOpenStore e CERT_STORE_PROV_MEMORY, puramente para que PFXExportCertStoreEx tenha um armazenamento de onde exportar, já que essa API trabalha contra um handle de armazenamento, em vez de um contexto de certificado simples
// 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);
PFXExportCertStoreEx em si segue a convenção comum Win32 de duas passadas: chame-a uma vez com um buffer de comprimento zero para saber quantos bytes o PFX precisa, aloque esse tanto, depois chame-a novamente para preencher o buffer. Assim que os bytes estão em disco, o PDFlibPas exclui o contêiner de chave descartável com CRYPT_DELETEKEYSET, em vez de deixá-lo para trás, porque o PFX já carrega sua própria cópia de cada byte de material de chave que o contêiner detinha. Pule essa limpeza e toda chamada a PLCreateSelfSignedCertificate deixa um contêiner de chave órfão, nomeado por GUID, sentado no perfil do usuário chamador, que é exatamente o tipo de vazamento que um agente de CI rodando essa função em toda build vai acumular durante meses antes de alguém perceber
Um certificado autoassinado é seguro para usar em assinatura de produção?
Não: um certificado autoassinado é seguro para exercitar um caminho de código de assinatura e inseguro para uma assinatura em que qualquer um fora da equipe deveria confiar, porque nada o encadeia de volta a uma raiz que o software de uma parte confiante já confia. O próximo passo natural para um PFX assim é uma chamada de assinatura real, coberta em construindo uma bancada de conformidade e assinatura em Delphi com o PDFlibPas, onde um PFX construído dessa forma conduz a metade de assinatura de um pipeline que também roda preflight PDF/A e auditorias de ByteRange. Assinar, porém, é só metade do que fica em torno de um certificado, e a outra metade é exatamente onde uma folha autoassinada deveria falhar: assinatura e validação PAdES em Delphi com o PDFlibPas cobre as verificações de cadeia de confiança que um validador de conformidade roda, e um validador que percorre a cadeia de volta a uma raiz confiável não tem motivo para confiar em um certificado que essa função inventou cinco minutos atrás do nada
PLCreateSelfSignedCertificate é uma função entre as APIs de certificado e assinatura na biblioteca PDFlibPas para Delphi e C++Builder, e existe exatamente para a lacuna descrita aqui: um teste de assinatura que precisa de um par de chaves real por trás dele e nada externo para gerar um