Teknisk artikkel

RSASSA-PSS-params etter RFC 4055 i PDFium Delphi

PDFium Component versjon 3.114.20 retter RSASSA-PSS-params-kodingen i alle tre PAdES-signeringsbackender: Windows CNG, macOS Keychain og PKCS#11. RFC 4055 §3.1 gir hvert felt i RSASSA-PSS-params en eksplisitt kontekstspesifikk tagg, [0] til [3], og backendene skrev ut saltLength som et bart universelt INTEGER samtidig som de skrev ut en trailerField lik standardverdien sin. Signaturbytene var korrekte hele tiden. AlgorithmIdentifier-en som beskriver dem, var det ikke, og det alene er nok for at en validator avviser signaturen

Det frustrerende er hvor feilen gjemmer seg. En CMS-signatur har to halvdeler: den kryptografiske operasjonen og ASN.1-en som forteller validatoren hvordan operasjonen ble utført. Får du den første riktig og den andre gal, er resultatet et dokument som ingen spesifikasjonsfølgende verktøy kan skille fra en forfalskning. Denne artikkelen handler bare om den andre halvdelen: hvordan RSASSA-PSS-params må tagges, hvordan tre backender tok feil på samme måte, og hvordan den rettede DER-en ser ut i TDerWriter-termer

Hvorfor avviser en validator en RSASSA-PSS-signatur der bytene er korrekte?

Fordi RSASSA-PSS er det ene RSA-skjemaet der validatoren ikke kan gjenopprette parametrene fra signaturen selv. PKCS#1 v1.5-padding er fullt bestemt av OID-en sha256WithRSAEncryption, så parametrene er en bar NULL, og det finnes ingenting å ta feil av. PSS er parametrisert av en hashfunksjon, en maskegenereringsfunksjon med sin egen hash, og en saltlengde, og RFC 8017 §A.2.3 lar alle tre stå åpne. Signereren velger dem, AlgorithmIdentifier-en bærer dem, og validatoren må reprodusere dem nøyaktig før EMSA-PSS-VERIFY i det hele tatt kan begynne

Så når PDFium Component signerer med SHA-256, MGF1 over SHA-256 og et salt på 32 byte, må de tre fakta overleve DER-koding her og DER-dekoding i en annen implementasjon. En parameterblokk validatoren ikke klarer å parse, avslutter verifiseringen før noen modulær eksponentiering skjer. En blokk den parser annerledes, er verre, fordi RFC 4055 §3.1 gir saltLength en standardverdi på 20. En dekoder som hopper over et felt den ikke kjenner igjen, lander på den standardverdien, kjører EMSA-PSS-VERIFY med et salt på 20 byte mot en signatur beregnet med 32, og rapporterer en dårlig signatur uten antydning om at problemet er metadata og ikke nøkkelen. Begge utfallene er det 3.114.19-kodingen produserte, avhengig av hvor streng validatoren var, og ingen av dem peker på AlgorithmIdentifier-en

Hva RFC 4055 §3.1 faktisk krever av RSASSA-PSS-params

RFC 4055 §3.1 definerer RSASSA-PSS-params som en SEQUENCE av fire felt, hvert med en eksplisitt kontekstspesifikk tagg og en DEFAULT-verdi:

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

Eksplisitt tagging i DER betyr at hvert felt pakkes inn i en konstruert kontekstspesifikk TLV, A0 for [0], A1 for [1], A2 for [2] og A3 for [3], med den universelle kodingen av verdien nestet inni. Hvert felt er tagget nettopp fordi hvert felt er valgfritt gjennom standardverdien sin. Uten tagger kunne ikke en dekoder se om en SEQUENCE som holder én enkelt AlgorithmIdentifier, bærer hashAlgorithm eller maskGenAlgorithm, siden begge er SEQUENCE-typer; med dem identifiserer taggnummeret feltet uansett hvilke naboer som finnes. Verdiene PDFium Component sender ut, følger profilen i ETSI TS 119 312 §7, SHA-256, MGF1 med SHA-256 og et salt lik digestlengden, og de speiler nøyaktig det hvert plattformssignaturkall får beskjed om: en BCRYPT_PSS_PADDING_INFO med cbSalt 32 for NCryptSignHash, en CK_RSA_PKCS_PSS_PARAMS med sLen 32 for PKCS#11-mekanismen, og PSS-algoritmen for SHA-256-digestsignering i Security-rammeverket

