Techninis straipsnis

ECDSA PDF parašo tikrinimas Delphi: DER į P1363

ECDSA signatureValue CMS konteineryje yra DER SEQUENCE { INTEGER r, INTEGER s }. Windows CNG funkcija BCryptVerifySignature nepriima nė vieno iš jų: ji nori IEEE P1363 fiksuoto pločio r || s, be žymų ir be ilgių. HotPDF, natyvus VCL PDF komponentas Delphi ir C++Builder, konvertuoja tarp jų pagal griežtas DER taisykles prieš importuodamas raktą

Klaida, kurią tai apsaugo, yra specifinė ir demoralizuojanti. Acrobat atidaro dokumentą ir rodo žalią varnelę. Jūsų pačių tikrintuvas, einantis per tuos pačius baitus, grąžina negaliojantį, arba CNG grąžina STATUS_INVALID_SIGNATURE be jokio tolesnio paaiškinimo. Su parašu nieko blogo. Kas blogai, tai kad maždaug septyniasdešimt baitų ASN.1 buvo perduota API, kuris tikėjosi šešiasdešimt keturių baitų žalio sveikojo skaičiaus, o neatitikimas nematomas, jei nežinote, kur ieškoti

Kodėl BCryptVerifySignature atmeta galiojantį ECDSA parašą?

Todėl, kad abi iškvietimo pusės kalba skirtingais parašo kodavimais, ir nė viena to nepraneša. ISO 32000-1 §12.8 sako, kad parašo žodynas neša CMS blob'ą /Contents, RFC 5652 §5.3 sako, kad signatureValue kiekviename SignerInfo yra OCTET STRING, kurio turinys yra tai, ką apibrėžia parašo algoritmas. ECDSA atveju tas turinys yra SEC 1 DER struktūra: SEQUENCE, laikanti du INTEGER. Ji pagal dizainą yra kintamo ilgio, nes r ir s yra sveikieji skaičiai, o DER pašalina pirmaujančius nulinius oktetus iš sveikųjų skaičių

IEEE P1363 laikosi priešingo požiūrio. Jis apibrėžia parašą kaip dviejų koordinačių sujungimą, kiekvieną kairiajame krašte užpildytą nuliais lygiai iki kreivės lauko baitų pločio. P-256 parašas visada yra 64 baitai. Tos pačios parašo DER kodavimas paprastai yra 70 ar 71 baitas ir gali būti bet kur nuo maždaug 8 iki 72. Perduokite DER formą BCryptVerifySignature, ir vien ilgio patikra pasmerkia iškvietimą, todėl HotPDF normalizuoja prieš tikrindamas, o ne po to

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 taisyklės, kurių parašo nagrinėtojas negali sušvelninti

Kiekvienas čia išvardytas atmetimas yra atmetimas, kurį HotPDF atlieka sąmoningai, ir kiekvienas iš jų uždaro kelią, kurį atlaidus nagrinėtojas paliktų atvirą. Pagunda rašant konverterį yra rasti du INTEGER mazgus, nukopijuoti jų turinį ir judėti toliau. Tai veikia su gerai suformuotu duomenimis ir tyliai priima šeimą kintamo (malleable) perkodavimų su priešiška įvestimi. Todėl HPDFECDSANormalizeSignature atmeta neigiamą sveikąjį skaičių, tai yra bet kokį r ar s, kurio pirmasis turinio oktetas turi nustatytą aukštąjį bitą, nes galiojantis ECDSA skaliaras yra teigiamas. Jis atmeta reikšmę, kuri yra visiškai nulis, nes r = 0 ar s = 0 niekada nėra teisėtas parašas. Jis atmeta perteklinį pirmaujantį nulinį oktetą: X.690 §8.3 leidžia lygiai vieną, ir tik tada, kai kitas oktetas kitaip skaitytųsi kaip neigiamas, todėl 00, po kurio seka oktetas žemiau 0x80, yra perkodavimas, ne parašas. Jis atmeta neminimalią ilgio antraštę, nes X.690 §10.1 reikalauja apibrėžtos formos, užkoduotos mažiausiu oktetų skaičiumi, o ilgos formos ilgis, galėjęs būti trumpos formos, yra kitas baitų eilutė, nešanti tą pačią reikšmę. Jis atmeta sveikąjį skaičių, platesnį nei kreivės koordinatės dydis, nes ta reikšmė negali būti lauko elementas. Ir jis atmeta bet kokį sekantį mazgą po s, kartu su išorine SEQUENCE, kurios bendras ilgis nesutampa su viso blob'o ilgiu

