Teknisk artikel

CMS signedAttrs sorteret som DER SET OF i PDFium Component

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

Diagram til PDFium Component-artiklen der viser hvordan X.690-klausul 11.6 sorterer CMS signedAttrs SET OF: BuildSignedAttrs tilføjer content-type, message-digest, signing-certificate-v2 og cms-algorithm-protection i konstruktionsrækkefølge, mens konform DER ordner de fire medlemmer efter deres fulde encodings, 30 18, 30 2D, 30 2F, 30 37
Konstruktionsrækkefølgen og den kodede rækkefølge er uenige fra den allerførste signatur, fordi alle fire medlemmer bærer SEQUENCE-tagget 30 og længde-oktetten afgør sammenligningen

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

PDFium Component-diagram af verifier-divergensen bag den sorterede SET OF-fejl: signeren hasher DER-encoding'en med SET-tagget 31, Windows-stien CryptVerifyDetachedMessageSignature digester de modtagne oktetter så rækkefølge aldrig betyder noget og en fejlsorteret signatur går igennem, mens OpenSSL og BouncyCastle re-encoder, anvender X.690-sorteringen og rapporterer signaturen som broken
CryptVerifyDetachedMessageSignature digester oktetterne som de ankom, så et fejlsorteret sæt verificerer, mens OpenSSL og BouncyCastle re-encoder før digesting og rapporterer signaturen som broken snarere end som fejlkodet

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

Diagram af TDerWriter-API-parret i PDFium Component: SetOf pakker en præ-konkatenreret TBytes-buffer ind og bevarer den medlemsrækkefølge den fik overleveret, hvilket producerer output der ikke er DER, mens SetOfSorted holder medlemmerne som et TDerByteArrays-array, merge-sorterer dem efter deres fulde encodings og pakker en konform SET OF ind
Når attributterne først er konkatreneret til én buffer, er ordnings-informationen væk, så overload'et SetOfSorted der tager et array, er det der gør den rigtige ting udtrykkelig på kodningstidspunktet
// 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: SignedAttributes og UnsignedAttributes i SignerInfo, digestAlgorithms-sættet, certificates- og crls-sættene i SignedData, signerInfos-sættet, værdisættet inde i hver enkelt Attribute og attributsættet inde i hver RelativeDistinguishedName i et X.501-navn
  • SEQUENCE OF, aldrig sorteret: RDN-listen der udgør et Name, certs-feltet i SigningCertificateV2, 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