PDFium Component version 3.114.20 rättar kodningen av RSASSA-PSS-params i alla tre PAdES-signeringsbackendarna: Windows CNG, macOS Keychain och PKCS#11. RFC 4055 §3.1 ger varje fält i RSASSA-PSS-params en explicit kontextspecifik tagg, [0] till [3], och backendarna gav saltLength som ett bart universellt INTEGER medan de skrev ut ett trailerField lika med sitt standardvärde. Signaturbyten var korrekta hela tiden. AlgorithmIdentifier som beskrev dem var det inte, och det räcker i sig för att en verifierare ska avvisa signaturen
Det frustrerande är var buggen gömmer sig. En CMS-signatur har två halvor: den kryptografiska operationen och den ASN.1 som talar om för verifieraren hur operationen utfördes. Får du den första rätt och den andra fel är resultatet ett dokument som inget specifikationsföljande verktyg kan skilja från en förfalskning. Den här artikeln handlar bara om den andra halvan: hur RSASSA-PSS-params måste taggas, hur tre backendar gjorde fel på samma sätt, och hur den rättade DER:en ser ut i TDerWriter-termer
Varför avvisar en verifierare en RSASSA-PSS-signatur vars byten är korrekta?
För att RSASSA-PSS är det enda RSA-schemat där verifieraren inte kan återvinna parametrarna ur signaturen själv. PKCS#1 v1.5-padding är helt bestämd av OID:n sha256WithRSAEncryption, så dess parametrar är en bart NULL och det finns inget att få fel. PSS parametriseras av en hashfunktion, en maskgenereringsfunktion med sin egen hash och en saltlängd, och RFC 8017 §A.2.3 lämnar alla tre öppna. Signeraren väljer dem, AlgorithmIdentifier bär dem, och verifieraren måste återskapa dem exakt innan EMSA-PSS-VERIFY ens kan börja
Så när PDFium Component signerar med SHA-256, MGF1 över SHA-256 och ett salt på 32 byte måste de tre fakta överleva DER-kodning här och DER-avkodning i en annan implementation. Ett parameterblock som verifieraren inte kan parsa avslutar verifieringen innan någon modulär exponentiering sker. Ett block som den parsar annorlunda är värre, för RFC 4055 §3.1 ger saltLength ett standardvärde på 20. En avkodare som hoppar över ett fält den inte känner igen landar på det standardvärdet, kör EMSA-PSS-VERIFY med ett salt på 20 byte mot en signatur beräknad med 32, och rapporterar en dålig signatur utan minsta antydan om att problemet är metadata och inte nyckeln. Båda utfallen är vad 3.114.19-kodningen producerade, beroende på hur strikt verifieraren var, och ingen av dem pekar på AlgorithmIdentifier
Vad RFC 4055 §3.1 faktiskt kräver av RSASSA-PSS-params
RFC 4055 §3.1 definierar RSASSA-PSS-params som en SEQUENCE av fyra fält, vart och ett med en explicit kontextspecifik tagg och ett DEFAULT-värde:
// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }
Explicit taggning i DER betyder att varje fält lindas in i en konstruerad kontextspecifik TLV, A0 för [0], A1 för [1], A2 för [2] och A3 för [3], med värdets universella kodning inbäddad innanför. Varje fält är taggat just för att varje fält är valfritt genom sitt standardvärde. Utan taggar kunde en avkodare inte avgöra om en SEQUENCE som håller en enda AlgorithmIdentifier bar hashAlgorithm eller maskGenAlgorithm, eftersom båda är SEQUENCE-typer; med dem identifierar taggnumret fältet oavsett vilka grannar som finns. Värdena PDFium Component ger följer profilen i ETSI TS 119 312 §7, SHA-256, MGF1 med SHA-256 och ett salt lika med digestlängden, och de speglar exakt vad varje plattforms signeringsanrop får veta: en BCRYPT_PSS_PADDING_INFO med cbSalt 32 för NCryptSignHash, en CK_RSA_PKCS_PSS_PARAMS med sLen 32 för PKCS#11-mekanismen, och PSS-algoritmen för SHA-256-digestsignering i Security-ramverket
Hur tre backendar gjorde samma misstag
Kodningen i 3.114.19 taggade de två första fälten och lämnade de två sista bara, identiskt i TWinCmsSigner, TKeychainCmsSigner och TPkcs11CmsSigner. Den symmetrin är ingen tillfällighet: alla tre implementerar gränssnittet ICmsSigner från FPdfCms.pas, och deras GetSignatureAlgorithmParams-kroppar skrevs efter en enda mall. Mallen såg ut så här:
// Före 3.114.20: [0] och [1] taggade, [2] och [3] inte
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 där [2] EXPLICIT krävdes
W.IntegerOf(1))); // trailerField, lika med DEFAULT, måste saknas
En avkodare som går genom den SEQUENCE:n ser A0, läser hashfunktionen, ser A1, läser maskgenereringsfunktionen och möter sedan 02 01 20. Det är ett universellt INTEGER, och RSASSA-PSS-params har ingen otaggad INTEGER-medlem någonstans. En strikt avkodare stannar där. En förlåtande hoppar över det okända elementet, hittar aldrig någon A2, tilldelar saltLength dess standardvärde 20, stöter sedan på ytterligare ett löst INTEGER, 02 01 01, och har samma problem igen. Ingen av vägarna når fram till ett salt på 32 byte. En delad mall är effektiv när den är rätt och ett lika effektivt sätt att ha fel tre gånger när den inte är det, vilket är varför fixen landade i alla tre enheterna i en enda incheckning och varför de tre metodkropparna förblir strukturellt identiska efteråt. En framtida backend bör kopiera blocket från en av dessa i stället för att härleda det på nytt, för härledningen är precis där misstaget gjordes
Varför utelämnas trailerField i stället för att taggas som [3]?
För att X.690 §11.5 säger att en DER-kodare inte får koda en komponent vars värde är lika med dess DEFAULT, och trailerField har DEFAULT trailerFieldBC, vilket är heltalet 1. Den uppenbara rättelsen av den gamla koden, att byta ut det bara W.IntegerOf(1) mot W.ContextSpecific(3, W.IntegerOf(1), True), ger ett block som en tillåtande BER-avkodare accepterar och en strikt DER-avkodare har rätt att avvisa. Värdet är inte fel. Dess närvaro är det. Samma regel är varför de andra tre fälten är närvarande: SHA-256 är inte standardvärdet sha1, MGF1 med SHA-256 är inte standardvärdet mgf1SHA1, och 32 är inte standardvärdet 20. Hade backendens signering använt SHA-1 och ett salt på 20 byte skulle RFC 4055 §3.1 kollapsa parametrarna till en tom SEQUENCE, 30 00, och den tomma SEQUENCE:n snarare än NULL är vad en verifierare förväntar sig. PDFium Component ger aldrig den formen eftersom den aldrig signerar med de värdena, men det är fallet som fångar den som antar att "inga parametrar" alltid stavas 05 00
Det här är skillnaden mellan DER och BER som spelar roll specifikt för signaturer. BER låter en kodare ta med en komponent med standardvärde; DER förbjuder det, eftersom DER finns för att ett värde ska ha exakt en kodning, och en signatur över en struktur med två lagliga kodningar är en signatur man kan tvista om. Allt inuti CMS signedAttrs är DER av det skälet, och parameterblocket färdas inuti signedAttrs både genom attributet cmsAlgorithmProtection och i den yttre signatureAlgorithm, så det får ingen dispens
Den rättade TDerWriter-kodningen
PDFium Component bygger nu parametrarna med tre anrop till TDerWriter.ContextSpecific från FPdfAsn1.pas, ett per icke-standardfält, vart och ett med argumentet Constructed satt till True för att ge den explicita taggomslutningen, och ingen rad alls för trailerfältet. Det här är kroppen i TWinCmsSigner.GetSignatureAlgorithmParams med OID:erna utskrivna; enheterna för Keychain och PKCS#11 stavar samma värden som OID_SHA256, OID_MGF1 och OID_RSASSA_PSS:
function TWinCmsSigner.GetSignatureAlgorithmParams: TBytes;
var
W: TDerWriter;
begin
if FPaddingScheme = psRsaPss then
begin
W := TDerWriter.Create;
try
// RFC 4055 3.1 taggar alla fyra fälten. saltLength är [2]; ett bart
// INTEGER här läses som början på ett annat fält. trailerField
// är [3] med DEFAULT 1, och X.690 11.5 förbjuder att koda ett värde
// lika med standardvärdet, så det utelämnas 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 och ECDSA: AlgIdWithParams skriver NULL
end;
Två detaljer i maskineriet runt omkring spelar roll. TDerWriter.AlgId ger en AlgorithmIdentifier med NULL-parametrar, vilket är vad RFC 4055 §2.1 säger att kodare ska generera för det nästlade hashAlgorithm och för MGF1:s inre hash. Och CMS-byggaren i FPdfCms.pas parar signatur-OID:n med dessa byten genom TDerWriter.AlgIdWithParams, som sätter in NULL när parametrarna är nil; det är varför psRsaPkcs1v15 och psEcdsa helt enkelt returnerar nil och aldrig påverkades, och varför 1.2.840.113549.1.1.10, id-RSASSA-PSS, är den enda av de tre signatur-OID:erna som bär ett riktigt parameterblock. De resulterande byten för SHA-256-profilen är fasta och korta nog att kontrollera med ögat: en yttre 30 34-SEQUENCE som håller A0 0F runt den 15 byte stora SHA-256-AlgorithmIdentifiern, A1 1C runt den 28 byte stora MGF1-AlgorithmIdentifiern vars egna parametrar är samma SHA-256-AlgorithmIdentifier, och A2 03 02 01 20 för saltet. Om en dump av din signatureAlgorithm visar 02 01 20 på toppnivån i params-SEQUENCE:n i stället för inuti en A2, tittar du på 3.114.19-kodningen
Varför fångade inte testsviten en felformad AlgorithmIdentifier?
För att PAdES-testerna driver CMS-byggaren genom en låtsassignerare som rapporterar sha256WithRSAEncryption och returnerar nil från GetSignatureAlgorithmParams, så PSS-parameterblocket konstruerades aldrig i något test alls. Det är en rimlig design för tester som måste köra utan certifikatarkiv, Keychain eller token, och den har en blind fläck med en exakt form: allt som bara en riktig backend producerar testas bara av en riktig backend. Det andra lagret är mer intressant. PDFium Component placerar också signaturens AlgorithmIdentifier, parametrar inräknade, inuti det signerade attributet cmsAlgorithmProtection från RFC 6211, och en verifierare jämför den kopian mot den yttre signatureAlgorithm. Båda kopiorna kom från samma anrop, så de matchade perfekt och varje intern konsistenskontroll gick igenom. Kodningen var självkonsistent och fel, den kategori av bugg som ingen mängd jämförelser av en struktur mot sig själv kan avslöja, och samma lärdom med en annan struktur berättas i CMS signedAttrs och DER SET OF-sortering, där en SET som hashades i en ordning och emitterades i en annan såg fint ut tills en främmande verifierare räknade om hashen
Det som fångar den här klassen av buggar är en avkodare som inte skrivits av kodarens upphovsperson, körd mot den faktiska utdatan från den faktiska backendens. Windows-verifieringsvägen i PDFium Component går genom CryptoAPI i stället för bibliotekets egen läsare, och en avvisad PSS-signatur där är vad som ledde tillbaka till parametrarna. Varje ASN.1 som en implementation ger ifrån sig för att andra implementationer ska läsa förtjänar minst en rundresa genom en avkodare den inte styr över, och ju fler standardvärden och taggar strukturen har, desto mer är den rundresan värd
Var det här passar in i resten av PSS-historien
Den här fixen är oberoende av de två andra ställen där PSS kan gå fel i en PAdES-signatur, och att hålla dem åtskilda förkortar felsökningen. macOS-backenden kan upptäcka att en viss nyckel eller ett äldre system vägrar PSS och falla ned till PKCS#1 v1.5, och AlgorithmIdentifier måste följa nedgraderingen; det är en kapacitetsfråga som täcks i att signera PAdES med en macOS Keychain-identitet. PKCS#11-backenden kan ge token en CK_RSA_PKCS_PSS_PARAMS vars layout token läser annorlunda på grund av en breddmissmatchning hos ett heltal; det är en ABI-fråga som täcks i CK_ULONG och packningsfällan i PKCS#11. Den här artikeln handlar om det tredje felet, där nyckeln var villig, token beräknade rätt byten, och DER:en som beskrev resultatet inte följde RFC 4055 §3.1
En signerare som deklarerar PSS tar på sig en skyldighet som v1.5-signeraren aldrig hade: att beskriva sina egna parametrar i en form som en annan implementation avkodar till samma tre värden. RFC 4055 §3.1 fixerar taggarna, X.690 §11.5 fixerar vilka fält som får förekomma, och ETSI TS 119 312 §7 fixerar vilka värden som är värda att välja. Alla tre backendar i PDFium Delphi-komponenten levereras som källkod, så kroppen i GetSignatureAlgorithmParams ovan är den du kan läsa, dumpa och jämföra mot din egen verifierare i stället för att lita på