HotPDF podpiše PDF s potrdilom, ki je že v shrambi potrdil Windows, tako da povzetek posreduje samemu sistemu Windows, ta pa zahtevo izvede prek enega od dveh zaledij zasebnih ključev: CNG vrne podpis RSA v vrstnem redu big-endian, podedovani CryptoAPI CSP pa v vrstnem redu little-endian. Če ju zamenjate, je podpis CMS, ki ga HotPDF vstavi, obrnjen glede na zaledje, ki je dejansko odgovorilo, zato skladno preverjevalno orodje poroča, da je podpis neveljaven, čeprav bajti dokumenta niso bili nikoli spremenjeni
Za tem enim stavkom se skrivata dve nepovezani težavi, sistemski podpisovalnik potrdil HotPDF pa mora rešiti obe, preden sploh začne podpisovati. Neujemanje vrstnega reda bajtov je tiho: klic za podpisovanje še vedno vrne True, PDF se še vedno odpre, napaka pa se pokaže šele, ko pregledovalnik prebere strukturo CMS in jo zavrne. Druga težava je glasna in značilna za C++Builder: približno pol ducata funkcij crypt32 se noče povezati, ker uvozna knjižnica, ki jo priloži RAD Studio, teh funkcij ne izvozi. Nobena od teh težav ne obstaja, če vedno podpisujete samo z datoteko PFX, zato pogosto ujame razvijalce, ki prehajajo s podpisovanja na osnovi PFX z enim klicem na potrdilo, ki ga je oddelek IT že namestil v uporabnikov profil
Izbira potrdila iz shrambe
HotPDF to pot izpostavi prek HPDFSignPDFStreamWithSystemCertificate in HPDFSignPDFFileWithSystemCertificate, oba pa poganja zapis THPDFCertificateStoreSelector: Location (cslCurrentUser ali cslLocalMachine), StoreName ('MY', privzeta osebna shramba), prstni odtis SHA-1 Thumbprint in zastavica AllowUI. Prstni odtis se interno normalizira, zato se vezaji ali presledki, kopirani neposredno iz uporabniškega vmesnika upravitelja potrdil, odstranijo, preden se izvede primerjava
var
Selector: THPDFCertificateStoreSelector;
Options: THPDFCMSSignOptions;
begin
Selector := THPDFCertificateStoreSelector.Default; // cslCurrentUser, store 'MY'
Selector.Thumbprint := 'A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0';
Selector.AllowUI := False;
Options := HPDFCMSDefaultOptions(palBaseline_B_B);
if not HPDFSignPDFFileWithSystemCertificate('invoice.pdf',
'invoice-signed.pdf', Selector, Options) then
raise Exception.Create('Certificate-store signing failed');
end;
AllowUI = False je pomembnejši, kot se zdi, saj se neposredno preslika v CRYPT_ACQUIRE_SILENT_FLAG in Windows to upošteva dobesedno: če je zasebni ključ ujemajočega se potrdila na pametni kartici ali žetonu, ki potrebuje poziv za PIN, Windows pa ga še ni shranil v predpomnilnik, CryptAcquireCertificatePrivateKey spodleti, namesto da bi odprl pogovorno okno iz morebitnega storitvenega procesa. Ta napaka je glasna, gre za EHPDFCMSError, ki ga takoj vidite, vendar jo je mogoče zlahka napačno razumeti kot »potrdilo ni bilo najdeno«, čeprav je pravi vzrok žeton, ki čaka na PIN, ki ga nihče ne bo vnesel
Zakaj se CNG in CAPI ne strinjata glede vrstnega reda bajtov?
Katero zaledje odgovori, ni ugibanje: CryptAcquireCertificatePrivateKey to neposredno sporoči prek izhodnega parametra KeySpec, in prav ta ena vrednost določa vejo podpisovalnika HotPDF. Ključ ponudnika shrambe ključev CNG se vrne z vrednostjo KeySpec, nastavljeno na označevalnik CERT_NCRYPT_KEY_SPEC ($FFFFFFFF); vse drugo je tradicionalni ključ CryptoAPI CSP. Večina osebnih potrdil, izdanih ali uvoženih v trenutni namestitvi sistema Windows, se razreši v CNG, čeprav zaradi združljivosti še vedno obstaja podedovani vmesnik CSP, zato HotPDF zahteva CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG skupaj z CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG, preden preveri vrnjeno vrednost
Zaledji ne kličeta le različnih funkcij, NCryptSignHash za ključ CNG in CryptSignHashA za ključ CSP, temveč vrneta surovi podpis RSA v nasprotnem vrstnem redu bajtov. Izhod CNG se že ujema s pričakovanji PKCS#1: gre za oktetni niz big-endian, pri katerem je najpomembnejši bajt prvi, natanko tako, kot ustvari pretvorba I2OSP iz RFC 8017, in tako, kot ga CMS SignerInfo (RFC 5652) potrebuje v svojem polju signature po ISO 32000-1 §12.8.3. CryptoAPI-jev CryptSignHash pa nasprotno vrne podpis little-endian, dokumentirano posebnost, ki izhaja iz načina, kako so klasični CSP-ji interno predstavljali velika števila. Če na poti CAPI preskokate obrat, je vsak bajt podpisa na napačnem mestu; matematika RSA je še vedno pravilna, vendar oktetni niz, ki ga prebere preverjevalnik, ni tisti, ki ga določa PKCS#1
// CryptSignHashA returns the RSA signature least-significant byte first;
// CMS/PKCS#7 (ISO 32000-1 Section 12.8.3) needs it most-significant byte first.
for I := 0 to (Length(Signature) div 2) - 1 do
begin
Temp := Signature[I];
Signature[I] := Signature[High(Signature) - I];
Signature[High(Signature) - I] := Temp;
end;
Kaj pa povratni klic po meri?
Vsakdo, ki zaobide vgrajeni podpisovalnik shrambe potrdil HotPDF, podeduje isto pravilo vrstnega reda bajtov. HPDFCMSSignPDFStreamWithExternalSigner sprejme THPDFCMSSignDigestCallback, zaprtje tipa reference to function(const SignedAttributesSHA256: TBytes): TBytes, za podpisovanje prek HSM-ja, vmesne programske opreme pametne kartice ali česar koli drugega, kar ni potrdilo, za katero lahko shramba Windows posreduje ročico ključa. Ne glede na zaledje za tem povratnim klicem morajo biti vrnjeni bajti v vrstnem redu big-endian, preden jih HotPDF vključi v strukturo CMS
Signer :=
function(const SignedAttributesSHA256: TBytes): TBytes
begin
if UsesCngKeyStorageProvider then
Result := SignWithMyCngKey(SignedAttributesSHA256) // already big-endian
else
Result := ReverseBytes(SignWithMyLegacyToken(SignedAttributesSHA256));
end;
HPDFCMSSignPDFStreamWithExternalSigner(InputStream, OutputStream,
CertificateDER, Signer, Options);
Pri tej meji je vredno biti izrecen: obe vgrajeni poti podpisovanja HotPDF, CNG prek NCryptSignHash z oblazinjenjem PKCS#1 in CAPI prek CryptSignHashA, ciljata ključe RSA, ki podpisujejo 32-bajtni povzetek SHA-256. Nobena ne dogovarja oblike podpisa ECDSA. Potrdilo, katerega zasebni ključ temelji na EC, potrebuje podpisovalnik, ki ga napišete sami glede na HPDFCMSSignPDFStreamWithExternalSigner, pri čemer podpis ECDSA kodira na način, ki ga pričakuje CMS, namesto da bi predpostavil niz RSA s stalno dolžino, zato od vgrajenega podpisovalnika shrambe potrdil ne pričakujte pravilnega ravnanja z žetonom, opremljenim s potrdilom EC
Zakaj C++Builder ne uspe povezati CertOpenStore?
Ker privzeta uvozna knjižnica C++Builderja v RAD Studiu, import32.lib, ne izvozi CertOpenStore niti petih sorodnih funkcij: CertEnumCertificatesInStore, CertGetCertificateContextProperty, CertFreeCertificateContext, CertCloseStore in CryptAcquireCertificatePrivateKey. Gradnje Delphi tega ne opazijo, ker dcc32/dcc64 statični uvoz external 'crypt32.dll' neposredno razrešita v uvozno tabelo PE. C++Builder je drugačen: prevajalnik Delphi za gradnjo paketa izda OMF .obj, ta se poveže z ilink32, takrat pa je ista deklaracija external le še nerazrešen simbol, ki čaka na uvozno knjižnico v ukazni vrstici. Tudi usmerjanje povezovalnika v imenik psdk Windows SDK-ja, kjer celotni crypt32.lib izvaža vseh šest simbolov, tega ne popravi: ilink32 poveže samo uvozne knjižnice, ki so dejansko navedene v njegovi ukazni vrstici, privzeto import32.lib cp32mt.lib, iskalna pot pa ne povzroči samodejnega dodajanja česa drugega iz tega imenika. Zagon tdump nad import32.lib vrzel neposredno potrdi, saj vrne nič zadetkov za CertOpenStore, medtem ko jih SDK-jev crypt32.lib vrne šest
HotPDF to razreši enako, kot drugje v knjižnici že obravnava naštevanje potrdil: namesto da bi povezovalnik prosil za te simbole, jih naloži med izvajanjem. Interni zapis THPDFCryptoProcs vsebuje ročico za crypt32.dll, ročico za advapi32.dll in enajst polj kazalcev na funkcije; LoadCryptoProcs naloži obe DLL-datoteki in z GetProcAddress natanko enkrat razreši vsako vstopno točko, na začetku HPDFSignPDFStreamWithSystemCertificate, ter takoj sproži EHPDFCMSError, če kaj manjka, namesto da bi pozneje odpovedal z napako access violation globoko v postopku podpisovanja
type
TCertOpenStoreFn = function(lpszStoreProvider: Pointer; dwEncodingType: DWORD;
hCryptProv: NativeUInt; dwFlags: DWORD; pvPara: Pointer): HCERTSTORE; stdcall;
var
Crypt32Handle: HMODULE;
CertOpenStore: TCertOpenStoreFn;
begin
Crypt32Handle := LoadLibrary('crypt32.dll');
if Crypt32Handle = 0 then
raise Exception.Create('crypt32.dll could not be loaded');
@CertOpenStore := GetProcAddress(Crypt32Handle, 'CertOpenStore');
// ... use CertOpenStore, then FreeLibrary(Crypt32Handle) when signing returns
end;
Nalaganje se izvede enkrat na klic, ne lenobno v vsakem pomočniku, ker zaprtje, ki izbira med CNG in CAPI, zajame naloženo tabelo funkcij po vrednosti in mora ostati živo ves čas postopka podpisovanja, vključno s povratnim klicem v HPDFCMSSignPDFStreamWithExternalSigner; obe ročici DLL se sprostita v zunanjem bloku finally, ko se podpisovanje konča ali sproži izjemo. Nič od tega se ne dotika javnega vmesnika: HPDFSignPDFStreamWithSystemCertificate, HPDFSignPDFFileWithSystemCertificate in THPDFCertificateStoreSelector ohranijo povsem enake podpise kot prej, zato je prevzem popravka za obstoječe klicatelje le ponovna gradnja, ne sprememba kode
Česa to ne zajema
Pravilni vrstni red bajtov in pravilna povezava v C++Builderju ustvarita CMS SignerInfo, ki ga lahko preverjevalnik razčleni, ter podpis, ki ga lahko aritmetično preveri; to pa ne pove ničesar o tem, ali naj preverjevalnik zaupa potrdilu v ozadju, saj so gradnja verige, preverjanje preklica in politika časovnega žiga ločene zadeve, dodane prek možnosti CMS, ne pa nekaj, kar bi pravilnost vrstnega reda bajtov zagotovila sama od sebe. Dve podrobnosti vzdrževanja sta enako pomembni kot kriptografija: PCCERT_CONTEXT, vrnjen pri iskanju potrdila, je treba sprostiti s CertFreeCertificateContext, preden se shramba zapre, pridobljeno ročico ključa CNG ali CSP pa je treba, kadar API sporoči, da je v lasti klicatelja, sprostiti prek ustreznega klica lastnega zaledja, nikoli prek klica drugega zaledja. Če je rezultat svValid, ki ga dobite po vsem tem, ožji, kot ste pričakovali, članek o preverjanju podpisov PDF natančno razloži, kaj ta zastavica zagotavlja in česa ne. Ker potrdilo ves čas ostane v oskrbi sistema Windows, se podpisovanje iz shrambe potrdil izogne celotni napadalni površini: ni datoteke PKCS#12 za razčlenjevanje in ni ASN.1, po katerem bi morali hoditi sami, kar je težava, ki jo utrjevanje PKCS#12 in ASN.1 v HotPDF obravnava za pot podpisovanja iz datoteke PFX
Podpisovanje iz shrambe potrdil, podpisovanje PFX in povratni klici zunanjega podpisovalnika so tri vrata v isti cevovod CMS/PKCS#7 znotraj komponente HotPDF PDF za Delphi in C++Builder, izbira pravega pa je večinoma odvisna od tega, kdo sme hraniti zasebni ključ: vaš proces, datoteka PFX ali sam Windows