Technický článek

Kódování RSASSA-PSS-params podle RFC 4055 v PDFium Delphi

PDFium Component ve verzi 3.114.20 opravuje kódování RSASSA-PSS-params ve všech třech PAdES podepisovacích backendech, Windows CNG, macOS Keychain a PKCS#11. RFC 4055 §3.1 dává každému poli RSASSA-PSS-params explicitní context-specific tag, [0] po [3], a backendy emitovaly saltLength jako holý univerzální INTEGER, zatímco zapisovaly trailerField rovné svému defaultu. Bajty podpisu byly celou dobu správné. AlgorithmIdentifier, který je popisoval, nebyl a to samo stačí, aby verifikátor odmítl podpis

Frustrující část je místo, kde se bug schovává. CMS podpis má dvě poloviny, kryptografickou operaci a ASN.1, která verifikátorovi říká, jak operace proběhla. Trefte první správně a druhou špatně a výsledkem je dokument, který žádný nástroj držící se specifikace nedokáže odlišit od padělku. Tenhle článek je jen o té druhé polovině: jak se musí tagovat RSASSA-PSS-params, jak se tři backendy spletly stejným způsobem a jak vypadá opravené DER v pojmech TDerWriter

Proč verifikátor odmítne podpis RSASSA-PSS, jehož bajty jsou správné?

Protože RSASSA-PSS je to jediné RSA schéma, u kterého si verifikátor nedokáže obnovit parametry z podpisu samotného. Padding PKCS#1 v1.5 je plně určený OID sha256WithRSAEncryption, takže jeho parametry jsou holé NULL a není co pokazit. PSS je parametrizované hašovací funkcí, mask generating funkcí s vlastním hashem a délkou soli a RFC 8017 §A.2.3 nechává všechny tři otevřené. Podepisující si je vybere, AlgorithmIdentifier je nese a verifikátor je musí reprodukovat přesně, než může EMSA-PSS-VERIFY vůbec začít

Když tedy PDFium Component podepisuje se SHA-256, MGF1 nad SHA-256 a 32bajtovou solí, ta tři fakta musí přežít kódování DER tady a dekódování DER na jiné implementaci. Blok parametrů, který verifikátor nedokáže sparsovat, ukončí verifikaci, než se stane jakákoli modulární exponentiace. Blok, který parsuje jinak, je horší, protože RFC 4055 §3.1 dává saltLength default 20. Dekodér, který přeskočí pole, které nepozná, přistane na tom defaultu, pustí EMSA-PSS-VERIFY s 20bajtovou solí proti podpisu spočítanému s 32 a nahlásí špatný podpis bez tušinky, že problém je v metadatech, ne v klíči. Oba výsledky produkovalo kódování ve 3.114.19 podle toho, jak přísný verifikátor byl a žádný neukazoval na AlgorithmIdentifier

Co RFC 4055 §3.1 doopravdy vyžaduje od RSASSA-PSS-params

RFC 4055 §3.1 definuje RSASSA-PSS-params jako SEQUENCE čtyř polí, každé nesoucí explicitní context-specific tag a hodnotu DEFAULT:

// RSASSA-PSS-params ::= SEQUENCE {
//   hashAlgorithm      [0] HashAlgorithm      DEFAULT sha1,
//   maskGenAlgorithm   [1] MaskGenAlgorithm   DEFAULT mgf1SHA1,
//   saltLength         [2] INTEGER            DEFAULT 20,
//   trailerField       [3] TrailerField       DEFAULT trailerFieldBC
// }

Explicitní tagování v DER znamená, že každé pole je zabalené v konstruované context-specific TLV, A0 pro [0], A1 pro [1], A2 pro [2] a A3 pro [3], s univerzálním kódováním hodnoty vnořeným uvnitř. Každé pole je tagované právě proto, že každé pole je volitelné svým defaultem. Bez tagů by dekodér nemohl rozlišit, zda SEQUENCE držící jediný AlgorithmIdentifier nese hashAlgorithm nebo maskGenAlgorithm, protože oba jsou typy SEQUENCE; s nimi číslo tagu identifikuje pole bez ohledu na to, kteří sousedé jsou přítomní. Hodnoty, které emituje PDFium Component, následují profil z ETSI TS 119 312 §7, SHA-256, MGF1 se SHA-256 a sůl rovná délce digestu, a zrcadlí přesně to, co se říká každému platformnímu podepisovacímu volání: BCRYPT_PSS_PADDING_INFO s cbSalt 32 pro NCryptSignHash, CK_RSA_PKCS_PSS_PARAMS s sLen 32 pro mechanismus PKCS#11 a PSS algoritmus SHA-256 digest-podepisování ve Security frameworku