Diagram fra PDFium Component over RSASSA-PSS-params per RFC 4055: hashAlgorithm A0, maskGenAlgorithm A1 og saltLength A2 bærer eksplisitte konteksttagger med DEFAULT-verdier, ETSI-profilen sender ut SHA-256, MGF1 med SHA-256 og salt 32, og trailerField A3 er lik trailerFieldBC, så DER utelater den helt
Hvert felt er tagget nettopp fordi hvert felt er valgfritt gjennom standardverdien sin, så taggnummeret identifiserer feltet uansett hvilke naboer kodingen utelater

Hvordan tre backender gjorde den samme feilen

3.114.19-kodingen tagget de to første feltene og lot de to siste stå bare, identisk i TWinCmsSigner, TKeychainCmsSigner og TPkcs11CmsSigner. Den symmetrien er ikke tilfeldig: alle tre implementerer ICmsSigner-grensesnittet fra FPdfCms.pas, og kroppene deres i GetSignatureAlgorithmParams ble skrevet etter én mal. Malen så slik ut:

// Før 3.114.20: [0] og [1] tagget, [2] og [3] ikke
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),      // bart INTEGER der [2] EXPLICIT var påkrevd
  W.IntegerOf(1)));     // trailerField, lik DEFAULT, må være fraværende

En dekoder som går gjennom den SEQUENCE-en, ser A0, leser hashfunksjonen, ser A1, leser maskegenereringsfunksjonen, og møter deretter 02 01 20. Det er et universelt INTEGER, og RSASSA-PSS-params har ikke noe utagget INTEGER-medlem noe sted. En streng dekoder stopper der. En ettergivende hopper over det ukjente elementet, finner aldri noen A2, tildeler saltLength standardverdien sin på 20, treffer så et andre løsrevet INTEGER, 02 01 01, og har det samme problemet igjen. Ingen av veiene ender på et salt på 32 byte. En delt mal er effektiv når den er riktig, og en like effektiv måte å ta feil tre ganger når den ikke er det, og derfor landet rettelsen i alle tre enhetene i én commit, og derfor forblir de tre metodekroppene strukturelt identiske etterpå. En framtidig backend bør kopiere blokken fra én av disse i stedet for å utlede den på nytt, fordi det er nettopp i utledningen feilen ble gjort

Diagram fra PDFium Component over DER-feilen i 3.114.19: etter A0 og A1 møtte parametrene en bar 02 01 20 der den eksplisitte A2-taggen hører hjemme, en streng dekoder stoppet og en ettergivende verifiserte med standardsaltet på 20 byte, mens 3.114.20 pakker saltet på 32 byte inn i A2
RSASSA-PSS-params har ikke noe utagget INTEGER-medlem, så de løsrevne bytene var enten en parsefeil eller et stille fall tilbake til standard saltlengde, og ingen av veiene nådde signerernes 32

Hvorfor utelates trailerField i stedet for å tagges som [3]?

