En signerad PDF som ändrats efter signering är inte automatiskt trasig. ISO 32000-1 tillåter inkrementella uppdateringar ovanpå en signatur, och bara vissa av dem bryter policyn signeraren satte. HotPDF Component för Delphi och C++Builder besvarar den frågan med AnalyzeLoadedSignatureRevisions, som klassificerar varje revision efter signering och betygsätter den mot DocMDP och FieldMDP. Scenariot är bekant för alla som levererar kontraktsprogramvara: din kund signerar ett köpeavtal, skickar ut det, och får tillbaka det med en bilagesida fäst vid det. Läsaren visar en gul stapel som säger att signaturen är intakt men att dokumentet har ändrats sedan det signerades, och ingen i rummet kan säga om det är ett normalt motsigneringsarbetsflöde eller någon som tyst redigerar ett signerat kontrakt
Vad räknas som en laglig ändring efter signering?
En ändring är laglig när dess semantiska kategori faller inom behörigheten den certifierande signaturen deklarerade. ISO 32000-1 §12.8.2.2 definierar DocMDP-transformen med ett /P-värde på 1, 2 eller 3: 1 tillåter inga ändringar alls, 2 tillåter formulärifyllning och signering, 3 tillåter formulärifyllning, signering och annoteringar. HotPDF exponerar dessa som THPDFDocMDPPermission-värdena dmpNoChanges, dmpFormFillAndSign och dmpFormFillSignAndAnnotate, med dmpNone reserverat för inspektionsresultat utan någon DocMDP-transform alls
Kategorierna är ordnade, och den ordningen är motorn i hela kontrollen. THPDFRevisionModificationLevel kör rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, avsiktligt ordnade så en högre ordinal aldrig är mindre restriktiv. Ett helt dokument reduceras till den maximala nivån observerad över varje revision efter signaturen, och DocMDP-jämförelsen blir ett enda heltalstest. En nyans spelar roll tidigt: vid dmpNoChanges accepterar analysen fortfarande rmlLongTermValidation. Att lägga till DSS- och VRI-valideringsmaterial eller en dokumenttidsstämpel till en certifierad fil är underhåll av signaturen, inte modifiering av dokumentet, och att behandla det som en överträdelse skulle bryta varje långtidsarkiveringsarbetsflöde som existerar
Hur bygger HotPDF om revisionskedjan?
Strukturellt, inte heuristiskt. Enligt ISO 32000-1 §7.5.6 lägger en inkrementell uppdatering till ett nytt korsreferensavsnitt vars /Prev pekar på det föregående, så HotPDF läser startxref från svansen, tolkar avsnittet där, följer /Prev bakåt och upprepar, och returnerar avsnitten äldsta först. Två säkerhetsgränser sitter i den loopen och båda är värda att känna till vid triage av en fil som misslyckas: en /Prev som pekar på en offset redan besökt avslutar vandringen med en explicit cykeldiagnos i stället för att snurra, och en kedja längre än tusen revisioner avvisas rakt av. Båda dyker upp i Analysis.Issue med funktionen returnerande False, och ingen av dem bör tapetseras över, eftersom en cyklisk /Prev är en missformad eller fientlig fil snarare än en ovanlig sådan
Fyra historiska former dyker upp i verkliga dokument och alla fyra hanteras: traditionella xref-tabeller tolkade rad för rad, korsreferensströmmar dekomprimerade och avkodade genom sina /W- och /Index-fält, hybridreferensfiler vars traditionella trailer bär en /XRefStm-nyckel som tolkas och slås samman till samma revision (Office-producentfallet, täckt i artikeln om hybrida korsreferensströmmar), och objekt som lever inuti en ObjStm-container, vilka spelar roll eftersom en modern uppdatering vanligtvis lägger den ändrade ordboken i en komprimerad ström i stället för att skriva den direkt, som beskrivet i stycket om objektströmmar och inkrementella uppdateringar. Signaturen förankrar uppdelningen: /ByteRange[2] + /ByteRange[3] blir SignedRevisionLength, och varje avsnitt vid eller bortom den offseten är efter-signering. Om byteintervallet fortfarande hashar korrekt är en separat fråga, besvarad av VerifyLoadedSignature och täckt i artikeln om att verifiera digitala PDF-signaturer
Hur varje ändrat objekt klassificeras
Klassificering körs per objekt, och sprider sig sedan längs referenser. För varje objektnummer ett efter-signering-avsnitt rör vid läser HotPDF den nya kroppen och kroppen som den stod i den signerade ögonblicksbilden; en identisk kropp är rmlNone, eftersom producenter faktiskt skriver om objekt utan att ändra dem. Igenkännarna är smala med avsikt. Ett /Type /DocTimeStamp-objekt, eller ett vars /SubFilter är ETSI.RFC3161, är rmlLongTermValidation, liksom allt nåbart från katalogens /DSS-träd; en /Type /Sig-ordbok är rmlFormFillAndSign. För containrar är testet vilka nycklar som flyttades, inte vad objektet är: katalogen får bara vinna eller ändra /DSS, /Extensions eller /AcroForm; AcroForm-ordboken bara /Fields, /SigFlags, /NeedAppearances, /DR, /DA eller /Q; en sida bara /Annots; ett fält eller en widget bara /V, /AP, /AS eller /M. Allt utanför de mängderna faller till rmlOther, vilket är exakt hur den tillagda bilagesidan fångas: att lägga till en sida ordnar om sidträdet på sätt ingen vitlista täcker, och ingen mängd legitim formulärifyllning liknar det
Sedan sprider sig nivåerna, med varje container som ärver den maximala nivån av de ändrade barn den pekar på, itererat tills tilldelningen stabiliseras. Det är det som får utseendeströmmar att fungera. Ett ifyllt textfält skriver om /V och pekar på en fräsch /AP-ström, och den strömmen är på egen hand en anonym klump av innehållsoperatorer utan typ att känna igen; eftersom fältet som äger den är rmlFormFillAndSign, ärver strömmen samma nivå i stället för att falla igenom till rmlOther. Samma spridning för DSS-kontext till certifikat- och återkallelseströmmar som annars skulle vara oklassificerbara
Varför räknas ett oläsbart objekt som en överträdelse?
Eftersom alternativet är en validerare besegrad genom att skriva något den inte förstår. Tre situationer landar i rmlOther utan appell i HotPDF: ett objekt vars kropp inte kunde läsas från revisionen, ett objekt revisionen markerar som frigjort, och ett objekt som inte matchar någon av igenkännarna ovan. Var och en registrerar en specifik diagnos i revisionens Issue-fält, så en operatör kan se vilket objektnummer som producerade utslaget
Frigöring är den skarpaste av de tre. En efter-signering-revision som markerar ett tidigare definierat objekt som fritt har tagit bort innehåll från ett signerat dokument, och ingen behörighetsnivå under §12.8.2.2 tillåter det; objektnumren hamnar i FreedObjectNumbers och revisionen höjs till rmlOther. Oläsbara objekt följer samma logik av en annan anledning. En validerare som inte kan tolka ett objekt har ingen grund för att kalla det ofarligt, och det ärliga svaret på det är inte tystnad. Att rapportera en ovanlig men ofarlig konstruktion som en överträdelse kostar en mänsklig granskning; det motsatta felet skickar ett signerat kontrakt med en obemärkt redigering inuti det
Att läsa utslaget i Delphi
Anropet är kort. Ladda dokumentet, välj ett signaturindex, läs posten; överlagringen utan parametrar öppnar filen dokumentet laddades från igen, och TStream-överlagringen tar anropar-tillhandahållna byte och återställer strömpositionen innan den returnerar. PolicyCompliant är den enda boolean de flesta anropare vill ha, som kombinerar tre oberoende beslut: den strukturella giltigheten hos behörighetsordböckerna, DocMDPCompliant, och FieldMDPCompliant. Håll komponenterna synliga i ditt gränssnitt i stället för att kollapsa dem, och notera att ett dokument utan DocMDP-transform lämnar DocMDPCompliant på True, eftersom en vanlig godkännandesignatur inte deklarerar någon policy att bryta mot och den aggregerade ModificationLevel då är beskrivande snarare än ett utslag
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;
För triage vill du vanligtvis ha uppdelningen per revision i stället för sammanfattningen, eftersom den säger när i dokumenthistoriken saker gick fel. Varje post i Analysis.Revisions bär sitt index i kedjan, korsreferensoffseten den skrevs vid, sin egen modifieringsnivå, och de inblandade objektnumren
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öms separat, och det är avsiktligt
Ett dokument kan uppfylla DocMDP och ändå vara illegitimt, vilket är varför FieldMDPCompliant är en distinkt boolean snarare än invikt i nivåjämförelsen. ISO 32000-1 §12.8.2.4 definierar FieldMDP-transformen, och §12.7.5.5 den relaterade /SigFieldLock-posten, för att frysa namngivna formulärfält vid ögonblicket för signering även där dokumentet som helhet fortfarande tillåter formulärifyllning. Att fylla i ett fält är en nivå 2-åtgärd; att fylla i ett fält signeraren låst är en överträdelse oavsett nivå. HotPDF läser omfattningen till THPDFFieldLockAction som flaAll, flaInclude eller flaExclude, med flaNone för resultat utan någon låspolicy, och namnen till Permissions.FieldNames: flaAll låser allt, flaInclude låser de listade namnen, flaExclude låser allt utom dem. En detalj spelar roll när man läser resultaten, i det att bara fält redan närvarande i den signerade ögonblicksbilden rapporteras i ChangedFieldNames, eftersom ett fält skapat helt efter signering inte har något signerat tillstånd att motsäga och fångas av DocMDP-vägen i stället
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;
Vad den här analysen inte talar om för dig
Den verifierar inte en signatur. AnalyzeLoadedSignatureRevisions resonerar om struktur och behörigheter; om det signerade byteintervallet fortfarande hashar till värdet i CMS-blobben, och om signerarcertifikatet kedjar till något du litar på, besvaras av VerifyLoadedSignature och VerifyLoadedSignatureWithTrust. En fil kan vara perfekt policykonform och kryptografiskt värdelös, så de två kontrollerna hör hemma sida vid sida i varje verklig godkännandegrind. Den läser inte heller avsikt inuti innehållsströmmar: en sida vars innehållsström ersattes helt fångas som en ändring utanför vitlistan, men analysen kommer inte tala om för dig att ersättningen bytte ut en betalningssiffra. Ett rmlOther-utslag betyder att en människa bör titta, inte att bedrägeri skedde, och ett konformt utslag betyder att ändringen passar en tillåten kategori, inte att ändringen var önskad. När allt du behöver är vad signeraren deklarerade, utan revisionsvandringen, returnerar GetLoadedSignaturePermissions policyordböckerna på egen hand
Allt som beskrivs här körs naturligt i Delphi och C++Builder utan någon extern signeringstjänst i loopen, vilket är det som gör det praktiskt att köra på varje inkommande dokument i stället för bara de som någon redan misstänkte. Det fullständiga signatur- och revisions-API:et, inklusive behörighets- och verifieringsmetoderna det parar ihop sig med, är del av HotPDF Component för Delphi och C++Builder