Articol tehnic

Certificate de test autosemnate în Delphi cu CryptoAPI

Funcția PLCreateSelfSignedCertificate a PDF Library for Delphi 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. PDF Library for Delphi 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=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;

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

Diagramă PDF Library for Delphi a parametrului dwFlags din CryptGenKey, împărțit într-un cuvânt înalt care poartă KeyBits shl 16 ca lungime a cheii RSA și un cuvânt jos care poartă fanioane de comportament precum CRYPT_EXPORTABLE
Un 2048 nud aterizează în jumătatea de indicatori de comportament, deci furnizorul revine discret la lungimea sa implicită de cheie
// Lungimea cheii se află în cei 16 biți superiori ai steagurilor CryptGenKey;
// cuvântul inferior poartă steaguri de comportament precum 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

Diagramă PDF Library for Delphi a cascadei de eșec silențios declanșate de omiterea CRYPT_EXPORTABLE, în care generarea cheii, crearea certificatului și exportul PFX raportează toate succesul, până când un apel de semnare ulterior nu găsește nicio cheie privată în PFX
Omiterea CRYPT_EXPORTABLE produce un PFX cu aspect obișnuit a cărui cheie privată lipsă iese la iveală doar în timpul semnării

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 PDF Library for Delphi. 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

// Tipul de furnizor folosit pentru a deschide containerul de chei trebuie să corespundă
// tipului de furnizor înregistrat în informațiile de furnizor-de-chei ale certificatului.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... se generează cheia, se construiește blob-ul de nume subiect, apoi:
KeyProvInfo.ProvType := PROV_RSA_AES;   // aceeași constantă, ambele locuri de apel

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

// Fiecare apel deschide un container de unică folosință denumit după un GUID nou:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... se generează cheia, se autosemnează certificatul, se exportă PFX-ul ...
// apoi se șterge containerul odată ce PFX-ul deține propria copie a cheii:
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, PDF Library for Delphi ș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

Diagramă PDF Library for Delphi a lanțului de apeluri PLCreateSelfSignedCertificate, de la un container de chei cu nume GUID prin CryptGenKey, CertStrToNameW și CertCreateSelfSignCertificate cu ProvType corespunzător, către un export PFX în două treceri, urmat de ștergerea containerului
Un container GUID de unică folosință alimentează un lanț CryptoAPI direct care se termină în PFX și este șters imediat după

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 PDF Library for Delphi, 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 PDF Library for Delphi 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 PDF Library for Delphi 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