Šie du paskutiniai svarbesni, nei atrodo. Baitai po SEQUENCE yra klasikinis parašo kintamumo trikas: pridėkite šiukšlių, ir atlaidus tikrintuvas vis tiek sako galiojantis, nors baitų eilutė, kurią jis patikrino, nėra baitų eilutė, kuri buvo pasirašyta. Ta pati intuicija veda ASN.1 ilgio sugriežtinimą, aprašytą pastaboje apie PKCS#12 nagrinėjimą, ir čia veikia ta pati intuicija. Tikrinimo kelyje priimta struktūra, kurios niekada neišleido atitinkantis pasirašantysis, yra defektas, ne mandagumas

Koordinatės plotis priklauso kreivei, ne parašui

HotPDF išveda išvesties plotį iš pavadintos kreivės OID, niekada iš ką tik nagrinėto DER ilgio. Tai antra konvertavimo pusė, ir ta pusė, kurią lengva subtiliai sugadinti. RFC 5480 §2.1.1 identifikuoja kreivę sertifikato SubjectPublicKeyInfo parametruose, o HPDFECDSACurveFromOID susieja tris OID, kuriuos palaiko HotPDF: 1.2.840.10045.3.1.7 P-256, 1.3.132.0.34 P-384 ir 1.3.132.0.35 P-521. HPDFECDSACoordinateSize tada grąžina 32, 48 ar 66 baitus, o P1363 buferis yra dvigubai didesnis: 64, 96 ar 132. Kiekvienas dekoduotas sveikasis skaičius yra dešiniuoju kraštu lygiuojamas į savo pusę, todėl trumpas r užpildomas nuliais kairėje, o ne perstumiamas. P-521 yra tas, kuris žmones pagauna, nes 521 bitas yra 65,125 baito ir apvalinamas iki 66, duodant 132 baitų parašą, kurio joks laipsnio-dviem intuicija nebūtų numačiusi. Viešasis raktas keliauja kartu kaip nesuspausto EC taško, pagal RFC 5480 §2.2, tai yra 0x04, po kurio X ir Y, todėl HotPDF patikrina, kad jis yra lygiai 1 + 2 * CoordinateSize baitų ir prasideda 0x04, prieš paliesdamas 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;

Atkreipkite dėmesį į paskutinį parametrą. HPDFECDSAVerifyDigest taip pat priima eseP1363 iškvietėjams, jau turintiems fiksuoto pločio parašą, iš aparatinio žetono ar nuotolinės pasirašymo paslaugos, grąžinančios žalią r || s. Tas kelias vis tiek užtikrina ilgio ir ne-nulio patikrą abiejose pusėse, todėl teisingo dydžio buferis, pilnas nulių, atmetamas, o ne perduodamas tiekėjui

Kodėl bendrinis ECDSA algoritmo vardas nepavyksta senesnėje Windows versijoje?

Todėl, kad bendrinis vardas naujesnis nei diegimo bazė, į kurią įdiegiate. CNG atskleidžia algoritmo identifikatorių ECDSA, kuris išveda kreivę iš importuoto rakto, ir tai švarus būdas šį kodą rašyti, tačiau BCryptOpenAlgorithmProvider garantuotai jį išsprendžia tik naujesnėse Windows versijose. Senesnėje mašinoje atidarymo iškvietimas nepavyksta, tiekėjo rankena lieka nil, o kiekvienas ECDSA tikrinimas jūsų programoje praneša nepalaikomas dėl visiškai geros parašo. HotPDF išvengia šio nuokrypio atidarydamas identifikatorius pagal kiekvieną kreivę atskirai. Jis vieną kartą išsprendžia ECDSA_P256, ECDSA_P384 ir ECDSA_P521, talpina po vieną tiekėjo rankeną kiekvienai kreivei ir jas uždaro modulio finalizavimo metu. Kiekvienas tikrinimas tada atlieka tik pigų darbą: importuoja laikiną viešąjį raktą iš ECCPUBLICBLOB, kviečia BCryptVerifySignature, sunaikina raktą. Nėra pakartotinio LoadLibrary, nėra pakartotinio GetProcAddress, nėra tiekėjo atidarymo ir uždarymo kiekvienam parašui. Kelių šimtų dokumentų paketinis tikrinimas jaučia skirtumą, kaip ir paslaugos procesas, kuris priešingu atveju kaupintų tiekėjo rankenas apkrovoje

Rezultatų kodai išlieka sąžiningi apie šį skirtumą. evrProviderUnavailable reiškia, kad mašina negalėjo suteikti HotPDF tiekėjo; evrInvalid reiškia, kad CNG atsakė STATUS_INVALID_SIGNATURE. Šių dviejų sujungimas į vieną nesėkmę yra tai, kaip diegimo problema pranešama klaidingai kaip suklastotas dokumentas. Tas pats atskyrimas tarp aplinkos nesėkmės ir kriptografinės nesėkmės tęsiasi per CNG ir CAPI tvarkymą pasirašymo pusėje, aprašytą straipsnyje apie sertifikatų saugyklos pasirašymą ir baitų tvarką

