Technický článek

Ověření ECDSA podpisů PDF v Delphi: z DER do P1363

ECDSA signatureValue uvnitř kontejneru CMS je DER SEQUENCE { INTEGER r, INTEGER s }. Windows funkce CNG BCryptVerifySignature nepřijímá ani jedno z toho: chce IEEE P1363 s pevnou šířkou r || s, bez tagů a bez délek. HotPDF, nativní VCL PDF komponenta pro Delphi a C++Builder, mezi nimi převádí podle přísných pravidel DER ještě předtím, než importuje klíč

Selhání, kterému to zabraňuje, je konkrétní a demoralizující. Acrobat otevře dokument a ukáže zelenou fajfku. Váš vlastní verifikátor, procházející stejné bajty, vrátí neplatný, nebo CNG vrátí STATUS_INVALID_SIGNATURE bez dalšího vysvětlení. S podpisem není nic v nepořádku. Špatně je, že se do API, které očekávalo šedesát čtyři bajtů syrového celého čísla, předalo zhruba sedmdesát bajtů ASN.1, a tento nesoulad je neviditelný, pokud nevíte, že po něm máte pátrat

Proč BCryptVerifySignature odmítá platný ECDSA podpis?

Protože obě strany volání mluví odlišným kódováním podpisu a ani jedna to neoznámí. ISO 32000-1 §12.8 říká, že slovník podpisu nese blob CMS v /Contents; RFC 5652 §5.3 říká, že signatureValue v každém SignerInfo je OCTET STRING, jehož obsah je cokoli, co definuje algoritmus podpisu. Pro ECDSA je tímto obsahem struktura DER dle SEC 1: SEQUENCE držící dva INTEGERy. Je záměrně proměnlivé délky, protože r a s jsou celá čísla a DER odstraňuje vedoucí nulové oktety z celých čísel

IEEE P1363 zastává opačný pohled. Definuje podpis jako zřetězení dvou souřadnic, z nichž každá je zleva doplněna nulami přesně na bajtovou šířku pole křivky. Podpis P-256 má vždy 64 bajtů. DER kódování stejného podpisu má obvykle 70 nebo 71 bajtů a může se pohybovat zhruba od 8 do 72. Předáte-li formu DER do BCryptVerifySignature, samotná kontrola délky volání odsoudí k neúspěchu, a proto HotPDF normalizuje před ověřením, nikoli až po něm

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;

Pravidla DER, která parser podpisu nesmí uvolnit

Každé zde uvedené odmítnutí je odmítnutí, které HotPDF provádí záměrně, a každé z nich uzavírá cestu, kterou by shovívavý parser nechal otevřenou. Pokušením při psaní konvertoru je najít dva uzly INTEGER, zkopírovat jejich obsah a jít dál. To funguje na dobře formovaném vstupu a potichu přijímá celou rodinu tvárných překódování na nepřátelském vstupu. Proto HPDFECDSANormalizeSignature odmítá záporné celé číslo, tedy jakékoli r nebo s, jehož první obsahový oktet má nastavený nejvyšší bit, protože platný ECDSA skalár je kladný. Odmítá hodnotu, která je zcela nulová, protože r = 0 nebo s = 0 nikdy není legitimní podpis. Odmítá nadbytečný vedoucí nulový oktet: X.690 §8.3 povoluje přesně jeden, a to jen tehdy, kdy by další oktet jinak byl čten jako záporný, takže 00 následované oktetem pod 0x80 je překódování, nikoli podpis. Odmítá neminimalizovanou hlavičku délky, protože X.690 §10.1 vyžaduje určitou formu kódovanou v nejmenším počtu oktetů, a dlouhá forma délky, která mohla být krátkou formou, je jiný bajtový řetězec nesoucí stejný význam. Odmítá celé číslo širší než velikost souřadnice křivky, protože taková hodnota nemůže být prvkem pole. A odmítá jakýkoli uzel navíc za s, spolu s vnější SEQUENCE, jejíž celková délka neodpovídá délce celého blobu

