PDF Library for Delphi (PDFlibPas) izvleče certifikate znotraj PDF podpisa s čistim sprehodom po DER čez CMS SignedData, shranjen v /Contents, brez CryptoAPI. Od v3.539.10 je vsako ugnezdeno branje omejeno s svojim starševskim elementom, ničelno oblazinjenje za CMS je obrezano na dolžino, ki jo CMS razglasi, identifikatorji objektov pa kodirajo svoj združeni prvi podidentifikator v base-128. Pravilo meja in popravek OID sta oba zamenjala kodo, ki je dajala napačne odgovore brez sprožitve napake, pravilo oblazinjenja pa preprečuje, da bi strožji bralnik zavrnil prave podpise
Stran branja je pomembnejša, kot se zdi. Orodja za dolgoročno validacijo morajo iz obstoječega podpisa izvleči podpisnikov certifikat in njegove izdajatelje, preden lahko poberejo podatke o preklicu, revizijsko poročilo mora povedati, kdo je podpisal, izgradnja z Lazarusom na Linuxu pa nima na razpolago Windows funkcij za sporočila. Razčlenjevalnik na takem mestu ob slabem vhodu le redko crkne. Način odpovedi, ki boli, je števec certifikatov, ki vključuje bajte sosednjega elementa, ujemanje podpisnika z napačnim poljem ali OID, ki se tiho spremeni v drug OID. Podpisni cevovod, zgrajen na tem, poroča samozavestne nesmisle
Branje podpisnikovih certifikatov iz podpisanega PDF
Pet metod TPDFlib pokriva stran branja, vse sprejmejo InputFile, Password, FieldName: vsak klic odpre datoteko samo za branje, odgovori in jo spet zapre. GetSignatureEmbeddedCertificateCount in GetSignatureEmbeddedCertificateDER naštejeta množico certifikatov v vrstnem redu kodiranja, GetSignatureSignerCertificateDER vrne certifikat, ki je ustvaril dani SignerInfo, GetSignatureCertificateChainLength in GetSignatureCertificateChainDER pa sprehodita od tega podpisnika do najbolj oddaljenega izdajatelja, ki ga podpis sam nosi. Kazala so od nič. Rezultate hranite v AnsiString, zato jih knjižnica ravno tako vrača: DER paket, poslan skozi string ali TStrings, gre skozi pretvorbo nabora znakov in pride nazaj pokvarjen
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 v tem izpisu zahtevata pozornost. Števec 0 ni diagnoza: manjkajoče polje, napačno geslo, paket, ki ni DER, in SignedData, ki preprosto izpusti neobvezno množico certifikatov, vsi vrnejo 0 ali prazen niz, zato zabeležite ime polja poleg številke. Tudi veriga, ki se konča, preden doseže samododeljeni certifikat, ni napaka. Gradnik verige uporablja le certifikate, vdelane v podpis, preostale izdajatelje pa je treba pobrati prek naslovov, ki jih poroča GetCertificateIssuerURLs
Koliko od /Contents je sploh CMS?
CMS pripada le predpona, ki jo razglasi zunanji SEQUENCE, PLTrimCMSPadding pa vse za njo obreže. Podpisnik rezervira šestnajstiški niz /Contents, preden CMS sploh obstaja, ker mora biti /ByteRange, opisan v ISO 32000-1 §12.8.1, določen najprej, zato je reža dimenzionirana velikodušno, neuporabljeni rep pa so ničle. PLTrimCMSPadding prebere prvi TLV, zahteva oznako $30 in vrne bajte do konca tega elementa; vse, kar se ne začne z dobro oblikovanim SEQUENCE, pride nazaj prazno. Ta vrhnja raven je edini kraj, kjer so bajti na koncu zakoniti, in razlikovanje je pomembno za naslednji razdelek: strogo pravilo »element mora pojesti cel medpomnilnik« bi zavrnilo vsak resnični podpis, popustljivo pravilo, uporabljeno na vsaki globini, pa ugnezdenim poljem dovoli branje bajtov, ki niso njihovi
Zakaj bralnik DER potrebuje končni odmik starša?
Ugnezderni element je veljaven le, če se konča znotraj svojega starša, preizkus proti koncu medpomnilnika tega ne dokaže. Nizko-nivojski DERReadTLV v PDFlibASN1 omeji vsak element s celotnim nizom, kar je pravi preizkus za najbolj zunanji objekt in napačen za vse pod njim. Predstavljajte si SignerInfo, katerega issuerAndSerialNumber razglasi 40 bajtov, medtem ko ime izdajatelja znotraj njega zahteva 60. Vsak bajt je še vedno v medpomnilniku, zato bralnik, omejen z medpomnilnikom, sprejme ime, prebere serijsko številko iz algoritma povzetka, ki mu sledi, in nato primerja ta par z vdelanimi certifikati. Pred v3.539.10 je sprehod po CMS bral točno tako. Popravek je majhna ovijalka, ki prenese končni položaj starša v vsako branje
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// znotraj starša ni ničesar več: branje se ne sme niti začeti
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset zdaj sedi eno za elementom; ne sme prečkati starša
Result := Offset <= ParentEnd;
end;
// each level records its own end and hands it down:
// 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
Enota, PDFlibCMSRead, zdaj prede te konce skozi ContentInfo, ovijalko [0] EXPLICIT, polja SignedData do signerInfos, SignerIdentifier v obeh oblikah issuerAndSerialNumber in [0] subjectKeyIdentifier (RFC 5652 §5.3) ter polja tbsCertificate, prebrana iz vsakega vdelanega certifikata pri ujemanju podpisnika. Znotraj množice certifikatov in množice signerInfos element, ki steče čez konec množice, ustavi zanko: PLExtractCMSCertificates vrne certifikate, ki jih je že sprejel, naslednjih bajtov crls ali signerInfos pa nikoli ne zalepi na zadnjega. Ujemanje izdajatelja in serije zahteva tudi obe polovici, saj je serijska številka unikatna le znotraj enega izdajatelja
Zakaj je 2.999.3 izšel kot 1.15.3?
Prva dva loka OID se združita v en podidentifikator, ne v en bajt, ta podidentifikator pa je kodiran v base-128 kot vsak drug lok. X.690 §8.19.4 ga definira kot 40 * arc1 + arc2; starejši DER_OID je to vrednost zapisal s Byte(...), kar je pravilno le do 127, vrednosti 2.47. Za 2.999 je vsota 1079, pretvorba v bajt obdrži 55, 55 pa se dekodira kot 1.15, zato identifikator tiho poimenuje drugo vejo drevesa. Vrednosti od 128 do 255 odpovejo drugače: izdajo en bajt z nastavljenim nadaljevalnim bitom, ki pogoltne naslednji lok. Večina identifikatorjev PKI (1.2.840..., 2.5.29..., 0.4.0...) meje nikoli ne doseže, zato je hrošč preživel; loki joint-iso-itu-t od 2.48 navzgor pa jo dosežejo. DER_OID služi tako kodirniku za podpisane atribute kot primerjevalniku v DERFindExtensionByOID in preizkusu vrste vsebine SignedData, zato je napačno kodiranje pokvarilo pisanje in iskanje hkrati
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.
Združena vrednost je namerno shranjena v UInt64. DER_OID razčlenjuje loke v Int64, zato je lahko zakonit drugi lok velik kot Int64.MaxValue, prištevanje 80 za arc1 = 2 pa prelije predznačeno 64-bitno celo število. UInt64 prenese Int64.MaxValue + 80 brez prevrta, deset-bajtni pomožni medpomnilnik pa drži deset 7-bitnih skupin, ki jih 64-bitna vrednost potrebuje. Preizkusni vektorji, vredni hranjenja, so tisti na obeh straneh meje: 2.47 mora ostati en bajt, 2.48 pa mora postati dva
Kaj zagotavlja CMS sprehod na strani branja?
PDFlibCMSRead zagotavlja strukturo in nič drugega: vrne bajte, ki ležijo tam, kjer pravi RFC 5652, podpisa, povzetka ali obdobja veljavnosti pa ne preverja. Sprehod sprejme samo DER, zato DERReadTLV odkloni nedoločene dolžine in več-bajtne številke oznak, CMS, kodiran v BER od nedoslednega podpisnika, pa poroča nič certifikatov namesto delnega ugibanja. Certifikati atributov in ostale alternative CertificateChoices so preskočene, ker jih nič nadalje ne more uporabiti. Kriptografsko preverjanje ostane pri kodi, ki je njegova last, ta pa se začne s preizkusi pokritosti bajtov, opisanimi v podpisovanju PAdES in validaciji ByteRange v Delphiju, in nadaljuje z razvrščanjem tega, kaj se je spremenilo po podpisu PDF
Širša lekcija velja za vsak dvojiški format: »znotraj medpomnilnika« je lastnost varnosti pomnilnika, »znotraj starša« je lastnost pravilnosti, razčlenjevalnik pa potrebuje oboje. Isto razmišljanje o sovražnih dolžinah prežema utrjevanje razčlenjevalnika PDF v Pascalu pred zlonamernimi datotekami. API za izvlečenje certifikatov, gradnjo verig in dolgoročno validacijo, obravnavani tukaj, so del losLab PDF Library for Delphi za Delphi, C++Builder in Lazarus