Tekninen artikkeli

RSASSA-PSS-params-koodaus RFC 4055:n mukaan Delphissä

PDFium Component -versio 3.114.20 korjaa RSASSA-PSS-params-koodauksen kaikissa kolmessa PAdES-allekirjoituksen taustajärjestelmässä: Windows CNG, macOS Keychain ja PKCS#11. RFC 4055 §3.1 antaa jokaiselle RSASSA-PSS-params-rakenteen kentälle eksplisiittisen kontekstikohtaisen tagin, [0]:sta [3]:een, ja taustajärjestelmät tuottivat saltLength-arvon paljaana yleisenä INTEGERinä samalla kun ne kirjoittivat oletusarvonsa suuruisen trailerField-kentän. Allekirjoituksen tavut olivat oikein koko ajan. Niitä kuvaava AlgorithmIdentifier ei ollut, ja se yksin riittää saamaan tarkistajan hylkäämään allekirjoituksen

Turhauttavaa on se, mihin bugi kätkeytyy. CMS-allekirjoituksessa on kaksi puoliskoa: kryptografinen operaatio ja se ASN.1, joka kertoo tarkistajalle, miten operaatio suoritettiin. Saa ensimmäisen oikein ja toisen väärin, niin tuloksena on asiakirja, jota mikään spesifikaatiota noudattava työkalu ei pysty erottamaan väärennöksestä. Tämä artikkeli käsittelee vain tuota toista puoliskoa: miten RSASSA-PSS-params on tagattava, miten kolme taustajärjestelmää sai sen väärin samalla tavalla ja miltä korjattu DER näyttää TDerWriter-termein

Miksi tarkistaja hylkää RSASSA-PSS-allekirjoituksen, jonka tavut ovat oikein?

Koska RSASSA-PSS on ainoa RSA-järjestelmä, jossa tarkistaja ei voi palauttaa parametreja itse allekirjoituksesta. PKCS#1 v1.5 -täyttö määräytyy täysin OID:n sha256WithRSAEncryption perusteella, joten sen parametrit ovat paljas NULL eikä siinä ole mitään väärin saatavaa. PSS parametroidaan tiivistefunktiolla, omalla tiivisteellään varustetulla masking generation function -funktiolla ja suolapituudella, ja RFC 8017 §A.2.3 jättää kaikki kolme avoimiksi. Allekirjoittaja valitsee ne, AlgorithmIdentifier kantaa ne, ja tarkistajan on toistettava ne täsmälleen ennen kuin EMSA-PSS-VERIFY voi edes alkaa

Kun PDFium Component siis allekirjoittaa SHA-256:lla, MGF1:llä SHA-256:n yli ja 32 tavun suolalla, noiden kolmen tosiasian on selvittävä DER-koodauksesta täällä ja DER-purkamisesta eri toteutuksessa. Parametrilohko, jota tarkistaja ei pysty jäsentämään, lopettaa varmennuksen ennen kuin yhtään modulaarista potenssiin korotusta tapahtuu. Lohko, jonka se jäsentää eri tavalla, on pahempi, koska RFC 4055 §3.1 antaa saltLength-arvolle oletuksen 20. Purkaja, joka ohittaa tuntemattoman kentän, laskeutuu tuohon oletukseen, ajaa EMSA-PSS-VERIFYn 20 tavun suolalla 32 tavun suolalla laskettua allekirjoitusta vasten ja raportoi huonosta allekirjoituksesta ilman vihjettäkään siitä, että ongelma on metatiedoissa eikä avaimessa. Molemmat lopputulokset ovat niitä, joita 3.114.19:n koodaus tuotti riippuen siitä, kuinka tiukka tarkistaja oli, eivätkä kumpikaan osoita AlgorithmIdentifieria

Mitä RFC 4055 §3.1 todella vaatii RSASSA-PSS-params-rakenteelta

