PDF Library for Delphi (PDFlibPas) haalt de certificaten binnen een PDF-handtekening op met een pure DER-walk over de CMS SignedData in /Contents, zonder dat CryptoAPI eraan te pas komt. Sinds v3.539.10 wordt elke geneste read begrensd door zijn parent-element, wordt de nulopvulling na de CMS afgekapt op de lengte die de CMS zelf aangeeft, en coderen object identifiers hun gecombineerde eerste subidentifier in base-128. De grensregel en de OID-fix vervingen allebei code die foute antwoorden produceerde zonder een fout te melden, en de opvullingsregel zorgt ervoor dat de strengere lezer echte handtekeningen niet afwijst
De leeszijde is belangrijker dan hij lijkt. Long-term validation-tooling moet het ondertekenaarcertificaat en zijn issuers uit een bestaande handtekening halen voordat het revocation-data kan ophalen, een auditraapport moet kunnen zeggen wie er ondertekend heeft, en een Lazarus-build op Linux heeft geen Windows-messagefuncties om op terug te vallen. Een parser in die positie crasht zelden op slechte invoer. De foutmodus die pijn doet is een certificaattelling die bytes van een buur meeneemt, een ondertekenaar-match tegen het verkeerde veld, of een OID die stilletjes in een andere OID verandert. Een handtekeningpijplijn daar bovenop rapporteert overtuigende onzin
De ondertekenaarcertificaten uit een ondertekende PDF lezen
Vijf TPDFlib-methoden dekken de leeszijde, en ze nemen allemaal InputFile, Password, FieldName: elke aanroep opent het bestand alleen-lezen, geeft antwoord en sluit het weer. GetSignatureEmbeddedCertificateCount en GetSignatureEmbeddedCertificateDER sommeren de certificaten-set op in encoding-volgorde, GetSignatureSignerCertificateDER geeft het certificaat terug dat een gegeven SignerInfo produceerde, en GetSignatureCertificateChainLength / GetSignatureCertificateChainDER lopen vanaf die ondertekenaar richting de verste issuer die de handtekening zelf meedraagt. Indexen zijn zero-based. Bewaar de resultaten in een AnsiString, en dat is ook precies waarom de library ze zo teruggeeft: een DER-blob die via string of een TStrings gaat, passeert een tekensetconversie en komt gecorrumpeerd terug
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;
Aan die uitvoer zitten twee dingen die aandacht vragen. Een telling van 0 is geen diagnose: een ontbrekend veld, een verkeerd wachtwoord, een blob die geen DER is en een SignedData die de optionele certificaten-set simpelweg weglaat, komen allemaal terug als 0 of een lege string, dus log de veldnaam naast het getal. En een keten die eindigt vóór een self-issued certificaat is ook geen fout. De chain builder gebruikt alleen certificaten die in de handtekening zitten, dus de resterende issuers moeten opgehaald worden via de adressen die GetCertificateIssuerURLs meldt
Hoeveel van /Contents is eigenlijk de CMS?
Alleen het prefix dat de buitenste SEQUENCE aangeeft hoort bij de CMS, en PLTrimCMSPadding kapt alles daarna af. Een ondertekenaar reserveert de hex-string van /Contents voordat de CMS bestaat, omdat de /ByteRange uit ISO 32000-1 §12.8.1 eerst vast moet liggen, dus de plek wordt ruim bemeten en de ongebruikte staart bestaat uit nullen. PLTrimCMSPadding leest de eerste TLV, eist tag $30 en geeft de bytes tot het einde van dat element terug; alles wat niet met een goed gevormde SEQUENCE begint, komt leeg terug. Dat topniveau is de enige plek waar bytes aan het einde legaal zijn, en dat onderscheid is belangrijk voor de volgende paragraaf: een strenge "het element moet de hele buffer opeten"-regel zou elke handtekening uit de praktijk afwijzen, terwijl een soepele regel op elke diepte geneste velden bytes laat lezen die niet van hen zijn
Waarom heeft een DER-lezer de eind-offset van de parent nodig?
Een genest element is alleen geldig als het binnen zijn parent eindigt, en controleren tegen het einde van de buffer bewijst dat niet. De low-level DERReadTLV in PDFlibASN1 begrenst elk element tegen de hele string, wat voor het buitenste object de juiste controle is en voor alles eronder de verkeerde. Denk aan een SignerInfo waarvan issuerAndSerialNumber 40 bytes aangeeft terwijl de issuer Name daarbinnen 60 claimt. Elke byte zit nog in de buffer, dus een bufferbegrensde lezer accepteert de Name, leest het serienummer uit het digest-algoritme dat erop volgt, en vergelijkt dat paar vervolgens met de ingesloten certificaten. Vóór v3.539.10 las de CMS-walker precies zo. De fix is een kleine wrapper die de eindpositie van de parent in elke read meedraagt
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// niets meer over binnen de parent: weiger een read te starten
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset staat nu één voorbij het element; hij mag de parent niet passeren
Result := Offset <= ParentEnd;
end;
// elk niveau noteert zijn eigen einde en geeft dat door:
// OuterEnd := end of ContentInfo (RFC 5652 section 3)
// ExplicitEnd := end of content [0] EXPLICIT
// ContentEnd := end of SignedData (RFC 5652 section 5.1)
// SignerEnd / InnerEnd for SignerInfo and issuerAndSerialNumber
De unit, PDFlibCMSRead, voert die eindes nu door via ContentInfo, de [0] EXPLICIT-wrapper, de SignedData-velden tot aan signerInfos, de SignerIdentifier in zowel zijn issuerAndSerialNumber- als [0] subjectKeyIdentifier-vorm (RFC 5652 §5.3), en de tbsCertificate-velden die bij het matchen van de ondertekenaar uit elk ingesloten certificaat gelezen worden. Binnen de certificaten-set en de signerInfos-set stopt een element dat voorbij het einde van de set loopt de lus: PLExtractCMSCertificates geeft de certificaten terug die hij al geaccepteerd had en plakt de daaropvolgende crls- of signerInfos-bytes nooit aan de laatste vast. De issuer-and-serial-match vereist ook allebei de helften, want een serienummer is alleen uniek binnen één issuer
Waarom kwam 2.999.3 eruit als 1.15.3?
De eerste twee arcs van een OID combineren tot één subidentifier, niet tot één byte, en die subidentifier wordt net als elke andere arc in base-128 gecodeerd. X.690 §8.19.4 definieert hem als 40 * arc1 + arc2; de oudere DER_OID schreef die waarde met Byte(...), wat alleen klopt tot 127, de waarde van 2.47. Voor 2.999 is de som 1079, de byte-cast houdt 55 over, en 55 decodeert als 1.15, dus de identifier benoemt stilletjes een andere tak van de boom. Waarden van 128 tot 255 gaan op een andere manier mis: ze sturen één byte uit met de continuation bit gezet, die de volgende arc inslikt. De meeste PKI-identifiers (1.2.840..., 2.5.29..., 0.4.0...) komen de grens nooit tegen, en daarom is de bug overleefd; de joint-iso-itu-t-arcs vanaf 2.48 wel. DER_OID dient zowel de encoder voor signed attributes als de matcher in DERFindExtensionByOID en de SignedData content-type-controle, dus een verkeerde encoding brak zowel het schrijven als het opzoeken
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.
De gecombineerde waarde zit met opzet in een UInt64. DER_OID parst arcs in Int64, dus een legale tweede arc kan net zo groot zijn als Int64.MaxValue, en er 80 bij optellen voor arc1 = 2 laat een signed 64-bit integer overlopen. UInt64 draagt Int64.MaxValue + 80 zonder te wrappen, en de scratch-buffer van tien bytes herbergt de tien 7-bit groepen die een 64-bit waarde nodig heeft. Testvectoren die de moeite van bewaren waard zijn, staan aan weerszijden van de grens: 2.47 moet één byte blijven, en 2.48 moet er twee worden
Wat garandeert de CMS-walker aan de leeszijde?
PDFlibCMSRead garandeert structuur en niets anders: hij geeft bytes terug die zitten waar RFC 5652 zegt dat ze horen, en verifieert geen handtekening, digest of geldigheidsperiode. De walker accepteert alleen DER, dus DERReadTLV wijst indefinite lengths en multi-byte tagnummers af, en een BER-gecodeerde CMS van een niet-conforme ondertekenaar meldt nul certificaten in plaats van een half gegokt antwoord. Attribute certificates en de andere CertificateChoices-alternatieven worden overgeslagen, omdat er stroomafwaarts niets is dat ze kan gebruiken. Cryptografische verificatie blijft bij de code die ervoor verantwoordelijk is, te beginnen met de byte coverage-controles uit PAdES-ondertekening en ByteRange-validatie in Delphi en daarna het classificeren van wat er verandert nadat een PDF is ondertekend
De bredere les geldt voor elk binair formaat: "binnen de buffer" is een memory-safety-eigenschap, "binnen de parent" is een correctheidseigenschap, en een parser heeft allebei nodig. Hetzelfde denken over vijandige lengtes loopt door het verharden van een Pascal PDF-parser tegen kwaadaardige bestanden. De certificaatextractie, de chain building en de long-term validation-API's die hier behandeld worden, schepen mee met losLab PDF Library for Delphi, voor Delphi, C++Builder en Lazarus