Diagram RSASSA-PSS-params podle RFC 4055 v PDFium Component: hashAlgorithm A0, maskGenAlgorithm A1 a saltLength A2 nesou explicitní context tagy s hodnotami DEFAULT, profil ETSI emituje SHA-256, MGF1 se SHA-256 a sůl 32 a trailerField A3 se rovná trailerFieldBC, takže DER ho vynechává úplně
Každé pole je tagované právě proto, že každé pole je volitelné svým defaultem, takže číslo tagu identifikuje pole bez ohledu na to, které sousedy enkodér vynechá

Jak tři backendy udělaly tutéž chybu

Kódování ve 3.114.19 tagovalo první dvě pole a poslední dvě nechalo holé, identicky v TWinCmsSigner, TKeychainCmsSigner a TPkcs11CmsSigner. Ta symetrie není náhoda: všechny tři implementují rozhraní ICmsSigner z FPdfCms.pas a jejich těla GetSignatureAlgorithmParams byla psaná podle jedné šablony. Šablona zněla takto:

// Před 3.114.20: [0] a [1] tagované, [2] a [3] ne
Result := W.Sequence(Concat4(
  W.ContextSpecific(0, W.AlgId(OID_SHA256), True),
  W.ContextSpecific(1, W.Sequence(ConcatBytes(
    W.OID(OID_MGF1), W.AlgId(OID_SHA256))), True),
  W.IntegerOf(32),      // holý INTEGER tam, kde byl vyžadován [2] EXPLICIT
  W.IntegerOf(1)));     // trailerField, rovné DEFAULT, musí chybět

Dekodér procházející tu SEQUENCE vidí A0, přečte hašovací algoritmus, vidí A1, přečte mask generating funkci a pak potká 02 01 20. To je univerzální INTEGER a RSASSA-PSS-params nemá nikde žádný netagovaný INTEGER člen. Přísný dekodér se tu zastaví. Shovívavý přeskočí nerozpoznaný prvek, nikdy nenajde A2, přiřadí saltLength svůj default 20, pak narazí na druhý zbloudilý INTEGER, 02 01 01, a má tentýž problém znovu. Ani jedna cesta se nedostane k 32bajtové soli. Sdílená šablona je efektivní, když je správná a stejně efektivní způsob, jak se splést třikrát, když není, proto oprava přistála ve všech třech jednotkách v jednom commitu a proto tři method bodies zůstávají strukturálně identické i poté. Budoucí backend by měl blok zkopírovat z jednoho z nich místo znovu odvozování, protože odvození je přesně místo, kde se chyba stala

Diagram DER bugu 3.114.19 v PDFium Component: po A0 a A1 potkaly parametry holé 02 01 20 tam, kam patří explicitní tag A2, přísný dekodér se zastavil a shovívavý podepsal s defaultní 20bajtovou solí, zatímco 3.114.20 balí 32bajtovou sůl do A2
RSASSA-PSS-params nemá žádný netagovaný INTEGER člen, takže zbloudilé bajty byly buď selhání parsování, nebo tichý návrat k defaultní délce soli a ani jedna cesta se nedostala k 32 podepisujícího

Proč se trailerField vynechává místo tagování jako [3]?

