La funzione PLCreateSelfSignedCertificate di PDFlibPas costruisce un certificato RSA/SHA-256 autofirmato e lo esporta, chiave privata inclusa, direttamente in un file PFX protetto da password, usando null'altro che la Win32 CryptoAPI già installata su ogni macchina Windows. Nessuno strumento esterno, nessuna autorità di certificazione, nessun passaggio manuale con makecert o OpenSSL: una chiamata di funzione, un certificato abbastanza buono da far girare un test di firma
Lo scenario che rende questa funzione utile da avere è quasi sempre una pipeline CI. Un test di fumo per la firma ha bisogno di un vero PFX con dietro una vera chiave privata, e includerne uno nel repository è di per sé un problema di sicurezza, poiché una chiave privata committata è una chiave privata trapelata dal momento in cui quel commit atterra. Invocare makecert.exe o un'esecuzione OpenSSL da uno script di build funziona anch'esso, ma allora la pipeline dipende da uno strumento che deve essere installato, trovato nel PATH, e mantenuto coerente per versione su ogni agente di build. Generare il certificato dentro lo stesso processo che esegue il test, con le stesse chiamate Win32 CryptoAPI che Windows già distribuisce, rimuove del tutto quella dipendenza
Cosa produce realmente PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate produce un file PFX protetto da password che contiene un certificato RSA autofirmato e la sua chiave privata, firmato con sha256RSA, guidato da cinque parametri: SubjectName, PFXFileName, PFXPassword, ValidDays, e KeyBits, e restituisce un semplice flag Boolean di successo. SubjectName accetta una stringa X.500 completa come 'CN=Alice, O=Example', e un nome semplice senza alcun segno = al suo interno viene automaticamente preceduto da CN=. ValidDays sotto 1 ricade su 365, e KeyBits fuori dall'intervallo da 1024 a 16384 ricade su 2048. PDFlibPas distribuisce questa funzione dalla v3.224.0, raggiungibile non solo dall'unit Delphi ma anche attraverso le superfici DLL e ActiveX, e il proprio commento di documentazione è schietto su dove smette di essere utile: ogni visualizzatore mainstream marca un certificato autofirmato come non affidabile a meno che qualcuno non lo installi esplicitamente, quindi tratta ciò che produce come un certificato per esercitare un percorso di codice, non come una firma su cui chiunque al di fuori del tuo team dovrebbe fare affidamento
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;
Perché CryptGenKey codifica la lunghezza della chiave nel parametro flags?
CryptGenKey impacchetta due impostazioni non correlate in un singolo parametro dwFlags. La word bassa porta flag di comportamento, CRYPT_EXPORTABLE tra questi, mentre la word alta, per una chiave di scambio chiave RSA, porta la lunghezza di chiave richiesta in bit. Passare 2048 come se fosse solo un altro flag lo fa atterrare nella word bassa invece, dove non corrisponde a nessun flag di comportamento che CryptoAPI definisca, quindi la chiamata genera una chiave a qualunque lunghezza predefinita su cui il provider ricada invece della lunghezza che il chiamante pensava di aver richiesto. Ottenere una vera chiave RSA a 2048 bit significa spostare prima il numero nella word 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;
Cosa succede se dimentichi CRYPT_EXPORTABLE?
Elimina CRYPT_EXPORTABLE da quello stesso valore di flag e CryptGenKey continua comunque a riuscire, ma marca la chiave privata generata come non esportabile a livello di CSP. Tutto ciò che segue continua anch'esso a segnalare successo: CertCreateSelfSignCertificate restituisce un contesto certificato valido, e PFXExportCertStoreEx, anche chiamato con EXPORT_PRIVATE_KEYS, riesce comunque e scrive un file PFX che si apre, si analizza, e sembra del tutto ordinario. Ciò che non contiene è la chiave privata, perché il CSP ha rifiutato di lasciarla uscire dal proprio contenitore chiave, e PFXExportCertStoreEx non tratta mai quel rifiuto come motivo per far fallire l'intera esportazione
Il fallimento si manifesta solo più tardi, e in un punto del tutto diverso: una chiamata di firma apre quel PFX, trova un certificato senza chiave privata allegata, e segnala esattamente l'errore che otterresti da un PFX corrotto o sbagliato, non da un flag mancante tre livelli più a monte. Chiunque stia facendo debug solo dal lato firma può bruciare un pomeriggio sul file sbagliato prima di rendersi conto che il vero bug è un singolo bit mancante al momento della generazione della chiave, in una chiamata di funzione completamente diversa, magari in uno script di build completamente diverso
Perché ProvType deve corrispondere tra CryptAcquireContextW e il certificato?
ProvType deve corrispondere perché CertCreateSelfSignCertificate risolve la chiave privata del nuovo certificato tramite un record CRYPT_KEY_PROV_INFO, e un campo in quel record, ProvType, deve nominare esattamente lo stesso valore di tipo CSP passato a CryptAcquireContextW quando il contenitore chiave è stato aperto, PROV_RSA_AES, numericamente 24, nell'implementazione di PDFlibPas. Imposta ProvType a zero, o a qualsiasi costante di provider diversa da quella a cui il contenitore effettivamente appartiene, e il certificato può comunque essere creato, ma il suo collegamento registrato di ritorno alla chiave privata non si risolve più nel contenitore che la detiene, il che emerge più tardi come un fallimento di firma o esportazione che non ha nulla a che fare con il contenuto crittografico effettivo del certificato
// 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
Mettere insieme i pezzi: dal contenitore GUID al PFX protetto da password
La catena di chiamate dentro PLCreateSelfSignedCertificate segue una linea diritta, aprendo un contenitore chiave nuovo di zecca nominato secondo un GUID appena generato cosicché esecuzioni CI concorrenti non collidano mai su nomi di contenitore, generando la coppia di chiavi RSA al suo interno con i due flag trattati sopra, codificando SubjectName in un blob di nome X.500 tramite CertStrToNameW, e chiamando CertCreateSelfSignCertificate con una finestra di validità calcolata da ValidDays e passata come una semplice struttura in forma SYSTEMTIME. Il contesto certificato risultante entra in uno store certificati in memoria aperto con CertOpenStore e CERT_STORE_PROV_MEMORY, puramente cosicché PFXExportCertStoreEx abbia uno store da cui esportare, poiché quell'API lavora contro un handle di store piuttosto che un semplice contesto certificato
// 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 stessa segue la normale convenzione Win32 a due passaggi: chiamala una volta con un buffer di lunghezza zero per sapere quanti byte il PFX richiede, allocane altrettanti, poi chiamala di nuovo per riempire il buffer. Una volta che i byte sono su disco, PDFlibPas elimina il contenitore chiave usa-e-getta con CRYPT_DELETEKEYSET invece di lasciarlo indietro, perché il PFX porta già la propria copia di ogni byte del materiale chiave che il contenitore deteneva. Salta quella pulizia e ogni chiamata a PLCreateSelfSignedCertificate lascia un contenitore chiave orfano, nominato con GUID, seduto nel profilo dell'utente chiamante, che è esattamente il tipo di perdita che un agente CI che esegue questa funzione su ogni build accumulerà per mesi prima che qualcuno se ne accorga
È sicuro usare un certificato autofirmato per la firma in produzione?
No: un certificato autofirmato è sicuro per esercitare un percorso di codice di firma e non sicuro per una firma su cui chiunque al di fuori del team sia atteso fidarsi, perché nulla lo ricollega a una radice di cui il software di una parte affidante già si fidi. Il passo successivo naturale per un PFX come questo è una vera chiamata di firma, trattata in costruire un banco di lavoro di conformità e firma in Delphi con PDFlibPas, dove un PFX costruito in questo modo guida la metà di firma di una pipeline che esegue anche preflight PDF/A e audit ByteRange. La firma è però solo metà di ciò che ruota attorno a un certificato, e l'altra metà è esattamente dove una foglia autofirmata dovrebbe fallire: la firma e validazione PAdES in Delphi con PDFlibPas tratta i controlli di catena di fiducia che un validatore di conformità esegue, e un validatore che risale la catena fino a una radice fidata non ha motivo di fidarsi di un certificato che questa funzione ha inventato cinque minuti fa dal nulla
PLCreateSelfSignedCertificate è una funzione tra le API di certificati e firma nella libreria PDF PDFlibPas per Delphi e C++Builder, ed esiste esattamente per la lacuna descritta qui: un test di firma che ha bisogno di una vera coppia di chiavi dietro di sé e nulla di esterno per generarne una