Techninis straipsnis

RSASSA-PSS-params kodavimas pagal RFC 4055 Delphi

PDFium Component 3.114.20 versija sutvarko RSASSA-PSS-params kodavimą visose trijose PAdES pasirašymo realizacijose: Windows CNG, macOS Keychain ir PKCS#11. RFC 4055 §3.1 kiekvienam RSASSA-PSS-params laukui duoda aiškią konteksto žymą nuo [0] iki [3], o realizacijos saltLength išvesdavo kaip pliką universalų INTEGER, tuo pat metu išrašydamos trailerField, lygų jo numatytajai reikšmei. Parašo baitai visą laiką buvo teisingi. Juos aprašantis AlgorithmIdentifier — ne, ir vien to pakanka, kad tikrintuvas parašą atmestų

Erzina tai, kur klaida slepiasi. CMS parašas turi dvi puses: kriptografinę operaciją ir ASN.1, pasakantį tikrintuvui, kaip ta operacija buvo atlikta. Padarykite pirmą teisingai, o antrą klaidingai — ir gausite dokumentą, kurio joks specifikaciją atitinkantis įrankis negalės atskirti nuo klastotės. Šis straipsnis vien apie antrąją pusę: kaip RSASSA-PSS-params turi būti pažymėtas, kaip trys realizacijos suklydo vienodai ir kaip pataisytas DER atrodo TDerWriter terminais

Kodėl tikrintuvas atmeta RSASSA-PSS parašą, kurio baitai teisingi?

Nes RSASSA-PSS yra vienintelė RSA schema, kurioje tikrintuvas negali atkurti parametrų iš paties parašo. PKCS#1 v1.5 paminkštinimą visiškai nustato OID sha256WithRSAEncryption, tad jo parametrai yra plikas NULL ir nėra ko suklysti. PSS parametrizuojamas maišos funkcija, kaukių generavimo funkcija su savo maiša ir druskos ilgiu, o RFC 8017 §A.2.3 visus tris palieka atvirus. Juos pasirenka pasirašytojas, juos neša AlgorithmIdentifier, ir tikrintuvas turi juos tiksliai atkartoti, kol EMSA-PSS-VERIFY apskritai gali prasidėti

Tad kai PDFium Component pasirašo su SHA-256, MGF1 per SHA-256 ir 32 baitų druska, tie trys faktai turi išlikti DER kodavime čia ir DER dekodavime kitoje realizacijoje. Parametrų blokas, kurio tikrintuvas negali išanalizuoti, užbaigia tikrinimą dar prieš bet kokį modulinį kėlimą laipsniu. Blokas, kurį jis išanalizuoja kitaip, yra blogiau, nes RFC 4055 §3.1 saltLength duoda numatytąją reikšmę 20. Dekoderis, praleidžiantis neatpažintą lauką, patenka į tą numatytąją reikšmę, paleidžia EMSA-PSS-VERIFY su 20 baitų druska prieš parašą, apskaičiuotą su 32, ir praneša blogą parašą, neužsimindamas, kad problema yra metaduomenys, o ne raktas. Abu rezultatai yra tai, ką pagamindavo 3.114.19 kodavimas, priklausomai nuo to, koks griežtas buvo tikrintuvas, ir nė vienas nenurodo į AlgorithmIdentifier

Ko RFC 4055 §3.1 iš tikrųjų reikalauja iš RSASSA-PSS-params

RFC 4055 §3.1 apibrėžia RSASSA-PSS-params kaip SEQUENCE iš keturių laukų, kurių kiekvienas neša aiškią konteksto žymą ir DEFAULT reikšmę:

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

Aiškusis žymėjimas DER reikšmą kiekvieną lauką įvynioja į sukonstruotą konteksto TLV: A0 skirtas [0], A1 — [1], A2 — [2], o A3 — [3], o reikšmės universalusis kodavimas įdedamas į vidų. Kiekvienas laukas pažymėtas būtent todėl, kad kiekvienas laukas yra neprivalomas per savo numatytąją reikšmę. Be žymų dekoderis negalėtų pasakyti, ar SEQUENCE su vienu AlgorithmIdentifier neša hashAlgorithm, ar maskGenAlgorithm, nes abu yra SEQUENCE tipai; su jomis žymos numeris identifikuoja lauką nepaisant to, kurių kaimynų yra. Reikšmės, kurias išveda PDFium Component, seka ETSI TS 119 312 §7 profilį — SHA-256, MGF1 su SHA-256 ir druska, lygia maišos ilgiui — ir tiksliai atspindi tai, kam sakoma kiekvienas platformos pasirašymo kvietimas: BCRYPT_PSS_PADDING_INFO su cbSalt 32 NCryptSignHash atveju, CK_RSA_PKCS_PSS_PARAMS su sLen 32 PKCS#11 mechanizmui ir SHA-256 digest pasirašymo PSS algoritmas Security sistemoje

