pyhanko.sign.diff_analysis module

New in version 0.2.0: pyhanko.sign.diff_analysis extracted from pyhanko.sign.validation and restructured into a more rule-based format.

This module defines utilities for difference analysis between revisions of the same PDF file. PyHanko uses this functionality to validate signatures on files that have been modified after signing (using PDF’s incremental update feature).

In pyHanko’s validation model, every incremental update is disallowed by default. For a change to be accepted, it must be cleared by at least one whitelisting rule. These rules can moreover qualify the modification level at which they accept the change (see ModificationLevel). Additionally, any rule can veto an entire revision as suspect by raising a SuspiciousModification exception. Whitelisting rules are encouraged to apply their vetoes liberally.

Whitelisting rules are bundled in DiffPolicy objects for use by the validator.

Guidelines for developing rules for use with StandardDiffPolicy

Caution

These APIs aren’t fully stable yet, so some changes might still occur between now and the first major release.

In general, you should keep the following informal guidelines in mind when putting together custom diff rules.

  • All rules are either executed completely (i.e. their generators exhausted) or aborted.

  • If the diff runner aborts a rule, this always means that the entire revision is rejected. In other words, for accepted revisions, all rules will always have run to completion.

  • Whitelisting rules are allowed to informally delegate some checking to other rules, provided that this is documented clearly.

    Note

    Example: CatalogModificationRule ignores /AcroForm, which is validated by another rule entirely.

  • Rules should be entirely stateless. “Clearing” a reference by yielding it does not imply that the revision cannot be vetoed by that same rule further down the road (this is why the first point is important).

class pyhanko.sign.diff_analysis.ModificationLevel(value)

Bases: pyhanko.pdf_utils.misc.OrderedEnum

Records the (semantic) modification level of a document.

Compare MDPPerm, which records the document modification policy associated with a particular signature, as opposed to the empirical judgment indicated by this enum.

NONE = 0

The document was not modified at all (i.e. it is byte-for-byte unchanged).

LTA_UPDATES = 1

The only updates are of the type that would be allowed as part of signature long term archival (LTA) processing. That is to say, updates to the document security store or new document time stamps. For the purposes of evaluating whether a document has been modified in the sense defined in the PAdES and ISO 32000-2 standards, these updates do not count. Adding form fields is permissible at this level, but only if they are signature fields. This is necessary for proper document timestamp support.

FORM_FILLING = 2

The only updates are extra signatures and updates to form field values or their appearance streams, in addition to the previous levels.

ANNOTATIONS = 3

In addition to the previous levels, manipulating annotations is also allowed at this level.

Note

This level is currently unused by the default diff policy, and modifications to annotations other than those permitted to fill in forms are treated as suspicious.

OTHER = 4

The document has been modified in ways that aren’t on the validator’s whitelist. This always invalidates the corresponding signature, irrespective of cryptographical integrity or /DocMDP settings.

exception pyhanko.sign.diff_analysis.SuspiciousModification

Bases: ValueError

Error indicating a suspicious modification

class pyhanko.sign.diff_analysis.QualifiedWhitelistRule

Bases: object

Abstract base class for a whitelisting rule that outputs references together with the modification level at which they’re cleared.

This is intended for use by complicated whitelisting rules that need to differentiate between multiple levels.

apply_qualified(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.ReferenceUpdate]]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.WhitelistRule

Bases: object

Abstract base class for a whitelisting rule that simply outputs cleared references without specifying a modification level.

These rules are more flexible than rules of type QualifiedWhitelistRule, since the modification level can be specified separately (see WhitelistRule.as_qualified()).

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.sign.diff_analysis.ReferenceUpdate]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

as_qualified(level: pyhanko.sign.diff_analysis.ModificationLevel)pyhanko.sign.diff_analysis.QualifiedWhitelistRule

Construct a new QualifiedWhitelistRule that whitelists the object references from this rule at the level specified.

Parameters

level – The modification level at which the output of this rule should be cleared.

Returns

A QualifiedWhitelistRule backed by this rule.

pyhanko.sign.diff_analysis.qualify(level: pyhanko.sign.diff_analysis.ModificationLevel, rule_result: Generator[X, None, R], transform: Callable[[X], pyhanko.sign.diff_analysis.ReferenceUpdate] = <function <lambda>>) → Generator[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.ReferenceUpdate], None, R]