Poslední dva body jsou důležitější, než se zdají. Bajty navíc za SEQUENCE jsou klasický trik tvárnosti podpisu (signature malleability): připojíte balast a shovívavý verifikátor stále řekne platné, přestože bajtový řetězec, který ověřil, není bajtovým řetězcem, který byl podepsán. Stejný instinkt řídí i zpřísnění délek ASN.1 popsané v poznámce o parsování PKCS#12, a je to tentýž instinkt i zde. Na cestě ověřování je přijatá struktura, kterou nikdy nevyprodukoval konformní signatář, defekt, nikoli laskavost

Šířka souřadnice patří křivce, nikoli podpisu

HotPDF odvozuje výstupní šířku z OID pojmenované křivky, nikdy z délky DER, které právě naparsoval. To je druhá polovina převodu a ta polovina, ve které se dá snadno jemně splést. RFC 5480 §2.1.1 identifikuje křivku v parametrech SubjectPublicKeyInfo certifikátu a HPDFECDSACurveFromOID mapuje tři OID, které HotPDF podporuje: 1.2.840.10045.3.1.7 pro P-256, 1.3.132.0.34 pro P-384 a 1.3.132.0.35 pro P-521. HPDFECDSACoordinateSize pak vrátí 32, 48 nebo 66 bajtů a buffer P1363 je dvojnásobek toho: 64, 96 nebo 132. Každé dekódované celé číslo je zarovnáno vpravo do své poloviny, takže krátké r je zleva doplněno nulami, nikoli posunuto. P-521 je ten, který lidi chytí, protože 521 bitů je 65,125 bajtu a zaokrouhluje se nahoru na 66, což dává 132bajtový podpis, který by žádná intuice založená na mocninách dvou nepředpověděla. Veřejný klíč cestuje spolu s ním jako nekomprimovaný EC bod dle RFC 5480 §2.2, tedy 0x04 následované X a Y, takže HotPDF zkontroluje, že má přesně 1 + 2 * CoordinateSize bajtů a začíná 0x04, ještě než se dotkne 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;

Všimněte si posledního parametru. HPDFECDSAVerifyDigest přijímá také eseP1363 pro volající, kteří už mají podpis s pevnou šířkou, z hardwarového tokenu nebo vzdálené podepisovací služby, která vrací syrové r || s. I tato cesta stále vynucuje kontrolu délky a nenulovosti na obou polovinách, takže buffer správné velikosti plný nul je odmítnut, a nikoli propuštěn dál k poskytovateli

Proč obecný název algoritmu ECDSA selže na starším Windows?

Protože obecný název je novější než nasazovaná základna, do které se dodává. CNG vystavuje identifikátor algoritmu ECDSA, který odvodí křivku z importovaného klíče, a je to čistý způsob, jak tento kód napsat, ale BCryptOpenAlgorithmProvider jej garantovaně vyřeší jen na novějších verzích Windows. Na starším stroji volání otevření selže, handle poskytovatele zůstane nil a každé ověření ECDSA ve vaší aplikaci hlásí nepodporováno u podpisu, který je naprosto v pořádku. HotPDF se tomuto útesu vyhýbá tak, že místo toho otevírá identifikátory pro jednotlivé křivky. Jednou vyřeší ECDSA_P256, ECDSA_P384 a ECDSA_P521, uloží do cache jeden handle poskytovatele na křivku a zavře je při finalizaci unitu. Každé ověření pak dělá jen levnou práci: importuje dočasný veřejný klíč z ECCPUBLICBLOB, zavolá BCryptVerifySignature, klíč zničí. Žádné opakované LoadLibrary, žádné opakované GetProcAddress, žádné otevírání a zavírání poskytovatele na podpis. Dávkové ověřování několika stovek dokumentů ten rozdíl pocítí, stejně jako servisní proces, který by jinak pod zátěží churnoval handly poskytovatelů

Návratové kódy zůstávají v tomto rozlišení upřímné. evrProviderUnavailable znamená, že stroj nemohl HotPDF poskytnout poskytovatele; evrInvalid znamená, že CNG odpověděl STATUS_INVALID_SIGNATURE. Sloučením těchto dvou do jednoho selhání se problém s nasazením chybně nahlásí jako zfalšovaný dokument. Stejné oddělení mezi selháním prostředí a kryptografickým selháním se táhne i přes zpracování CNG a CAPI na straně podepisování, popsané v článku o podepisování z úložiště certifikátů a pořadí bajtů

Který certifikát to podepsal? SignerIdentifier jsou dvě různé věci

