Een workbench die compliance-validatie aan digitale ondertekening koppelt, moet vier stappen coördineren, in deze volgorde, en ze de hele weg door aan één set bytes gebonden houden. Hij draait een PDF/A- of PDF/UA-preflight. Hij past de fixes toe die de bevindingen eisen en slaat een gecorrigeerde revisie op. Hij ondertekent precies die revisie. Daarna leest hij het ondertekende bestand terug en bevestigt dat de handtekening het echt dekt. De volgorde is niet cosmetisch. Sla de teruglezing over en u vertrouwt uw eigen write-path; laat de preflight tegen de verkeerde revisie draaien en uw compliancerapport beschrijft een bestand dat u nooit leverde
Het deel dat de meeste zelfgebouwde pipelines verkeerd krijgen is de naad tussen validatie en ondertekening. Draai ze als twee afzonderlijke tools met een herstel-pas ertussen en er komen minstens drie afzonderlijke revisies van het bestand in bestaan, elk met zijn eigen bytes. Het preflightrapport dat u een auditor overhandigt beschrijft er daar één van. De handtekening bevriest een ander. Niets in het bestand stelt dat het dezelfde revisie is, en vaak zijn ze dat niet. PDF Library for Delphi, de losLab PDF Developer Library voor Delphi en C++Builder, zet preflight en PAdES-ondertekening achter één enkele facade-klasse, zodat de hele sequentie in één proces kan leven dat nooit uit het oog verliest over welke bytes hij het heeft. Elke aanroep hieronder bestaat vandaag in de bibliotheek, en zo ook elke val die ernaast wordt opgemerkt
Drie revisies van één document, en hoe de kloof ontstaat
Tel de saves. Het origineel arriveert van upstream. De herstel-pas laadt het, zet een compliancemodus aan en schrijft een gecorrigeerde revisie. De ondertekeningspas voegt een handtekening toe als een incrementele update, wat een derde schrijfbewerking is. Drie saves, drie byte-indelingen, en een preflightrapport betekent niets tenzij het benoemt welke van de drie het dekt. Een SHA-256 van het bestand, vastgelegd naast elke preflight-run en elke handtekening, is het goedkope ankerpunt dat u laat bewijzen dat de revisie die u valideerde de revisie is die u ondertekende
Eén bibliotheekgedrag trekt die discipline verder aan. Compliance-fixes aangevraagd via SetPDFAMode of SetPDFUAMode treden niet in werking wanneer u ze aanroept. Ze worden tijdens de save toegepast. Auto-reparaties zoals het forceren van annotatie-print-vlaggen of het toewijzen van een PDF/UA-tab-volgorde landen in het outputbestand en nergens anders, dus een controle gedraaid tegen het document dat u zojuist in het geheugen "repareerde" vertelt u niets over de bytes die naar de ondertekenaar gaan. Sla eerst op, doe dan preflight op het opgeslagen bestand. De in-memory-toestand is een concept; alleen het bestand op schijf is echt
Preflight vanaf schijf, en de nul die twee dingen betekent
Het flat preflight-toegangspunt is CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 selecteert PDF/A (ISO 19005), test 2 selecteert PDF/UA (ISO 14289). Het opent het bestand via de streaming-reader van de bibliotheek, dus het is niet nodig om eerst LoadFromFile aan te roepen, en het retourneert een string-list-handle die één bevinding per item draagt:
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;
De val zit in de returnwaarde, en het is het soort dat elke happy-path-test doorstaat. Nul betekent "geen bevindingen." Nul betekent ook "het bestand kon niet geopend worden," omdat de implementatie 0 retourneert telkens wanneer de resultatenlijst leeg terugkomt, een leesfout inbegrepen. Een workbench die 0 als een groen licht leest, zal vrolijk een bestand goedkeuren dat een ander proces heeft vergrendeld. De aanroep paaren met LastErrorCode, zoals hierboven, is wat de twee gevallen scheidt. De checker opent het bestand ook met een deny-write-sharemodus, dus als uw herstel-stap nog een writer-handle vasthoudt, faalt preflight om een reden die niets met compliance te maken heeft en alles met een stream die u vergat vrij te geven
Wanneer een persoon in plaats van een pipeline de bevindingen moet lezen, rendert CreatePreflightReport ze als een leesbaar rapport. ComparePreflightReports diff't twee runs, wat een nette manier is om te laten zien dat de herstel de oorspronkelijke bevindingen opruimde zonder er stilletjes nieuwe te introduceren
De gecontroleerde revisie ondertekenen met een SignProcess
Zodra de opgeslagen revisie door de preflight komt en zijn hash op record staat, ondertekent u precies dat bestand en geen ander. De SignProcess-API leest als een builder. Open een proces-handle, configureer het regel voor regel, commit, en lees dan de resultaatcode terug
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);
Twee regels in die sequentie dragen meer gewicht dan ze lijken. SetSignProcessCustomSubFilter met ETSI.CAdES.detached kiest een PAdES-handtekening zoals geprofileerd in ETSI EN 319 142-1 in plaats van de legacy adbe.pkcs7.detached-familie, wat het verschil is tussen een handtekening die een Europese validator accepteert en een die hij markeert. SetSignProcessReserveContentsBytes vult de /Contents-placeholder op, en de grootte die u hier kiest is een beslissing over de toekomst: als er ooit een handtekening-timestamp volgt, moet de vergrote CMS in de ruimte passen die u nu reserveert, omdat de placeholder later niet kan groeien zonder het geheel opnieuw te ondertekenen. Ruim reserveren en u verspilt een paar kilobytes. Te krap reserveren en de timestamp-stap faalt over maanden met een overflow die u met moeite terugkoppelt naar deze ene regel
GetSignProcessResult antwoordt met een code, niet met een boolean, en de codes zijn het bewaren waard. 1 is succes. 4 is een verkeerd PDF-wachtwoord, 7 een verkeerd certificaatwachtwoord, 9 een PFX die geen private key draagt, 11 een fout tijdens het aanbrengen van de handtekening. Vouw die samen tot een true/false en u gooit het ene stuk informatie weg dat een verkeerd-wachtwoord-supportcase onderscheidt van een key-zonder-private-part-case. Log de integer
Teruglezen: het bestand dat u zojuist produceerde auditen
Geen workbench zou het pad moeten vertrouwen dat het bestand schreef dat hij op het punt staat te certificeren. De audit-klasse TPDFlibSignDoc heropent de ondertekende output en leest de handtekening-woordenlijstvermeldingen rechtstreeks van schijf:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Lees de grootte vóór Open: het audit-object houdt een share lock op het bestand
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 betekent dat het veld ondertekend is
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;
De ValueKey-argumenten mappen op woordenlijstvermeldingen. Key 0 retourneert de rauwe CMS uit /Contents, keys 2 en 3 de /Filter- en /SubFilter-namen, en 11 tot en met 14 de vier ByteRange-nummers. Tekstwaarden komen terug via GetSignatureTextValueByName: key 0 is de geclaimde ondertekeningstijd, en key 5 onderscheidt een gewone Sig van een DocTimeStamp, wat ertoe doet zodra een document beide draagt
De bestandsgrootte-vastlegging bovenaan dat voorbeeld is dragend, geen huishouden. TPDFlibSignDoc.Open houdt het bestand tijdens zijn hele levensduur onder een restrictieve share-lock, dus alles wat de rauwe bytes nodig heeft (de ondertekende range hashen, de CMS-digest herberekenen) moet het bestand lezen voordat Open wordt aangeroepen. De SigningWorkbench-demo van de bibliotheek zelf leest om precies deze reden eerst het hele bestand in het geheugen, en een workbench die de volgorde negeert faalt intermitterend, op welke machine dan ook die toevallig de race verliest
ByteRange-rekenkunde die dekking bewijst
Een gezond bestand met één handtekening heeft een ByteRange van de vorm [0 a b c]: dekking begint bij offset 0, slaat de hex /Contents-placeholder tussen a en b over, en hervat dan tot byte b+c. Wanneer b+c gelijk is aan de bestandsgrootte, dekt de handtekening alles tot het einde van het bestand, wat het resultaat is dat u wilt. Wanneer het tekortschiet, heeft iemand een incrementele update toegevoegd nadat de handtekening was geschreven. Dat is volkomen legitiem onder ISO 32000-1§12.8, aangezien latere formulier-invullingen, een tweede handtekening en een DSS-woordenlijst allemaal precies op deze manier arriveren. Het is ook precies het feit dat een audit-trail op ondertekeningsmoment zou moeten vastleggen in plaats van onder druk te reconstrueren tijdens een geschil
Let op de integer-breedte terwijl u deze rekenkunde doet. De flat-API GetSignProcessByteRange geeft een 32-bit Integer terug, maar de onderliggende waarden zijn Int64, dus op een bestand groter dan 2 GB kapt de flat-accessor stil af. Grijp naar de class-layer TPDFlibSigner.GetByteRange, die Int64 retourneert, of parseer de waarden uit GetSignatureValueByName zoals de audit-code hierboven doet
Wat de bibliotheek aan u overlaat
Twee grenzen zijn beter op ontwerpmoment te leren dan in de laatste sprint. De flat TPDFlib-API draagt helemaal geen handtekening-verificatie-wrapper. Cryptografische verificatie leeft één laag lager, in TPDFlibSignatureVerifier, waarvan VerifySignature antwoordt met valid, invalid of unknown. Er is ook geen ingebouwde HTTP-client voor RFC 3161-timestamp-authorities. De bibliotheek berekent de hash om in te dienen en embedt de vergrote CMS opnieuw zodra een token terugkomt, maar de netwerk-round-trip naar de TSA moet u zelf schrijven. Beide zijn eenvoudig te wrappen en oprecht onaangenaam om te missen in de week voor een release, dus ontwerp ze in vanaf de eerste schets
Eén vraag over compliance is het waard om ronduit te beantwoorden, omdat hij bepaalt waar de laatste poort komt: breekt het toevoegen van een handtekening PDF/A? Niet op zichzelf. De handtekening arriveert als een incrementele update, en ISO 19005-2 en verder staan expliciet ondertekende documenten toe. De kneep is de handtekeningweergave, die onder dezelfde regels valt als alle andere pagina-inhoud, ingebedde lettertypen en geen apparaatafhankelijke kleur inbegrepen. Dus de laatste poort in de workbench is nog één preflight-run, ditmaal tegen de ondertekende output. Behandel CheckFileCompliance als de snelle in-pipeline-controle en verifieer release-kandidaten nog steeds met een onafhankelijke tool zoals veraPDF, aangezien validators overlappende maar niet identieke regelsets implementeren; als de twee van mening verschillen, noemt de bevindingstekst meestal de clausule om te gaan lezen
Eén sequencing-punt valt uit dit alles voort. Ondertekenen en timestampen zijn geen enkele pas: de baseline-handtekening wordt eerst geschreven, daarna versterkt een afzonderlijk timestamp-proces de CMS binnen de gereserveerde /Contents-ruimte, wat precies is waarom de reserve-bytes-regel eerder zoveel gewicht droeg. Voor de timestamp- en langetermijnvalidatie-lagen die op deze workbench bouwen, brengt de PAdES-ondertekening- en validatie-walkthrough de handtekening van baseline naar B-LT, en de preflight-helft gaat dieper in de PDF/A- en PDF/UA-preflightgids. Volledige API-documentatie en trial-downloads staan op de PDF Library for Delphi-productpagina