PDF Library for Delphi (PDFlibPas) plockar ut certifikaten inuti en PDF-signatur genom en ren DER-genomgång av den CMS-SignedData som lagras i /Contents, helt utan CryptoAPI. Sedan v3.539.10 avgränsas varje nästlad läsning av sitt förälderelement, nollutfyllnaden efter CMS:en kapas vid den längd CMS:en själv deklarerar, och objektidentifierare kodar sin sammanslagna första subidentifierare i base-128. Både gränsregeln och OID-fixen ersatte kod som gav fel svar utan att kasta något fel, och utfyllnadsregeln ser till att den strängare läsaren inte avvisar riktiga signaturer
Lässidan spelar större roll än den verkar. Verktyg för long-term validation måste plocka ut signerarcertifikatet och dess utfärdare ur en befintlig signatur innan de kan hämta spärrdata, en granskningsrapport måste kunna säga vem som har signerat, och ett Lazarus-bygge på Linux har inga Windows-meddelandefunktioner att luta sig mot. En parser i den situationen kraschar sällan på dålig indata. Det feltläge som gör ont är ett certifikatantal som räknar med byte som tillhör en granne, en signerarmatchning mot fel fält, eller en OID som tyst byts ut mot en annan OID. En signaturpipeline byggd ovanpå det rapporterar självsäkert nonsens
Läsa ut signerarcertifikaten ur en signerad PDF
Fem TPDFlib-metoder täcker lässidan, och samtliga tar InputFile, Password, FieldName: varje anrop öppnar filen skrivskyddat, svarar och stänger den igen. GetSignatureEmbeddedCertificateCount och GetSignatureEmbeddedCertificateDER räknar upp certifikatsmängden i kodningsordning, GetSignatureSignerCertificateDER returnerar certifikatet som gav upphov till en given SignerInfo, och GetSignatureCertificateChainLength / GetSignatureCertificateChainDER går från den signeraren mot den mest avlägsna utfärdare som signaturen själv bär med sig. Indexen är nollbaserade. Lägg resultatet i AnsiString, vilket är därför biblioteket returnerar dem just så: en DER-blob som routas genom string eller en TStrings går via en teckenuppsättningskonvertering och kommer tillbaka korrumperad
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;
Två saker i det där resultatet behöver varsamhet. Ett antal 0 är ingen diagnos: ett saknat fält, ett fel lösenord, en blob som inte är DER, och en SignedData som helt enkelt utelämnar den valfria certifikatsmängden kommer alla tillbaka som 0 eller en tom sträng, så logga fältnamnet bredvid siffran. Och en kedja som slutar före ett självutfärdat certifikat är inte heller något fel. Kedjebyggaren använder bara certifikat som är inbäddade i signaturen, så de återstående utfärdarna måste hämtas via de adresser GetCertificateIssuerURLs rapporterar
Hur mycket av /Contents är egentligen CMS?
Bara prefixet som den yttre SEQUENCE:en deklarerar tillhör CMS:en, och PLTrimCMSPadding kapar allt efter den. En signerare reserverar hex-strängen i /Contents innan CMS:en finns, eftersom /ByteRange som beskrivs i ISO 32000-1 §12.8.1 måste vara fast först, så facket dimensioneras generöst och den oanvända svansen är nollor. PLTrimCMSPadding läser första TLV:n, kräver tagg $30 och returnerar bytena fram till slutet av elementet; allt som inte börjar med en välbildad SEQUENCE kommer tillbaka tomt. Bara den toppnivån är platsen där avslutande byte är lagliga, och distinktionen spelar roll för nästa avsnitt: en strikt regel om att "elementet måste konsumera hela bufferten" skulle avvisa varje signatur ur verkligheten, medan en slapp regel tillämpad på varje djup låter nästlade fält läsa byte de inte äger
Varför behöver en DER-läsare förälderns slutoffset?
Ett nästlat element är bara giltigt om det slutar inuti sin förälder, och en kontroll mot buffertens slut bevisar inte det. Den lågnivå-DERReadTLV som finns i PDFlibASN1 avgränsar varje element mot hela strängen, vilket är rätt kontroll för yttersta objektet och fel kontroll för allt under det. Föreställ dig en SignerInfo vars issuerAndSerialNumber deklarerar 40 byte medan issuer-Name:n inuti den påstår 60. Varje byte ligger fortfarande i bufferten, så en buffertbunden läsare accepterar Name:n, läser serienumret ur den digest-algoritm som följer och jämför sedan det paret mot de inbäddade certifikaten. Före v3.539.10 läste CMS-walkern på exakt det sättet. Fixen är en liten wrapper som bär förälderns slutposition in i varje läsning
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// inget kvar inuti föräldern: vägra att påbörja en läsning
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset ligger nu ett steg efter elementet; det får inte passera föräldern
Result := Offset <= ParentEnd;
end;
// varje nivå antecknar sitt eget slut och skickar det vidare:
// 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
Uniten PDFlibCMSRead bär nu de där ändpunkterna genom ContentInfo, [0] EXPLICIT-wrappern, SignedData-fälten upp till signerInfos, SignerIdentifier i båda sina former issuerAndSerialNumber och [0] subjectKeyIdentifier (RFC 5652 §5.3), samt tbsCertificate-fälten som läses ur varje inbäddat certifikat när signeraren matchas. Inuti certifikatsmängden och signerInfos-mängden stannar loopen för ett element som passerar mängdens slut: PLExtractCMSCertificates returnerar certifikaten den redan accepterat och limmar aldrig fast följande crls- eller signerInfos-byte på den sista. Matchningen issuer plus serienummer kräver också båda halvorna, eftersom ett serienummer bara är unikt inom en utfärdare
Varför blev 2.999.3 till 1.15.3?
En OIDs två första arcs slås ihop till en subidentifierare, inte till en byte, och den subidentifieraren kodas i base-128 som alla andra arcs. X.690 §8.19.4 definierar den som 40 * arc1 + arc2; den tidigare DER_OID skrev det värdet med Byte(...), vilket bara stämmer upp till 127, värdet för 2.47. För 2.999 är summan 1079, byte-casten behåller 55, och 55 avkods som 1.15, så identifieraren namnger i tysthet en annan gren av trädet. Värden från 128 till 255 fallerar på ett annat sätt: en byte med continuation-bitten satt sänds ut och sväljer nästa arc. De flesta PKI-identifierare (1.2.840..., 2.5.29..., 0.4.0...) når aldrig gränsen, vilket är därför buggen överlevde; joint-iso-itu-t-arcs från 2.48 och uppåt gör det. DER_OID står i tjänst både hos kodaren för signed attributes och hos matcharen i DERFindExtensionByOID och kontrollen av SignedData content-type, så en felaktig kodning bröt både skrivning och uppslagning
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.
Det sammanslagna värdet ligger avsiktligt i en UInt64. DER_OID parsar arcs till Int64, så en giltig andra arc kan vara lika stor som Int64.MaxValue, och att addera 80 för arc1 = 2 spiller över ett signed 64-bitars heltal. En UInt64 bär Int64.MaxValue + 80 utan att wrappa runt, och tiobytes-scratchbufferten rymmer de tio 7-bitarsgrupper som ett 64-bitars värde behöver. Testvektorer värda att behålla är de på varsin sida om gränsen: 2.47 måste förbli en byte, och 2.48 måste bli två
Vad garanterar CMS-walkern på lässidan?
PDFlibCMSRead garanterar strukturen och inget annat: den returnerar byte som ligger där RFC 5652 säger att de ska ligga och verifierar varken signatur, digest eller giltighetstid. Walkern accepterar bara DER, så DERReadTLV avvisar obestämda längder och flerbyttaggnummer, och en BER-kodad CMS från en regelvidrig signerare rapporterar noll certifikat i stället för en partiell gissning. Attributcertifikat och de övriga CertificateChoices-alternativen hoppas över eftersom ingenting nedströms kan använda dem. Den kryptografiska verifieringen stannar hos koden som äger den, vilket börjar med byte-täckningskontrollerna som beskrivs i PAdES-signering och ByteRange-validering i Delphi och fortsätter med att klassificera vad som ändrats efter att en PDF signerats
Den vidare lärdomen gäller alla binära format: "inuti bufferten" är en minnessäkerhetsegenskap, "inuti föräldern" är en korrekthetsegenskap, och en parser behöver båda. Samma tänk kring fientliga längder löper genom att härda en Pascal-PDF-parser mot skadliga filer. Certifikatextraktionen, kedjebyggandet och long-term validation-API:erna som tas upp här följer med losLab PDF Library for Delphi, för Delphi, C++Builder och Lazarus