Teknisk artikel

Kodning af RSASSA-PSS-params efter RFC 4055 i PDFium Delphi

PDFium Component version 3.114.20 retter RSASSA-PSS-params-kodningen i alle tre PAdES-signeringsbackends: Windows CNG, macOS Keychain og PKCS#11. RFC 4055 §3.1 giver hvert felt af RSASSA-PSS-params en eksplicit kontekstspecifik tag, [0] til [3], og backendsne emitterede saltLength som en bar universel INTEGER, samtidig med at de skrev en trailerField ud, der var lig dens default. Signatur-bytesne var korrekte hele tiden. Den AlgorithmIdentifier, der beskriver dem, var det ikke, og det alene er nok til, at en verifikator afviser signaturen

Det frustrerende er, hvor bugen gemmer sig. En CMS-signatur har to halvdele: Den kryptografiske operation og den ASN.1, der fortæller verifikatoren, hvordan operationen blev udført. Få den første rigtig og den anden forkert, og resultatet er et dokument, intet specifikationstro værktøj kan skelne fra en forfalskning. Denne artikel handler kun om den anden halvdel: Hvordan RSASSA-PSS-params skal tags, hvordan tre backends fik det forkert på samme måde, og hvordan den korrigerede DER ser ud i TDerWriter-termer

Hvorfor afviser en verifikator en RSASSA-PSS-signatur, hvis bytes er korrekte?

For RSASSA-PSS er det ene RSA-skema, hvor verifikatoren ikke kan gendanne parametrene fra signaturen selv. PKCS#1 v1.5-padding er fuldt bestemt af OID'en sha256WithRSAEncryption, så dens parametre er en bar NULL, og der er intet at ramme forkert. PSS er parametriseret af en hashfunktion, en mask generation-funktion med sin egen hash og en saltlængde, og RFC 8017 §A.2.3 lader alle tre stå åbne. Signeren vælger dem, AlgorithmIdentifier'en bærer dem, og verifikatoren skal reproducere dem eksakt, før EMSA-PSS-VERIFY overhovedet kan begynde

Så når PDFium Component signerer med SHA-256, MGF1 over SHA-256 og en 32-byte-salt, skal de tre fakta overleve DER-kodning her og DER-afkodning på en anden implementering. En parametersblok, verifikatoren ikke kan parse, afslutter verifikationen, før nogen modulareksponentiation sker. En blok, den parser anderledes, er værre, for RFC 4055 §3.1 giver saltLength en default på 20. En dekoder, der springer et felt over, den ikke genkender, lander på den default, kører EMSA-PSS-VERIFY med en 20-byte-salt mod en signatur beregnet med 32 og rapporterer en dårlig signatur uden nogen antydning af, at problemet er metadata og ikke nøglen. Begge udfald er, hvad 3.114.19-kodningen producerede, afhængigt af hvor striks verifikatoren var, og ingen af dem peger på AlgorithmIdentifier'en

Hvad RFC 4055 §3.1 faktisk kræver af RSASSA-PSS-params

RFC 4055 §3.1 definerer RSASSA-PSS-params som en SEQUENCE med fire felter, hvert bærende en eksplicit kontekstspecifik tag og en DEFAULT-værdi:

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

Eksplicit tagging i DER betyder, at hvert felt pakkes ind i en konstrueret kontekstspecifik TLV, A0 for [0], A1 for [1], A2 for [2] og A3 for [3], med værdiens universelle kodning indlejret indeni. Hvert felt er tagget netop fordi hvert felt er valgfrit gennem sin default. Uden tags kunne en dekoder ikke afgøre, om en SEQUENCE med én enkelt AlgorithmIdentifier bar hashAlgorithm eller maskGenAlgorithm, da begge er SEQUENCE-typer; med dem identificerer tagnummeret feltet uanset hvilke naboer, der er til stede. De værdier, PDFium Component emitterer, følger profilen i ETSI TS 119 312 §7: SHA-256, MGF1 med SHA-256 og en salt lig digest-længden, og de spejler præcis, hvad hvert platform-signeringskald får at vide: En BCRYPT_PSS_PADDING_INFO med cbSalt 32 til NCryptSignHash, en CK_RSA_PKCS_PSS_PARAMS med sLen 32 til PKCS#11-mekanismen og SHA-256 digest-signerende PSS-algoritme i Security framework

PDFium Component-diagram over RSASSA-PSS-params efter RFC 4055: hashAlgorithm A0, maskGenAlgorithm A1 og saltLength A2 bærer eksplicitte context-tags med DEFAULT-værdier, ETSI-profilen emitterer SHA-256, MGF1 med SHA-256 og salt 32, og trailerField A3 er lig trailerFieldBC, så DER udelader den helt
Hvert felt er tagget netop fordi hvert felt er valgfrit gennem sin default, så tagnummeret identificerer feltet uanset hvilke naboer encoderen lader ude

Hvordan tre backends lavede den samme fejl