This is a helper function for rule implementors. It attaches a fixed modification level to an existing reference update generator, respecting the original generator’s return value (if relevant).

A prototypical use would be of the following form:

def some_generator_function():
    # do stuff
    for ref in some_list:
        # do stuff
        yield ref

    # do more stuff
    return summary_value

# ...

def some_qualified_generator_function():
    summary_value = yield from qualify(
        ModificationLevel.FORM_FILLING,
        some_generator_function()
    )

Provided that some_generator_function yields ReferenceUpdate objects, the yield type of the resulting generator will be tuples of the form (level, ref).

Parameters
  • level – The modification level to set.

  • rule_result – A generator that outputs references to be whitelisted.

  • transform – Function to apply to the reference object before appending the modification level and yielding it. Defaults to the identity.

Returns

A converted generator that outputs references qualified at the modification level specified.

class pyhanko.sign.diff_analysis.DocInfoRule

Bases: pyhanko.sign.diff_analysis.WhitelistRule

Rule that allows the /Info dictionary in the trailer to be updated.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.sign.diff_analysis.ReferenceUpdate]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.DSSCompareRule

Bases: pyhanko.sign.diff_analysis.WhitelistRule

Rule that allows changes to the document security store (DSS).

This rule will validate the structure of the DSS quite rigidly, and will raise SuspiciousModification whenever it encounters structural problems with the DSS. Similarly, modifications that remove items from the DSS also count as suspicious.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.sign.diff_analysis.ReferenceUpdate]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.MetadataUpdateRule(check_xml_syntax=True, always_refuse_stream_override=False)

Bases: pyhanko.sign.diff_analysis.WhitelistRule

Rule to adjudicate updates to the XMP metadata stream.

The content of the metadata isn’t actually validated in any significant way; this class only checks whether the XML is well-formed.

Parameters
  • check_xml_syntax – Do a well-formedness check on the XML syntax. Default True.

  • always_refuse_stream_override

    Always refuse to override the metadata stream if its object ID existed in a prior revision, including if the new stream overrides the old metadata stream and the syntax check passes. Default False.

    Note

    In other situations, pyHanko will reject stream overrides on general principle, since combined with the fault-tolerance of some PDF readers, these can allow an attacker to manipulate parts of the signed content in subtle but significant ways.

    In case of the metadata stream, the risk is significantly mitigated thanks to the XML syntax check on both versions of the stream, but if you’re feeling extra paranoid, you can turn the default behaviour back on by setting always_refuse_stream_override to True.

static is_well_formed_xml(metadata_ref: pyhanko.pdf_utils.generic.Reference)

Checks whether the provided stream consists of well-formed XML data. Note that this does not perform any more advanced XML or XMP validation, the check is purely syntactic.

Parameters

metadata_ref – A reference to a (purported) metadata stream.

Raises

SuspiciousModification – if there are indications that the reference doesn’t point to an XML stream.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.sign.diff_analysis.ReferenceUpdate]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.CatalogModificationRule(ignored_keys=None)

Bases: pyhanko.sign.diff_analysis.QualifiedWhitelistRule

Rule that adjudicates modifications to the document catalog.

Parameters

ignored_keys

Values in the document catalog that may change between revisions. The default ones are /AcroForm, /DSS, /Extensions, /Metadata and /MarkInfo.

Checking for /AcroForm, /DSS and /Metadata is delegated to FormUpdatingRule, DSSCompareRule and MetadataUpdateRule, respectively.

apply_qualified(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.pdf_utils.generic.Reference]]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.ObjectStreamRule

Bases: pyhanko.sign.diff_analysis.WhitelistRule

Rule that allows object streams to be added.

Note that this rule only whitelists the object streams themselves (provided they do not override any existing objects, obviously), not the objects in them.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.pdf_utils.generic.Reference]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.XrefStreamRule

Bases: pyhanko.sign.diff_analysis.WhitelistRule

Rule that allows new cross-reference streams to be defined.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[pyhanko.pdf_utils.generic.Reference]

Apply the rule to the changes between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.FormUpdatingRule(field_rules: List[pyhanko.sign.diff_analysis.FieldMDPRule], ignored_acroform_keys=None)

Bases: object

Special whitelisting rule that validates changes to the form attached to the input document.

This rule is special in two ways:

  • it outputs FormUpdate objects instead of references;

  • it delegates most of the hard work to sub-rules (instances of FieldMDPRule).

