ECDSA signatureValue vnútri CMS kontajnera je DER SEQUENCE { INTEGER r, INTEGER s }. Funkcia Windows CNG BCryptVerifySignature neprijíma ani jedno z toho: chce IEEE P1363 s pevnou šírkou r || s, bez tagov a bez dĺžok. HotPDF, natívna VCL PDF komponenta pre Delphi a C++Builder, konvertuje medzi týmito dvoma podľa prísnych pravidiel DER skôr, než importuje kľúč
Zlyhanie, ktorému toto predchádza, je špecifické a demoralizujúce. Acrobat otvorí dokument a ukáže zelenú fajku. Váš vlastný verifikátor, prechádzajúci tie isté bajty, vráti neplatné, alebo CNG vráti STATUS_INVALID_SIGNATURE bez ďalšieho vysvetlenia. S podpisom nie je nič zle. Zle je to, že približne sedemdesiat bajtov ASN.1 bolo odovzdaných API, ktoré očakávalo šesťdesiatštyri bajtov surového celého čísla, a nesúlad je neviditeľný, pokiaľ neviete, kde ho hľadať
Prečo BCryptVerifySignature odmietne platný ECDSA podpis?
Pretože obe strany volania hovoria rôznymi kódovaniami podpisu a ani jedna to neoznamuje. ISO 32000-1 §12.8 hovorí, že signature slovník nesie CMS blob v /Contents; RFC 5652 §5.3 hovorí, že signatureValue v každom SignerInfo je OCTET STRING, ktorého obsah je čokoľvek, čo definuje algoritmus podpisu. Pre ECDSA je týmto obsahom SEC 1 DER štruktúra: SEQUENCE držiaca dve INTEGER. Je variabilnej dĺžky podľa návrhu, pretože r a s sú celé čísla a DER odstraňuje vedúce nulové oktety z celých čísel
IEEE P1363 zastáva opačný pohľad. Definuje podpis ako zreťazenie dvoch súradníc, každá zľava doplnená nulami presne na bajtovú šírku poľa krivky. Podpis P-256 má vždy 64 bajtov. DER kódovanie toho istého podpisu má normálne 70 alebo 71 bajtov a môže byť kdekoľvek od približne 8 do 72. Odovzdajte DER formu funkcii BCryptVerifySignature a samotná kontrola dĺžky volanie odsúdi, čo je dôvod, prečo HotPDF normalizuje pred overením, nie po ňom
uses
HPDFECDSA;
// Converts a CMS signatureValue into the fixed-width form CNG expects.
// ARaw comes back as 64 bytes for P-256, 96 for P-384, 132 for P-521.
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
out ARaw: TBytes): Boolean;
begin
Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;
Pravidlá DER, ktoré parser podpisu nesmie uvoľniť
Každé tu uvedené odmietnutie je odmietnutie, ktoré HotPDF vykonáva zámerne, a každé z nich uzatvára cestu, ktorú by zhovievavý parser nechal otvorenú. Pokušením pri písaní konvertora je nájsť dva uzly INTEGER, skopírovať ich obsah a pokračovať ďalej. To funguje na dobre tvorenom vstupe a potichu prijíma rodinu tvárnych (malleable) prekódovaní na nepriateľskom vstupe. Takže HPDFECDSANormalizeSignature odmietne záporné celé číslo, teda akékoľvek r alebo s, ktorého prvý obsahový oktet má nastavený vysoký bit, pretože platný ECDSA skalár je kladný. Odmietne hodnotu, ktorá je celá nula, keďže r = 0 alebo s = 0 nikdy nie je legitímny podpis. Odmietne nadbytočný vedúci nulový oktet: X.690 §8.3 povoľuje presne jeden, a len vtedy, keď by sa ďalší oktet inak čítal ako záporný, takže 00 nasledované oktetom pod 0x80 je prekódovanie, nie podpis. Odmietne neminimálnu hlavičku dĺžky, pretože X.690 §10.1 vyžaduje definitívnu formu kódovanú v čo najmenej oktetoch, a long-form dĺžka, ktorá mohla byť short-form, je iný bajtový reťazec nesúci rovnaký význam. Odmietne celé číslo širšie, než je veľkosť súradnice krivky, keďže taká hodnota nemôže byť prvkom poľa. A odmietne akýkoľvek zvyšný uzol za s, spolu s vonkajším SEQUENCE, ktorého celková dĺžka sa nerovná dĺžke celého blobu
Na tých posledných dvoch záleží viac, než sa zdá. Zvyšné bajty za SEQUENCE sú klasický trik tvárnosti podpisu (malleability): pripojte odpad, a zhovievavý verifikátor stále povie platné, kým bajtový reťazec, ktorý validoval, nie je bajtový reťazec, ktorý bol podpísaný. Rovnaký inštinkt ženie posilnenie dĺžky ASN.1 popísané v poznámke o parsovaní PKCS#12, a je to ten istý inštinkt aj tu. Vo verifikačnej ceste je akceptovaná štruktúra, ktorú nikdy nevydal konformný signer, chyba, nie zdvorilosť
Šírka súradnice patrí krivke, nie podpisu
HotPDF odvádza výstupnú šírku z OID pomenovanej krivky, nikdy z dĺžky práve parsovaného DER. Toto je druhá polovica konverzie a polovica, ktorú je ľahké jemne pokaziť. RFC 5480 §2.1.1 identifikuje krivku v parametroch SubjectPublicKeyInfo certifikátu, a HPDFECDSACurveFromOID mapuje tri OID, ktoré HotPDF podporuje: 1.2.840.10045.3.1.7 pre P-256, 1.3.132.0.34 pre P-384 a 1.3.132.0.35 pre P-521. HPDFECDSACoordinateSize potom vráti 32, 48 alebo 66 bajtov, a P1363 buffer je dvojnásobok toho: 64, 96 alebo 132. Každé dekódované celé číslo je zarovnané doprava do svojej polovice, takže krátke r je zľava doplnené nulami, nie posunuté. P-521 je ten, ktorý ľudí chytá, pretože 521 bitov je 65,125 bajtu a zaokrúhľuje sa nahor na 66, čo dáva 132-bajtový podpis, ktorý by žiadna intuícia mocnín dvojky nepredpovedala. Verejný kľúč cestuje popri tom ako nekomprimovaný EC bod podľa RFC 5480 §2.2, čo je 0x04 nasledované X a Y, takže HotPDF skontroluje, či má presne 1 + 2 * CoordinateSize bajtov a začína 0x04, skôr než sa dotkne CNG
var
Digest, SigDER, PublicPoint: TBytes;
Curve: THPDFECDSACurve;
Res: THPDFECDSAVerifyResult;
begin
// secp256r1, taken from the certificate SubjectPublicKeyInfo parameters
Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');
// PublicPoint must be $04 || X || Y, so 1 + 2 * 32 = 65 bytes for P-256
Res := HPDFECDSAVerifyDigest(Digest, SigDER, PublicPoint, Curve, eseDER);
case Res of
evrValid:
Memo1.Lines.Add('signature verifies');
evrInvalid:
Memo1.Lines.Add('signature does not match the digest');
evrMalformed:
Memo1.Lines.Add('DER encoding or public point rejected');
evrUnsupported:
Memo1.Lines.Add('curve or algorithm not supported here');
evrProviderUnavailable:
Memo1.Lines.Add('bcrypt.dll or the curve provider is missing');
evrProviderError:
Memo1.Lines.Add('CNG returned an unexpected status');
end;
end;
Všimnite si posledný parameter. HPDFECDSAVerifyDigest tiež prijíma eseP1363 pre volajúcich, ktorí už držia podpis s pevnou šírkou, z hardvérového tokenu alebo vzdialenej podpisovacej služby, ktorá vracia surové r || s. Táto cesta stále vynucuje kontrolu dĺžky a nenulovosti na oboch polovicách, takže buffer správnej veľkosti plný núl je odmietnutý namiesto toho, aby prešiel k providerovi
Prečo generický názov ECDSA algoritmu zlyhá na staršom Windows?
Pretože generický názov je novší, než je základňa nasadenia, do ktorej dodávate. CNG vystavuje identifikátor algoritmu ECDSA, ktorý odvodí krivku z importovaného kľúča, a je to čistý spôsob, ako napísať tento kód, ale BCryptOpenAlgorithmProvider je garantovaný vyriešiť ho len na novších verziách Windows. Na staršom stroji volanie open zlyhá, handle providera ostane nil, a každá ECDSA verifikácia vo vašej aplikácii nahlási nepodporované na podpise, ktorý je úplne v poriadku. HotPDF sa vyhne tomuto útesu tak, že namiesto toho otvára identifikátory podľa jednotlivých kriviek. Vyrieši ECDSA_P256, ECDSA_P384 a ECDSA_P521 raz, cachuje jeden provider handle na krivku a zatvára ich pri finalizácii unitu. Každá verifikácia potom robí len lacnú prácu: importuje dočasný verejný kľúč z ECCPUBLICBLOB, zavolá BCryptVerifySignature, zničí kľúč. Žiadne opakované LoadLibrary, žiadne opakované GetProcAddress, žiadne otváranie a zatváranie providera na podpis. Dávková verifikácia niekoľkých stoviek dokumentov ten rozdiel pocíti, a rovnako aj service proces, ktorý by inak pod záťažou vymieňal provider handle
Návratové kódy ostávajú úprimné o tomto rozlíšení. evrProviderUnavailable znamená, že stroj nedokázal HotPDF poskytnúť providera; evrInvalid znamená, že CNG odpovedal STATUS_INVALID_SIGNATURE. Zlúčenie týchto dvoch do jedného zlyhania je spôsob, ako sa problém nasadenia nesprávne nahlási ako sfalšovaný dokument. Rovnaké oddelenie zlyhania prostredia a kryptografického zlyhania prechádza cez spracovanie CNG a CAPI na strane podpisovania, pokryté v článku o podpisovaní zo certificate store a poradí bajtov
Ktorý certifikát toto podpísal? SignerIdentifier sú dve rôzne veci
RFC 5652 §5.3 robí zo SignerIdentifier CHOICE, a verifikátor, ktorý spracuje len jednu vetvu, potichu overí voči nesprávnemu kľúču. Prvou vetvou je issuerAndSerialNumber, SEQUENCE držiaca Name vydavateľa v surovom DER a INTEGER sériové číslo, a jej párovanie je bajtové porovnanie voči každému certifikátu v množine CMS certificates. Druhou vetvou je [0] subjectKeyIdentifier, implicitne tagovaný OCTET STRING, a jej párovanie vyžaduje kopanie do certifikátu namiesto porovnávania jeho hlavičkových polí
Toto kopanie má vrstvu, ktorá ľudí prekvapí. Identifikátor kľúča žije v rozšírení X.509v3, takže HotPDF prejde poľom [3] extensions v tbsCertificate, nájde rozšírenie, ktorého OID je 2.5.29.14, preskočí voliteľný kritický BOOLEAN a vezme OCTET STRING extnValue. Tento bajtový reťazec nie je identifikátor. Podľa RFC 5280 §4.2.1.2 je jeho obsah sám o sebe DER, a typ KeyIdentifier je ďalší OCTET STRING, takže parsujete druhýkrát, aby ste sa dostali k skutočným bajtom. Zastavte sa o vrstvu skôr, a porovnávate 22-bajtový obal proti 20-bajtovému identifikátoru, žiadny certifikát nikdy nesedí, a verifikátor sa vráti k akejkoľvek heuristike, ktorú ste napísali ďalej, čo je skutočné riziko. Zobrať prvý certifikát v množine je lákavá skratka a je nesprávna vždy, keď CMS nesie reťaz, čo je väčšinu času, pretože leaf nemusí byť prvý. HotPDF akceptuje nespárovaný certifikát len vtedy, keď kontajner drží presne jeden; s viacerými prítomnými certifikátmi je povinná presná zhoda SignerIdentifier. Overenie digestu voči verejnému kľúču strednej CA nevyprodukuje priateľskú chybu, vyprodukuje sebavedomé neplatné na dokumente, ktorý je v poriadku
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('signed.pdf') > 0 then
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
if Pdf.VerifyLoadedSignatureEx(I, Info) = svValid then
Memo1.Lines.Add(Format('%s: %s %s over %s, signer %s',
[String(Info.FieldName), String(Info.PublicKeyAlgorithm),
String(Info.CurveName), String(Info.HashAlgorithm),
String(Info.SignerName)]))
else
Memo1.Lines.Add(Format('%s: not valid', [String(Info.FieldName)]));
finally
Pdf.Free;
end;
end;
THPDFSignatureInfo.CurveName nahlási P-256, P-384 alebo P-521, takže audit log zaznamená, ktorá krivka bola skutočne použitá, nie len slovo ECDSA. Zvyšná dokumentová prevádzka okolo tohto volania, konkrétne ako sa hashujú segmenty /ByteRange a prečo sa digest musí počítať cez súbor namiesto parsovaného stromu objektov, je predmetom sprievodného článku o overovaní PDF podpisov
Čo vám toto nedáva
Zelený výsledok z HPDFECDSAVerifyDigest odpovedá len na jednu otázku: tieto bajty boli podpísané súkromným kľúčom zodpovedajúcim tomuto verejnému kľúču. Nehovorí nič o tom, či tento kľúč patrí niekomu, komu by ste mali dôverovať. Budovanie reťaze k trust anchor, revokácia cez CRL alebo OCSP a kontroly politiky sú samostatná práca, a akýkoľvek produkt, ktorý nahlási platný podpis bez nich, nahlasuje menej, než používateľ predpokladá. Dátumy platnosti certifikátu sú vystavené samostatne v THPDFSignatureInfo presne z tohto dôvodu: podpis môže byť kryptograficky platný, kým certifikát, ktorý ho vytvoril, expiroval pred dvoma rokmi. Podpora kriviek je tiež zámerne úzka. Spracované sú tri NIST prime krivky, a podpis nad akoukoľvek inou krivkou vráti nepodporované namiesto hádania. Cesta CNG je len pre Windows, čo je správny obchod pre VCL komponentu, ale stojí za zváženie, skôr než okolo nej naplánujete cross-platform službu. A prísnosť nie je konfigurovateľná: neexistuje zhovievavý mód, ktorý akceptuje neminimálnu dĺžku DER, pretože nejaký starší signer jednu vydal. Ak sa s takým súborom stretnete v produkcii, poctivá reakcia je zaznamenať ho a naháňať producenta, nie rozširovať parser, kým súbor neprejde
Cesta overenia ECDSA tu popísaná sa dodáva ako súčasť štandardnej HotPDF Component pre Delphi a C++Builder, popri cestách RSA PKCS#1 v1.5 a RSA-PSS a kompletnom zázname informácií o podpise; produktová stránka nesie kompletnú referenciu digitálneho podpisu