PDFium Component baut den CMS-signedAttrs-Block über TDerWriter.SetOfSorted, denn X.690-Klausel 11.6 verlangt, dass die Mitglieder eines SET OF in aufsteigender Reihenfolge ihrer eigenen DER-Encodings emittiert werden. Bauen Sie sie stattdessen in Konstruktionsreihenfolge, und Sie bekommen Signaturen, die Ihr eigener Verifier akzeptiert und jeder Dritt-PAdES-Validator ablehnt
Dieser letzte Satz ist der ganze Grund, warum dieser Defekt eine vollständige Regressions-Suite überlebte. Der Signaturpfad war abgedeckt, der Verifikationspfad war abgedeckt, der Round Trip war auf jeder Plattform grün, und die Ausgabe war trotzdem kein gültiges DER. Ein Test, der mit Ihrem Writer signiert und mit Ihrem Reader verifiziert, kann eine Abweichung von den Encoding-Regeln nicht erkennen, denn beide Hälften teilen die Abweichung
Was X.690-Klausel 11.6 tatsächlich verlangt
Die Mitglieder eines SET-OF-Werts müssen sortiert sein, und der Sortierschlüssel ist das vollständige DER-Encoding jedes Mitglieds, verglichen als unsigned Octet-String, wobei das kürzere Encoding zuerst kommt, wenn eines ein Präfix des anderen ist. Das ist eine kanonische Einschränkung statt einer Empfehlung: DER admitiert exakt ein Encoding eines gegebenen Werts, also ist ein unsortiertes SET OF kein anders geordnetes Encoding desselben Werts, es ist überhaupt kein DER-Encoding. Weil ASN.1-Tags in einem TLV zuerst kommen, entscheidet sich der Vergleich meist in den ersten zwei Octets, und da jedes Mitglied einer CMS-Attributliste mit 30 beginnt, dem SEQUENCE-Tag, entscheidet das Längen-Octet. Die geforderte Ordnung sieht daher fast so aus, als würde man die Attribute nach Größe ordnen, was man wissen sollte, wenn man auf einen Hex-Dump starrt und sich fragt, warum die Ordnung willkürlich erscheint
Warum ist schon die minimale Attributmenge aus der Reihe?
Weil die natürliche Konstruktionsreihenfolge und die Encoded-Reihenfolge ab der allerersten Signatur, die Sie erzeugen, widersprechen, ohne dass optionale Attribute beteiligt sind. BuildSignedAttrs in FPdfCms.pas hängt content-type, dann message-digest, dann signing-certificate-v2, dann cms-algorithm-protection an. Mit SHA-256-Digests und einem RSA-Signaturalgorithmus beginnen die vier Attribut-TLVs mit diesen Bytes
// Von BuildSignedAttrs erzeugte Konstruktionsreihenfolge
// content-type 30 18 (24 Inhalts-Octets)
// message-digest 30 2F (47)
// signing-certificate-v2 30 37 (55)
// cms-algorithm-protection 30 2D (45)
//
// Von X.690 11.6 verlangte DER-Reihenfolge
// 30 18 content-type
// 30 2D cms-algorithm-protection
// 30 2F message-digest
// 30 37 signing-certificate-v2
Zwei der vier Attribute bewegen sich. cms-algorithm-protection bei 30 2D muss vor signing-certificate-v2 bei 30 37 kommen, und es muss auch vor message-digest bei 30 2F kommen. Also war die kleinste konforme PAdES-B-B-Attributmenge, die PDFium Component emittieren kann, unter dem alten Code schon falsch geordnet, bevor irgendjemand signing-time, commitment-type-indication oder einen signature-policy-identifier hinzufügt. Es gab nie eine Konfiguration, in der der Bug nicht feuerte
Warum bestehen unsere eigenen Tests, während jeder externe Validator scheitert?
Weil die zwei Familien von Verifiern den Message Digest über verschiedene Bytes berechnen. RFC 5652-Klausel 5.4 sagt, die Eingabe zur Signaturoperation sei das DER-Encoding des SignedAttributes-Werts, wobei das IMPLICIT-[0]-Tag durch das SET-Tag 31 ersetzt wird, und der Satz, der alles entscheidet, ist das DER-Encoding, nicht die Octets, die Sie empfangen haben. Der über CryptVerifyDetachedMessageSignature erreichte Windows-Plattform-Verifier nimmt die empfangenen Octets, tauscht das führende Tag und hasht sie, wie sie ankamen, also geht Ordnung nie ein, und ein falsch geordnetes SET OF digesteert exakt zu dem, was der Signierer berechnet hat. OpenSSL und BouncyCastle, worauf die weit verbreiteten europäischen Validatoren gebaut sind, parsen die Attributmenge in Strukturen und re-encoden vor dem Digesten. Re-Encoding wendet die Sortierung an, der Digest matcht nicht mehr den signierten, und die Signatur wird als kaputt gemeldet statt als falsch encodiert, was Sie auf die Jagd durch Key-Material und Zertifikatsketten schickt nach einem Defekt, der in einer Vergleichsfunktion lebt
Verallgemeinern Sie über diesen einen Bug hinaus, denn das ist der teure Teil. Ein Same-Origin-Round-Trip ist immun gegen jede Abweichung von einer Interop-Spezifikation, die beide Hälften Ihres Codes teilen; er beweist, dass Ihr Writer und Ihr Reader miteinander übereinstimmen, was nicht die Eigenschaft ist, die irgendjemanden interessiert. Für alles, was den Prozess als Wire-Format verlässt, ist die einzige bedeutsame Regression eine, die Bytes gegen die Spezifikation pinnt oder eine fremde Implementierung über Ihre Ausgabe laufen lässt. Das OpenSSL-CMS-Verifikations-Backend verdient seinen Platz hier aus einem zweiten Grund neben der Portabilität: Es ist ein wirklich unabhängiger Leser Ihres eigenen DER
Der Fix, und wo er schon existierte
Der DER-Writer hatte die sortierte Variante die ganze Zeit. TDerWriter in FPdfAsn1.pas legt sowohl SetOf offen, das einen vor-konkatenierten Content-Puffer in ein SET-Tag wickelt und die Ordnung erhält, in der es übergeben wurde, als auch SetOfSorted, das die Mitglieder einzeln als TDerByteArrays nimmt und sie vor dem Wickeln per Merge-Sort nach ihren Encodings sortiert. Der PDF-MAC-Code in FPdfMac.pas baute seine authentifizierte Attributmenge bereits mit SetOfSorted; nur der CMS-Signer nicht. Die Mitglieder bis unmittelbar vor den Wickel-Aufruf als Array zu halten, ist die strukturelle Hälfte des Fixes, denn sobald Attribute zu einem einzigen Puffer konkateniert sind, ist die Ordnungsinformation weg, und keine Sorgfalt flussabwärts kann sie wiederherstellen. Eine Writer-API, die für SET OF nur SetOf(Content: TBytes) anbietet, ist eine API, die diesen Bug einlädt; die Array-nehmende Überladung ist, was das Richtige ausdrückbar macht
// Vorher: Mitglieder behalten die Reihenfolge, in der AddAttr sie angehängt hat
Result:= W.SetOf(ConcatMany(AttrList));
// Nachher: X.690-11.6-Ordnung am Punkt des Encodings angewendet
Result:= W.SetOfSorted(AttrList);
SET OF wird sortiert, SEQUENCE OF nicht
Das ist die am häufigsten verwechselte Unterscheidung, und sie rückwärts zu bekommen ist genauso schädlich wie sie zu ignorieren. Ein SEQUENCE OF ist eine geordnete Sammlung: Seine Ordnung ist Teil des Werts, und Sie dürfen sie nie sortieren. Ein SET OF ist als Wert ungeordnet, also erlegt DER eine kanonische Ordnung auf, um das Encoding eindeutig zu machen. Ein SEQUENCE OF zu sortieren korrumpiert Daten; ein SET OF unsortiert zu lassen erzeugt etwas, das kein DER ist
- SET OF, muss sortiert werden:
SignedAttributesundUnsignedAttributesinSignerInfo, diedigestAlgorithms-Menge, diecertificates- undcrls-Mengen inSignedData, diesignerInfos-Menge, die Wertmenge in jedem einzelnenAttributeund die Attributmenge in jedemRelativeDistinguishedNameeines X.501-Name - SEQUENCE OF, nie sortieren: die RDN-Liste, die einen
Nameausmacht, dascerts-Feld vonSigningCertificateV2, Erweiterungslisten und die General-Name-Listen darin
Ein RDN ist ein SET OF, das eine Ebene höher in einem SEQUENCE OF sitzt, also enthält ein Distinguished Name beide Regeln innerhalb von zwei Verschachtelungsebenen. Dieses Layout erklärt auch, warum Ein-Mitglied-Mengen so verzeihend sind: Eine Menge mit einem Element ist trivial sortiert, weshalb die Attributwertmengen, die mit W.SetOf in BuildSignedAttrs gebaut wurden, wie geschrieben korrekt waren und keine Änderung brauchten. Der Bug lebt nur, wo eine Menge wirklich zwei oder mehr Mitglieder hält, und mehrwertige RDNs sind selten genug, dass die meisten Name-Encoder jahrelang mit demselben Fehler davonkommen
Wie verhindern Sie, dass das wiederkommt?
Asserten Sie auf Bytes, nicht auf Urteile. Der billigste dauerhafte Check ist ein Selbst-Audit, das die eigene Ausgabe durchläuft: Parsen Sie das emittierte signedAttrs, vergleichen Sie jedes Mitglied mit dem vorherigen mit demselben Unsigned-Octet-String-Vergleich, den der Writer verwendet, und lassen Sie den Build scheitern, wenn ein Paar aus der Reihe ist. Es läuft in Mikrosekunden, braucht keine externe Bibliothek, und anders als ein Verifikations-Round-Trip testet es die Eigenschaft, die die Spezifikation tatsächlich behauptet
// Die Mitglieder eines SET OF durchlaufen und die X.690-11.6-Ordnung bestätigen.
// Members ist die an SetOfSorted übergebene Liste oder das Ergebnis des
// Neu-Parsens eines emittierten 31 xx / A0 xx-Blocks
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;
// Im Signiertest nicht bei »unser Verifier sagt gültig« aufhören.
// Die Bytes pinnen, die der Signierer gehasht hat
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');
Zwei Grenzen lohnen die klare Ansage. Das Sortieren von signedAttrs macht eine Signatur nicht von selbst interoperabel, denn alles andere im Container muss auch halten, und die Gründe, aus denen Validatoren sonst einwandfrei klingende PAdES-Signaturen ablehnen, sind vielfältig genug für eine eigene Diskussion. Und ein sortiertes SET OF ist eine Eigenschaft dessen, was Sie schreiben, nicht dessen, was Sie lesen: PDFium Component muss weiterhin eine eingehende Signatur akzeptieren, deren signedAttrs unsortiert ankommen, denn solche Dateien existieren in freier Wildbahn, und RFC 5652-Klausel 5.4 sagt einem Verifier, zu re-encoden statt abzulehnen. Streng am Output, tolerant am Input, wie üblich. Der DER-Writer, der CMS-Builder und der in dem PAdES-B-B-Walkthrough beschriebene Signaturpfad fahren alle als lesbarer Pascal-Quelltext mit dem PDFium Delphi component, was überhaupt der Punkt ist, Quelltext für eine Signaturbibliothek auszuliefern: Wenn ein Validator irgendwo in Europa mit Ihrem Dokument uneins ist, können Sie die exakten Bytes lesen, die Ihr Code emittiert hat, und die exakte Klausel, die sie verletzen, statt ein Ticket einzureichen und zu warten