Kuris sertifikatas tai pasirašė? SignerIdentifier yra du skirtingi dalykai

RFC 5652 §5.3 padaro SignerIdentifier CHOICE, o tikrintuvas, tvarkantis tik vieną atšaką, tyliai patikrins pagal netinkamą raktą. Pirmoji atšaka yra issuerAndSerialNumber, SEQUENCE, laikanti leidėjo Name žalios DER formos ir serijos INTEGER, o jos atitikimas yra baitų palyginimas su kiekvienu sertifikatu CMS certificates rinkinyje. Antroji atšaka yra [0] subjectKeyIdentifier, netiesiogiai žymėtas OCTET STRING, o jos atitikimas reikalauja kasti giliau į sertifikatą, o ne lyginti jo antraštės laukus

Kasimas turi sluoksnį, kuris žmones nustebina. Rakto identifikatorius gyvena X.509v3 plėtinyje, todėl HotPDF eina per tbsCertificate [3] plėtinių lauką, randa plėtinį, kurio OID yra 2.5.29.14, praleidžia pasirinktinį critical BOOLEAN ir paima extnValue OCTET STRING. Tas oktetų eilutė nėra identifikatorius. Pagal RFC 5280 §4.2.1.2 jo turinys pats yra DER, o KeyIdentifier tipas yra kitas OCTET STRING, todėl nagrinėjate antrą kartą, kad pasiektumėte tikruosius baitus. Sustokite vienu sluoksniu anksčiau, ir palyginsite 22 baitų apvalkalą su 20 baitų identifikatoriumi, joks sertifikatas niekada nesutaps, o tikrintuvas grįš prie kokios euristikos, kurią po to parašėte, o tai yra tikras pavojus. Pirmo sertifikato paėmimas rinkinyje yra viliojantis trumpasis kelias, ir jis neteisingas visada, kai CMS neša grandinę, o tai yra dažniausiai, nes lapas neprivalo eiti pirmas. HotPDF priima nesutaptą sertifikatą tik tada, kai konteineris turi lygiai vieną; esant keliems sertifikatams, tikslus SignerIdentifier atitikimas yra privalomas. Santraukos tikrinimas pagal tarpinio CA viešąjį raktą nesukuria draugiškos klaidos, jis sukuria patikimą negaliojantį rezultatą dokumentui, kuris yra puikus

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 praneša P-256, P-384 arba P-521, kad audito žurnalas įrašytų, kuri kreivė buvo iš tikrųjų naudota, o ne tik žodį ECDSA. Dokumento lygio infrastruktūra aplink šį iškvietimą, ypač kaip /ByteRange segmentai maišuojami ir kodėl santrauka turi būti apskaičiuota per failą, o ne per nagrinėtą objektų medį, yra bendrinio straipsnio apie PDF parašų tikrinimą tema

Ko tai jums nesuteikia

Žalias rezultatas iš HPDFECDSAVerifyDigest atsako tik į vieną klausimą: šiuos baitus pasirašė privatus raktas, atitinkantis šį viešąjį raktą. Jis nieko nesako apie tai, ar tas raktas priklauso kam nors, kuo turėtumėte pasitikėti. Grandinės sudarymas iki pasitikėjimo šaltinio, atšaukimas per CRL ar OCSP ir politikos patikros yra atskiras darbas, o bet koks produktas, praneša galiojantį parašą be jų, praneša mažiau, nei vartotojas mano. Sertifikato galiojimo datos atskirai atskleidžiamos THPDFSignatureInfo būtent dėl to: parašas gali kriptografiškai patikrinti, o sertifikatas, kuris jį sukūrė, galiojo baigė prieš dvejus metus. Kreivių palaikymas taip pat sąmoningai siauras. Trys NIST prime kreivės tvarkomos, o parašas su bet kuria kita kreive grąžina nepalaikomas, o ne spėjimą. CNG kelias yra tik Windows, kas yra teisingas mainas VCL komponentui, tačiau verta paminėti prieš planuojant kelias platformas apimančią paslaugą aplink jį. O griežtumas nėra konfigūruojamas: nėra atlaidaus režimo, priimančio neminimalų DER ilgį, nes koks nors senas pasirašantysis tokį išleido. Jei sutinkate tokį failą produkcijoje, sąžiningas atsakymas yra jį užfiksuoti ir vytis gamintoją, ne plėsti nagrinėtoją, kol failas praeis

Čia aprašytas ECDSA tikrinimo kelias pristatomas standartiniame HotPDF Component, skirtame Delphi ir C++Builder, kartu su RSA PKCS#1 v1.5 ir RSA-PSS keliais bei pilnu parašo informacijos įrašu; produkto puslapyje pateikiama pilna skaitmeninio parašo nuoroda