PDF Library for Delphi (PDFlibPas) vytahuje certifikáty uvnitř PDF podpisu čistým procházením DER přes CMS SignedData uložené v /Contents, bez zapojení CryptoAPI. Od v3.539.10 je každé vnořené čtení ohraničené rodičovským prvkem, nulová výplň za CMS se ořeže na délku, kterou CMS deklaruje, a objektové identifikátory kódují svůj sloučený první subidentifikátor v base-128. Pravidlo hranic i oprava OID nahradily kód, který vydával špatné výsledky bez jakékoli chyby, a pravidlo výplně drží přísnějšího čtenáře od odmítání skutečných podpisů
Čtecí strana je důležitější, než vypadá. Nástroje pro dlouhodobou validaci musí vytáhnout certifikát podepisujícího a jeho vystavitele z existujícího podpisu, než stáhnou revokační data, auditní zpráva musí říct, kdo podepsal, a Lazarus build na Linuxu se nemá o co opřít — žádné Windows message funkce tam nejsou. Parser v takové pozici na špatném vstupu málokdy spadne. Bolavý režim selhání je počet certifikátů, do kterého se připlatí bajty souseda, shoda podepisujícího provedená proti špatnému poli nebo OID, které se potichu promění v úplně jiné OID. Podpisová pipeline postavená na takovém základu hlásí sebejistý nesmysl
Čtení certifikátů podepisujícího z podepsaného PDF
Čtecí stranu pokrývá pět metod TPDFlib a všechny berou InputFile, Password, FieldName: každé volání otevře soubor jen pro čtení, odpoví a zase ho zavře. GetSignatureEmbeddedCertificateCount a GetSignatureEmbeddedCertificateDER vypisují certifikáty v sadě v pořadí kódování, GetSignatureSignerCertificateDER vrátí certifikát, který vyrobil dané SignerInfo, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER jdou od podepisujícího k nejvzdálenějšímu vystaviteli, který podpis sám nese. Indexy se číslují od nuly. Výsledky držte v AnsiStringu, a proto je knihovna taky takhle vrací: DER blob puštěný přes string nebo TStrings projde konverzí znakové sady a vrátí se poškozený
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;
Dvě věci v tomhle výstupu chtějí pozornost. Počet 0 není diagnóza: chybějící pole, špatné heslo, blob, který není DER, i SignedData, které volitelnou sadu certifikátů prostě vynechá, všechno se vrátí jako 0 nebo prázdný řetězec, takže si vedle čísla logujte název pole. A řetěz, který skončí dřív než u sebepodepsaného certifikátu, taky není chyba. Stavitel řetězu používá jen certifikáty vložené v podpisu, takže zbývající vystavitele je potřeba stáhnout přes adresy, které hlásí GetCertificateIssuerURLs
Kolik z /Contents je doopravdy CMS?
CMS patří jen prefix, který vnější SEQUENCE deklaruje, a PLTrimCMSPadding všechno za ním ořeže. Podepisující si rezervuje hex string /Contents dřív, než CMS vůbec existuje, protože /ByteRange popsaný v ISO 32000-1 §12.8.1 se musí zafixovat nejdřív, takže slot má velkorysou velikost a nepoužitý ocas jsou nuly. PLTrimCMSPadding přečte první TLV, vyžaduje tag $30 a vrátí bajty až do konce toho prvku; cokoli, co nezačíná korektně poskládanou SEQUENCE, se vrátí prázdné. Ta nejvyšší úroveň je jediné místo, kde jsou koncové bajty legální, a tenhle rozdíl je důležitý pro další sekci: přísné pravidlo „prvek musí spotřebovat celý buffer“ by odmítlo každý reálný podpis, zatímco laxní pravidlo aplikované v každé hloubce nechá vnořená pole číst bajty, které nevlastní
Proč potřebuje DER čtenář koncový offset rodiče?
Vnořený prvek je validní jen tehdy, když končí uvnitř svého rodiče, a kontrola proti konci bufferu to nedokáže. Nízkoúrovňové DERReadTLV v PDFlibASN1 ohraničuje každý prvek celým řetězcem, což je správná kontrola pro nejvnější objekt a špatná pro všechno pod ním. Představte si SignerInfo, jehož issuerAndSerialNumber deklaruje 40 bajtů, zatímco issuer Name uvnitř si nárokuje 60. Každý bajt je pořád v bufferu, takže čtenář ohraničený bufferem Name přijme, sériové číslo pak přečte z navazujícího digest algoritmu a tenhle pár pak porovnává s vloženými certifikáty. Před v3.539.10 četl CMS walker přesně takhle. Opravou je malý wrapper, který nese koncovou pozici rodiče do každého čtení
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// uvnitř rodiče už nic nezbývá: čtení nezačínat
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset teď stojí jeden krok za prvkem; nesmí přesáhnout rodiče
Result := Offset <= ParentEnd;
end;
// každá úroveň si zapsala vlastní konec a předává ho dál:
// 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
Unit PDFlibCMSRead teď protahuje tyhle konce přes ContentInfo, wrapper [0] EXPLICIT, pole SignedData až po signerInfos, SignerIdentifier v obou podobách issuerAndSerialNumber i [0] subjectKeyIdentifier (RFC 5652 §5.3) a pole tbsCertificate čtená z každého vloženého certifikátu při párování podepisujícího. Uvnitř sady certifikátů i sady signerInfos prvek, který přeběhne za konec sady, zastaví smyčku: PLExtractCMSCertificates vrátí certifikáty, které už přijal, a bajty navazujících crls nebo signerInfos už nikdy neslepí na poslední z nich. Párování issuer-and-serial taky vyžaduje obě poloviny, protože sériové číslo je unikátní jen v rámci jednoho vystavitele
Proč vyšlo 2.999.3 jako 1.15.3?
První dva oblouky OID (arc) se sloučí do jednoho subidentifikátoru, ne do jednoho bajtu, a tenhle subidentifikátor se kóduje v base-128 jako každý jiný arc. X.690 §8.19.4 ho definuje jako 40 * arc1 + arc2; dřívější DER_OID zapsal tuhle hodnotu přes Byte(...), což je správně jen do 127, tedy do hodnoty 2.47. Pro 2.999 je součet 1079, přetypování na bajt ponechá 55 a 55 se dekóduje jako 1.15, takže identifikátor potichu pojmenuje úplně jinou větev stromu. Hodnoty od 128 do 255 selžou jinak — vypustí jeden bajt s nastaveným continuation bitem, který spolkne další arc. Většina PKI identifikátorů (1.2.840..., 2.5.29..., 0.4.0...) se hranice nikdy nedotkne, a proto vada přežila; oblouky joint-iso-itu-t od 2.48 výš se jí už dotknou. DER_OID slouží i kodéru signed attributes, i matcheru v DERFindExtensionByOID a kontrole content-type SignedData, takže špatné kódování rozbilo zápis i vyhledávání naráz
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.
Sloučená hodnota se schovává do UInt64 úmyslně. DER_OID parsuje oblouky do Int64, takže legální druhý arc může být klidně Int64.MaxValue a přičtení 80 pro arc1 = 2 přeteče znaménkové 64bitové celé číslo. UInt64 unese Int64.MaxValue + 80 bez přeložení a desetibajtový pomocný buffer pojme těch deset 7bitových skupin, které 64bitová hodnota potřebuje. Testovací vektory, které stojí za držení, jsou ty po obou stranách hranice: 2.47 musí zůstat jeden bajt a 2.48 se musí stát dvěma
Co čtecí CMS walker garantuje?
PDFlibCMSRead garantuje strukturu a nic víc: vrací bajty tam, kde je RFC 5652 chce mít, a neověřuje žádný podpis, digest ani dobu platnosti. Walker bere jen DER, takže DERReadTLV odmítne neurčité délky a vícebajtová čísla tagů a BER kódované CMS od nekonformního podepisujícího nahlásí nulový počet certifikátů místo částečného odhadu. Attribute certificates a ostatní alternativy CertificateChoices se přeskočí, protože žádný navazující kód je neumí použít. Kryptografické ověření necháváme kódu, kterému patří — začíná kontrolami pokrytí bajtů popsanými v PAdES podepisování a validaci ByteRange v Delphi a pokračuje tříděním toho, co se po podepsání PDF změnilo
Širší ponaučení platí pro jakýkoli binární formát: „uvnitř bufferu“ je vlastnost memory safety, „uvnitř rodiče“ je vlastnost správnosti a parser potřebuje obojí. Stejné myšlení o zákeřných délkách provází otužování Pascal parseru PDF proti zákeřným souborům. Extrakce certifikátů, stavba řetězu a API pro dlouhodobou validaci popsané tady sjíždějí s losLab PDF Library for Delphi pro Delphi, C++Builder a Lazarus