Teknisk artikkel

Analyse av PDF-endringer etter signering med PDFium i Delphi

For å finne ut hva som endret seg i en PDF etter at den ble signert, tilbyr PDFium Component for Delphi og Lazarus TPdf.AnalyzeSignatureRevisions, en analyse av revisjonsendringer etter signering som bygger hver inkrementell revisjon opp igjen fra de opprinnelige filbytene, vurderer hver senere objektendring mot signaturens DocMDP- og FieldMDP-regler, og rapporterer skyggeobjektdefinisjoner som en egen risiko. Situasjonen den retter seg mot, er kjent for alle som håndterer kontrakter: et sertifisert skjema går ut, kommer tilbake med to ytterligere inkrementelle lagringer, og hver signatur verifiserer fortsatt. Det er forventet, fordi en signatur bare dekker bytene av sin egen revisjon. Det egentlige spørsmålet er om de senere lagringene var tillatt, og en grønn hake på signaturen svarer ikke på det

Hvorfor kan ikke PDFium signatur-API vise hva som endret seg etter signering?

PDFium signatur-API kan ikke vise endringer etter signering, fordi den bare leser signaturordboken: /Contents, /ByteRange, /SubFilter og DocMDP-tillatelsesverdien. PDFium har ingen inkrementell revisjonsgraf, parser ikke FieldMDP-transformparametere og tilbyr ingen objektnivå-diff mellom revisjoner, så analysatoren i FPdfPades.pas jobber direkte på rå byte i stedet. Det har en praktisk konsekvens du bør designe rundt. TPdf.AnalyzeSignatureRevisions leser bytene som ble tatt vare på da dokumentet ble lastet, aldri en kopi produsert av SaveAs, fordi en omskrevet fil har mistet selve revisjonsstrukturen som analyseres. Kom dokumentet fra en progressiv kilde som ikke er ferdig nedlastet, returnerer rapporten SourceStatus = pvssIncomplete og Status = prasIndeterminate i stedet for å analysere en avkuttet fil

Bygge revisjonsgrenser opp igjen fra startxref, xref-strømmer og /Prev

Analysatoren bygger revisjonsgrensene opp igjen ved å følge hver startxref bakover gjennom klassiske xref-tabeller, kryssreferansestrømmer, hybridreferanse-/XRefStm-oppføringer og /Prev-kjeden, slik definert for inkrementelle oppdateringer i ISO 32000-1 §7.5.6 og §7.5.8. Hver signaturs dekkede lengde er slutten på sitt andre ByteRange-spenn, og analysatoren mapper den lengden til revisjonen hvis xref-seksjon den faller innenfor. Samsvarer ingen revisjon, får signaturen prrCoveredRevisionNotFound og en Indeterminate-status. Tilstanden til hvert objekt spilles deretter av opp til den dekkede revisjonen, og hver senere xref-oppføring sammenlignes med den tilstanden. Det betyr mer enn det høres ut som: noen skrivere gjentar hele xref-tabellen ved hver inkrementell lagring, og en oppføring som fortsatt peker på det samme uendrede objektet, hoppes over i stedet for å rapporteres som en endring. Uten den sammenligningen ville en helt lovlig utfylling av skjema drukne i hundrevis av falske endringer

Hvordan AnalyzeSignatureRevisions bygger inkrementelle revisjoner opp igjen fra rå PDF-byte i Delphi: signaturens ByteRange slutter inne i den dekkede revisjonen, xref Prev-kjeden går bakover gjennom hver senere lagring, objekttilstanden spilles av til den dekkede revisjonen, og uendrede gjentatte oppføringer hoppes over i stedet for å rapporteres som endringer
En signatur dekker bare bytene av sin egen revisjon, så analysatoren mapper det andre ByteRange-spennet til en revisjon og vurderer hver senere xref-oppføring mot den avspilte objekttilstanden

Skyggedefinisjoner er tilfellet som fortjener mest oppmerksomhet. En objektkropp som dukker opp inne i en senere revisjons byte-område, men ikke refereres av revisjonens xref, er usynlig for en vanlig viser, og er likevel nøyaktig den typen scenelegging skyggeangrep er avhengige av: skjult innhold plantes før eller etter signering og aktiveres senere ved å flippe en referanse. AnalyzePadesSignatureRevisionsBytes registrerer et slikt objekt som en ikke-autoritativ endring med IsAuthoritative = False, vurderer det til prdSuspicious uavhengig av tillatelsesnivået, og legger prrUnreferencedObjectDefinition til risikosettet. To beslektede risikoer dekker andre strukturelle triks: prrDuplicateObjectDefinition utløses når én xref-seksjon lister det samme objektet mer enn én gang, og prrSignatureObjectRedefined utløses når en senere revisjon redefinerer et eksisterende signaturobjekt

