PDF Library for Delphi (PDFlibPas) poimii PDF-allekirjoituksen sisällä olevat sertifikaatit puhtaalla DER-kävelyllä /Contentsissa säilytetyn CMS SignedData -rakenteen läpi ilman CryptoAPIa. Versiosta v3.539.10 alkaen jokainen sisäkkäinen luku rajautuu vanhempaan alkioonsa, CMS:n jälkeinen nollatäyte katkaistaan siinä kohdassa, jonka CMS ilmoittaa pituudekseen, ja objektitunnisteet koodaavat yhdistetyn ensimmäisen alitunnisteensa base-128-koodauksella. Rajaussääntö ja OID-korjaus korvasivat molemmat koodia, joka tuotti vääriä vastauksia nostamatta virhettä, ja täytteen karsiminen pitää tiukemman lukijan hylkäämästä oikeita allekirjoituksia
Lukupuoli on tärkeämpi kuin näyttää. Pitkän aikavälin validointityökalun on poimittava allekirjoittajan sertifikaatti ja sen myöntäjät olemassa olevasta allekirjoituksesta ennen kuin se voi hakea mitätöintitietoja, auditointiraportin on kerrottava kuka allekirjoitti, ja Linuxille rakennettu Lazarus-versio ei nojaa mihinkään Windows-viestifunktioon. Jäsennin tuossa asemassa kaatuu harvoin huonoon syötteeseen. Kipeä vikatila on sertifikaattilukumäärä, johon kuuluu naapurille kuuluvia tavuja, allekirjoittajan täsmäytys väärää kenttää vasten tai OID, joka hiljaa muuttuu toiseksi OID:ksi. Sen varaan rakennettu allekirjoitusputki raportoi itsevarmasti täyttä roskaa
Allekirjoittajan sertifikaattien lukeminen allekirjoitetusta PDF:stä
Viisi TPDFlib-metodia kattaa lukupuolen, ja kaikille niistä annetaan InputFile, Password, FieldName: jokainen kutsu avaa tiedoston vain luku -tilassa, vastaa ja sulkee sen taas. GetSignatureEmbeddedCertificateCount ja GetSignatureEmbeddedCertificateDER luettelevat sertifikaattijoukon koodausjärjestyksessä, GetSignatureSignerCertificateDER palauttaa sertifikaatin, joka tuotti kyseisen SignerInfon, ja GetSignatureCertificateChainLength / GetSignatureCertificateChainDER kävelevät kyseisestä allekirjoittajasta kohti kauimmaista myöntäjää, jonka allekirjoitus itse mukanaan kantaa. Indeksit alkavat nollasta. Pidä tulokset AnsiStringissä, ja juuri siksi kirjasto palauttaa ne sellaisena: DER-lohko, joka kulkee stringin tai TStringsin kautta, menee merkistömuunnoksen läpi ja palaa turmeltuneena
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;
Tuossa tulosteessa on kaksi kohtaa, jotka vaativat huomiota. Luku 0 ei ole diagnoosi: puuttuva kenttä, väärä salasana, ei-DER-lohko ja SignedData, joka jättää valinnaisen sertifikaattijoukon kokonaan pois, palautuvat kaikki arvona 0 tai tyhjänä merkkijonona, joten kirjaa kentän nimi luvun viereen. Eikä virhe ole myöskään se, että ketju päättyy ennen self-issued-sertifikaattia. Ketjun rakentaja käyttää vain allekirjoitukseen upotettuja sertifikaatteja, joten loput myöntäjät on noudettava osoitteista, jotka GetCertificateIssuerURLs raportoi
Kuinka suuri osa /Contentsista oikeasti on CMS:ää?
CMS:ään kuuluu vain se etuliite, jonka uloin SEQUENCE ilmoittaa, ja PLTrimCMSPadding katkaisee kaiken sen jälkeen. Allekirjoittaja varaa /Contents-heksamerkkijonon ennen kuin CMS on ylipäätään olemassa, koska ISO 32000-1 §12.8.1:n mukainen /ByteRange on kiinnitettävä ensin, joten slotti varataan reilulla marginaalilla ja käyttämättä jäävä häntä on nollia. PLTrimCMSPadding lukee ensimmäisen TLV:n, vaatii tagia $30 ja palauttaa tavut kyseisen alkion loppuun saakka; kaikki, mikä ei ala well-formed-SEQUENCElla, palautuu tyhjänä. Ylin taso on ainoa paikka, jossa häntätavut ovat laillisia, ja erottelu ratkaisee seuraavassa luvussa: tiukka "alkion on kulutettava koko puskuri" -sääntö hylkäisi jokaisen oikean maailman allekirjoituksen, kun taas löyhä sääntö jokaisella syvyydellä päästäisi sisäkkäiset kentät lukemaan tavuja, jotka eivät ole niiden omia
Miksi DER-lukija tarvitsee vanhemman päättymiskohdan?
Sisäkkäinen alkio on kelvollinen vain, jos se päättyy vanhempansa sisälle, eikä tarkistus puskurin loppua vasten todista sitä. PDFlibASN1n matalan tason DERReadTLV rajaa jokaisen alkion koko merkkijonoon, mikä on oikea tarkistus uloimmalle objektille ja väärä kaikelle sen alapuolelle. Kuvittele SignerInfo, jonka issuerAndSerialNumber ilmoittaa 40 tavua, vaikka sen sisällä oleva issuer Name vaatii 60. Jokainen tavu on yhä puskurissa, joten puskuriin rajautuva lukija hyväksyy Namen, lukee sarjanumeron sitä seuraavasta tiivistysalgoritmista ja vertaa sitten tuota paria upotettuihin sertifikaatteihin. Ennen v3.539.10:ää CMS-kävelijä luki täsmälleen noin. Korjaus on pieni kääre, joka vie vanhemman päättymiskohdan jokaisen luvun mukana
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// vanhemman sisällä ei ole enää mitään jäljellä: luku ei saa alkaa
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset osoittaa nyt alkion jälkeistä kohtaa; se ei saa ylittää vanhempaa
Result := Offset <= ParentEnd;
end;
// jokainen taso kirjaa oman loppunsa ja välittää sen alaspäin:
// OuterEnd := ContentInfo:n loppu (RFC 5652, osa 3)
// ExplicitEnd := sisällön [0] EXPLICITin loppu
// ContentEnd := SignedData:n loppu (RFC 5652, osa 5.1)
// SignerEnd / InnerEnd SignerInfolle ja issuerAndSerialNumberille
Yksikkö PDFlibCMSRead vie nyt nämä loput läpi ContentInfon, [0] EXPLICIT-kääreen, SignedData-kenttien aina signerInfosia myöten, SignerIdentifierin kummassakin issuerAndSerialNumber- ja [0] subjectKeyIdentifier-muodossaan (RFC 5652 §5.3) sekä tbsCertificate-kenttien, jotka luetaan jokaisesta upotetusta sertifikaatista allekirjoittajaa täsmäytettäessä. Sertifikaattijoukon ja signerInfos-joukon sisällä joukon lopun yli juokseva alkio pysäyttää silmukan: PLExtractCMSCertificates palauttaa jo hyväksymänsä sertifikaatit eikä koskaan liimaa seuraavia crls- tai signerInfos-tavuja viimeisen päälle. Issuer-ja-sarjanumero-täsmäytys vaatii myös molemmat puolet, sillä sarjanumero on yksilöllinen vain yhden myöntäjän sisällä
Miksi 2.999.3 tuli ulos arvona 1.15.3?
OID:n kaksi ensimmäistä kaarta yhdistyvät yhdeksi alitunnisteeksi, emmekä yhdeksi tavuksi, ja se alitunniste koodataan base-128:lla kuten kaikki muut kaaretkin. X.690 §8.19.4 määrittelee sen arvoksi 40 * arc1 + arc2; aiempi DER_OID kirjoitti arvon Byte(...)-muunnoksella, mikä pitää paikkansa vain arvoon 127 asti, eli arvoon 2.47 saakka. Arvolla 2.999 summa on 1079, tavuun castaus säästää luvusta 55, ja 55 dekoodautuu arvoksi 1.15, joten tunniste nimeää hiljaisesti puun eri haaran. Arvot 128–255 epäonnistuvat toisin: ne päästävät ulos yhden tavun, jonka jatko-bitti on päällä ja joka nielee seuraavan kaaren. Useimmat PKI-tunnisteet (1.2.840..., 2.5.29..., 0.4.0...) eivät koskaan yletä rajalle, minkä vuoksi vika eli hengissä; joint-iso-itu-t-kaaret 2.48 ylöspäin ylettävät. DER_OID palvelee sekä allekirjoitettujen attribuuttien kooderia että täsmäyttäjää DERFindExtensionByOIDssa ja SignedData-sisältötyypin tarkistuksessa, joten väärä koodaus rikkoi sekä kirjoituksen että haun
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.
Yhdistetty arvo pidetään tarkoituksella UInt64ssa. DER_OID jäsentää kaaret Int64hin, joten laillinen toinen kaari voi olla jopa Int64.MaxValue:n suuruinen, ja arvon 80 lisääminen arc1 = 2:lle ylivuottaa etumerkillisen 64-bittisen kokonaisluvun. UInt64 kantaa arvon Int64.MaxValue + 80 kiertymättä, ja kymmenen tavun työpuskuri mahtuu ne kymmenen 7-bittistä ryhmää, joita 64-bittinen arvo tarvitsee. Säilyttämisen arvoiset testivektorit ovat rajan kummalla puolella olevat: 2.47:n on pysyttävä yhtenä tavuna, ja 2.48:n on muututtava kahdeksi
Mitä lukupuolen CMS-kävelijä takaa?
PDFlibCMSRead takaa rakenteen eikä muuta: se palauttaa tavut sieltä, minne RFC 5652 sanoo niiden kuuluvan, eikä varmista allekirjoitusta, tiivistettä eikä voimassaoloaikaa. Kävelijä hyväksyy vain DER:n, joten DERReadTLV hylkää määrittelemättömät pituudet ja monitavuiset taginumerot, ja standardia noudattamattoman allekirjoittajan BER-koodattu CMS raportoi nolla sertifikaattia osittaisen arvauksen sijaan. Attribuuttisertifikaatit ja muut CertificateChoices-vaihtoehdot ohitetaan, koska mikään myöhempi vaihe ei voi käyttää niitä. Kryptograafinen varmennus jää sen koodin vastuulle, joka sen omistaa: se alkaa kohdassa PAdES-allekirjoitus ja ByteRange-validointi Delphissä kuvatuista tavukattavuustarkistuksista ja jatkuu artikkelissa PDF:n allekirjoittamisen jälkeen muuttuneen sisällön luokittelu
Laajempi oppi pätee mihin tahansa binääriformaattiin: "puskurin sisällä" on muistiturvallisuusominaisuus, "vanhemman sisällä" on korrektiusominaisuus, ja jäsennin tarvitsee molempia. Sama ajattelu vihamielisistä pituuksista kulkee läpi kirjoituksessa Pascal-PDF-jäsennimen kovettaminen haitallisia tiedostoja vastaan. Täällä käsitellyt sertifikaattien poiminta, ketjun rakennus ja pitkän aikavälin validoinnin API:t toimitetaan mukana tuotteessa losLab PDF Library for Delphi, joka toimii Delphillä, C++Builderilla ja Lazaruksella