ECDSA signatureValue unutar CMS kontejnera je DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG funkcija BCryptVerifySignature ne prihvata nijedno od toga: ona želi IEEE P1363 fiksne širine r || s, bez tagova i bez dužina. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, konvertuje između to dvoje po strogim DER pravilima pre nego što uveze ključ
Kvar koji ovo sprečava je specifičan i obeshrabrujući. Acrobat otvori dokument i pokaže zelenu kvačicu. Vaš sopstveni verifikator, obilazeći iste bajtove, vraća nevažeći, ili CNG vrati STATUS_INVALID_SIGNATURE bez daljeg objašnjenja. Ništa nije pogrešno sa potpisom. Pogrešno je to što je otprilike sedamdeset bajtova ASN.1 prosleđeno API-ju koji je očekivao šezdeset četiri bajta sirovog celog broja, a neusklađenost je nevidljiva ako ne znate da je tražite
Zašto BCryptVerifySignature odbija validan ECDSA potpis?
Zato što dve strane poziva govore različitim enkodiranjima potpisa, a nijedna to ne najavljuje. ISO 32000-1 §12.8 kaže da rečnik potpisa nosi CMS blob u /Contents; RFC 5652 §5.3 kaže da je signatureValue u svakom SignerInfo OCTET STRING čiji je sadržaj šta god algoritam potpisa definiše. Za ECDSA taj sadržaj je SEC 1 DER struktura: SEQUENCE koja drži dva INTEGER-a. Namerno je promenljive dužine, jer su r i s celi brojevi, a DER uklanja vodeće nula-oktete iz celih brojeva
IEEE P1363 zauzima suprotan stav. Definiše potpis kao spajanje dve koordinate, svaka popunjena nulama sleva do tačno bajt-širine polja krive. P-256 potpis je uvek 64 bajta. DER enkodiranje istog potpisa je obično 70 ili 71 bajt i može biti bilo gde od otprilike 8 do 72. Predajte DER oblik funkciji BCryptVerifySignature i sama provera dužine osuđuje poziv na propast, zato HotPDF normalizuje pre nego što verifikuje, a ne posle
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;
DER pravila koja parser potpisa ne sme da olabavi
Svako odbijanje navedeno ovde je odbijanje koje HotPDF izvršava namerno, i svako od njih zatvara putanju koju bi popustljiv parser ostavio otvorenom. Iskušenje pri pisanju konvertora je da se pronađu dva INTEGER čvora, kopira njihov sadržaj, i nastavi dalje. To radi na dobro formiranom inputu, a tiho prihvata čitavu porodicu podložnih ponovnih enkodiranja na neprijateljskom inputu. Zato HPDFECDSANormalizeSignature odbija negativan ceo broj, odnosno svaki r ili s čiji prvi sadržajni oktet ima postavljen visok bit, jer je validan ECDSA skalar pozitivan. Odbija vrednost koja je u potpunosti nula, pošto r = 0 ili s = 0 nikada nije legitiman potpis. Odbija suvišan vodeći nula-oktet: X.690 §8.3 dozvoljava tačno jedan, i to samo kada bi se sledeći oktet inače čitao kao negativan, pa je 00 praćen oktetom ispod 0x80 ponovno enkodiranje, ne potpis. Odbija neminimalno zaglavlje dužine, jer X.690 §10.1 zahteva definitivni oblik enkodiran u najmanjem broju okteta, a long-form dužina koja je mogla biti short-form je drugačiji niz bajtova koji nosi isto značenje. Odbija ceo broj širi od veličine koordinate krive, jer takva vrednost ne može biti element polja. I odbija svaki naknadni čvor posle s, zajedno sa spoljnim SEQUENCE čija ukupna dužina nije jednaka dužini celog bloba
Ta poslednja dva znače više nego što deluju. Zaostali bajtovi posle SEQUENCE su klasičan trik malleability potpisa: dodajte smeće, i popustljiv verifikator i dalje kaže validan, dok niz bajtova koji je validirao nije niz bajtova koji je bio potpisan. Isti instinkt pokreće otvrdnjavanje ASN.1 dužina opisano u belešci o parsiranju PKCS#12, i isti je instinkt ovde. U putanji verifikacije, prihvaćena struktura koju usaglašen potpisnik nikada nije emitovao je defekt, ne ljubaznost
Širina koordinate pripada krivoj, ne potpisu
HotPDF izvodi izlaznu širinu iz imenovanog OID-a krive, nikad iz dužine DER-a koji je upravo parsirao. Ovo je druga polovina konverzije i ona koju je lako suptilno pogrešno uraditi. RFC 5480 §2.1.1 identifikuje krivu u parametrima SubjectPublicKeyInfo sertifikata, a HPDFECDSACurveFromOID mapira tri OID-a koje HotPDF podržava: 1.2.840.10045.3.1.7 za P-256, 1.3.132.0.34 za P-384, i 1.3.132.0.35 za P-521. HPDFECDSACoordinateSize zatim vraća 32, 48, ili 66 bajtova, a P1363 bafer je duplo veći: 64, 96, ili 132. Svaki dekodiran ceo broj je desno-poravnat u svojoj polovini, tako da se kratak r popunjava nulama sleva umesto da se pomera. P-521 je ta koja hvata ljude u zamku, jer je 521 bit 65,125 bajta i zaokružuje se na 66, dajući potpis od 132 bajta koji nijedna intuicija stepena dvojke ne bi predvidela. Javni ključ putuje uz njega kao nekompresovana EC tačka prema RFC 5480 §2.2, što je 0x04 praćen sa X i Y, pa HotPDF proverava da je tačno 1 + 2 * CoordinateSize bajtova i da počinje sa 0x04 pre nego što dotakne 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;
Obratite pažnju na poslednji parametar. HPDFECDSAVerifyDigest takođe prihvata eseP1363 za pozivaoce koji već drže potpis fiksne širine, iz hardverskog tokena ili udaljenog servisa za potpisivanje koji vraća sirov r || s. Ta putanja i dalje sprovodi proveru dužine i proveru da nijedna polovina nije nula, pa se bafer ispravne veličine pun nula odbija, umesto da se prosledi provajderu
Zašto generičko ime ECDSA algoritma otkazuje na starijem Windowsu?
Zato što je generičko ime novije od bazne verzije na koju isporučujete. CNG izlaže identifikator algoritma ECDSA koji izvodi krivu iz uvezenog ključa, i to je čist način da se ovaj kod napiše, ali BCryptOpenAlgorithmProvider je garantovano razrešava samo na novijim verzijama Windowsa. Na starijoj mašini poziv za otvaranje otkazuje, handle provajdera ostaje nil, i svaka ECDSA verifikacija u vašoj aplikaciji prijavljuje nepodržano na potpisu koji je savršeno dobar. HotPDF izbegava tu provaliju tako što umesto toga otvara identifikatore po krivoj. Razrešava ECDSA_P256, ECDSA_P384, i ECDSA_P521 jednom, kešira po jedan handle provajdera po krivoj, i zatvara ih u finalizaciji unita. Svaka verifikacija zatim radi samo jeftin posao: uveze privremeni javni ključ iz ECCPUBLICBLOB, pozove BCryptVerifySignature, uništi ključ. Bez ponovljenog LoadLibrary, bez ponovljenog GetProcAddress, bez otvaranja i zatvaranja provajdera po potpisu. Batch verifikacija nekoliko stotina dokumenata oseti razliku, kao i uslužni proces koji bi inače trošio handle-ove provajdera pod opterećenjem
Kodovi rezultata ostaju iskreni o toj razlici. evrProviderUnavailable znači da mašina nije mogla da da HotPDF-u provajdera; evrInvalid znači da je CNG odgovorio sa STATUS_INVALID_SIGNATURE. Spajanje ta dva u jedan neuspeh je način na koji se problem sa deploymentom pogrešno prijavljuje kao falsifikovan dokument. Isto razdvajanje između greške okruženja i kriptografske greške provlači se kroz obradu CNG i CAPI na strani potpisivanja, pokrivenu u članku o potpisivanju iz skladišta sertifikata i redosledu bajtova
Ko je potpisao ovo? SignerIdentifier su dve različite stvari
RFC 5652 §5.3 čini SignerIdentifier tipom CHOICE, a verifikator koji obrađuje samo jednu granu će tiho verifikovati protiv pogrešnog ključa. Prva grana je issuerAndSerialNumber, SEQUENCE koja drži Name izdavaoca u sirovom DER-u i serijski INTEGER, a poklapanje se svodi na poređenje bajtova prema svakom sertifikatu u CMS skupu certificates. Druga grana je [0] subjectKeyIdentifier, implicitno tagovan OCTET STRING, a njegovo poklapanje zahteva kopanje po sertifikatu, a ne poređenje polja njegovog zaglavlja
To kopanje ima sloj koji ljude iznenadi. Identifikator ključa živi u X.509v3 ekstenziji, pa HotPDF obilazi polje [3] ekstenzija tbsCertificate-a, pronalazi ekstenziju čiji je OID 2.5.29.14, preskače opcioni kritički BOOLEAN, i uzima extnValue OCTET STRING. Taj niz okteta nije identifikator. Prema RFC 5280 §4.2.1.2 njegov sadržaj je sam po sebi DER, a tip KeyIdentifier je još jedan OCTET STRING, pa se parsira drugi put da bi se stiglo do stvarnih bajtova. Stanite jedan sloj ranije i upoređujete 22-bajtni omotač sa 20-bajtnim identifikatorom, nijedan sertifikat se nikada ne poklapa, a verifikator pada nazad na kakvu god heuristiku ste sledeće napisali, što je stvarna opasnost. Uzimanje prvog sertifikata u skupu je zamamna prečica i pogrešna je kad god CMS nosi lanac, što je najčešće, jer list nije obavezan da bude prvi. HotPDF prihvata nepoklopljen sertifikat samo kada kontejner sadrži tačno jedan; sa više prisutnih sertifikata, tačno poklapanje SignerIdentifier je obavezno. Verifikacija digesta protiv javnog ključa posredničkog CA ne proizvodi prijateljsku grešku, proizvodi samouverenu nevažeću verifikaciju na dokumentu koji je u redu
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 prijavljuje P-256, P-384, ili P-521, tako da audit log beleži koja je kriva zaista korišćena, a ne samo reč ECDSA. Instalacija na nivou dokumenta oko ovog poziva, konkretno kako se segmenti /ByteRange heširaju i zašto digest mora biti izračunat preko fajla, a ne preko parsiranog stabla objekata, tema je pratećeg članka o verifikaciji PDF potpisa
Šta ovo ne daje
Zeleni rezultat iz HPDFECDSAVerifyDigest odgovara samo na jedno pitanje: ove bajtove je potpisao privatni ključ koji odgovara ovom javnom ključu. Ne govori ništa o tome da li taj ključ pripada nekome kome biste trebalo da verujete. Izgradnja lanca do sidra poverenja, opoziv kroz CRL ili OCSP, i provere politike su odvojen posao, a svaki proizvod koji prijavljuje validan potpis bez toga prijavljuje manje nego što korisnik pretpostavlja. Datumi važenja sertifikata se iz istog razloga posebno izlažu u THPDFSignatureInfo: potpis se može kriptografski verifikovati dok je sertifikat koji ga je napravio istekao pre dve godine. Podrška za krive je takođe namerno uska. Obrađuju se tri NIST prime krive, a potpis preko bilo koje druge krive vraća nepodržano umesto pogađanja. CNG putanja je isključivo za Windows, što je ispravna trampa za VCL komponentu, ali vredi je naglasiti pre nego što oko nje planirate cross-platform servis. A strogost nije podesiva: ne postoji popustljiv mod koji prihvata neminimalnu DER dužinu zato što ju je neki zastareo potpisnik emitovao. Ako naiđete na takav fajl u produkciji, iskren odgovor je da ga zabeležite i potražite objašnjenje od proizvođača, ne da proširujete parser dok fajl ne prođe
Putanja ECDSA verifikacije opisana ovde isporučuje se kao deo standardne HotPDF Component za Delphi i C++Builder, zajedno sa RSA PKCS#1 v1.5 i RSA-PSS putanjama i kompletnim zapisom informacija o potpisu; proizvodna stranica nosi kompletnu referencu digitalnih potpisa