En arbetsbänk (workbench) som kedjar (chains) efterlevnadsvalidering till digital signering måste samordna fyra steg, i denna ordning, och hålla dem knutna till en och samma uppsättning bytes hela vägen igenom. Den kör en PDF/A- eller PDF/UA-preflight. Den tillämpar de fixar som resultaten (findings) kräver och sparar en korrigerad revision. Den signerar exakt den revisionen. Sedan läser den tillbaka den signerade filen och bekräftar att signaturen verkligen täcker den. Ordningen är inte kosmetisk. Hoppa över tillbakaläsningen (read-back) och du litar på din egen skrivväg (write path); låt preflighten köra mot fel revision och din efterlevnadsrapport beskriver en fil du aldrig skeppade
Den del de flesta hemmasnickrade pipelines får fel är sömmen (seam) mellan validering och signering. Kör dem som två separata verktyg med en åtgärdspassering (remediation pass) emellan och minst tre distinkta revisioner av filen uppstår, var och en med sina egna bytes. Preflight-rapporten du lämnar till en revisor (auditor) beskriver en av dem. Signaturen fryser en annan. Ingenting i filen anger att de är samma revision, och ofta är de inte det. PDFlibPas, losLabs PDF-utvecklarbibliotek för Delphi och C++Builder, lägger preflight och PAdES-signering bakom en enda fasadklass, så att hela sekvensen kan leva i en process som aldrig tappar bort vilka bytes den pratar om. Varje anrop nedan existerar i biblioteket idag, och det gör även varje fälla som noteras bredvid det
Tre revisioner av ett dokument, och hur klyftan öppnar sig
Räkna sparningarna (saves). Originalet anländer uppströms (upstream). Åtgärdspasseringen (remediation pass) laddar den, slår på ett efterlevnadsläge (compliance mode) och skriver en korrigerad revision. Signeringspasseringen lägger till en signatur som en inkrementell uppdatering, vilket är en tredje skrivning. Tre sparningar, tre bytelayouter, och en preflight-rapport betyder ingenting såvida den inte namnger vilken av de tre den täcker. En SHA-256 av filen, registrerad bredvid varje preflight-körning och varje signatur, är det billiga ankaret som låter dig bevisa att revisionen du validerade är revisionen du signerade
Ett biblioteksbeteende stramar åt (tightens) den disciplinen ytterligare. Efterlevnadsfixar som begärs genom SetPDFAMode eller SetPDFUAMode träder inte i kraft (take effect) när du anropar dem. De tillämpas under sparningen. Autoreparationer som att tvinga fram annotationsutskriftsflaggor (annotation print flags) eller att tilldela en PDF/UA-tabbordning landar i utdatafilen och ingen annanstans, så en kontroll som körs mot dokumentet du precis "fixade" i minnet säger dig ingenting om de bytes som är på väg till signeraren. Spara först, preflighta sedan den sparade filen. Tillståndet i minnet är ett utkast (draft); endast filen på disken är verklig
Preflight från disk, och nollan som betyder två saker
Den platta preflight-ingångspunkten (flat preflight entry point) är CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 väljer PDF/A (ISO 19005), test 2 väljer PDF/UA (ISO 14289). Den öppnar filen genom bibliotekets strömmande läsare (streaming reader), så det finns inget behov av att anropa LoadFromFile först, och den returnerar ett string-list-handtag (string-list handle) som bär ett resultat (finding) per post:
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;
Fällan sitter i returvärdet, och det är av den sorten som klarar varje happy-path-test. Noll betyder "inga resultat" (no findings). Noll betyder också "filen kunde inte öppnas," eftersom implementationen returnerar 0 närhelst resultatlistan kommer tillbaka tom, vilket inkluderar ett läsfel. En arbetsbänk som läser 0 som grönt ljus kommer glatt att godkänna en fil som någon annan process har låst. Att para ihop anropet med LastErrorCode, som ovan, är det som separerar de två fallen. Kontrolleraren öppnar också filen med ett deny-write share mode, så om ditt åtgärdssteg fortfarande håller ett skrivarhandtag (writer handle), misslyckas preflighten av en anledning som inte har något med efterlevnad att göra och allt att göra med en ström (stream) du glömde att frigöra
När en person snarare än en pipeline behöver läsa resultaten, renderar CreatePreflightReport dem som en läsbar rapport. ComparePreflightReports diffar två körningar, vilket är ett snyggt (tidy) sätt att visa att åtgärden (remediation) rensade de ursprungliga resultaten utan att i smyg introducera nya
Signera den kontrollerade revisionen med en SignProcess
Så snart den sparade revisionen passerar preflighten och dess hash finns registrerad, signera exakt den filen och ingen annan. SignProcess-API:et läses som en builder (builder). Öppna ett processhandtag, konfigurera det rad för rad, bekräfta (commit), och läs sedan tillbaka resultatkoden
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);
Två rader i den sekvensen bär mer vikt än de ser ut att göra. SetSignProcessCustomSubFilter med ETSI.CAdES.detached väljer en PAdES-signatur profilerad enligt ETSI EN 319 142-1 istället för den äldre adbe.pkcs7.detached-familjen, vilket är skillnaden mellan en signatur en europeisk validator accepterar och en den flaggar. SetSignProcessReserveContentsBytes drygar ut (pads) /Contents-platshållaren (placeholder), och storleken du väljer här är ett beslut om framtiden: om en signaturstidsstämpel (signature timestamp) någonsin ska följa, måste den utvidgade CMS:en (enlarged CMS) passa i det utrymme du reserverar nu, eftersom platshållaren inte kan växa senare utan att signera om alltsammans. Reservera generöst och du slösar bort ett par kilobyte. Reservera för snävt och tidsstämpelsteget misslyckas månader från nu med ett spill (overflow) du kommer ha svårt att koppla tillbaka till denna enda rad
GetSignProcessResult svarar med en kod, inte en boolean, och koderna är värda att behålla. 1 är framgång. 4 är fel PDF-lösenord, 7 ett felaktigt certifikatlösenord, 9 en PFX som inte bär på någon privat nyckel, 11 ett misslyckande medan signaturen applicerades. Kollapsa dem till ett sant/falskt och du kastar bort det enda stycke information som skiljer ett fel-lösenord-supportärende (wrong-password support case) från ett nyckel-utan-privat-del-ärende (key-without-private-part one). Logga heltalet
Tillbakaläsning (Read-back): revision (auditing) av filen du just producerade
Ingen arbetsbänk bör lita på sökvägen (path) som skrev filen den ska till att intyga (certify). Revisionsklassen TPDFlibSignDoc öppnar om den signerade utmatningen och läser signaturordboksposterna (signature dictionary entries) rakt från disken:
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;
ValueKey-argumenten mappar till ordboksposter (dictionary entries). Nyckel 0 returnerar det råa CMS:et från /Contents, nycklar 2 och 3 namnen /Filter och /SubFilter, och 11 till 14 de fyra ByteRange-numren. Textvärden kommer tillbaka genom GetSignatureTextValueByName i stället: nyckel 0 är den påstådda (claimed) signeringstiden, och nyckel 5 skiljer en vanlig Sig från en DocTimeStamp, vilket spelar roll så snart ett dokument bär på båda
Filstorleksfångsten (file-size capture) längst upp i det exemplet är bärande (load-bearing), inte städning (housekeeping). TPDFlibSignDoc.Open håller filen under ett restriktivt delat lås (restrictive share lock) under hela sin livstid, så allt som behöver de råa byten (att hasha det signerade omfånget, omberäkna CMS-digesten) måste läsa filen innan Open anropas. Bibliotekets egna SigningWorkbench-demo läser in hela filen i minnet först av exakt denna anledning, och en arbetsbänk som struntar i ordningsföljden misslyckas intermittent (intermittently), på den maskin som råkar förlora kapplöpningen (lose the race)
ByteRange-aritmetik som bevisar täckning
En frisk ensignaturfil (single-signature file) har en ByteRange med formen [0 a b c]: täckningen börjar på offset 0, hoppar över den hex-baserade /Contents-platshållaren mellan a och b, och återupptas sedan genom byte b+c. När b+c motsvarar filstorleken täcker signaturen allt fram till filens slut (end of file), vilket är det resultat du vill ha. När det inte räcker till (falls short), har någon lagt till en inkrementell uppdatering efter att signaturen skrevs. Det är fullt legitimt enligt ISO 32000-1§12.8, eftersom senare formulärifyllningar (form fills), en andra signatur, och en DSS-ordbok alla anländer på exakt detta sätt. Det är också precis det faktum som ett granskningsspår (audit trail) bör registrera vid signeringstillfället snarare än att rekonstruera det under press under en tvist (dispute)
Håll ett öga på heltalsbredden (integer width) medan du gör denna aritmetik. Det platta API:ets (flat API's) GetSignProcessByteRange lämnar tillbaka en 32-bitars Integer, men de underliggande värdena är Int64, så på en fil över 2 GB trunkerar den platta åtkomstmetoden (flat accessor) tyst. Sträck dig efter klasslagrets TPDFlibSigner.GetByteRange, vilket returnerar Int64, eller tolka (parse) ut värdena ur GetSignatureValueByName på samma sätt som granskningskoden (audit code) ovan gör
Vad biblioteket lämnar till dig
Två gränser (boundaries) lärs bäst in vid designtillfället än i slutsprinten. Det platta TPDFlib API:et bär inte på något omslag (wrapper) för signaturverifiering alls. Kryptografisk verifiering lever ett lager ner, i TPDFlibSignatureVerifier, vars VerifySignature svarar giltig (valid), ogiltig (invalid), eller okänd. Det finns heller ingen inbyggd HTTP-klient för RFC 3161 tidsstämpelsauktoriteter. Biblioteket beräknar den hash som ska skickas in och bäddar in (re-embeds) det utökade CMS:et på nytt när en token kommer tillbaka, men nätverksresan (network round trip) till TSA:n är din att skriva. Båda är okomplicerade (straightforward) att wrappa och genuint obehagliga att upptäcka saknas veckan innan en release, så designa in dem från den första skissen
En fråga kring efterlevnad är värd att göra upp i klarspråk (settling plainly), eftersom den avgör var den sista grinden (gate) hamnar: bryter ett tillägg av en signatur mot PDF/A? Inte i sig självt. Signaturen anländer som en inkrementell uppdatering, och ISO 19005-2 och framåt tillåter explicit signerade dokument. Haken (catch) är signaturens utseende (appearance), som lyder under samma regler som allt annat sidinnehåll, inbäddade typsnitt och ingen enhetsberoende (device-dependent) färg inkluderat. Så den sista grinden i arbetsbänken är ännu en preflight-körning, denna gång mot den signerade utmatningen. Behandla CheckFileCompliance som den snabba i-pipeline-kontrollen och verifiera fortfarande releasekandidater med ett oberoende verktyg som veraPDF, eftersom validatorer implementerar överlappande men inte identiska regelverk; när de två inte är överens, namnger oftast resultattexten (finding text) klausulen man ska gå och läsa
En sekvenseringspunkt (sequencing point) faller ut från allt detta. Signering och tidsstämpling är inte en enda passering (single pass): grundsignaturen (baseline signature) skrivs först, sedan utökar en separat tidsstämpelprocess CMS:et inuti det reserverade /Contents-utrymmet, vilket är exakt varför reserve-bytes-raden tidigare bar så mycket vikt. För tidsstämpel- och långtidsvalideringslagren som bygger på denna arbetsbänk, bär genomgången av PAdES-signering och validering signaturen från baseline till B-LT, och preflight-halvan går djupare i PDF/A och PDF/UA preflight-guiden. Fullständig API-dokumentation och utvärderingsnedladdningar lever på PDFlibPas produktsida