signatureValue ECDSA znotraj vsebnika CMS je DER SEQUENCE { INTEGER r, INTEGER s }. Funkcija Windows CNG BCryptVerifySignature ne sprejme nobenega od teh: želi IEEE P1363 s fiksno širino r || s, brez oznak in brez dolžin. HotPDF, izvorna komponenta VCL PDF za Delphi in C++Builder, pretvarja med obema po strogih pravilih DER, preden uvozi ključ
Napaka, ki jo to preprečuje, je specifična in obupujoča. Acrobat odpre dokument in pokaže zeleno kljukico. Vaš lasten preverjevalnik, ki prehodi iste bajte, vrne neveljavno, ali pa CNG vrne STATUS_INVALID_SIGNATURE brez nadaljnje razlage. S podpisom ni nič narobe. Narobe je, da je bilo približno sedemdeset bajtov ASN.1 posredovanih API-ju, ki je pričakoval štiriinšestdeset bajtov surovega celega števila, neujemanje pa je nevidno, razen če veste, da ga morate iskati
Zakaj BCryptVerifySignature zavrne veljaven podpis ECDSA?
Ker obe strani klica govorita različni kodiranji podpisa in nobena tega ne razglasi. ISO 32000-1 §12.8 pravi, da slovar podpisa nosi blob CMS v /Contents; RFC 5652 §5.3 pravi, da je signatureValue v vsakem SignerInfo OCTET STRING, katerega vsebina je karkoli, kar definira algoritem podpisa. Za ECDSA je ta vsebina struktura DER SEC 1: SEQUENCE, ki drži dva INTEGER. Po zasnovi je spremenljive dolžine, ker sta r in s celi števili, DER pa iz celih števil odstrani vodilne ničelne oktete
IEEE P1363 ima nasproten pogled. Podpis definira kot spojitev obeh koordinat, vsako z ničlami zapolnjeno z leve natanko do bajtne širine polja krivulje. Podpis P-256 je vedno 64 bajtov. Kodiranje DER istega podpisa je običajno 70 ali 71 bajtov in je lahko kjerkoli med približno 8 in 72. Predajte obliko DER funkciji BCryptVerifySignature in že sam preverjanje dolžine obsodi klic, zato HotPDF normalizira pred preverjanjem, ne po njem
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;
Pravila DER, ki jih razčlenjevalnik podpisov ne sme omiliti
Vsaka tukaj naštevana zavrnitev je zavrnitev, ki jo HotPDF izvaja namerno, vsaka pa zapre pot, ki bi jo prizanesljiv razčlenjevalnik pustil odprto. Skušnjava pri pisanju pretvornika je najti oba vozla INTEGER, skopirati njuno vsebino in nadaljevati. To deluje na dobro oblikovanem vhodu in tiho sprejme družino spremenljivih ponovnih kodiranj na sovražnem vhodu. Zato HPDFECDSANormalizeSignature zavrne negativno celo število, torej vsak r ali s, katerega prvi vsebinski oktet ima nastavljen visoki bit, ker je veljaven skalar ECDSA pozitiven. Zavrne vrednost, ki je v celoti nič, saj r = 0 ali s = 0 nikoli ni legitimen podpis. Zavrne odvečen vodilni ničelni oktet: X.690 §8.3 dovoli natanko enega, in to le, kadar bi se naslednji oktet sicer bral kot negativen, tako da je 00, ki mu sledi oktet pod 0x80, ponovno kodiranje, ne pa podpis. Zavrne neminimalno glavo dolžine, ker X.690 §10.1 zahteva določeno obliko, kodirano v najmanjšem številu oktetov, dolga oblika dolžine, ki bi lahko bila kratka, pa je drug bajtni niz z istim pomenom. Zavrne celo število, širše od velikosti koordinate krivulje, ker taka vrednost ne more biti element polja. In zavrne vsak sledeč vozel za s, skupaj z zunanjim SEQUENCE, čigar skupna dolžina se ne ujema z dolžino celotnega bloba
Zadnji dve stvari sta pomembnejši, kot se zdi na prvi pogled. Sledeči bajti za SEQUENCE so klasičen trik spremenljivosti podpisa: dodajte smeti, in prizanesljiv preverjevalnik še vedno pravi veljavno, medtem ko bajtni niz, ki ga je preveril, ni bajtni niz, ki je bil podpisan. Isti instinkt poganja utrjevanje dolžine ASN.1, opisano v opombi o razčlenjevanju PKCS#12, tukaj pa gre za isti instinkt. Na poti preverjanja je sprejeta struktura, ki je noben skladen podpisnik nikoli ni izdal, napaka, ne pa uslužnost
Širina koordinate pripada krivulji, ne podpisu
HotPDF izpelje izhodno širino iz poimenovanega OID krivulje, nikoli iz dolžine DER, ki ga je pravkar razčlenil. To je druga polovica pretvorbe in polovica, ki jo je lahko subtilno narediti napačno. RFC 5480 §2.1.1 identificira krivuljo v parametrih SubjectPublicKeyInfo certifikata, HPDFECDSACurveFromOID pa preslika tri OID-je, ki jih HotPDF podpira: 1.2.840.10045.3.1.7 za P-256, 1.3.132.0.34 za P-384 in 1.3.132.0.35 za P-521. HPDFECDSACoordinateSize nato vrne 32, 48 ali 66 bajtov, medpomnilnik P1363 pa je dvakratnik tega: 64, 96 ali 132. Vsako dekodirano celo število je desno poravnano v svojo polovico, tako da je kratek r z leve zapolnjen z ničlami, ne pa premaknjen. P-521 je tisti, ki ujame ljudi, ker je 521 bitov 65,125 bajta in se zaokroži navzgor na 66, kar da 132-bajtni podpis, ki ga nobena intuicija potence dvojke ne bi predvidela. Javni ključ potuje zraven kot nestisnjena točka EC po RFC 5480 §2.2, kar je 0x04, ki mu sledita X in Y, zato HotPDF preveri, da je natanko 1 + 2 * CoordinateSize bajtov in se začne z 0x04, preden se 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;
Opazite zadnji parameter. HPDFECDSAVerifyDigest sprejme tudi eseP1363 za klicatelje, ki že imajo podpis s fiksno širino, iz strojnega žetona ali oddaljene storitve podpisovanja, ki vrne surov r || s. Ta pot še vedno uveljavlja preverjanje dolžine in preverjanje ničelnosti na obeh polovicah, tako da je medpomnilnik pravilne velikosti, poln ničel, zavrnjen namesto posredovan ponudniku
Zakaj generično ime algoritma ECDSA odpove na starejšem Windows?
Ker je generično ime novejše od uvajalne osnove, v katero pošiljate. CNG izpostavlja identifikator algoritma ECDSA, ki krivuljo izpelje iz uvoženega ključa, in je čist način za pisanje te kode, vendar je BCryptOpenAlgorithmProvider zagotovljeno razrešljiv le na novejših različicah Windows. Na starejšem stroju klic odpiranja odpove, ročnik ponudnika ostane nil, vsako preverjanje ECDSA v vaši aplikaciji pa poroča nepodprto na podpisu, ki je popolnoma v redu. HotPDF se prepadu izogne tako, da namesto tega odpre identifikatorje po krivulji. Enkrat razreši ECDSA_P256, ECDSA_P384 in ECDSA_P521, predpomni po en ročnik ponudnika na krivuljo in jih zapre ob finalizaciji enote. Vsako preverjanje nato opravi le poceni delo: uvozi začasen javni ključ iz ECCPUBLICBLOB, pokliče BCryptVerifySignature, uniči ključ. Brez ponavljajočega se LoadLibrary, brez ponavljajočega se GetProcAddress, brez odpiranja in zapiranja ponudnika za vsak podpis. Paketno preverjanje nekaj sto dokumentov razliko občuti, prav tako storitveni proces, ki bi sicer pod obremenitvijo prežvekoval ročnike ponudnikov
Kode rezultata ostajajo pošteno razmejene. evrProviderUnavailable pomeni, da stroj HotPDF ni mogel dati ponudnika; evrInvalid pomeni, da je CNG odgovoril STATUS_INVALID_SIGNATURE. Zlivanje teh dveh v eno napako je način, kako se težava z uvajanjem napačno poroča kot ponarejen dokument. Ista ločitev med napako okolja in kriptografsko napako teče skozi obravnavo CNG in CAPI na strani podpisovanja, obravnavano v članku o podpisovanju iz shrambe certifikatov in vrstnem redu bajtov
Kateri certifikat je to podpisal? SignerIdentifier sta dve različni stvari
RFC 5652 §5.3 naredi SignerIdentifier CHOICE, preverjevalnik, ki obravnava le eno vejo, pa bo tiho preveril proti napačnemu ključu. Prva veja je issuerAndSerialNumber, SEQUENCE, ki drži izdajateljevo ime v surovem DER in serijski INTEGER, ujemanje pa je bajtna primerjava proti vsakemu certifikatu v naboru certificates CMS. Druga veja je [0] subjectKeyIdentifier, implicitno označen OCTET STRING, ujemanje pa zahteva kopanje v certifikat, ne pa primerjavo polj njegove glave
Kopanje ima sloj, ki ljudi preseneti. Identifikator ključa živi v razširitvi X.509v3, zato HotPDF prehodi polje razširitev [3] tbsCertificate, najde razširitev, katere OID je 2.5.29.14, preskoči izbirni kritični BOOLEAN in vzame OCTET STRING extnValue. Ta bajtni niz ni identifikator. Po RFC 5280 §4.2.1.2 je njegova vsebina sama DER, tip KeyIdentifier pa je še en OCTET STRING, zato razčlenite drugič, da dosežete dejanske bajte. Ustavite se en sloj prezgodaj in primerjate 22-bajtno ovojnico z 20-bajtnim identifikatorjem, noben certifikat se nikoli ne ujema, preverjevalnik pa se zateče k kakršni koli hevristiki, ki ste jo napisali naslednjo, kar je resnična nevarnost. Vzeti prvi certifikat v naboru je mikavna bližnjica in je napačna, kadarkoli CMS nosi verigo, kar je večino časa, ker list ni zahtevano, da pride prvi. HotPDF sprejme neujemajoč certifikat le, kadar vsebnik drži natanko enega; ob prisotnosti več certifikatov je natančno ujemanje SignerIdentifier obvezno. Preverjanje digesta proti javnemu ključu vmesnega CA ne proizvede prijazne napake, proizvede samozavestno neveljavno na dokumentu, ki je v 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 poroča P-256, P-384 ali P-521, tako da revizijski dnevnik zabeleži, katera krivulja je bila dejansko uporabljena, ne pa le besedo ECDSA. Vodovodje na ravni dokumenta okoli tega klica, predvsem kako so segmenti /ByteRange zgoščeni in zakaj mora biti digest izračunan čez datoteko, ne pa čez razčlenjeno drevo objektov, je predmet spremljajočega članka o preverjanju digitalnih podpisov PDF
Kaj vam to ne da
Zelen rezultat iz HPDFECDSAVerifyDigest odgovori le na eno vprašanje: te bajte je podpisal zasebni ključ, ki se ujema s tem javnim ključem. Ne pove nič o tem, ali ta ključ pripada komu, ki bi mu morali zaupati. Gradnja verige do sidra zaupanja, preklic prek CRL ali OCSP in preverjanja politik so ločeno delo, vsak izdelek, ki poroča veljaven podpis brez njih, pa poroča manj, kot uporabnik domneva. Datumi veljavnosti certifikata so iz istega razloga izpostavljeni ločeno v THPDFSignatureInfo: podpis se lahko kriptografsko preveri, medtem ko je certifikat, ki ga je izdelal, potekel pred dvema letoma. Podpora krivulj je tudi namerno ozka. Obravnavane so tri praštevilske krivulje NIST, podpis prek katerekoli druge krivulje pa vrne nepodprto namesto uganjenja. Pot CNG je le za Windows, kar je pravilna menjava za komponento VCL, vendar vredno omeniti, preden okoli nje načrtujete medplatformno storitev. In strogost ni nastavljiva: ni prizanesljivega načina, ki bi sprejel neminimalno dolžino DER, ker je nek zastarel podpisnik izdal takšno. Če v produkciji naletite na tako datoteko, je pošten odziv, da jo zabeležite in poiščete izdelovalca, ne pa da razčlenjevalnik širite, dokler datoteka ne prestane
Tukaj opisana pot preverjanja ECDSA je del standardne HotPDF Component za Delphi in C++Builder, skupaj s potmi RSA PKCS#1 v1.5 in RSA-PSS ter celotnim zapisom informacij o podpisu; stran izdelka nosi celoten referenčni opis digitalnega podpisa