Technisch artikel

CMS signedAttrs DER SET OF sorteren in PDFium Component

PDFium Component bouwt het CMS-signedAttrs-blok via TDerWriter.SetOfSorted, omdat X.690-clausule 11.6 vereist dat de leden van een SET OF worden uitgezonden in oplopende volgorde van hun eigen DER-encoderingen. Zet je ze in constructievolgorde in elkaar, dan krijg je handtekeningen die je eigen verifier accepteert en elke third-party PAdES-validator afwijst

Die laatste zin is de hele reden dat dit defect een volledige regressiesuite overleefde. Het signing-pad was gedekt, het verificatiepad was gedekt, de round trip was groen op elk platform, en de output was nog steeds geen geldige DER. Een test die met je writer ondertekent en met je reader verifieert, kan een afwijking van de encoderingsregels niet detecteren, omdat beide helften de afwijking delen

Wat X.690-clausule 11.6 werkelijk vereist

De leden van een SET OF-waarde moeten gesorteerd worden, en de sortkey is de volledige DER-encoding van elk lid, vergeleken als een unsigned octet string, waarbij de kortere encoding als eerste komt wanneer de een een prefix van de ander is. Dit is een canonieke restrictie, geen suggestie: DER kent precies één encoding toe aan een gegeven waarde, dus een ongesorteerde SET OF is geen anders geordende encoding van dezelfde waarde, het is helemaal geen DER-encoding. Omdat ASN.1-tags als eerste in een TLV komen, beslist de vergelijking zich meestal binnen de eerste twee octets, en aangezien elk lid van een CMS-attributenlijst begint met 30, de SEQUENCE-tag, wordt de gelijke stand beslecht op het lengte-octet. De vereiste volgorde lijkt daarom bijna op het sorteren van de attributen op grootte, wat het weten waard is wanneer je naar een hex dump staart en je afvraagt waarom de volgorde willekeurig oogt

Diagram voor het PDFium Component-artikel dat laat zien hoe X.690-clausule 11.6 de CMS signedAttrs SET OF sorteert: BuildSignedAttrs voegt content-type, message-digest, signing-certificate-v2 en cms-algorithm-protection toe in constructievolgorde, terwijl conforme DER de vier leden ordent op hun volledige encodings, 30 18, 30 2D, 30 2F, 30 37
De constructievolgorde en de gecodeerde volgorde verschillen al vanaf de allereerste handtekening, omdat alle vier de leden de SEQUENCE-tag 30 dragen en het lengte-octet de vergelijking beslist

Waarom is de minimale attributenset al uit volgorde?

Omdat de natuurlijke constructievolgorde en de gecodeerde volgorde al vanaf de allereerste handtekening die je produceert verschillen, zonder dat er optionele attributen aan te pas komen. BuildSignedAttrs in FPdfCms.pas voegt content-type toe, dan message-digest, dan signing-certificate-v2, dan cms-algorithm-protection. Met SHA-256-digests en een RSA-handtekeningalgoritme beginnen de vier attribuut-TLV's met deze bytes

// Constructievolgorde geproduceerd door 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-volgorde vereist door X.690 11.6
//   30 18  content-type
//   30 2D  cms-algorithm-protection
//   30 2F  message-digest
//   30 37  signing-certificate-v2

Twee van de vier attributen verplaatsen. cms-algorithm-protection op 30 2D moet vóór signing-certificate-v2 op 30 37 komen, en ook vóór message-digest op 30 2F. Dus de kleinste conforme PAdES B-B-attributenset die PDFium Component kan uitzenden, was onder de oude code al verkeerd geordend, voordat iemand signing-time, commitment-type-indication of een signature-policy-identifier toevoegt. Er was nooit een configuratie waarin de bug niet afging

Waarom slagen onze eigen tests terwijl elke externe validator faalt?