En skyggeobjektdefinisjon inne i et senere PDF-revisjons byte-område: objektkroppen finnes, men ingen xref-oppføring refererer den, så visere viser den aldri, og AnalyzeSignatureRevisions i PDFium Component registrerer den som ikke-autoritativ, vurderer den til prdSuspicious og reiser prrUnreferencedObjectDefinition ved siden av duplikat- og redefinert signatur-risikoene
Skjult innhold plantes før eller etter signering og aktiveres senere ved å flippe en referanse, og det er derfor en ureferert kropp vurderes som suspicious uavhengig av DocMDP-tillatelsesnivået
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åndheves DocMDP og FieldMDP for hver signatur?

DocMDP og FieldMDP håndheves separat for hver signatur, ved signaturens egen dekkede revisjon, så en sertifiseringssignatur og en senere godkjenningssignatur i samme fil kan nå ulike kjennelser om den samme endringen. Hvert senere objekt klassifiseres først til en TPadesRevisionChangeKind ut fra /Type-, /Subtype- og /FT-oppføringene sine og ut fra rollen den spiller i side-, skjema-, annotasjons- og DSS-grafene. Alt som bærer /JavaScript, /JS, /Launch, /OpenAction, /AA, /RichMedia eller /EmbeddedFile blir prckActiveContent. Beslutningen følger så ISO 32000-1 §12.8.2.2: med P=1 er alt unntatt kryssreferansedata og valideringsmateriale forbudt; P=2 tillater skjemautfylling og ytterligere signaturer, men avviser annotasjonsendringer; P=3 tillater også annotasjoner. Sideinnhold, dokumentstruktur, metadata, aktivt innhold og slettede objekter er forbudt på ethvert DocMDP-nivå, og vurderes til prdSuspicious når signaturen ikke bærer noen DocMDP i det hele tatt, siden en godkjenningssignatur formelt ikke forbyr noe, men leseren ikke lenger ser det som ble signert

FieldMDP, fra ISO 32000-1 §12.8.2.4, innsnevrer skjemafeltbeslutningen ytterligere. pfmaAll låser hvert felt, pfmaInclude låser bare de listede feltene, og pfmaExclude låser alt unntatt de listede feltene. For å anvende Include eller Exclude, løser analysatoren hvert endret felt opp til sitt fullt kvalifiserte navn gjennom /Parent-kjeden og sammenligner det med låselisten ved eksakt treff, så list terminale feltnavn i stedet for å forvente at et foreldrenavn dekker barna sine. Kan et navn ikke løses opp, eller bruker transformen en handling parseren ikke gjenkjenner, blir endringen prdIndeterminate, og prrFieldMdpUnresolved reises. Per-endring-kjennelser rulles så sammen verst først, med Suspicious rangert over Disallowed, Disallowed over Indeterminate og Indeterminate over Allowed, så ett skyggeobjekt veier tyngre enn et hvilket som helst antall legitime feltoppdateringer

Vurderingspipelinen AnalyzeSignatureRevisions anvender på hver endring etter signering i Delphi: en TPadesRevisionChangeKind fra Type- og Subtype-oppføringer, en DocMDP-beslutning ved den dekkede revisjonen fra P=1 til P=3, en FieldMDP-låsesjekk på fullt kvalifiserte feltnavn, og en verst-først-rullering fra prdSuspicious ned til prdAllowed
Ett skyggeobjekt veier tyngre enn et hvilket som helst antall legitime feltoppdateringer fordi Suspicious rangerer over Disallowed, Indeterminate og Allowed, mens noen risikoer registreres ved siden av statusen uten å nedgradere den

Hvorfor kommer noen endringer tilbake som Indeterminate i stedet for trygge?

Endringer kommer tilbake som Indeterminate når analysatoren ikke kan bevise at en endring er tillatt, fordi i en signatursjekk må et ukjent aldri rapporteres som tillatt. Ett vanlig tilfelle håndteres i stedet presist: langsiktig validering legger til en /DSS og skriver katalogen om, noe som ellers ville talt som en strukturell endring under P=1. Analysatoren stripper /DSS og /Extensions fra de gamle og nye katalogordbøkene og sammenligner resten; når ingenting annet skiller seg, behandles omskrivingen som en valideringsmateriale-oppdatering og tillates, så B-LT- og B-LTA-utvidelse bryter ikke en sertifiseringssignatur. Andre hull er med vilje igjen åpne. Type-2-oppføringer i en kryssreferansestrøm peker inn i komprimerte objektstrømmer, og analysatoren ekspanderer ikke objektstrømmer inne i denne sikkerhetsgrensen, så de endringene dukker opp som prckCompressedObject med prrCompressedObjectUnresolved, forbudt under P=1 og Indeterminate ellers. Harde budsjetter på 1024 revisjoner, 1 000 000 objektnumre og 2 000 000 rapporterte endringer gir prrResourceLimitExceeded, og en ødelagt xref-kjede gir prrMalformedRevisionChain; begge ender som Indeterminate, aldri som et pass

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

