PDFium Component bygger CMS-signedAttrs-blokken gennem TDerWriter.SetOfSorted, fordi X.690-klausul 11.6 kræver at medlemmerne af en SET OF udsendes i stigende rækkefølge af deres egne DER-encodings. Samler du dem i stedet i konstruktionsrækkefølge, får du signaturer din egen verifier accepterer, og enhver tredjeparts-PAdES-validator afviser
Den sidste sætning er hele grunden til at denne defekt overlevede en fuld regression-suite. Signerings-stien var dækket, verifikations-stien var dækket, round trip'en var grøn på alle platforme, og outputtet var stadig ikke valid DER. En test der signerer med din writer og verificerer med din reader kan ikke opdage en afvigelse fra kodningsreglerne, fordi begge halvdele deler afvigelsen
Hvad X.690-klausul 11.6 faktisk kræver
Medlemmerne af en SET OF-værdi skal sorteres, og sorteringsnøglen er hele DER-encoding'en af hvert medlem, sammenlignet som en usigneret oktet-streng, hvor den kortere encoding kommer først når den ene er et præfiks for den anden. Det er en kanonisk restriktion snarere end et forslag: DER tillader præcis én encoding af en given værdi, så en usorteret SET OF er ikke en anderledes ordnet encoding af samme værdi — den er slet ikke en DER-encoding. Fordi ASN.1-tags kommer først i en TLV, afgøres sammenligningen normalt inden for de første to oktetter, og da hvert medlem i en CMS-attributliste starter med 30, SEQUENCE-tagget, falder afgørelsen på længde-oktetten. Den krævede rækkefølge ligner derfor næsten en sortering af attributterne efter størrelse, hvilket er værd at vide når du stirrer på en hex dump og undrer dig over at rækkefølgen ser vilkårlig ud
Hvorfor er det mindste attributsæt allerede ude af orden?
Fordi den naturlige konstruktionsrækkefølge og den kodede rækkefølge er uenige fra den allerførste signatur du producerer, uden at der er valgfrie attributter involveret. BuildSignedAttrs i FPdfCms.pas tilføjer content-type, så message-digest, så signing-certificate-v2, så cms-algorithm-protection. Med SHA-256-digests og en RSA-signaturalgoritme begynder de fire attribut-TLV'er med disse bytes
// Konstruktionsrækkefølge produceret af 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-rækkefølge krævet af X.690 11.6
// 30 18 content-type
// 30 2D cms-algorithm-protection
// 30 2F message-digest
// 30 37 signing-certificate-v2
To af de fire attributter flytter sig. cms-algorithm-protection ved 30 2D skal gå foran signing-certificate-v2 ved 30 37, og den skal også gå foran message-digest ved 30 2F. Så selv det mindste konforme PAdES B-B-attributsæt PDFium Component kan udsende, var allerede fejlsorteret under den gamle kode — før nogen overhovedet tilføjer signing-time, commitment-type-indication eller en signature-policy-identifier. Der fandtes aldrig en konfiguration hvor fejlen ikke ramte
Hvorfor består vores egne tests, mens enhver ekstern validator fejler?
Fordi de to verifier-familier beregner message-digest'en over forskellige bytes. RFC 5652-klausul 5.4 siger at input til signerings-operationen er DER-encoding'en af SignedAttributes-værdien, med det IMPLICIT [0]-tag erstattet af SET-tagget 31, og sætningen der afgør alting er the DER encoding, ikke the octets you received. Windows-platformens verifier, der nås gennem CryptVerifyDetachedMessageSignature, tager de modtagne oktetter, bytter det ledende tag og hasher dem som de ankom, så rækkefølge kommer aldrig ind i billedet, og en fejlsorteret SET OF digester præcis til det signeren beregnede. OpenSSL og BouncyCastle — det de udbredte europæiske validatorer er bygget på — parser attributsættet til strukturer og re-encoder før digesting. Re-encoding anvender sorteringen, digeste'et matcher ikke længere det der blev signeret, og signaturen rapporteres som broken snarere end som fejlkodet, hvilket sender dig på jagt gennem nøglemateriale og certifikat-kæder efter en defekt der bor i en sammenligningsfunktion
Generaliser ud over denne ene fejl, for det er den dyre del. En same-origin round trip er immun over for enhver afvigelse fra en interoperabilitetsspecifikation som begge halvdele af din kode deler; den beviser at din writer og din reader er enige med hinanden, hvilket ikke er den egenskab nogen bekymrer sig om. For alt der forlader processen som et wire-format, er den eneste meningsfulde regression én der piner bytes fast mod specifikationen eller kører en fremmed implementering over dit output. OpenSSL CMS-verifikations-backend'et fortjener sin plads her af en anden grund end portabilitet: det er en ægte uafhængig læser af din egen DER
Rettelsen, og hvor den allerede eksisterede
DER-writer'en havde den sorterede variant hele tiden. TDerWriter i FPdfAsn1.pas eksponerer både SetOf, som pakker en præ-konkatenreret content-buffer ind i et SET-tag og bevarer den rækkefølge den fik overleveret, og SetOfSorted, som tager medlemmerne enkeltvis som en TDerByteArrays og merge-sorterer dem efter deres encodings før indpakningen. PDF-MAC-koden i FPdfMac.pas byggede allerede sit authenticated attribute-sæt med SetOfSorted; det var kun CMS-signeren der ikke gjorde. At holde medlemmerne som et array helt frem til indpaknings-kaldet er den strukturelle halvdel af rettelsen, for når attributterne først er konkatreneret til én buffer, er ordnings-informationen væk, og ingen downstream-omhu kan genskabe den. Et writer-API der kun tilbyder SetOf(Content: TBytes) til SET OF, er et API der inviterer til denne fejl; overload'et der tager et array, er det der gør den rigtige ting udtrykkelig
// Før: medlemmerne beholder den rækkefølge AddAttr tilføjede dem
Result:= W.SetOf(ConcatMany(AttrList));
// Efter: X.690 11.6-ordningen anvendt ved selve kodningen
Result:= W.SetOfSorted(AttrList);
SET OF sorteres, SEQUENCE OF gør ikke
Det er den skelnen der oftest forvirres, og at få den omvendt er lige så skadeligt som at ignorere den. En SEQUENCE OF er en ordnet samling: dens rækkefølge er en del af værdien, og du må aldrig sortere den. En SET OF er uordnet som værdi, så DER pålægger en kanonisk rækkefølge for at gøre encoding'en entydig. At sortere en SEQUENCE OF korrumperer data; at lade en SET OF stå usorteret producerer noget der ikke er DER
- SET OF, skal sorteres:
SignedAttributesogUnsignedAttributesiSignerInfo,digestAlgorithms-sættet,certificates- ogcrls-sættene iSignedData,signerInfos-sættet, værdisættet inde i hver enkeltAttributeog attributsættet inde i hverRelativeDistinguishedNamei et X.501-navn - SEQUENCE OF, aldrig sorteret: RDN-listen der udgør et
Name,certs-feltet iSigningCertificateV2, extension-lister og general-name-listerne inde i dem
En RDN er en SET OF der sidder inde i en SEQUENCE OF ét niveau oppe, så et distinguished name indeholder begge regler inden for to niveauer af indlejring. Det layout forklarer også hvorfor sæt med ét enkelt medlem er så tilgivende: et sæt med ét element er trivielt sorteret, hvilket er hvorfor attributværdi-sættene bygget med W.SetOf inde i BuildSignedAttrs var korrekte som skrevet og ikke krævede nogen ændring. Fejlen bor kun der hvor et sæt reelt holder to eller flere medlemmer, og multi-værdi-RDN'er er sjældne nok til at de fleste name-encodere slipper af sted med samme fejl i årevis
Hvordan forhindrer du at det kommer tilbage?
Assert på bytes, ikke på domme. Den billigste holdbare check er en self-audit der gennemgår sit eget output: parse det udsendte signedAttrs, sammenlign hvert medlem med det foregående med den samme usignerede-oktet-streng-sammenligning writer'en bruger, og lad build'et fejle hvis et par er ude af orden. Den kører på mikrosekunder, kræver intet eksternt bibliotek, og i modsætning til en verifikations-round-trip tester den den egenskab specifikationen faktisk beskriver
// Gennemgå medlemmerne af en SET OF og bekræft X.690 11.6-ordningen.
// Members er listen der gives til SetOfSorted, eller resultatet af
// at re-parse en udsendt 31 xx / A0 xx-blok
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 signing-testen skal du ikke stoppe ved "vores verifier sagde valid".
// Pin de bytes signeren hashede
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');
To begrænsninger er værd at nævne lige ud. At sortere signedAttrs gør ikke i sig selv en signatur interoperabel, for alt andet i containeren skal også holde, og grundene til at validatorer afviser ellers sunde PAdES-signaturer er varierede nok til at fylde deres egen diskussion. Og en sorteret SET OF er en egenskab ved det du skriver, ikke ved det du læser: PDFium Component skal stadig acceptere en indkommende signatur hvis signedAttrs ankommer usorteret, for sådanne filer findes derude i virkeligheden, og RFC 5652-klausul 5.4 fortæller en verifier at re-encode frem for at afvise. Streng på output, tolerant på input, som sædvanlig. DER-writer'en, CMS-byggeren og signerings-stien beskrevet i PAdES B-B-gennemgangen leveres alle som læsbar Pascal-kilde med PDFium Delphi component, hvilket er hele pointen med at levere kildekode til et signeringsbibliotek: når en validator et sted i Europa er uenig med dit dokument, kan du læse de nøjagtige bytes din kode udsendte og den nøjagtige klausul de krænker, i stedet for at oprette en ticket og vente