RFC 5652 §5.3 dělá z SignerIdentifier CHOICE, a verifikátor, který zvládá jen jednu z variant, bude potichu ověřovat proti špatnému klíči. První variantou je issuerAndSerialNumber, SEQUENCE držící Name vydavatele v syrovém DER a sériové INTEGER, a jeho párování je bajtové porovnání proti každému certifikátu v množině certificates CMS. Druhou variantou je [0] subjectKeyIdentifier, implicitně tagovaný OCTET STRING, a jeho párování vyžaduje ponořit se do certifikátu, nikoli jen porovnat pole hlavičky

Toto ponoření má vrstvu, která lidi překvapí. Identifikátor klíče žije v rozšíření X.509v3, takže HotPDF prochází pole rozšíření [3] tbsCertificate, najde rozšíření, jehož OID je 2.5.29.14, přeskočí volitelný critical BOOLEAN a vezme OCTET STRING extnValue. Tento bajtový řetězec není identifikátor. Podle RFC 5280 §4.2.1.2 je jeho obsah sám o sobě DER a typ KeyIdentifier je další OCTET STRING, takže parsujete podruhé, abyste se dostali ke skutečným bajtům. Zastavíte-li se o jednu vrstvu dřív, porovnáváte 22bajtový obal proti 20bajtovému identifikátoru, žádný certifikát nikdy nesedí a verifikátor se propadne na jakoukoli heuristiku, kterou jste napsali dál, a to je to skutečné riziko. Vzít první certifikát v množině je lákavá zkratka a je špatná vždy, když CMS nese řetěz certifikátů, což je většina případů, protože listový certifikát nemusí být první. HotPDF přijme neshodný certifikát jen tehdy, když kontejner drží přesně jeden; při více přítomných certifikátech je povinná přesná shoda SignerIdentifier. Ověření digestu proti veřejnému klíči mezilehlé CA nevyprodukuje přátelskou chybu, vyprodukuje sebejisté neplatné u dokumentu, který je v pořádku

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 hlásí P-256, P-384 nebo P-521, takže auditní log zaznamená, která křivka byla skutečně použita, a ne jen slovo ECDSA. Instalatérská práce na úrovni dokumentu kolem tohoto volání, konkrétně jak se hashují segmenty /ByteRange a proč se digest musí počítat nad souborem, nikoli nad naparsovaným stromem objektů, je předmětem doprovodného článku o ověřování digitálních podpisů PDF

Co vám to nedá

Zelený výsledek z HPDFECDSAVerifyDigest odpovídá jen na jednu otázku: tyto bajty podepsal soukromý klíč odpovídající tomuto veřejnému klíči. Neříká nic o tom, zda tento klíč patří někomu, komu byste měli důvěřovat. Budování řetězu k důvěryhodnému kotvišti, revokace přes CRL nebo OCSP a kontroly politik jsou samostatná práce, a jakýkoli produkt, který bez nich hlásí platný podpis, hlásí méně, než uživatel předpokládá. Data platnosti certifikátu jsou z přesně tohoto důvodu vystavena samostatně v THPDFSignatureInfo: podpis se může kryptograficky ověřit, přestože certifikát, který jej vytvořil, vypršel před dvěma lety. Podpora křivek je také záměrně úzká. Zpracovávají se tři prvočíselné křivky NIST a podpis nad jakoukoli jinou křivkou vrátí nepodporováno, nikoli odhad. Cesta CNG je jen pro Windows, což je správný kompromis pro VCL komponentu, ale stojí za to to zmínit dřív, než kolem ní naplánujete multiplatformní službu. A přísnost není konfigurovatelná: neexistuje shovívavý režim, který by přijal neminimalizovanou délku DER jen proto, že ji vyprodukoval nějaký starší signatář. Pokud se s takovým souborem setkáte v produkci, poctivou odpovědí je zaznamenat jej a jít po producentovi, nikoli rozšiřovat parser, dokud soubor neprojde

Zde popsaná cesta ověřování ECDSA je součástí standardní HotPDF Component pro Delphi a C++Builder, spolu s cestami RSA PKCS#1 v1.5 a RSA-PSS a kompletním záznamem informací o podpisu; produktová stránka obsahuje úplnou referenci digitálních podpisů