Protože X.690 §11.5 říká, že DER enkodér nesmí kódovat komponentu, jejíž hodnota se rovná jejímu DEFAULT, a trailerField má DEFAULT trailerFieldBC, což je celé číslo 1. Zjevná náprava starého kódu, vyměnění holého W.IntegerOf(1) za W.ContextSpecific(3, W.IntegerOf(1), True), dá blok, který shovívavý BER dekodér akceptuje a přísný DER dekodér má právo odmítnout. Hodnota není špatná. Její přítomnost je. Tutéž pravidlo je důvod, proč ostatní tři pole jsou přítomná: SHA-256 není defaultní sha1, MGF1 se SHA-256 není defaultní mgf1SHA1 a 32 není defaultní 20. Podepisoval-li by backend se SHA-1 a 20bajtovou solí, zredukoval by RFC 4055 §3.1 parametry na prázdnou SEQUENCE, 30 00, a tu prázdnou SEQUENCE místo NULL je to, co verifikátor čeká. PDFium Component tu podobu nikdy neemituje, protože s těmi hodnotami nikdy nepodepisuje, ale je to případ, který chytne každého, kdo předpokládá, že „bez parametrů" se vždycky hláskuje 05 00

Tohle je rozdíl mezi DER a BER, který u podpisů konkrétně záleží. BER dovoluje enkodéru zahrnout komponentu s defaultní hodnotou; DER to zakazuje, protože DER existuje proto, aby jedna hodnota měla přesně jedno kódování a podpis nad strukturou se dvěma legálními kódováními je podpis, o kterém se dá polemizovat. Všechno uvnitř CMS signedAttrs je proto DER a blok parametrů cestuje uvnitř signedAttrs skrz atribut cmsAlgorithmProtection stejně jako ve vnějším signatureAlgorithm, takže výjimku nedostává

Diagram pravidla X.690 11.5 v opravě PSS v PDFium Component: trailerField rovné svému DEFAULT trailerFieldBC musí zůstat nepřítomné, protože tagovaný obal A3 je kódování, které přísné DER odmítá, zatímco saltLength 32 se liší od defaultních 20 a musí být přítomné jako A2
DER existuje proto, aby jedna hodnota měla přesně jedno kódování a komponenta rovná svému defaultu už má nejkratší kódování, jaké je, totiž neobjevit se vůbec

Opravené kódování TDerWriter

PDFium Component teď staví parametry třemi voláními TDerWriter.ContextSpecific z FPdfAsn1.pas, jedno na nedefaultní pole, každé s argumentem Constructed nastaveným na True, aby vznikl obal explicitního tagu, a bez jediné řádky pro pole traileru. Toto je tělo TWinCmsSigner.GetSignatureAlgorithmParams s vypsanými OID; jednotky Keychain a PKCS#11 hláskují tytéž hodnoty jako OID_SHA256, OID_MGF1 a OID_RSASSA_PSS:

function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
  W: TDerWriter;
begin
  if FPaddingScheme = psRsaPss then
  begin
    W := TDerWriter.Create;
    try
      // RFC 4055 3.1 taguje všechna čtyři pole. saltLength je [2]; holý
      // INTEGER se tady čte jako začátek dalšího pole. trailerField
      // je [3] s DEFAULT 1 a X.690 11.5 zakazuje kódovat hodnotu
      // rovnou defaultu, takže se vynechává úplně
      Result := W.Sequence(Concat3(
        W.ContextSpecific(0, W.AlgId('2.16.840.1.101.3.4.2.1'), True),
        W.ContextSpecific(1, W.Sequence(ConcatBytes(
          W.OID('1.2.840.113549.1.1.8'),          // id-mgf1
          W.AlgId('2.16.840.1.101.3.4.2.1'))), True),
        W.ContextSpecific(2, W.IntegerOf(32), True)));
    finally
      W.Free;
    end;
  end
  else
    Result := nil;   // PKCS#1 v1.5 a ECDSA: AlgIdWithParams zapisuje NULL
end;

