PDF Library for Delphi (PDFlibPas) ištraukia PDF parašo viduje esančius sertifikatus atlikdama gryną DER peržiūrą po CMS SignedData, saugomą /Contents, be jokios CryptoAPI. Nuo v3.539.10 kiekvienas įdėtinis skaitymas ribojamas tėvinio elemento ribomis, nulinis užpildas po CMS nukerpamas iki CMS deklaruoto ilgio, o objektų identifikatoriai sujungtąjį pirmąjį subidentifikatorių koduoja base-128. Ribų taisyklė ir OID pataisa abi pakeitė kodą, kuris duodavo neteisingus atsakymus nekeliodamas klaidos, o užpildo taisyklė apsaugo griežtesnį skaitytuvą nuo realių parašų atmetimo
Skaitymo pusė svarbesnė, nei atrodo. Ilgalaikio patvirtinimo įrankiai prieš imdami atšaukimo duomenis turi iš esamo parašo ištraukti pasirašiusiojo sertifikatą ir jo išdavėjus, audito ataskaita turi pasakyti, kas pasirašė, o Lazarus versija Linux aplinkoje neturi Windows pranešimų funkcijų, kuo remtis. Tokioje pozicijoje esantis analizatorius blogą įvedimą beveik niekada nepaverčia avarija. Skausmingiausias gedimo variantas – sertifikatų skaičius, į kurį įsiterpia kaimyno baitai, pasirašiusiojo atitikimas pagal netinkamą lauką arba OID, kuris tyliai pavirsta kitu OID. Ant tokių pamatų pastatytas parašų pipeline praneša pasitikinčias nesąmones
Pasirašiusiojo sertifikatų skaitymas iš pasirašyto PDF
Skaitymo pusę dengia penki TPDFlib metodai, ir visi jie priima InputFile, Password, FieldName: kiekvienas iškvietimas atveria failą tik skaitymui, atsako ir vėl jį užveria. GetSignatureEmbeddedCertificateCount ir GetSignatureEmbeddedCertificateDER išvardija sertifikatų aibę kodavimo tvarka, GetSignatureSignerCertificateDER grąžina sertifikatą, iš kurio kilo tam tikras SignerInfo, o GetSignatureCertificateChainLength / GetSignatureCertificateChainDER eina nuo to pasirašiusiojo link toliausio išdavėjo, kurį pats parašas nešasi. Indeksai skaičiuojami nuo nulio. Rezultatus laikykite AnsiString – todėl biblioteka ir grąžina juos taip: DER duomenų pluoštas, praleistas pro string ar TStrings, eina pro koduotės konversiją ir grįžta sugadintas
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;
Toje išvestyje dviejų dalykų reikia įsikelti į galvą. Skaičius 0 nėra diagnozė: dingęs laukas, netinkamas slaptažodis, ne DER duomenų pluoštas ir SignedData, kuri tiesiog praleidžia pasirinktinę sertifikatų aibę, visi grįžta kaip 0 arba tuščia eilutė, tad šalia skaičiaus į žurnalą rašykite ir lauko pavadinimą. Ir grandinė, pasibaigusi dar prieš pasiekdama nuosavo išdavėjo sertifikatą, taip pat nėra klaida. Grandinės kūrėjas naudoja tik paraše įmontuotus sertifikatus, todėl likusius išdavėjus tenka pasiekti per adresus, kuriuos praneša GetCertificateIssuerURLs
Kuri /Contents dalis iš tikrųjų yra CMS?
CMS priklauso tik išorinio SEQUENCE deklaruojamas priešdėlis, o PLTrimCMSPadding viską po jo nukerpa. Pasirašytojas /Contents šešioliktainę eilutę rezervuoja dar prieš gimstant CMS, nes /ByteRange, aprašytas ISO 32000-1 §12.8.1, turi būti sutvarkytas anksčiau, tad lizdas padaromas su atsarga, o nepanaudota uodega lieka nuliai. PLTrimCMSPadding skaito pirmąjį TLV, reikalauja žymės $30 ir grąžina baitus iki to elemento pabaigos; kas nesideda gerai suformuota SEQUENCE, grįžta tuščia. Tas viršutinis lygis yra vienintelė vieta, kur galiniai baitai teisėti, ir šis skirtumas svarbus kitam skyriui: griežta „elementas privalo suvalgyti visą buferį“ taisyklė atmestų kiekvieną realų parašą, o atsipalaidavusi taisyklė, taikoma kiekviename gilyje, leidžia įdėtiniams laukams skaityti svetimus baitus
Kodėl DER skaitytuvui reikia tėvinio elemento pabaigos poslinkio?
Įdėtinis elementas teisėtas tik tada, kai baigiasi savo tėvo viduje, o patikrinimas pagal buferio pabaigą to neįrodo. Žemo lygio DERReadTLV faile PDFlibASN1 kiekvieną elementą riboja visos eilutės atžvilgiu, kas yra teisingas patikrinimas pačiam išoriniam objektui ir neteisingas viskam, kas žemiau jo. Įsivaizduokite SignerInfo, kurio issuerAndSerialNumber deklaruoja 40 baitų, o jo viduje esantis išdavėjo Name tvirtina 60. Visi baitai tebėra buferio viduje, todėl buferiu ribotas skaitytuvas priima Name, iš po jo einančio santraukos algoritmo ištraukia serijos numerį ir tada tą porą lygina su įmontuotais sertifikatais. Iki v3.539.10 CMS peržiūra skaitė būtent taip. Pataisa – nedidelis įvyniojimas, kuris kiekvieną skaitymą aprūpina tėvo pabaigos pozicija
function ReadTLVWithin(const Data: AnsiString; ParentEnd: Integer;
var Offset: Integer; out Tag: Byte; out Start, Len: Integer): Boolean;
begin
Result := False;
// tėvo viduje nieko neliko: atsisakome pradėti skaitymą
if (Offset < 1) or (Offset >= ParentEnd) then
Exit;
if not DERReadTLV(Data, Offset, Tag, Start, Len) then
Exit;
// Offset dabar stovi vienu žingsniu už elemento; jis negali pereiti tėvo ribos
Result := Offset <= ParentEnd;
end;
// kiekvienas lygis įsimena savo pabaigą ir perduoda ją žemyn:
// OuterEnd := ContentInfo pabaiga (RFC 5652 section 3)
// ExplicitEnd := content [0] EXPLICIT pabaiga
// ContentEnd := SignedData pabaiga (RFC 5652 section 5.1)
// SignerEnd / InnerEnd skirti SignerInfo ir issuerAndSerialNumber
Unitas PDFlibCMSRead dabar tuos galus perneša pro ContentInfo, [0] EXPLICIT įvyniojimą, SignedData laukus iki pat signerInfos, SignerIdentifier abiem jo pavidalais – issuerAndSerialNumber ir [0] subjectKeyIdentifier (RFC 5652 §5.3) – bei tbsCertificate laukus, skaitomus iš kiekvieno įmontuoto sertifikato ieškant pasirašiusiojo. Sertifikatų aibės ir signerInfos aibės viduje elementas, nuėjęs už aibės galo, sustabdo ciklą: PLExtractCMSCertificates grąžina jau priimtus sertifikatus ir niekada nepriklijuoja paskutiniam sekančių crls ar signerInfos baitų. Išdavėjo ir serijos numerio atitikimui reikia abiejų pusių, nes serijos numeris unikalus tik vieno išdavėjo rėmuose
Kodėl 2.999.3 pavirto 1.15.3?
Pirmosios dvi OID šakos susijungia į vieną subidentifikatorių, o ne į vieną baitą, ir tas subidentifikatorius koduojamas base-128 kaip ir bet kuri kita šaka. X.690 §8.19.4 jį apibrėžia kaip 40 * arc1 + arc2; ankstesnis DER_OID tą reikšmę rašė su Byte(...), kas teisinga tik iki 127 – tai 2.47 reikšmė. Skaičiuojant 2.999 suma yra 1079, konversija į baitą palieka 55, o 55 iškoduojasi kaip 1.15, tad identifikatorius tyliai pavadina kitą medžio šaką. Reikšmės nuo 128 iki 255 klysta kitaip: išleidžia vieną baitą su įjungtu tęsimo bitu, kuris praryja kitą šaką. Dauguma PKI identifikatorių (1.2.840..., 2.5.29..., 0.4.0...) iki ribos niekada nenukeliauja – todėl klaida ir išgyveno; joint-iso-itu-t šakos nuo 2.48 aukštyn ją pasiekia. DER_OID aptarnauja ir pasirašytų atributų koduotuvą, ir atitikimo paiešką DERFindExtensionByOID bei SignedData turinio tipo patikrą, todėl neteisingas kodavimas pakenkdavo ir rašymui, ir paieškai
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.
Sujungta reikšmė UInt64 laikoma tyčia. DER_OID šakas išanalysuoja į Int64, todėl teisėta antroji šaka gali būti iki Int64.MaxValue dydžio, o pridėjus 80, kai arc1 = 2, 64 bitų sveikasis skaičius su ženklu perpildomas. UInt64 neša Int64.MaxValue + 80 be apvyniojimo, o dešimties baitų darbinis buferis telpa dešimt 7 bitų grupių, kurių reikia 64 bitų reikšmei. Testiniai vektoriai, kuriuos verta saugoti, yra tie abipus ribos: 2.47 turi likti vienu baitu, o 2.48 – tapti dviem
Ką garantuoja skaitymo pusės CMS peržiūra?
PDFlibCMSRead garantuoja struktūrą ir nieko kito: jis grąžina baitus, kurie sėdi ten, kur liepia RFC 5652, ir nepatikrina nei parašo, nei santraukos, nei galiojimo laiko. Peržiūra priima tik DER, todėl DERReadTLV atmeta neapibrėžtus ilgius ir kelių baitų žymių numerius, o BER koduota CMS iš standartą pažeidžiančio pasirašytojo praneša nulinį sertifikatų skaičių, o ne dalinį spėjimą. Atributų sertifikatai ir kiti CertificateChoices variantai praleidžiami, nes niekas žemiau jų panaudoti negali. Kriptografinį patvirtinimą paliekame kodui, kuris juo rūpinasi: jis prasideda baitų aprėpties patikromis, aprašytomis straipsnyje apie PAdES pasirašymą ir ByteRange patikrinimą Delphi, ir tęsiasi klasifikuojant, kas pasikeitė po to, kai PDF buvo pasirašytas
Platesnė pamoka galioja bet kuriam dvejetainiam formatui: „buferio viduje“ yra atminties saugos savybė, „tėvo viduje“ – teisingumo savybė, o analizatoriui reikia abiejų. Ta pati mintis apie priešiškus ilgius eina pro Pascal PDF analizatoriaus sukietinimą prieš kenkėjiškus failus. Čia aptarti sertifikatų ištraukimo, grandinių kūrimo ir ilgalaikio patvirtinimo API keliauja kartu su losLab PDF Library for Delphi, skirta Delphi, C++Builder ir Lazarus