Teknisk artikkel

DocMDP og FieldMDP: revisjon av PDF-endringer i Delphi

En signert PDF som ble endret etter signering er ikke automatisk ødelagt. ISO 32000-1 tillater inkrementelle oppdateringer oppå en signatur, og bare noen av dem bryter policyen signeringen satte. HotPDF Component for Delphi og C++Builder svarer på det spørsmålet med AnalyzeLoadedSignatureRevisions, som klassifiserer hver revisjon etter signering og vurderer den mot DocMDP og FieldMDP. Scenarioet er kjent for alle som leverer kontraktsprogramvare: kunden din signerer en kjøpsavtale, sender den ut, og får den tilbake med en vedleggsside festet til. Leseren viser en gul stripe som sier signaturen er intakt, men dokumentet er endret siden det ble signert, og ingen i rommet kan si om det er en normal medsigneringsarbeidsflyt eller noen som stille redigerer en signert kontrakt

Hva regnes som en lovlig endring etter signering?

En endring er lovlig når dens semantiske kategori faller innenfor tillatelsen som den sertifiserende signaturen erklærte. ISO 32000-1 §12.8.2.2 definerer DocMDP-transformen med en /P-verdi på 1, 2 eller 3: 1 tillater ingen endringer i det hele tatt, 2 tillater skjemautfylling og signering, 3 tillater skjemautfylling, signering og annotasjoner. HotPDF eksponerer disse som THPDFDocMDPPermission-verdiene dmpNoChanges, dmpFormFillAndSign og dmpFormFillSignAndAnnotate, med dmpNone reservert for inspeksjonsresultater som ikke bærer noen DocMDP-transform i det hele tatt

Kategoriene er ordnet, og den ordningen er motoren i hele sjekken. THPDFRevisionModificationLevel kjører rmlNone, rmlLongTermValidation, rmlFormFillAndSign, rmlAnnotations, rmlOther, bevisst ordnet slik at et større ordinaltall aldri er mindre restriktivt. Et helt dokument reduseres til det maksimale nivået observert på tvers av hver revisjon etter signaturen, og DocMDP-sammenligningen blir en enkel heltallstest. Én nyanse betyr noe tidlig: ved dmpNoChanges godtar analysen fortsatt rmlLongTermValidation. Å legge til DSS- og VRI-valideringsmateriale eller et dokumenttidsstempel til en sertifisert fil er vedlikehold av signaturen, ikke endring av dokumentet, og å behandle det som et brudd ville ødelegge hver eneste langtidsarkiveringsarbeidsflyt som finnes

Hvordan bygger HotPDF opp revisjonskjeden på nytt?

Strukturelt, ikke heuristisk. Ifølge ISO 32000-1 §7.5.6 legger en inkrementell oppdatering til en ny kryssreferanseseksjon hvis /Prev peker på den forrige, så HotPDF leser startxref fra halen, parser seksjonen der, følger /Prev bakover og gjentar, og returnerer seksjonene eldst først. To sikkerhetsgrenser sitter i den løkken, og begge er verdt å kjenne til ved triage av en fil som feiler: en /Prev som peker på en offset som allerede er besøkt, avslutter gangen med en eksplisitt syklusdiagnose i stedet for å spinne, og en kjede lengre enn tusen revisjoner blir avvist utvetydig. Begge dukker opp i Analysis.Issue med funksjonen som returnerer False, og ingen av dem bør glattes over, fordi en syklisk /Prev er en feilformet eller fiendtlig fil snarere enn en uvanlig en

