PDF Library for Delphi (PDFlibPas) vytiahne certifikáty vnútri PDF podpisu čistým DER prechodom cez CMS SignedData uložené v /Contents, bez akéhokoľvek zapojenia CryptoAPI. Od v3.539.10 je každé vnorené čítanie ohraničené svojím rodičovským elementom, nulový padding za CMS sa oreže na dĺžku, ktorú CMS deklaruje, a objektové identifikátory kódujú svoj zlúčený prvý subidentifikátor v base-128. Pravidlo hraníc aj oprava OID nahradili kód, ktorý vyrábal nesprávne výsledky bez vyhodenia chyby, a pravidlo paddingu bráni prísnejšiemu čítaču, aby odmietol reálne podpisy
Čítacia strana je dôležitejšia, než vyzerá. Nástroje dlhodobej validácie musia vytiahnuť certifikát podpisovateľa a jeho vystaviteľov z existujúceho podpisu skôr, než siahnu po revokačných dátach, audit report musí povedať, kto podpísal, a Lazarus zostavenie na Linuxe sa nemá o čo oprieť v podobe Windows message funkcií. Parser v takejto pozícii na zlom vstupe málokedy spadne. Režim zlyhania, ktorý naozaj bolí, je počet certifikátov zahŕňajúci bajty patriace susedovi, zhoda podpisovateľa nájdená v nesprávnom poli alebo OID, ktoré sa potichu zmení na iné OID. Podpisová pipeline postavená na takomto základe hlási sebaistý nesmysel
Čítanie certifikátov podpisovateľa z podpísaného PDF
Čítaciu stranu pokrýva päť metód TPDFlib a všetky berú InputFile, Password, FieldName: každé volanie otvorí súbor len na čítanie, odpovie a znovu ho zavrie. GetSignatureEmbeddedCertificateCount a GetSignatureEmbeddedCertificateDER vymenúvajú certifikáty v sade v poradí kódovania, GetSignatureSignerCertificateDER vráti certifikát, ktorý vytvoril daný SignerInfo, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER prechádzajú od podpisovateľa k najvzdialenejšiemu vystaviteľovi, ktorého podpis sám nesie. Indexy sa číslujú od nuly. Výsledky držte v AnsiString, presne preto ich knižnica takto aj vracia: DER blob putujúci cez string alebo TStrings prejde konverziou znakovej sady a vráti sa poškodený
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;
Dve veci v tomto výstupe si zaslúžia pozornosť. Počet 0 nie je diagnóza: chýbajúce pole, nesprávne heslo, blob, ktorý nie je DER, aj SignedData, ktoré jednoducho vynechá voliteľnú sadu certifikátov, sa vrátia všetky ako 0 alebo prázdny reťazec, takže si vedľa čísla logujte názov poľa. Ani reťaz, ktorá skončí skôr než narazí na self-issued certifikát, nie je chyba. Staviteľ reťazu používa len certifikáty vložené v podpise, takže zostávajúcich vystaviteľov treba dohľadať cez adresy, ktoré nahlási GetCertificateIssuerURLs
Koľko z /Contents je vlastne CMS?
CMS patrí len prefix, ktorý deklaruje vonkajší SEQUENCE, a PLTrimCMSPadding odreže všetko za ním. Podpisovateľ rezervuje hex reťazec /Contents skôr, než CMS vôbec existuje, pretože /ByteRange popísaný v ISO 32000-1 §12.8.1 sa musí stanoviť ako prvý, takže slot má veľkorysú veľkosť a nepoužitý chvost tvoria nuly. PLTrimCMSPadding prečíta prvú TLV, vyžaduje tag $30 a vráti bajty až po koniec tohto elementu; čokoľvek, čo nezačína korektne utvoreným SEQUENCE, sa vráti prázdne. Táto najvyššia úroveň je jediné miesto, kde sú koncové bajty legálne, a toto rozlíšenie má význam pre ďalšiu sekciu: prísne pravidlo „element musí spotrebovať celý buffer“ by odmietlo každý reálny podpis, zatiaľ čo laxné pravidlo aplikované v každej hĺbke nechá vnorené polia čítať bajty, ktoré nevlastnia
Prečo potrebuje DER čítač koncový offset rodiča?
Vnorený element je platný len vtedy, keď končí vnútri svojho rodiča, a kontrola proti koncu bufferu to nedokáže dokázať. Nízkourovnňový DERReadTLV v PDFlibASN1 ohraničuje každý element celým reťazcom, čo je správna kontrola pre najvonkajší objekt a nesprávna pre všetko pod ním. Predstavte si SignerInfo, ktorého issuerAndSerialNumber deklaruje 40 bajtov, kým issuer Name vnútri neho nárokuje 60. Každý bajt je stále v bufferi, takže čítač ohraničený bufferom Name prijme, sériové číslo prečíta z digest algoritmu, ktorý nasleduje, a túto dvojicu potom porovná s vloženými certifikátmi. Pred v3.539.10 čítal CMS walker presne takto. Opravou je malý wrapper, ktorý vnáša koncovú pozíciu rodiča do každého čítania
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// v rodičovi nič neostalo: čítanie nezačínať
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset je teraz jeden za elementom; nesmie prekročiť rodiča
Result := Offset <= ParentEnd;
end;
// každá úroveň si zaznamená svoj koniec a odovzdá ho ďalej:
// OuterEnd := koniec ContentInfo (RFC 5652 sekcia 3)
// ExplicitEnd := koniec obsahu [0] EXPLICIT
// ContentEnd := koniec SignedData (RFC 5652 sekcia 5.1)
// SignerEnd / InnerEnd pre SignerInfo a issuerAndSerialNumber
Jednotka PDFlibCMSRead teraz prevlecie tie konce cez ContentInfo, wrapper [0] EXPLICIT, polia SignedData až po signerInfos, SignerIdentifier v oboch podobách issuerAndSerialNumber aj [0] subjectKeyIdentifier (RFC 5652 §5.3) a polia tbsCertificate čítané z každého vloženého certifikátu pri pátraní po podpisovateľovi. Vnútri sady certifikátov a sady signerInfos zastaví slučku element prebiehajúci za koniec sady: PLExtractCMSCertificates vráti certifikáty, ktoré už prijala, a nasledujúce bajty crls či signerInfos nikdy nalepí na posledný. Zhoda issuer-and-serial navyše vyžaduje obe polovice, keďže sériové číslo je unikátne len v rámci jedného vystaviteľa
Prečo vyšlo 2.999.3 ako 1.15.3?
Prvé dve vetvy OID sa zlučujú do jedného subidentifikátora, nie do jedného bajtu, a ten sa kóduje v base-128 ako každá iná vetva. X.690 §8.19.4 ho definuje ako 40 * arc1 + arc2; skorší DER_OID zapisoval túto hodnotu cez Byte(...), čo sedí len do 127, teda po hodnotu 2.47. Pre 2.999 vyjde súčet 1079, pretypovanie na bajt nechá 55 a 55 sa dekóduje ako 1.15, takže identifikátor potichu pomenová inú vetvu stromu. Hodnoty od 128 do 255 zlyhávajú inak: emitujú jeden bajt s nastaveným continuation bitom, ktorý prehltne ďalšiu vetvu. Väčšina PKI identifikátorov (1.2.840..., 2.5.29..., 0.4.0...) sa k hranici nikdy nedostane, preto defekt prežil; vetvy joint-iso-itu-t od 2.48 nahor sa dostanú. DER_OID obsluhuje aj kódovač podpísaných atribútov, aj vyhľadávač v DERFindExtensionByOID a kontrolu content-type SignedData, takže zlé kódovanie rozbilo zápis aj vyhľadávanie naraz
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.
Zlúčená hodnota sa v UInt64 drží zámerne. DER_OID parsuje vetvy do Int64, takže legálna druhá vetva môže mať až Int64.MaxValue a pripočítanie 80 pre arc1 = 2 pretečie znamienkové 64-bitové celé číslo. UInt64 unesie Int64.MaxValue + 80 bez pretičenia a desaťbajtový pracovný buffer pojme desať 7-bitových skupín, ktoré 64-bitová hodnota potrebuje. Testovacie vektory, ktoré stoja za udržanie, sú tie po oboch stranách hranice: 2.47 musí ostať jeden bajt a 2.48 sa musí stať dvoma
Čo garantuje čítací CMS walker?
PDFlibCMSRead garantuje štruktúru a nič viac: vracia bajty tam, kde ich RFC 5652 očakáva, a neoveruje žiadny podpis, digest ani dobu platnosti. Walker prijíma len DER, takže DERReadTLV odmietne neurčité dĺžky a viacbajtové čísla tagov a BER kódované CMS od nekonformného podpisovateľa nahlási ako nula certifikátov, nie čiastočný tip. Certifikáty atribútov a ďalšie alternatívy CertificateChoices sa preskočia, lebo nič downstream ich nevyužije. Kryptografické overenie ostáva pri kóde, ktorému patrí: začína kontrolami pokrytia bajtov popísanými v článku o PAdES podpisovaní a validácii ByteRange v Delphi a pokračuje triedením toho, čo sa po podpísaní PDF zmenilo
Širšie poučenie platí pre každý binárny formát: „vnútri bufferu“ je vlastnosť memory safety, „vnútri rodiča“ je vlastnosť korektnosti a parser potrebuje oboje. Rovnaké uvažovanie o nepriateľských dĺžkach prebieha článkom o posilňovaní Pascal PDF parsera proti škodlivým súborom. Extrakcia certifikátov, stavba reťazov a API dlhodobej validácie popísané tu sa dodávajú s losLab PDF Library for Delphi pre Delphi, C++Builder aj Lazarus