Teknisk artikel

Ændringsanalyse af signerede PDF'er med PDFium i Delphi

For at finde ud af, hvad der ændredes i en PDF, efter den blev signeret, leverer PDFium Component til Delphi og Lazarus TPdf.AnalyzeSignatureRevisions, en post-signature revisionsændringsanalyser, der genopbygger hver inkrementel revision fra de originale filbytes, vurderer hver senere objektændring mod den signaturs DocMDP- og FieldMDP-regler og rapporterer shadow-objektdefinitioner som en separat risiko. Situationen, den sigter på, er kendt af alle, der håndterer kontrakter: en certificeret formular går ud, kommer tilbage med to yderligere inkrementelle gemninger, og hver signatur verificerer stadig. Det var ventet, for en signatur dækker kun bytes af sin egen revision. Det egentlige spørgsmål er, hvorvidt de senere gemninger var tilladte, og et grønt flueben på signaturen besvarer det ikke

Hvorfor kan PDFium signature-API'et ikke vise, hvad der ændredes efter signering?

PDFium signature-API'et kan ikke vise post-signature ændringer, fordi det kun læser signaturdictionaryen: /Contents, /ByteRange, /SubFilter og DocMDP-tilladelsesværdien. PDFium har ingen inkrementel revisionsgraf, parser ikke FieldMDP-transformparametre og tilbyder ingen objektniveau-diff mellem revisioner, så analyseren i FPdfPades.pas arbejder i stedet direkte på rå bytes. Det har en praktisk konsekvens, du bør designe omkring. TPdf.AnalyzeSignatureRevisions læser de bytes, der blev bevaret, da dokumentet blev indlæst, aldrig en kopi produceret af SaveAs, for en omskrevet fil har mistet netop den revisionsstruktur, der analyseres. Kom dokumentet fra en progressiv kilde, der ikke er færdig med at downloade, returnerer rapporten SourceStatus = pvssIncomplete og Status = prasIndeterminate frem for at analysere en trunkeret fil

Genopbygning af revisionsgrænser fra startxref, xref streams og /Prev

Analyseren genopbygger revisionsgrænser ved at følge hver startxref tilbage gennem klassiske xref-tabeller, cross-reference streams, hybrid-reference /XRefStm-entries og /Prev-kæden, som defineret for inkrementelle opdateringer i ISO 32000-1 §7.5.6 og §7.5.8. Hver signaturs dækkede længde er slutningen af dens anden ByteRange-span, og analyseren mapper den længde til den revision, hvis xref-sektion den falder inden i. Matcher ingen revision, får signaturen prrCoveredRevisionNotFound og en Indeterminate-status. Herefter afspilles hvert objekts tilstand op til den dækkede revision, og hver senere xref-entry sammenlignes med den tilstand. Det tæller mere, end det lyder: nogle writers genskriver den komplette xref-tabel ved hver inkrementel gemning, og en entry, der stadig peger på samme uændrede objekt, springes over i stedet for at blive rapporteret som en ændring. Uden den sammenligning ville en helt lovlig formularudfyldelse drukne i hundredvis af falske ændringer

Hvordan AnalyzeSignatureRevisions genopbygger inkrementelle revisioner fra rå PDF-bytes i Delphi: signaturens ByteRange slutter inde i den dækkede revision, xref Prev-kæden vandrer tilbage gennem hver senere gemning, objekttilstand afspilles til den dækkede revision, og uændrede gentagne entries springes over i stedet for at blive rapporteret som ændringer
En signatur dækker kun bytes af sin egen revision, så analyseren mapper det andet ByteRange-span til en revision og vurderer hver senere xref-entry mod den afspillede objekttilstand

Shadow-definitioner er det tilfælde, der fortjener mest opmærksomhed. En objektkrop, der optræder inde i en senere revisions byte-område, men ikke refereres af den revisions xref, er usynlig for en normal viewer, og den er netop den slags staging, som shadow-angreb er afhængige af: skjult indhold plantes før eller efter signering og aktiveres senere ved at vende en reference. AnalyzePadesSignatureRevisionsBytes registrerer sådan et objekt som en ikke-autoritativ ændring med IsAuthoritative = False, vurderer den prdSuspicious uanset tilladelsesniveau og tilføjer prrUnreferencedObjectDefinition til risikosættet. To relaterede risici dækker andre strukturelle trick: prrDuplicateObjectDefinition udløses, når én xref-sektion lister samme objekt mere end én gang, og prrSignatureObjectRedefined udløses, når en senere revision redefinerer et eksisterende signaturobjekt