RFC 4055 §3.1 määrittelee RSASSA-PSS-params-rakenteen neljän kentän SEQUENCE-rakenteeksi, joista jokainen kantaa eksplisiittistä kontekstikohtaista tagia ja DEFAULT-arvoa:

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

Eksplisiittinen tagaus DER:ssä tarkoittaa, että jokainen kenttä on kääritty rakennettuun kontekstikohtaiseen TLV:hen, A0 tagille [0], A1 tagille [1], A2 tagille [2] ja A3 tagille [3], ja arvon yleinen koodaus on sisäkkäin sen sisällä. Jokainen kenttä on tagattu juuri siksi, että jokainen kenttä on valinnainen oletusarvonsa kautta. Ilman tageja purkaja ei pystyisi sanomaan, kantaako yhden AlgorithmIdentifierin sisältävä SEQUENCE kenttää hashAlgorithm vai maskGenAlgorithm, koska molemmat ovat SEQUENCE-tyyppejä; tagien kanssa taginumero yksilöi kentän riippumatta siitä, mitkä naapurit ovat läsnä. Arvot, joita PDFium Component tuottaa, noudattavat ETSI TS 119 312 §7:n profiilia: SHA-256, MGF1 SHA-256:lla ja tiivistepituuden suuruinen suola, ja ne kuvastavat täsmälleen sitä, mitä kullekin alustan allekirjoituskutsulle kerrotaan: BCRYPT_PSS_PADDING_INFO arvolla cbSalt 32 NCryptSignHash-kutsulle, CK_RSA_PKCS_PSS_PARAMS arvolla sLen 32 PKCS#11-mekanismille ja SHA-256-tiivisteinen PSS-allekirjoitusalgoritmi Security-kehyksessä

PDFium Componentin kaavio RSASSA-PSS-params-rakenteesta RFC 4055:n mukaan: hashAlgorithm A0, maskGenAlgorithm A1 ja saltLength A2 kantavat eksplisiittiset kontekstitagit ja DEFAULT-arvot, ETSI-profiili tuottaa SHA-256:n, MGF1:n SHA-256:lla ja suolan 32, ja trailerField A3 on yhtä kuin trailerFieldBC joten DER jättää sen kokonaan pois
Jokainen kenttä on tagattu juuri siksi, että jokainen kenttä on valinnainen oletusarvonsa kautta, joten taginumero yksilöi kentän riippumatta siitä, mitkä naapurit kooderi jättää pois

Miten kolme taustajärjestelmää teki saman virheen

3.114.19:n koodaus tagasi kaksi ensimmäistä kenttää ja jätti kaksi viimeistä paljaiksi, samalla tavalla TWinCmsSigner-luokassa, TKeychainCmsSigner-luokassa ja TPkcs11CmsSigner-luokassa. Tuo symmetria ei ole sattumaa: kaikki kolme toteuttavat FPdfCms.pas-tiedoston ICmsSigner-rajapinnan, ja niiden GetSignatureAlgorithmParams-rungot kirjoitettiin yhdestä mallipohjasta. Mallipohja näytti tältä:

// Ennen versiota 3.114.20: [0] ja [1] tagattu, [2] ja [3] eivät
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),      // paljas INTEGER siinä missä [2] EXPLICIT oli pakollinen
  W.IntegerOf(1)));     // trailerField, joka on yhtä kuin DEFAULT, on jätettävä pois

Purkaja, joka kävelee tuon SEQUENCEn läpi, näkee A0, lukee tiivistealgoritmin, näkee A1, lukee mask generation function -funktion ja kohtaa sitten 02 01 20. Se on yleinen INTEGER, eikä RSASSA-PSS-params-rakenteessa ole yhtään tagitonta INTEGER-jäsentä missään. Tiukka purkaja pysähtyy siihen. Salliva ohittaa tuntemattoman elementin, ei löydä A2-tagia, antaa saltLength-arvolle sen oletuksen 20, osuu sitten toiseen irralliseen INTEGERiin 02 01 01 ja kohtaa saman ongelman uudelleen. Kumpikaan polku ei yllä 32 tavun suolaan. Jaettu mallipohja on tehokas kun se on oikein ja yhtä tehokas tapa olla väärässä kolme kertaa kun se ei ole, minkä takia korjaus laskeutui kaikkiin kolmeen yksikköön yhdessä commitissa ja minkä takia nuo kolme metodirunkoa pysyvät korjauksen jälkeen rakenteellisesti identtisinä. Tulevan taustajärjestelmän kannattaa kopioida lohko jostakin näistä sen sijaan että johtaisi sen uudelleen, koska juuri johtaminen oli se kohta, jossa virhe tehtiin

