En arbejdsbænk, der kæder compliance-validering sammen med digital signering, skal koordinere fire trin i netop denne rækkefølge og holde dem bundet til ét og samme sæt bytes hele vejen igennem. Den kører en PDF/A- eller PDF/UA-preflight. Den anvender de rettelser, fundene kræver, og gemmer en korrigeret revision. Den signerer præcis den revision. Derefter læser den den signerede fil tilbage og bekræfter, at signaturen faktisk dækker den. Rækkefølgen er ikke kosmetik. Springer du tilbagelæsningen over, stoler du på din egen skrivesti; lader du preflight køre mod den forkerte revision, beskriver din compliance-rapport en fil, du aldrig har leveret
Det, de fleste hjemmebyggede pipelines rammer forkert, er sømmen mellem validering og signering. Kører man dem som to separate værktøjer med en afhjælpningsrunde imellem, opstår der mindst tre forskellige revisioner af filen, hver med sine egne bytes. Preflight-rapporten, du rækker en revisor, beskriver den ene af dem. Signaturen fryser en anden. Intet i filen fastslår, at de er samme revision, og ofte er de det ikke. PDF Library for Delphi, losLabs PDF Developer Library til Delphi og C++Builder, lægger preflight og PAdES-signering bag én facadeklasse, så hele sekvensen kan leve i én proces, der aldrig mister overblikket over, hvilke bytes den taler om. Hvert eneste kald nedenfor findes i biblioteket i dag, og det samme gør hver eneste faldgrube, der nævnes undervejs
Tre revisioner af ét dokument, og hvordan hullet opstår
Tæl gemmeoperationerne. Originalen ankommer fra et opstrømssystem. Afhjælpningsrunden indlæser den, slår en compliance-tilstand til og skriver en korrigeret revision. Signeringsrunden tilføjer en signatur som en inkrementel opdatering, hvilket er en tredje skrivning. Tre gemmeoperationer, tre byte-layouts, og en preflight-rapport betyder intet, medmindre den navngiver, hvilken af de tre den dækker. En SHA-256 af filen, noteret ved siden af hver preflight-kørsel og hver signatur, er det billige anker, der lader dig bevise, at den revision, du validerede, er den revision, du signerede
Én adfærd i biblioteket strammer den disciplin yderligere. Compliance-rettelser, der anmodes om via SetPDFAMode eller SetPDFUAMode, træder ikke i kraft, når du kalder dem. De anvendes under gemmeoperationen. Automatiske reparationer som at gennemtvinge print-flag på annotationer eller tildele en PDF/UA-tabulatorrækkefølge lander i outputfilen og intet andet sted, så et tjek kørt mod det dokument, du lige har "rettet" i hukommelsen, fortæller dig intet om de bytes, der er på vej til signeringen. Gem først, og kør så preflight på den gemte fil. Tilstanden i hukommelsen er et udkast; kun filen på disken er virkelig
Preflight fra disk, og nullet der betyder to ting
Det flade indgangspunkt til preflight er CheckFileCompliance(FileName, Password, ComplianceTest, Options). Test 1 vælger PDF/A (ISO 19005), test 2 vælger PDF/UA (ISO 14289). Den åbner filen gennem bibliotekets streaming-læser, så der er ingen grund til at kalde LoadFromFile først, og den returnerer et handle til en strengliste med ét fund 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ælden ligger i returværdien, og det er den slags, der består enhver happy path-test. Nul betyder "ingen fund". Nul betyder også "filen kunne ikke åbnes", for implementeringen returnerer 0, hver gang resultatlisten kommer tom tilbage — en læsefejl inklusive. En arbejdsbænk, der læser 0 som grønt lys, vil glad og gerne godkende en fil, som en anden proces har låst. At parre kaldet med LastErrorCode, sådan som ovenfor, er det, der skiller de to tilfælde ad. Checkeren åbner desuden filen med en deny-write share-tilstand, så hvis dit afhjælpningstrin stadig holder et skrive-handle, fejler preflight af en grund, der intet har med compliance at gøre og alt med en stream, du glemte at frigive
Når det er et menneske og ikke en pipeline, der skal læse fundene, gengiver CreatePreflightReport dem som en læsbar rapport. ComparePreflightReports sammenligner to kørsler, hvilket er en ryddelig måde at vise, at afhjælpningen fjernede de oprindelige fund uden stille og roligt at indføre nye
Signering af den tjekkede revision med en SignProcess
Når den gemte revision består preflight, og dens hash er noteret, så signér præcis den fil og ingen anden. SignProcess-API'et læses som en builder. Åbn et proces-handle, konfigurér det linje for linje, commit, og læs så resultatkoden tilbage
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); // plads 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 sekvens vejer tungere, end de ser ud til. SetSignProcessCustomSubFilter med ETSI.CAdES.detached vælger en PAdES-signatur som profileret i ETSI EN 319 142-1 frem for den ældre adbe.pkcs7.detached-familie, og det er forskellen på en signatur, en europæisk validator accepterer, og en, den flagger. SetSignProcessReserveContentsBytes polstrer /Contents-pladsholderen, og den størrelse, du vælger her, er en beslutning om fremtiden: skal der nogensinde følge et signaturtidsstempel efter, skal den forstørrede CMS kunne være i den plads, du reserverer nu, for pladsholderen kan ikke vokse senere uden at hele molevitten signeres om. Reservér rundhåndet, og du spilder nogle få kilobyte. Reservér for stramt, og tidsstempeltrinnet fejler om flere måneder med et overløb, du vil have svært ved at koble tilbage til netop denne ene linje
GetSignProcessResult svarer med en kode, ikke en boolean, og koderne er værd at holde fast i. 1 er succes. 4 er en forkert PDF-adgangskode, 7 en forkert certifikatadgangskode, 9 en PFX uden privat nøgle, 11 en fejl, mens signaturen blev påført. Klapper du dem sammen til true/false, smider du netop den ene oplysning væk, der skiller en supportsag om forkert adgangskode fra en om en nøgle uden privat del. Log heltallet
Tilbagelæsning: revision af den fil, du netop har produceret
Ingen arbejdsbænk bør stole på den sti, der skrev den fil, den er ved at certificere. Revisionsklassen TPDFlibSignDoc genåbner det signerede output og læser signaturordbogens poster direkte fra disken:
var
Doc: TPDFlibSignDoc;
Names: TStringList;
FS: TFileStream;
I: Integer;
SourceSize, RangeStart, GapStart, TailStart, TailLen: Int64;
begin
// Fang størrelsen før Open: revisionsobjektet holder en share-lå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 betyder, at feltet er signeret
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-argumenterne peger på poster i ordbogen. Nøgle 0 returnerer den rå CMS fra /Contents, nøgle 2 og 3 navnene /Filter og /SubFilter, og 11 til 14 de fire ByteRange-tal. Tekstværdier kommer i stedet tilbage via GetSignatureTextValueByName: nøgle 0 er det angivne signeringstidspunkt, og nøgle 5 skiller en almindelig Sig fra en DocTimeStamp, hvilket betyder noget, så snart et dokument bærer begge
Opsamlingen af filstørrelsen øverst i det eksempel er bærende, ikke oprydning. TPDFlibSignDoc.Open holder filen under en restriktiv share-lås i hele sin levetid, så alt, der har brug for de rå bytes (hashing af det signerede område, genberegning af CMS-digestet), skal læse filen, før Open bliver kaldt. Bibliotekets egen SigningWorkbench-demo indlæser hele filen i hukommelsen først af netop den grund, og en arbejdsbænk, der ignorerer rækkefølgen, fejler sporadisk — på den maskine, der tilfældigvis taber kapløbet
ByteRange-aritmetik, der beviser dækningen
En sund fil med én signatur har en ByteRange på formen [0 a b c]: dækningen begynder ved offset 0, springer den hexadecimale /Contents-pladsholder mellem a og b over og fortsætter derefter til og med byte b+c. Når b+c svarer til filstørrelsen, dækker signaturen alt til filens ende, og det er det resultat, du vil have. Når den kommer til kort, har nogen tilføjet en inkrementel opdatering, efter signaturen blev skrevet. Det er fuldt ud legitimt under ISO 32000-1§12.8, eftersom senere formularudfyldninger, en anden signatur og en DSS-ordbog alle ankommer på præcis den måde. Det er også netop den kendsgerning, et revisionsspor bør registrere på signeringstidspunktet frem for at rekonstruere under pres i en tvist
Hold øje med heltalsbredden, mens du regner. GetSignProcessByteRange i det flade API giver en 32-bit Integer tilbage, men de underliggende værdier er Int64, så på en fil over 2 GB afkorter den flade accessor stiltiende. Grib i stedet efter klasselagets TPDFlibSigner.GetByteRange, der returnerer Int64, eller parse værdierne ud af GetSignatureValueByName, sådan som revisionskoden ovenfor gør
Hvad biblioteket overlader til dig
To grænser læres bedre i designfasen end i den sidste sprint. Det flade TPDFlib-API har slet ingen wrapper til signaturverifikation. Den kryptografiske verifikation bor et lag længere nede, i TPDFlibSignatureVerifier, hvis VerifySignature svarer gyldig, ugyldig eller ukendt. Der er heller ingen indbygget HTTP-klient til RFC 3161-tidsstempelmyndigheder. Biblioteket beregner den hash, der skal indsendes, og genindlejrer den udvidede CMS, når et token kommer tilbage, men netværksturen til TSA'en skal du selv skrive. Begge dele er ligetil at pakke ind og oprigtigt ubehagelige at opdage manglende ugen før en udgivelse, så tegn dem ind fra den første skitse
Ét spørgsmål om compliance er værd at afklare klart, for det afgør, hvor den sidste port skal ligge: bryder det PDF/A at tilføje en signatur? Ikke i sig selv. Signaturen ankommer som en inkrementel opdatering, og ISO 19005-2 og fremefter tillader udtrykkeligt signerede dokumenter. Hagen er signaturens udseende, som spiller efter samme regler som alt andet sideindhold, indlejrede skrifttyper og ingen enhedsafhængige farver inklusive. Så den sidste port i arbejdsbænken er endnu en preflight-kørsel, denne gang mod det signerede output. Betragt CheckFileCompliance som det hurtige tjek inde i pipelinen, og verificér stadig release-kandidater med et uafhængigt værktøj som veraPDF, eftersom validatorer implementerer overlappende, men ikke identiske regelsæt; når de to er uenige, navngiver fundets tekst som regel den klausul, man skal slå op
Én pointe om rækkefølgen falder ud af det hele. Signering og tidsstempling er ikke ét gennemløb: baseline-signaturen skrives først, og derefter udvider en separat tidsstempelproces den CMS, der ligger i den reserverede /Contents-plads, hvilket er præcis derfor, linjen med reserverede bytes tidligere vejede så tungt. For de lag med tidsstempling og langtidsvalidering, der bygger oven på denne arbejdsbænk, fører gennemgangen af PAdES-signering og -validering signaturen fra baseline til B-LT, og preflight-halvdelen går i dybden i guiden til PDF/A- og PDF/UA-preflight. Fuld API-dokumentation og prøveversioner findes på PDF Library for Delphi-produktsiden