PDFlibPas funkcija PLCreateSelfSignedCertificate sukuria savarankiškai pasirašytą RSA/SHA-256 sertifikatą ir eksportuoja jį kartu su privačiuoju raktu tiesiai į slaptažodžiu apsaugotą PFX failą, naudodama tik Win32 CryptoAPI, kuri jau įdiegta kiekviename Windows kompiuteryje. Nereikia išorinio įrankio, sertifikavimo institucijos, rankinio makecert ar OpenSSL veiksmo: vienas funkcijos iškvietimas ir vienas sertifikatas, kurio pakanka pasirašymo testui
Ši funkcija beveik visada tampa reikalinga CI konvejeryje. Pasirašymo dūmų testui reikia tikro PFX su tikru privačiuoju raktu, o įrašyti jį į saugyklą būtų atskira saugumo problema, nes įkeltas privatusis raktas tampa nutekėjusiu raktu nuo pat to momento, kai įrašomas pakeitimas. Iš kūrimo scenarijaus iškviesti makecert.exe arba OpenSSL taip pat veikia, tačiau tada konvejeris priklauso nuo įrankio, kurį reikia įdiegti, rasti PATH kelyje ir išlaikyti tos pačios versijos visuose kūrimo agentuose. Sertifikato generavimas tame pačiame procese, kuris vykdo testą, naudojant tuos pačius Win32 CryptoAPI iškvietimus, jau pateikiamus Windows, visiškai pašalina šią priklausomybę
Ką iš tikrųjų sukuria PLCreateSelfSignedCertificate?
PLCreateSelfSignedCertificate sukuria slaptažodžiu apsaugotą PFX failą, kuriame laikomas savarankiškai pasirašytas RSA sertifikatas ir jo privatusis raktas, pasirašytas naudojant sha256RSA, o veikimą lemia penki parametrai: SubjectName, PFXFileName, PFXPassword, ValidDays ir KeyBits; funkcija grąžina paprastą Boolean sėkmės požymį. SubjectName priima visą X.500 eilutę, pavyzdžiui, 'CN=Alice, O=Example', o trumpam vardui, kuriame nėra ženklo =, automatiškai pridedamas priešdėlis CN=. Mažesnė nei 1 ValidDays reikšmė pakeičiama į 365, o už 1024–16384 ribų esanti KeyBits reikšmė pakeičiama į 2048. PDFlibPas šią funkciją teikia nuo v3.224.0, ją galima pasiekti ne tik iš Delphi modulio, bet ir per DLL bei ActiveX sąsajas, o jos dokumentacijos komentare aiškiai nurodoma riba: kiekviena pagrindinė peržiūros programa savarankiškai pasirašytą sertifikatą pažymi kaip nepatikimą, nebent kas nors jį aiškiai įdiegia, todėl tai, ką funkcija sukuria, laikykite kodo kelio testavimo sertifikatu, o ne parašu, kuriuo reikėtų pasitikėti už jūsų komandos ribų
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;
Kodėl CryptGenKey rakto ilgį koduoja vėliavų parametre?
CryptGenKey į vieną dwFlags parametrą supakuoja du nesusijusius nustatymus. Apatinis žodis saugo veikimo vėliavas, tarp jų CRYPT_EXPORTABLE, o viršutinis žodis RSA raktų mainų raktui saugo pageidaujamą rakto ilgį bitais. Perdavus 2048 tarsi dar vieną vėliavą, ši reikšmė patenka į apatinį žodį, kur nesutampa su jokia CryptoAPI apibrėžta veikimo vėliava, todėl iškvietimas sukuria tokio numatytojo ilgio raktą, kurį pasirenka teikėjas, o ne tokio ilgio, kokio tikėjosi kviečiantis kodas. Norint gauti tikrą 2048 bitų RSA raktą, skaičių pirmiausia reikia perkelti į viršutinį žodį
// 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;
Kas nutinka pamiršus CRYPT_EXPORTABLE?
Pašalinus CRYPT_EXPORTABLE iš tos pačios vėliavų reikšmės, CryptGenKey vis tiek sėkmingai baigiasi, tačiau CSP lygiu sugeneruotas privatusis raktas pažymimas kaip neeksportuojamas. Visi tolesni veiksmai taip pat praneša apie sėkmę: CertCreateSelfSignCertificate grąžina galiojantį sertifikato kontekstą, o PFXExportCertStoreEx, net iškviesta su EXPORT_PRIVATE_KEYS, taip pat sėkmingai baigia darbą ir įrašo PFX failą, kuris atsidaro, išanalizuojamas ir atrodo visiškai įprastai. Tačiau jame nėra privataus rakto, nes CSP neleido jo iškelti iš raktų konteinerio, o PFXExportCertStoreEx niekada nelaiko tokio atsisakymo priežastimi nutraukti visą eksportą
Klaida pasirodo tik vėliau ir visai kitoje vietoje: pasirašymo iškvietimas atidaro PFX, randa sertifikatą be prijungto privataus rakto ir praneša būtent tokią klaidą, kokią gautumėte dėl sugadinto ar netinkamo PFX, o ne dėl trūkstamos vėliavos, esančios trimis sluoksniais aukščiau. Vien iš pasirašymo pusės derinantis asmuo gali visą popietę ieškoti problemos netinkamame faile, kol supras, kad tikroji klaida yra vienas trūkstamas bitas rakto generavimo metu, visai kitame funkcijos iškvietime, galbūt net visai kitame kūrimo scenarijuje
Kodėl ProvType turi sutapti tarp CryptAcquireContextW ir sertifikato?
ProvType turi sutapti, nes CertCreateSelfSignCertificate naujo sertifikato privatųjį raktą suranda per CRYPT_KEY_PROV_INFO įrašą, o vienas šio įrašo laukas, ProvType, turi nurodyti visiškai tą pačią CSP tipo reikšmę, kuri buvo perduota CryptAcquireContextW atidarant raktų konteinerį, t. y. PROV_RSA_AES, skaitinę reikšmę 24, naudojamą PDFlibPas įgyvendinime. Nustačius ProvType į nulį arba bet kurią kitą teikėjo konstantą, nesutampančią su ta, kuriai priklauso konteineris, sertifikatas vis tiek gali būti sukurtas, tačiau užregistruota nuoroda į privatųjį raktą neberanda konteinerio, kuriame jis laikomas, todėl vėliau kyla pasirašymo arba eksporto klaida, nesusijusi su tikruoju sertifikato kriptografiniu turiniu
// 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
Viskas kartu: nuo GUID konteinerio iki slaptažodžiu apsaugoto PFX
Funkcijos PLCreateSelfSignedCertificate viduje iškvietimų grandinė eina viena tiesia linija: atidaro naują raktų konteinerį, pavadintą naujai sugeneruotu GUID, kad lygiagrečiai vykdomi CI paleidimai nesusidurtų dėl konteinerių pavadinimų, jame sugeneruoja RSA raktų porą naudodama abi pirmiau aptartas vėliavas, per CertStrToNameW užkoduoja SubjectName į X.500 vardo objektą ir iškviečia CertCreateSelfSignCertificate su galiojimo langu, apskaičiuotu pagal ValidDays ir perduotu kaip paprasta SYSTEMTIME formos struktūra. Gautas sertifikato kontekstas patalpinamas į atmintyje esantį sertifikatų kaupiklį, atidarytą naudojant CertOpenStore ir CERT_STORE_PROV_MEMORY, vien tam, kad PFXExportCertStoreEx turėtų iš ko eksportuoti, nes ši API veikia su kaupiklio rankena, o ne su atskiru sertifikato kontekstu
// 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);
Pats PFXExportCertStoreEx laikosi įprasto Win32 dviejų etapų susitarimo: pirmą kartą iškviečiamas su nulinio ilgio buferiu, kad būtų sužinota, kiek baitų reikia PFX, tada paskiriamas tokio dydžio buferis ir iškvietimas kartojamas jam užpildyti. Įrašius baitus į diską, PDFlibPas ištrina laikinąjį raktų konteinerį naudodama CRYPT_DELETEKEYSET, o ne palieka jį, nes PFX jau turi savo kiekvieno konteineryje buvusio rakto medžiagos baito kopiją. Praleidus šį valymą, kiekvienas PLCreateSelfSignedCertificate iškvietimas palieka našlaičiu tapusį GUID pavadintą raktų konteinerį kviečiančio vartotojo profilyje, ir būtent tokį nuotėkį CI agentas, kiekvieną kūrimą vykdantis šią funkciją, gali kaupti mėnesius, kol kas nors tai pastebės
Ar saugu savarankiškai pasirašytą sertifikatą naudoti gamybiniam pasirašymui?
Ne: savarankiškai pasirašytą sertifikatą saugu naudoti pasirašymo kodo keliui tikrinti, tačiau nesaugu naudoti parašui, kuriuo turėtų pasitikėti kas nors už komandos ribų, nes jis nesusietas su šaknimi, kuria jau pasitiki pasikliaujančios šalies programinė įranga. Natūralus kitas tokio PFX žingsnis yra tikras pasirašymo iškvietimas, aprašytas straipsnyje atitikties ir pasirašymo darbo vietos kūrimas Delphi naudojant PDFlibPas, kuriame taip sukurtas PFX valdo konvejerio pasirašymo dalį, kartu vykdančią PDF/A išankstinį patikrinimą ir ByteRange auditus. Tačiau pasirašymas yra tik pusė to, kas susiję su sertifikatu, o kita pusė yra būtent ta vieta, kur savarankiškai pasirašytas galutinis sertifikatas turi būti atmestas: straipsnyje PAdES pasirašymas ir tikrinimas Delphi naudojant PDFlibPas aprašomi pasitikėjimo grandinės patikrinimai, kuriuos vykdo atitikties tikrintuvas, o tikrintuvas, grįžtantis grandine iki patikimos šaknies, neturi priežasties pasitikėti sertifikatu, kurį ši funkcija prieš penkias minutes sukūrė iš nieko
PLCreateSelfSignedCertificate yra viena iš sertifikatų ir pasirašymo API, esančių PDFlibPas PDF bibliotekoje, skirtoje Delphi ir C++Builder, ir ji skirta būtent čia aprašytai spragai: pasirašymo testui, kuriam reikia tikros raktų poros ir jokio išorinio jos generavimo įrankio