PDFium Component schema su RSASSA-PSS-params pagal RFC 4055: hashAlgorithm A0, maskGenAlgorithm A1 ir saltLength A2 neša aiškias konteksto žymas su DEFAULT reikšmėmis, ETSI profilis išveda SHA-256, MGF1 su SHA-256 ir druską 32, o trailerField A3 lygus trailerFieldBC, tad DER jo visai neįtraukia
Kiekvienas laukas pažymėtas būtent todėl, kad kiekvienas laukas yra neprivalomas per savo numatytąją reikšmę, tad žymos numeris identifikuoja lauką nepaisant to, kurių kaimynų kodavimo įrenginys neįtraukia

Kaip trys realizacijos suklydo vienodai

3.114.19 kodavimas pažymėdavo pirmus du laukus, o paskutinius du palikdavo plikus — vienodai TWinCmsSigner, TKeychainCmsSigner ir TPkcs11CmsSigner. Ta simetrija nėra atsitiktinumas: visos trys įgyvendina ICmsSigner sąsają iš FPdfCms.pas, o jų GetSignatureAlgorithmParams turiniai buvo parašyti pagal vieną šabloną. Tas šablonas atrodė taip:

// Iki 3.114.20: [0] ir [1] pažymėti, [2] ir [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),      // plikas INTEGER ten, kur reikėjo [2] EXPLICIT
  W.IntegerOf(1)));     // trailerField, lygus DEFAULT, turi būti praleistas

Dekoderis, einantis per tą SEQUENCE, pamato A0, perskaito maišos algoritmą, pamato A1, perskaito kaukių generavimo funkciją, o tada sutinka 02 01 20. Tai universalus INTEGER, o RSASSA-PSS-params niekur neturi nepažymėto INTEGER nario. Griežtas dekoderis tuo ir sustoja. Atlaidusis praleidžia neatpažintą elementą, neranda jokio A2, priskiria saltLength numatytąją reikšmę 20, o paskui užkliudo antrą paklydusį INTEGER 02 01 01 ir vėl turi tą pačią problemą. Nė vienas kelias nepasiekia 32 baitų druskos. Bendras šablonas yra efektyvus, kai teisingas, ir lygiai taip pat efektyvus būdas suklysti tris kartus, kai ne — todėl pataisa ir atsirado visuose trijuose moduliuose vienu įrašu ir todėl tie trys metodų turiniai po jos lieka struktūriškai identiški. Būsima realizacija turėtų nukopijuoti bloką iš vienos jų, o ne išvesti jį iš naujo, nes būtent išvedime klaida ir buvo padaryta

PDFium Component schema su 3.114.19 DER klaida: po A0 ir A1 parametrai sutiko pliką 02 01 20 ten, kur priklauso A2 aiški žyma, griežtas dekoderis sustojo, o atlaidusis pasirašė su numatytąja 20 baitų druska, o 3.114.20 apgaubia 32 baitų druską A2
RSASSA-PSS-params neturi nepažymėto INTEGER nario, tad paklydę baitai buvo arba analizės gedimas, arba tylus nusileidimas į numatytąjį druskos ilgį, ir nė vienas kelias nepasiekė pasirašytojo 32

Kodėl trailerField praleidžiamas, o ne pažymimas kaip [3]?

