A PDFium Component 3.114.20 verziója javítja az RSASSA-PSS-params kódolását mindhárom PAdES-aláíró háttérben: a Windows CNG-ben, a macOS Keychainben és a PKCS#11-ben. Az RFC 4055 §3.1 az RSASSA-PSS-params minden mezőjéhez explicit, kontextus-specifikus taget rendel, a [0]-tól a [3]-ig, a hátterek viszont a saltLength értékét csupasz, univerzális INTEGER-ként bocsátották ki, miközben kiírtak egy trailerField elemet is az alapértékével egyenlő értékkel. Az aláírás bájtjai végig helyesek voltak. Az őket leíró AlgorithmIdentifier nem, és ez önmagában elég ahhoz, hogy egy validátor visszautasítsa az aláírást
A bosszantó rész az, hol bújik meg a hiba. Egy CMS-aláírásnak két fele van: a kriptográfiai művelet, és az az ASN.1, amely megmondja a validátornak, hogyan hajtották végre a műveletet. Ha az első helyes, a második viszont hibás, az eredmény olyan dokumentum, amelyet egyetlen specifikációkövető eszköz sem tud megkülönböztetni egy hamisítványtól. Ez a cikk kizárólag arról a második felről szól: hogyan kell megcímkézni az RSASSA-PSS-params elemet, hogyan rontotta el mindhárom háttér ugyanúgy, és hogyan néz ki a javított DER a TDerWriter fogalmaival
Miért utasít el egy validátor egy helyes bájtokkal rendelkező RSASSA-PSS aláírást?
Mert az RSASSA-PSS az az egyetlen RSA-séma, ahol a validátor nem tudja az aláírásból magából visszafejteni a paramétereket. A PKCS#1 v1.5 kitöltést teljes egészében meghatározza a sha256WithRSAEncryption OID, így a paraméterei csupasz NULL érték, és nincs mit elrontani. A PSS-t egy hash-függvény, egy saját hash-sel rendelkező maszkgeneráló függvény és egy sóhossz paraméterezi, és az RFC 8017 §A.2.3 mindhármat nyitva hagyja. Az aláíró választja meg őket, az AlgorithmIdentifier hordozza őket, a validátornak pedig pontosan reprodukálnia kell mindet, mielőtt az EMSA-PSS-VERIFY egyáltalán elkezdődhet
Amikor tehát a PDFium Component SHA-256-tal, SHA-256 feletti MGF1-gyel és 32 bájtos sóval ír alá, ennek a három ténynek túl kell élnie a DER-kódolást itt, és a DER-dekódolást egy másik implementációban. Egy paraméterblokk, amelyet a validátor nem tud feldolgozni, még a moduláris hatványozás előtt véget vet az ellenőrzésnek. Egy blokk, amelyet másként dolgoz fel, rosszabb, mert az RFC 4055 §3.1 a saltLength alapértékét 20-ban adja meg. Az a dekódoló, amely átugorja az általa nem ismert mezőt, erre az alapértékre esik, 20 bájtos sóval futtatja az EMSA-PSS-VERIFY-t egy 32-vel számolt aláíráson, és hibás aláírást jelent, anélkül hogy utalna rá, hogy a probléma metaadat, nem pedig a kulcs. Mindkét kimenet az, amit a 3.114.19 kódolása előállított, attól függően, mennyire volt szigorú a validátor, és egyik sem az AlgorithmIdentifierre mutat
Mit ír elő valójában az RFC 4055 §3.1 az RSASSA-PSS-params elemtől
Az RFC 4055 §3.1 az RSASSA-PSS-params elemet négy mezőből álló SEQUENCE-ként definiálja, mindegyikük explicit, kontextus-specifikus taget és DEFAULT értéket hordoz:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
Az explicit címkézés a DER-ben azt jelenti, hogy minden mezőt egy konstruált, kontextus-specifikus TLV vesz körül, A0 a [0]-hoz, A1 az [1]-hez, A2 a [2]-höz és A3 a [3]-hoz, belül pedig az érték univerzális kódolása van beágyazva. Minden mező pontosan azért kap címkét, mert minden mező opcionális az alapértéke révén. Címkék nélkül a dekódoló nem tudná megmondani, hogy egyetlen AlgorithmIdentifiert tartalmazó SEQUENCE a hashAlgorithm vagy a maskGenAlgorithm mezőt hordozza-e, mivel mindkettő SEQUENCE típus; a címkékkel viszont a tagszám azonosítja a mezőt, függetlenül attól, mely szomszédok vannak jelen. Azok az értékek, amelyeket a PDFium Component kibocsát, az ETSI TS 119 312 §7 profilját követik: SHA-256, MGF1 SHA-256-tal és a digest hosszával egyenlő só, és pontosan azt tükrözik, amit az egyes platformok aláíróhívása megkap: egy BCRYPT_PSS_PADDING_INFO 32-es cbSalt értékkel a NCryptSignHash számára, egy CK_RSA_PKCS_PSS_PARAMS 32-es sLen értékkel a PKCS#11 mechanizmushoz, és a SHA-256 digest-aláíró PSS-algoritmus a Security frameworkben
Hogyan követte el ugyanazt a hibát három háttér
A 3.114.19 kódolása az első két mezőt címkézte meg, az utolsó kettőt csupaszon hagyta, méghozzá azonosan a TWinCmsSigner, a TKeychainCmsSigner és a TPkcs11CmsSigner osztályban. Ez a szimmetria nem véletlen: mindhárom az FPdfCms.pas ICmsSigner interfészét valósítja meg, a GetSignatureAlgorithmParams törzsei pedig egyetlen sablon alapján íródtak. A sablon így hangzott:
// A 3.114.20 előtt: [0] és [1] címkézve, [2] és [3] nem
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), // csupasz INTEGER ott, ahol [2] EXPLICIT kellett volna
W.IntegerOf(1))); // a DEFAULT-dal egyenlő trailerField-nek hiányoznia kell
Az a dekódoló, amely végigjárja ezt a SEQUENCE-t, látja az A0-t, beolvassa a hash-algoritmust, látja az A1-et, beolvassa a maszkgeneráló függvényt, majd találkozik a 02 01 20 bájtsorozattal. Az egy univerzális INTEGER, az RSASSA-PSS-params elemnek pedig sehol nincs címke nélküli INTEGER tagja. A szigorú dekódoló ott megáll. Az elnéző átugorja a fel nem ismert elemet, soha nem talál A2-t, a saltLength értékének a 20-as alapértéket adja, majd belefut egy második kóbor INTEGER-be, a 02 01 01-be, és ugyanabba a problémába kerül újra. Egyik út sem jut el a 32 bájtos sóig. A közös sablon hatékony, amikor helyes, és ugyanolyan hatékony módja annak, hogy háromszor legyen hibás, amikor nem; ezért került be a javítás mindhárom unitba egyetlen commitban, és ezért maradt a három metódustörzs szerkezetileg azonos utána is. Egy jövőbeli háttérnek inkább innen kell másolnia a blokkot, semmint újra levezetnie, mert a levezetés az, ahol a hiba keletkezett
Miért marad ki a trailerField a [3] címkézés helyett?
Mert az X.690 §11.5 szerint a DER-kódoló nem kódolhat olyan komponenst, amelynek az értéke megegyezik a DEFAULT értékével, a trailerField DEFAULT értéke pedig trailerFieldBC, azaz az 1 egész szám. A régi kód kézenfekvő javítása — a csupasz W.IntegerOf(1) lecserélése W.ContextSpecific(3, W.IntegerOf(1), True) hívásra — olyan blokkot ad, amelyet egy megengedő BER-dekódoló elfogad, egy szigorú DER-dekódoló viszont jogosult visszautasítani. Az érték nem hibás. A jelenléte az. Ugyanez a szabály az oka annak is, hogy a másik három mező ott van: a SHA-256 nem a sha1 alapérték, az MGF1 SHA-256-tal nem a mgf1SHA1 alapérték, a 32 pedig nem a 20-as alapérték. Ha a háttér SHA-1-gyel és 20 bájtos sóval írt volna alá, az RFC 4055 §3.1 összeomlasztaná a paramétereket egy üres SEQUENCE-re, 30 00-ra, és a validátor épp ezt az üres SEQUENCE-t várja, nem NULL-t. A PDFium Component soha nem bocsátja ki ezt az alakot, mert soha nem ír alá ezekkel az értékekkel, de épp ez az az eset, amely elcsípi azt, aki azt hiszi, hogy a „nincs paraméter" mindig 05 00-ként íródik
Ez az a DER és a BER közötti különbség, amely kifejezetten az aláírásoknál számít. A BER megengedi a kódolónak, hogy alapértékkel rendelkező komponenst is belefoglaljon; a DER megtiltja, mert a DER azért létezik, hogy egy értéknek pontosan egy kódolása legyen, két legális kódolással rendelkező struktúrára adott aláírás pedig olyan aláírás, amelyről vitatkozni lehet. A CMS signedAttrs belsejében ezért minden DER, a paraméterblokk pedig a signedAttrs belsejében utazik, a cmsAlgorithmProtection attribútumon keresztül éppúgy, mint a külső signatureAlgorithm elemben, így nem kap felmentést
A javított TDerWriter-kódolás
A PDFium Component mostantól három TDerWriter.ContextSpecific hívással építi fel a paramétereket az FPdfAsn1.pas-ból, egyet-egyet minden nem alapértelmezett mezőhöz, mindegyiknél True értékre állítva a Constructed argumentumot az explicit címkeburkoló előállításához, a trailer mezőhöz pedig egyetlen sort sem. Íme a TWinCmsSigner.GetSignatureAlgorithmParams törzse kiírt OID-okkal; a Keychain és a PKCS#11 unit ugyanezeket az értékeket OID_SHA256, OID_MGF1 és OID_RSASSA_PSS formában írja ki:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// Az RFC 4055 3.1 mind a négy mezőt címkézi. A saltLength a [2]; egy csupasz
// INTEGER itt egy másik mező kezdeteként olvasódik. A trailerField
// a [3], DEFAULT értéke 1, és az X.690 11.5 tiltja az alapértékkel
// egyenlő érték kódolását, ezért teljesen kimarad
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 és ECDSA: az AlgIdWithParams NULL-t ír
end;
Két részlet számít a környező gépezetből. A TDerWriter.AlgId NULL paraméterekkel rendelkező AlgorithmIdentifiert állít elő, és az RFC 4055 §2.1 pontosan ezt írja elő a kódolóknak a beágyazott hashAlgorithm és az MGF1 belső hash számára. A FPdfCms.pas-ban lévő CMS-építő pedig a TDerWriter.AlgIdWithParams úton párosítja az aláírás-OID-t ezekkel a bájtokkal, és NULL-t helyettesít be, ha a paraméterek nil értékűek; ezért ad vissza a psRsaPkcs1v15 és a psEcdsa egyszerűen nil értéket, és ezért nem is érintette őket soha a hiba, és ezért az 1.2.840.113549.1.1.10, az id-RSASSA-PSS az egyetlen a három aláírás-OID közül, amely valódi paraméterblokkot hordoz. A SHA-256 profilhoz tartozó eredő bájtok rögzítettek, és elég rövidek ahhoz, hogy szemmel ellenőrizhetők legyenek: egy külső 30 34 SEQUENCE, benne A0 0F a 15 bájtos SHA-256 AlgorithmIdentifier körül, A1 1C a 28 bájtos MGF1 AlgorithmIdentifier körül, amelynek a saját paraméterei ugyanaz a SHA-256 AlgorithmIdentifier, és A2 03 02 01 20 a sóhoz. Ha a signatureAlgorithm dumpjában a 02 01 20 a paraméter-SEQUENCE legfelső szintjén jelenik meg, nem pedig egy A2 belsejében, akkor a 3.114.19 kódolását látja
Miért nem csípte el a tesztsor a hibás AlgorithmIdentifiert?
Mert a PAdES-tesztek egy hamis aláíróval hajtják meg a CMS-építőt, amely sha256WithRSAEncryption-ot jelent, és nil értéket ad vissza a GetSignatureAlgorithmParams hívásból, így a PSS-paraméterblokk egyetlen tesztben sem épült fel. Ez ésszerű terv azokhoz a tesztekhez, amelyeknek tanúsítványtár, Keychain vagy token nélkül kell futniuk, és van egy pontosan körülírható alakú vakfoltja: amit kizárólag valódi háttér állít elő, azt kizárólag valódi háttér gyakorolja. A második réteg érdekesebb. A PDFium Component az aláírás AlgorithmIdentifierét, paraméterekkel együtt, az RFC 6211 cmsAlgorithmProtection aláírt attribútumába is beleteszi, a validátor pedig összeveti ezt a másolatot a külső signatureAlgorithm elemmel. Mindkét másolat ugyanabból a hívásból származott, így tökéletesen egyeztek, és minden belső konzisztenciaellenőrzés átment. A kódolás önmagával konzisztens és hibás volt — ez az a hibakategória, amelyet semmilyen mennyiségű önmagával való összehasonlítás nem képes felfedni —, és ugyanezt a tanulságot más struktúrával a CMS signedAttrs és a DER SET OF rendezés mondja el, ahol egy SET az egyik sorrendben hashelve és a másikban kibocsátva rendben lévőnek látszott, amíg egy idegen validátor újra nem számolta a hasht
Amit viszont elcsíp ezt a hibakategóriát, az egy olyan dekódoló, amelyet nem a kódoló szerzője írt, és amelyet a tényleges háttér tényleges kimenetén futtatnak. A PDFium Component Windows-verifikációs útja a CryptoAPI-n megy át, nem a könyvtár saját olvasóján, és egy ott visszautasított PSS-aláírás vezetett vissza a paraméterekhez. Bármely ASN.1, amelyet egy implementáció más implementációk számára bocsát ki, megérdemel legalább egy körbejáratást egy olyan dekódolón keresztül, amelyet nem ő irányít, és minél több alapértéke és címkéje van a struktúrának, annál többet ér az a körbejáratás
Hogyan illeszkedik ez a PSS-történet többi részéhez
Ez a javítás független attól a két másik helytől, ahol a PSS elromolhat egy PAdES-aláírásban, és a szétválasztásuk lerövidíti a hibakeresést. A macOS-háttér úgy találhatja, hogy egy adott kulcs vagy egy régebbi rendszer visszautasítja a PSS-t, és visszalép PKCS#1 v1.5-re, az AlgorithmIdentifiernek pedig követnie kell a visszalépést; ez képességkérdés, amelyet a PAdES-aláírás macOS Keychain-identitással című írás tárgyal. A PKCS#11-háttér átadhat a tokennek egy olyan CK_RSA_PKCS_PSS_PARAMS struktúrát, amelynek az elrendezését a token egy egészszélesség-eltérés miatt másként olvassa; ez ABI-kérdés, amelyet a CK_ULONG és a PKCS#11 csomagolási csapdája tárgyal. Ez a cikk a harmadik hibáról szól, amikor a kulcs hajlandó volt, a token a helyes bájtokat számolta ki, az eredményt leíró DER viszont nem felelt meg az RFC 4055 §3.1-nek
Az a jegyző, amely PSS-t hirdet meg, olyan kötelezettséget vállal, amely a v1.5-ös jegyzőnek soha nem volt: úgy kell leírnia a saját paramétereit, hogy egy másik implementáció ugyanarra a három értékre dekódolja őket. Az RFC 4055 §3.1 a címkéket rögzíti, az X.690 §11.5 azt, hogy mely mezők szerepelhetnek, az ETSI TS 119 312 §7 pedig az érdemes választásokat. A PDFium Delphi komponens mindhárom háttere forrásként érkezik, így a fenti GetSignatureAlgorithmParams törzs az, amelyet elolvashat, kidumpolhat és összevethet a saját validátorával ahelyett, hogy vakon megbízna benne