PDF Library for Delphi (PDFlibPas) izvlači sertifikate iz PDF potpisa čistim DER prolazom kroz CMS SignedData smešten u /Contents, bez ikakve uloge CryptoAPI-ja. Od v3.539.10 svako ugnježđeno čitanje ograđeno je roditeljskim elementom, nula-dopuna iza CMS-a seče se na dužinu koju sam CMS deklariše, a object identifieri koduju svoj objedinjeni prvi podidentifikator u base-128. Pravilo o granicama i OID popravka zamenile su kod koji je davao pogrešne odgovore bez podizanja greške, a pravilo o dopuni čuva strožijeg čitača od odbijanja pravih potpisa
Strana čitanja je važnija nego što izgleda. Alati za dugoročnu validaciju moraju prvo da izvuku sertifikat potpisnika i njegove izdavače iz postojećeg potpisa pre nego što dođu do podataka o opozivu, izveštaj revizije mora da kaže ko je potpisao, a Lazarus build na Linuxu nema Windows funkcije za poruke na koje bi se oslonio. Parser u takvoj poziciji retko pada na lošem ulazu. Bolni režim kvara je broj sertifikata koji uključuje bajtove pripadajuće komšiji, poklapanje potpisnika učinjeno na pogrešnom polju, ili OID koji tiho postane sasvim drugi OID. Potpisni pipeline izgrađen na takvoj osnovi javlja samouverene besmislice
Izvlačenje sertifikata potpisnika iz potpisanog PDF-a
Pet metoda TPDFlib-a pokriva stranu čitanja, i sve uzimaju InputFile, Password, FieldName: svaki poziv otvara fajl samo za čitanje, odgovori i ponovo ga zatvori. GetSignatureEmbeddedCertificateCount i GetSignatureEmbeddedCertificateDER nabrajaju sertifikate iz skupa u redosledu enkodovanja, GetSignatureSignerCertificateDER vraća sertifikat koji je proizveo dati SignerInfo, a GetSignatureCertificateChainLength / GetSignatureCertificateChainDER idu od tog potpisnika ka najdaljem izdavaču koji potpis sam nosi. Indeksi kreću od nule. Rezultate držite u AnsiString-u, i baš tako ih biblioteka i vraća: DER blob proveden kroz string ili TStrings prolazi kroz konverziju skupa znakova i vraća se pokvaren
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 stvari u tom izlazu traže pažnju. Broj 0 nije dijagnoza: nepostojeće polje, pogrešna lozinka, blob koji nije DER, i SignedData koji jednostavno izostavi opcioni skup sertifikata — sve se to vraća kao 0 ili prazan string, pa uz broj logujte i ime polja. I lanac koji se završi pre samoizdatog sertifikata takođe nije greška. Graditelj lanca koristi samo sertifikate ugrađene u potpis, pa preostale izdavače treba dovući preko adresa koje GetCertificateIssuerURLs prijavi
Koliki deo /Contents je zapravo CMS?
Samo prefiks koji spoljašnji SEQUENCE deklariše pripada CMS-u, i PLTrimCMSPadding seče sve iza njega. Potpisnik rezerviše /Contents heksadecimalni string pre nego što CMS uopšte postoji, jer /ByteRange opisan u ISO 32000-1 §12.8.1 mora prvo da bude fiksiran, pa je slot dimenzionisan darežljivo a neiskorišćeni rep su nule. PLTrimCMSPadding pročita prvi TLV, zahteva tag $30 i vraća bajtove do kraja tog elementa; sve što ne počinje korektno oblikovanim SEQUENCE-om vraća se kao prazno. Taj najviši nivo je jedino mesto gde su završni bajtovi legalni, i ta razlika znači za sledeći odeljak: strogo pravilo „element mora da potroši ceo bafer“ odbilo bi svaki potpis iz stvarnog sveta, dok labavo pravilo primenjeno na svakoj dubini pušta ugnježđena polja da čitaju bajtove koji nisu njihovi
Zašto DER čitaču treba završni offset roditelja?
Ugnježđeni element je validan samo ako se završava unutar svog roditelja, i provera protiv kraja bafera to ne dokazuje. Niskonivojni DERReadTLV u PDFlibASN1 ograđuje svaki element celim stringom, što je prava provera za najspoljašnji objekat a pogrešna za sve ispod njega. Zamislite SignerInfo čiji issuerAndSerialNumber deklariše 40 bajtova dok issuer Name unutar njega tvrdi 60. Svaki bajt je i dalje u baferu, pa čitač ograđen baferom prihvati Name, pročita serijski broj iz digest algoritma koji sledi, i onda taj par uporedi sa ugrađenim sertifikatima. Pre v3.539.10 CMS walker je čitao baš tako. Popravka je mali omotač koji u svako čitanje unosi krajnju poziciju roditelja
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// u roditelju ništa nije ostalo: odbij da započneš čitanje
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset je sada jedan iza elementa; ne sme preći roditelja
Result := Offset <= ParentEnd;
end;
// svaki nivo beleži svoj kraj i predaje ga nadole:
// 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 omotač, polja SignedData sve do signerInfos, SignerIdentifier u oba njegova oblika, issuerAndSerialNumber i [0] subjectKeyIdentifier (RFC 5652 §5.3), i polja tbsCertificate pročitana iz svakog ugrađenog sertifikata pri poklapanju potpisnika. Unutar skupa sertifikata i skupa signerInfos, element koji pređe kraj skupa zaustavlja petlju: PLExtractCMSCertificates vraća sertifikate koje je već prihvatio i nikada ne zalepi sledeće crls ili signerInfos bajtove na poslednji. Poklapanje issuer-and-serial takođe zahteva obe polovine, jer je serijski broj jedinstven samo unutar jednog izdavača
Zašto je 2.999.3 izašao kao 1.15.3?
Prva dva luka OID-a spajaju se u jedan podidentifikator, a ne u jedan bajt, i taj podidentifikator se base-128 koduje kao i svaki drugi luk. X.690 §8.19.4 definiše ga kao 40 * arc1 + arc2; stariji DER_OID upisivao je tu vrednost kroz Byte(...), što je tačno samo do 127, vrednosti od 2.47. Za 2.999 zbir je 1079, byte cast zadrži 55, a 55 se dekoduje kao 1.15, pa identifikator tiho imenuje sasvim drugu granu stabla. Vrednosti od 128 do 255 padaju drugačije, emitujući jedan bajt sa postavljenim continuation bitom koji proguta sledeći luk. Većina PKI identifikatora (1.2.840..., 2.5.29..., 0.4.0...) nikada ne dodirne tu granicu, pa je i preživela; joint-iso-itu-t lukovi od 2.48 naviše je dodiruju. DER_OID služi i enkoderu za signed attributes i tražilici u DERFindExtensionByOID i proveri SignedData content-type-a, pa je pogrešno kodovanje 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.
Objedinjena vrednost se drži u UInt64-u sasvim namerno. DER_OID parsira lukove u Int64, pa zakonit drugi luk može biti i do Int64.MaxValue, a dodavanje 80 za arc1 = 2 prekorači potpisani 64-bitni integer. UInt64 nosi Int64.MaxValue + 80 bez prelivanja, a desetobajtni pomoćni bafer drži deset 7-bitnih grupa koliko 64-bitna vrednost traži. Test vektori vredni čuvanja su oni s obe strane granice: 2.47 mora da ostane jedan bajt, a 2.48 mora da postane dva
Šta garantuje CMS walker na strani čitanja?
PDFlibCMSRead garantuje strukturu i ništa više: vraća bajtove koji stoje tamo gde RFC 5652 kaže da treba da stoje i ne proverava potpis, digest ni period validnosti. Walker prima samo DER, pa DERReadTLV odbija indefinite dužine i višebajtne brojeve tagova, a BER-kodovan CMS od nenormativnog potpisnika javlja nula sertifikata umesto delimične pretpostavke. Atribute sertifikati i ostale CertificateChoices alternative preskaču se jer ih niko nizvodno ne može iskoristiti. Kriptografska verifikacija ostaje kod koda koji je poseduje: počinje proverama prekrivenosti bajtova opisanim u tekstu o PAdES potpisivanju i ByteRange validaciji u Delphiju i nastavlja se sa klasifikacijom onoga što se promenilo nakon što je PDF potpisan
Šira lekcija važi za svaki binarni format: „unutar bafera“ je osobina memory safety-ja, „unutar roditelja“ je osobina ispravnosti, i parseru treba obe. Isto razmišljanje o neprijateljskim dužinama prolazi kroz tekst o ojačavanju Pascal PDF parsera protiv zlonamernih fajlova. API-ji za izvlačenje sertifikata, izgradnju lanca i dugoročnu validaciju razmatrani ovde isporučuju se uz losLab PDF Library for Delphi, za Delphi, C++Builder i Lazarus