PDFium Component bygger CMS-signedAttrs-blocket genom TDerWriter.SetOfSorted, eftersom X.690-klausul 11.6 kräver att medlemmarna i en SET OF skrivs ut i stigande ordning av sina egna DER-kodningar. Sätter du ihop dem i konstruktionsordning i stället får du signaturer som din egen verifierare accepterar och som varje tredjeparts-PAdES-validerare avvisar
Den där sista meningen är hela anledningen till att defekten överlevde en komplett regressionssvit. Signeringsvägen var täckt, verifieringsvägen var täckt, rundturen var grön på varje plattform, och ändå var utdata inte giltig DER. Ett test som signerar med din writer och verifierar med din reader kan inte upptäcka en avvikelse från kodningsreglerna, eftersom båda halvorna delar avvikelsen
Vad X.690-klausul 11.6 faktiskt kräver
Medlemmarna i ett SET OF-värde måste sorteras, och sorteringsnyckeln är varje medlems kompletta DER-kodning, jämförd som en osignerad oktettsträng, med den kortare kodningen först när den ena är ett prefix av den andra. Det här är en kanonisk begränsning snarare än en rekommendation: DER tillåter exakt en kodning av ett givet värde, så ett osorterat SET OF är inte en annorlunda ordnad kodning av samma värde, det är inte en DER-kodning alls. Eftersom ASN.1-taggar kommer först i en TLV avgörs jämförelsen oftast inom de två första oktetterna, och eftersom varje medlem i en CMS-attributlista börjar med 30, SEQUENCE-taggen, avgörs oavgjorda fall av längdoktetten. Den krävda ordningen ser därför nästan ut som att sortera attributen efter storlek, vilket är värt att veta när du sitter och stirrar på en hexdump och undrar varför ordningen verkar godtycklig
Varför är den minimala attributuppsättningen redan i fel ordning?
För att den naturliga konstruktionsordningen och den kodade ordningen skiljer sig åt redan från den första signaturen du producerar, utan att några valfria attribut är inblandade. BuildSignedAttrs i FPdfCms.pas lägger till content-type, sedan message-digest, sedan signing-certificate-v2, sedan cms-algorithm-protection. Med SHA-256-digester och en RSA-signaturalgoritm börjar de fyra attribut-TLV:erna med de här byten
// Konstruktionsordning som produceras av BuildSignedAttrs
// content-type 30 18 (24 content octets)
// message-digest 30 2F (47)
// signing-certificate-v2 30 37 (55)
// cms-algorithm-protection 30 2D (45)
//
// DER-ordning krävs enligt X.690 11.6
// 30 18 content-type
// 30 2D cms-algorithm-protection
// 30 2F message-digest
// 30 37 signing-certificate-v2
Två av de fyra attributen flyttas. cms-algorithm-protection vid 30 2D måste komma före signing-certificate-v2 vid 30 37, och det måste också komma före message-digest vid 30 2F. Så den minsta konforma PAdES B-B-attributuppsättningen som PDFium Component kan producera var redan felordnad under den gamla koden, innan någon hinner lägga till signing-time, commitment-type-indication eller en signature-policy-identifier. Det fanns aldrig någon konfiguration där buggen inte slog till
Varför passerar våra egna tester medan alla externa validerare misslyckas?
För att de två verifierarfamiljerna beräknar meddelandedigesten över olika bytes. RFC 5652-klausul 5.4 säger att indata till signeringsoperationen är DER-kodningen av SignedAttributes-värdet, med den IMPLICITA [0]-taggen utbytt mot SET-taggen 31, och frasen som avgör allt är DER-kodningen, inte oktetterna du fick. Windows-verifieraren som nås via CryptVerifyDetachedMessageSignature tar emot oktetterna, byter ut den ledande taggen och hashar dem som de kom, så ordningen spelar aldrig in och ett felordnat SET OF digestar till exakt det signeraren beräknade. OpenSSL och BouncyCastle, som de brett utbredda europeiska validerarna bygger på, parsar attributuppsättningen till strukturer och kodar om innan de digestar. Omkodningen tillämpar sorteringen, digesten matchar inte längre den som signerades, och signaturen rapporteras som trasig i stället för felkodad, vilket skickar dig på jakt genom nyckelmaterial och certifikatkedjor efter en defekt som bor i en jämförelsefunktion
Generalisera bortom just den här buggen, för det är den dyra delen. En rundtur med samma ursprung är immun mot varje avvikelse från en interoperabilitetsspecifikation som båda halvorna av din kod delar; den bevisar att din writer och din reader håller med varandra, vilket inte är egenskapen någon bryr sig om. För allt som lämnar processen som ett wire-format är den enda meningsfulla regressionen en som spikar bytes mot specifikationen eller kör en främmande implementation över din utdata. OpenSSL CMS-verifieringsbackend förtjänar sin plats här av en andra anledning utöver portabilitet: den är en genuint oberoende läsare av din egen DER
Fixen, och var den redan fanns
DER-writern hade den sorterade varianten hela tiden. TDerWriter i FPdfAsn1.pas exponerar både SetOf, som slår en SET-tagg runt en på förhand sammanfogad innehållsbuffert och bevarar vilken ordning den än fick, och SetOfSorted, som tar medlemmarna individuellt som en TDerByteArrays och merge-sorterar dem efter sina kodningar innan den slår runt dem. PDF MAC-koden i FPdfMac.pas byggde redan sin autentiserade attributuppsättning med SetOfSorted; bara CMS-signeraren gjorde inte det. Att behålla medlemmarna som en array ända fram till wrappningsanropet är den strukturella halvan av fixen, för när attributen väl har sammanfogats till en enda buffert är ordningsinformationen borta och ingen omsorg nedströms kan återställa den. Ett writer-API som bara erbjuder SetOf(Content: TBytes) för SET OF är ett API som inbjuder till den här buggen; overloaden som tar en array är det som gör det rätta uttryckbart
// Före: medlemmarna behåller den ordning AddAttr lade till dem
Result:= W.SetOf(ConcatMany(AttrList));
// Efter: X.690 11.6-ordning tillämpas vid kodningspunkten
Result:= W.SetOfSorted(AttrList);
SET OF sorteras, SEQUENCE OF gör det inte
Det här är skillnaden som oftast blandas ihop, och att få den omvänt är lika skadligt som att ignorera den. En SEQUENCE OF är en ordnad samling: dess ordning är en del av värdet och du får aldrig sortera den. Ett SET OF är oordnat som värde, så DER påtvingar en kanonisk ordning för att göra kodningen unik. Att sortera en SEQUENCE OF korrumperar data; att lämna ett SET OF osorterat producerar något som inte är DER
- SET OF, måste sorteras:
SignedAttributesochUnsignedAttributesiSignerInfo,digestAlgorithms-uppsättningen,certificates- ochcrls-uppsättningarna iSignedData,signerInfos-uppsättningen, värdeuppsättningen inuti varje enskiltAttribute, och attributuppsättningen inuti varjeRelativeDistinguishedNamei ett X.501-Name - SEQUENCE OF, sorteras aldrig: RDN-listan som bygger upp ett
Name,certs-fältet iSigningCertificateV2, extensionslistor, och general-name-listor inuti dem
En RDN är ett SET OF som sitter inuti en SEQUENCE OF en nivå upp, så ett distinguished name innehåller båda reglerna inom två nivåer av nästling. Den layouten förklarar också varför enkelmedlemsuppsättningar är så förlåtande: en uppsättning med ett element är trivialt sorterad, vilket är varför attributvärdesuppsättningarna som byggs med W.SetOf inuti BuildSignedAttrs var korrekta som skrivna och inte behövde ändras. Buggen lever bara där en uppsättning verkligen håller två eller fler medlemmar, och flervärdiga RDN:er är sällsynta nog att de flesta namnkodare kommer undan med samma misstag i åratal
Hur förhindrar du att det här kommer tillbaka?
Asserta på bytes, inte på utslag. Den billigaste hållbara kontrollen är en självgranskning som går igenom sin egen utdata: parsea det emitterade signedAttrs, jämför varje medlem mot den föregående med samma osignerade-oktettsträng-jämförelse som writern använder, och faila bygget om något par är i fel ordning. Den körs på mikrosekunder, kräver inget externt bibliotek, och till skillnad från en verifieringsrundtur testar den egenskapen som specifikationen faktiskt anger
// Gå igenom medlemmarna i ett SET OF och bekräfta X.690 11.6-ordningen.
// Members är listan som gavs till SetOfSorted, eller resultatet av
// att omparsa ett emitterat 31 xx / A0 xx-block
function SetOfIsSorted(const Members: TDerByteArrays): Boolean;
var
I: Integer;
begin
for I:= 1 to Length(Members)- 1 do
if CompareDerBytes(Members[I- 1], Members[I])> 0 then
Exit(False);
Result:= True;
end;
// I signeringstestet, stanna inte vid "vår verifierare sa giltig".
// Spika bytes som signeraren hashade
Attrs:= BuildSignedAttrs(W, Digest, CertHash, DigestOid, SigOid, nil, Options);
Assert(Attrs[0]= $31, 'signedAttrs must carry the SET tag when hashed');
Assert(SetOfIsSorted(SplitSetOfMembers(Attrs)), 'signedAttrs not in DER order');
Två begränsningar är värda att säga rakt ut. Att sortera signedAttrs gör inte en signatur interoperabel i sig själv, eftersom allt annat i containern också måste hålla och anledningarna till att validerare avvisar annars sund PAdES-signaturer är varierade nog att fylla en egen diskussion. Och ett sorterat SET OF är en egenskap hos det du skriver, inte hos det du läser: PDFium Component måste fortfarande acceptera en inkommande signatur vars signedAttrs anländer osorterade, för sådana filer finns i det vilda och RFC 5652-klausul 5.4 säger åt en verifierare att koda om i stället för att avvisa. Strikt på utdata, tolerant på indata, som vanligt. DER-writern, CMS-byggaren och signeringsvägen som beskrivs i PAdES B-B-genomgången levereras alla som läsbar Pascal-källkod med PDFium Delphi-komponenten, vilket är hela poängen med att leverera källkod för ett signeringsbibliotek: när en validerare någonstans i Europa inte håller med om ditt dokument kan du läsa exakt de bytes din kod emitterade och exakt vilken klausul de bryter mot, i stället för att skicka in en ticket och vänta