Radno okruženje koje povezuje validaciju usaglašenosti sa digitalnim potpisivanjem mora da koordinira četiri koraka, tačno ovim redosledom, i da ih drži vezanim za jedan isti skup bajtova tokom celog procesa. Ono pokreće PDF/A ili PDF/UA preflight (prethodnu proveru). Zatim primenjuje sve neophodne ispravke koje nalazi zahtevaju i čuva korigovanu reviziju. Potom potpisuje upravo tu reviziju. Na kraju, ponovo učitava potpisanu datoteku i potvrđuje da je potpis zaista pokriva. Redosled nije estetske prirode. Ako preskočite ponovno učitavanje, verujete sopstvenom procesu upisa; ako preflight pokrenete nad pogrešnom revizijom, vaš izveštaj o usaglašenosti opisuje datoteku koju nikada niste isporučili.
Deo u kom većina kućno razvijenih rešenja greši jeste prelaz između validacije i potpisivanja. Ako ih pokrenete kao dva odvojena alata sa fazom ispravke između njih, stvoriće se najmanje tri različite revizije datoteke, svaka sa svojim rasporedom bajtova. Preflight izveštaj koji predajete revizoru opisuje jednu od njih. Potpis zamrzava drugu. Ništa u datoteci ne garantuje da se radi o istoj reviziji, a često i nije tako. PDFlibPas, losLab PDF biblioteka za programere u Delphi-ju i C++Builder-u, postavlja preflight i PAdES potpisivanje iza jedne klase fasade, tako da ceo redosled može da živi u jednom procesu koji nikada ne gubi trag o tome o kojim bajtovima se radi. Svaki poziv naveden u nastavku postoji u biblioteci danas, kao i svaka zamka zabeležena uz njega.
Tri revizije jednog dokumenta i kako nastaje razlika
Izbrojte čuvanja. Original stiže iz spoljnog izvora. Faza ispravke ga učitava, uključuje režim usaglašenosti i upisuje korigovanu reviziju. Faza potpisivanja dodaje potpis kao inkrementalno ažuriranje, što je treći upis. Tri čuvanja, tri rasporeda bajtova, a preflight izveštaj ne znači ništa osim ako ne navodi koju od ove tri verzije pokriva. SHA-256 kontrolna suma datoteke, zabeležena uz svaku preflight proveru i svaki potpis, predstavlja pouzdan način da dokažete da je revizija koju ste proverili upravo ona koju ste i potpisali.
Jedno ponašanje biblioteke dodatno pojačava ovu disciplinu. Ispravke usaglašenosti zatražene preko SetPDFAMode ili SetPDFUAMode do not take effect when you call them. They are applied during the save. Auto-repairs like forcing annotation print flags or assigning a PDF/UA tab order land in the output file and nowhere else, so a check run against the document you just "fixed" in memory tells you nothing about the bytes headed for the signer. Save first, then preflight the saved file. The in-memory state is a draft; only the file on disk is real.
Preflight sa diska i nula koja znači dve stvari
Direktna ulazna tačka za preflight je funkcija CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 bira PDF/A (ISO 19005), dok test 2 bira PDF/UA (ISO 14289). Ona otvara datoteku preko čitača toka biblioteke, pa nema potrebe za prethodnim pozivanjem LoadFromFile, i vraća identifikator liste stringova (string-list handle) koja sadrži po jedan nalaz po stavci:
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;
Zamka leži u povratnoj vrednosti, i to je vrsta problema koja prolazi sve uobičajene testove. Nula znači „nema nalaza”. Nula takođe znači „datoteka se ne može otvoriti”, jer implementacija vraća 0 kad god je lista rezultata prazna, uključujući i neuspeh pri čitanju. Radno okruženje koje čita 0 kao zeleno svetlo će rado odobriti datoteku koju je neki drugi proces zaključao. Povezivanje poziva sa LastErrorCode, kao što je prikazano gore, jeste ono što razdvaja ova dva slučaja. Provera takođe otvara datoteku u režimu deljenja koji zabranjuje upis (deny-write share mode), pa ako vaša faza ispravke i dalje drži otvoren pisac (writer handle), preflight će otkazati iz razloga koji nema veze sa usaglašenošću, već sa tokom (stream) koji ste zaboravili da oslobodite.
Kada čovek, a ne automatizovani sistem, treba da pročita nalaze, CreatePreflightReport ih generiše kao čitljiv izveštaj. ComparePreflightReports upoređuje dva pokretanja, što je odličan način da se pokaže da je ispravka uklonila prvobitne nalaze bez uvođenja novih problema.
Potpisivanje proverene revizije pomoću SignProcess-a
Nakon što sačuvana revizija prođe preflight i njena kontrolna suma bude zabeležena, potpišite tačno tu datoteku i nijednu drugu. SignProcess API se koristi po principu graditelja (builder). Otvorite identifikator procesa, konfigurišite ga liniju po liniju, potvrdite izmene, a zatim pročitajte povratni kod rezultata.
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);
Dve linije u tom nizu nose veću težinu nego što na prvi pogled izgleda. Poziv SetSignProcessCustomSubFilter sa parametrom ETSI.CAdES.detached bira PAdES potpis definisan u ETSI EN 319 142-1 standardu umesto zastarele adbe.pkcs7.detached porodice, što predstavlja razliku između potpisa koji evropski verifikator prihvata i onog koji označava kao neispravan. Poziv SetSignProcessReserveContentsBytes popunjava /Contents rezervisano mesto, a veličina koju ovde odaberete je odluka o budućnosti: ako kasnije treba dodati vremenski žig potpisa, prošireni CMS mora stati u prostor koji sada rezervišete, jer se rezervisano mesto ne može naknadno proširiti bez ponovnog potpisivanja celog dokumenta. Ako rezervišete previše, izgubićete nekoliko kilobajta. Ako rezervišete premalo, korak dodavanja vremenskog žiga će otkazati mesecima kasnije sa greškom o prekoračenju koju ćete teško povezati sa ovom jednom linijom koda.
Metoda GetSignProcessResult vraća kod rezultata, a ne logičku vrednost, i te kodove vredi sačuvati. Vrednost 1 označava uspeh. Vrednost 4 je pogrešna lozinka za PDF, 7 je pogrešna lozinka za sertifikat, 9 označava PFX koji ne sadrži privatni ključ, dok 11 predstavlja neuspeh tokom same primene potpisa. Ako ove kodove svedete na običnu true/false vrednost, odbacićete informaciju koja vam omogućava da razlikujete problem sa lozinkom od problema sa nepostojećim privatnim ključem. Zabeležite ovaj ceo broj.
Ponovno čitanje: revizija datoteke koju ste upravo kreirali
Nijedno radno okruženje ne bi trebalo da veruje procesu koji je upisao datoteku koju se sprema da sertifikuje. Klasa za reviziju TPDFlibSignDoc ponovo otvara potpisanu datoteku i čita unose iz rečnika potpisa direktno sa diska:
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;
Argumenti ValueKey se mapiraju na unose u rečniku. Ključ 0 vraća sirovi CMS iz /Contents, ključevi 2 i 3 nazive za /Filter i /SubFilter, a ključevi od 11 do 14 četiri ByteRange broja. Tekstualne vrednosti se vraćaju preko funkcije GetSignatureTextValueByName: ključ 0 predstavlja deklarisano vreme potpisivanja, dok ključ 5 razlikuje običan Sig od vremenskog žiga dokumenta DocTimeStamp, što postaje važno kada dokument sadrži oba.
Snimanje veličine datoteke na početku tog primera je od suštinskog značaja, a ne samo formalnost. Metoda TPDFlibSignDoc.Open drži datoteku pod restriktivnim zaključavanjem deljenja tokom celog svog životnog veka, tako da sve što zahteva sirove bajtove (heširanje potpisanog opsega, ponovno izračunavanje CMS rezimea) mora pročitati datoteku pre nego što se pozove Open. Demostracioni primer SigningWorkbench u samoj biblioteci učitava celu datoteku u memoriju pre svega upravo iz ovog razloga, a radno okruženje koje ignoriše ovaj redosled povremeno otkazuje, u zavisnosti od toga koji proces pobedi u trci.
ByteRange aritmetika koja dokazuje pokrivenost
Ispravna datoteka sa jednim potpisom ima ByteRange u formatu [0 a b c]: pokrivenost počinje na ofsetu 0, preskače heksadecimalno /Contents mesto između ofseta a i b, a zatim se nastavlja do bajta b+c. Kada je b+c jednako veličini datoteke, potpis pokriva sve do kraja datoteke, što je željeni rezultat. Kada je manji, to znači da je neko dodao inkrementalno ažuriranje nakon što je potpis upisan. To je potpuno legitimno prema ISO 32000-1§12.8 standardu, jer kasnija popunjavanja obrazaca, drugi potpis i DSS rečnik stižu upravo na ovaj način. To je takođe činjenica koju revizorski trag treba da zabeleži u trenutku potpisivanja, umesto da se rekonstruiše pod pritiskom tokom spora.
Obratite pažnju na širinu celobrojnog tipa podataka dok radite ovu aritmetiku. Funkcija GetSignProcessByteRange iz ravnog API-ja vraća 32-bitni Integer, ali su osnovne vrednosti zapravo tipa Int64, pa na datoteci većoj od 2 GB ravni pristupnik tiho skraćuje vrednost. Koristite klasni nivo TPDFlibSigner.GetByteRange koji vraća Int64, ili analizirajte vrednosti iz GetSignatureValueByName na način na koji to radi gore navedeni kod za reviziju.
Ono što biblioteka prepušta vama
Dve granice je bolje naučiti u fazi projektovanja nego u samom finišu razvoja. Ravan TPDFlib API uopšte ne sadrži omotač za verifikaciju potpisa. Kriptografska verifikacija se nalazi jedan nivo ispod, u klasi TPDFlibSignatureVerifier, čija metoda VerifySignature vraća odgovore: validan, nevalidan ili nepoznat. Takođe, ne postoji ugrađeni HTTP klijent za servere vremenskih žigova prema RFC 3161 standardu. Biblioteka izračunava heš za slanje i ponovo ugrađuje prošireni CMS kada token stigne nazad, ali mrežnu komunikaciju sa TSA serverom morate napisati sami. Oba elementa je lako implementirati, ali je izuzetno neprijatno saznati da nedostaju nedelju dana pre puštanja u rad, pa ih isplanirajte od samog početka.
Jedno pitanje o usaglašenosti vredi jasno razjasniti, jer ono odlučuje gde ide poslednja kapija: da li dodavanje potpisa narušava PDF/A status? Ne samo po sebi. Potpis stiže kao inkrementalno ažuriranje, a ISO 19005-2 i noviji standardi eksplicitno dozvoljavaju potpisane dokumente. Problem je u vizuelnom prikazu potpisa, koji mora da prati ista pravila kao i bilo koji drugi sadržaj stranice, uključujući ugrađene fontove i odsustvo boja zavisnih od uređaja. Zato je poslednji korak u radnom okruženju još jedna preflight provera, ovog puta nad potpisanim izlazom. Koristite CheckFileCompliance za brzu proveru unutar sistema, ali i dalje verifikujte verzije za izdavanje pomoću nezavisnog alata kao što je veraPDF, jer verifikatori implementiraju slična, ali ne i identična pravila; kada se ova dva alata ne slažu, tekst nalaza obično navodi tačnu klauzulu koju treba proučiti.
Iz svega ovoga proizilazi jedna važna sekvenca. Potpisivanje i dodavanje vremenskog žiga se ne vrše u jednom prolazu: prvo se upisuje osnovni potpis, a zatim poseban proces vremenskog žiga proširuje CMS unutar rezervisanog /Contents prostora, što je upravo razlog zašto je ranije pomenuta linija za rezervaciju bajtova imala toliku važnost. Za nivoe vremenskih žigova i dugoročne validacije koji se oslanjaju na ovo radno okruženje, vodič za PAdES potpisivanje i validaciju vodi potpis od osnovnog do B-LT nivoa, dok preflight faza ide detaljnije u vodiču za PDF/A i PDF/UA preflight provere. Kompletna API dokumentacija i probne verzije dostupne su na PDFlibPas stranici proizvoda.