Annak a munkapadnak, amely a megfelelőség-ellenőrzést a digitális aláíráshoz fűzi, négy lépést kell összehangolnia, ebben a sorrendben, és végig ugyanahhoz a bájtsorozathoz kell kötnie mindet. Lefuttat egy PDF/A vagy PDF/UA előellenőrzést. Elvégzi a leletek által megkövetelt javításokat, és elment egy helyesbített revíziót. Pontosan azt a revíziót írja alá. Aztán visszaolvassa az aláírt fájlt, és megerősíti, hogy az aláírás valóban lefedi. A sorrend nem kozmetika. Hagyja ki a visszaolvasást, és máris a saját írási útvonalában bízik; futtassa az előellenőrzést rossz revízión, és a megfelelőségi jelentés egy soha ki nem adott fájlról szól
A házilag épített futószalagok többsége az ellenőrzés és az aláírás közötti illesztésnél hibázik. Futtassa a kettőt két külön eszközként, közéjük ékelt javítási menettel, és a fájlnak legalább három különböző revíziója jön létre, mindegyik a maga bájtjaival. Az auditornak átadott előellenőrzési jelentés az egyiket írja le. Az aláírás egy másikat fagyaszt be. A fájlban semmi nem mondja ki, hogy ugyanarról a revízióról van szó, és gyakran nem is arról. A PDF Library for Delphi, a losLab PDF fejlesztői könyvtára Delphihez és C++Builderhez, egyetlen facade osztály mögé teszi az előellenőrzést és a PAdES aláírást, így az egész sorozat egyetlen folyamatban élhet, amely soha nem téveszti szem elől, mely bájtokról beszél. Az alábbi hívások mindegyike létezik ma is a könyvtárban, és mellettük minden megjegyzett csapda szintén
Egy dokumentum három revíziója, és hogyan nyílik a rés
Számolja meg a mentéseket. Az eredeti a felsőbb rendszerből érkezik. A javítási menet betölti, bekapcsol egy megfelelőségi módot, és kiír egy helyesbített revíziót. Az aláírási menet inkrementális frissítésként fűzi hozzá az aláírást, ez a harmadik írás. Három mentés, három bájtelrendezés, és egy előellenőrzési jelentés semmit nem ér, ha nem nevezi meg, a háromból melyiket fedi le. A fájl SHA-256 lenyomata, minden előellenőrzési futás és minden aláírás mellé feljegyezve, az az olcsó horgony, amellyel bizonyítható, hogy az ellenőrzött revízió ugyanaz, mint az aláírt
Egy könyvtárbeli viselkedés tovább szigorítja ezt a fegyelmet. A SetPDFAMode vagy SetPDFUAMode hívással kért megfelelőségi javítások nem a hívás pillanatában lépnek életbe. A mentés során érvényesülnek. Az olyan automatikus javítások, mint a jegyzetek nyomtatási jelzőinek kikényszerítése vagy a PDF/UA tabulátorsorrend kiosztása, a kimeneti fájlba kerülnek és sehová máshová, így az imént a memóriában „megjavított” dokumentumon futtatott ellenőrzés semmit nem árul el az aláíróhoz tartó bájtokról. Előbb mentsen, aztán ellenőrizze elő a mentett fájlt. A memóriabeli állapot piszkozat; csak a lemezen lévő fájl valódi
Előellenőrzés lemezről, és a nulla, amely két dolgot jelent
A lapos előellenőrzési belépési pont a CheckFileCompliance(FileName, Password, ComplianceTest, Options). Az 1-es teszt a PDF/A-t (ISO 19005), a 2-es teszt a PDF/UA-t (ISO 14289) választja. A fájlt a könyvtár folyamatolvasóján át nyitja meg, tehát nincs szükség előzetes LoadFromFile hívásra, és egy sztringlista-azonosítót ad vissza, amelynek minden eleme egy-egy lelet:
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;
A csapda a visszatérési értékben ül, méghozzá az a fajta, amely minden derűs úton haladó teszten átcsúszik. A nulla azt jelenti, hogy „nincs lelet”. A nulla azt is jelenti, hogy „a fájlt nem lehetett megnyitni”, mert a megvalósítás mindig 0-t ad vissza, valahányszor az eredménylista üresen tér vissza, az olvasási hibát is beleértve. Az a munkapad, amely a 0-t zöld jelzésnek olvassa, vidáman jóváhagy egy olyan fájlt, amelyet egy másik folyamat épp zárolva tart. A hívás LastErrorCode ellenőrzéssel való párosítása, ahogy fent látható, választja szét a két esetet. Az ellenőrző ráadásul írástiltó megosztási móddal nyitja meg a fájlt, tehát ha a javítási lépés még mindig fog egy írókezelőt, az előellenőrzés olyan okból bukik el, amelynek semmi köze a megfelelőséghez, és minden köze egy elfelejtett folyam felszabadításához
Amikor nem futószalag, hanem ember olvassa a leleteket, a CreatePreflightReport olvasható jelentéssé formálja őket. A ComparePreflightReports két futás különbségét mutatja meg, ami rendes módja annak, hogy kiderüljön: a javítás felszámolta az eredeti leleteket, és közben nem hozott be némán újakat
Az ellenőrzött revízió aláírása SignProcess segítségével
Miután a mentett revízió átment az előellenőrzésen, és a lenyomata rögzítve van, pontosan azt a fájlt írja alá, és semmi mást. A SignProcess API építőmintaként olvasható. Nyisson egy folyamatazonosítót, állítsa be soronként, véglegesítse, majd olvassa vissza az eredménykódot
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 alapszint
PDF.SetSignProcessDigestAlgorithm(ProcessID, 2); // SHA-256
PDF.SetSignProcessReserveContentsBytes(ProcessID, 8192); // hely egy későbbi időbélyegnek
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
A sorozat két sora többet nyom a latban, mint amennyinek látszik. A SetSignProcessCustomSubFilter az ETSI.CAdES.detached értékkel az ETSI EN 319 142-1 szerint profilozott PAdES aláírást választja a régi adbe.pkcs7.detached család helyett, és ez a különbség egy európai validátor által elfogadott és egy általa megjelölt aláírás között. A SetSignProcessReserveContentsBytes tömi ki a /Contents helyfoglalót, és az itt választott méret a jövőről szóló döntés: ha valaha aláírás-időbélyeg következik, a megnőtt CMS-nek bele kell férnie a most lefoglalt helybe, mert a helyfoglaló később nem tud nőni az egész újraaláírása nélkül. Foglaljon bőven, és pár kilobájtot pazarol. Foglaljon szűken, és az időbélyegzési lépés hónapok múlva bukik el egy túlcsordulással, amelyet nehezen fog visszakötni ehhez az egyetlen sorhoz
A GetSignProcessResult kóddal felel, nem logikai értékkel, és a kódokat érdemes megőrizni. Az 1 a siker. A 4 hibás PDF-jelszó, a 7 hibás tanúsítványjelszó, a 9 olyan PFX, amely nem hordoz privát kulcsot, a 11 pedig az aláírás alkalmazása közben fellépő hiba. Ha ezeket igaz/hamis értékké lapítja, épp azt az információt dobja el, amely megkülönbözteti a hibás jelszavas támogatási esetet a privát rész nélküli kulcstól. Naplózza az egész számot
Visszaolvasás: az imént előállított fájl auditálása
Egyetlen munkapad se bízzon abban az útvonalban, amely megírta a most tanúsítandó fájlt. A TPDFlibSignDoc auditosztály újranyitja az aláírt kimenetet, és közvetlenül a lemezről olvassa az aláírásszótár bejegyzéseit:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// A méret rögzítése az Open előtt: az auditobjektum megosztási zárat tart a fájlon
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 // a > 0 azt jelenti, hogy a mező alá van írva
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;
A ValueKey argumentumok szótárbejegyzésekre képeznek le. A 0-s kulcs a /Contents nyers CMS-ét adja vissza, a 2-es és a 3-as a /Filter és /SubFilter nevét, a 11-től 14-ig terjedők pedig a négy ByteRange-számot. A szöveges értékek ehelyett a GetSignatureTextValueByName hívásán át jönnek: a 0-s kulcs az állított aláírási idő, az 5-ös pedig megkülönbözteti a közönséges Sig mezőt a DocTimeStamp mezőtől, ami akkortól számít, amikor egy dokumentum mindkettőt hordozza
A példa tetején álló fájlméret-rögzítés teherhordó elem, nem házimunka. A TPDFlibSignDoc.Open a teljes élettartama alatt szigorú megosztási zár alatt tartja a fájlt, tehát mindennek, ami a nyers bájtokat igényli (az aláírt tartomány lenyomatolása, a CMS kivonatának újraszámítása), az Open hívása előtt kell elolvasnia a fájlt. A könyvtár saját SigningWorkbench demója pontosan ezért olvassa be előbb az egész fájlt a memóriába, és az a munkapad, amely a sorrendet figyelmen kívül hagyja, esetlegesen hibázik, azon a gépen, amelyik épp elveszíti a versenyfutást
ByteRange-számtan, amely bizonyítja a lefedettséget
Egy egészséges, egyetlen aláírást hordozó fájl ByteRange-e [0 a b c] alakú: a lefedettség a 0 eltolásnál kezdődik, kihagyja a hexadecimális /Contents helyfoglalót a és b között, majd a b+c bájtig folytatódik. Amikor b+c egyenlő a fájl méretével, az aláírás mindent lefed a fájl végéig, és ez a kívánt eredmény. Amikor elmarad tőle, valaki inkrementális frissítést fűzött a fájlhoz az aláírás megírása után. Ez az ISO 32000-1§12.8 szerint teljesen jogszerű, hiszen a későbbi űrlapkitöltések, egy második aláírás és egy DSS szótár is pontosan így érkezik. És pontosan ez az a tény, amelyet egy audit nyomvonalnak aláíráskor kell rögzítenie, nem pedig egy vita hevében rekonstruálnia
Ügyeljen az egész szám szélességére, miközben ezt a számítást végzi. A lapos API GetSignProcessByteRange hívása 32 bites Integer értéket ad vissza, az alatta fekvő értékek viszont Int64 típusúak, tehát egy 2 GB feletti fájlon a lapos hozzáférő némán csonkol. Nyúljon az osztályréteg TPDFlibSigner.GetByteRange hívásáért, amely Int64 értéket ad, vagy fejtse ki az értékeket a GetSignatureValueByName hívásából úgy, ahogy a fenti auditkód teszi
Amit a könyvtár Önre hagy
Két határvonalat jobb tervezéskor megismerni, mint a záró hajrában. A lapos TPDFlib API egyáltalán nem hordoz aláírás-ellenőrző burkolót. A kriptográfiai ellenőrzés egy réteggel lejjebb, a TPDFlibSignatureVerifier osztályban lakik, amelynek VerifySignature metódusa érvényes, érvénytelen vagy ismeretlen választ ad. Beépített HTTP-kliens sincs az RFC 3161 időbélyeg-szolgáltatókhoz. A könyvtár kiszámítja a beküldendő lenyomatot, és visszaágyazza a kibővített CMS-t, amint a token megjön, de a TSA felé menő hálózati körutat Önnek kell megírnia. Mindkettő egyszerűen becsomagolható, és őszintén kellemetlen a kiadás előtti héten hiányzónak találni, ezért az első vázlattól kezdve tervezze be őket
Egy megfelelőségi kérdést érdemes nyíltan lezárni, mert eldönti, hová kerül az utolsó kapu: elrontja-e a PDF/A-t egy aláírás hozzáadása? Önmagában nem. Az aláírás inkrementális frissítésként érkezik, és az ISO 19005-2 óta a szabvány kifejezetten megengedi az aláírt dokumentumokat. A bökkenő az aláírás megjelenése, amely ugyanazon szabályok szerint játszik, mint bármely más oldaltartalom, beágyazott betűkészletekkel és eszközfüggő szín nélkül. Így a munkapad utolsó kapuja még egy előellenőrzési futás, ezúttal az aláírt kimeneten. A CheckFileCompliance hívást kezelje gyors, futószalagon belüli ellenőrzésként, a kiadásra jelölteket pedig továbbra is vizsgálja független eszközzel, például a veraPDF programmal, mivel a validátorok átfedő, de nem azonos szabálykészleteket valósítanak meg; ha a kettő nem ért egyet, a lelet szövege általában megnevezi az elolvasandó szakaszt
Egy sorrendi tanulság mindebből következik. Az aláírás és az időbélyegzés nem egyetlen menet: előbb az alapszintű aláírás íródik meg, majd egy külön időbélyeg-folyamat bővíti a CMS-t a lefoglalt /Contents helyen belül, és épp ezért nyomott olyan sokat a latban a korábbi foglalási sor. Az erre a munkapadra épülő időbélyeg- és hosszú távú érvényesítési rétegekhez a PAdES aláírásról és érvényesítésről szóló végigvezetés viszi az aláírást az alapszinttől a B-LT szintig, az előellenőrzési fél pedig a PDF/A és PDF/UA előellenőrzési útmutatóban megy mélyebbre. A teljes API-dokumentáció és a próbaverziók a PDF Library for Delphi termékoldalán érhetők el