HotPDF semnează un PDF folosind un certificat aflat deja în magazinul de certificate Windows predând rezumatul (digest) direct sistemului Windows, iar Windows finalizează cererea printr-unul din două backend-uri de cheie privată: CNG, care returnează semnătura RSA în ordine big-endian, sau CSP-ul CryptoAPI vechi, care o returnează little-endian. Confundați cele două și semnătura CMS pe care HotPDF o încorporează va avea octeții inversați pentru backend-ul care a răspuns efectiv, astfel încât un validator conform raportează semnătura ca invalidă, deși octeții documentului nu au fost atinși deloc
În spatele acestei singure propoziții se ascund două probleme fără legătură între ele, iar semnatarul de certificat de sistem al HotPDF trebuie să le rezolve pe amândouă înainte de a semna orice. Discrepanța de ordine a octeților este silențioasă: apelul de semnare tot returnează True, PDF-ul tot se deschide, iar eșecul apare abia când un vizualizator parcurge structura CMS și o respinge. A doua problemă este zgomotoasă și specifică pentru C++Builder: jumătate de duzină de funcții crypt32 refuză să se lege (link), deoarece biblioteca de import livrată de RAD Studio nu le exportă. Niciuna dintre probleme nu apare dacă semnați mereu cu un fișier PFX, motiv pentru care ele tind să prindă din urmă dezvoltatorii care trec de la semnarea într-un singur apel bazată pe PFX la un certificat deja instalat de departamentul IT în profilul utilizatorului
Selectarea unui certificat din magazin
HotPDF expune această cale prin HPDFSignPDFStreamWithSystemCertificate și HPDFSignPDFFileWithSystemCertificate, ambele guvernate de o înregistrare THPDFCertificateStoreSelector: Location (cslCurrentUser sau cslLocalMachine), StoreName ('MY', magazinul personal, implicit), o Thumbprint SHA-1 și un indicator AllowUI. Amprenta (thumbprint) este normalizată intern, astfel încât cratimele sau spațiile copiate direct din interfața Certificate Manager sunt eliminate înainte de a rula comparația
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 contează mai mult decât pare, pentru că se mapează direct la CRYPT_ACQUIRE_SILENT_FLAG, iar Windows respectă asta la literă: dacă cheia privată a certificatului potrivit se află pe un card inteligent sau un token care necesită un prompt PIN pe care Windows nu l-a memorat deja, CryptAcquireCertificatePrivateKey eșuează în loc să afișeze un dialog dintr-un proces care ar putea fi un serviciu. Acest eșec este zgomotos, o excepție EHPDFCMSError pe care o vedeți imediat, dar este ușor de interpretat greșit ca „certificat negăsit”, când cauza reală este un token care așteaptă un PIN pe care nimeni nu-l va introduce
De ce nu sunt CNG și CAPI de acord asupra ordinii octeților?
Care backend răspunde nu este o presupunere: CryptAcquireCertificatePrivateKey raportează asta direct printr-un parametru de ieșire KeySpec, iar exact această valoare este cea pe care se bazează ramificarea semnatarului HotPDF. O cheie de tip CNG Key Storage Provider revine cu KeySpec setat la sentinela CERT_NCRYPT_KEY_SPEC ($FFFFFFFF); orice altă valoare indică o cheie CSP CryptoAPI tradițională. Majoritatea certificatelor personale emise sau importate pe o instalare Windows curentă se rezolvă la CNG, chiar dacă un shim CSP vechi mai există pentru compatibilitate, motiv pentru care HotPDF solicită CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG împreună cu CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG înainte de a examina valoarea returnată
Cele două backend-uri nu doar apelează funcții diferite, NCryptSignHash pentru o cheie CNG, CryptSignHashA pentru o cheie CSP; ele returnează semnătura RSA brută în ordine opusă a octeților. Ieșirea CNG se potrivește deja cu ce așteaptă PKCS#1: un șir de octeți big-endian, cu octetul cel mai semnificativ primul, exact ce produce conversia I2OSP din RFC 8017 și ce necesită un SignerInfo CMS (RFC 5652) în câmpul său de semnătură conform ISO 32000-1 §12.8.3. CryptSignHash din CryptoAPI, în schimb, returnează semnătura little-endian, o particularitate documentată care își are originea în modul în care CSP-urile clasice reprezentau intern numerele mari. Săriți peste inversare pe calea CAPI și fiecare octet din semnătură ajunge în locul greșit; matematica RSA rămâne corectă, dar șirul de octeți pe care îl citește un verificator nu este cel definit de 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;
Ce se întâmplă cu un callback de semnare personalizat?
Oricine ocolește semnatarul încorporat de certificat de sistem al HotPDF moștenește aceeași regulă privind ordinea octeților. HPDFCMSSignPDFStreamWithExternalSigner preia un THPDFCMSSignDigestCallback, o închidere (closure) de tipul reference to function(const SignedAttributesSHA256: TBytes): TBytes, pentru semnare printr-un HSM, un stack middleware de card inteligent sau orice altceva care nu este un certificat pentru care magazinul Windows vă poate oferi un handle de cheie. Indiferent de backend-ul din spatele acelui callback, octeții pe care îi returnează trebuie să ajungă în ordine big-endian înainte ca HotPDF să îi integreze în structura 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);
Merită precizat aici o limită explicită: cele două căi de semnare încorporate ale HotPDF, CNG prin NCryptSignHash cu padding PKCS#1 și CAPI prin CryptSignHashA, vizează amândouă chei RSA care semnează un digest SHA-256 de 32 de octeți. Niciuna nu negociază un format de semnătură ECDSA. Un certificat a cărui cheie privată este bazată pe curbe eliptice (EC) are nevoie de un semnatar scris chiar de dvs. prin HPDFCMSSignPDFStreamWithExternalSigner, codificând semnătura ECDSA în modul așteptat de CMS, nu presupunând un șir de octeți RSA de lungime fixă, așa că nu vă așteptați ca semnatarul încorporat de certificat de sistem să funcționeze corect pentru un token provizionat cu un certificat EC
De ce eșuează C++Builder la linkarea CertOpenStore?
Pentru că biblioteca de import implicită a C++Builder din RAD Studio, import32.lib, nu exportă CertOpenStore și nici cinci dintre funcțiile învecinate: CertEnumCertificatesInStore, CertGetCertificateContextProperty, CertFreeCertificateContext, CertCloseStore și CryptAcquireCertificatePrivateKey. Build-urile Delphi nu întâmpină niciodată această problemă, pentru că dcc32/dcc64 rezolvă un import static external 'crypt32.dll' direct în tabelul de import PE. C++Builder este diferit: compilatorul Delphi emite un fișier .obj OMF pentru build-ul pachetului, ilink32 îl linkează, iar în acel moment aceeași declarație external devine doar un simbol nerezolvat care așteaptă o bibliotecă de import în linia de comandă. Îndreptarea linkerului către directorul psdk al Windows SDK, unde crypt32.lib complet exportă efectiv toate cele șase simboluri, nu rezolvă nici asta: ilink32 linkează doar bibliotecile de import numite efectiv în linia sa de comandă, import32.lib cp32mt.lib implicit, iar adăugarea unei căi de căutare nu îl face să preia ceva suplimentar de acolo. Rularea tdump asupra import32.lib confirmă direct lipsa, zero rezultate pentru CertOpenStore, față de șase potriviri clare în crypt32.lib din SDK
HotPDF rezolvă asta la fel cum gestionează deja enumerarea certificatelor în altă parte a bibliotecii: în loc să ceară linkerului aceste simboluri, le încarcă la runtime. O înregistrare internă THPDFCryptoProcs transportă un handle crypt32.dll, un handle advapi32.dll și unsprezece câmpuri de pointeri de funcții; LoadCryptoProcs încarcă ambele DLL-uri și rezolvă fiecare punct de intrare cu GetProcAddress exact o singură dată, la începutul HPDFSignPDFStreamWithSystemCertificate, ridicând imediat EHPDFCMSError dacă lipsește ceva, în loc să eșueze mai târziu cu o încălcare de acces în adâncul fluxului de semnare
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;
Încărcarea se face o singură dată per apel, nu leneș în interiorul fiecărui helper, pentru că închiderea (closure) care alege între CNG și CAPI capturează tabelul de funcții încărcat prin valoare și trebuie să rămână validă pe tot parcursul fluxului de semnare, inclusiv pentru callback-ul din HPDFCMSSignPDFStreamWithExternalSigner; ambele handle-uri de DLL sunt eliberate în blocul finally cel mai exterior odată ce semnarea se termină sau ridică o excepție. Nimic din toate acestea nu afectează suprafața publică: HPDFSignPDFStreamWithSystemCertificate, HPDFSignPDFFileWithSystemCertificate și THPDFCertificateStoreSelector își păstrează exact semnăturile pe care le aveau înainte, așa că preluarea corecției înseamnă doar o recompilare pentru codul apelant existent, nu o modificare de cod
Ce nu acoperă acest lucru
Obținerea unei ordini corecte a octeților și rezolvarea linkării în C++Builder produc un SignerInfo CMS pe care un validator îl poate analiza și o semnătură pe care o poate verifica aritmetic; nu spune nimic despre dacă acel validator ar trebui să aibă încredere în certificatul din spate, deoarece construirea lanțului, verificarea revocării și politica de marcă temporală sunt aspecte separate, adăugate deasupra prin opțiunile CMS, nu ceva ce corectitudinea ordinii octeților oferă gratuit. Două detalii de întreținere contează la fel de mult ca și criptografia: PCCERT_CONTEXT returnat de căutarea certificatului trebuie eliberat cu CertFreeCertificateContext înainte ca magazinul să se închidă, iar un handle de cheie CNG sau CSP obținut, atunci când API-ul raportează că apelantul îl deține, trebuie eliberat prin apelul propriu al backend-ului corespunzător, niciodată prin celălalt. Dacă rezultatul svValid obținut după toate acestea se dovedește mai restrâns decât v-ați așteptat, articolul despre verificarea semnăturilor digitale PDF explică exact ce promite și ce nu promite acest indicator. Pentru că certificatul rămâne tot timpul în custodia Windows aici, semnarea din magazinul de certificate evită o întreagă suprafață de atac: nu există niciun fișier PKCS#12 de analizat și niciun ASN.1 de parcurs manual, ceea ce reprezintă exact problema pe care întărirea PKCS#12 și ASN.1 a HotPDF o rezolvă pentru calea de semnare bazată pe fișier PFX
Semnarea din magazinul de certificate, semnarea PFX și callback-urile de semnatar extern sunt trei uși către același pipeline CMS/PKCS#7 din componenta PDF HotPDF pentru Delphi și C++Builder, iar alegerea celei potrivite ține în cea mai mare parte de cine are voie să dețină cheia privată: procesul dvs., un fișier PFX sau Windows însuși