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