Tehnički članak

RSASSA-PSS-params po RFC 4055 u PDFium Delphiju

PDFium Component verzija 3.114.20 ispravlja kodiranje RSASSA-PSS-params u sva tri PAdES backend-a za potpisivanje, Windows CNG, macOS Keychain i PKCS#11. RFC 4055 §3.1 daje svakom polju RSASSA-PSS-params eksplicitan context-specific tag, [0] do [3], a backend-ovi su emitovali saltLength kao goli univerzalni INTEGER dok su ispisivali trailerField jednak svojoj podrazumevanoj vrednosti. Bajtovi potpisa bili su tačni sve vreme. AlgorithmIdentifier koji ih opisuje nije, a samo to je dovoljno da verifikator odbije potpis

Frustrirajući deo je gde se bug krije. CMS potpis ima dve polovine, kriptografsku operaciju i ASN.1 koji verifikatoru govori kako je operacija izvršena. Uradite prvu tačno a drugu pogrešno, i rezultat je dokument koji nijedan alat koji prati specifikaciju ne može da razlikuje od falsifikata. Ovaj članak je samo o toj drugoj polovini: kako RSASSA-PSS-params mora biti tagovan, kako su tri backend-a pogrešila na isti način, i kako ispravljeni DER izgleda u terminima TDerWriter-a

Zašto verifikator odbija RSASSA-PSS potpis čiji su bajtovi tačni?

Zato što je RSASSA-PSS jedina RSA šema gde verifikator ne može da povrati parametre iz samog potpisa. PKCS#1 v1.5 padding potpuno je određen OID-om sha256WithRSAEncryption, pa su njegovi parametri goli NULL i nema šta da se pogreši. PSS je parametrizovan hash funkcijom, mask generation funkcijom sa sopstvenim hash-om, i dužinom salt-a, a RFC 8017 §A.2.3 ostavlja sve tri otvorene. Potpisnik ih bira, AlgorithmIdentifier ih nosi, i verifikator mora da ih reprodukuje tačno pre nego što EMSA-PSS-VERIFY uopšte može da počne

Pa kada PDFium Component potpisuje sa SHA-256, MGF1 nad SHA-256 i salt-om od 32 bajta, te tri činjenice moraju da prežive DER kodiranje ovde i DER dekodiranje na drugoj implementaciji. Blok parametara koji verifikator ne može da parsira završava verifikaciju pre nego što se dogodi bilo kakvo modularno stepenovanje. Blok koji parsira drugačije je gori, jer RFC 4055 §3.1 daje saltLength-u podrazumevanu vrednost 20. Dekoder koji preskoči polje koje ne prepoznaje sleće na tu podrazumevanu vrednost, pokreće EMSA-PSS-VERIFY sa salt-om od 20 bajtova nad potpisom izračunatim sa 32, i prijavljuje loš potpis bez ijednog nagoveštaja da je problem u metapodacima a ne u ključu. Oba ishoda su ono što je kodiranje iz 3.114.19 proizvodilo, zavisno od toga koliko je verifikator strog, i nijedan ne pokazuje na AlgorithmIdentifier

Šta RFC 4055 §3.1 zaista zahteva od RSASSA-PSS-params

RFC 4055 §3.1 definiše RSASSA-PSS-params kao SEQUENCE od četiri polja, svako sa eksplicitnim context-specific tagom i DEFAULT vrednošću:

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

Eksplicitno tagovanje u DER-u znači da je svako polje umotano u constructed context-specific TLV, A0 za [0], A1 za [1], A2 za [2] i A3 za [3], sa univerzalnim kodiranjem vrednosti ugnježdenim unutra. Svako polje je tagovano upravo zato što je svako polje opciono kroz svoju podrazumevanu vrednost. Bez tagova dekoder ne bi mogao da razlikuje da li SEQUENCE koji drži jedan AlgorithmIdentifier nosi hashAlgorithm ili maskGenAlgorithm, jer su oba SEQUENCE tipovi; sa njima, broj taga identifikuje polje bez obzira na to koji su susedi prisutni. Vrednosti koje PDFium Component emituje prate profil iz ETSI TS 119 312 §7, SHA-256, MGF1 sa SHA-256 i salt jednak dužini sažetka, i preslikavaju tačno ono što se svakom platformskom pozivu potpisivanja kaže: BCRYPT_PSS_PADDING_INFO sa cbSalt 32 za NCryptSignHash, CK_RSA_PKCS_PSS_PARAMS sa sLen 32 za PKCS#11 mehanizam, i PSS algoritam za potpisivanje SHA-256 sažetkom u Security framework-u