Fire historiske former dukker opp i reelle dokumenter og alle fire håndteres: tradisjonelle xref-tabeller parset linje for linje, kryssreferansestrømmer dekomprimert og dekodet gjennom sine /W- og /Index-felt, hybride referansefiler hvis tradisjonelle trailer bærer en /XRefStm-nøkkel som blir parset og slått sammen inn i samme revisjon (Office-produsent-tilfellet, dekket i artikkelen om hybride kryssreferansestrømmer), og objekter som lever inne i en ObjStm-container, som betyr noe fordi en moderne oppdatering vanligvis putter den endrede ordboken i en komprimert strøm i stedet for å skrive den direkte, som beskrevet i artikkelen om objektstrømmer og inkrementelle oppdateringer. Signaturen forankrer delingen: /ByteRange[2] + /ByteRange[3] blir SignedRevisionLength, og hver seksjon på eller forbi den offseten er etter signering. Hvorvidt byteintervallet fortsatt hasher korrekt er et separat spørsmål, besvart av VerifyLoadedSignature og dekket i artikkelen om verifisering av digitale PDF-signaturer

Hvordan hvert endrede objekt blir klassifisert

Klassifisering kjøres per objekt, og forplanter seg deretter langs referanser. For hvert objektnummer en seksjon etter signering rører, leser HotPDF den nye kroppen og kroppen slik den sto i det signerte øyeblikksbildet; en identisk kropp er rmlNone, fordi produsenter faktisk skriver om objekter uten å endre dem. Gjenkjennerne er smale med hensikt. Et /Type /DocTimeStamp-objekt, eller ett hvis /SubFilter er ETSI.RFC3161, er rmlLongTermValidation, det samme er alt som kan nås fra katalogens /DSS-tre; en /Type /Sig-ordbok er rmlFormFillAndSign. For containere er testen hvilke nøkler som flyttet seg, ikke hva objektet er: katalogen kan bare få eller endre /DSS, /Extensions eller /AcroForm; AcroForm-ordboken bare /Fields, /SigFlags, /NeedAppearances, /DR, /DA eller /Q; en side bare /Annots; et felt eller en widget bare /V, /AP, /AS eller /M. Alt utenfor disse settene faller til rmlOther, som er nøyaktig hvordan den påhengte vedleggssiden fanges: å legge til en side omorganiserer sidetreet på måter ingen hvitliste dekker, og ingen mengde legitim skjemautfylling ligner på det

Deretter forplanter nivåene seg, hvor hver container arver det maksimale nivået av de endrede barna den peker på, iterert til tilordningen stabiliserer seg. Dette er det som gjør at utseendestrømmer fungerer. Et utfylt tekstfelt skriver om /V og peker på en fersk /AP-strøm, og den strømmen er isolert sett en anonym blob av innholdsoperatorer uten noen type å gjenkjenne; fordi feltet som eier den er rmlFormFillAndSign, arver strømmen samme nivå i stedet for å falle gjennom til rmlOther. Den samme forplantningen bærer DSS-kontekst inn i sertifikat- og tilbakekallingsstrømmer som ellers ville vært uklassifiserbare

Hvorfor teller et uleselig objekt som et brudd?

Fordi alternativet er en validator som beseires ved å skrive noe den ikke forstår. Tre situasjoner ender på rmlOther uten anke i HotPDF: et objekt hvis kropp ikke kunne leses fra revisjonen, et objekt revisjonen markerer som frigjort, og et objekt som ikke matcher noen av gjenkjennerne over. Hver registrerer en spesifikk diagnose i revisjonens Issue-felt, slik at en operatør kan se hvilket objektnummer som produserte dommen

Frigjøring er den skarpeste av de tre. En revisjon etter signering som markerer et tidligere definert objekt som fritt, har slettet innhold fra et signert dokument, og ingen tillatelsesnivå under §12.8.2.2 tillater det; objektnumrene havner i FreedObjectNumbers og revisjonen heves til rmlOther. Uleselige objekter følger samme logikk av en annen grunn. En validator som ikke kan parse et objekt har ikke noe grunnlag for å kalle det ufarlig, og det ærlige svaret på det er ikke stillhet. Å rapportere en uvanlig, men uskyldig konstruksjon som et brudd koster en menneskelig gjennomgang; det motsatte feilgrepet sender ut en signert kontrakt med en ubemerket redigering inni seg

Å lese dommen i Delphi

