Teknisk artikel

Selvsignerede testcertifikater i Delphi med CryptoAPI

PDFlibPas' PLCreateSelfSignedCertificate-funktion bygger et selvsigneret RSA/SHA-256-certifikat og eksporterer det, privat nøgle inkluderet, direkte ind i en adgangskode-beskyttet PFX-fil, ved brug af intet andet end Win32 CryptoAPI allerede installeret på hver Windows-maskine. Intet eksternt værktøj, ingen certifikatmyndighed, intet manuelt makecert- eller OpenSSL-trin: ét funktionskald, ét certifikat godt nok til at drive en signerings-test

Scenariet der gør denne funktion værd at have, er næsten altid en CI-pipeline. En signerings-røgtest har brug for en rigtig PFX med en rigtig privat nøgle bag sig, og at tjekke en ind i repositoryet er sit eget sikkerhedsproblem, da en committed privat nøgle er en lækket privat nøgle fra det øjeblik den commit lander. At kalde ud til makecert.exe eller et OpenSSL-kald fra et build-script fungerer også, men så afhænger pipelinen af et værktøj, der skal installeres, findes på PATH, og holdes versions-konsistent på tværs af hver build-agent. At generere certifikatet inde i den samme proces, der kører testen, med de samme Win32-CryptoAPI-kald Windows allerede leverer, fjerner den afhængighed helt

Hvad producerer PLCreateSelfSignedCertificate rent faktisk?

PLCreateSelfSignedCertificate producerer en adgangskode-beskyttet PFX-fil, der holder et selvsigneret RSA-certifikat og dets private nøgle, signeret med sha256RSA, drevet af fem parametre: SubjectName, PFXFileName, PFXPassword, ValidDays, og KeyBits, og den returnerer et rent Boolean-succesflag. SubjectName accepterer en fuld X.500-streng såsom 'CN=Alice, O=Example', og et rent navn uden noget =-tegn i sig præfikses automatisk med CN=. ValidDays under 1 falder tilbage til 365, og KeyBits uden for 1024-til-16384-intervallet falder tilbage til 2048. PDFlibPas har leveret denne funktion siden v3.224.0, tilgængelig ikke kun fra Delphi-unit'en, men også gennem DLL- og ActiveX-fladerne, og dens egen doc-kommentar er direkte om, hvor den holder op med at være nyttig: enhver mainstream fremviser markerer et selvsigneret certifikat som ikke-betroet, medmindre nogen installerer det eksplicit, så behandl hvad den producerer som et certifikat til at udøve en kodevej, ikke en signatur nogen uden for teamet bør bedes stole på

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;

Hvorfor koder CryptGenKey nøglelængden ind i flags-parameteren?

CryptGenKey pakker to urelaterede indstillinger ind i en enkelt dwFlags-parameter. Det lave ord bærer opførsels-flag, CRYPT_EXPORTABLE iblandt dem, mens det høje ord, for en RSA-nøgle-udveksling-nøgle, bærer den anmodede nøglelængde i bits. At sende 2048, som var det bare endnu et flag, lander det i det lave ord i stedet, hvor det ikke matcher noget opførsels-flag CryptoAPI definerer, så kaldet genererer en nøgle ved hvilken som helst standard-længde, udbyderen falder tilbage til, frem for den længde, kalderen troede, den bad om. At få en faktisk 2048-bit RSA-nøgle betyder at skifte tallet ind i det høje ord først

// 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;

Hvad sker der, hvis man glemmer CRYPT_EXPORTABLE?

Drop CRYPT_EXPORTABLE fra den samme flags-værdi, og CryptGenKey lykkes stadig, men den markerer den genererede private nøgle ikke-eksporterbar på CSP-niveau. Alt nedstrøms bliver ved med at rapportere succes også: CertCreateSelfSignCertificate returnerer en gyldig certifikat-kontekst, og PFXExportCertStoreEx, selv kaldt med EXPORT_PRIVATE_KEYS, lykkes alligevel og skriver en PFX-fil, der åbner, parser og ser helt almindelig ud. Hvad den ikke indeholder, er den private nøgle, fordi CSP'en nægtede at lade den slippe ud af nøglecontaineren, og PFXExportCertStoreEx behandler aldrig den nægtelse som en grund til at lade hele eksporten fejle

