Teknisk artikel

Självsignerade testcertifikat i Delphi med CryptoAPI

PDFlibPas funktion PLCreateSelfSignedCertificate bygger ett självsignerat RSA/SHA-256-certifikat och exporterar det, privat nyckel inräknad, direkt in i en lösenordsskyddad PFX-fil, med bara Win32 CryptoAPI som redan är installerat på varje Windows-maskin. Inget externt verktyg, ingen certifikatutfärdare, inget manuellt makecert- eller OpenSSL-steg: ett funktionsanrop, ett certifikat gott nog för att driva ett signeringstest

Scenariot som gör den här funktionen värd att ha är nästan alltid en CI-pipeline. Ett signeringsröktest behöver en riktig PFX med en riktig privat nyckel bakom sig, och att checka in en i repositoryt är sitt eget säkerhetsproblem, eftersom en committad privat nyckel är en läckt privat nyckel från det ögonblick den committen landar. Att köra makecert.exe eller ett OpenSSL-anrop från ett byggskript via shell fungerar också, men då beror pipelinen på ett verktyg som måste installeras, hittas på PATH, och hållas versionskonsekvent över varje byggagent. Att generera certifikatet inuti samma process som kör testet, med samma Win32 CryptoAPI-anrop Windows redan levererar, tar bort det beroendet helt

Vad producerar PLCreateSelfSignedCertificate egentligen?

PLCreateSelfSignedCertificate producerar en lösenordsskyddad PFX-fil som håller ett självsignerat RSA-certifikat och dess privata nyckel, signerad med sha256RSA, driven av fem parametrar: SubjectName, PFXFileName, PFXPassword, ValidDays, och KeyBits, och den returnerar en enkel boolesk framgångsflagga. SubjectName accepterar en full X.500-sträng som 'CN=Alice, O=Example', och ett bart namn utan något =-tecken i sig prefixas automatiskt med CN=. ValidDays under 1 faller tillbaka till 365, och KeyBits utanför intervallet 1024 till 16384 faller tillbaka till 2048. PDFlibPas har levererat den här funktionen sedan v3.224.0, nåbar inte bara från Delphi-enheten utan också genom DLL- och ActiveX-ytorna, och dess egen dokumentationskommentar är rak på sak om var den slutar vara användbar: varje mainstream-visare markerar ett självsignerat certifikat som otillförlitligt om inte någon installerar det uttryckligen, så behandla vad den producerar som ett certifikat för att utöva en kodväg, inte en signatur någon utanför ditt team bör ombes lita 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;

Varför kodar CryptGenKey nyckellängden i flaggparametern?

CryptGenKey packar två orelaterade inställningar in i en enda dwFlags-parameter. Lågordet bär beteendeflaggor, CRYPT_EXPORTABLE bland dem, medan högordet, för en RSA-nyckelutbytesnyckel, bär den begärda nyckellängden i bitar. Att skicka 2048 som om det vore bara en annan flagga landar den i lågordet istället, där den inte matchar någon beteendeflagga CryptoAPI definierar, så anropet genererar en nyckel vid vilken standardlängd leverantören än faller tillbaka till snarare än längden anroparen trodde de begärde. Att få en faktisk 2048-bitars RSA-nyckel betyder att skifta talet in i högordet 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;

Vad händer om du glömmer CRYPT_EXPORTABLE?

Ta bort CRYPT_EXPORTABLE från samma flaggvärde och CryptGenKey lyckas ändå, men den markerar den genererade privata nyckeln icke-exporterbar på CSP-nivå. Allt nedströms fortsätter också rapportera framgång: CertCreateSelfSignCertificate returnerar en giltig certifikatkontext, och PFXExportCertStoreEx, även anropad med EXPORT_PRIVATE_KEYS, lyckas ändå och skriver en PFX-fil som öppnas, tolkas, och ser helt vanlig ut. Vad den inte innehåller är den privata nyckeln, eftersom CSP:n vägrade släppa ut den ur nyckelbehållaren, och PFXExportCertStoreEx behandlar aldrig den vägran som en anledning att misslyckas hela exporten

