Teknisk artikkel

Selvsignerte testsertifikater i Delphi med CryptoAPI

PDFlibPas' PLCreateSelfSignedCertificate-funksjon bygger et selvsignert RSA/SHA-256-sertifikat og eksporterer det, privatnøkkel inkludert, rett inn i en passordbeskyttet PFX-fil, ved bruk av ikke noe annet enn Win32 CryptoAPI allerede installert på hver Windows-maskin. Ikke noe eksternt verktøy, ingen sertifiseringsinstans, ikke noe manuelt makecert- eller OpenSSL-trinn: ett funksjonskall, ett sertifikat godt nok til å drive en signeringstest

Scenarioet som gjør denne funksjonen verdt å ha, er nesten alltid en CI-pipeline. En signerings-smoke-test trenger en ekte PFX med en ekte privatnøkkel bak seg, og å sjekke en inn i repositoryet er sitt eget sikkerhetsproblem, ettersom en committet privatnøkkel er en lekket privatnøkkel fra det øyeblikket den committen lander. Å skalle ut til makecert.exe eller et OpenSSL-kall fra et byggeskript fungerer også, men da avhenger pipelinen av et verktøy som må installeres, finnes på PATH, og holdes versjons-konsistent på tvers av hver byggeagent. Å generere sertifikatet inne i den samme prosessen som kjører testen, med de samme Win32-CryptoAPI-kallene Windows allerede leverer, fjerner den avhengigheten fullstendig

Hva produserer PLCreateSelfSignedCertificate egentlig?

PLCreateSelfSignedCertificate produserer en passordbeskyttet PFX-fil som holder et selvsignert RSA-sertifikat og privatnøkkelen dets, signert med sha256RSA, drevet av fem parametere: SubjectName, PFXFileName, PFXPassword, ValidDays, og KeyBits, og den returnerer et rent boolsk suksessflagg. SubjectName aksepterer en full X.500-streng slik som 'CN=Alice, O=Example', og et bart navn uten noe =-tegn i seg, prefikses automatisk med CN=. ValidDays under 1 faller tilbake til 365, og KeyBits utenfor 1024-til-16384-området faller tilbake til 2048. PDFlibPas har levert denne funksjonen siden v3.224.0, tilgjengelig ikke bare fra Delphi-enheten men også gjennom DLL- og ActiveX-overflatene, og sin egen doc-kommentar er brutalt ærlig om hvor den slutter å være nyttig: hver mainstream-fremviser markerer et selvsignert sertifikat som ikke-pålitelig med mindre noen installerer det eksplisitt, så behandle hva den produserer som et sertifikat for å utøve en kodevei, ikke en signatur noen utenfor teamet ditt bør bes om å 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økkellengden i flags-parameteren?

CryptGenKey pakker to urelaterte innstillinger inn i én enkelt dwFlags-parameter. Det nedre ordet bærer atferdsflagg, CRYPT_EXPORTABLE blant dem, mens det øvre ordet, for en RSA-nøkkelutvekslings-nøkkel, bærer den forespurte nøkkellengden i bit. Å sende 2048 som om det bare var enda et flagg, lander det i det nedre ordet i stedet, der det ikke matcher noe atferdsflagg CryptoAPI definerer, så kallet genererer en nøkkel ved hvilken som helst standardlengde leverandøren faller tilbake til, i stedet for lengden kalleren trodde den ba om. Å få en faktisk 2048-bit RSA-nøkkel betyr å skifte tallet inn i det øvre ordet 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;

Hva skjer hvis man glemmer CRYPT_EXPORTABLE?

Fjern CRYPT_EXPORTABLE fra den samme flags-verdien, og CryptGenKey lykkes fortsatt, men den markerer den genererte privatnøkkelen ikke-eksporterbar på CSP-nivå. Alt nedstrøms fortsetter å rapportere suksess også: CertCreateSelfSignCertificate returnerer en gyldig sertifikatkontekst, og PFXExportCertStoreEx, selv kalt med EXPORT_PRIVATE_KEYS, lykkes uansett og skriver en PFX-fil som åpner, parser, og ser helt vanlig ut. Hva den ikke inneholder, er privatnøkkelen, fordi CSP-en nektet å la den ut av nøkkelbeholderen, og PFXExportCertStoreEx behandler aldri det avslaget som en grunn til å feile hele eksporten

