En signeret PDF, der er ændret efter signering, er ikke automatisk defekt. ISO 32000-1 tillader inkrementelle opdateringer oven på en signatur, og kun nogle af dem bryder den politik, underskriveren satte. HotPDF Component til Delphi og C++Builder besvarer det spørgsmål med AnalyzeLoadedSignatureRevisions, som klassificerer hver revision efter signering og bedømmer den mod DocMDP og FieldMDP. Scenariet er velkendt for alle, der leverer kontraktsoftware: kunden underskriver en købsaftale, sender den ud, og får den tilbage med en bilagsside vedhæftet. Læseren viser en gul bjælke, der siger, at signaturen er intakt, men dokumentet er ændret siden underskrift, og ingen i rummet kan sige, om det er et normalt medunderskrift-workflow, eller om nogen stille redigerer en signeret kontrakt
Hvad tæller som en lovlig ændring efter signering?
En ændring er lovlig, når dens semantiske kategori falder inden for den tilladelse, den certificerende signatur erklærede. ISO 32000-1 §12.8.2.2 definerer DocMDP-transformen med en /P-værdi på 1, 2 eller 3: 1 tillader ingen ændringer overhovedet, 2 tillader udfyldning af formularer og signering, 3 tillader udfyldning af formularer, signering og annoteringer. HotPDF eksponerer dem som THPDFDocMDPPermission-værdierne dmpNoChanges, dmpFormFillAndSign og dmpFormFillSignAndAnnotate, med dmpNone reserveret til inspektionsresultater uden nogen DocMDP-transform overhovedet
Kategorierne er ordnede, og den ordning er hele motoren i tjekket. THPDFRevisionModificationLevel kører rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, bevidst arrangeret sådan, at et højere ordinaltal aldrig er mindre restriktivt. Et helt dokument reduceres til det maksimale niveau observeret på tværs af hver revision efter signaturen, og DocMDP-sammenligningen bliver en enkelt heltalstest. Én nuance betyder noget tidligt: ved dmpNoChanges accepterer analysen stadig rmlLongTermValidation. At tilføje DSS- og VRI-valideringsmateriale eller et dokumenttidsstempel til en certificeret fil er vedligeholdelse af signaturen, ikke ændring af dokumentet, og at behandle det som en overtrædelse ville ødelægge hvert eneste langtidsarkiveringsworkflow, der findes
Hvordan genopbygger HotPDF revisionskæden?
Strukturelt, ikke heuristisk. Ifølge ISO 32000-1 §7.5.6 tilføjer en inkrementel opdatering en ny cross-reference-sektion, hvis /Prev peger på den forrige, så HotPDF læser startxref fra halen, parser sektionen der, følger /Prev baglæns og gentager, og returnerer sektionerne med den ældste først. To sikkerhedsgrænser sidder i den løkke, og begge er værd at kende, når man fejlretter en fil, der fejler: en /Prev, der peger på en offset, der allerede er besøgt, afslutter gennemgangen med en eksplicit cyklusdiagnose i stedet for at spinde rundt, og en kæde længere end tusind revisioner afvises helt. Begge dele viser sig i Analysis.Issue med funktionen, der returnerer False, og ingen af dem bør glattes over, fordi en cyklisk /Prev er en misdannet eller fjendtlig fil frem for en usædvanlig én
Fire historiske former dukker op i virkelige dokumenter, og alle fire håndteres: traditionelle xref-tabeller parset linje for linje, cross-reference-streams dekomprimeret og afkodet via deres /W- og /Index-felter, hybrid-reference-filer, hvis traditionelle trailer bærer en /XRefStm-nøgle, der parses og flettes ind i samme revision (Office-producer-tilfældet, dækket i artiklen om hybride cross-reference-streams), og objekter, der lever inde i en ObjStm-container, hvilket betyder noget, fordi en moderne opdatering normalt putter det ændrede dictionary i en komprimeret strøm frem for at skrive det direkte, som beskrevet i stykket om objektstrømme og inkrementelle opdateringer. Signaturen forankrer opdelingen: /ByteRange[2] + /ByteRange[3] bliver til SignedRevisionLength, og hver sektion på eller ud over den offset er efter signering. Om byte-intervallet stadig hasher korrekt, er et separat spørgsmål, besvaret af VerifyLoadedSignature og dækket i artiklen om verificering af digitale PDF-signaturer
Hvordan klassificeres hvert ændret objekt?
Klassificering kører pr. objekt og forplanter sig så langs referencer. For hvert objektnummer, som en sektion efter signering rører ved, læser HotPDF den nye krop og kroppen, som den stod i det signerede snapshot; en identisk krop er rmlNone, fordi producenter reelt omskriver objekter uden at ændre dem. Genkenderne er snævre med vilje. Et /Type /DocTimeStamp-objekt, eller ét hvis /SubFilter er ETSI.RFC3161, er rmlLongTermValidation, ligesom alt tilgængeligt fra katalogets /DSS-træ; et /Type /Sig-dictionary er rmlFormFillAndSign. For containere er testen, hvilke nøgler der flyttede sig, ikke hvad objektet er: kataloget må kun tilføje eller ændre /DSS, /Extensions eller /AcroForm; AcroForm-dictionary'et kun /Fields, /SigFlags, /NeedAppearances, /DR, /DA eller /Q; en side kun /Annots; et felt eller widget kun /V, /AP, /AS eller /M. Alt uden for de sæt falder til rmlOther, hvilket er præcis, hvordan den tilføjede bilagsside fanges: at tilføje en side omorganiserer sidetræet på måder, ingen whitelist dækker, og ingen mængde legitim formularudfyldning ligner det
Så forplanter niveauerne sig, hvor hver container arver det maksimale niveau af de ændrede børn, den peger på, gentaget indtil tildelingen stabiliserer sig. Det er det, der får appearance streams til at virke. Et udfyldt tekstfelt omskriver /V og peger på en frisk /AP-strøm, og den strøm er alene en anonym blob af indholdsoperatorer uden type at genkende; fordi feltet, der ejer den, er rmlFormFillAndSign, arver strømmen samme niveau i stedet for at falde igennem til rmlOther. Samme forplantning bærer DSS-kontekst videre til certifikat- og tilbagekaldelses-streams, der ellers ville være uklassificerbare
Hvorfor tæller et ulæseligt objekt som en overtrædelse?
Fordi alternativet er en validator, besejret ved at skrive noget, den ikke forstår. Tre situationer ender ved rmlOther uden appel i HotPDF: et objekt, hvis krop ikke kunne læses fra revisionen, et objekt, revisionen markerer som frit, og et objekt, der matcher ingen af genkenderne ovenfor. Hver registrerer en specifik diagnose i revisionens Issue-felt, så en operatør kan se, hvilket objektnummer der producerede dommen
Frigørelse er den skarpeste af de tre. En revision efter signering, der markerer et tidligere defineret objekt som frit, har slettet indhold fra et signeret dokument, og intet tilladelsesniveau under §12.8.2.2 tillader det; objektnumrene lander i FreedObjectNumbers, og revisionen hæves til rmlOther. Ulæselige objekter følger samme logik af en anden grund. En validator, der ikke kan parse et objekt, har intet grundlag for at kalde det harmløst, og det ærlige svar på det er ikke tavshed. At rapportere en usædvanlig, men uskadelig konstruktion som en overtrædelse koster et menneskeligt review; den modsatte fejl sender en signeret kontrakt af sted med en ubemærket redigering indeni
Aflæsning af dommen i Delphi
Kaldet er kort. Indlæs dokumentet, vælg et signaturindeks, læs recorden; overloadet uden parametre genåbner filen, dokumentet blev indlæst fra, og TStream-overloadet tager kalderleverede bytes og genopretter strøm-positionen, før det returnerer. PolicyCompliant er den ene boolean, de fleste kaldere ønsker, og den kombinerer tre uafhængige beslutninger: den strukturelle gyldighed af tilladelses-dictionaries, DocMDPCompliant og FieldMDPCompliant. Hold komponenterne synlige i din UI frem for at kollapse dem, og bemærk, at et dokument uden DocMDP-transform lader DocMDPCompliant forblive True, da en almindelig godkendelsessignatur ikke erklærer nogen politik at bryde, og den samlede ModificationLevel derefter er beskrivende frem for en dom
var
Pdf: THotPDF;
Analysis: THPDFSignatureRevisionAnalysis;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-countersigned.pdf') > 0 then
begin
if Pdf.AnalyzeLoadedSignatureRevisions(0, Analysis) then
begin
if Analysis.PolicyCompliant then
Writeln('Post-signature changes stay inside the signing policy')
else
Writeln('Policy violation: ', string(Analysis.Issue));
end
else
Writeln('Analysis could not run: ', string(Analysis.Issue));
end;
finally
Pdf.Free;
end;
end;
Til triage vil man som regel have opdelingen pr. revision frem for opsummeringen, fordi den siger, hvornår i dokumentets historik tingene gik galt. Hver post i Analysis.Revisions bærer sit indeks i kæden, den cross-reference-offset, den blev skrevet ved, sit eget ændringsniveau og de involverede objektnumre
const
LevelNames: array[THPDFRevisionModificationLevel] of string =
('none', 'long-term validation', 'form fill and sign',
'annotations', 'other');
var
I: Integer;
begin
Writeln(Format('%d revisions in chain, signature sits at index %d',
[Analysis.TotalRevisionCount, Analysis.SignedRevisionIndex]));
for I := 0 to High(Analysis.Revisions) do
Writeln(Format(' rev %d at offset %d: %s (%d changed, %d freed) %s',
[Analysis.Revisions[I].RevisionIndex,
Analysis.Revisions[I].XRefOffset,
LevelNames[Analysis.Revisions[I].ModificationLevel],
Length(Analysis.Revisions[I].ChangedObjectNumbers),
Length(Analysis.Revisions[I].FreedObjectNumbers),
string(Analysis.Revisions[I].Issue)]));
end;
FieldMDP bedømmes separat, og det er bevidst
Et dokument kan overholde DocMDP og stadig være ulovligt, hvilket er grunden til, at FieldMDPCompliant er en selvstændig boolean frem for foldet ind i niveausammenligningen. ISO 32000-1 §12.8.2.4 definerer FieldMDP-transformen, og §12.7.5.5 den relaterede /SigFieldLock-post, til at fastfryse navngivne formularfelter på signeringstidspunktet, selv hvor dokumentet som helhed stadig tillader formularudfyldning. At udfylde et felt er en niveau-2-handling; at udfylde et felt, underskriveren har låst, er en overtrædelse uanset niveau. HotPDF læser omfanget ind i THPDFFieldLockAction som flaAll, flaInclude eller flaExclude, med flaNone til resultater uden nogen lock-politik, og navnene ind i Permissions.FieldNames: flaAll låser alt, flaInclude låser de opførte navne, flaExclude låser alt undtagen dem. Én detalje betyder noget ved læsning af resultaterne, idet kun felter, der allerede fandtes i det signerede snapshot, rapporteres i ChangedFieldNames, fordi et felt, der er oprettet helt efter signering, ikke har nogen signeret tilstand at modsige, og det fanges i stedet af DocMDP-stien
var
Source: TFileStream;
Analysis: THPDFSignatureRevisionAnalysis;
I: Integer;
begin
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
if Pdf.AnalyzeLoadedSignatureRevisions(0, Source, Analysis) then
if Analysis.Permissions.HasFieldMDP and (not Analysis.FieldMDPCompliant) then
for I := 0 to High(Analysis.ChangedFieldNames) do
Writeln('modified after locking: ',
string(Analysis.ChangedFieldNames[I]));
finally
Source.Free; // stream position was restored before the call returned
end;
end;
Hvad denne analyse ikke fortæller dig
Den verificerer ikke en signatur. AnalyzeLoadedSignatureRevisions ræsonnerer om struktur og tilladelser; om det signerede byte-interval stadig hasher til værdien i CMS-blobben, og om underskrivercertifikatet kæder til noget, man stoler på, besvares af VerifyLoadedSignature og VerifyLoadedSignatureWithTrust. En fil kan være helt politik-overholdende og kryptografisk værdiløs, så de to tjek hører side om side i enhver reel accept-gate. Den læser heller ikke hensigt inde i indholdsstrømme: en side, hvis indholdsstrøm blev erstattet helt, fanges som en ændring uden for whitelisten, men analysen fortæller ikke, at udskiftningen byttede et betalingstal. En rmlOther-dom betyder, at et menneske bør kigge, ikke at der skete svindel, og en overholdende dom betyder, at ændringen passer i en tilladt kategori, ikke at ændringen var ønsket. Når alt man behøver er, hvad underskriveren erklærede, uden revisionsgennemgangen, returnerer GetLoadedSignaturePermissions politik-dictionaries alene
Alt beskrevet her kører nativt i Delphi og C++Builder uden nogen ekstern signeringstjeneste i løkken, hvilket er det, der gør det praktisk at køre på hvert indgående dokument frem for kun dem, nogen allerede havde mistanke til. Den fulde signatur- og revisions-API, inklusive tilladelses- og verificeringsmetoderne den parres med, er en del af HotPDF Component til Delphi og C++Builder