En shadow-objektdefinition inde i et senere PDF-revisions byte-område: objektkroppen eksisterer, men ingen xref-entry refererer den, så viewere viser den aldrig, og AnalyzeSignatureRevisions i PDFium Component registrerer den som ikke-autoritativ, vurderer den prdSuspicious og rejser prrUnreferencedObjectDefinition ved siden af duplikat- og redefinerede-signatur-risiciene
Skjult indhold plantes før eller efter signering og aktiveres senere ved at vende en reference, hvilket er grunden til, at en ikke-refereret krop vurderes suspicious uanset DocMDP-tilladelsesniveauet
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;

Hvordan håndhæves DocMDP og FieldMDP for hver signatur?

DocMDP og FieldMDP håndhæves separat for hver signatur, ved den signaturs egen dækkede revision, så en certificeringssignatur og en senere godkendelsessignatur i samme fil kan nå forskellige domme om samme ændring. Hvert senere objekt klassificeres først i en TPadesRevisionChangeKind ud fra sine /Type-, /Subtype- og /FT-entries og ud fra den rolle, det spiller i side-, formular-, annotations- og DSS-graferne. Alt, der bærer /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia eller /EmbeddedFile, bliver prckActiveContent. Beslutningen følger derefter ISO 32000-1 §12.8.2.2: med P=1 er alt undtagen cross-reference-data og valideringsmateriale ikke tilladt; P=2 tillader formularudfyldelse og yderligere signaturer men afviser annotationsændringer; P=3 tillader også annotationer. Sideindhold, dokumentstruktur, metadata, aktivt indhold og slettede objekter er ikke tilladt under noget DocMDP-niveau og vurderes prdSuspicious, når signaturen slet ikke bærer DocMDP, da en godkendelsessignatur formelt ikke forbyder noget, men læseren ikke længere ser, hvad der blev signeret

FieldMDP, fra ISO 32000-1 §12.8.2.4, indsnævrer formularfelt-beslutningen yderligere. pfmaAll låser hvert felt, pfmaInclude låser kun de listede felter, og pfmaExclude låser alt undtagen de listede felter. For at anvende Include eller Exclude resolver analyseren hvert ændret felt til dets fuldt kvalificerede navn gennem /Parent-kæden og sammenligner det med låselisten ved eksakt match, så list terminale feltnavne frem for at forvente, at et forældrenavn dækker sine børn. Kan et navn ikke resolves, eller bruger transformen en handling, parseren ikke genkender, bliver ændringen prdIndeterminate, og prrFieldMdpUnresolved rejses. Beslutninger pr. ændring ruller derefter op worst-first, med Suspicious rangeret over Disallowed, Disallowed over Indeterminate og Indeterminate over Allowed, så ét shadow-objekt vejer tungere end et hvilket som helst antal legitime feltopdateringer

Vurderingspipelinen, som AnalyzeSignatureRevisions anvender på hver post-signature ændring i Delphi: en TPadesRevisionChangeKind fra Type- og Subtype-entries, en DocMDP-beslutning ved den dækkede revision fra P=1 til P=3, et FieldMDP-låsetjek på fuldt kvalificerede feltnavne og en worst-first roll-up fra prdSuspicious ned til prdAllowed
Ét shadow-objekt vejer tungere end et hvilket som helst antal legitime feltopdateringer, fordi Suspicious rangerer over Disallowed, Indeterminate og Allowed, mens nogle risici registreres ved siden af status uden at nedgradere den

Hvorfor kommer nogle ændringer tilbage som Indeterminate i stedet for sikre?

Ændringer kommer tilbage som Indeterminate, når analyseren ikke kan bevise, at en ændring er tilladt, for i et signaturetjek må et ukendt aldrig rapporteres som tilladt. Ét almindeligt tilfælde håndteres i stedet præcist: langsigtet validering tilføjer en /DSS og omskriver kataloget, hvilket ellers ville tælle som en strukturel ændring under P=1. Analyseren fjerner /DSS og /Extensions fra de gamle og nye katalog-dictionaries og sammenligner resten; er der intet andet, der adskiller sig, behandles omskrivningen som en valideringsmateriale-opdatering og tillades, så B-LT- og B-LTA-augmentering ikke knækker en certificeringssignatur. Andre huller efterlades åbne med vilje. Type-2-entries i en cross-reference stream peger ind i komprimerede object streams, og analyseren ekspanderer ikke object streams inden for denne sikkerhedsgrænse, så de ændringer dukker op som prckCompressedObject med prrCompressedObjectUnresolved, ikke tilladt under P=1 og Indeterminate ellers. Hårde budgetter på 1024 revisioner, 1.000.000 objektnumre og 2.000.000 rapporterede ændringer producerer prrResourceLimitExceeded, og en brudt xref-kæde producerer prrMalformedRevisionChain; begge ender som Indeterminate, aldrig som en bestået

