PDFium Component v različici 3.114.20 popravi kodiranje RSASSA-PSS-params v vseh treh zaledjih za podpisovanje PAdES: Windows CNG, macOS Keychain in PKCS#11. RFC 4055 §3.1 vsakemu polju RSASSA-PSS-params daje izrecno oznako, specifično za kontekst, od [0] do [3], zaledja pa so saltLength oddajala kot goli univerzalni INTEGER, hkrati pa izpisala trailerField, enak svoji privzeti vrednosti. Bajti podpisa so bili ves čas pravilni. AlgorithmIdentifier, ki jih opisuje, pa ne, in že to zadostuje, da preverjevalnik podpis zavrne
Najužalostnejši del je, kje se hrošč skriva. Podpis CMS ima dve polovici: kriptografsko operacijo in ASN.1, ki preverjevalniku pove, kako je bila operacija izvedena. Prvo naredite prav in drugo narobe in dobite dokument, ki ga nobeno orodje, ki sledi specifikaciji, ne loči od ponaredka. Ta članek je le o drugi polovici: kako mora biti RSASSA-PSS-params označen, kako so tri zaledja zgrešila na isti način in kako je videti popravljeni DER v izrazih TDerWriter
Zakaj preverjevalnik zavrne podpis RSASSA-PSS, katerega bajti so pravilni?
Ker je RSASSA-PSS edina shema RSA, pri kateri preverjevalnik parametrov ne more obnoviti iz samega podpisa. Polnjenje PKCS#1 v1.5 je v celoti določeno z OID sha256WithRSAEncryption, zato so njegovi parametri goli NULL in ni česa zamočiti. PSS je parametriran s funkcijo zgoščevanja, funkcijo generiranja maske z lastno zgoščevalno funkcijo in dolžino soli, RFC 8017 §A.2.3 pa vse tri pušča odprte. Podpisnik jih izbere, AlgorithmIdentifier jih nosi, preverjevalnik pa jih mora natančno ponoviti, preden lahko EMSA-PSS-VERIFY sploh začne
Ko torej PDFium Component podpiše s SHA-256, MGF1 nad SHA-256 in 32-bajtno soljo, morajo ta tri dejstva preživeti kodiranje DER tukaj in dekodiranje DER v drugi izvedbi. Blok parametrov, ki ga preverjevalnik ne more razčleniti, konča preverjanje, preden se zgodi kakršno koli modularno potenciranje. Blok, ki ga razčleni drugače, je hujši, ker RFC 4055 §3.1 daje saltLength privzeto vrednost 20. Dekoder, ki preskoči polje, ki ga ne prepozna, pristane na tej privzeti vrednosti, požene EMSA-PSS-VERIFY z 20-bajtno soljo proti podpisu, izračunanemu s 32, in prijavi slab podpis brez namiga, da je težava v metapodatkih in ne v ključu. Oba izida je proizvedlo kodiranje v 3.114.19, odvisno od tega, kako strog je bil preverjevalnik, in noben ne kaže na AlgorithmIdentifier
Kaj RFC 4055 §3.1 pravzaprav zahteva od RSASSA-PSS-params
RFC 4055 §3.1 definira RSASSA-PSS-params kot SEQUENCE štirih polj, pri čemer vsako nosi izrecno oznako, specifično za kontekst, in privzeto vrednost:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
Izrecno označevanje v DER pomeni, da je vsako polje ovito v konstruiran TLV, specifičen za kontekst — A0 za [0], A1 za [1], A2 za [2] in A3 za [3] — z univerzalnim kodiranjem vrednosti, gnezdenim znotraj. Vsako polje je označeno ravno zato, ker je vsako polje neobvezno po svoji privzeti vrednosti. Brez oznak dekoder ne bi mogel ugotoviti, ali SEQUENCE z enim samim AlgorithmIdentifier nosi hashAlgorithm ali maskGenAlgorithm, ker sta oba tipa SEQUENCE; z njimi številka oznake identificira polje ne glede na to, kateri sosedje so prisotni. Vrednosti, ki jih oddaja PDFium Component, sledijo profilu iz ETSI TS 119 312 §7: SHA-256, MGF1 s SHA-256 in sol, enaka dolžini zgoščene vrednosti, zrcalijo pa natanko to, kar se pove vsakemu klicu podpisovanja na platformi: BCRYPT_PSS_PADDING_INFO s cbSalt 32 za NCryptSignHash, CK_RSA_PKCS_PSS_PARAMS s sLen 32 za mehanizem PKCS#11 in algoritem PSS za podpisovanje zgoščene vrednosti SHA-256 v ogrodju Security
Kako so tri zaledja naredila isto napako
Kodiranje v različici 3.114.19 je označilo prvi dve polji in zadnji dve pustilo goli, enako v TWinCmsSigner, TKeychainCmsSigner in TPkcs11CmsSigner. Ta simetrija ni naključje: vsa tri izvajajo vmesnik ICmsSigner iz FPdfCms.pas, njihova telesa GetSignatureAlgorithmParams pa so bila napisana po enem predlogu. Predloga je bila takšna:
// Pred 3.114.20: [0] in [1] označena, [2] in [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 tam, kjer se je zahteval [2] EXPLICIT
W.IntegerOf(1))); // trailerField, enak DEFAULT, mora biti odsoten
Dekoder, ki hodi po tej sekvenci SEQUENCE, vidi A0, prebere algoritem zgoščevanja, vidi A1, prebere funkcijo generiranja maske in nato naleti na 02 01 20. To je univerzalni INTEGER, RSASSA-PSS-params pa nima nikjer člana INTEGER brez oznake. Strog dekoder se tu ustavi. Popustljiv preskoči neprepoznani element, ne najde nobenega A2, dodeli saltLength privzeto vrednost 20, nato naleti na drugi zalutali INTEGER, 02 01 01, in ima isto težavo znova. Nobena pot ne prispe do 32-bajtne soli. Skupna predloga je učinkovita, kadar je pravilna, in enako učinkovit način, da se zmotite trikrat, kadar ni, zato je popravek padel v vse tri enote v enem commitu in zato telesa vseh treh metod po njem ostajajo strukturno enaka. Prihodnje zaledje naj blok prekopira iz enega od teh, namesto da bi ga znova izpeljevalo, ker je izpeljava natanko tam, kjer je nastala napaka
Zakaj je trailerField izpuščen in ne označen kot [3]?
Ker X.690 §11.5 pravi, da pisec DER ne sme kodirati komponente, katere vrednost je enaka njeni privzeti vrednosti DEFAULT, trailerField pa ima privzeto vrednost trailerFieldBC, to je celo število 1. Očitni popravek stare kode, zamenjava golega W.IntegerOf(1) s W.ContextSpecific(3, W.IntegerOf(1), True), da blok, ki ga popustljiv dekoder BER sprejme, strog dekoder DER pa ga je upravičen zavrniti. Vrednost ni napačna. Njena prisotnost je. Isto pravilo je razlog, da so druga tri polja prisotna: SHA-256 ni privzeti sha1, MGF1 s SHA-256 ni privzeti mgf1SHA1 in 32 ni privzetih 20. Če bi zaledje podpisovalo s SHA-1 in 20-bajtno soljo, bi RFC 4055 §3.1 parametre zložil v prazen SEQUENCE, 30 00, in prav ta prazen SEQUENCE in ne NULL je tisto, kar preverjevalnik pričakuje. PDFium Component te oblike nikoli ne odda, ker nikoli ne podpisuje s temi vrednostmi, a prav ta primer ujame vsakogar, ki predpostavi, da se "brez parametrov" vedno zapiše kot 05 00
To je razlika med DER in BER, ki šteje prav pri podpisih. BER piscu dovoli, da vključi komponento s privzeto vrednostjo; DER to prepove, ker DER obstaja zato, da ima ena vrednost natanko eno kodiranje, podpis nad strukturo z dvema zakonitima kodiranjema pa je podpis, o katerem se je mogoče prepirati. Vse znotraj signedAttrs v CMS je zato DER, blok parametrov pa potuje znotraj signedAttrs prek atributa cmsAlgorithmProtection in tudi v zunanjem signatureAlgorithm, zato ni oproščen
Popravljeno kodiranje v TDerWriter
PDFium Component parametre zdaj zgradi s tremi klici TDerWriter.ContextSpecific iz FPdfAsn1.pas, po enim na neprivzeto polje, vsak z argumentom Constructed, nastavljenim na True, da nastane ovitek z izrecno oznako, in brez vsake vrstice za polje trailer. To je telo TWinCmsSigner.GetSignatureAlgorithmParams z izpisanimi OID; enoti Keychain in PKCS#11 iste vrednosti zapišeta kot OID_SHA256, OID_MGF1 in OID_RSASSA_PSS:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1 označi vsa štiri polja. saltLength je [2]; goli
// INTEGER se tu bere kot začetek drugega polja. trailerField
// je [3] s privzeto vrednostjo 1, X.690 11.5 pa prepoveduje kodiranje
// vrednosti, enake privzeti, zato je v celoti izpuščen
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 in ECDSA: AlgIdWithParams zapiše NULL
end;
Pomembni sta dve podrobnosti okoliške mašinerije. TDerWriter.AlgId proizvede AlgorithmIdentifier s parametri NULL, kar RFC 4055 §2.1 naroča piscev, naj ustvarijo za gnezdeni hashAlgorithm in za notranjo zgoščevalno funkcijo MGF1. In gradnik CMS v FPdfCms.pas OID podpisa s temi bajti poveže prek TDerWriter.AlgIdWithParams, ki nadomesti NULL, kadar so parametri nil; zato psRsaPkcs1v15 in psEcdsa preprosto vrneta nil in nista bila nikoli prizadeta, in zato je 1.2.840.113549.1.1.10, id-RSASSA-PSS, edini OID podpisa od teh treh, ki nosi resničen blok parametrov. Nastali bajti za profil SHA-256 so fiksni in dovolj kratki za preverjanje na oko: zunanji SEQUENCE 30 34, ki drži A0 0F okoli 15-bajtnega AlgorithmIdentifier SHA-256, A1 1C okoli 28-bajtnega AlgorithmIdentifier MGF1, katerega lastni parametri so ta isti AlgorithmIdentifier SHA-256, in A2 03 02 01 20 za sol. Če izpis vašega signatureAlgorithm pokaže 02 01 20 na vrhnji ravni SEQUENCE parametrov in ne znotraj A2, gledate kodiranje iz 3.114.19
Zakaj preskusni niz ni ujel popačenega AlgorithmIdentifier?
Ker preskusi PAdES ženejo gradnik CMS skozi lažnega podpisnika, ki poroča sha256WithRSAEncryption in iz GetSignatureAlgorithmParams vrne nil, zato blok parametrov PSS v preskusu ni bil nikoli zgrajen. To je razumna zasnova za preskuse, ki morajo teči brez hrambe potrdil, Keychaina ali žetona, in ima slepo pego s prav določeno obliko: kar proizvede le resnično zaledje, preizkusi tudi le resnično zaledje. Druga plast je bolj zanimiva. PDFium Component podpisni AlgorithmIdentifier, skupaj s parametri, položi tudi v podpisani atribut cmsAlgorithmProtection iz RFC 6211, preverjevalnik pa to kopijo primerja z zunanjim signatureAlgorithm. Obe kopiji sta prišli iz istega klica, zato sta se popolnoma ujemali in vsako notranje preverjanje skladnosti je šlo skozi. Kodiranje je bilo samoskladno in napačno, to je kategorija hrošča, ki je nobeno primerjanje strukture s samo sabo ne more razkriti, in isto lekcijo z drugačno strukturo pripoveduje CMS signedAttrs in razvrščanje DER SET OF, kjer je bil SET zgoščen v enem vrstnem redu in oddan v drugem videti v redu, dokler tuji preverjevalnik ni znova izračunal zgoščene vrednosti
Kar pa to vrsto hrošča res ujame, je dekoder, ki ga ni napisal avtor pisca, pognan nad resničnim izhodom resničnega zaledja. Pot preverjanja v sistemu Windows v PDFium Component gre skozi CryptoAPI in ne skozi bralnik knjižnice same, in zavrnjen podpis PSS tam je tisto, kar je vodilo nazaj do parametrov. Vsak ASN.1, ki ga izvedba odda, da bi ga brale druge izvedbe, si zasluži vsaj en obhod skozi dekoder, ki ga ne nadzoruje, in več privzetih vrednosti in oznak kot jih ima struktura, več je ta obhod vreden
Kam to spada glede na preostalo zgodbo PSS
Ta popravek je neodvisen od drugih dveh mest, kjer lahko PSS v podpisu PAdES zaide, in njuno ločevanje skrajša odpravljanje napak. Zaledje za macOS lahko ugotovi, da določen ključ ali starejši sistem PSS zavrača, in se spusti na PKCS#1 v1.5, AlgorithmIdentifier pa mora temu spustu slediti; to je vprašanje zmogljivosti, obdelano v podpisovanju PAdES z identiteto macOS Keychain. Zaledje PKCS#11 lahko žetonu poda CK_RSA_PKCS_PSS_PARAMS, katerega postavitev žeton bere drugače zaradi neujemanja v širini celega števila; to je vprašanje ABI-ja, obdelano v CK_ULONG in pasti pakiranja pri PKCS#11. Ta članek je o tretji odpovedi, kjer je bil ključ voljan, žeton je izračunal prave bajte, DER, ki opisuje rezultat, pa ni ustrezal RFC 4055 §3.1
Podpisnik, ki razglasi PSS, prevzame obveznost, ki je podpisnik v1.5 ni imel: svoje parametre opisati v obliki, ki jo druga izvedba dekodira v iste tri vrednosti. RFC 4055 §3.1 določi oznake, X.690 §11.5 določi, katera polja se smejo pojaviti, ETSI TS 119 312 §7 pa določi vrednosti, ki jih velja izbrati. Vsa tri zaledja komponente PDFium za Delphi izhajajo kot izvorna koda, zato je telo GetSignatureAlgorithmParams zgoraj tisto, ki ga lahko preberete, izpišete in primerjate z lastnim preverjevalnikom, namesto da bi mu zaupali