En arbeidsbenk som kjeder samsvarsvalidering sammen med digital signering må koordinere fire trinn, i denne rekkefølgen, og holde dem knyttet til ett og samme sett med byte hele veien. Den kjører en PDF/A- eller PDF/UA-preflight. Den bruker de rettelsene funnene krever, og lagrer en korrigert revisjon. Den signerer nøyaktig den revisjonen. Deretter leser den den signerte filen tilbake og bekrefter at signaturen virkelig dekker den. Rekkefølgen er ikke kosmetisk. Hopper du over tilbakelesingen, stoler du på din egen skrivevei; lar du preflighten kjøre mot feil revisjon, beskriver samsvarsrapporten en fil du aldri leverte
Den delen de fleste hjemmesnekrede pipelinene bommer på, er skjøten mellom validering og signering. Kjører du dem som to separate verktøy med en utbedringsrunde imellom, oppstår minst tre ulike revisjoner av filen, hver med sine egne byte. Preflight-rapporten du gir en revisor, beskriver den ene. Signaturen fryser en annen. Ingenting i filen sier at de er samme revisjon, og ofte er de det ikke. PDF Library for Delphi, losLabs PDF Developer Library for Delphi og C++Builder, samler preflight og PAdES-signering bak én fasadeklasse, slik at hele sekvensen kan leve i én prosess som aldri mister oversikten over hvilke byte den snakker om. Hvert kall nedenfor finnes i biblioteket i dag, og det samme gjør hver felle som nevnes ved siden av
Tre revisjoner av ett dokument, og hvordan gapet oppstår
Tell lagringene. Originalen kommer fra et oppstrømssystem. Utbedringsrunden laster den, slår på en samsvarsmodus og skriver en korrigert revisjon. Signeringsrunden legger til en signatur som en inkrementell oppdatering, og det er en tredje skriving. Tre lagringer, tre byteoppsett, og en preflight-rapport betyr ingenting med mindre den navngir hvilken av de tre den dekker. En SHA-256 av filen, notert ved siden av hver preflight-kjøring og hver signatur, er det billige ankeret som lar deg bevise at revisjonen du validerte, er revisjonen du signerte
Én oppførsel i biblioteket strammer den disiplinen ytterligere. Samsvarsrettelser som bes om gjennom SetPDFAMode eller SetPDFUAMode, trer ikke i kraft når du kaller dem. De blir anvendt under lagringen. Automatiske reparasjoner som å tvinge fram utskriftsflagg på annotasjoner eller tildele en PDF/UA-tabulatorrekkefølge havner i utdatafilen og ingen andre steder, så en kontroll kjørt mot dokumentet du nettopp «fikset» i minnet, forteller deg ingenting om de bytene som er på vei til signereren. Lagre først, kjør så preflight på den lagrede filen. Tilstanden i minnet er et utkast; bare filen på disk er virkelig
Preflight fra disk, og nullen som betyr to ting
Det flate inngangspunktet for preflight er CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 velger PDF/A (ISO 19005), test 2 velger PDF/UA (ISO 14289). Den åpner filen gjennom bibliotekets strømmende leser, så det er ikke nødvendig å kalle LoadFromFile først, og den returnerer et strenglistehåndtak som bærer ett funn per oppføring:
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;
Fellen sitter i returverdien, og den er av den typen som består enhver test langs den lykkelige stien. Null betyr «ingen funn». Null betyr også «filen kunne ikke åpnes», fordi implementasjonen returnerer 0 hver gang resultatlisten kommer tom tilbake, en lesefeil inkludert. En arbeidsbenk som leser 0 som grønt lys, godkjenner villig en fil som en annen prosess har låst. Å pare kallet med LastErrorCode, slik som ovenfor, er det som skiller de to tilfellene. Kontrolløren åpner dessuten filen med en delingsmodus som nekter skriving, så hvis utbedringstrinnet ditt fortsatt holder et skrivehåndtak, feiler preflighten av en grunn som ikke har noe med samsvar å gjøre, og alt med en strøm du glemte å frigjøre
Når et menneske og ikke en pipeline skal lese funnene, gjengir CreatePreflightReport dem som en lesbar rapport. ComparePreflightReports sammenligner to kjøringer, som er en ryddig måte å vise at utbedringen fjernet de opprinnelige funnene uten stilltiende å innføre nye
Signering av den kontrollerte revisjonen med en SignProcess
Når den lagrede revisjonen har bestått preflight og hashen er notert, signerer du nøyaktig den filen og ingen annen. SignProcess-API-et leser som en byggmester. Åpne et prosesshåndtak, konfigurer det linje for linje, bekreft, og les så resultatkoden tilbake
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); // plass til et senere tidsstempel
PDF.EndSignProcessToFile(ProcessID, 'invoice-signed.pdf');
if PDF.GetSignProcessResult(ProcessID) <> 1 then
Writeln('Sign failed, code ', PDF.GetSignProcessResult(ProcessID));
PDF.ReleaseSignProcess(ProcessID);
To linjer i den sekvensen veier tyngre enn de ser ut til. SetSignProcessCustomSubFilter med ETSI.CAdES.detached velger en PAdES-signatur slik den er profilert i ETSI EN 319 142-1, framfor den eldre adbe.pkcs7.detached-familien, og det er forskjellen på en signatur en europeisk validator godtar, og en den flagger. SetSignProcessReserveContentsBytes polstrer plassholderen /Contents, og størrelsen du velger her, er en beslutning om framtiden: skal et signaturtidsstempel noen gang følge etter, må den utvidede CMS-en få plass i det du reserverer nå, for plassholderen kan ikke vokse senere uten at hele dokumentet signeres på nytt. Reserverer du rundhåndet, kaster du bort noen kilobyte. Reserverer du for knapt, feiler tidsstemplingstrinnet om mange måneder med et overløp du vil slite med å knytte tilbake til denne ene linjen
GetSignProcessResult svarer med en kode, ikke en boolsk verdi, og kodene er verdt å ta vare på. 1 er suksess. 4 er feil PDF-passord, 7 feil sertifikatpassord, 9 en PFX som ikke bærer noen privat nøkkel, 11 en feil mens signaturen ble påført. Slår du dem sammen til sant/usant, kaster du bort den ene opplysningen som skiller en støttesak om feil passord fra en om en nøkkel uten privat del. Logg heltallet
Tilbakelesing: å granske filen du nettopp produserte
Ingen arbeidsbenk bør stole på den skriveveien som produserte filen den er i ferd med å sertifisere. Granskingsklassen TPDFlibSignDoc åpner den signerte utdatafilen på nytt og leser oppføringene i signaturordboken rett fra disk:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Fang størrelsen før Open: granskingsobjektet holder en dellås på filen
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 betyr at feltet er signert
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-argumentene kartlegges mot oppføringer i ordboken. Nøkkel 0 returnerer rå CMS fra /Contents, nøkkel 2 og 3 navnene /Filter og /SubFilter, og 11 til 14 de fire ByteRange-tallene. Tekstverdier kommer i stedet tilbake gjennom GetSignatureTextValueByName: nøkkel 0 er det oppgitte signeringstidspunktet, og nøkkel 5 skiller en vanlig Sig fra en DocTimeStamp, noe som betyr noe så snart et dokument bærer begge
Innhentingen av filstørrelsen øverst i det eksempelet er bærende, ikke husholdning. TPDFlibSignDoc.Open holder filen under en restriktiv dellås gjennom hele sin levetid, så alt som trenger de rå bytene (hashing av det signerte området, ny beregning av CMS-digesten) må lese filen før Open blir kalt. Bibliotekets egen SigningWorkbench-demo leser hele filen inn i minnet først av nøyaktig denne grunnen, og en arbeidsbenk som overser rekkefølgen, feiler sporadisk, på den maskinen som tilfeldigvis taper kappløpet
ByteRange-aritmetikk som beviser dekning
En sunn fil med én signatur har et ByteRange på formen [0 a b c]: dekningen starter på offset 0, hopper over den heksadesimale plassholderen /Contents mellom a og b, og fortsetter så gjennom byte b+c. Når b+c er lik filstørrelsen, dekker signaturen alt til slutten av filen, og det er resultatet du vil ha. Når den kommer til kort, har noen lagt til en inkrementell oppdatering etter at signaturen ble skrevet. Det er fullt legitimt etter ISO 32000-1§12.8, siden senere skjemautfyllinger, en andre signatur og en DSS-ordbok alle kommer nøyaktig på denne måten. Det er også nettopp det faktumet et revisjonsspor bør notere på signeringstidspunktet, framfor å rekonstruere det under press i en tvist
Følg med på heltallsbredden mens du gjør denne aritmetikken. Det flate API-ets GetSignProcessByteRange gir tilbake en 32-bits Integer, men de underliggende verdiene er Int64, så på en fil over 2 GB avkorter den flate aksessoren stilltiende. Grip heller til klasselagets TPDFlibSigner.GetByteRange, som returnerer Int64, eller parse verdiene ut av GetSignatureValueByName slik granskingskoden ovenfor gjør
Hva biblioteket overlater til deg
To grenser læres bedre ved designbordet enn i sluttspurten. Det flate TPDFlib-API-et bærer ingen innpakning for signaturverifisering i det hele tatt. Kryptografisk verifisering ligger ett lag ned, i TPDFlibSignatureVerifier, hvis VerifySignature svarer gyldig, ugyldig eller ukjent. Det finnes heller ingen innebygd HTTP-klient for RFC 3161-tidsstempelmyndigheter. Biblioteket beregner hashen som skal sendes inn, og bygger den utvidede CMS-en inn igjen når et token kommer tilbake, men nettverksrunden til TSA-en må du skrive selv. Begge er greie å pakke inn og genuint ubehagelige å oppdage som manglende uken før en utgivelse, så tegn dem inn fra første skisse
Ett spørsmål om samsvar er verdt å avklare rett fram, fordi det avgjør hvor den siste porten skal stå: bryter det PDF/A å legge til en signatur? Ikke i seg selv. Signaturen kommer som en inkrementell oppdatering, og ISO 19005-2 og senere tillater eksplisitt signerte dokumenter. Haken er signaturens utseende, som følger de samme reglene som alt annet sideinnhold, innebygde skrifter og fravær av enhetsavhengige farger inkludert. Så den siste porten i arbeidsbenken er nok en preflight-kjøring, denne gangen mot den signerte utdatafilen. Behandle CheckFileCompliance som den raske kontrollen inne i pipelinen, og verifiser likevel utgivelseskandidater med et uavhengig verktøy som veraPDF, siden validatorer implementerer overlappende, men ikke identiske regelsett; når de to er uenige, navngir funnteksten som regel klausulen du bør lese
Ett rekkefølgepoeng faller ut av alt dette. Signering og tidsstempling er ikke én runde: baseline-signaturen skrives først, deretter utvider en separat tidsstemplingsprosess CMS-en inne i den reserverte /Contents-plassen, som er nøyaktig derfor linjen om reserverte byte tidligere veide så tungt. For tidsstempel- og langtidsvalideringslagene som bygger på denne arbeidsbenken, tar gjennomgangen av PAdES-signering og -validering signaturen fra baseline til B-LT, og preflight-halvdelen går dypere i guiden til PDF/A- og PDF/UA-preflight. Fullstendig API-dokumentasjon og prøveversjoner finnes på produktsiden for PDF Library for Delphi