Műszaki cikk

ECDSA PDF aláírás-ellenőrzés Delphiben: DER-től P1363-ig

Egy CMS konténerben lévő ECDSA signatureValue egy DER SEQUENCE { INTEGER r, INTEGER s }. A Windows CNG BCryptVerifySignature függvénye egyiket sem fogadja el: IEEE P1363 fix szélességű r || s-t akar, tag-ek és hosszmezők nélkül. A HotPDF, a Delphi és C++Builder natív VCL PDF komponense, a kettő között szigorú DER szabályok szerint konvertál, mielőtt importálna egy kulcsot

Az így megelőzött hiba specifikus és demoralizáló. Az Acrobat megnyitja a dokumentumot, és zöld pipát mutat. A saját ellenőrződ, amely ugyanazokat a bájtokat járja be, érvénytelennek jelöli, vagy a CNG STATUS_INVALID_SIGNATURE-t ad vissza további magyarázat nélkül. Semmi baj nincs az aláírással. Ami baj van, az az, hogy hozzávetőleg hetven bájt ASN.1-et adtak át egy olyan API-nak, amely hatvannégy bájt nyers egészet várt, és az eltérés láthatatlan, hacsak nem tudod, hogy keresned kell

HotPDF: egymás melletti anatómia egy ECDSA aláírásról változó hosszú DER SEQUENCE-ként ASN.1 keretbájtokkal, és a rögzített szélességű P1363 r és s pufferként, amelyet a BCryptVerifySignature elfogad
Az átalakítás megtartja a hatvan négy hasznos bájtot, és eldobja minden címkét és hosszt; a CNG soha nem látja ugyanannak az aláírásnak a DER írásmódját

Miért utasítja el a BCryptVerifySignature egy érvényes ECDSA aláírást?

Mert a hívás két oldala eltérő aláírás-kódolásban beszél, és egyik sem jelenti be ezt. Az ISO 32000-1 §12.8 szerint egy aláírás-szótár egy CMS blobot hordoz a /Contents-ben; az RFC 5652 §5.3 szerint a signatureValue minden SignerInfo-ban egy OCTET STRING, amelynek tartalma bármi, amit az aláíró algoritmus definiál. ECDSA esetén ez a tartalom a SEC 1 DER struktúra: egy SEQUENCE, amely két INTEGER-t tart. Változó hosszúságú, tervezés szerint, mert az r és s egészek, és a DER lecsupaszítja a vezető nulla oktetteket az egészekből

Az IEEE P1363 az ellenkező nézetet vallja. Az aláírást a két koordináta összefűzéseként definiálja, mindegyiket balról nullákkal kitöltve pontosan a görbe mezőjének bájtszélességéig. Egy P-256 aláírás mindig 64 bájt. Ugyanazon aláírás DER kódolása általában 70 vagy 71 bájt, és bárhol lehet nagyjából 8 és 72 között. Add át a DER formát a BCryptVerifySignature-nek, és már maga a hosszellenőrzés is elveszejti a hívást, ezért normalizál a HotPDF, mielőtt ellenőrizne, nem utána

uses
  HPDFECDSA;

// A CMS signatureValue értékét a CNG által várt fix szélességű formára alakítja
// Az ARaw P-256 esetén 64, P-384 esetén 96, P-521 esetén 132 bájtként tér vissza
function ToP1363(const ADerSig: TBytes; ACurve: THPDFECDSACurve;
  out ARaw: TBytes): Boolean;
begin
  Result := HPDFECDSANormalizeSignature(ADerSig, ACurve, eseDER, ARaw);
end;

A DER szabályok, amelyeket egy aláírás-elemzőnek nem szabad enyhítenie