PDFium Component dijagram RSASSA-PSS-params po RFC 4055: hashAlgorithm A0, maskGenAlgorithm A1 i saltLength A2 nose eksplicitne context tagove sa DEFAULT vrednostima, ETSI profil emituje SHA-256, MGF1 sa SHA-256 i salt 32, a trailerField A3 jednak je trailerFieldBC pa ga DER potpuno izostavlja
Svako polje je tagovano upravo zato što je svako polje opciono kroz svoju podrazumevanu vrednost, pa broj taga identifikuje polje bez obzira na to koje susede enkoder izostavi

Kako su tri backend-a napravila istu grešku

Kodiranje u 3.114.19 tagovalo je prva dva polja a poslednja dva ostavilo gola, identično u TWinCmsSigner, TKeychainCmsSigner i TPkcs11CmsSigner. Ta simetrija nije slučajnost: sva tri implementiraju interfejs ICmsSigner iz FPdfCms.pas, a njihova tela GetSignatureAlgorithmParams napisana su po jednom šablonu. Šablon je izgledao ovako:

// Pre 3.114.20: [0] i [1] tagovani, [2] i [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),      // goli INTEGER tamo gde je [2] EXPLICIT bio obavezan
  W.IntegerOf(1)));     // trailerField, jednak DEFAULT-u, mora biti odsutan

Dekoder koji prolazi kroz taj SEQUENCE vidi A0, pročita hash algoritam, vidi A1, pročita mask generation funkciju, i zatim sretne 02 01 20. To je univerzalni INTEGER, a RSASSA-PSS-params nigde nema netagovanog INTEGER člana. Strog dekoder se tu zaustavlja. Blag preskoči neprepoznati element, nikad ne nađe A2, dodeli saltLength-u podrazumevanu vrednost 20, zatim udari u drugi zalutali INTEGER, 02 01 01, i ima isti problem ponovo. Nijedna putanja ne stiže do salt-a od 32 bajta. Deljeni šablon je efikasan kada je tačan i jednako efikasan način da se pogreši tri puta kada nije, i zato je ispravka sletela u sva tri unita u jednom commitu i zato su ta tri tela metoda i posle toga strukturno identična. Budući backend treba da kopira blok iz nekog od ovih umesto da ga izvodi iznova, jer je izvođenje upravo mesto gde je greška napravljena

PDFium Component dijagram DER buga iz 3.114.19: posle A0 i A1 parametri su sreli goli 02 01 20 tamo gde pripada eksplicitni tag A2, strog dekoder se zaustavio a blag potpisao sa podrazumevanim salt-om od 20 bajtova, dok 3.114.20 umotava salt od 32 bajta u A2
RSASSA-PSS-params nema netagovanog INTEGER člana, pa su zalutali bajtovi bili ili otkaz parsiranja ili tiho vraćanje na podrazumevanu dužinu salt-a, i nijedna putanja nije stigla do potpisnikovih 32

Zašto se trailerField izostavlja umesto da se taguje kao [3]?