PDFium Componentin kaavio 3.114.19:n DER-viasta: A0- ja A1-tagien jälkeen parametrit kohtasivat paljaan 02 01 20 siinä missä A2-eksplisiittitagin kuuluisi olla, tiukka purkaja pysähtyi ja salliva allekirjoitti oletusarvoisella 20 tavun suolalla, kun taas 3.114.20 käärii 32 tavun suolan A2-tagiin
RSASSA-PSS-params-rakenteessa ei ole tagitonta INTEGER-jäsentä, joten irralliset tavut olivat joko jäsennysvirhe tai hiljainen paluu oletussuolapituuteen, eikä kumpikaan polku yltänyt allekirjoittajan arvoon 32

Miksi trailerField jätetään pois sen sijaan että se tagattaisiin [3]:na?

Koska X.690 §11.5 sanoo, ettei DER-kooderi saa koodata komponenttia, jonka arvo on yhtä kuin sen DEFAULT, ja trailerField-kentän DEFAULT on trailerFieldBC, eli kokonaisluku 1. Ilmeinen korjaus vanhaan koodiin, paljaan W.IntegerOf(1)-kutsun korvaaminen kutsulla W.ContextSpecific(3, W.IntegerOf(1), True), tuottaa lohkon, jonka salliva BER-purkaja hyväksyy ja jonka tiukka DER-purkaja on oikeutettu hylkäämään. Arvo ei ole väärä. Sen läsnäolo on. Sama sääntö on syy siihen, miksi kolme muuta kenttää ovat läsnä: SHA-256 ei ole oletus sha1, MGF1 SHA-256:lla ei ole oletus mgf1SHA1, eikä 32 ole oletus 20. Jos taustajärjestelmä olisi allekirjoittanut SHA-1:llä ja 20 tavun suolalla, RFC 4055 §3.1 kutistaisi parametrit tyhjäksi SEQUENCE-rakenteeksi 30 00, ja juuri sitä tyhjää SEQUENCE-rakennetta eikä NULL-arvoa tarkistaja odottaa. PDFium Component ei koskaan tuota tuota muotoa, koska se ei koskaan allekirjoita noilla arvoilla, mutta juuri se tapaus nappaa kenet tahansa, joka olettaa, että ei parametreja kirjoitetaan aina muodossa 05 00

Tämä on se DER:n ja BER:n ero, jolla on merkitystä erityisesti allekirjoituksille. BER sallii kooderin sisällyttää oletusarvollisen komponentin; DER kieltää sen, koska DER on olemassa siksi, että yhdellä arvolla olisi täsmälleen yksi koodaus, ja allekirjoitus rakenteen yli, jolla on kaksi laillista koodausta, on allekirjoitus, josta voi kiistellä. Kaikki CMS:n signedAttrs-rakenteen sisällä on DER:ää juuri siksi, ja parametrilohko kulkee signedAttrs-rakenteen sisällä sekä cmsAlgorithmProtection-attribuutin että ulomman signatureAlgorithm-kentän kautta, joten se ei saa vapautusta

