Darbo aplinka (workbench), sujungianti atitikties patvirtinimą su skaitmeniniu pasirašymu, turi suderinti keturis žingsnius būtent šia tvarka ir visą laiką dirbti su tuo pačiu baitų rinkiniu. Ji atlieka PDF/A arba PDF/UA išankstinį patikrinimą (preflight). Tada pritaiko reikiamus pataisymus pagal gautus rezultatus ir išsaugo pataisytą versiją. Tada pasirašo būtent tą versiją. Galiausiai ji perskaito pasirašytą failą ir patvirtina, kad parašas tikrai jį apima. Ši eilės tvarka nėra atsitiktinė. Praleiskite perskaitymą ir pasitikėsite savo pačių rašymo keliu; jei išankstinį patikrinimą atliksite su neteisinga versija, jūsų atitikties ataskaita aprašys failą, kurio niekada neišsiuntėte.
Dalis, kurioje dauguma savarankiškai sukurtų sistemų daro klaidų, yra perėjimas tarp patvirtinimo ir pasirašymo. Vykdykite juos kaip du atskirus įrankius su taisymo (remediation) žingsniu tarp jų, ir atsiras bent trys skirtingos failo versijos, kiekviena su savo baitais. Išankstinio patikrinimo ataskaita, kurią pateikiate auditoriui, aprašo vieną iš jų. Parašas užrakina kitą. Faile nėra nieko, kas patvirtintų, kad tai ta pati versija, ir dažnai taip nėra. „PDFlibPas“ – „losLab“ PDF kūrėjų biblioteka, skirta „Delphi“ ir „C++Builder“, patalpina išankstinį patikrinimą ir PAdES pasirašymą po viena fasado (facade) klase, oro todėl visa seka gali vykti viename procese, kuris niekada nepraranda informacijos apie tai, su kuriais baitais dirbama. Kiekvienas žemiau nurodytas iškvietimas šiuo metu egzistuoja bibliotekoje, kaip ir visi šalia paminėti spąstai.
Trys to paties dokumento versijos ir kaip atsiranda atotrūkis
Suskaičiuokite įrašymus. Originalas gaunamas iš išorės. Taisymo žingsnis jį įkelia, įjungia atitikties režimą ir įrašo pataisytą versiją. Pasirašymo žingsnis prideda parašą kaip prieauginį atnaujinimą (incremental update) – tai yra trečiasis rašymas. Trys įrašymai, trys baitų išdėstymai, o išankstinio patikrinimo ataskaita nieko nereiškia, jei joje nenurodyta, kurią iš šių trijų versijų ji apima. Failo SHA-256 reikšmė, įrašyta šalia kiekvieno išankstinio patikrinimo ir kiekvieno parašo, yra patikimas inkaras, leidžiantis įrodyti, kad versija, kurią patvirtinote, yra ta pati, kurią pasirašėte.
Vienas bibliotekos elgsenos aspektas dar labiau sustiprina šią tvarką. Atitikties pataisymai, kurių prašoma per SetPDFAMode arba SetPDFUAMode, neįsigalioja iškart po iškvietimo. Jie pritaikomi įrašymo (save) metu. Automatiniai pataisymai, tokie kaip privalomas anotacijų spausdinimo vėliavėlių nustatymas arba PDF/UA tabuliavimo tvarkos priskyrimas, atsiranda tik išvesties faile, todėl patikra, atlikta su ką tik atmintyje „pataisytu“ dokumentu, nieko nepasako apie baitus, keliaujančius pas pasirašymo modulį. Pirmiausia įrašykite, o tada atlikite išsaugoto failo išankstinį patikrinimą. Būsena atmintyje yra tik projektas; tikras yra tik diskas faile.
Išankstinis patikrinimas iš disko ir nulis, turintis dvi reikšmes
Pagrindinis išankstinio patikrinimo taškas yra CheckFileCompliance(FileName, Password, ComplianceTest, Options). Testas „1“ parenka PDF/A (ISO 19005), testas „2“ parenka PDF/UA (ISO 14289). Jis atidaro failą per bibliotekos srautinį skaitytuvą (streaming reader), todėl nereikia iš anksto iškviesti LoadFromFile, ir grąžina eilučių sąrašo identifikatorių (handle), kuriame kiekvienas įrašas rodo po vieną radinį:
var
PDF: TPDFlib;
ListID, I: Integer;
begin
PDF := TPDFlib.Create;
try
ListID := PDF.CheckFileCompliance('invoice-fixed.pdf', '', 1, 0); // 1 = PDF/A
if ListID = 0 then
begin
if PDF.LastErrorCode <> 0 then
raise Exception.Create('Preflight could not read the file')
else
Writeln('No PDF/A findings');
end
else
begin
for I := 0 to PDF.GetStringListCount(ListID) - 1 do
Writeln(PDF.GetStringListItem(ListID, I));
PDF.ReleaseStringList(ListID);
end;
finally
PDF.Free;
end;
end;
Spąstai slypi grąžinamoje reikšmėje – tai tokia klaida, kuri lengvai praeina visus sėkmingo scenarijaus testus. Nulis reiškia „nerasta jokių atitikties trūkumų“. Nulis taip pat reiškia „nepavyko atidaryti failo“, kadangi realizacija grąžina 0 kaskart, kai rezultatų sąrašas lieka tuščias, įskaitant skaitymo nesėkmes. Darbo aplinka, kuri nulinį rezultatą laiko žalia šviesa, lengvai patvirtins failą, kurį yra užrakinęs kitas procesas. Iškvietimo susiejimas su LastErrorCode, kaip parodyta aukščiau, padeda atskirti šiuos du atvejus. Tikrinimo modulis taip pat atidaro failą su bendro naudojimo režimu deny-write, todėl, jei jūsų taisymo žingsnis vis dar laiko atidarytą rašymo srautą, patikra nepavyks dėl priežasties, kuri nesusijusi su atitiktimi, bet tiesiogiai susijusi su srautu, kurį pamiršote atlaisvinti.
Kai radinius turi perskaityti žmogus, o ne automatizuotas konvejeris, CreatePreflightReport atvaizduoja juos kaip lengvai skaitomą ataskaitą. Metodas ComparePreflightReports palygina du tikrinimus – tai patogus būdas parodyti, kad taisymo žingsnis pašalino pradinius trūkumus ir neįnešė naujų problemų.
Patvirtintos versijos pasirašymas su SignProcess
Kai išsaugota versija sėkmingai praeina išankstinį patikrinimą ir jos maišos kodas (hash) yra užregistruotas, pasirašykite būtent tą failą ir jokio kito. „SignProcess“ API veikia kaip kūrėjas (builder). Atidarykite proceso identifikatorių, sukonfigūruokite jį eilutė po eilutės, patvirtinkite (commit) ir perskaitykite grąžinamą rezultato kodą.
ProcessID := PDF.NewSignProcessFromFile('invoice-fixed.pdf', '');
if ProcessID = 0 then
raise Exception.Create('Cannot open source for signing');
PDF.SetSignProcessField(ProcessID, 'ApprovalSig');
PDF.SetSignProcessPFXFromFile(ProcessID, 'company.pfx', PfxPassword);
PDF.SetSignProcessInfo(ProcessID, 'Invoice approval', 'Berlin', 'billing@example.com');
PDF.SetSignProcessCustomSubFilter(ProcessID, 'ETSI.CAdES.detached'); // PAdES baseline
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // room for a later timestamp
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
Dvi eilutės šioje sekoje yra kur kas svarbesnės, nei atrodo iš pirmo žvilgsnio. Metodas SetSignProcessCustomSubFilter su reikšme ETSI.CAdES.detached parenka PAdES parašą, suderintą su ETSI EN 319 142-1 standartu, užuot naudojęs pasenusią adbe.pkcs7.detached šeimą – būtent tai lemia, ar parašą Europos tikrinimo įrankiai pripažins, ar pažymės kaip įtartiną. Metodas SetSignProcessReserveContentsBytes rezervuoja vietą /Contents užpildymui. Pasirinktas dydis yra sprendimas ateičiai: jei vėliau reikės pridėti parašo laiko žymą (timestamp), padidintas CMS privalo tilpti į jūsų dabar rezervuotą vietą, nes rezervuota vieta negali padidėti vėliavėlės vėliau neperrašant viso parašo. Rezervuosite per daug – prarasite kelis kilobaitus failo dydžio. Rezervuosite per mažai – laiko žymos gavimas po kelių mėnesių nepavyks dėl vietos trūkumo, kurio priežastį bus sunku susieti su šia viena kodo eilute.
Metodas GetSignProcessResult grąžina kodą, o ne loginę reikšmę, ir šie kodai yra labai naudingi. Reikšmė 1 reiškia sėkmę, 4 – neteisingą PDF slaptažodį, 7 – neteisingą sertifikato slaptažodį, 9 – PFX failą be privataus rakto, 11 – klaidą taikant patį parašą. Supaprastinę šiuos kodus iki tiesa/netiesa (true/false) prarasite informaciją, leidžiančią atskirti neteisingo slaptažodžio klaidą nuo rakto be privačios dalies problemos. Registruokite gautą sveikąjį skaičių.
Perskaitymas: ką tik sukurto failo auditas
Jokia darbo aplinka neturėtų pasitikėti keliu, kuriuo įrašė ką tik sertifikuojamą failą. Audito klasė TPDFlibSignDoc iš naujo atidaro pasirašytą failą ir nuskaito parašo žodyno įrašus tiesiai iš disko:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Capture the size before Open: the audit object holds a share lock on the file
FS := TFileStream.Create('invoice-signed.pdf', fmOpenRead or fmShareDenyNone);
SourceSize := FS.Size;
FS.Free;
Doc := TPDFlibSignDoc.Create;
Names := TStringList.Create;
try
if not Doc.Open('invoice-signed.pdf', '', False) then Exit;
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // > 0 means the field is signed
begin
RangeStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
GapStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
TailStart := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
TailLen := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (RangeStart = 0) and (TailStart + TailLen = SourceSize) then
Writeln(Names[I], ': signature covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unusual ByteRange layout');
end;
Doc.Close;
finally
Names.Free;
Doc.Free;
end;
end;
Failo dydžio nustatymas šio pavyzdžio pradžioje yra kritinis žingsnis, o ne tiesiog tvarkymas. Metodas TPDFlibSignDoc.Open laiko failą su griežtu bendro naudojimo užraktu (share lock) visą jo gyvavimo ciklą, todėl bet koks veiksmas, kuriam reikia neapdorotų baitų (maišos skaičiavimas, CMS santraukos perskaičiavimas), turi nuskaityti failą prieš iškviečiant Open. Pačios bibliotekos „SigningWorkbench“ demonstracinė programa būtent dėl šios priežasties pirmiausia įkelia visą failą į atmintį, o darbo aplinka, kuri ignoruoja šį eiliškumą, veiks nestabiliai (priklausomai nuo to, kuris procesas laimės lenktynes).
ByteRange aritmetika, įrodanti parašo aprėptį
Tvarkingas vieno parašo failas turi ByteRange formą [0 a b c]: aprėptis prasideda nuo poslinkio (offset) 0, praleidžia šešioliktainį /Contents užpildą tarp „a“ ir „b“, o tada tęsiasi per „b+c“ baitų. Kai „b+c“ yra lygus failo dydžiui, parašas apima visą failą iki jo pabaigos – tai yra pageidaujamas rezultatas. Kai reikšmė yra mažesnė, tai reiškia, kad po parašo įrašymo kažkas pridėjo prieauginį atnaujinimą (incremental update). Tai visiškai leistina pagal ISO 32000-1§12.8 standartą, kadangi vėlesni formų užpildymai, antrasis parašas ir DSS žodynas pridedami būtent tokiu būdu. Tačiau šį faktą audito seka turėtų užregistruoti būtent pasirašymo metu, o ne bandyti atkurti kilus ginčui.
Atlikdami šią aritmetiką, stebėkite sveikojo skaičiaus bitų skaičių. Pagrindinio lygio API funkcija GetSignProcessByteRange grąžina 32 bitų sveikąjį skaičių, tačiau tikrosios reikšmės yra Int64, todėl, jei failo dydis viršija 2 GB, šis metodas tyliai nupjauna reikšmę. Naudokite klasės lygio TPDFlibSigner.GetByteRange, kuri grąžina Int64, arba išanalizuokite reikšmes iš GetSignatureValueByName, kaip parodyta audito kode aukščiau.
Ką biblioteka palieka įgyvendinti jums
Du apribojimus geriau išsiaiškinti projektavimo etape, o ne paskutinę savaitę prieš paleidimą. Pagrindinis TPDFlib API neturi jokio parašo tikrinimo (verification) apvalkalo. Kriptografinis tikrinimas vyksta vienu lygmeniu žemiau – TPDFlibSignatureVerifier klasėje, kurios metodas VerifySignature grąžina validų, nevalidų arba nežinomą statusą. Taip pat nėra integruoto HTTP kliento, skirto RFC 3161 laiko žymos serveriams (TSA). Biblioteka apskaičiuoja maišos kodą pateikimui ir vėl įterpia gautą CMS žetoną, tačiau tinklo užklausą į TSA turite parašyti patys. Abu šiuos komponentus aprašyti gana paprasta, tačiau labai nemalonu pastebėti jų trūkumą prieš pat išleidimą, todėl suprojektuokite juos iš anksto.
Vieną klausimą apie atitiktį verta išsiaiškinti iškart, nes jis nulemia paskutinio patikros taško vietą: ar parašo pridėjimas pažeidžia PDF/A atitiktį? Pats savaime ne. Parašas pridedamas kaip prieauginis atnaujinimas, o standartas nuo ISO 19005-2 aiškiai leidžia pasirašytus dokumentus. Problema gali kilti dėl paties parašo vizualinio atvaizdavimo (signature appearance), kuriam taikomos tos pačios taisyklės kaip ir bet kokiam kitam puslapio turiniui – įskaitant šriftų įterpimą ir draudimą naudoti nuo įrenginio priklausančias spalvas. Todėl paskutinis darbo aplinkos taškas yra dar vienas išankstinis patikrinimas, atliekamas su pasirašytu failu. Naudokite CheckFileCompliance greitam tikrinimui konvejeryje, tačiau galutines versijas patikrinkite nepriklausomu įrankiu (pavyzdžiui, „veraPDF“), nes skirtingi tikrintuvai naudoja persidengiančias, bet nevienodas taisykles. Kai jie nesutaria, radinio tekstas paprastai nurodo konkrečią standarto sąlygą, kurią verta perskaityti.
Pasirašymas ir laiko žymos uždėjimas nėra atliekami vienu žingsniu: pirmiausia įrašomas bazinis parašas, o tada atskiras laiko žymos procesas papildo CMS rezervuotoje /Contents vietoje – būtent todėl vietos rezervavimo eilutė kodo pradžioje buvo tokia svarbi. Laiko žymoms ir ilgalaikio patvirtinimo lygmenims, kurie kuriami ant šios darbo aplinkos pamatų, PAdES pasirašymo ir tikrinimo vadovas paaiškina parašo kelią nuo bazinio iki B-LT lygio, o išankstinio tikrinimo dalis išsamiau aprašyta PDF/A ir PDF/UA preflight vadove. Pilną API dokumentaciją ir bandomąsias versijas rasite PDFlibPas produkto puslapyje.