Nes X.690 §11.5 sako, kad DER kodavimo įrenginys neturi koduoti komponento, kurio reikšmė lygi jo DEFAULT, o trailerField turi DEFAULT trailerFieldBC, tai yra sveikąjį skaičių 1. Akivaizdi senojo kodo pataisa — pakeisti pliką W.IntegerOf(1) į W.ContextSpecific(3, W.IntegerOf(1), True) — duoda bloką, kurį atlaidus BER dekoderis priima, o griežtas DER dekoderis turi teisę atmesti. Reikšmė neteisinga ne dėl vertės. Neteisingas jos buvimas. Ta pati taisyklė paaiškina, kodėl kiti trys laukai yra vietoje: SHA-256 nėra numatytoji sha1, MGF1 su SHA-256 nėra numatytoji mgf1SHA1, o 32 nėra numatytieji 20. Jei realizacija būtų pasirašinėjusi su SHA-1 ir 20 baitų druska, RFC 4055 §3.1 parametrus suglaustų į tuščią SEQUENCE 30 00, ir būtent tuščios SEQUENCE, o ne NULL, tikrintuvas ir tikėtųsi. PDFium Component niekada neišveda tokios formos, nes niekada nepasirašo su tokiomis reikšmėmis, bet būtent tas atvejis pagauna kiekvieną, kuris mano, kad „jokių parametrų" visada rašoma 05 00

Tai skirtumas tarp DER ir BER, kuris svarbus būtent parašams. BER leidžia kodavimo įrenginiui įtraukti komponentą su numatytąja reikšme; DER to draudžia, nes DER egzistuoja tam, kad viena reikšmė turėtų lygiai vieną kodavimą, o parašas ant struktūros su dviem teisėtais kodavimais yra parašas, dėl kurio galima ginčytis. Dėl to viskas CMS signedAttrs viduje yra DER, o parametrų blokas keliauja signedAttrs viduje ir per cmsAlgorithmProtection atributą, ir išoriniame signatureAlgorithm, tad jokios išimties negauna

PDFium Component schema su X.690 11.5 taisykle PSS pataisoje: trailerField, lygus savo DEFAULT trailerFieldBC, turi likti praleistas, nes pažymėtas A3 apvalkalas yra kodavimas, kurį griežtas DER atmeta, o saltLength 32 skiriasi nuo numatytosios 20 ir turi būti vietoje kaip A2
DER egzistuoja tam, kad viena reikšmė turėtų lygiai vieną kodavimą, o komponentas, lygus savo numatytajai reikšmei, jau turi trumpiausią įmanomą kodavimą — tai, kad jo apskritai nėra

Pataisytas TDerWriter kodavimas

PDFium Component dabar parametrus kuria trimis TDerWriter.ContextSpecific kvietimais iš FPdfAsn1.pas, po vieną kiekvienam ne numatytajam laukui, kiekvieną su Constructed argumentu, nustatytu į True, kad būtų gautas aiškios žymos apvalkalas, ir visai be eilutės trailer laukui. Tai yra TWinCmsSigner.GetSignatureAlgorithmParams turinys su išrašytais OID; Keychain ir PKCS#11 moduliai tas pačias reikšmes rašo kaip OID_SHA256, OID_MGF1 ir OID_RSASSA_PSS:

function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
  W: TDerWriter;
begin
  if FPaddingScheme = psRsaPss then
  begin
    W := TDerWriter.Create;
    try
      // RFC 4055 3.1 žymi visus keturis laukus. saltLength yra [2];
      // plikas INTEGER čia perskaitomas kaip kito lauko pradžia. trailerField
      // yra [3] su DEFAULT 1, o X.690 11.5 draudžia koduoti reikšmę,
      // lygią numatytajai, tad jis praleidžiamas visiškai
      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 ir ECDSA: AlgIdWithParams rašo NULL
end;

Svarbios dvi aplinkinės mechanikos detalės. TDerWriter.AlgId sukuria AlgorithmIdentifier su NULL parametrais, ką RFC 4055 §2.1 ir liepia kodavimo įrenginiams generuoti įdėtam hashAlgorithm ir MGF1 vidinei maišai. O CMS kūrėjas FPdfCms.pas faile susieja parašo OID su šiais baitais per TDerWriter.AlgIdWithParams, kuris pakeičia NULL, kai parametrai yra nil; todėl psRsaPkcs1v15 ir psEcdsa tiesiog grąžina nil ir niekada nebuvo paliesti, ir todėl 1.2.840.113549.1.1.10, id-RSASSA-PSS, yra vienintelis iš trijų parašo OID, nešantis tikrą parametrų bloką. Gauti SHA-256 profilio baitai yra fiksuoti ir pakankamai trumpi, kad juos būtų galima patikrinti akimis: išorinė 30 34 SEQUENCE, laikanti A0 0F aplink 15 baitų SHA-256 AlgorithmIdentifier, A1 1C aplink 28 baitų MGF1 AlgorithmIdentifier, kurio paties parametrai yra ta pati SHA-256 AlgorithmIdentifier, ir A2 03 02 01 20 druskai. Jei jūsų signatureAlgorithm išklotinė rodo 02 01 20 parametrų SEQUENCE viršutiniame lygmenyje, o ne A2 viduje, žiūrite į 3.114.19 kodavimą

