Incremental update analysis
Changed in version 0.2.0: The initial ad-hoc approach was replaced by a more extensible and
maintainable rule-based validation system. See
pyhanko.sign.diff_analysis.
As explained in the CLI documentation, the PDF standard has provisions that allow files to be updated by appending so-called “incremental updates”. This also works for signed documents, since appending data does not destroy the cryptographic integrity of the signed data.
However, since incremental updates can change essentially any aspect of the resulting document, validators need to be careful to evaluate whether these updates were added for a legitimate reason. Examples of such legitimate reasons could include the following:
adding a second signature,
adding comments,
filling in (part of) a form,
updating document metadata,
performing cryptographic “bookkeeping work” such as appending fresh document timestamps and/or revocation information to ensure the long-term verifiability of a signature.
Not all of these reasons are necessarily always valid: the signer can tell
the validator which modifications they allow to go ahead without invalidating
their signature. This can either be done through the “DocMDP” setting (see
MDPPerm), or for form fields, more granularly
using FieldMDP settings (see FieldMDPSpec).
That being said, the standard does not specify a concrete procedure for
validating any of this. PyHanko takes a reject-by-default approach: the
difference analysis tool uses rules to compare document revisions, and judge
which object updating operations are legitimate (at a given
MDPPerm level). Any modifications for which
there is no justification invalidate the signature.
The default diff policy is defined in
DEFAULT_DIFF_POLICY, but you can define
your own, either by implementing your own subclass of
DiffPolicy, or by defining your own rules
and passing those to an instance of StandardDiffPolicy.
StandardDiffPolicy takes care of some
boilerplate for you, and is the mechanism backing
DEFAULT_DIFF_POLICY.
Explaining precisely how to implement custom diff rules is beyond the scope
of this guide, but you can take a look at the source of
the diff_analysis module for more information.
To actually use a diff policy, pass it to
validate_pdf_signature() via the
diff_policy parameter.
from pyhanko.keys import load_cert_from_pemder
from pyhanko_certvalidator import ValidationContext
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko.sign.validation import validate_pdf_signature
from pyhanko.sign.diff_analysis import DEFAULT_DIFF_POLICY
root_cert = load_cert_from_pemder('path/to/certfile')
vc = ValidationContext(trust_roots=[root_cert])
# Substitute your own DiffPolicy instance here; the default is used
# for illustration.
diff_policy = DEFAULT_DIFF_POLICY
with open('document.pdf', 'rb') as doc:
r = PdfFileReader(doc)
sig = r.embedded_signatures[0]
status = validate_pdf_signature(sig, vc, diff_policy=diff_policy)
print(status.pretty_print_details())
The modification_level
and docmdp_ok attributes
on PdfSignatureStatus will tell you to what degree the signed file has been
modified after signing (according to the diff policy used).
Warning
The most lenient MDP level,
ANNOTATE, is currently not
supported by the default diff policy.
Danger
Due to the lack of standardisation when it comes to signature validation, correctly adjudicating incremental updates is inherently somewhat risky and ill-defined, so until pyHanko matures, you probably shouldn’t rely on its judgments too heavily.
Should you run into unexpected results, by all means start a discussion. All information helps!
If necessary, you can opt to turn off difference analysis altogether. This is sometimes a very reasonable thing to do, e.g. in the following cases:
you don’t trust pyHanko to correctly evaluate the changes;
the (sometimes rather large) performance cost of doing the diff analysis is not worth the benefits;
you need validate only one signature, after which the document shouldn’t change at all.
In these cases, you might want to rely on the
coverage property
of PdfSignatureStatus instead. This property describes the degree to which
a given signature covers a file, and is much cheaper/easier to compute.
Anyhow, to disable diff analysis completely, it suffices to pass the
skip_diff parameter to
validate_pdf_signature().
from pyhanko.keys import load_cert_from_pemder
from pyhanko_certvalidator import ValidationContext
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko.sign.validation import validate_pdf_signature
root_cert = load_cert_from_pemder('path/to/certfile')
vc = ValidationContext(trust_roots=[root_cert])
with open('document.pdf', 'rb') as doc:
r = PdfFileReader(doc)
sig = r.embedded_signatures[0]
status = validate_pdf_signature(sig, vc, skip_diff=True)
print(status.pretty_print_details())