A DiffPolicy can have at most one FormUpdatingRule, but there is no limit on the number of FieldMDPRule objects attached to it.

FormUpdate objects contain a reference plus metadata about the form field it belongs to.

Parameters
  • field_rules – A list of FieldMDPRule objects to validate the individual form fields.

  • ignored_acroform_keys – Keys in the /AcroForm dictionary that may be changed. Changes are potentially subject to validation by other rules.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Evaluate changes in the document’s form between two revisions.

Parameters
  • old – The older, base revision.

  • new – The newer revision to be vetted.

class pyhanko.sign.diff_analysis.FormUpdate(updated_ref: pyhanko.pdf_utils.generic.Reference, paths_checked: Optional[Union[pyhanko.pdf_utils.reader.RawPdfPath, Iterable[pyhanko.pdf_utils.reader.RawPdfPath]]] = None, blanket_approve: bool = False, field_name: Optional[str] = None, valid_when_locked: bool = False)

Bases: pyhanko.sign.diff_analysis.ReferenceUpdate

Container for a reference together with (optional) metadata.

Currently, this metadata consists of the relevant field’s (fully qualified) name, and whether the update should be approved or not if said field is locked by the FieldMDP policy currently in force.

field_name: Optional[str] = None

The relevant field’s fully qualified name, or None if there’s either no obvious associated field, or if there are multiple reasonable candidates.

valid_when_locked: bool = False

Flag indicating whether the update is valid even when the field is locked. This is only relevant if field_name is not None.

class pyhanko.sign.diff_analysis.FieldMDPRule

Bases: object

Sub-rules attached to a FormUpdatingRule.