Minden itt felsorolt elutasítás olyan elutasítás, amelyet a HotPDF szándékosan végez, és mindegyik lezár egy utat, amelyet egy engedékeny elemző nyitva hagyna. A csábítás egy konverter írásakor az, hogy megtaláld a két INTEGER csomópontot, lemásold a tartalmukat, és továbblépj. Ez jól-formált bemeneten működik, és csendesen elfogadja a hajlítható (malleable) újrakódolások egy egész családját ellenséges bemeneten. Így a HPDFECDSANormalizeSignature elutasít egy negatív egészt, ami azt jelenti, hogy bármely r vagy s, amelynek első tartalombájtja beállított magas bittel rendelkezik, mert egy érvényes ECDSA skalár pozitív. Elutasít egy teljesen nulla értéket, mivel az r = 0 vagy s = 0 sosem jogszerű aláírás. Elutasít egy redundáns vezető nulla oktettet: az X.690 §8.3 pontosan egyet engedélyez, és csak akkor, ha a következő oktett egyébként negatívnak olvasódna, így egy 00, amelyet egy 0x80 alatti oktett követ, újrakódolás, nem aláírás. Elutasít egy nem-minimális hosszfejlécet, mert az X.690 §10.1 megköveteli a definitív formát a legkevesebb oktettel kódolva, és egy hosszú-formájú hossz, amely lehetett volna rövid-formájú, más bájtsztring, ugyanazt a jelentést hordozva. Elutasít egy a görbe koordinátaméreténél szélesebb egészt, mivel az az érték nem lehet mezőelem. És elutasít bármely csomópontot s után, valamint egy külső SEQUENCE-t, amelynek teljes hossza nem egyenlő a teljes blob hosszával

Ez az utolsó kettő fontosabb, mint amilyennek látszik. A SEQUENCE utáni maradék bájtok a klasszikus aláírás-hajlíthatósági trükk: fűzz hozzá szemetet, és egy engedékeny ellenőrző még mindig érvényesnek mondja, miközben az a bájtsztring, amit ellenőrzött, nem az a bájtsztring, amit aláírtak. Ugyanez az ösztön hajtja az ASN.1 hosszmegerősítést, amelyet a PKCS#12 elemzésről szóló jegyzet ír le, és ugyanez az ösztön itt is. Egy ellenőrzési útvonalon egy elfogadott struktúra, amelyet sosem bocsátott ki megfelelő aláíró, hiba, nem udvariasság

A koordinátaszélesség a görbéhez tartozik, nem az aláíráshoz

A HotPDF a kimeneti szélességet a megnevezett görbe OID-jából származtatja, sosem az éppen elemzett DER hosszából. Ez a konverzió második fele, és az a fele, amelyet könnyű finoman elrontani. Az RFC 5480 §2.1.1 azonosítja a görbét a tanúsítvány SubjectPublicKeyInfo paramétereiben, és a HPDFECDSACurveFromOID leképezi a három OID-ot, amelyet a HotPDF támogat: 1.2.840.10045.3.1.7 a P-256-hoz, 1.3.132.0.34 a P-384-hez, és 1.3.132.0.35 a P-521-hez. A HPDFECDSACoordinateSize ezután 32, 48 vagy 66 bájtot ad vissza, és a P1363 puffer ennek kétszerese: 64, 96 vagy 132. Minden dekódolt egész jobbra van igazítva a saját felébe, így egy rövid r balról nullákkal van kitöltve, nem eltolva. A P-521 az, amelyik megfog embereket, mert 521 bit 65,125 bájt, ami 66-ra kerekítve 132 bájtos aláírást ad, amit semmilyen kettő-hatványos megérzés nem jósolt volna meg. A nyilvános kulcs egy nem tömörített EC pontként utazik az RFC 5480 §2.2 szerint, ami 0x04, amelyet X és Y követ, így a HotPDF ellenőrzi, hogy pontosan 1 + 2 * CoordinateSize bájt-e, és 0x04-gyel kezdődik-e, mielőtt hozzáérne a CNG-hez

HotPDF: referenciatáblázat: az NIST P-256, P-384 és P-521 görbék hozzárendelése RFC 5480 OID-ikhoz, koordináta-szélességeikhez és P1363 aláírásösszegeikhez, felette generikus és görbénkénti CNG szolgáltatók összehasonlítása
A szélesség a görbe OID-jéből jön, nem a feldolgozott hosszból; a P-521 66 bájtos koordinátákra kerekít fel, és a megnevezett szolgáltatók ott nyílnak, ahol az általános ECDSA azonosító megbukik
var
  Digest, SigDER, PublicPoint: TBytes;
  Curve: THPDFECDSACurve;
  Res: THPDFECDSAVerifyResult;