PDFium Componentin kaavio X.690 11.5 -säännöstä PSS-korjauksessa: trailerField, joka on yhtä kuin sen DEFAULT trailerFieldBC, on jätettävä pois, koska tagattu A3-kääre on se koodaus jonka tiukka DER hylkää, kun taas saltLength 32 poikkeaa oletuksesta 20 ja on esiinnyttävä A2-tagina
DER on olemassa siksi, että yhdellä arvolla olisi täsmälleen yksi koodaus, ja oletusarvonsa suuruinen komponentti on jo lyhyimmässä mahdollisessa koodauksessa, eli ei esiinny lainkaan

Korjattu TDerWriter-koodaus

PDFium Component rakentaa parametrit nyt kolmella TDerWriter.ContextSpecific-kutsulla tiedostosta FPdfAsn1.pas, yksi per ei-oletuskenttä, kukin Constructed-argumentilla True eksplisiittitagin kääreen tuottamiseksi, eikä trailer-kentälle ole yhtään riviä. Tämä on TWinCmsSigner.GetSignatureAlgorithmParams-metodin runko OID-arvojen kirjoitettuna auki; Keychain- ja PKCS#11-yksiköt kirjoittavat samat arvot muodossa OID_SHA256, OID_MGF1 ja OID_RSASSA_PSS:

function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
  W: TDerWriter;
begin
  if FPaddingScheme = psRsaPss then
  begin
    W := TDerWriter.Create;
    try
      // RFC 4055 3.1 tagaa kaikki neljä kenttää. saltLength on [2]; paljas
      // INTEGER tässä luetaan toisen kentän alkuna. trailerField
      // on [3] oletusarvolla 1, ja X.690 11.5 kieltää arvon koodaamisen
      // oletuksen suuruiseksi, joten se jätetään kokonaan pois
      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 ja ECDSA: AlgIdWithParams kirjoittaa NULL
end;

Kaksi yksityiskohtaa ympäröivässä koneistossa on merkityksellisiä. TDerWriter.AlgId tuottaa AlgorithmIdentifierin NULL-parametreilla, mikä on se, mitä RFC 4055 §2.1 käskee kooderien tuottaa sisäkkäiselle hashAlgorithm-kentälle ja MGF1:n sisäiselle tiivisteelle. Ja FPdfCms.pas-tiedoston CMS-rakentaja parittaa allekirjoituksen OID:n näiden tavujen kanssa TDerWriter.AlgIdWithParams-funktion kautta, joka korvaa NULL-arvon kun parametrit ovat nil; siksi psRsaPkcs1v15 ja psEcdsa yksinkertaisesti palauttavat nil eivätkä koskaan olleet vaarassa, ja siksi 1.2.840.113549.1.1.10, id-RSASSA-PSS, on kolmesta allekirjoituksen OID:stä ainoa, joka kantaa oikeaa parametrilohkoa. Syntyvät tavut SHA-256-profiilille ovat kiinteät ja tarpeeksi lyhyet silmämääräiseen tarkistukseen: ulompi 30 34 SEQUENCE, joka pitää sisällään A0 0F 15-tavuisen SHA-256-AlgorithmIdentifierin ympärillä, A1 1C 28-tavuisen MGF1-AlgorithmIdentifierin ympärillä, jonka omat parametrit ovat tuo sama SHA-256-AlgorithmIdentifier, ja A2 03 02 01 20 suolalle. Jos signatureAlgorithm-kenttäsi dumppi näyttää muotoa 02 01 20 parametrien SEQUENCEn ylätasolla eikä A2-tagin sisällä, katsot 3.114.19:n koodausta

Miksi testisarja ei napannut virheellistä AlgorithmIdentifieria?