Kallet er kort. Last inn dokumentet, velg en signaturindeks, les posten; overloaden uten parametere åpner filen dokumentet ble lastet fra på nytt, og TStream-overloaden tar bytes levert av den som kaller, og gjenoppretter strømposisjonen før den returnerer. PolicyCompliant er den ene boolske verdien de fleste vil ha, som kombinerer tre uavhengige beslutninger: den strukturelle gyldigheten til tillatelsesordbøkene, DocMDPCompliant, og FieldMDPCompliant. Hold komponentene synlige i brukergrensesnittet ditt i stedet for å kollapse dem, og merk at et dokument uten DocMDP-transform lar DocMDPCompliant stå på True, siden en vanlig godkjenningssignatur ikke erklærer noen policy å bryte, og den samlede ModificationLevel er da beskrivende snarere enn 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;

For triage vil du vanligvis ha nedbrytingen per revisjon i stedet for oppsummeringen, fordi den sier når i dokumenthistorien ting gikk galt. Hver oppføring i Analysis.Revisions bærer sin indeks i kjeden, kryssreferanseoffsetten den ble skrevet ved, sitt eget endringsnivå, og objektnumrene involvert

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 vurderes separat, og det er med hensikt

Et dokument kan tilfredsstille DocMDP og likevel være illegitimt, og det er derfor FieldMDPCompliant er en egen boolsk verdi i stedet for å foldes inn i nivåsammenligningen. ISO 32000-1 §12.8.2.4 definerer FieldMDP-transformen, og §12.7.5.5 den relaterte /SigFieldLock-oppføringen, for å fryse navngitte skjemafelter i øyeblikket signeringen skjer, selv der dokumentet som helhet fortsatt tillater skjemautfylling. Å fylle ut et felt er en nivå 2-handling; å fylle ut et felt signereren har låst er et brudd uansett nivå. HotPDF leser omfanget inn i THPDFFieldLockAction som flaAll, flaInclude eller flaExclude, med flaNone for resultater uten noen låsepolicy, og navnene inn i Permissions.FieldNames: flaAll låser alt, flaInclude låser de oppførte navnene, flaExclude låser alt unntatt dem. Én detalj betyr noe ved lesing av resultatene, nemlig at bare felt som allerede er til stede i det signerte øyeblikksbildet rapporteres i ChangedFieldNames, fordi et felt opprettet helt etter signering ikke har noen signert tilstand å motsi, og fanges i stedet av 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;

Hva denne analysen ikke forteller deg

Den verifiserer ikke en signatur. AnalyzeLoadedSignatureRevisions resonnerer om struktur og tillatelser; hvorvidt det signerte byteintervallet fortsatt hasher til verdien i CMS-blobben, og hvorvidt signeringssertifikatet kjeder til noe du stoler på, besvares av VerifyLoadedSignature og VerifyLoadedSignatureWithTrust. En fil kan være fullstendig policy-samsvarende og kryptografisk verdiløs, så de to sjekkene hører sammen side om side i enhver reell godkjenningsport. Den leser heller ikke intensjon inne i innholdsstrømmer: en side hvis innholdsstrøm ble erstattet i sin helhet fanges som en endring utenfor hvitlisten, men analysen forteller deg ikke at erstatningen byttet ut et betalingsbeløp. En rmlOther-dom betyr at et menneske bør se på den, ikke at bedrageri skjedde, og en samsvarende dom betyr at endringen passer inn i en tillatt kategori, ikke at endringen var ønsket. Når alt du trenger er det signereren erklærte, uten revisjonsgangen, returnerer GetLoadedSignaturePermissions policyordbøkene alene

Alt beskrevet her kjører nativt i Delphi og C++Builder uten noen ekstern signeringstjeneste i sløyfen, som er det som gjør det praktisk å kjøre på hvert innkommende dokument i stedet for bare de noen allerede mistenkte. Det fullstendige signatur- og revisjons-API-et, inkludert tillatelses- og verifiseringsmetodene det parres med, er del av HotPDF Component for Delphi og C++Builder