ECDSA signatureValue unutar CMS kontejnera je DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG funkcija BCryptVerifySignature ne prihvaća nijedno od toga: želi IEEE P1363 fiksne širine r || s, bez oznaka i bez duljina. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, pretvara između to dvoje prema strogim DER pravilima prije uvoza ključa
Kvar koji se ovime sprječava specifičan je i obeshrabrujući. Acrobat otvori dokument i pokaže zelenu kvačicu. Vaš vlastiti verifikator, koji prolazi kroz iste bajtove, vraća nevažeći, ili CNG vraća STATUS_INVALID_SIGNATURE bez ikakvog daljnjeg objašnjenja. Ništa nije pogrešno s potpisom. Ono što je pogrešno je da je otprilike sedamdeset bajtova ASN.1 proslijeđeno API-ju koji je očekivao šezdeset i četiri bajta sirovog cijelog broja, a neusklađenost je nevidljiva osim ako ne znate da je treba tražiti
Zašto BCryptVerifySignature odbija valjani ECDSA potpis?
Zato što dvije strane poziva govore različitim kodiranjima potpisa, i nijedna to ne najavljuje. ISO 32000-1 §12.8 kaže da rječ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 ono što algoritam potpisa definira. Za ECDSA taj sadržaj je SEC 1 DER struktura: SEQUENCE koji sadrži dva INTEGER-a. Namjerno je promjenjive duljine, jer su r i s cijeli brojevi, a DER uklanja vodeće nul-oktete iz cijelih brojeva
IEEE P1363 zauzima suprotno stajalište. Definira potpis kao spajanje dviju koordinata, svaka nadopunjena nulama slijeva do točno širine polja krivulje u bajtovima. P-256 potpis je uvijek 64 bajta. DER kodiranje istog potpisa obično je 70 ili 71 bajt i može biti bilo gdje od otprilike 8 do 72. Predajte DER oblik funkciji BCryptVerifySignature i sama provjera duljine osuđuje poziv na propast, zato HotPDF normalizira prije provjere, a ne poslije
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 smije olabaviti
Svako odbijanje navedeno ovdje je odbijanje koje HotPDF namjerno provodi, a svako zatvara put koji bi popustljiv parser ostavio otvorenim. Iskušenje pri pisanju konvertera je pronaći dva INTEGER čvora, kopirati njihov sadržaj i nastaviti dalje. To radi na dobro oblikovanom ulazu, a tiho prihvaća obitelj savitljivih ponovnih kodiranja na neprijateljskom ulazu. Zato HPDFECDSANormalizeSignature odbija negativan cijeli broj, odnosno bilo koji r ili s čiji prvi oktet sadržaja ima postavljen visoki bit, jer je valjani ECDSA skalar pozitivan. Odbija vrijednost koja je u potpunosti nula, budući da r = 0 ili s = 0 nikad nije legitiman potpis. Odbija suvišan vodeći nul-oktet: X.690 §8.3 dopušta točno jedan, i to samo kad bi se sljedeći oktet inače čitao kao negativan, pa je 00 nakon kojeg slijedi oktet ispod 0x80 ponovno kodiranje, a ne potpis. Odbija ne-minimalno zaglavlje duljine, jer X.690 §10.1 zahtijeva definitivan oblik kodiran u najmanjem broju okteta, a duga forma duljine koja je mogla biti kratka forma je drugi niz bajtova koji nosi isto značenje. Odbija cijeli broj širi od veličine koordinate krivulje, budući da ta vrijednost ne može biti element polja. I odbija bilo koji čvor koji slijedi nakon s, zajedno s vanjskim SEQUENCE čija ukupna duljina nije jednaka duljini cijelog bloba
Ta posljednja dva su važnija nego što izgledaju. Preostali bajtovi nakon SEQUENCE klasičan su trik savitljivosti potpisa: dodajte smeće, a popustljiv verifikator i dalje kaže valjano dok niz bajtova koji je validirao nije niz bajtova koji je bio potpisan. Isti instinkt pokreće pojačavanje duljine ASN.1 opisano u bilješci o parsiranju PKCS#12, i isti je instinkt ovdje. U putu provjere, prihvaćena struktura koju nijedan usklađeni potpisnik nikad nije emitirao je nedostatak, a ne ljubaznost
Širina koordinate pripada krivulji, a ne potpisu
HotPDF izvodi izlaznu širinu iz imenovanog OID-a krivulje, nikad iz duljine DER-a koji je upravo parsirao. Ovo je druga polovica pretvorbe i polovica koju je lako suptilno pogrešno napraviti. RFC 5480 §2.1.1 identificira krivulju u SubjectPublicKeyInfo parametrima certifikata, 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 tada vraća 32, 48 ili 66 bajtova, a P1363 spremnik je dvostruko toliko: 64, 96 ili 132. Svaki dekodirani cijeli broj poravnava se desno u svojoj polovici, pa se kratki r nadopunjuje nulama slijeva umjesto da se pomiče. P-521 je onaj koji uhvati ljude, jer je 521 bit 65,125 bajta i zaokružuje se na 66, dajući potpis od 132 bajta koji nijedna intuicija potencije broja dva ne bi predvidjela. Javni ključ putuje uz to kao nekomprimirana EC točka prema RFC 5480 §2.2, što je 0x04 praćen X-om i Y-om, pa HotPDF provjerava da je točno 1 + 2 * CoordinateSize bajtova i da počinje s 0x04 prije 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;
Primijetite posljednji parametar. HPDFECDSAVerifyDigest također prihvaća eseP1363 za pozivatelje koji već drže potpis fiksne širine, iz hardverskog tokena ili udaljene usluge potpisivanja koja vraća sirovi r || s. Taj put i dalje provodi provjeru duljine i provjeru nenulte vrijednosti na obje polovice, pa je spremnik ispravne veličine pun nula odbijen, a ne proslijeđen pružatelju
Zašto generičko ime ECDSA algoritma otkaže na starijem Windowsu?
Zato što je generičko ime novije od baze implementacije u koju ga isporučujete. CNG izlaže identifikator algoritma ECDSA koji zaključuje krivulju iz uvezenog ključa, i to je čist način pisanja ovog koda, ali BCryptOpenAlgorithmProvider garantirano ga razrješava samo na novijim verzijama Windowsa. Na starijem stroju poziv otvaranja ne uspije, ručka pružatelja ostaje nil, a svaka ECDSA provjera u vašoj aplikaciji prijavljuje nepodržano na potpisu koji je savršeno dobar. HotPDF izbjegava taj ponor otvarajući identifikatore po krivulji umjesto toga. Razrješava ECDSA_P256, ECDSA_P384 i ECDSA_P521 jednom, sprema po jednu ručku pružatelja po krivulji i zatvara ih pri finalizaciji jedinice. Svaka provjera tada radi samo jeftin posao: uveze privremeni javni ključ iz ECCPUBLICBLOB, pozove BCryptVerifySignature, uništi ključ. Nema ponovljenog LoadLibrary, nema ponovljenog GetProcAddress, nema otvaranja i zatvaranja pružatelja po potpisu. Serijska provjera nekoliko stotina dokumenata osjeti razliku, kao i uslužni proces koji bi inače trošio ručke pružatelja pod opterećenjem
Povratni kodovi ostaju iskreni o razlici. evrProviderUnavailable znači da stroj nije mogao dati HotPDF-u pružatelja; evrInvalid znači da je CNG odgovorio s STATUS_INVALID_SIGNATURE. Sažimanje ta dva u jedan neuspjeh je način na koji se problem implementacije pogrešno prijavi kao krivotvoreni dokument. Ista razdioba između neuspjeha okoline i kriptografskog neuspjeha proteže se kroz obradu CNG-a i CAPI-ja na strani potpisivanja, obrađenu u članku o potpisivanju iz spremišta certifikata i redoslijedu bajtova
Tko je potpisao ovo? SignerIdentifier je dvije različite stvari
RFC 5652 §5.3 čini SignerIdentifier tipom CHOICE, a verifikator koji obrađuje samo jednu granu tiho će provjeriti prema pogrešnom ključu. Prva grana je issuerAndSerialNumber, SEQUENCE koji sadrži izdavatelja Name u sirovom DER-u i serijski INTEGER, a njegovo podudaranje je usporedba bajtova prema svakom certifikatu u CMS skupu certificates. Druga grana je [0] subjectKeyIdentifier, implicitno označen OCTET STRING, a njegovo podudaranje zahtijeva kopanje u certifikat umjesto usporedbe njegovih polja zaglavlja
Kopanje ima sloj koji ljude iznenadi. Identifikator ključa živi u X.509v3 proširenju, pa HotPDF prolazi kroz polje [3] extensions tbsCertificate, pronalazi proširenje čiji je OID 2.5.29.14, preskače opcionalni critical BOOLEAN i uzima extnValue OCTET STRING. Taj niz okteta nije identifikator. Prema RFC 5280 §4.2.1.2 njegov sadržaj je sam DER, a tip KeyIdentifier je još jedan OCTET STRING, pa parsirate drugi put da dosegnete stvarne bajtove. Zaustavite se sloj prerano i usporedit ćete 22-bajtni omot s 20-bajtnim identifikatorom, nijedan certifikat se nikad neće podudarati, a verifikator pada natrag na bilo koju heuristiku koju ste sljedeću napisali, što je pravi rizik. Uzimanje prvog certifikata u skupu je primamljiva prečica i pogrešna je kad god CMS nosi lanac, što je većina slučajeva, jer list nije obavezan doći prvi. HotPDF prihvaća nepodudarni certifikat samo kad kontejner sadrži točno jedan; s više certifikata prisutnih, točno podudaranje SignerIdentifier je obavezno. Provjera sažetka prema javnom ključu posredničkog CA ne proizvodi prijateljsku pogrešku, proizvodi sigurno nevažeće 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 revizijski zapis bilježi koja je krivulja stvarno korištena, a ne samo riječ ECDSA. Dokumentska mehanika oko ovog poziva, konkretno kako se hashiraju segmenti /ByteRange i zašto se sažetak mora računati preko datoteke, a ne preko parsiranog stabla objekata, tema je popratnog članka o provjeri PDF potpisa
Što vam ovo ne daje
Zeleni rezultat iz HPDFECDSAVerifyDigest odgovara samo na jedno pitanje: ti bajtovi bili su potpisani privatnim ključem koji odgovara ovom javnom ključu. Ne govori ništa o tome pripada li taj ključ nekome komu biste trebali vjerovati. Izgradnja lanca do sidra povjerenja, opoziv kroz CRL ili OCSP, te provjere politike zaseban su posao, a bilo koji proizvod koji prijavljuje valjani potpis bez njih prijavljuje manje nego što korisnik pretpostavlja. Datumi valjanosti certifikata izloženi su zasebno u THPDFSignatureInfo upravo iz tog razloga: potpis se može kriptografski provjeriti dok je certifikat koji ga je izradio istekao prije dvije godine. Podrška za krivulje također je namjerno uska. Tri NIST prime krivulje su obrađene, a potpis preko bilo koje druge krivulje vraća nepodržano umjesto pretpostavke. CNG put je samo za Windows, što je ispravan kompromis za VCL komponentu, ali vrijedi ga navesti prije nego što isplanirate međuplatformsku uslugu oko toga. I strogost nije konfigurabilna: ne postoji popustljiv način koji prihvaća ne-minimalnu DER duljinu jer je neki zastarjeli potpisnik jednu emitirao. Ako susretnete takvu datoteku u produkciji, pošten odgovor je zabilježiti je i potražiti proizvođača, a ne širiti parser dok datoteka ne prođe
Put provjere ECDSA opisan ovdje isporučuje se kao dio standardnog HotPDF Component za Delphi i C++Builder, uz putove RSA PKCS#1 v1.5 i RSA-PSS i potpuni zapis podataka o potpisu; produktna stranica sadrži potpunu referencu digitalnih potpisa