Koska PAdES-testit ajavat CMS-rakentajan väärennetyn allekirjoittajan läpi, joka raportoi sha256WithRSAEncryption-arvon ja palauttaa nil-arvon GetSignatureAlgorithmParams-funktiosta, joten PSS-parametrilohkoa ei koskaan rakennettu yhdessäkään testissä. Se on järkevä ratkaisu testeille, joiden on pakko toimia ilman varmennevarastoa, Keychainia tai tokenia, ja siinä on tarkkarajainen sokea piste: mitä tahansa, mitä tuottaa vain oikea taustajärjestelmä, harjoittaa vain oikea taustajärjestelmä. Toinen kerros on mielenkiintoisempi. PDFium Component sijoittaa allekirjoituksen AlgorithmIdentifierin, parametrit mukaan lukien, myös RFC 6211:n cmsAlgorithmProtection-allekirjoitusattribuutin sisään, ja tarkistaja vertaa tuota kopiota ulompaan signatureAlgorithm-kenttään. Molemmat kopiot tulivat samasta kutsusta, joten ne täsmäsivät täydellisesti ja jokainen sisäinen johdonmukaisuustarkistus meni läpi. Koodaus oli itsensä kanssa johdonmukainen ja väärä, se bugiluokka, jota mikään rakenteen vertaaminen itseensä ei voi paljastaa, ja sama oppi eri rakenteella kerrotaan artikkelissa CMS:n signedAttrs-rakenteesta ja DER SET OF -lajittelusta, jossa yhdessä järjestyksessä tiivistetty ja toisessa järjestyksessä tuotettu SET näytti hyvältä kunnes vieras tarkistaja laski tiivisteen uudelleen

Sen sijaan tämän bugiluokan nappaa kiinni purkaja, jota kooderin tekijä ei kirjoittanut, ajettuna oikean taustajärjestelmän todellista tulostetta vasten. PDFium Componentin Windows-varmennuspolku kulkee CryptoAPI:n eikä kirjaston oman lukijan kautta, ja hylätty PSS-allekirjoitus siellä oli se, mikä johti takaisin parametreihin. Mikä tahansa ASN.1, jonka toteutus tuottaa muiden toteutusten luettavaksi, ansaitsee ainakin yhden edestakaisen kierroksen purkajan kautta, jota se ei itse hallitse, ja mitä enemmän rakenteessa on oletuksia ja tageja, sitä enemmän tuo kierros on arvoinen

Mihin tämä asettuu muun PSS-tarinan joukkoon

Tämä korjaus on riippumaton kahdesta muusta paikasta, joissa PSS voi mennä pieleen PAdES-allekirjoituksessa, ja niiden pitäminen erillään lyhentää virheenjäljitystä. macOS-taustajärjestelmä voi havaita, että tietty avain tai vanhempi järjestelmä kieltäytyy PSS:stä, ja laskeutua PKCS#1 v1.5:een, jolloin AlgorithmIdentifierin on seurattava laskua; se on kyvykkyyskysymys, jota käsitellään artikkelissa PAdES-allekirjoittamisesta macOS Keychain -identiteetillä. PKCS#11-taustajärjestelmä voi ojentaa tokenille CK_RSA_PKCS_PSS_PARAMS-rakenteen, jonka token lukee eri tavalla kokonaislukuleveyden epäsuhdan takia; se on ABI-kysymys, jota käsitellään artikkelissa CK_ULONG-tyypistä ja PKCS#11:n pakkausansasta. Tämä artikkeli käsittelee kolmatta vikaa, jossa avain oli halukas, token laski oikeat tavut, eikä tulosta kuvaava DER noudattanut RFC 4055 §3.1:tä

PSS:n julistava allekirjoittaja ottaa velvoitteen, jota v1.5-allekirjoittajalla ei koskaan ollut: kuvata omat parametrinsa muodossa, jonka toinen toteutus purkaa samoiksi kolmeksi arvoksi. RFC 4055 §3.1 kiinnittää tagit, X.690 §11.5 kiinnittää sen, mitkä kentät saavat esiintyä, ja ETSI TS 119 312 §7 kiinnittää ne arvot, jotka kannattaa valita. PDFium Delphi -komponentin kaikki kolme taustajärjestelmää toimitetaan lähdekoodina, joten yllä oleva GetSignatureAlgorithmParams-runko on se, jonka voit lukea, dumpata ja verrata omaa tarkistajaasi vasten sen sijaan, että luottaisit siihen sokeasti