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

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;

// 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;

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

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;

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

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 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