Funcția PLCreateSelfSignedCertificate a PDFlibPas construiește un certificat RSA/SHA-256 autosemnat și îl exportă, cheie privată inclusă, direct într-un fișier PFX protejat prin parolă, folosind nimic altceva decât Win32 CryptoAPI deja instalat pe fiecare mașină Windows. Niciun instrument extern, nicio autoritate de certificare, niciun pas manual makecert sau OpenSSL: un apel de funcție, un certificat suficient de bun pentru a conduce un test de semnare
Scenariul care face această funcție să merite avută este aproape întotdeauna o conductă CI. Un test de fum de semnare are nevoie de un PFX real cu o cheie privată reală în spate, iar înregistrarea unuia în depozit este propria sa problemă de securitate, întrucât o cheie privată commisă este o cheie privată expusă din momentul în care acel commit aterizează. Apelarea externă a makecert.exe sau o invocare OpenSSL dintr-un script de build funcționează de asemenea, dar apoi conducta depinde de un instrument care trebuie instalat, găsit pe PATH, și menținut consistent ca versiune pe fiecare agent de build. Generarea certificatului în interiorul aceluiași proces care rulează testul, cu aceleași apeluri Win32 CryptoAPI pe care Windows le livrează deja, elimină complet acea dependență
Ce produce de fapt PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate produce un fișier PFX protejat prin parolă care conține un certificat RSA autosemnat și cheia sa privată, semnat cu sha256RSA, condus de cinci parametri: SubjectName, PFXFileName, PFXPassword, ValidDays și KeyBits, și returnează un simplu steag de succes Boolean. SubjectName acceptă un șir X.500 complet precum 'CN=Alice, O=Example', iar un nume simplu fără niciun semn = în el este prefixat automat cu CN=. ValidDays sub 1 revine implicit la 365, iar KeyBits în afara intervalului 1024 la 16384 revine implicit la 2048. PDFlibPas a livrat această funcție încă de la v3.224.0, accesibilă nu doar din unitatea Delphi, ci și prin suprafețele DLL și ActiveX, iar propriul său comentariu de documentație este direct despre unde încetează să fie util: fiecare vizualizator popular marchează un certificat autosemnat ca nesigur, dacă cineva nu îl instalează explicit, așa că tratați ce produce ca un certificat pentru exercitarea unei căi de cod, nu o semnătură pe care oricine din afara echipei dvs. ar trebui rugat să se bazeze
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;
De ce codifică CryptGenKey lungimea cheii în parametrul de steaguri?
CryptGenKey împachetează două setări fără legătură într-un singur parametru dwFlags. Cuvântul inferior poartă steaguri de comportament, CRYPT_EXPORTABLE printre ele, în timp ce cuvântul superior, pentru o cheie de schimb de chei RSA, poartă lungimea de cheie cerută în biți. Transmiterea lui 2048 ca și cum ar fi doar un alt steag îl plasează în cuvântul inferior în schimb, unde nu se potrivește cu niciun steag de comportament pe care îl definește CryptoAPI, așa că apelul generează o cheie la orice lungime implicită revine furnizorul, nu lungimea la care apelantul credea că a cerut. Obținerea unei chei RSA efective de 2048 de biți înseamnă deplasarea numărului în cuvântul superior mai întâi
// 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;
Ce se întâmplă dacă uitați CRYPT_EXPORTABLE?
Eliminați CRYPT_EXPORTABLE din aceeași valoare de steaguri, iar CryptGenKey tot reușește, dar marchează cheia privată generată ca neexportabilă la nivel CSP. Tot ce urmează continuă de asemenea să raporteze succes: CertCreateSelfSignCertificate returnează un context de certificat valid, iar PFXExportCertStoreEx, chiar apelat cu EXPORT_PRIVATE_KEYS, reușește oricum și scrie un fișier PFX care se deschide, se analizează, și arată complet obișnuit. Ce nu conține este cheia privată, pentru că CSP-ul a refuzat să o lase să iasă din containerul de chei, iar PFXExportCertStoreEx nu tratează niciodată acel refuz ca un motiv pentru a eșua întregul export
Eșecul apare abia mai târziu, și cu totul altundeva: un apel de semnare deschide acel PFX, găsește un certificat fără nicio cheie privată atașată, și raportează exact eroarea pe care ați obține-o dintr-un PFX corupt sau greșit, nu dintr-un steag lipsă trei straturi mai sus. Oricine depanează doar din partea de semnare poate arde o după-amiază pe fișierul greșit înainte de a realiza că bug-ul efectiv este un singur bit lipsă la momentul generării cheii, într-un apel de funcție complet diferit, posibil într-un script de build complet diferit
De ce trebuie ca ProvType să se potrivească între CryptAcquireContextW și certificat?
ProvType trebuie să se potrivească pentru că CertCreateSelfSignCertificate rezolvă cheia privată a noului certificat printr-o înregistrare CRYPT_KEY_PROV_INFO, iar un câmp din acea înregistrare, ProvType, trebuie să numească exact aceeași valoare de tip CSP transmisă lui CryptAcquireContextW atunci când a fost deschis containerul de chei, PROV_RSA_AES, numeric 24, în implementarea PDFlibPas. Setați ProvType la zero, sau la orice constantă de furnizor alta decât cea căreia îi aparține efectiv containerul, iar certificatul tot poate fi creat, dar legătura sa înregistrată înapoi la cheia privată nu se mai rezolvă la containerul care o deține, ceea ce apare mai târziu ca un eșec de semnare sau export care nu are nimic de-a face cu conținutul criptografic real al certificatului
// 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
Punând totul cap la cap: de la container GUID la PFX protejat prin parolă
Lanțul de apeluri din interiorul PLCreateSelfSignedCertificate urmează o singură linie dreaptă, deschizând un container de chei proaspăt numit după un GUID nou generat, astfel încât rulările CI concurente nu se ciocnesc niciodată pe nume de container, generând perechea de chei RSA în interiorul lui cu cele două steaguri acoperite mai sus, codificând SubjectName într-un blob de nume X.500 prin CertStrToNameW, și apelând CertCreateSelfSignCertificate cu o fereastră de valabilitate calculată din ValidDays și predată ca o structură simplă în formă SYSTEMTIME. Contextul de certificat rezultat merge într-un magazin de certificate în memorie deschis cu CertOpenStore și CERT_STORE_PROV_MEMORY, pur pentru că PFXExportCertStoreEx are nevoie de un magazin din care să exporte, întrucât acel API lucrează pe un handle de magazin, nu pe un simplu context de certificat
// 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 însuși urmează convenția obișnuită Win32 în două treceri: îl apelați o dată cu un buffer de lungime zero pentru a afla câți octeți are nevoie PFX-ul, alocați atât, apoi îl apelați din nou pentru a umple bufferul. Odată ce octeții sunt pe disc, PDFlibPas șterge containerul de chei de unică folosință cu CRYPT_DELETEKEYSET în loc să îl lase în urmă, pentru că PFX-ul deja poartă propria sa copie a fiecărui octet de material de cheie deținut de container. Săriți peste acea curățare, iar fiecare apel la PLCreateSelfSignedCertificate lasă un container de chei orfelinizat, numit după GUID, așezat în profilul utilizatorului apelant, ceea ce este exact genul de scurgere pe care un agent CI care rulează această funcție la fiecare build o va acumula timp de luni întregi înainte ca cineva să observe
Este sigur de folosit un certificat autosemnat pentru semnarea de producție?
Nu: un certificat autosemnat este sigur pentru exercitarea unei căi de cod de semnare și nesigur pentru o semnătură pe care oricine din afara echipei este de așteptat să o aibă încredere, pentru că nimic nu îl leagă înapoi de o rădăcină în care software-ul unei părți relevante are deja încredere. Pasul următor natural pentru un PFX construit astfel este un apel de semnare efectiv, acoperit în construirea unei bănci de lucru de conformitate și semnare în Delphi cu PDFlibPas, unde un PFX construit astfel conduce jumătatea de semnare a unei conducte care de asemenea rulează preflight PDF/A și audituri ByteRange. Semnarea este doar jumătate din ce stă în jurul unui certificat, totuși, iar cealaltă jumătate este exact unde un frunziș autosemnat ar trebui să eșueze: semnarea și validarea PAdES în Delphi cu PDFlibPas acoperă verificările de lanț-de-încredere pe care le rulează un validator de conformitate, iar un validator care parcurge lanțul înapoi la o rădăcină de încredere nu are niciun motiv să aibă încredere într-un certificat pe care această funcție l-a inventat acum cinci minute din nimic
PLCreateSelfSignedCertificate este o funcție printre API-urile de certificat și semnare din biblioteca PDF PDFlibPas pentru Delphi și C++Builder, și există exact pentru golul descris aici: un test de semnare care are nevoie de o pereche de chei reală în spate și nimic extern pentru a genera una