Die PDF Library for Delphi (PDFlibPas) holt die Zertifikate innerhalb einer PDF-Signatur über einen reinen DER-Durchlauf durch das CMS SignedData in /Contents, ganz ohne CryptoAPI. Seit v3.539.10 ist jeder verschachtelte Lesevorgang durch sein Elternelement begrenzt, das Null-Padding hinter dem CMS wird an der vom CMS deklarierten Länge abgeschnitten, und Object Identifier kodieren ihren kombinierten ersten Subidentifier in Base-128. Die Grenzregel und der OID-Fix haben beide Code ersetzt, der falsche Ergebnisse lieferte, ohne einen Fehler zu werfen, und die Padding-Regel sorgt dafür, dass der strengere Reader echte Signaturen nicht abweist
Die Leseseite ist wichtiger, als sie aussieht. Long-Term-Validation-Werkzeuge müssen das Signaturzertifikat und seine Issuer aus einer bestehenden Signatur herausziehen, bevor sie Sperrdaten nachladen können, ein Prüfbericht muss sagen, wer unterschrieben hat, und ein Lazarus-Build unter Linux hat keine Windows-Message-Funktionen, auf die er sich stützen könnte. Ein Parser in dieser Position stürzt an schlechtem Input selten ab. Der Fehlermodus, der wehtut, ist eine Zertifikatsanzahl, die Bytes des Nachbarn mitzählt, ein Signer-Match gegen das falsche Feld oder eine OID, die sich lautlos in eine andere OID verwandelt. Eine Signatur-Pipeline, die darauf aufbaut, meldet selbstsicheren Unsinn
Signaturzertifikate aus einer signierten PDF lesen
Fünf TPDFlib-Methoden decken die Leseseite ab, und alle nehmen InputFile, Password, FieldName: Jeder Aufruf öffnet die Datei schreibgeschützt, antwortet und schließt sie wieder. GetSignatureEmbeddedCertificateCount und GetSignatureEmbeddedCertificateDER zählen das Zertifikats-Set in Kodierreihenfolge durch, GetSignatureSignerCertificateDER liefert das Zertifikat, das ein bestimmtes SignerInfo erzeugt hat, und GetSignatureCertificateChainLength / GetSignatureCertificateChainDER laufen von diesem Signer hinauf zum entferntesten Issuer, den die Signatur selbst mitbringt. Die Indizes sind nullbasiert. Halten Sie die Ergebnisse in AnsiString, weshalb die Bibliothek sie auch so zurückgibt: Ein DER-Blob, der durch string oder eine TStrings läuft, geht durch eine Zeichensatzkonvertierung und kommt korrupt zurück
uses
SysUtils, Classes, PDFlibrary;
procedure SaveDer(const FileName: string; const Der: AnsiString);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if Der <> '' then
Fs.WriteBuffer(Der[1], Length(Der));
finally
Fs.Free;
end;
end;
const
Src = 'contract-signed.pdf';
Field = 'Signature1';
var
Pdf: TPDFlib;
ChainLen, I: Integer;
SignerDer, LastDer: AnsiString;
begin
Pdf := TPDFlib.Create;
try
WriteLn('Certificates in the CMS: ',
Pdf.GetSignatureEmbeddedCertificateCount(Src, '', Field));
SignerDer := Pdf.GetSignatureSignerCertificateDER(Src, '', Field, 0);
if SignerDer = '' then
raise Exception.Create('signer certificate missing or not matched');
SaveDer('signer.cer', SignerDer);
ChainLen := Pdf.GetSignatureCertificateChainLength(Src, '', Field, 0);
for I := 0 to ChainLen - 1 do
SaveDer(Format('chain-%d.cer', [I]),
Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, I));
if ChainLen > 0 then
begin
LastDer := Pdf.GetSignatureCertificateChainDER(Src, '', Field, 0, ChainLen - 1);
WriteLn('Next issuer, if any: ', Pdf.GetCertificateIssuerURLs(LastDer));
end;
finally
Pdf.Free;
end;
end;
Zwei Dinge in dieser Ausgabe brauchen Aufmerksamkeit. Eine 0 ist keine Diagnose: Ein fehlendes Feld, ein falsches Passwort, ein Blob, der kein DER ist, und ein SignedData, das das optionale Zertifikats-Set schlicht weglässt, liefern alle 0 oder einen leeren String zurück — loggen Sie den Feldnamen also neben der Zahl mit. Und eine Kette, die vor einem selbst ausgestellten Zertifikat endet, ist ebenfalls kein Fehler. Der Kettenbauer nutzt nur Zertifikate, die in der Signatur eingebettet sind, die restlichen Issuer müssen also über die Adressen beschafft werden, die GetCertificateIssuerURLs meldet
Wie viel von /Contents ist überhaupt das CMS?
Nur der Präfix, den die äußere SEQUENCE deklariert, gehört zum CMS, und PLTrimCMSPadding schneidet alles dahinter ab. Ein Signer reserviert den /Contents-Hex-String, bevor das CMS existiert, weil das in ISO 32000-1 §12.8.1 beschriebene /ByteRange zuerst feststehen muss, daher ist der Slot großzügig bemessen und der ungenutzte Schwanz besteht aus Nullen. PLTrimCMSPadding liest das erste TLV, verlangt Tag $30 und gibt die Bytes bis zum Ende dieses Elements zurück; alles, was nicht mit einer wohlgeformten SEQUENCE beginnt, kommt leer zurück. Diese oberste Ebene ist der eine Ort, an dem nachlaufende Bytes legal sind, und der Unterschied ist für den nächsten Abschnitt wichtig: Eine strenge Regel „das Element muss den ganzen Buffer konsumieren“ würde jede reale Signatur abweisen, während eine lasche Regel auf jeder Tiefe verschachtelten Feldern Bytes lesen lässt, die ihnen nicht gehören
Warum braucht ein DER-Reader das End-Offset des Elternelements?
Ein verschachteltes Element ist nur gültig, wenn es innerhalb seines Elternelements endet, und eine Prüfung gegen das Ende des Buffers beweist das nicht. Das Low-level-DERReadTLV in PDFlibASN1 begrenzt jedes Element gegen den gesamten String, was für das äußerste Objekt die richtige Prüfung ist und für alles darunter die falsche. Stellen Sie sich ein SignerInfo vor, dessen issuerAndSerialNumber 40 Bytes deklariert, während der Issuer Name darin 60 beansprucht. Jedes Byte liegt weiterhin im Buffer, also akzeptiert ein bufferbegrenzter Reader den Name, liest die Seriennummer aus dem Digest-Algorithmus, der folgt, und vergleicht dieses Paar dann gegen die eingebetteten Zertifikate. Vor v3.539.10 hat der CMS-Walker exakt so gelesen. Der Fix ist ein kleiner Wrapper, der die Endposition des Elternelements in jeden Lesevorgang hineinträgt
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// nichts mehr im Elternelement übrig: keinen Lesevorgang beginnen
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset steht jetzt eins hinter dem Element; es darf das Elternelement nicht überschreiten
Result := Offset <= ParentEnd;
end;
// jede Ebene notiert ihr eigenes Ende und reicht es weiter:
// OuterEnd := Ende von ContentInfo (RFC 5652 Abschnitt 3)
// ExplicitEnd := Ende von content [0] EXPLICIT
// ContentEnd := Ende von SignedData (RFC 5652 Abschnitt 5.1)
// SignerEnd / InnerEnd für SignerInfo und issuerAndSerialNumber
Die Unit PDFlibCMSRead reicht diese Enden jetzt durch ContentInfo durch, durch den [0] EXPLICIT-Wrapper, durch die SignedData-Felder bis hinauf zu signerInfos, durch den SignerIdentifier in beiden Formen issuerAndSerialNumber und [0] subjectKeyIdentifier (RFC 5652 §5.3) und durch die tbsCertificate-Felder, die beim Signer-Abgleich aus jedem eingebetteten Zertifikat gelesen werden. Innerhalb des Zertifikats-Sets und des signerInfos-Sets stoppt ein Element, das über das Set-Ende hinausläuft, die Schleife: PLExtractCMSCertificates gibt die bereits akzeptierten Zertifikate zurück und klebt die folgenden crls- oder signerInfos-Bytes nie an das letzte an. Der Issuer-und-Seriennummer-Abgleich verlangt zudem beide Hälften, denn eine Seriennummer ist nur innerhalb eines Issuers eindeutig
Warum wurde aus 2.999.3 plötzlich 1.15.3?
Die ersten beiden Bögen einer OID verschmelzen zu einem Subidentifier, nicht zu einem Byte, und dieser Subidentifier wird wie jeder andere Bogen Base-128-kodiert. X.690 §8.19.4 definiert ihn als 40 * arc1 + arc2; das frühere DER_OID schrieb diesen Wert mit Byte(...), was nur bis 127 korrekt ist, dem Wert von 2.47. Bei 2.999 ist die Summe 1079, der Byte-Cast behält 55, und 55 dekodiert als 1.15 — der Identifier benennt also lautlos einen anderen Zweig des Baums. Werte von 128 bis 255 scheitern anders: Sie geben ein Byte mit gesetztem Continuation-Bit aus, das den nächsten Bogen verschluckt. Die meisten PKI-Identifier (1.2.840..., 2.5.29..., 0.4.0...) erreichen die Grenze nie, deshalb hat der Bug überlebt; die joint-iso-itu-t-Bögen ab 2.48 aufwärts schon. DER_OID bedient sowohl den Encoder für signed attributes als auch den Matcher in DERFindExtensionByOID und die SignedData-Content-Type-Prüfung, eine falsche Kodierung hat also Schreiben und Lookup gleichermaßen lahmgelegt
uses
SysUtils, PDFlibASN1;
function Hex(const S: AnsiString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + IntToHex(Byte(S[I]), 2) + ' ';
Result := Trim(Result);
end;
begin
WriteLn(Hex(DER_OID('2.999.3'))); // 06 03 88 37 03
WriteLn(Hex(DER_OID('2.47.1'))); // 06 02 7F 01
WriteLn(Hex(DER_OID('2.48.1'))); // 06 03 81 00 01
WriteLn(Hex(DER_OID('2.5.29.14'))); // 06 03 55 1D 0E
WriteLn(Hex(DER_OID('1.2.840.113549.1.7.2'))); // 06 09 2A 86 48 86 F7 0D 01 07 02
end.
Der kombinierte Wert liegt absichtlich in einem UInt64. DER_OID parst Bögen in Int64, ein legaler zweiter Bogen kann also so groß wie Int64.MaxValue sein, und plus 80 für arc1 = 2 läuft ein vorzeichenbehafteter 64-Bit-Integer über. UInt64 trägt Int64.MaxValue + 80 ohne Wrap-around, und der Zehn-Byte-Scratch-Buffer fasst die zehn 7-Bit-Gruppen, die ein 64-Bit-Wert braucht. Testvektoren, die man behalten sollte, sind diejenigen beidseits der Grenze: 2.47 muss ein Byte bleiben, und 2.48 muss zu zweien werden
Was garantiert der CMS-Walker auf der Leseseite?
PDFlibCMSRead garantiert Struktur und nichts weiter: Es gibt die Bytes zurück, die laut RFC 5652 dort sitzen sollten, und verifiziert keine Signatur, keinen Digest und keine Gültigkeitsdauer. Der Walker akzeptiert nur DER, DERReadTLV weist also indefinite lengths und mehrbytige Tag-Nummern zurück, und ein BER-kodiertes CMS von einem nicht konformen Signer meldet null Zertifikate statt einer halben Vermutung. Attributzertifikate und die anderen CertificateChoices-Alternativen werden übersprungen, weil sie nachgelagert niemand verwenden kann. Die kryptografische Verifizierung bleibt bei dem Code, dem sie gehört — sie beginnt mit den in PAdES-Signierung und ByteRange-Validierung in Delphi beschriebenen Byte-Abdeckungsprüfungen und geht weiter mit dem Einordnen, was sich nach dem Signieren einer PDF geändert hat
Die allgemeinere Lehre gilt für jedes Binärformat: „innerhalb des Buffers“ ist eine Memory-Safety-Eigenschaft, „innerhalb des Elternelements“ ist eine Korrektheitseigenschaft, und ein Parser braucht beides. Dasselbe Denken über feindliche Längen zieht sich durch das Härten eines Pascal-PDF-Parsers gegen bösartige Dateien. Die hier besprochenen APIs für Zertifikatsextraktion, Kettenaufbau und Long-Term-Validation kommen mit der losLab PDF Library for Delphi für Delphi, C++Builder und Lazarus