Zato što X.690 §11.5 kaže da DER enkoder ne sme da kodira komponentu čija je vrednost jednaka njenoj DEFAULT vrednosti, a trailerField ima DEFAULT trailerFieldBC, što je ceo broj 1. Očigledna ispravka starog koda, zamena golih W.IntegerOf(1) sa W.ContextSpecific(3, W.IntegerOf(1), True), daje blok koji permisivni BER dekoder prihvata a strog DER dekoder ima pravo da odbije. Vrednost nije pogrešna. Njeno prisustvo jeste. Isto pravilo objašnjava zašto su ostala tri polja prisutna: SHA-256 nije podrazumevani sha1, MGF1 sa SHA-256 nije podrazumevani mgf1SHA1, a 32 nije podrazumevanih 20. Da je backend potpisivao sa SHA-1 i salt-om od 20 bajtova, RFC 4055 §3.1 bi sažeo parametre u prazan SEQUENCE, 30 00, i taj prazan SEQUENCE a ne NULL je ono što verifikator očekuje. PDFium Component nikad ne emituje taj oblik jer nikad ne potpisuje tim vrednostima, ali to je slučaj koji hvata svakoga ko pretpostavi da se „nema parametara" uvek piše kao 05 00

To je razlika između DER-a i BER-a koja je važna posebno za potpise. BER dozvoljava enkoderu da uključi komponentu sa podrazumevanom vrednošću; DER to zabranjuje, jer DER postoji da bi jedna vrednost imala tačno jedno kodiranje, a potpis nad strukturom sa dva legalna kodiranja je potpis oko kojeg se može raspravljati. Sve unutar CMS signedAttrs je DER iz tog razloga, a blok parametara putuje unutar signedAttrs i kroz atribut cmsAlgorithmProtection kao i u spoljnom signatureAlgorithm, pa ne dobija nikakvo izuzeće

PDFium Component dijagram pravila X.690 11.5 u PSS ispravci: trailerField jednak svom DEFAULT-u trailerFieldBC mora ostati odsutan jer je tagovani A3 omotač kodiranje koje strog DER odbija, dok saltLength 32 odstupa od podrazumevanih 20 i mora biti prisutan kao A2
DER postoji da bi jedna vrednost imala tačno jedno kodiranje, a komponenta jednaka svojoj podrazumevanoj vrednosti već ima najkraće kodiranje koje postoji, naime da se ne pojavi uopšte

Ispravljeno TDerWriter kodiranje

PDFium Component sada gradi parametre sa tri poziva TDerWriter.ContextSpecific iz FPdfAsn1.pas, po jedan za svako polje koje nije podrazumevano, svaki sa argumentom Constructed postavljenim na True da proizvede explicit-tag omotač, i bez ijedne linije za polje trailer. Ovo je telo TWinCmsSigner.GetSignatureAlgorithmParams sa ispisanim OID-ovima; Keychain i PKCS#11 uniti pišu iste vrednosti kao OID_SHA256, OID_MGF1 i 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 sva četiri polja. saltLength je [2]; goli
      // INTEGER ovde se čita kao početak drugog polja. trailerField
      // je [3] sa DEFAULT 1, a X.690 11.5 zabranjuje kodiranje vrednosti
      // jednake podrazumevanoj, pa se potpuno izostavlja
      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 i ECDSA: AlgIdWithParams piše NULL
end;

Dva detalja okolne mašinerije su važna. TDerWriter.AlgId proizvodi AlgorithmIdentifier sa NULL parametrima, što je ono što RFC 4055 §2.1 nalaže enkoderima da generišu za ugnježdeni hashAlgorithm i za unutrašnji hash MGF1. A CMS builder u FPdfCms.pas uparuje OID potpisa sa ovim bajtovima preko TDerWriter.AlgIdWithParams, koji zamenjuje NULL kada su parametri nil; zato psRsaPkcs1v15 i psEcdsa jednostavno vraćaju nil i nikad nisu bili pogođeni, i zato je 1.2.840.113549.1.1.10, id-RSASSA-PSS, jedini OID potpisa od ta tri koji nosi pravi blok parametara. Dobijeni bajtovi za SHA-256 profil su fiksni i dovoljno kratki da se provere okom: spoljni SEQUENCE 30 34 koji drži A0 0F oko SHA-256 AlgorithmIdentifier-a od 15 bajtova, A1 1C oko MGF1 AlgorithmIdentifier-a od 28 bajtova čiji su sopstveni parametri taj isti SHA-256 AlgorithmIdentifier, i A2 03 02 01 20 za salt. Ako dump vašeg signatureAlgorithm-a pokazuje 02 01 20 na najvišem nivou SEQUENCE-a parametara a ne unutar A2, gledate kodiranje iz 3.114.19