Omdat de twee families van verifiers de message digest over verschillende bytes berekenen. RFC 5652-clausule 5.4 zegt dat de input van de handtekeningoperatie de DER-encoding van de SignedAttributes-waarde is, met de IMPLICIT-[0]-tag vervangen door de SET-tag 31, en de zin die alles beslist is de DER-encoding, niet de octets die je hebt ontvangen. De Windows-platform-verifier die via CryptVerifyDetachedMessageSignature wordt bereikt, neemt de ontvangen octets, verwisselt de leidende tag en hasht ze zoals ze zijn gearriveerd, dus volgorde doet er nooit toe en een verkeerd geordende SET OF digesteert naar precies wat de ondertekenaar heeft berekend. OpenSSL en BouncyCastle, waar de breed uitgerolde Europese validators op zijn gebouwd, parsen de attributenset in structuren en re-encoden vóór het digesten. Re-encoding past de sort toe, de digest komt niet meer overeen met de ondertekende, en de handtekening wordt gemeld als kapot in plaats van verkeerd gecodeerd, wat je op jacht stuurt door key material en certificaatketens naar een defect dat in een vergelijkingsfunctie woont

PDFium Component-diagram van de verifier-divergentie achter de sorted SET OF-bug: de ondertekenaar hasht de DER-encoding met de SET-tag 31, het Windows-pad van CryptVerifyDetachedMessageSignature digesteert de ontvangen octets zodat volgorde nooit uitmaakt en een verkeerd geordende handtekening slaagt, terwijl OpenSSL en BouncyCastle re-encoden, de X.690-sort toepassen en de handtekening als kapot melden
CryptVerifyDetachedMessageSignature digesteert de octets zoals ze zijn gearriveerd, dus een verkeerd geordende set verifieert, terwijl OpenSSL en BouncyCastle re-encoden vóór het digesten en de handtekening als kapot melden in plaats van verkeerd gecodeerd

Generaliseer voorbij deze ene bug, want dat is het dure deel. Een same-origin round trip is immuun voor elke afwijking van een interoperabiliteitsspecificatie die beide helften van je code delen; het bewijst dat je writer en je reader het met elkaar eens zijn, en dat is niet de eigenschap waar iemand om geeft. Voor alles dat het proces als wire format verlaat, is de enige betekenisvolle regressie er een die bytes vastpint tegen de specificatie of een vreemde implementatie over je output laat draaien. De OpenSSL CMS-verificatiebackend verdient zijn plek hier om een tweede reden naast portabiliteit: het is een echt onafhankelijke reader van je eigen DER

De fix, en waar hij al bestond

De DER-writer had de gesorteerde variant de hele tijd al. TDerWriter in FPdfAsn1.pas stelt zowel SetOf beschikbaar, die een vooraf samengevoegde contentbuffer in een SET-tag wikkelt en elke volgorde behoudt waarin hij is aangereikt, als SetOfSorted, die de leden individueel als een TDerByteArrays neemt en ze merge-sorteert op hun encodings vóór het wikkelen. De PDF-MAC-code in FPdfMac.pas bouwde zijn authenticated attribute set al met SetOfSorted; alleen de CMS-ondertekenaar niet. De leden tot aan de wikkelcall als een array houden is de structurele helft van de fix, want zodra attributen in één buffer zijn samengevoegd is de ordeningsinformatie weg en kan geen enkele zorg downstream haar herstellen. Een writer-API die voor SET OF alleen SetOf(Content: TBytes) aanbiedt, is een API die deze bug uitnodigt; de overload die een array neemt, maakt het juiste uitdrukbaar

Diagram van het TDerWriter-API-paar in PDFium Component: SetOf wikkelt een vooraf samengevoegde TBytes-buffer en behoudt elke lidvolgorde waarin hij is aangereikt, met output die geen DER is, terwijl SetOfSorted de leden als een TDerByteArrays-array houdt, ze merge-sorteert op hun volledige encodings en een conforme SET OF wikkelt
Zodra attributen in één buffer zijn samengevoegd is de ordeningsinformatie weg, dus de array-nemende SetOfSorted-overload is wat het juiste uitdrukbaar maakt op het punt van encoderen
// Voorheen: leden behouden de volgorde waarin AddAttr ze toevoegde
Result:= W.SetOf(ConcatMany(AttrList));