Fordi X.690 §11.5 sier at en DER-innkoder ikke skal kode en komponent hvis verdi er lik DEFAULT-verdien, og trailerField har DEFAULT trailerFieldBC, som er heltallet 1. Den opplagte rettelsen av den gamle koden, å bytte ut den bare W.IntegerOf(1) med W.ContextSpecific(3, W.IntegerOf(1), True), gir en blokk en ettergivende BER-dekoder godtar og en streng DER-dekoder har rett til å avvise. Verdien er ikke gal. Det er tilstedeværelsen som er det. Den samme regelen er grunnen til at de tre andre feltene er til stede: SHA-256 er ikke standardverdien sha1, MGF1 med SHA-256 er ikke standardverdien mgf1SHA1, og 32 er ikke standardverdien 20. Hadde backendene signert med SHA-1 og et salt på 20 byte, ville RFC 4055 §3.1 kollapse parametrene til en tom SEQUENCE, 30 00, og den tomme SEQUENCE-en, ikke NULL, er det en validator forventer. PDFium Component sender aldri ut den formen, fordi den aldri signerer med de verdiene, men det er tilfellet som fanger enhver som antar at «ingen parametre» alltid staves 05 00

Dette er forskjellen mellom DER og BER som betyr noe spesielt for signaturer. BER lar en innkoder ta med en komponent med standardverdi; DER forbyr det, fordi DER finnes for at én verdi skal ha nøyaktig én koding, og en signatur over en struktur med to lovlige kodinger er en signatur det kan krangles om. Alt inne i CMS signedAttrs er DER av den grunnen, og parameterblokken reiser inne i signedAttrs gjennom cmsAlgorithmProtection-attributtet i tillegg til i den ytre signatureAlgorithm, så den får ingen fritak

Diagram fra PDFium Component over X.690 11.5-regelen i PSS-rettelsen: trailerField lik sin DEFAULT trailerFieldBC må forbli fraværende, fordi en tagget A3-innpakning er kodingen streng DER avviser, mens saltLength 32 avviker fra standardverdien 20 og må være til stede som A2
DER finnes for at én verdi skal ha nøyaktig én koding, og en komponent lik standardverdien sin har allerede den korteste kodingen som finnes, nemlig å ikke være der i det hele tatt

Den rettede TDerWriter-kodingen

PDFium Component bygger nå parametrene med tre kall til TDerWriter.ContextSpecific fra FPdfAsn1.pas, ett per felt som ikke har standardverdi, hvert med Constructed-argumentet satt til True for å produsere innpakningen med den eksplisitte taggen, og ingen linje i det hele tatt for trailer-feltet. Dette er kroppen til TWinCmsSigner.GetSignatureAlgorithmParams med OID-ene skrevet ut; Keychain- og PKCS#11-enhetene staver de samme verdiene som OID_SHA256, OID_MGF1 og OID_RSASSA_PSS:

function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
  W: TDerWriter;
begin
  if FPaddingScheme = psRsaPss then
  begin
    W := TDerWriter.Create;
    try
      // RFC 4055 3.1 tagger alle fire felt. saltLength er [2]; et bart
      // INTEGER her leses som starten på et annet felt. trailerField
      // er [3] med DEFAULT 1, og X.690 11.5 forbyr å kode en verdi
      // lik standardverdien, så den utelates helt
      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 og ECDSA: AlgIdWithParams skriver NULL
end;

To detaljer i maskineriet rundt betyr noe. TDerWriter.AlgId produserer en AlgorithmIdentifier med NULL-parametre, som er det RFC 4055 §2.1 sier innkodere skal generere for den nestede hashAlgorithm og for MGF1s indre hash. Og CMS-byggeren i FPdfCms.pas parrer signatur-OID-en med disse bytene gjennom TDerWriter.AlgIdWithParams, som setter inn NULL når parametrene er nil; det er derfor psRsaPkcs1v15 og psEcdsa rett og slett returnerer nil og aldri var påvirket, og derfor 1.2.840.113549.1.1.10, id-RSASSA-PSS, er den eneste av de tre signatur-OID-ene som bærer en ekte parameterblokk. Bytene som blir resultatet for SHA-256-profilen, er faste og korte nok til å kontrolleres med øyet: en ytre 30 34-SEQUENCE som holder A0 0F rundt SHA-256-AlgorithmIdentifier-en på 15 byte, A1 1C rundt MGF1-AlgorithmIdentifier-en på 28 byte, hvis egne parametre er den samme SHA-256-AlgorithmIdentifier-en, og A2 03 02 01 20 for saltet. Viser en dump av signatureAlgorithm din 02 01 20 på toppnivået i params-SEQUENCE-en i stedet for inne i en A2, ser du på 3.114.19-kodingen