Zašto test suite nije uhvatio malformiran AlgorithmIdentifier?

Zato što PAdES testovi gone CMS builder kroz lažnog potpisnika koji prijavljuje sha256WithRSAEncryption i vraća nil iz GetSignatureAlgorithmParams, pa blok PSS parametara nikad nije bio izgrađen ni u jednom testu. To je razuman dizajn za testove koji moraju da rade bez certificate store-a, Keychain-a ili tokena, i ima slepu tačku preciznog oblika: sve što proizvodi samo pravi backend vežba samo pravi backend. Drugi sloj je zanimljiviji. PDFium Component takođe smešta AlgorithmIdentifier potpisa, uključujući parametre, unutar potpisanog atributa cmsAlgorithmProtection iz RFC 6211, a verifikator poredi tu kopiju sa spoljnim signatureAlgorithm. Obe kopije dolaze iz istog poziva, pa su se savršeno poklapale i svaka interna provera konzistentnosti je prošla. Kodiranje je bilo samo-konzistentno i pogrešno, kategorija buga koju nikakvo poređenje strukture sa samom sobom ne može razotkriti, a ista pouka sa drugom strukturom ispričana je u tekstu CMS signedAttrs i DER SET OF sortiranje, gde je SET sažiman u jednom redu a emitovan u drugom izgledao dobro dok strani verifikator nije ponovo izračunao hash

Ono što zaista hvata ovu klasu bugova je dekoder koji nije napisao autor enkodera, pokrenut nad stvarnim izlazom stvarnog backend-a. Windows putanja verifikacije u PDFium Component-u ide kroz CryptoAPI a ne kroz sopstveni reader biblioteke, i odbijen PSS potpis tamo je ono što je odvelo nazad do parametara. Svaki ASN.1 koji implementacija emituje da bi ga čitale druge implementacije zaslužuje najmanje jedan round trip kroz dekoder koji ne kontroliše, a što struktura ima više podrazumevanih vrednosti i tagova, to taj round trip više vredi

Gde se ovo uklapa u ostatak PSS priče

Ova ispravka je nezavisna od druga dva mesta gde PSS može da pođe naopako u PAdES potpisu, i njihovo razdvajanje skraćuje debagovanje. macOS backend može da ustanovi da određeni ključ ili stariji sistem odbija PSS i da se spusti na PKCS#1 v1.5, i AlgorithmIdentifier mora da prati to spuštanje; to je pitanje mogućnosti, obrađeno u tekstu PAdES potpisivanje sa macOS Keychain identitetom. PKCS#11 backend može da preda tokenu CK_RSA_PKCS_PSS_PARAMS čiji raspored token čita drugačije zbog neslaganja širine celog broja; to je ABI pitanje, obrađeno u tekstu CK_ULONG i PKCS#11 zamka sa pakovanjem. Ovaj članak je o trećem otkazu, gde je ključ bio voljan, token izračunao tačne bajtove, a DER koji opisuje rezultat nije bio u skladu sa RFC 4055 §3.1

Potpisnik koji deklariše PSS preuzima obavezu koju v1.5 potpisnik nikad nije imao: da opiše sopstvene parametre u obliku koji druga implementacija dekodira u iste tri vrednosti. RFC 4055 §3.1 fiksira tagove, X.690 §11.5 fiksira koja polja smeju da se pojave, a ETSI TS 119 312 §7 fiksira vrednosti koje vredi izabrati. Sva tri backend-a PDFium Delphi komponente isporučuju se kao izvorni kod, pa je telo GetSignatureAlgorithmParams iznad ono koje možete čitati, dumpovati i porediti sa sopstvenim verifikatorom umesto da ga uzimate na veru