3.114.19-kodningen taggede de to første felter og lod de to sidste være bare, identisk i TWinCmsSigner, TKeychainCmsSigner og TPkcs11CmsSigner. Den symmetri er ingen tilfældighed: Alle tre implementerer ICmsSigner-grænsefladen fra FPdfCms.pas, og deres GetSignatureAlgorithmParams-kroppe var skrevet efter én skabelon. Skabelonen lød sådan:

// 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),      // bar INTEGER, hvor [2] EXPLICIT var krævet
  W.IntegerOf(1)));     // trailerField, lig DEFAULT, skal være fraværende

En dekoder, der gennemløber den SEQUENCE, ser A0, læser hash-algoritmen, ser A1, læser mask generation-funktionen og møder derefter 02 01 20. Det er en universel INTEGER, og RSASSA-PSS-params har intet utagget INTEGER-medlem noget sted. En striks dekoder stopper dér. En efterladende springer det ukendte element over, finder aldrig en A2, tildeler saltLength sin default på 20, støder derefter ind i en anden løs INTEGER, 02 01 01, og har samme problem igen. Ingen af vejene lander ved en 32-byte-salt. En delt skabelon er effektiv, når den er rigtig, og en lige så effektiv måde at tage fejl tre gange på, når den ikke er, hvilket er grunden til, at fixet landede i alle tre enheder i én commit, og til at de tre metodekroppe forbliver strukturelt identiske bagefter. En fremtidig backend bør kopiere blokken fra en af disse frem for at udlede den igen, for udledningen er præcis dér, hvor fejlen blev begået

PDFium Component-diagram over 3.114.19 DER-bugen: Efter A0 og A1 mødte params en bar 02 01 20, hvor A2-eksplicit-tagget hører hjemme, en striks dekoder standsede, og en efterladnende signerede med default 20-byte-salten, mens 3.114.20 pakker 32-byte-salten ind i A2
RSASSA-PSS-params har intet utagget INTEGER-medlem, så de løse bytes var enten en parse-fejl eller et lydløst fald tilbage til default-saltlængden, og ingen af vejene nåede signerens 32

Hvorfor udelades trailerField frem for at tagges som [3]?

For X.690 §11.5 siger, at en DER-encoder ikke må kode en komponent, hvis værdi er lig dens DEFAULT, og trailerField har DEFAULT trailerFieldBC, hvilket er tallet 1. Den oplagte rettelse af den gamle kode — at erstatte den bare W.IntegerOf(1) med W.ContextSpecific(3, W.IntegerOf(1), True) — giver en blok, som en efterladende BER-dekoder accepterer, og som en striks DER-dekoder har ret til at afvise. Værdien er ikke forkert. Tilstedeværelsen er det. Samme regel er grunden til, at de andre tre felter er til stede: SHA-256 er ikke default sha1, MGF1 med SHA-256 er ikke default mgf1SHA1, og 32 er ikke default 20. Havde backenden signeret med SHA-1 og en 20-byte-salt, ville RFC 4055 §3.1 kollapse parametrene til en tom SEQUENCE, 30 00, og den tomme SEQUENCE frem for NULL er det, en verifikator forventer. PDFium Component emitterer aldrig den form, for den signerer aldrig med de værdier, men det er casen, der fanger enhver, der antager, at "ingen parametre" altid staves 05 00

Dette er den forskel mellem DER og BER, der betyder noget specifikt for signaturer. BER tillader en encoder at inkludere en komponent med default-værdi; DER forbyder det, for DER findes, så én værdi har præcis én kodning, og en signatur over en struktur med to lovlige kodninger er en signatur, man kan disputere om. Alt inde i CMS signedAttrs er DER af den grund, og parametersblokken rejser inde i signedAttrs gennem cmsAlgorithmProtection-attributten så vel som i den ydre signatureAlgorithm, så den får ingen fritagelse

PDFium Component-diagram over X.690 11.5-reglen i PSS-fixet: trailerField lig dens DEFAULT trailerFieldBC skal forblive fraværende, fordi et tagget A3-wrapper er den kodning, striks DER afviser, mens saltLength 32 adskiller sig fra default 20 og skal være til stede som A2
DER findes, så én værdi har præcis én kodning, og en komponent lig dens default har allerede den korteste kodning, der findes, nemlig slet ikke at optræde

Den korrigerede TDerWriter-kodning

