A PDF Library for Delphi (PDFlibPas) a PDF-aláírásban tárolt tanúsítványokat a /Contents-ban lakó CMS SignedData tisztán DER-bejárással nyeri ki, CryptoAPI nélkül. A v3.539.10 óta minden egymásba ágyazott olvasást a szülőeleme határol, a CMS utáni nulla padding a CMS által deklarált hossznál vágódik le, és az objektumazonosítók az összefésült első szubidentifikátorukat base-128-ban kódolják. A határszabály és az OID-javítás is olyan kódot váltott fel, ami hibádobás nélkül produkált rossz válaszokat, a padding-szabály pedig megakadályozza, hogy a szigorúbb olvasó elutasítsa a valódi aláírásokat
Az olvasó oldal fontosabb, mint amennyire látszik. A long-term validation eszköznek ki kell nyernie az aláíró tanúsítványát és a kibocsátóit egy meglévő aláírásból, mielőtt visszavonási adatot tudna szerezni, egy audit riportnak meg kell mondania, ki írt alá, a Linuxon fordított Lazarus buildnek pedig nincs mire támaszkodnia Windows üzenetfüggvények terén. Egy ebben a pozícióban ülő parser rossz bemeneten ritkán bukik el hangosan. A fájó hibamód az, amikor a tanúsítványszámláló beleszámol egy szomszéd bájtjaiba, az aláíróját egy rossz mezőhöz hasonlítják, vagy egy OID csendben másik OID-dá változik. Erre épülő aláírás-feldolgozó lánc magabiztosan jelent értelmetlenségeket
Az aláíró tanúsítványok kiolvasása egy aláírt PDF-ből
Öt TPDFlib metódus fedi le az olvasó oldalt, és mindegyik átveszi az InputFile, Password, FieldName paramétereket: minden hívás csak olvasásra nyitja meg a fájlt, válaszol, majd bezárja. A GetSignatureEmbeddedCertificateCount és a GetSignatureEmbeddedCertificateDER a tanúsítványhalmazt sorolja fel kódolási sorrendben, a GetSignatureSignerCertificateDER azt a tanúsítványt adja vissza, amiből egy adott SignerInfo született, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER páros pedig ettől az aláírótól indul el az aláírás által hordozott legtávolabbi kibocsátó felé. Az indexek nullától számolnak. Az eredményt AnsiString-ben tartsd, ezért adja vissza őket a library is így: egy string-en vagy TStrings-en áteresztett DER blob karakterkészlet-konverzión megy át, és sérülten jön vissza
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;
Két dologra kell figyelni ebben a kimenetben. A 0-s darabszám nem diagnózis: hiányzó mező, rossz jelszó, nem DER blob, és egy SignedData, ami egyszerűen elhagyja az opcionális tanúsítványhalmazt, mind 0-t vagy üres stringet ad vissza, ezért a szám mellé írd be a mezőnevet. És az a lánc sem hiba, ami önmagát kibocsátó tanúsítvány előtt ér véget. A láncépítő csak az aláírásba ágyazott tanúsítványokat használja, így a maradék kibocsátókat azokon a címeken kell behúzni, amiket a GetCertificateIssuerURLs jelent
A /Contents mekkora része valójában a CMS?
Csak az a prefix tartozik a CMS-hez, amit a külső SEQUENCE deklarál, és a PLTrimCMSPadding minden mást levág utána. Az aláíró már a CMS létezése előtt lefoglalja a /Contents hex stringet, mert az ISO 32000-1 §12.8.1-e szerinti /ByteRange-et előbb rögzíteni kell, így a hely bőven van méretezve, és a fel nem használt farok nullákból áll. A PLTrimCMSPadding kiolvassa az első TLV-t, megköveteli a $30 taget, és az elem végéig adja vissza a bájtokat; ami nem jól formált SEQUENCE-zel indul, az üresen jön vissza. A felső szint az egyetlen hely, ahol a feleslegesen ottmaradt bájtok legálisak, és ez a különbség a következő szakasz szempontjából is fontos: egy szigorú „az elemnek a teljes buffert el kell fogyasztania" szabály minden valós aláírást elutasítana, egy minden mélységben engedékeny szabály viszont hagyná, hogy az egymásba ágyazott mezők olyan bájtokat olvassanak, amik nem az övéik
Miért kell egy DER olvasónak a szülő végoffsetje?
Egy egymásba ágyazott elem csak akkor érvényes, ha a szülőjén belül ér véget, és ezt a buffer végéhez viszonyított ellenőrzés nem bizonyítja. A PDFlibASN1 alacsony szintű DERReadTLV-je minden elemet a teljes stringhez mér, ami a legkülső objektumnál helyes, minden attól lejjebbihez képest rossz. Képzelj el egy SignerInfo-t, aminek az issuerAndSerialNumber-je 40 bájtot deklarál, miközben a benne lakó issuer Name 60-at állít. Minden bájt még mindig a bufferben van, így egy bufferhez kötött olvasó elfogadja a Name-et, a sorozatszámot a következő digest algoritmusából olvassa ki, majd ezt a párt hasonlítja a beágyazott tanúsítványokhoz. A v3.539.10 előtt a CMS-bejáró pontosan így olvasott. A javítás egy kis wrapper, ami minden olvasásba beleviszi a szülő végpozícióját
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// a szülőn belül nem maradt semmi: nem indít olvasást
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// az Offset most az elemen eggyel túl ül; nem mehet át a szülőn
Result := Offset <= ParentEnd;
end;
// minden szint feljegyzi a saját végét, és lefelé adja:
// OuterEnd := a ContentInfo vége (RFC 5652 3. szakasz)
// ExplicitEnd := a content [0] EXPLICIT vége
// ContentEnd := a SignedData vége (RFC 5652 5.1. szakasz)
// SignerEnd / InnerEnd a SignerInfo-hoz és az issuerAndSerialNumber-hoz
A PDFlibCMSRead unit ezeknek a végeknek a menetét mostantól végigviszi a ContentInfón, a [0] EXPLICIT csomagoláson, a signerInfos-ig tartó SignedData mezőkön, a SignerIdentifier mindkét alakján, az issuerAndSerialNumber-en és a [0] subjectKeyIdentifier-en (RFC 5652 §5.3), valamint az aláíró párosítása során minden beágyazott tanúsítványból kiolvasott tbsCertificate mezőkön. A tanúsítványhalmazon és a signerInfos halmazon belül egy olyan elem, ami túlcsúszik a halmaz végén, megállítja a ciklust: a PLExtractCMSCertificates visszaadja azokat a tanúsítványokat, amiket már elfogadott, és sosem ragasztja az utolsóra a következő crls vagy signerInfos bájtokat. Az issuer-and-serial párosításhoz mindkét fele kell, mert egy sorozatszám csak egy kibocsátón belül egyedi
Miért lett a 2.999.3-ból 1.15.3?
Egy OID első két íve egyetlen szubidentifikátorrá áll össze, nem egy bájttá, és ez a szubidentifikátor ugyanúgy base-128-ban kódolódik, mint a többi ív. Az X.690 §8.19.4-e 40 * arc1 + arc2-ként definiálja; a korábbi DER_OID ezt az értéket Byte(...)-tal írta ki, ami csak 127-ig, a 2.47 értékéig helyes. A 2.999 esetén az összeg 1079, a bájtos cast 55-et tart meg, az 55 pedig 1.15-ként dekódolódik, így az azonosító csendben a fa egy másik ágát nevezi meg. A 128 és 255 közti értékek máshogy buknak el: egy bájtot bocsátanak ki bekapcsolt folytatási bittel, ami lenyeli a következő ívet. A legtöbb PKI azonosító (1.2.840..., 2.5.29..., 0.4.0...) sose ér a határig, ezért maradhatott észrevétlen a hiba; a joint-iso-itu-t ívek 2.48-tól felfelé igen. A DER_OID az aláírt attribútumok kódolóját és a DERFindExtensionByOID matcherét, valamint a SignedData content-type ellenőrzését is kiszolgálja, így egy rossz kódolás egyszerre törte el az írást és a keresést
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.
Az összefésült érték szándékosan UInt64-ben lakik. A DER_OID az íveket Int64-be parseolja, így egy legális második ív Int64.MaxValue nagyságú is lehet, és a 80 hozzáadása arc1 = 2 esetén túlcsordít egy előjeles 64 bites egészre. A UInt64 átbírja a Int64.MaxValue + 80-ot csordulás nélkül, és a tíz bájtos segédbuffer befogadja azokat a tíz 7 bites csoportot, amik egy 64 bites értéknek kellenek. Megőrzésre érdemes tesztvektorok a határ két oldalán állók: a 2.47-nek egy bájton kell maradnia, a 2.48-nak kettővé kell válnia
Mit garantál az olvasó oldali CMS-bejáró?
A PDFlibCMSRead a struktúrát garantálja és semmi mást: olyan bájtokat ad vissza, amik pontosan ott ülnek, ahová az RFC 5652 teszi őket, és egyetlen aláírást, digestet vagy érvényességi időszakot sem ellenőriz. A bejáró csak DER-t fogad el, így a DERReadTLV elutasítja a határozatlan hosszakat és a több bájtos tag számokat, egy nem megfelelő aláírótól származó, BER-kódolású CMS pedig részleges találgatás helyett zérus tanúsítványt jelent. Az attribútumtanúsítványokat és a CertificateChoices többi alternatíváját kihagyja, mert semmi a lefolyásban nem tudja őket hasznosítani. A kriptográfiai ellenőrzés annál a kódnál marad, amié: ez indul a PAdES aláírás és ByteRange validáció Delphiben cikkben írt bájt-lefedettségi ellenőrzésekkel, és folytatódik a PDF aláírása után megváltozott tartalom osztályozásával
A tágabb tanulság minden bináris formátumra átvihető: a „bufferben van" memóriabiztonsági tulajdonság, a „szülőn belül van" helyességi tulajdonság, és egy parsernek mindkettő kell. Ugyanez a gondolkodásmód az ellenséges hosszakról fut végig a Pascal PDF parser rosszindulatú fájlok elleni megerősítésében. Az itt tárgyalt tanúsítványkinyerési, láncépítési és long-term validation API-k a Delphire, C++Builderre és Lazarusra szánt losLab PDF Library for Delphi részeként kerülnek kiadásra