apply(context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Apply the rule to the given FieldComparisonContext.

Parameters

context – The context of this form revision evaluation, given as an instance of FieldComparisonContext.

class pyhanko.sign.diff_analysis.FieldComparisonSpec(field_type: str, old_field_ref: Optional[pyhanko.pdf_utils.generic.Reference], new_field_ref: Optional[pyhanko.pdf_utils.generic.Reference], old_canonical_path: Optional[pyhanko.pdf_utils.reader.RawPdfPath])

Bases: object

Helper object that specifies a form field name together with references to its old and new versions.

field_type: str

The (fully qualified) form field name.

old_field_ref: Optional[pyhanko.pdf_utils.generic.Reference]

A reference to the field’s dictionary in the old revision, if present.

new_field_ref: Optional[pyhanko.pdf_utils.generic.Reference]

A reference to the field’s dictionary in the new revision, if present.

old_canonical_path: Optional[pyhanko.pdf_utils.reader.RawPdfPath]

Path from the trailer through the AcroForm structure to this field (in the older revision). If the field is new, set to None.

property old_field
Returns

The field’s dictionary in the old revision, if present, otherwise None.

property new_field
Returns

The field’s dictionary in the new revision, if present, otherwise None.

old_annotation_paths()
class pyhanko.sign.diff_analysis.FieldComparisonContext(field_specs: Dict[str, pyhanko.sign.diff_analysis.FieldComparisonSpec], old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver)

Bases: object

Context for a form diffing operation.

field_specs: Dict[str, pyhanko.sign.diff_analysis.FieldComparisonSpec]

Dictionary mapping field names to FieldComparisonSpec objects.

old: pyhanko.pdf_utils.reader.HistoricalResolver

The older, base revision.

new: pyhanko.pdf_utils.reader.HistoricalResolver

The newer revision.

class pyhanko.sign.diff_analysis.GenericFieldModificationRule(always_modifiable=None, value_update_keys=None)

Bases: pyhanko.sign.diff_analysis.BaseFieldModificationRule

This rule allows non-signature form fields to be modified at ModificationLevel.FORM_FILLING.

This rule will take field locks into account if the FieldComparisonContext includes a FieldMDPSpec.

For (invisible) document timestamps, this is allowed at ModificationLevel.LTA_UPDATES, but in all other cases the modification level will be bumped to ModificationLevel.FORM_FILLING.

check_form_field(fq_name: str, spec: pyhanko.sign.diff_analysis.FieldComparisonSpec, context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Investigate updates to a particular form field. This function is called by apply() for every form field in the new revision.

Parameters
  • fq_name – The fully qualified name of the form field.j

  • spec – The FieldComparisonSpec object describing the old state of the field in relation to the new state.

  • context – The full FieldComparisonContext that is currently being evaluated.

Returns

An iterable yielding FormUpdate objects qualified with an appropriate ModificationLevel.

class pyhanko.sign.diff_analysis.SigFieldCreationRule(approve_widget_bindings=True)

Bases: pyhanko.sign.diff_analysis.FieldMDPRule

This rule allows signature fields to be created at the root of the form hierarchy, but disallows the creation of other types of fields. It also disallows field deletion.

In addition, this rule will allow newly created signature fields to attach themselves as widget annotations to pages.

The creation of invisible signature fields is considered a modification at level ModificationLevel.LTA_UPDATES, but appearance-related changes will be qualified with ModificationLevel.FORM_FILLING.

apply(context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Apply the rule to the given FieldComparisonContext.

Parameters

context – The context of this form revision evaluation, given as an instance of FieldComparisonContext.

class pyhanko.sign.diff_analysis.SigFieldModificationRule(always_modifiable=None, value_update_keys=None)

Bases: pyhanko.sign.diff_analysis.BaseFieldModificationRule

This rule allows signature fields to be filled in, and set an appearance if desired. Deleting values from signature fields is disallowed, as is modifying signature fields that already contain a signature.

This rule will take field locks into account if the FieldComparisonContext includes a FieldMDPSpec.

For (invisible) document timestamps, this is allowed at ModificationLevel.LTA_UPDATES, but in all other cases the modification level will be bumped to ModificationLevel.FORM_FILLING.

check_form_field(fq_name: str, spec: pyhanko.sign.diff_analysis.FieldComparisonSpec, context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Investigate updates to a particular form field. This function is called by apply() for every form field in the new revision.

Parameters
  • fq_name – The fully qualified name of the form field.j

  • spec – The FieldComparisonSpec object describing the old state of the field in relation to the new state.

  • context – The full FieldComparisonContext that is currently being evaluated.

Returns

An iterable yielding FormUpdate objects qualified with an appropriate ModificationLevel.

class pyhanko.sign.diff_analysis.BaseFieldModificationRule(always_modifiable=None, value_update_keys=None)

Bases: pyhanko.sign.diff_analysis.FieldMDPRule

Base class that implements some boilerplate to validate modifications to individual form fields.

compare_fields(spec: pyhanko.sign.diff_analysis.FieldComparisonSpec) → bool

Helper method to compare field dictionaries.

Parameters

spec – The current FieldComparisonSpec.

Returns

True if the modifications are permissible even when the field is locked, False otherwise. If keys beyond those in value_update_keys are changed, a SuspiciousModification is raised.

apply(context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Apply the rule to the given FieldComparisonContext.

Parameters

context – The context of this form revision evaluation, given as an instance of FieldComparisonContext.

check_form_field(fq_name: str, spec: pyhanko.sign.diff_analysis.FieldComparisonSpec, context: pyhanko.sign.diff_analysis.FieldComparisonContext) → Iterable[Tuple[pyhanko.sign.diff_analysis.ModificationLevel, pyhanko.sign.diff_analysis.FormUpdate]]

Investigate updates to a particular form field. This function is called by apply() for every form field in the new revision.

Parameters
  • fq_name – The fully qualified name of the form field.j

  • spec – The FieldComparisonSpec object describing the old state of the field in relation to the new state.

  • context – The full FieldComparisonContext that is currently being evaluated.

Returns

An iterable yielding FormUpdate objects qualified with an appropriate ModificationLevel.

class pyhanko.sign.diff_analysis.DiffPolicy

Bases: object

Analyse the differences between two revisions.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver, field_mdp_spec: Optional[pyhanko.sign.fields.FieldMDPSpec] = None, doc_mdp: Optional[pyhanko.sign.fields.MDPPerm] = None)pyhanko.sign.diff_analysis.DiffResult

Execute the policy on a pair of revisions, with the MDP values provided. SuspiciousModification exceptions should be propagated.

Parameters
  • old – The older, base revision.

  • new – The newer revision.

  • field_mdp_spec – The field MDP spec that’s currently active.

  • doc_mdp – The DocMDP spec that’s currently active.

Returns

A DiffResult object summarising the policy’s judgment.

review_file(reader: pyhanko.pdf_utils.reader.PdfFileReader, base_revision: Union[int, pyhanko.pdf_utils.reader.HistoricalResolver], field_mdp_spec: Optional[pyhanko.sign.fields.FieldMDPSpec] = None, doc_mdp: Optional[pyhanko.sign.fields.MDPPerm] = None) → Union[pyhanko.sign.diff_analysis.DiffResult, pyhanko.sign.diff_analysis.SuspiciousModification]

Compare the current state of a file to an earlier version, with the MDP values provided. SuspiciousModification exceptions should be propagated.

If there are multiple revisions between the base revision and the current one, the precise manner in which the review is conducted is left up to the implementing class. In particular, subclasses may choose to review each intermediate revision individually, or handle them all at once.

Parameters
  • reader – PDF reader representing the current state of the file.

  • base_revision – The older, base revision. You can choose between providing it as a revision index, or a HistoricalResolver instance.

  • field_mdp_spec – The field MDP spec that’s currently active.

  • doc_mdp – The DocMDP spec that’s currently active.

Returns

A DiffResult object summarising the policy’s judgment.

class pyhanko.sign.diff_analysis.StandardDiffPolicy(global_rules: List[pyhanko.sign.diff_analysis.QualifiedWhitelistRule], form_rule: Optional[pyhanko.sign.diff_analysis.FormUpdatingRule], reject_object_freeing=True)

Bases: pyhanko.sign.diff_analysis.DiffPolicy

Run a list of rules to analyse the differences between two revisions.

Parameters
  • global_rules – The QualifiedWhitelistRule objects encoding the rules to apply.

  • form_rule – The FormUpdatingRule that adjudicates changes to form fields and their values.

  • reject_object_freeing

    Always fail revisions that free objects that existed prior to signing.

    Note

    PyHanko resolves freed references to the null object in PDF, and a freeing instruction in a cross-reference section is always registered as a change that needs to be approved, regardless of the value of this setting.

    It is theoretically possible for a rule to permit deleting content, in which case allowing objects to be freed might be reasonable. That said, pyHanko takes the conservative default position to reject all object freeing instructions as suspect.

apply(old: pyhanko.pdf_utils.reader.HistoricalResolver, new: pyhanko.pdf_utils.reader.HistoricalResolver, field_mdp_spec: Optional[pyhanko.sign.fields.FieldMDPSpec] = None, doc_mdp: Optional[pyhanko.sign.fields.MDPPerm] = None)pyhanko.sign.diff_analysis.DiffResult

Execute the policy on a pair of revisions, with the MDP values provided. SuspiciousModification exceptions should be propagated.

Parameters
  • old – The older, base revision.

  • new – The newer revision.

  • field_mdp_spec – The field MDP spec that’s currently active.

  • doc_mdp – The DocMDP spec that’s currently active.

Returns

A DiffResult object summarising the policy’s judgment.

review_file(reader: pyhanko.pdf_utils.reader.PdfFileReader, base_revision: Union[int, pyhanko.pdf_utils.reader.HistoricalResolver], field_mdp_spec: Optional[pyhanko.sign.fields.FieldMDPSpec] = None, doc_mdp: Optional[pyhanko.sign.fields.MDPPerm] = None) → Union[pyhanko.sign.diff_analysis.DiffResult, pyhanko.sign.diff_analysis.SuspiciousModification]

Implementation of DiffPolicy.review_file() that reviews each intermediate revision between the base revision and the current one individually.

pyhanko.sign.diff_analysis.DEFAULT_DIFF_POLICY = <pyhanko.sign.diff_analysis.StandardDiffPolicy object>

Default DiffPolicy implementation.

This policy includes the following rules, all with the default settings. The unqualified rules in the list all have their updates qualified at level LTA_UPDATES.

pyhanko.sign.diff_analysis.NO_CHANGES_DIFF_POLICY = <pyhanko.sign.diff_analysis.StandardDiffPolicy object>

DiffPolicy implementation that does not provide any rules, and will therefore simply reject all changes.

class pyhanko.sign.diff_analysis.DiffResult(modification_level: pyhanko.sign.diff_analysis.ModificationLevel, changed_form_fields: Set[str])

Bases: object

Encodes the result of a difference analysis on two revisions.

Returned by DiffPolicy.apply().

modification_level: pyhanko.sign.diff_analysis.ModificationLevel

The strictest modification level at which all changes pass muster.

changed_form_fields: Set[str]

Set containing the names of all changed form fields.

Note

For the purposes of this parameter, a change is defined as any FormUpdate where FormUpdate.valid_when_locked is False.