PDF Library for Delphi (PDFlibPas) henter ut sertifikatene inne i en PDF-signatur ved å gå rent i DER gjennom CMS SignedData som ligger i /Contents, helt uten CryptoAPI. Siden v3.539.10 er hvert nestet element avgrenset av forelderen sin, null-utfyllingen etter CMS-en kuttes ved lengden CMS-en selv deklarerer, og objektidentifikatorene koder sin sammenslåtte første subidentifikator i base-128. Både grenseregelen og OID-fiksen erstattet kode som ga feil svar uten å reise en feil, og utfyllingsregelen sørger for at den strengere leseren ikke avviser ekte signaturer
Lesesiden betyr mer enn den ser ut som. Verktøy for long-term validation må trekke ut signersertifikatet og dets utstedere fra en eksisterende signatur før de kan hente tilbakekallingsdata, en revisjonsrapport må si hvem som signerte, og en Lazarus-bygg på Linux har ingen Windows-meldingsfunksjoner å støtte seg på. En parser i den posisjonen krasjer sjelden på dårlig input. Feilmodusen som gjør vondt, er et sertifikatantall som teller med byte som tilhører en nabo, et signermatch mot feil felt, eller en OID som i stillhet blir til en annen OID. En signaturpipeline bygget på den rapporterer selvsikker vås
Lese ut signersertifikatene fra en signert PDF
Fem TPDFlib-metoder dekker lesesiden, og alle tar InputFile, Password, FieldName: hvert kall åpner filen skrivebeskyttet, svarer og lukker den igjen. GetSignatureEmbeddedCertificateCount og GetSignatureEmbeddedCertificateDER lister sertifikatsettet opp i koderrekkefølge, GetSignatureSignerCertificateDER returnerer sertifikatet som produserte en gitt SignerInfo, og GetSignatureCertificateChainLength / GetSignatureCertificateChainDER går fra den signeren mot den ytterste utstederen signaturen selv bærer med seg. Indeksene er nullbaserte. Behold resultatene i AnsiString, som er grunnen til at biblioteket returnerer dem slik: en DER-blob sendt gjennom string eller en TStrings går gjennom en tegnsettkonvertering og kommer tilbake korrupt
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;
To ting i det outputtet trenger oppmerksomhet. Et antall på 0 er ingen diagnose: et manglende felt, et feil passord, en blob som ikke er DER, og en SignedData som rett og slett utelater det valgfrie sertifikatsettet, kommer alle tilbake som 0 eller en tom streng, så logg feltnavnet ved siden av tallet. Og en kjede som stopper før et selvutstedt sertifikat er heller ingen feil. Kjedebyggeren bruker bare sertifikatene som er innbakt i signaturen, så de gjenværende utstederne må hentes gjennom adressene GetCertificateIssuerURLs rapporterer
Hvor mye av /Contents er egentlig CMS?
Bare prefikset som den ytre SEQUENCE-en deklarerer, tilhører CMS-en, og PLTrimCMSPadding kutter alt etter det. Signeren reserverer heksstrengen for /Contents før CMS-en finnes, fordi /ByteRange beskrevet i ISO 32000-1 §12.8.1 må være fast først, så plassen dimensjoneres romslig og den ubrukte halen består av nuller. PLTrimCMSPadding leser den første TLV-en, krever tag $30 og returnerer bytene frem til slutten av det elementet; alt som ikke starter med en velformet SEQUENCE, kommer tilbake tomt. Det øverste nivået er det eneste stedet hvor etterfølgende byte er lovlige, og skillet betyr noe for neste avsnitt: en streng «elementet må konsumere hele bufferen»-regel ville avvist hver ekte signatur der ute, mens en slapp regel brukt på alle dyp lar nestede felt lese byte de ikke eier
Hvorfor trenger en DER-leser forelderen sin slutoffset?
Et nestet element er bare gyldig hvis det slutter inne i forelderen, og å sjekke mot bufferens slutt beviser ikke det. Lavnivå-DERReadTLV i PDFlibASN1 avgrenser hvert element mot hele strengen, som er riktig sjekk for det ytterste objektet og feil sjekk for alt under det. Tenk deg en SignerInfo hvis issuerAndSerialNumber deklarerer 40 byte mens issuer-Name-en inni den hevder 60. Hver byte er fortsatt i bufferen, så en bufferavgrenset leser godtar Name-en, leser serienummeret ut av digestalgoritmen som følger, og sammenligner så det paret mot de innbakte sertifikatene. Før v3.539.10 leste CMS-walkeren nøyaktig slik. Fiksen er en liten wrapper som bærer forelderens sluttposisjon inn i hver lesing
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// ingenting igjen inne i forelderen: nekt å starte en lesing
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset ligger nå én forbi elementet; den får ikke passere forelderen
Result := Offset <= ParentEnd;
end;
// hvert nivå registrerer sin egen slutt og gir den videre:
// 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 trår nå de sluttene gjennom ContentInfo, [0] EXPLICIT-wrappern, SignedData-feltene opp til signerInfos, SignerIdentifier i både issuerAndSerialNumber- og [0] subjectKeyIdentifier-form (RFC 5652 §5.3), og tbsCertificate-feltene lest fra hvert innbakt sertifikat når signeren matches. Inne i sertifikatsettet og signerInfos-settet stopper et element som løper forbi settets slutt, løkka: PLExtractCMSCertificates returnerer sertifikatene den allerede hadde godtatt og limer aldri de følgende crls- eller signerInfos-bytene på den siste. Matchen mellom utsteder og serienummer krever også begge halvdeler, siden et serienummer bare er unikt innen én utsteder
Hvorfor ble 2.999.3 til 1.15.3?
De to første buene i en OID kombineres til én subidentifikator, ikke én byte, og den subidentifikatoren base-128-kodes som alle andre buer. X.690 §8.19.4 definerer den som 40 * arc1 + arc2; den tidligere DER_OID skrev den verdien med Byte(...), som bare stemmer opp til 127, verdien av 2.47. For 2.999 er summen 1079, byte-casten beholder 55, og 55 dekodes som 1.15, så identifikatoren peker i stillhet på en annen gren av treet. Verdier fra 128 til 255 feiler annerledes og sender én byte med continuation-biten satt, som svelger neste bue. De fleste PKI-identifikatorene (1.2.840..., 2.5.29..., 0.4.0...) kommer aldri til grensen, derfor overlevde feilen; joint-iso-itu-t-buene fra 2.48 og oppover gjør det. DER_OID brukes både av enkoderen for signerte attributter og av matcheren i DERFindExtensionByOID og SignedData content-type-sjekken, så feil koding slo ut både skriving og oppslag
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.
Den kombinerte verdien holdes med vilje i en UInt64. DER_OID parser buer inn i Int64, så en lovlig andre bue kan være så stor som Int64.MaxValue, og å legge til 80 for arc1 = 2 flyter over et signert 64-bits heltall. UInt64 bærer Int64.MaxValue + 80 uten å wrappe, og scratch-bufferen på ti byte rommer de ti 7-bitsgruppene en 64-bits verdi trenger. Testvectorer verdt å beholde er de på hver side av grensen: 2.47 må forbli én byte, og 2.48 må bli to
Hva garanterer lesesidens CMS-walker?
PDFlibCMSRead garanterer struktur og ingenting annet: den returnerer byte som sitter der RFC 5652 sier de skal sitte, og verifiserer verken signatur, digest eller gyldighetsperiode. Walkeren godtar bare DER, så DERReadTLV avviser ubestemte lengder og flerbyte-tagnummer, og en BER-kodet CMS fra en uregjerlig signerer rapporterer null sertifikater i stedet for en delvis gjetning. Attributsertifikater og de andre CertificateChoices-alternativene hoppes over fordi ingenting lenger ned kan bruke dem. Kryptografisk verifikasjon blir hos koden som eier den, og den starter med byte-dekningskontrollene beskrevet i PAdES-signering og ByteRange-validering i Delphi og fortsetter med å klassifisere hva som endret seg etter at en PDF ble signert
Den bredere lærdommen gjelder alle binære formater: «inne i bufferen» er en minnesikkerhetsegenskap, «inne i forelderen» er en korrekthetsegenskap, og en parser trenger begge. Den samme tenkningen om fiendtlige lengder går gjennom herding av en Pascal-PDF-parser mot ondsinnede filer. Sertifikatuttrekket, kjedebyggingen og long-term validation-API-ene omtalt her følger med losLab PDF Library for Delphi, for Delphi, C++Builder og Lazarus