Fejlen viser sig først senere, og et helt andet sted: et signerings-kald åbner den PFX, finder et certifikat med ingen privat nøgle vedhæftet, og rapporterer præcis den fejl, man ville få fra en korrupt eller forkert PFX, ikke fra et manglende flag tre lag opstrøms. Alle der fejlfinder fra signerings-siden alene, kan brænde en eftermiddag på den forkerte fil, før de indser, at den faktiske bug er én enkelt manglende bit ved nøglegenererings-tidspunktet, i et helt andet funktionskald, muligvis i et helt andet build-script

Hvorfor skal ProvType matche mellem CryptAcquireContextW og certifikatet?

ProvType skal matche, fordi CertCreateSelfSignCertificate løser det nye certifikats private nøgle gennem en CRYPT_KEY_PROV_INFO-record, og ét felt i den record, ProvType, skal navngive nøjagtig samme CSP-type-værdi sendt til CryptAcquireContextW, da nøgle-containeren blev åbnet, PROV_RSA_AES, numerisk 24, i PDFlibPas' implementering. Sæt ProvType til nul, eller til en hvilken som helst udbyder-konstant end den, containeren rent faktisk tilhører, og certifikatet kan stadig blive oprettet, men dets registrerede link tilbage til den private nøgle løser ikke længere til den container, der holder den, hvilket dukker op senere som en signerings- eller eksport-fejl, der intet har at gøre med certifikatets faktiske kryptografiske indhold

// 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

At samle det: fra GUID-container til adgangskode-beskyttet PFX

Kaldkæden inde i PLCreateSelfSignedCertificate følger én lige linje: åbner en frisk nøgle-container navngivet efter et nygenereret GUID, så samtidige CI-kørsler aldrig kolliderer over container-navne, genererer RSA-nøgleparret indeni den med de to flag dækket ovenfor, koder SubjectName ind i en X.500-navn-blob gennem CertStrToNameW, og kalder CertCreateSelfSignCertificate med et gyldigheds-vindue beregnet fra ValidDays og overdraget som en almindelig SYSTEMTIME-formet struktur. Den resulterende certifikat-kontekst går ind i et hukommelses-certifikat-lager åbnet med CertOpenStore og CERT_STORE_PROV_MEMORY, rent for at PFXExportCertStoreEx har et lager at eksportere fra, da det API arbejder mod et lager-handle frem for en bar certifikat-kontekst

// 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 selv følger den almindelige Win32-to-pass-konvention: kald den én gang med en nul-længde-buffer for at lære, hvor mange bytes PFX'en har brug for, allokér det, kald den derefter igen for at fylde bufferen. Når bytene er på disk, sletter PDFlibPas engangs-nøgle-containeren med CRYPT_DELETEKEYSET frem for at efterlade den, fordi PFX'en allerede bærer sin egen kopi af hver byte af nøglemateriale, containeren holdt. Spring den oprydning over, og hvert kald til PLCreateSelfSignedCertificate efterlader en forældreløs, GUID-navngivet nøgle-container siddende i den kaldende brugers profil, hvilket er præcis den slags lækage, en CI-agent der kører denne funktion ved hver build, vil akkumulere over måneder, før nogen bemærker det

Er et selvsigneret certifikat sikkert at bruge til produktions-signering?

Nej: et selvsigneret certifikat er sikkert til at udøve en signerings-kodevej og usikkert til en signatur, nogen uden for teamet forventes at stole på, fordi intet kæder det tilbage til en rod, en tillidsgivende parts software allerede stoler på. Det naturlige næste skridt for en PFX som denne er et faktisk signerings-kald, dækket i at bygge en compliance- og signerings-workbench i Delphi med PDFlibPas, hvor en PFX bygget på denne måde driver signerings-halvdelen af en pipeline, der også kører PDF/A-preflight og ByteRange-audits. Signering er dog kun halvdelen af, hvad der sidder omkring et certifikat, og den anden halvdel er præcis, hvor en selvsigneret leaf skal fejle: PAdES-signering og -validering i Delphi med PDFlibPas dækker tillids-kæde-tjekkene en konformitets-validator kører, og en validator der går kæden tilbage til en betroet rod, har ingen grund til at stole på et certifikat, denne funktion opfandt for fem minutter siden ud af intet

PLCreateSelfSignedCertificate er én funktion blandt certifikat- og signerings-API'erne i PDFlibPas-PDF-biblioteket til Delphi og C++Builder, og den findes netop til hullet beskrevet her: en signerings-test der har brug for et rigtigt nøglepar bag sig og intet eksternt for at generere ét