begin
  // secp256r1, a tanúsítvány SubjectPublicKeyInfo paramétereiből véve
  Curve := HPDFECDSACurveFromOID('1.2.840.10045.3.1.7');

  // A PublicPoint formája $04 || X || Y kell legyen, ezért P-256 esetén 1 + 2 * 32 = 65 bájt
  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;

Figyeld meg az utolsó paramétert. A HPDFECDSAVerifyDigest az eseP1363-t is elfogadja azoknak a hívóknak, akik már fix szélességű aláírással rendelkeznek, egy hardveres tokentől vagy egy távoli aláíró szolgáltatástól, amely nyers r || s-t ad vissza. Ez az útvonal továbbra is kikényszeríti a hosszellenőrzést és a nem-nulla ellenőrzést mindkét félen, így egy megfelelő méretű, csupa nullát tartalmazó puffer visszautasításra kerül ahelyett, hogy átengednék a szolgáltatóhoz

Miért bukik el a generikus ECDSA algoritmusnév régebbi Windowson?

Mert a generikus név újabb, mint a célzott telepítési alapod. A CNG felkínál egy ECDSA algoritmusazonosítót, amely az importált kulcsból következteti ki a görbét, és ez a tiszta módja e kód megírásának, de a BCryptOpenAlgorithmProvider csak újabb Windows-verziókon garantáltan tudja feloldani. Egy régebbi gépen a megnyitási hívás elbukik, a szolgáltató-fogantyú nil marad, és minden ECDSA-ellenőrzés az alkalmazásodban nem-támogatottnak jelent egy tökéletesen jó aláírást. A HotPDF elkerüli ezt a szakadékot azzal, hogy inkább a görbénkénti azonosítókat nyitja meg. Feloldja az ECDSA_P256, ECDSA_P384 és ECDSA_P521 egyszer, gyorsítótárba tesz egy szolgáltató-fogantyút görbénként, és bezárja őket az egység finalizálásakor. Minden ellenőrzés ekkor csak az olcsó munkát végzi: importál egy ideiglenes nyilvános kulcsot egy ECCPUBLICBLOB-ból, meghívja a BCryptVerifySignature-t, elpusztítja a kulcsot. Nincs ismételt LoadLibrary, nincs ismételt GetProcAddress, nincs szolgáltató-megnyitás és -zárás aláírásonként. Néhány száz dokumentum kötegelt ellenőrzése érzi a különbséget, ahogy egy szolgáltatási folyamat is, amely különben terhelés alatt pörgetné a szolgáltató-fogantyúkat

A visszatérési kódok őszinték maradnak a megkülönböztetésről. Az evrProviderUnavailable azt jelenti, hogy a gép nem tudott szolgáltatót adni a HotPDF-nek; az evrInvalid azt jelenti, hogy a CNG STATUS_INVALID_SIGNATURE-t válaszolt. E kettő egyetlen hibává összeolvasztása az, ahogyan egy telepítési probléma hamisított dokumentumként jelentődik félre. Ugyanez az elválasztás a környezeti hiba és a kriptográfiai hiba között fut végig a CNG és CAPI kezelésen az aláíró oldalon, amelyet a tanúsítványtárban történő aláírásról és bájtsorrendről szóló cikk tárgyal

Melyik tanúsítvány írta ezt alá? A SignerIdentifier két különböző dolog

Az RFC 5652 §5.3 a SignerIdentifier-t egy CHOICE-szá teszi, és egy ellenőrző, amely csak az egyik ágat kezeli, csendben rossz kulcs ellen fog ellenőrizni. Az első ág az issuerAndSerialNumber, egy SEQUENCE, amely a kibocsátó Name-jét nyers DER formában és a sorozatszám INTEGER-t tartja, és az egyeztetés bájtösszehasonlítás minden tanúsítvány ellen a CMS certificates halmazában. A második ág a [0] subjectKeyIdentifier, egy implicit módon taggelt OCTET STRING, és az egyeztetéshez ásni kell a tanúsítványba ahelyett, hogy csak a fejlécmezőit hasonlítanánk össze

