PDF Library for Delphi (PDFlibPas) izvlači certifikate unutar PDF potpisa čistim DER prolazom kroz CMS SignedData pohranjen u /Contents, bez ikakvog CryptoAPI-ja. Od v3.539.10 svako ugniježđeno čitanje ograničeno je roditeljskim elementom, padding od nula nakon CMS-a reže se na duljinu koju CMS sam deklarira, a identifikatori objekata kodiraju svoj udruženi prvi subidentifier u base-128. Pravilo granica i OID ispravak zamijenili su kod koji je davao pogrešne odgovore bez da je išta prijavio, a pravilo punjenja čuva strožeg čitača od odbijanja pravih potpisa
Strana čitanja važnija je no što izgleda. Alati za long-term validation moraju izvući certifikat potpisnika i njegove izdavatelje iz postojećeg potpisa prije nego što mogu dohvatiti podatke o opozivu, revizijsko izvješće mora reći tko je potpisao, a Lazarus build na Linuxu nema Windows message funkcija na koje bi se oslonio. Parser u tom položaju rijetko padne na lošem ulazu. Način kvara koji boli jest broj certifikata koji uključuje bajtove koje posjeduje susjed, poklapanje potpisnika s pogrešnim poljem ili OID koji tiho postane drugi OID. Potpisni pipeline sagrađen na tome javlja samouvjereno besmislice
Izvlačenje certifikata potpisnika iz potpisanog PDF-a
Pet metoda TPDFlib pokriva stranu čitanja, i sve primaju InputFile, Password, FieldName: svaki poziv otvara datoteku samo za čitanje, odgovori, pa je ponovno zatvori. GetSignatureEmbeddedCertificateCount i GetSignatureEmbeddedCertificateDER nabrajaju certifikate iz skupa u redoslijedu kodiranja, GetSignatureSignerCertificateDER vraća certifikat koji je proizveo dani SignerInfo, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER prolaze od tog potpisnika prema najdaljem izdavatelju koji sam potpis nosi. Indeksi su zero-based. Rezultate držite u AnsiString, zato ih i biblioteka tako vraća: DER blob proveden kroz string ili TStrings prolazi kroz pretvorbu znakovnog skupa i vrati se oštećen
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;
Dvije stvari u tom ispisu traže pažnju. Broj 0 nije dijagnoza: nepostojeće polje, pogrešna lozinka, blob koji nije DER i SignedData koji jednostavno izostavi neobavezni skup certifikata svi se vrate kao 0 ili prazan string, pa zapisujte ime polja uz broj. I lanac koji završi prije samopotpisanog certifikata nije ni pogreška. Graditelj lanca koristi samo certifikate ugrađene u potpis, pa preostale izdavatelje morate dohvatiti preko adresa koje prijavi GetCertificateIssuerURLs
Koliko je /Contents zapravo CMS?
Samo prefiks koji vanjski SEQUENCE deklarira pripada CMS-u, a PLTrimCMSPadding reže sve iza toga. Potpisnik rezervira /Contents hex string prije nego CMS uopće postoji, jer /ByteRange koji opisuje ISO 32000-1 §12.8.1 mora biti fiksiran prvi, pa je slot dimenzioniran velikodušno, a neiskorišteni rep su nule. PLTrimCMSPadding pročita prvi TLV, zahtijeva tag $30 i vraća bajtove do kraja tog elementa; sve što ne počinje dobro oblikovanim SEQUENCE-om vrati se prazno. Ta najviša razina jedino je mjesto gdje su zaostali bajtovi legalni, i ta razlika važi za idući odjeljak: strogo pravilo „element mora potrošiti cijeli buffer" odbilo bi svaki stvarni potpis, dok popustljivo pravilo primijenjeno na svakoj dubini dopušta ugniježđenim poljima da čitaju bajtove koji nisu njihovi
Zašto DER čitač treba završni offset roditelja?
Ugniježđeni element valjan je samo ako završava unutar svojeg roditelja, a provjera prema kraju buffera to ne dokazuje. Niskorazinski DERReadTLV u PDFlibASN1 ograničava svaki element prema cijelom stringu, što je ispravna provjera za najvanjski objekt i pogrešna za sve ispod njega. Zamislite SignerInfo čiji issuerAndSerialNumber deklarira 40 bajtova dok issuer Name unutar njega traži 60. Svaki je bajt i dalje u bufferu, pa će čitač ograničen bufferom prihvatiti Name, pročitati serijski broj iz digest algoritma koji slijedi, a onda taj par usporediti s ugrađenim certifikatima. Prije v3.539.10 CMS walker čitao je točno tako. Ispravak je mali wrapper koji krajnju poziciju roditelja nosi u svako čitanje
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// u roditelju više ničeg nema: odbij pokrenuti čitanje
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset sada stoji jedan iza elementa; ne smije prijeći roditelja
Result := Offset <= ParentEnd;
end;
// svaka razina pamti svoj kraj i predaje ga dalje:
// 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
Jedinica PDFlibCMSRead sada provlači te krajeve kroz ContentInfo, [0] EXPLICIT omotnicu, polja SignedData sve do signerInfos, SignerIdentifier u objema njegovim oblicima, issuerAndSerialNumber i [0] subjectKeyIdentifier (RFC 5652 §5.3), te polja tbsCertificate pročitana iz svakog ugrađenog certifikata pri poklapanju potpisnika. Unutar skupa certifikata i skupa signerInfos element koji prijeđe kraj skupa zaustavlja petlju: PLExtractCMSCertificates vrati certifikate koje je već prihvatila i nikad ne zalijepi sljedeće bajtove crls ili signerInfos na posljednji. Poklapanje issuer-and-serial traži i obje polovice, jer je serijski broj jedinstven samo unutar jednog izdavatelja
Zašto je 2.999.3 izašao kao 1.15.3?
Prva dva luka OID-a spajaju se u jedan subidentifier, ne u jedan bajt, i taj se subidentifier kodira u base-128 kao i svaki drugi luk. X.690 §8.19.4 definira ga kao 40 * arc1 + arc2; stariji DER_OID zapisivao je tu vrijednost s Byte(...), što je točno samo do 127, vrijednosti od 2.47. Za 2.999 zbroj je 1079, pretvorba u bajt zadrži 55, a 55 se dekodira kao 1.15, pa identifier tiho imenuje drugu granu stabla. Vrijednosti od 128 do 255 padaju drugačije, emitirajući jedan bajt s postavljenim continuation bitom koji proguta sljedeći luk. Većina PKI identifikatora (1.2.840..., 2.5.29..., 0.4.0...) nikad ne dosegne granicu, zato je i preživjela; joint-iso-itu-t lukovi od 2.48 naviše dosegnu je. DER_OID služi i enkoderu za signed attributes i matchera u DERFindExtensionByOID i provjeri SignedData content-typea, pa je pogrešno kodiranje slomilo i pisanje i pretraživanje
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.
Udružena vrijednost drži se u UInt64 namjerno. DER_OID parsira lukove u Int64, pa drugi luk može biti sve do Int64.MaxValue, a pribrojavanje 80 za arc1 = 2 overflowa predznačeni 64-bitni cijeli broj. UInt64 nosi Int64.MaxValue + 80 bez omotavanja, a desetobajtni pomoćni buffer drži deset 7-bitnih grupa koje 64-bitna vrijednost treba. Test vektori vrijedni čuvanja su oni s obje strane granice: 2.47 mora ostati jedan bajt, a 2.48 mora postati dva
Što jamči CMS walker na strani čitanja?
PDFlibCMSRead jamči strukturu i ništa drugo: vraća bajtove koji sjede tamo gdje kaže RFC 5652 i ne provjerava potpis, digest ni razdoblje valjanosti. Walker prima samo DER, pa DERReadTLV odbija indefinite duljine i višebajtne brojeve tagova, a BER-kodirani CMS od neskladnog potpisnika prijavi nula certifikata umjesto djelomične domišljaje. Certifikati atributa i ostale CertificateChoices alternative preskaču se jer ih ništa nizvodno ne može iskoristiti. Kriptografska verifikacija ostaje kod koda koji je posjeduje, pa počinje s provjerama pokrivenosti bajtova opisanim u PAdES potpisivanju i validaciji ByteRangea u Delphiju, a nastavlja se s razvrstavanjem što se promijenilo nakon što je PDF potpisan
Šira lekcija vrijedi za svaki binarni format: „unutar buffera" svojstvo je memory safetyja, „unutar roditelja" svojstvo je ispravnosti, a parser treba oboje. Isto razmišljanje o neprijateljskim duljinama provlači se kroz otvrđivanje Pascal PDF parsera protiv zlonamjernih datoteka. Izvlačenje certifikata, gradnja lanca i long-term validation API-ji o kojima ovdje biva riječi isporučuju se uz losLab PDF Library za Delphi, za Delphi, C++Builder i Lazarus