Hvorfor fanget ikke testsettet en feilformet AlgorithmIdentifier?

Fordi PAdES-testene driver CMS-byggeren gjennom en falsk signerer som rapporterer sha256WithRSAEncryption og returnerer nil fra GetSignatureAlgorithmParams, så PSS-parameterblokken ble aldri bygget i noen test i det hele tatt. Det er et rimelig design for tester som må kjøre uten et sertifikatlager, en Keychain eller et token, og det har en blindson med en presis form: alt bare en ekte backend produserer, utøves bare av en ekte backend. Det andre laget er mer interessant. PDFium Component plasserer også signatur-AlgorithmIdentifier-en, med parametre, inne i cmsAlgorithmProtection-signaturattributtet fra RFC 6211, og en validator sammenligner den kopien mot den ytre signatureAlgorithm. Begge kopiene kom fra det samme kallet, så de matchet perfekt og hver intern konsistenssjekk gikk gjennom. Kodingen var selvkonsistent og gal, den kategorien feil som ingen mengde sammenligning av en struktur mot seg selv kan avsløre, og den samme lærdommen med en annen struktur fortelles i CMS signedAttrs og DER SET OF-sortering, der et SET hashet i én rekkefølge og sendt ut i en annen så fint ut helt til en fremmed validator regnet hashen på nytt

Det som faktisk fanger denne klassen feil, er en dekoder som ikke er skrevet av innkoderens opphavsperson, kjørt mot de faktiske utdataene fra den faktiske backend-en. Verifiseringsveien på Windows i PDFium Component går gjennom CryptoAPI og ikke gjennom bibliotekets egen leser, og en avvist PSS-signatur der er det som ledet tilbake til parametrene. Enhver ASN.1 en implementasjon sender ut for at andre implementasjoner skal lese den, fortjener minst én tur-retur gjennom en dekoder den ikke kontrollerer, og jo flere standardverdier og tagger strukturen har, jo mer er den turen verdt

Hvor dette passer inn i resten av PSS-historien

Denne rettelsen er uavhengig av de to andre stedene PSS kan gå galt i en PAdES-signatur, og å holde dem fra hverandre forkorter feilsøkingen. macOS-backenden kan oppdage at en bestemt nøkkel eller et eldre system avviser PSS, og nedgradere til PKCS#1 v1.5, og da må AlgorithmIdentifier-en følge nedgraderingen; det er et kapabilitetsspørsmål, dekket i å signere PAdES med en macOS Keychain-identitet. PKCS#11-backenden kan gi tokenet en CK_RSA_PKCS_PSS_PARAMS hvis layout tokenet leser annerledes på grunn av en mismatch i heltallsbredde; det er et ABI-spørsmål, dekket i CK_ULONG og PKCS#11-pakkefellen. Denne artikkelen handler om den tredje feilen, der nøkkelen var villig, tokenet regnet ut de riktige bytene, og DER-en som beskriver resultatet, ikke samsvarte med RFC 4055 §3.1

En signerer som erklærer PSS, påtar seg en forpliktelse v1.5-signereren aldri hadde: å beskrive sine egne parametre i en form en annen implementasjon dekoder til de samme tre verdiene. RFC 4055 §3.1 fastsetter taggene, X.690 §11.5 fastsetter hvilke felt som får forekomme, og ETSI TS 119 312 §7 fastsetter verdiene det er verdt å velge. Alle tre backendene i PDFium Delphi-komponenten leveres som kildekode, så kroppen i GetSignatureAlgorithmParams over er den du kan lese, dumpe og sammenligne mot din egen validator i stedet for å ta på tillit