Feilen viser seg først senere, og et helt annet sted: et signeringskall åpner den PFX-en, finner et sertifikat uten noen privatnøkkel festet, og rapporterer nøyaktig den feilen man ville fått fra en korrupt eller feil PFX, ikke fra et manglende flagg tre lag oppstrøms. Alle som feilsøker fra signeringssiden alene, kan brenne en ettermiddag på feil fil før de innser at den faktiske bugen er én enkelt manglende bit ved nøkkelgenererings-tidspunktet, i et helt annet funksjonskall, kanskje i et helt annet byggeskript

Hvorfor må ProvType stemme overens mellom CryptAcquireContextW og sertifikatet?

ProvType må stemme overens fordi CertCreateSelfSignCertificate løser det nye sertifikatets privatnøkkel gjennom en CRYPT_KEY_PROV_INFO-record, og ett felt i den recorden, ProvType, må navngi nøyaktig den samme CSP-type-verdien sendt til CryptAcquireContextW da nøkkelbeholderen ble åpnet, PROV_RSA_AES, numerisk 24, i PDFlibPas' implementasjon. Sett ProvType til null, eller til en hvilken som helst leverandørkonstant annet enn den beholderen faktisk tilhører, og sertifikatet kan fortsatt opprettes, men den registrerte lenken tilbake til privatnøkkelen løses ikke lenger til beholderen som holder den, noe som viser seg senere som en signerings- eller eksportfeil som ikke har noe med sertifikatets faktiske kryptografiske innhold å gjøre

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

Å sette det sammen: fra GUID-beholder til passordbeskyttet PFX

Kallkjeden inne i PLCreateSelfSignedCertificate følger én rett linje, åpner en fersk nøkkelbeholder oppkalt etter en nylig generert GUID slik at samtidige CI-kjøringer aldri kolliderer om beholdernavn, genererer RSA-nøkkelparet inni den med de to flaggene dekket ovenfor, koder SubjectName inn i en X.500-navn-blob gjennom CertStrToNameW, og kaller CertCreateSelfSignCertificate med et gyldighetsvindu beregnet fra ValidDays og overlevert som en ren SYSTEMTIME-formet struktur. Den resulterende sertifikatkonteksten går inn i et sertifikatlager i minnet åpnet med CertOpenStore og CERT_STORE_PROV_MEMORY, rent og skjært slik at PFXExportCertStoreEx har et lager å eksportere fra, ettersom det API-et jobber mot et lagerhåndtak snarere enn en bar sertifikatkontekst

// 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 vanlige Win32-to-passerings-konvensjonen: kall den én gang med en null-lengde buffer for å lære hvor mange bytes PFX-en trenger, allokér det mye, kall den så igjen for å fylle bufferen. Når bytene er på disk, sletter PDFlibPas engangs-nøkkelbeholderen med CRYPT_DELETEKEYSET i stedet for å la den bli liggende, fordi PFX-en allerede bærer sin egen kopi av hver byte nøkkelmateriale beholderen holdt. Hopp over den opprydningen, og hvert kall til PLCreateSelfSignedCertificate etterlater en foreldreløs, GUID-navngitt nøkkelbeholder liggende i den kallende brukerens profil, noe som er nøyaktig den typen lekkasje en CI-agent som kjører denne funksjonen ved hvert bygg, vil akkumulere i måneder før noen legger merke til det

Er et selvsignert sertifikat trygt å bruke for produksjonssignering?

Nei: et selvsignert sertifikat er trygt for å utøve en signerings-kodevei og utrygt for en signatur noen utenfor teamet forventes å stole på, fordi ingenting kjeder det tilbake til en rot en relierende parts programvare allerede stoler på. Det naturlige neste steget for en PFX som denne, er et faktisk signeringskall, dekket i å bygge en konformitets- og signerings-arbeidsbenk i Delphi med PDFlibPas, der en PFX bygget på denne måten driver signerings-halvparten av en pipeline som også kjører PDF/A-preflight og ByteRange-revisjoner. Signering er bare halvparten av det som sitter rundt et sertifikat, derimot, og den andre halvparten er nøyaktig der et selvsignert løvsertifikat skal feile: PAdES-signering og -validering i Delphi med PDFlibPas dekker tillitskjede-sjekkene en konformitetsvalidator kjører, og en validator som går kjeden tilbake til en pålitelig rot, har ingen grunn til å stole på et sertifikat denne funksjonen fant opp fem minutter siden fra ingenting

PLCreateSelfSignedCertificate er én funksjon blant sertifikat- og signerings-API-ene i PDFlibPas PDF-biblioteket for Delphi og C++Builder, og den finnes nettopp for gapet beskrevet her: en signeringstest som trenger et ekte nøkkelpar bak seg og ingenting eksternt for å generere ett