const
  StructuralRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
    prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
    prrSignatureObjectRedefined, prrResourceLimitExceeded];

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Nogle risici registreres uden at ændre Status, så test 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;

Rækkefølgen i den gate er bevidst. prrDuplicateObjectDefinition tilføjes til risikosættet uden selv at nedgradere Status, og en FieldMDP-transform, der ikke kan parses, påvirker kun status, når et formularfelt reelt ændres, så en gate, der kun kigger på Status, kan overse beviser, rapporten allerede indeholder. Husk også, hvad rapporten ikke påstår. TPadesRevisionAnalysisReport siger intet om, hvorvidt CMS-signaturen er kryptografisk gyldig, eller om signeringscertifikatet kæder til en rod, du stoler på. Revisionsanalyse besvarer spørgsmålet om, hvad der skete efter signeringen, og den hører ved siden af strukturel og tillidsvalidering, ikke i stedet for dem

At skrive seed values og MDP-låse ved signeringstid

Samme regler kan forfattes ved signering gennem TPadesSignatureFieldOptions, som er FieldOptions-medlemmet af både TPadesSignOptions og TPadesRemoteSignOptions. PDFium kan oprette en widget, men kan ikke skrive /SV, /Lock, en FieldMDP- eller DocMDP-transform eller katalog-/Perms-dictionaryen, så komponentens egen inkrementelle PAdES-writer producerer disse objekter inde i samme xref-opdatering som signaturen. FieldName sætter rodfeltets navn, RequiredSeedValues bliver /Ff-bitsene i seed value-dictionaryen beskrevet i ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations og AcceptableCertificates begrænser, hvad en senere signerer kan vælge, LockAction med LockFields skriver en indirekte /SigFieldLock, og CertificationPermission fra 1 til 3 gør signaturen til en certificeringssignatur. DocMDP- og FieldMDP-transformerne går begge i ét /Reference-array på signaturværdien, hver med /Data pegende på kataloget

var
  Options: TPadesSignOptions;
begin
  Options := TPadesSignOptions.Default;
  Options.CertificateThumbprint := 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678';
  Options.Reason := 'Approved for release';
  Options.FieldOptions.FieldName := 'Certification';
  Options.FieldOptions.CertificationPermission := 2;   // kun formularudfyldelse og signering
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // lås kun disse felter
  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;

Et par detaljer er nemme at tage fejl af, hvis du håndruller dette. Katalog-/Perms /DocMDP skal referere signaturværdi-dictionaryen, ikke widget-annotationen, og writeren beholder signaturværdien som sit eget indirekte objekt af den grund. En eksisterende /Perms-dictionary kan allerede holde /UR3 usage rights, så writeren kopierer den og indsætter /DocMDP i stedet for at erstatte den, i overensstemmelse med permissions dictionary i ISO 32000-1 §12.8.4. Et dokument, der allerede bærer /DocMDP, nægter en anden certificeringssignatur med EPadesCrypto, og det gør inkonsistente options også: en Include- eller Exclude-lås uden feltnavne, en All-lås med en feltliste, en legal attestation på en ikke-certificeringssignatur eller et punktum i rodfeltets navn. Fjernsignering tilføjer én regel mere, fordi signeringscertifikatet er ukendt, når PreparePadesRemoteSignature kører: at sætte CertificateRequired der kræver en eksplicit AcceptableCertificates-liste, mens lokal signering kan falde tilbage på det resolserede signeringscertifikat

Revisionsanalyse fuldender signaturværktøjskassen frem for at erstatte nogen del af den. Start med at inspicere PDF-signaturer og PAdES-niveauer med PDFium Component for at læse dictionaryen og basisniveauet, kig på hvorfor validatorer afviser PAdES-signaturer for de strukturelle fejl, der kommer før noget revisionsspørgsmål, og fold domen ind i en bredere PDF-sikkerhedsrisiko-audit ved siden af JavaScript- og embedded file-tjek. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions og den inkrementelle PAdES-writer vist her leveres med PDFium Component til Delphi, C++Builder og Lazarus