Dva detaily okolního stroje mají význam. TDerWriter.AlgId produkuje AlgorithmIdentifier s NULL parametry, což je to, co RFC 4055 §2.1 říká enkodérům generovat pro vnořené hashAlgorithm i pro vnitřní hash MGF1. A stavitel CMS v FPdfCms.pas páruje OID podpisu s těmito bajty přes TDerWriter.AlgIdWithParams, které substituuje NULL, když jsou parametry nil; proto psRsaPkcs1v15 a psEcdsa prostě vrací nil a nebyly nikdy postižené a proto 1.2.840.113549.1.1.10, id-RSASSA-PSS, je jediné OID podpisu ze tří nesoucí reálný blok parametrů. Výsledné bajty pro profil SHA-256 jsou fixní a dost krátké na kontrolu okem: vnější SEQUENCE 30 34 držící A0 0F kolem 15bajtového AlgorithmIdentifier SHA-256, A1 1C kolem 28bajtového AlgorithmIdentifier MGF1, jehož vlastními parametry je týž AlgorithmIdentifier SHA-256, a A2 03 02 01 20 pro sůl. Ukazuje-li dump vašeho signatureAlgorithm 02 01 20 na vrchní úrovni SEQUENCE parametrů místo uvnitř A2, díváte se na kódování ve 3.114.19

Proč test sada nechytla zdeformovaný AlgorithmIdentifier?

Protože testy PAdES řídí stavitele CMS přes fake signer, který hlásí sha256WithRSAEncryption a vrací nil z GetSignatureAlgorithmParams, takže blok parametrů PSS nebyl v testu vůbec nikdy postavený. To je rozumný design pro testy, které musí běžet bez úložiště certifikátů, Keychainu nebo tokenu a má slepé místo s přesným tvarem: cokoli, co produkuje jen reálný backend, protahuje jen reálný backend. Druhá vrstva je zajímavější. PDFium Component taky umisťuje AlgorithmIdentifier podpisu, parametry nevyjímaje, dovnitř podepsaného atributu cmsAlgorithmProtection z RFC 6211 a verifikátor srovnává tu kopii proti vnějšímu signatureAlgorithm. Obě kopie vzešly ze stejného volání, takže seděly dokonale a každá interní kontrola souladu prošla. Kódování bylo sebe-konzistentní a špatné, kategorie bugu, kterou žádné srovnávání struktury se samou sebou neodhalí, a tutéž lekci s jinou strukturou vypráví CMS signedAttrs a řazení DER SET OF, kde SET hashovaný v jednom pořadí a emitovaný v jiném vypadal dobře, dokud cizí verifikátor nepřepočítal hash

Co tuhle třídu bugů chytá, je dekodér nepsaný autorem enkodéru, pouštěný nad skutečným výstupem skutečného backendu. Windows verifikační cesta v PDFium Component jde přes CryptoAPI místo vlastního readeru knihovny a odmítnutý PSS podpis tam je to, co vedlo zpět k parametrům. Jakákoli ASN.1, kterou implementace emituje pro jiné implementace, aby ji četly, si zaslouží aspoň jeden round trip dekodérem, který nekontroluje, a čím víc defaultů a tagů struktura má, tím víc ten round trip stojí za to

Kam se tohle řadí k zbytku příběhu PSS

Ta oprava je nezávislá na dvou dalších místech, kde může PSS v PAdES podpisu selhat, a jejich držení odsun zkracuje debugování. macOS backend může zjistit, že konkrétní klíč nebo starší systém odmítá PSS, a downgradeovat na PKCS#1 v1.5 a AlgorithmIdentifier musí downgrade následovat; to je otázka schopností, kterou pokrývá podepisování PAdES identitou macOS Keychain. PKCS#11 backend může podat tokenu CK_RSA_PKCS_PSS_PARAMS, jehož rozložení token čte jinak kvůli nesourodosti šířek integerů; to je otázka ABI, kterou pokrývá CK_ULONG a packing past PKCS#11. Tenhle článek je o třetím selhání, kde klíč byl ochotný, token spočítal správné bajty a DER popisující výsledek neodpovídal RFC 4055 §3.1

Podepisující, který ohlašuje PSS, bere na sebe povinnost, kterou podepisující v1.5 nikdy neměl: popsat své vlastní parametry v podobě, kterou jiná implementace dekóduje na tytéž tři hodnoty. RFC 4055 §3.1 fixuje tagy, X.690 §11.5 fixuje, která pole se smí objevit, a ETSI TS 119 312 §7 fixuje hodnoty, které stojí za volbu. Všechny tři backendy PDFium Delphi komponenty se dodávají jako zdroják, takže tělo GetSignatureAlgorithmParams výše je to, které si můžete přečíst, vydumpovat a srovnat proti vlastnímu verifikátoru místo přijmout na důvěru