PDFlibPas:n PLCreateSelfSignedCertificate-funktio rakentaa itseallekirjoitetun RSA/SHA-256-sertifikaatin ja vie sen, yksityinen avain mukaan lukien, suoraan salasanasuojattuun PFX-tiedostoon, käyttäen pelkästään Win32 CryptoAPI:a, joka on jo asennettuna jokaisella Windows-koneella. Ei ulkoista työkalua, ei sertifikaattiviranomaista, ei manuaalista makecert- tai OpenSSL-vaihetta: yksi funktiokutsu, yksi sertifikaatti, joka riittää ajamaan allekirjoitustestin
Skenaario, joka tekee tästä funktiosta tarpeellisen, on lähes aina CI-putki. Allekirjoituksen savutesti tarvitsee todellisen PFX:n todellisella yksityisavaimella sen takana, ja sellaisen sisällyttäminen repositorioon on oma tietoturvaongelmansa, koska sitoutunut yksityisavain on vuotanut yksityisavain siitä hetkestä, kun tuo commit laskeutuu. Ulkoisen makecert.exe:n tai OpenSSL-kutsun ajaminen build-skriptistä toimii myös, mutta silloin putki riippuu työkalusta, joka on asennettava, löydettävä PATH:sta ja pidettävä versioyhtenäisenä jokaisella build-agentilla. Sertifikaatin generoiminen samassa prosessissa, joka ajaa testin, samoilla Win32 CryptoAPI -kutsuilla, jotka Windows jo toimittaa, poistaa tuon riippuvuuden kokonaan
Mitä PLCreateSelfSignedCertificate todella tuottaa?
PLCreateSelfSignedCertificate tuottaa salasanasuojatun PFX-tiedoston, joka kantaa itseallekirjoitetun RSA-sertifikaatin ja sen yksityisavaimen, allekirjoitettuna sha256RSA:lla, ohjattuna viidellä parametrilla: SubjectName, PFXFileName, PFXPassword, ValidDays ja KeyBits, ja se palauttaa tavallisen Boolean-onnistumislipun. SubjectName hyväksyy täyden X.500-merkkijonon kuten 'CN=Alice, O=Example', ja paljas nimi ilman =-merkkiä siinä saa automaattisesti CN=-etuliitteen. ValidDays alle 1:n palautuu 365:een, ja KeyBits alueen 1024–16384 ulkopuolella palautuu 2048:aan. PDFlibPas on toimittanut tämän funktion v3.224.0:sta lähtien, tavoitettavissa ei vain Delphi-yksiköstä vaan myös DLL- ja ActiveX-pinnoista, ja sen oma dokumentaatiokommentti on suora siitä, missä se lakkaa olemasta hyödyllinen: jokainen valtavirran katseluohjelma merkitsee itseallekirjoitetun sertifikaatin epäluotettavaksi, ellei joku asenna sitä nimenomaisesti, joten kohtele sen tuottamaa sertifikaattia koodipolun harjoittamiseen tarkoitettuna sertifikaattina, ei allekirjoituksena, johon kenenkään tiimin ulkopuolisen pitäisi pyytää luottaa
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;
Miksi CryptGenKey koodaa avainpituuden lippuparametriin?
CryptGenKey pakkaa kaksi toisiinsa liittymätöntä asetusta yhteen dwFlags-parametriin. Alempi sana kantaa käytöslippuja, CRYPT_EXPORTABLE niiden joukossa, kun taas ylempi sana, RSA-avaintenvaihtoavaimelle, kantaa pyydetyn avainpituuden bitteinä. 2048:n välittäminen ikään kuin se olisi vain toinen lippu sijoittaa sen alempaan sanaan sen sijaan, missä se ei täsmää minkään CryptoAPI:n määrittelemän käytöslipun kanssa, joten kutsu generoi avaimen millä tahansa oletuspituudella, johon tarjoaja palautuu, ei pituudella, jonka kutsuja luuli pyytäneensä. Todellisen 2048-bittisen RSA-avaimen saaminen tarkoittaa luvun siirtämistä ylempään sanaan ensin
// 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;
Mitä tapahtuu, jos unohdat CRYPT_EXPORTABLE:n?
Pudota CRYPT_EXPORTABLE samasta lippuarvosta, ja CryptGenKey silti onnistuu, mutta se merkitsee generoidun yksityisavaimen ei-vietäväksi CSP-tasolla. Kaikki alavirrassa jatkaa myös onnistumisen raportointia: CertCreateSelfSignCertificate palauttaa pätevän sertifikaattikontekstin, ja PFXExportCertStoreEx, jopa kutsuttuna EXPORT_PRIVATE_KEYS-lipulla, onnistuu joka tapauksessa ja kirjoittaa PFX-tiedoston, joka avautuu, jäsentyy ja näyttää täysin tavalliselta. Mitä se ei sisällä, on yksityisavain, koska CSP kieltäytyi päästämästä sitä ulos avainsäiliöstä, eikä PFXExportCertStoreEx koskaan kohtele tuota kieltäytymistä syynä epäonnistuttaa koko vienti
Epäonnistuminen näkyy vasta myöhemmin, ja täysin muualla: allekirjoituskutsu avaa tuon PFX:n, löytää sertifikaatin, johon ei ole liitetty yksityisavainta, ja raportoi täsmälleen sen virheen, jonka saisit vioittuneesta tai väärästä PFX:stä, ei puuttuvasta lipusta kolme kerrosta ylävirrassa. Kuka tahansa, joka debuggaa vain allekirjoituspuolelta, voi polttaa iltapäivän väärän tiedoston kanssa ennen kuin huomaa, että todellinen bugi on yksi puuttuva bitti avaimen generointihetkellä, täysin eri funktiokutsussa, mahdollisesti täysin eri build-skriptissä
Miksi ProvType:n täytyy täsmätä CryptAcquireContextW:n ja sertifikaatin välillä?
ProvType:n täytyy täsmätä, koska CertCreateSelfSignCertificate ratkaisee uuden sertifikaatin yksityisavaimen CRYPT_KEY_PROV_INFO-tietueen kautta, ja yksi tuon tietueen kenttä, ProvType, on nimettävä täsmälleen sama CSP-tyyppiarvo, joka välitettiin CryptAcquireContextW:lle, kun avainsäiliö avattiin, PROV_RSA_AES, numeerisesti 24, PDFlibPas:n toteutuksessa. Aseta ProvType nollaksi, tai miksi tahansa muuksi tarjoajavakioksi kuin se, johon säiliö todella kuuluu, ja sertifikaatti voidaan silti luoda, mutta sen tallennettu linkki takaisin yksityisavaimeen ei enää ratkea säiliöön, joka sitä kantaa, mikä paljastuu myöhemmin allekirjoitus- tai vientivirheenä, jolla ei ole mitään tekemistä sertifikaatin todellisen kryptografisen sisällön kanssa
// 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
Kokoaminen yhteen: GUID-säiliöstä salasanasuojattuun PFX:ään
Kutsuketju PLCreateSelfSignedCertificate:n sisällä noudattaa yhtä suoraa linjaa, avaten tuoreen avainsäiliön, joka on nimetty juuri generoidun GUID:n mukaan, jotta samanaikaiset CI-ajot eivät koskaan törmää säiliönimissä, generoiden RSA-avainparin sen sisällä kahdella yllä käsitellyllä lipulla, koodaten SubjectName:n X.500-nimiblobiksi CertStrToNameW:n kautta, ja kutsuen CertCreateSelfSignCertificate:a voimassaoloikkunalla, joka on laskettu ValidDays:sta ja annettu tavallisena SYSTEMTIME-muotoisena rakenteena. Tuloksena syntyvä sertifikaattikonteksti menee muistinvaraiseen sertifikaattivarastoon, joka avataan CertOpenStore:lla ja CERT_STORE_PROV_MEMORY:llä, puhtaasti siksi, että PFXExportCertStoreEx:lla on varasto, josta viedä, koska tuo API toimii varastokahvaa vasten paljaan sertifikaattikontekstin sijaan
// 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 itse noudattaa tavallista Win32:n kaksikierroskäytäntöä: kutsu sitä kerran nollapituisella puskurilla saadaksesi tietää, kuinka monta tavua PFX tarvitsee, varaa sen verran, ja kutsu sitä sitten uudelleen täyttääksesi puskurin. Heti kun tavut ovat levyllä, PDFlibPas poistaa kertakäyttöisen avainsäiliön CRYPT_DELETEKEYSET:llä jättämisen sijaan, koska PFX jo kantaa oman kopionsa jokaisesta säiliön kantaman avainmateriaalin tavusta. Ohita tuo siivous, ja jokainen PLCreateSelfSignedCertificate-kutsu jättää orvoksi GUID-nimisen avainsäiliön istumaan kutsuvan käyttäjän profiiliin, mikä on juuri sellainen vuoto, jota CI-agentti, joka ajaa tätä funktiota jokaisella buildilla, kerää kuukausien ajan ennen kuin kukaan huomaa
Onko itseallekirjoitettu sertifikaatti turvallinen käytettäväksi tuotantoallekirjoituksessa?
Ei: itseallekirjoitettu sertifikaatti on turvallinen allekirjoituskoodipolun harjoittamiseen ja turvaton allekirjoitukselle, johon kenenkään tiimin ulkopuolisen odotetaan luottavan, koska mikään ei ketjuta sitä takaisin juureen, johon luottavan osapuolen ohjelmisto jo luottaa. Luonnollinen seuraava askel tällaiselle PFX:lle on todellinen allekirjoituskutsu, käsitelty artikkelissa yhdenmukaisuus- ja allekirjoitustyöpenkin rakentaminen Delphissä PDFlibPas:lla, jossa tällä tavalla rakennettu PFX ajaa putken allekirjoituspuolta, joka myös ajaa PDF/A-esitarkistuksen ja ByteRange-tarkastukset. Allekirjoitus on kuitenkin vain puolet siitä, mikä on sertifikaatin ympärillä, ja toinen puoli on juuri se, missä itseallekirjoitetun lehden pitäisi epäonnistua: PAdES-allekirjoitus ja validointi Delphissä PDFlibPas:lla käsittelee luottamusketjun tarkistukset, joita yhdenmukaisuusvalidaattori ajaa, ja validaattorilla, joka kävelee ketjun takaisin luotettuun juureen, ei ole syytä luottaa sertifikaattiin, jonka tämä funktio keksi viisi minuuttia sitten tyhjästä
PLCreateSelfSignedCertificate on yksi funktio sertifikaatti- ja allekirjoitus-API:en joukossa PDFlibPas PDF-kirjastossa Delphille ja C++Builderille, ja se on olemassa juuri tässä kuvatulle aukolle: allekirjoitustesti, joka tarvitsee todellisen avainparin taakseen eikä mitään ulkoista sellaisen generoimiseksi