Kodėl testų rinkinys nepagavo sugadinto AlgorithmIdentifier?

Nes PAdES testai CMS kūrėją varo per netikrą pasirašytoją, kuris praneša sha256WithRSAEncryption ir grąžina nil iš GetSignatureAlgorithmParams, tad PSS parametrų blokas testuose nebuvo sukurtas nė karto. Tai pagrįstas sprendimas testams, kurie turi veikti be sertifikatų saugyklos, Keychain ar tokeno, ir jis turi akląją zoną su tikslia forma: visa, ką pagamina tik tikra realizacija, išbandoma tik tikra realizacija. Antrasis sluoksnis įdomesnis. PDFium Component taip pat įdeda parašo AlgorithmIdentifier, kartu su parametrais, į cmsAlgorithmProtection pasirašytą atributą iš RFC 6211, o tikrintuvas lygina tą kopiją su išoriniu signatureAlgorithm. Abi kopijos atėjo iš to paties kvietimo, tad sutapo nepriekaištingai ir kiekviena vidinio nuoseklumo patikra praėjo. Kodavimas buvo savyje nuoseklus ir klaidingas — tai klaidų kategorija, kurios neatskleidžia joks struktūros lyginimas su pačia savimi, ir ta pati pamoka su kita struktūra papasakota straipsnyje apie CMS signedAttrs ir DER SET OF rikiavimą, kur SET, maišuotas viena tvarka ir išvestas kita, atrodė gerai, kol svetimas tikrintuvas iš naujo apskaičiavo maišą

O ką tikrai pagauna šią klaidų klasę, tai dekoderis, kurio nerašė kodavimo įrenginio autorius, paleistas prieš tikrą tikros realizacijos išvestį. Windows tikrinimo kelias PDFium Component viduje eina per CryptoAPI, o ne per pačios bibliotekos skaitytuvą, ir būtent ten atmestas PSS parašas ir nuvedė atgal prie parametrų. Bet koks ASN.1, kurį realizacija išveda kitoms realizacijoms skaityti, nusipelno bent vienos kelionės pirmyn ir atgal per jai nepavaldų dekoderį, ir kuo daugiau struktūra turi numatytųjų reikšmių bei žymų, tuo ta kelionė vertingesnė

Kaip tai dera su likusia PSS istorija

Ši pataisa nepriklauso nuo kitų dviejų vietų, kur PSS PAdES paraše gali nueiti klaidingai, ir jų atskyrimas sutrumpina derinimą. macOS realizacija gali aptikti, kad konkretus raktas ar senesnė sistema atsisako PSS, ir nusileisti į PKCS#1 v1.5, o AlgorithmIdentifier turi sekti tą nusileidimą; tai galimybių klausimas, aptartas straipsnyje apie PAdES pasirašymą su macOS Keychain tapatybe. PKCS#11 realizacija gali paduoti tokenui CK_RSA_PKCS_PSS_PARAMS, kurio išdėstymą tokenas perskaito kitaip dėl sveikojo skaičiaus pločio neatitikimo; tai ABI klausimas, aptartas straipsnyje apie CK_ULONG ir PKCS#11 pakavimo spąstus. Šis straipsnis apie trečią nesėkmę, kai raktas buvo pasiruošęs, tokenas apskaičiavo teisingus baitus, o rezultatą aprašantis DER neatitiko RFC 4055 §3.1

Pasirašytojas, paskelbiantis PSS, prisiima prievolę, kurios v1.5 pasirašytojas niekada neturėjo: aprašyti savo parametrus tokia forma, kurią kita realizacija išdekoduoja į tas pačias tris reikšmes. RFC 4055 §3.1 fiksuoja žymas, X.690 §11.5 fiksuoja, kurie laukai gali pasirodyti, o ETSI TS 119 312 §7 fiksuoja vertas pasirinkti reikšmes. Visos trys PDFium Delphi komponento realizacijos platinamos kaip šaltinis, tad aukščiau esantis GetSignatureAlgorithmParams turinys yra tas, kurį galite perskaityti, išversti ir palyginti su savo tikrintuvu, o ne priimti tikėjimu