PDFium Component bygger nu parametrene med tre kald til TDerWriter.ContextSpecific fra FPdfAsn1.pas, ét pr. ikke-default felt, hvert med Constructed-argumentet sat til True for at producere den eksplicit-tag-wrapper, og slet ingen linje for trailer-feltet. Dette er kroppen af TWinCmsSigner.GetSignatureAlgorithmParams med OID'erne skrevet ud; Keychain- og PKCS#11-enhederne staver samme værdier 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 felter. saltLength er [2]; en bar
      // INTEGER her læses som starten på et andet felt. trailerField
      // er [3] med DEFAULT 1, og X.690 11.5 forbyder at kode en værdi
      // lig defaulten, så den udelades 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 ved maskineriet omkring tæller. TDerWriter.AlgId producerer en AlgorithmIdentifier med NULL-parametre, hvilket er det, RFC 4055 §2.1 beder encodere generere for det indlejrede hashAlgorithm og for MGF1's indre hash. Og CMS-byggeren i FPdfCms.pas parrer signatur-OID'en med disse bytes gennem TDerWriter.AlgIdWithParams, som substituerer NULL, når parametrene er nil; derfor returnerer psRsaPkcs1v15 og psEcdsa simpelthen nil og blev aldrig berørt, og derfor er 1.2.840.113549.1.1.10, id-RSASSA-PSS, den eneste af de tre signatur-OID'er, der bærer en rigtig parametersblok. De resulterende bytes for SHA-256-profilen er faste og korte nok til at tjekke med øjemål: En ydre 30 34-SEQUENCE, der holder A0 0F omkring den 15-byte SHA-256-AlgorithmIdentifier, A1 1C omkring den 28-byte MGF1-AlgorithmIdentifier, hvis egne parametre er netop den SHA-256-AlgorithmIdentifier, og A2 03 02 01 20 for salten. Viser et dump af din signatureAlgorithm 02 01 20 på toppen af params-SEQUENCE'n frem for inde i en A2, kigger du på 3.114.19-kodningen

Hvorfor fangede test-suitten ikke en misdannet AlgorithmIdentifier?

For PAdES-testene driver CMS-byggeren gennem en fake-signer, der rapporterer sha256WithRSAEncryption og returnerer nil fra GetSignatureAlgorithmParams, så PSS-parametersblokken blev aldrig konstrueret i en test overhovedet. Det er et fornuftigt design for tests, der skal køre uden et certifikatlager, en Keychain eller et token, og det har en blind plet med præcis form: Alt, hvad kun en rigtig backend producerer, øves kun af en rigtig backend. Det andet lag er mere interessant. PDFium Component placerer også signatur-AlgorithmIdentifier'en, parametre inkluderet, inde i cmsAlgorithmProtection-signed-attributten fra RFC 6211, og en verifikator sammenligner den kopi med den ydre signatureAlgorithm. Begge kopier kom fra samme kald, så de matchede perfekt, og hvert internt konsistens-tjek bestod. Kodningen var selv-konsistent og forkert, kategorien af bug, som ingen mængde sammenligning af en struktur med sig selv kan afsløre, og samme lektie med en anden struktur fortælles i CMS signedAttrs og DER SET OF-sortering, hvor et SET hashet i én orden og emitteret i en anden så fint ud, indtil en fremmed verifikator genberegnede hashen

Hvad der fanger denne klasse af bugs, er en dekoder, der ikke er skrevet af encoderens forfatter, kørt mod det faktiske output fra den faktiske backend. Windows-verifikationsvejen i PDFium Component går gennem CryptoAPI frem for bibliotekets egen reader, og en afvist PSS-signatur dér var det, der ledte tilbage til parametrene. Enhver ASN.1, en implementering emitterer for andre implementeringer at læse, fortjener mindst én round trip gennem en dekoder, den ikke selv styrer, og jo flere defaults og tags strukturen har, jo mere er den round trip værd

Hvor dette passer ind i resten af PSS-historien

Dette fix er uafhængigt af de to andre steder, PSS kan gå galt i en PAdES-signatur, og at holde dem adskilt forkorter fejlfinding. macOS-backenden kan opdage, at en bestemt nøgle eller et ældre system afviser PSS, og downgrade til PKCS#1 v1.5, og AlgorithmIdentifier'en skal følge downgraden; det er et capability-spørgsmål, dækket i signering af PAdES med en macOS Keychain-identitet. PKCS#11-backenden kan give tokenet en CK_RSA_PKCS_PSS_PARAMS, hvis layout tokenet læser anderledes på grund af et mismatch i integer-bredde; det er et ABI-spørgsmål, dækket i CK_ULONG og PKCS#11-packing-fælden. Denne artikel handler om det tredje fejltilfælde, hvor nøglen var villig, tokenet beregnede de rigtige bytes, og DER'en, der beskriver resultatet, ikke overholdt RFC 4055 §3.1

En signer, der deklarerer PSS, påtager sig en forpligtelse, v1.5-signeren aldrig havde: At beskrive sine egne parametre i en form, en anden implementering afkoder til de samme tre værdier. RFC 4055 §3.1 fastlægger tags, X.690 §11.5 fastlægger, hvilke felter må optræde, og ETSI TS 119 312 §7 fastlægger de værdier, det er værd at vælge. Alle tre backends i PDFium Delphi-komponenten ships som kildekode, så kroppen af GetSignatureAlgorithmParams ovenfor er den, du kan læse, dumpe og sammenligne med din egen verifikator frem for at tage på trust