För att ta reda på vad som ändrats i en PDF efter att den signerats tillhandahåller PDFium Component för Delphi och Lazarus TPdf.AnalyzeSignatureRevisions, en revisionsändringsanalysör för tiden efter signering som bygger upp varje inkrementell revision från filens ursprungliga byte, värderar varje senare objektändring mot signaturens DocMDP- och FieldMDP-regler, och rapporterar skuggobjektdefinitioner som en egen risk. Situationen den riktar sig mot är bekant för alla som hanterar kontrakt: ett certifierat formulär skickas ut, kommer tillbaka med två fler inkrementella sparningar, och varje signatur verifierar fortfarande. Det är förväntat, för en signatur täcker bara bytena i sin egen revision. Den verkliga frågan är om de senare sparningarna var tillåtna, och en grön bock på signaturen svarar inte på det
Varför kan PDFiums signatur-API inte visa vad som ändrats efter signeringen?
PDFiums signatur-API kan inte visa ändringar efter signeringen för att det bara läser signaturordboken: /Contents, /ByteRange, /SubFilter och DocMDP-behörighetsvärdet. PDFium har ingen inkrementell revisionsgraf, parsar inte FieldMDP-transformparametrar och erbjuder ingen diff på objektnivå mellan revisioner, så analysören i FPdfPades.pas arbetar direkt på råa byte i stället. Det har en praktisk konsekvens du bör designa kring. TPdf.AnalyzeSignatureRevisions läser de byte som behölls när dokumentet laddades, aldrig en kopia som SaveAs producerat, för en omskriven fil har förlorat just den revisionsstruktur som analyseras. Kom dokumentet från en progressiv källa som inte har laddat klart returnerar rapporten SourceStatus = pvssIncomplete och Status = prasIndeterminate i stället för att analysera en avhuggen fil
Bygga upp revisionsgränser från startxref, xref-strömmar och /Prev
Analysören bygger upp revisionsgränserna genom att följa varje startxref bakåt genom klassiska xref-tabeller, korsreferensströmmar, hybridreferensposter /XRefStm och kedjan /Prev, som definierats för inkrementella uppdateringar i ISO 32000-1 §7.5.6 och §7.5.8. Varje signaturs täckta längd är slutet på dess andra ByteRange-span, och analysören mappar den längden till den revision vars xref-sektion den faller inom. Matchar ingen revision får signaturen prrCoveredRevisionNotFound och en Indeterminate-status. Varje objekts tillstånd spelas sedan upp fram till den täckta revisionen, och varje senare xref-post jämförs med det tillståndet. Det spelar större roll än det låter: vissa skrivare återupprepar hela xref-tabellen vid varje inkrementell sparning, och en post som fortfarande pekar på samma oförändrade objekt hoppas över i stället för att rapporteras som en ändring. Utan den jämförelsen skulle en helt laglig ifyllning drunkna i hundratals falska ändringar
Skuggdefinitioner är fallet som förtjänar mest uppmärksamhet. En objektkropp som förekommer inuti en senare revisions byteområde men inte refereras av den revisionens xref är osynlig för en vanlig visare, men den är exakt den slags uppläggning som skuggattacker bygger på: dolt innehåll planteras före eller efter signeringen och aktiveras senare genom att en referens vänds. AnalyzePadesSignatureRevisionsBytes registrerar ett sådant objekt som en icke-auktoritativ ändring med IsAuthoritative = False, värderar det prdSuspicious oavsett behörighetsnivå och lägger till prrUnreferencedObjectDefinition i riskuppsättningen. Två besläktade risker täcker andra strukturycken: prrDuplicateObjectDefinition slår till när en xref-sektion listar samma objekt mer än en gång, och prrSignatureObjectRedefined när en senare revision omdefinierar ett befintligt signaturobjekt
uses
SysUtils, TypInfo, PDFium, FPdfPades;
const
ShadowTag: array[Boolean] of string = ('', ' (shadow)');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract-returned.pdf';
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions;
Writeln('Revisions: ', Report.RevisionCount,
' Signatures: ', Report.SignatureCount,
' Overall: ', GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus),
Ord(Report.Status)));
for i := 0 to High(Report.Signatures) do
with Report.Signatures[i] do
begin
Writeln(Format('Signature %d covers revision %d, %d later, P=%d, FieldMDP=%s',
[SignatureIndex, CoveredRevisionIndex, LaterRevisionCount,
DocMdpPermission,
GetEnumName(TypeInfo(TPadesFieldMdpAction), Ord(FieldMdpAction))]));
for j := 0 to High(Changes) do
Writeln(Format(' rev %d obj %d %s -> %s%s',
[Changes[j].RevisionIndex, Changes[j].ObjectNumber,
GetEnumName(TypeInfo(TPadesRevisionChangeKind), Ord(Changes[j].Kind)),
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Changes[j].Decision)),
ShadowTag[not Changes[j].IsAuthoritative]]));
end;
finally
Pdf.Free;
end;
end;
Hur tillämpas DocMDP och FieldMDP för varje signatur?
DocMDP och FieldMDP tillämpas separat för varje signatur, vid signaturens egen täckta revision, så en certifieringssignatur och en senare godkännandesignatur i samma fil kan nå olika domar om samma ändring. Varje senare objekt klassificeras först i en TPadesRevisionChangeKind utifrån sina /Type-, /Subtype- och /FT-poster och utifrån den roll det spelar i sidans, formulärets, annoteringarnas och DSS-graferna. Allt som bär /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia eller /EmbeddedFile blir prckActiveContent. Beslutet följer sedan ISO 32000-1 §12.8.2.2: med P=1 är allt utom korsreferensdata och valideringsmaterial förbjudet; P=2 tillåter ifyllning av formulär och ytterligare signaturer men avvisar annoteringsändringar; P=3 tillåter även annoteringar. Sidoinnehåll, dokumentstruktur, metadata, aktivt innehåll och raderade objekt är förbjudna på vilken DocMDP-nivå som helst, och värderas prdSuspicious när signaturen inte bär någon DocMDP alls, för en godkännandesignatur förbjuder ingenting formellt men läsaren ser inte längre vad som signerades
FieldMDP, ur ISO 32000-1 §12.8.2.4, snäper in formulärfältsbeslutet ytterligare. pfmaAll låser varje fält, pfmaInclude låser bara de listade fälten, och pfmaExclude låser allt utom de listade fälten. För att tillämpa Include eller Exclude löser analysören varje ändrat fält till sitt fullständigt kvalificerade namn genom kedjan /Parent och jämför det med låslistan på exakt matchning, så lista terminala fältnamn i stället för att förvänta dig att ett föräldranamn täcker sina barn. Går ett namn inte att lösa eller använder transformen en åtgärd parsern inte känner igen blir ändringen prdIndeterminate och prrFieldMdpUnresolved höjs. Per-ändringsdomarna rullas sedan ihop sämst-först, med Suspicious rankad över Disallowed, Disallowed över Indeterminate och Indeterminate över Allowed, så ett skuggobjekt väger tyngre än hur många legitima fältuppdateringar som helst
Varför kommer vissa ändringar tillbaka som Indeterminate i stället för säkra?
Ändringar kommer tillbaka som Indeterminate närhelst analysören inte kan bevisa att en ändring är tillåten, för i en signaturkontroll får ett okänt aldrig rapporteras som tillåtet. Ett vanligt fall hanteras i stället precist: långtidsvalidering lägger till en /DSS och skriver om katalogen, vilket annars skulle räknas som en strukturell ändring under P=1. Analysören stryker /DSS och /Extensions ur de gamla och nya katalogordböckerna och jämför resten; skiljer inget annat åt behandlas omskrivningen som en uppdatering av valideringsmaterial och tillåts, så B-LT- och B-LTA-utvidgning bryter inte en certifieringssignatur. Andra luckor lämnas medvetet öppna. Type-2-poster i en korsreferensström pekar in i komprimerade objektströmmar, och analysören expanderar inte objektströmmar inom denna säkerhetsgräns, så de ändringarna dyker upp som prckCompressedObject med prrCompressedObjectUnresolved, förbjudna under P=1 och Indeterminate annars. Hårda budgetar på 1024 revisioner, 1 000 000 objektnummer och 2 000 000 rapporterade ändringar ger prrResourceLimitExceeded, och en bruten xref-kedja ger prrMalformedRevisionChain; båda slutar som Indeterminate, aldrig som godkänt
const
StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrResourceLimitExceeded];
function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
// Vissa risker registreras utan att Status ändras, så testa dem först
if R.Risks * StructuralRisks <> [] then
Exit('review: structural risk in the revision chain');
case R.Status of
prasNoLaterChanges: Result := 'accept: nothing was added after signing';
prasAllowed: Result := 'accept: every later change is permitted';
prasDisallowed: Result := 'reject: a change violates DocMDP or FieldMDP';
prasSuspicious: Result := 'reject: shadow or unconstrained content change';
prasIndeterminate: Result := 'review: the analyzer could not decide';
else
Result := 'not checked: no signatures or no original bytes';
end;
end;
Ordningen i den grinden är medveten. prrDuplicateObjectDefinition läggs till i riskuppsättningen utan att själv sänka Status, och en FieldMDP-transform som inte går att parsa påverkar statusen först när ett formulärfält faktiskt ändras, så en grind som bara tittar på Status kan missa bevisning rapporten redan innehåller. Ha också i minnet vad rapporten inte påstår. TPadesRevisionAnalysisReport säger ingenting om huruvida CMS-signaturen är kryptografiskt giltig eller huruvida undertecknarcertifikatet kedjar till en rot du litar på. Revisionsanalys svarar på frågan om vad som hände efter signeringen, och den hör hemma bredvid strukturell och förtroendebaserad validering, inte i stället för dem
Skriva seed-värden och MDP-lås vid signeringstillfället
Samma regler kan författas vid signering genom TPadesSignatureFieldOptions, som är FieldOptions-medlemmen i både TPadesSignOptions och TPadesRemoteSignOptions. PDFium kan skapa en widget men kan inte skriva /SV, /Lock, en FieldMDP- eller DocMDP-transform, eller katalogordboken /Perms, så komponentens egen inkrementella PAdES-skrivare producerar dessa objekt inuti samma xref-uppdatering som signaturen. FieldName sätter rotfältnamnet, RequiredSeedValues blir /Ff-bitarna i seed-värdeordboken som beskrivs i ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations och AcceptableCertificates begränsar vad en senare undertecknare får välja, LockAction med LockFields skriver en indirekt /SigFieldLock, och CertificationPermission från 1 till 3 gör signaturen till en certifieringssignatur. DocMDP- och FieldMDP-transformerna går båda in i en enda /Reference-array på signaturvärdet, vart och ett med /Data pekande på katalogen
var
Options: TPadesSignOptions;
begin
Options := TPadesSignOptions.Default;
Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
Options.Reason := 'Approved for release';
Options.FieldOptions.FieldName := 'Certification';
Options.FieldOptions.CertificationPermission := 2; // endast ifyllning och signering
Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
Options.FieldOptions.LockAction := pfmaInclude; // lås bara dessa fält
SetLength(Options.FieldOptions.LockFields, 2);
Options.FieldOptions.LockFields[0] := 'Total';
Options.FieldOptions.LockFields[1] := 'IBAN';
if not Pdf.SignPades('contract-certified.pdf', Options) then
Writeln('Signing failed');
end;
Några detaljer är lätta att få fel om du snickrar ihop detta själv. Katalogens /Perms /DocMDP måste referera signaturvärdets ordbok, inte widget-annoteringen, och skrivaren behåller därför signaturvärdet som sitt eget indirekta objekt. En befintlig /Perms-ordbok kan redan hålla /UR3-användarrättigheter, så skrivaren kopierar den och infogar /DocMDP i stället för att ersätta den, enligt behörighetsordboken i ISO 32000-1 §12.8.4. Ett dokument som redan bär /DocMDP vägrar en andra certifieringssignatur med EPadesCrypto, och det gör inkonsekventa optioner också: ett Include- eller Exclude-lås utan fältnamn, ett All-lås med fältlista, en juridisk attest på en icke-certifieringssignatur, eller en punkt i rotfältnamnet. Fjärrsignering lägger till ytterligare en regel, för signeringscertifikatet är okänt när PreparePadesRemoteSignature körs: att sätta CertificateRequired där kräver en explicit lista AcceptableCertificates, medan lokal signering kan falla tillbaka på det upplösta undertecknarcertifikatet
Revisionsanalys kompletterar signaturverktygslådan i stället för att ersätta någon del av den. Börja med att inspektera PDF-signaturer och PAdES-nivåer med PDFium Component för att läsa ordboken och baslinjenivån, titta på varför validatorer avvisar PAdES-signaturer för de strukturella fel som kommer före någon revisionsfråga, och vik domen in i en bredare PDF-säkerhetsriskgranskning vid sidan av kontrollerna för JavaScript och inbäddade filer. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions och den inkrementella PAdES-skrivaren som visats här levereras med PDFium Component för Delphi, C++Builder och Lazarus