Skip to main content

scanmate-seal

Is a signed PDF still the document that was signed?

A parallel port of @scanmate/seal, not a replacement for it. The TypeScript remains the reference, and the only useful definition of "correct" for this package is that it reaches the same verdict — on the same bytes, for the same reasons — as the TypeScript it parallels.

pip install scanmate-seal
from pathlib import Path
from scanmate_seal import verify_signatures

report = verify_signatures(Path("agreement.pdf").read_bytes())
report.unbroken                       # every signature intact, and the last covers the whole file
report.signatures[0].signer.subject   # CN=A Person, O=A Company
report.signatures[0].problems         # why not, when not

Why it exists

The rest of this suite compares a returned document with the one that was issued. This asks the other question, of a born-digital return: has the file changed since it was signed? A signature answers that in its own bytes, and nothing else in the suite can.

Per signature, four findings that stay separate on purpose:

  1. The bytes the signature covers digest to what the signature says they do.
  2. That digest is signed by the key of the certificate it carries.
  3. How much of the file lies outside the signed ranges.
  4. Whether the certificate was in date when the file says it was signed.

They stay separate because they mean different things. A PDF may be signed and then legitimately added to — a second signature, a form field filled — and those later bytes are outside the first signature. Intact but not whole is "this much of the file is vouched for, and there is more", which is not the same as "this file was tampered with". Likewise an expired certificate does not make a file broken: unbroken is a statement about bytes, not about trust.

No PDF parser

Nothing here opens the PDF. Signature dictionaries are read from the file's own bytes, and that is by construction rather than by luck: /Contents holds the signature over the file, so it cannot be compressed into an object stream or encrypted without the offsets in /ByteRange ceasing to mean anything. Every signed PDF therefore carries its signatures in plain sight.

Which is also why this still reads a file a parser would refuse — and when the question is whether a file was tampered with, that is exactly the file you have.

What it does not say

It does not say a signature is legally valid. That is a question about trust lists, revocation, timestamps and jurisdiction, and this package checks none of them:

  • no certificate chain is built or validated against a trust store
  • no revocation is checked (no CRL, no OCSP)
  • no timestamp token is verified
  • /Name and /Reason are what the signing software wrote, not evidence

"The bytes still match the signature, and here is the certificate that made it" is a narrower claim, and it is the one this package makes.

Two departures from the TypeScript

verify_signatures is not async. verifySignatures is, because pkijs reaches WebCrypto through a promise — not because the algorithm waits on anything. No step of this does I/O, so an async def here would be a coroutine that never yields. The departure is in the calling convention only.

A SignatureProblem is one class, not a union of five. TypeScript's discriminated union maps to a single dataclass with kind as the discriminant and an optional payload, rather than to five near-empty classes.

Everything else is held to the TypeScript by goldens generated from the real build, over committed signed-PDF fixtures. The one value deliberately not held to parity is SignatureProblem.because, which is whichever library phrased a malformed CMS structure; no two phrase it alike. The kind is the contract.

Requirements

Python 3.11 or newer. Fully typed (PEP 561), mypy --strict clean.

Signature parsing is asn1crypto and signature checking is cryptography — two libraries where the TypeScript uses one, because cryptography ships no complete CMS SignedData reader and asn1crypto does no cryptography at all.

Release files for scanmate-seal 0.24.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for scanmate-seal 0.24.3
File Size Uploaded
scanmate_seal-0.24.3.tar.gz 24.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for scanmate-seal 0.24.3
File Interpreter ABI Platform
scanmate_seal-0.24.3-py3-none-any.whl Python 3 none any Details

Total release size: 45.0 kB

Release files / scanmate_seal-0.24.3.tar.gz

Download URL scanmate_seal-0.24.3.tar.gz
Size 24.3 kB
Tags Source
SHA-256 checksum
How to use checksums
64244b4daaa71175f71a2eb43aeb672290ec7b4e1fbe09b0b8ff232f13bc9e32
BLAKE2b-256 checksum
How to use checksums
01af354d9ca16d1d82d21124a988dc1c483d1f0e5e379a1a68ebd66386b0b749
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / scanmate_seal-0.24.3-py3-none-any.whl

Download URL scanmate_seal-0.24.3-py3-none-any.whl
Size 20.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ae8441614ba38e515b1ffbd03838f3aaea2b3386f46cf9ea471821e5ed1f6e8c
BLAKE2b-256 checksum
How to use checksums
7c4ed44794233f343466f67deb7c8ab5012ccbdf163f58ca082a6d507a7044b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.24.3 This release

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page