Az ásásnak van egy réteg, amely meglepi az embereket. A kulcsazonosító egy X.509v3 kiterjesztésben él, így a HotPDF bejárja a tbsCertificate [3] kiterjesztés-mezőjét, megtalálja a kiterjesztést, amelynek OID-je 2.5.29.14, kihagyja az opcionális kritikus BOOLEAN-t, és veszi az extnValue OCTET STRING-et. Ez az octet string nem az azonosító. Az RFC 5280 §4.2.1.2 szerint a tartalma maga is DER, és a KeyIdentifier típus egy másik OCTET STRING, így másodszor is elemzel, hogy elérd a tényleges bájtokat. Állj meg egy réteggel korábban, és egy 22-bájtos csomagolót hasonlítasz össze egy 20-bájtos azonosítóval, egyetlen tanúsítvány sem egyezik soha, és az ellenőrző visszaesik bármilyen heurisztikára, amit ezután írtál, ami a valódi veszély. Az első tanúsítvány kiválasztása a halmazban csábító rövidítés, és rossz, valahányszor a CMS láncot hordoz, ami a legtöbbször így van, mert a levélnek nem kötelező elsőként jönnie. A HotPDF csak akkor fogad el egy nem egyeztetett tanúsítványt, ha a konténer pontosan egyet tartalmaz; több tanúsítvány jelenlétében egy pontos SignerIdentifier egyezés kötelező. Egy digest ellenőrzése egy köztes CA nyilvános kulcsa ellen nem produkál barátságos hibát, hanem magabiztos érvénytelent egy egyébként rendben lévő dokumentumon

HotPDF: SignerIdentifier döntési folyamat: az issuerAndSerialNumber bájt-összehasonlítás szemben a subjectKeyIdentifier ággal, amelynek KeyIdentifiere extnValue OCTET STRING belsejében ágyazódik
A bal ág nyers fejlécmezők összehasonlításával egyeztet; a jobb ágnak kétszer kell átdolgoznia egy OCTET STRING burkot, mielőtt a húszbájtos azonosító egyáltalán megjelenne
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;

A THPDFSignatureInfo.CurveName jelenti a P-256, P-384 vagy P-521 értéket, így egy audit napló azt rögzíti, melyik görbét ténylegesen használták, nem csak azt a szót, hogy ECDSA. A dokumentumszintű gépezet e hívás körül, konkrétan az, hogy a /ByteRange szegmenseket hogyan kell hasholni, és miért kell a digest-et a fájl felett kiszámítani, nem az elemzett objektumfa felett, a PDF aláírások ellenőrzéséről szóló kísérőcikk témája

Amit ez nem ad meg

Egy zöld eredmény a HPDFECDSAVerifyDigest-től csak egy kérdésre válaszol: ezeket a bájtokat az e nyilvános kulcsnak megfelelő privát kulcs írta alá. Semmit sem mond arról, hogy ez a kulcs olyasvalakihez tartozik-e, akiben megbízhatsz. A lánc felépítése egy bizalmi horgonyig, a visszavonás CRL-en vagy OCSP-n keresztül, és a szabályzat-ellenőrzések külön munkák, és bármely termék, amely ezek nélkül jelent érvényes aláírást, kevesebbet jelent, mint amit a felhasználó feltételez. A tanúsítvány érvényességi dátumai külön jelennek meg a THPDFSignatureInfo-ban pontosan emiatt: egy aláírás kriptográfiailag érvényesíthető, miközben az azt létrehozó tanúsítvány két éve lejárt. A görbetámogatás is szándékosan szűk. Három NIST elsőrendű görbe van kezelve, és bármely más görbén lévő aláírás nem-támogatottat ad vissza tippelés helyett. A CNG útvonal csak Windowsra vonatkozik, ami a helyes csere egy VCL komponens számára, de érdemes megemlíteni, mielőtt egy platformfüggetlen szolgáltatást terveznél köré. És a szigorúság nem konfigurálható: nincs engedékeny mód, amely elfogadna egy nem-minimális DER hosszot, mert valami régi aláíró ilyet bocsátott ki. Ha ilyen fájllal találkozol éles környezetben, a tisztességes válasz az, hogy rögzíted, és utánajársz az előállítónak, nem az, hogy kiszélesíted az elemzőt, amíg a fájl átmegy

Az itt leírt ECDSA-ellenőrző útvonal a szabványos HotPDF Delphi Component részeként érkezik Delphihez és C++Builderhez, az RSA PKCS#1 v1.5 és RSA-PSS útvonalak, valamint a teljes aláírás-információs rekord mellett; a termékoldal tartalmazza a teljes digitális aláírás-referenciát