// Nu: X.690 11.6-ordering toegepast op het punt van encoderen
Result:= W.SetOfSorted(AttrList);

SET OF sorteert, SEQUENCE OF niet

Dit is het onderscheid dat het vaakst wordt verward, en het omgekeerd krijgen is net zo schadelijk als het negeren. Een SEQUENCE OF is een geordende collectie: de volgorde hoort bij de waarde en je mag er nooit aan sorteren. Een SET OF is als waarde ongeordend, dus legt DER een canonieke volgorde op om de encoding uniek te maken. Een SEQUENCE OF sorteren bederft data; een SET OF ongesorteerd laten produceert iets dat geen DER is

  • SET OF, moet gesorteerd: SignedAttributes en UnsignedAttributes in SignerInfo, de digestAlgorithms-set, de certificates- en crls-sets in SignedData, de signerInfos-set, de waardeset binnen elk afzonderlijk Attribute, en de attributenset binnen elke RelativeDistinguishedName van een X.501 Name
  • SEQUENCE OF, nooit sorteren: de RDN-lijst die een Name vormt, het certs-veld van SigningCertificateV2, extensielijsten en de general-name-lijsten erbinnen

Een RDN is een SET OF binnen een SEQUENCE OF één niveau hoger, dus een distinguished name bevat beide regels binnen twee niveaus van nesting. Die layout verklaart ook waarom sets met één lid zo vergevingsgezind zijn: een set met één element is triviaal gesorteerd, en daarom waren de attribuutwaardesets die met W.SetOf in BuildSignedAttrs zijn gebouwd zoals geschreven correct en hadden ze geen wijziging nodig. De bug leeft alleen waar een set echt twee of meer leden bevat, en multi-valued RDN's zijn zeldzaam genoeg dat de meeste name encoders jarenlang met dezelfde fout wegkomen

Hoe voorkom je dat dit terugkomt?

Assert op bytes, niet op verdicts. De goedkoopste blijvende check is een zelf-audit die door zijn eigen output wandelt: parse de uitgezonden signedAttrs, vergelijk elk lid met het vorige met dezelfde unsigned-octet-string-vergelijking die de writer gebruikt, en laat de build falen als een paar uit volgorde is. Hij draait in microseconden, heeft geen externe bibliotheek nodig, en in tegenstelling tot een verificatie-round-trip test hij de eigenschap die de specificatie daadwerkelijk stelt

// Wandel door de leden van een SET OF en bevestig X.690 11.6-ordering.
// Members is de lijst die aan SetOfSorted is gegeven, of het resultaat van
// het opnieuw parsen van een uitgezonden 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;

// Stop in de signing-test niet bij "onze verifier zei geldig".
// Pin de bytes die de ondertekenaar heeft gehasht
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');

Twee grenzen verdienen het om ronduit te worden gesteld. signedAttrs sorteren maakt een handtekening niet op zichzelf interoperabel, want al het andere in de container moet ook standhouden en de redenen waarom validators anderszins deugdelijke PAdES-handtekeningen afwijzen zijn gevarieerd genoeg om een eigen bespreking te vullen. En een gesorteerde SET OF is een eigenschap van wat je schrijft, niet van wat je leest: PDFium Component moet nog steeds een inkomende handtekening accepteren waarvan de signedAttrs ongesorteerd arriveren, want zulke bestanden bestaan in het wild en RFC 5652-clausule 5.4 zegt een verifier te re-encoden in plaats van af te wijzen. Strikt op output, tolerant op input, zoals gewoonlijk. De DER-writer, de CMS-builder en het signing-pad dat in de PAdES B-B-walkthrough wordt beschreven, worden allemaal meegeleverd als leesbare Pascal-bron met de PDFium Delphi component, wat überhaupt het punt is van broncode meeleveren bij een signing-bibliotheek: wanneer een validator ergens in Europa het oneens is met je document, kun je de exacte bytes lezen die je code heeft uitgezonden en de exacte clausule die ze schenden, in plaats van een ticket in te dienen en te wachten