Felet visar sig bara senare, och någon helt annanstans: ett signeringsanrop öppnar den PFX:en, hittar ett certifikat utan privat nyckel bifogad, och rapporterar exakt det fel du skulle få från en korrupt eller fel PFX, inte från en saknad flagga tre lager uppströms. Alla som felsöker enbart från signeringssidan kan förbränna en eftermiddag på fel fil innan de inser att den faktiska buggen är en enda saknad bit vid nyckelgenereringstillfället, i ett helt annat funktionsanrop, möjligen i ett helt annat byggskript

Varför måste ProvType matcha mellan CryptAcquireContextW och certifikatet?

ProvType måste matcha eftersom CertCreateSelfSignCertificate löser upp det nya certifikatets privata nyckel genom en CRYPT_KEY_PROV_INFO-post, och ett fält i den posten, ProvType, måste namnge exakt samma CSP-typvärde som skickades till CryptAcquireContextW när nyckelbehållaren öppnades, PROV_RSA_AES, numeriskt 24, i PDFlibPas implementation. Sätt ProvType till noll, eller till någon leverantörskonstant annan än den behållaren faktiskt tillhör, och certifikatet kan fortfarande skapas, men dess registrerade länk tillbaka till den privata nyckeln löser inte längre upp till behållaren som håller den, vilket visar sig senare som ett signerings- eller exportfel som inte har något att göra med certifikatets faktiska kryptografiska innehåll

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

Att sätta ihop det: från GUID-behållare till lösenordsskyddad PFX

Anropskedjan inuti PLCreateSelfSignedCertificate följer en rak linje: öppna en färsk nyckelbehållare namngiven efter en nygenererad GUID så samtidiga CI-körningar aldrig krockar över behållarnamn, generera RSA-nyckelparet inuti den med de två flaggorna som täcks ovan, koda SubjectName till en X.500-namnblob genom CertStrToNameW, och anropa CertCreateSelfSignCertificate med ett giltighetsfönster beräknat från ValidDays och lämnat över som en enkel SYSTEMTIME-formad struktur. Den resulterande certifikatkontexten går in i ett minnesbaserat certifikatlager öppnat med CertOpenStore och CERT_STORE_PROV_MEMORY, rent för att PFXExportCertStoreEx ska ha ett lager att exportera från, eftersom det API:et arbetar mot ett lagerhandtag snarare än en bar certifikatkontext

// 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 själv följer den vanliga Win32-tvåpasseringskonventionen: anropa den en gång med en nollängdsbuffert för att lära sig hur många byte PFX:en behöver, allokera så mycket, anropa den sedan igen för att fylla bufferten. När byten väl är på disk raderar PDFlibPas den engångsanvända nyckelbehållaren med CRYPT_DELETEKEYSET istället för att lämna kvar den, eftersom PFX:en redan bär sin egen kopia av varje byte nyckelmaterial behållaren höll. Hoppa över den städningen och varje anrop till PLCreateSelfSignedCertificate lämnar en föräldralös, GUID-namngiven nyckelbehållare sittande i den anropande användarens profil, vilket är precis den typen av läcka en CI-agent som kör den här funktionen vid varje bygge kommer ackumulera under månader innan någon märker det

Är ett självsignerat certifikat säkert att använda för produktionssignering?

Nej: ett självsignerat certifikat är säkert för att utöva en signeringskodväg och osäkert för en signatur någon utanför teamet förväntas lita på, eftersom inget kedjar det tillbaka till en rot en förlitande parts programvara redan litar på. Nästa naturliga steg för en PFX byggd på det här sättet är ett faktiskt signeringsanrop, täckt i att bygga en konformitets- och signeringsarbetsbänk i Delphi med PDFlibPas, där en PFX byggd på det här sättet driver signeringshalvan av en pipeline som också kör PDF/A-förkontroll och ByteRange-granskningar. Signering är dock bara hälften av vad som finns runt ett certifikat, och den andra hälften är precis var en självsignerad slutentitet förväntas misslyckas: PAdES-signering och validering i Delphi med PDFlibPas täcker förtroendekedjekontrollerna en konformitetsvaliderare kör, och en validerare som går kedjan tillbaka till en betrodd rot har ingen anledning att lita på ett certifikat den här funktionen just uppfann fem minuter sedan från ingenting

PLCreateSelfSignedCertificate är en funktion bland certifikat- och signerings-API:erna i PDFlibPas PDF-biblioteket för Delphi och C++Builder, och den existerar för precis luckan som beskrivs här: ett signeringstest som behöver ett riktigt nyckelpar bakom sig och inget externt för att generera ett