function RevisionVerdict(const R: TPadesRevisionAnalysisReport): string;
begin
  // Noen risikoer registreres uten å endre 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;

Rekkefølgen i den gaten er bevisst. prrDuplicateObjectDefinition legges til risikosettet uten å nedgradere Status av seg selv, og en FieldMDP-transform som ikke kan parses, påvirker bare statusen når et skjemafelt faktisk endres, så en gate som bare ser på Status kan gå glipp av bevis rapporten allerede inneholder. Ha i tankene hva rapporten ikke hevder heller. TPadesRevisionAnalysisReport sier ingenting om hvorvidt CMS-signaturen er kryptografisk gyldig eller om signatorsertifikatet kjeder til en rot du stoler på. Revisjonsanalyse svarer på spørsmålet om hva som skjedde etter signering, og den hører hjemme ved siden av strukturell og tillitsvalidering, ikke i stedet for dem

Skrive seed-verdier og MDP-låser ved signeringstid

De samme reglene kan forfattes ved signering gjennom TPadesSignatureFieldOptions, som er FieldOptions-medlemmet til både TPadesSignOptions og TPadesRemoteSignOptions. PDFium kan lage en widget, men kan ikke skrive /SV, /Lock, en FieldMDP- eller DocMDP-transform, eller katalogens /Perms-ordbok, så komponentens egen inkrementelle PAdES-skriver produserer disse objektene inne i den samme xref-oppdateringen som signaturen. FieldName setter rotfeltets navn, RequiredSeedValues blir /Ff-bitene i seed-verdi-ordboken beskrevet i ISO 32000-1 §12.7.4.5, Reasons, LegalAttestations og AcceptableCertificates begrenser hva en senere signatar kan velge, LockAction med LockFields skriver en indirekte /SigFieldLock, og CertificationPermission fra 1 til 3 gjør signaturen til en sertifiseringssignatur. DocMDP- og FieldMDP-transformene går begge inn i én /Reference-matrise på signaturverdien, hver med /Data som peker 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;   // kun skjemautfylling og signering
  Options.FieldOptions.RequiredSeedValues := [psvcSubFilter, psvcDigestMethod];
  Options.FieldOptions.LockAction := pfmaInclude;      // lås bare disse feltene
  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;

Noen detaljer er lette å bomme på hvis du håndruller dette. Katalogens /Perms /DocMDP må referere signaturverdiordboken, ikke widget-annotasjonen, og skriveren beholder signaturverdien som sitt eget indirekte objekt av den grunn. En eksisterende /Perms-ordbok kan allerede holde /UR3-bruksrettigheter, så skriveren kopierer den og setter inn /DocMDP i stedet for å erstatte den, ifølge tillatelsesordboken i ISO 32000-1 §12.8.4. Et dokument som allerede bærer /DocMDP, nekter en annen sertifiseringssignatur med EPadesCrypto, og det gjør inkonsistente valg også: en Include- eller Exclude-lås uten feltnavn, en All-lås med feltliste, en juridisk attest på en ikke-sertifiseringssignatur, eller et punktum i rotfeltets navn. Fjernsignering legger til én regel til, fordi signatorsertifikatet er ukjent når PreparePadesRemoteSignature kjører: å sette CertificateRequired der krever en eksplisitt AcceptableCertificates-liste, mens lokal signering kan falle tilbake til det løste signatorsertifikatet

Revisjonsanalyse fullfører signaturverktøykassen i stedet for å erstatte noen del av den. Start med å inspisere PDF-signaturer og PAdES-nivåer med PDFium Component for å lese ordboken og baselinjenivået, se på hvorfor validatorer avviser PAdES-signaturer for de strukturelle feilene som kommer før ethvert revisjonsspørsmål, og fold kjennelsen inn i en bredere revisjon av PDF-sikkerhetsrisikoer ved siden av JavaScript- og innbygd fil-sjekker. TPdf.AnalyzeSignatureRevisions, TPadesSignatureFieldOptions og den inkrementelle PAdES-skriveren vist her følger med i PDFium Component for Delphi, C++Builder og Lazarus