De functie PLCreateSelfSignedCertificate van PDFlibPas bouwt een zelfondertekend RSA/SHA-256-certificaat en exporteert dit, inclusief private key, rechtstreeks naar een met wachtwoord beveiligd PFX-bestand, met niets anders dan de Win32 CryptoAPI die al op elke Windows-machine is geïnstalleerd. Geen extern gereedschap, geen certificaatautoriteit, geen handmatige makecert- of OpenSSL-stap: één functieaanroep, één certificaat goed genoeg om een ondertekeningstest aan te drijven
Het scenario dat deze functie de moeite waard maakt, is bijna altijd een CI-pipeline. Een ondertekening-smoke-test heeft een echte PFX nodig met een echte private key erachter, en er eentje in de repository inchecken is een eigen beveiligingsprobleem, aangezien een ingecheckte private key vanaf het moment dat die commit landt een gelekte private key is. Uitbesteden aan makecert.exe of een OpenSSL-aanroep vanuit een buildscript werkt ook, maar dan hangt de pipeline af van een tool die geïnstalleerd, op PATH gevonden, en versieconsistent gehouden moet worden over elke build-agent. Het certificaat genereren binnen hetzelfde proces dat de test draait, met dezelfde Win32-CryptoAPI-aanroepen die Windows al meelevert, verwijdert die afhankelijkheid volledig
Wat produceert PLCreateSelfSignedCertificate eigenlijk?
PLCreateSelfSignedCertificate produceert een met wachtwoord beveiligd PFX-bestand met daarin een zelfondertekend RSA-certificaat en zijn private key, ondertekend met sha256RSA, aangedreven door vijf parameters: SubjectName, PFXFileName, PFXPassword, ValidDays, en KeyBits, en het geeft een gewone Booleaanse succesvlag terug. SubjectName accepteert een volledige X.500-string zoals 'CN=Alice, O=Example', en een kale naam zonder =-teken erin wordt automatisch voorafgegaan door CN=. ValidDays onder 1 valt terug op 365, en KeyBits buiten het bereik 1024 tot 16384 valt terug op 2048. PDFlibPas levert deze functie al sinds v3.224.0, bereikbaar niet alleen vanuit de Delphi-unit maar ook via de DLL- en ActiveX-oppervlakken, en het eigen doc-commentaar is bot over waar het ophoudt bruikbaar te zijn: elke gangbare viewer markeert een zelfondertekend certificaat als onbetrouwbaar tenzij iemand het expliciet installeert, dus behandel wat het produceert als een certificaat om een codepad te oefenen, niet als een handtekening die iemand buiten uw team gevraagd zou moeten worden te vertrouwen
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;
Waarom codeert CryptGenKey de sleutellengte in de flags-parameter?
CryptGenKey pakt twee ongerelateerde instellingen in één enkele dwFlags-parameter. Het lage woord draagt gedragsvlaggen, CRYPT_EXPORTABLE daaronder, terwijl het hoge woord, voor een RSA-sleuteluitwisselingssleutel, de gevraagde sleutellengte in bits draagt. 2048 doorgeven alsof het gewoon nog een vlag was, laat het in het lage woord terechtkomen, waar het met geen enkele gedragsvlag overeenkomt die CryptoAPI definieert, dus genereert de aanroep een sleutel met welke standaardlengte de provider ook terugvalt, in plaats van de lengte die de aanroeper dacht op te vragen. Een werkelijke 2048-bits RSA-sleutel krijgen betekent het getal eerst naar het hoge woord verschuiven
// 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;
Wat gebeurt er als u CRYPT_EXPORTABLE vergeet?
Laat CRYPT_EXPORTABLE weg uit diezelfde flags-waarde en CryptGenKey slaagt nog steeds, maar het markeert de gegenereerde private key op CSP-niveau als niet-exporteerbaar. Alles stroomafwaarts blijft ook succes rapporteren: CertCreateSelfSignCertificate geeft een geldige certificaatcontext terug, en PFXExportCertStoreEx, zelfs aangeroepen met EXPORT_PRIVATE_KEYS, slaagt toch en schrijft een PFX-bestand dat opent, parst, en er volkomen gewoon uitziet. Wat het niet bevat, is de private key, omdat de CSP weigerde deze uit de sleutelcontainer te laten, en PFXExportCertStoreEx die weigering nooit behandelt als een reden om de hele export te laten falen
De storing komt pas later aan het licht, en ergens compleet anders: een ondertekeningsaanroep opent die PFX, vindt een certificaat zonder gekoppelde private key, en rapporteert precies de fout die u zou krijgen van een corrupte of verkeerde PFX, niet van een ontbrekende vlag drie lagen stroomopwaarts. Iedereen die alleen vanaf de ondertekeningskant debugt, kan een middag verspillen aan het verkeerde bestand voordat hij beseft dat de werkelijke bug een enkel ontbrekend bit is op het moment van sleutelgeneratie, in een compleet andere functieaanroep, mogelijk in een compleet ander buildscript
Waarom moet ProvType overeenkomen tussen CryptAcquireContextW en het certificaat?
ProvType moet overeenkomen omdat CertCreateSelfSignCertificate de private key van het nieuwe certificaat oplost via een CRYPT_KEY_PROV_INFO-record, en één veld in dat record, ProvType, precies dezelfde CSP-typewaarde moet noemen die aan CryptAcquireContextW is doorgegeven toen de sleutelcontainer werd geopend, PROV_RSA_AES, numeriek 24, in de implementatie van PDFlibPas. Zet ProvType op nul, of op een andere providerconstante dan degene waartoe de container daadwerkelijk behoort, en het certificaat kan nog steeds worden aangemaakt, maar zijn geregistreerde koppeling terug naar de private key lost niet meer op naar de container die deze bevat, wat later naar boven komt als een ondertekenings- of exportstoring die niets te maken heeft met de werkelijke cryptografische inhoud van het certificaat
// 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
Het geheel samenvoegen: van GUID-container naar met wachtwoord beveiligde PFX
De aanroepketen binnen PLCreateSelfSignedCertificate volgt één rechte lijn: een verse sleutelcontainer openen genoemd naar een nieuw gegenereerde GUID, zodat gelijktijdige CI-runs nooit botsen over containernamen, het RSA-sleutelpaar erbinnen genereren met de twee hierboven behandelde vlaggen, SubjectName coderen naar een X.500-naam-blob via CertStrToNameW, en CertCreateSelfSignCertificate aanroepen met een geldigheidsvenster berekend uit ValidDays en overhandigd als een gewone in SYSTEMTIME-vorm gegoten structuur. De resulterende certificaatcontext gaat in een certificaatopslag in het geheugen, geopend met CertOpenStore en CERT_STORE_PROV_MEMORY, puur zodat PFXExportCertStoreEx een opslag heeft om uit te exporteren, aangezien die API werkt tegen een opslag-handle in plaats van een kale certificaatcontext
// 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 zelf volgt de gewone Win32-tweepassenconventie: roep het één keer aan met een buffer van lengte nul om te leren hoeveel bytes de PFX nodig heeft, wijs zoveel toe, en roep het dan opnieuw aan om de buffer te vullen. Zodra de bytes op schijf staan, verwijdert PDFlibPas de wegwerp-sleutelcontainer met CRYPT_DELETEKEYSET in plaats van deze achter te laten, omdat de PFX al zijn eigen kopie draagt van elke byte sleutelmateriaal die de container bevatte. Sla die opruiming over en elke aanroep van PLCreateSelfSignedCertificate laat een verweesde, op GUID genoemde sleutelcontainer achter in het profiel van de aanroepende gebruiker, precies het soort lek dat een CI-agent die deze functie bij elke build draait, maanden lang opstapelt voordat iemand het opmerkt
Is een zelfondertekend certificaat veilig voor productie-ondertekening?
Nee: een zelfondertekend certificaat is veilig om een ondertekeningscodepad te oefenen en onveilig voor een handtekening die iemand buiten het team verondersteld wordt te vertrouwen, omdat niets het terugkoppelt naar een root die de software van een vertrouwende partij al vertrouwt. De natuurlijke volgende stap voor zo'n PFX is een echte ondertekeningsaanroep, behandeld in het bouwen van een compliance- en ondertekeningsworkbench in Delphi met PDFlibPas, waar een PFX zo gebouwd de ondertekeningshelft aandrijft van een pipeline die ook PDF/A-preflight en ByteRange-audits draait. Ondertekenen is echter maar de helft van wat er om een certificaat heen zit, en de andere helft is precies waar een zelfondertekend blad-certificaat verondersteld wordt te falen: PAdES-ondertekening en -validatie in Delphi met PDFlibPas behandelt de vertrouwensketen-controles die een conformiteitsvalidator uitvoert, en een validator die de keten terugloopt naar een vertrouwde root heeft geen enkele reden om een certificaat te vertrouwen dat deze functie vijf minuten geleden uit het niets heeft verzonnen
PLCreateSelfSignedCertificate is één functie onder de certificaat- en ondertekenings-API's in de PDFlibPas PDF-bibliotheek voor Delphi en C++Builder, en het bestaat precies voor het hier beschreven gat: een ondertekeningstest die een echt sleutelpaar erachter nodig heeft en niets externs om er eentje te genereren