Batch PDF signing for Belgian administrative workflows, usable from both a command line and a CustomTkinter GUI, with three modes:
beid— qualified (QES-grade) cryptographic signatures with the Belgian eID card via PKCS#11, stamping a visible vignette (cardholder photo + name + date) on each document. One PIN entry per document.azure— personal advanced (AES) signatures with the user's own certificate held in Azure Key Vault, after a single Microsoft Entra ID login per batch; only the document digest ever leaves the machine.image— a simple image stamp (no cryptographic value).
Signatures are PAdES up to B-LTA by default: trusted RFC 3161 timestamp, embedded revocation info (LTV), and an archival timestamp chain — with trust anchors drawn from the EU Trusted List (eID) or the organisation's internal CA (Azure). Every signed file is re-validated on the spot and the achieved level is reported; levels are never silently downgraded.
Batches are validated against a template PDF (exact page count + dimensions) before signing, outputs never overwrite existing files, and the whole thing ships as two standalone PyInstaller binaries (windowed GUI + headless CLI) for Linux and Windows.
⚖️ The eID mode uses the card's non-repudiation certificate, legally equivalent to a handwritten signature. The national register number is embedded in every signature produced — mind the distribution of the signed PDFs.
| Mode | Requires | Nature | Output |
|---|---|---|---|
beid |
reader + eID card + PIN per document | cryptographic eID signature (pyHanko via the PKCS#11 middleware) — qualified-grade (QES) | visible vignette: cardholder photo + "Signed by:" / name / date |
azure |
Microsoft (Entra ID) login, one per batch + a personal key/cert in Azure Key Vault | cryptographic personal signature — advanced (AES), not qualified | visible vignette: "Signed by:" / name / date (no photo) |
image |
nothing | image stamp (this is not a cryptographic signature) | the supplied image, placed at a chosen position |
In all modes, the same vignette/image + page + position is applied to all the documents in the batch — template validation guarantees the files are geometrically identical.
beid(eID) carries the strongest legal weight (qualified certificate, legally equivalent to a handwritten signature) but needs the physical card, a reader, and one PIN entry per document.azuresigns with the user's personal certificate held in Azure Key Vault after one interactive Microsoft login per batch — far better ergonomics for large batches, but the result is an advanced electronic signature (AES): fine for internal documents; for documents relied upon by external third parties, a publicly recognised (qualified) issuer would be needed.imageis a visual stamp only — no cryptographic value.
If a template PDF is supplied, each input is accepted only if it has the same page count AND exactly identical per-page dimensions (strict equality, no tolerance). Rejected files are never signed; the rejection reason is displayed (CLI summary / GUI table).
Files are written as {name}_signe.pdf in the output folder and are never
overwritten: on collision, - 1, - 2, … are appended.
beidmode: the Belgian eID middleware installed (https://eid.belgium.be), which provides the PKCS#11 library (libbeidpkcs11.so/beidpkcs11.dll/…dylib), a reader + inserted eID card, and the PC/SC service (pcscd) running. The PIN is requested for each document. Levels ≥ b-t additionally need network access (TSA, EU trusted list, OCSP/CRL — see Signature levels below);b-bis offline.azuremode: no hardware — outbound network tologin.microsoftonline.com, the Key Vault URL, the TSA (≥ b-t) and the internal CA's CRL/OCSP (≥ b-lt); per-user keys provisioned in Key Vault (see the azure section above).imagemode: nothing special — pure PDF stamping.- Graphical interface:
customtkinter+ a Python withtkinterand a display. The page preview (step 6) is rendered by pypdfium2 (the bundled PDFium engine) — no external dependency to install. (poppler /pdftoppmis now only an optional fallback if already present.)
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
# GUI on Ubuntu/Debian, if tkinter is missing: sudo apt install python3-tk# eID signing (vignette bottom-right of the last page), PAdES B-LTA by default:
./venv/bin/python sign_pdfs_beid.py --input ../pdfs --output ../signes --mode beid
# eID signing at a lighter level (basic signature, fully offline):
./venv/bin/python sign_pdfs_beid.py --input ../pdfs --output ../signes --pades-level b-b
# Image stamp (no card), validated against a template:
./venv/bin/python sign_pdfs_beid.py --mode image \
--template ../pdfs/MODELE.pdf --input ../pdfs --output ../signes \
--image-path signature.png --page 1 --x 360 --y 150
# Azure mode (personal Key Vault certificate, one Microsoft login per batch):
./venv/bin/python sign_pdfs_beid.py --mode azure \
--azure-vault-url https://myorg-sign.vault.azure.net \
--azure-trust-anchors ./internal-ca-chain.pem \
--input ../pdfs --output ../signes
# Graphical interface:
./venv/bin/python sign_pdfs_beid.py --gui| Flag | Meaning |
|---|---|
--gui |
launch the graphical interface; otherwise run in console mode. |
--input <paths…> |
files and/or folders to process (folders are globbed for *.pdf). |
--output <folder> |
output folder ({name}_signe.pdf, never overwritten). |
--template <pdf> |
template PDF; if supplied, inputs are validated against it. |
--mode beid|image |
signature mode (default beid). |
--image-path <img> |
image to stamp (required in --mode image). |
--page <N> |
target page, 1-based. Image: insertion page. beID: vignette page. |
--x <pt> --y <pt> |
lower-left corner, in points from the page's bottom-left. beID: omit both ⇒ bottom-right of the last page. |
--pades-level <lvl> |
PAdES baseline level: b-b, b-t, b-lt, b-lta (default b-lta). See Signature levels below. |
--timestamp-url <url> |
RFC 3161 TSA for levels ≥ b-t. Precedence: flag > CACHET_TSA_URL env > http://timestamp.digicert.com. |
--trust-list-url <url> |
EU LOTL URL seeding the LTV trust anchors. Precedence: flag > CACHET_LOTL_URL env > the official EU URL. |
--refresh-trust-list |
force re-download of the EU trusted list (bypass the 24 h cache). |
--digest <alg> |
signature digest: sha256 (default), sha384, sha512. |
--no-verify |
skip the post-signing self-verification (levels ≥ b-t). |
--pades |
deprecated no-op (PAdES is now the default); use --pades-level. |
--legacy-cms |
deprecated: legacy non-PAdES adbe.pkcs7.detached signature (no timestamp, no LTV). Incompatible with levels above b-b. |
--lib <path> |
path to the eID PKCS#11 library (otherwise OS-default value). |
--field <name> |
base name of the signature field (beid/azure modes). |
--azure-vault-url <url> |
Key Vault URL (required in azure mode; env CACHET_AZURE_VAULT_URL). |
--azure-key-name <name> |
explicit key override — bypasses the per-user derivation and is flagged in the output (env CACHET_AZURE_KEY_NAME). |
--azure-key-name-template <tpl> |
per-user key derivation, default sig-{upn}; placeholders {upn}, {upn_local}, {oid}, sanitised to the Key Vault charset (env CACHET_AZURE_KEY_NAME_TEMPLATE). |
--azure-cert-name <name> |
certificate name if it differs from the key name (env CACHET_AZURE_CERT_NAME). |
--azure-auth <m> |
interactive (browser; GUI default), device-code (CLI default), default (DefaultAzureCredential — testing/CI only, breaks the per-user model). Env CACHET_AZURE_AUTH. |
--azure-trust-anchors <path> |
PEM/DER file or directory with the internal CA chain used as LTV trust anchors in azure mode (required for b-lt/b-lta and for self-verification at b-t). Env CACHET_AZURE_TRUST_ANCHORS. |
--azure-graph |
opt-in: use the Microsoft Graph /me displayName for the vignette. |
| Level | Adds | Network needed |
|---|---|---|
b-b |
basic eID signature | none (offline) |
b-t |
+ trusted RFC 3161 timestamp | TSA (+ EU trusted list unless --no-verify) |
b-lt |
+ revocation info (OCSP/CRL) embedded in the document (LTV) | TSA, EU trusted list, OCSP/CRL endpoints |
b-lta |
+ archival document-timestamp chain (default) | same as b-lt |
- Long-term validation (LTV): from
b-ltupward, everything needed to validate the signature later (CA certificates from the EU trusted list, OCSP/CRL responses) is embedded at signing time, so the PDF stays verifiable after certificates expire.b-ltaadds a document timestamp so the evidence chain itself stays provable — for genuine archival, that chain must be renewed periodically (see BUILD.md). - ⚠ Free vs qualified timestamps: the default TSA
(
http://timestamp.digicert.com) is free and yields technically valid B-T/B-LTA signatures, but not qualified timestamps in the eIDAS sense. For genuine qualified long-term preservation, point--timestamp-urlat a qualified TSA (see the EU trusted list for QTSPs offering QTST services). - Self-verification: after writing each signed PDF (levels ≥ b-t), the file
is re-opened and validated, and the achieved level is reported in the
summary (e.g.
PAdES-B-LTA, LTV ok). A mismatch marks the document failed — the level is never silently downgraded;--no-verifyskips this check. - Offline behaviour:
b-bandimagemode work fully offline. Levels ≥ b-t fail with an actionable error naming the unreachable endpoint (TSA, EU trusted list, OCSP/CRL). - 🔒 Privacy: the signer's national register number (RRN) is embedded in every eID signature (it is part of the certificate). The CLI warns at startup and in the summary — mind how signed PDFs are distributed.
- What it is: each user signs in their own name with their personal certificate + non-exportable key held in Azure Key Vault, after an interactive Microsoft Entra ID login (one per batch — not per document). Only the document digest is sent to Azure; the document never leaves the machine. The result is an advanced electronic signature (AES) — appropriate for internal documents; it is not a qualified signature (QES).
- Prerequisite (Azure admin): provision, per user, a Key Vault key +
certificate issued by the organisation's internal CA (ADCS / Entra-issued),
named after the key template (default
sig-{upn}, e.g.sig-jane-doe-example-org), and grant each usergeton certificates andsignon their own key (access policy / RBAC). Provisioning itself is out of scope for this tool. - Per-user rule: the key name is derived from the signed-in user's UPN,
so a user can only sign with their own key; an explicit
--azure-key-namebypass is visibly flagged, and Key Vault access policy remains the hard authorization gate. - Trust/LTV: azure mode builds its validation context from the internal
CA chain (
--azure-trust-anchors) — the EU trusted list is not used here (it is eID-specific). For b-lt/b-lta the internal CA must publish reachable CRL/OCSP endpoints, otherwise signing fails with a clear error (never a silent downgrade). - Network:
login.microsoftonline.com, the vault URL, the TSA (≥ b-t) and the internal CA's CRL/OCSP endpoints (≥ b-lt) must be reachable.
Backward compatibility: the legacy positional form
inputs… output_folderis still accepted if--input/--outputare absent.
A vertical wizard walks through the flow: 1. template → 2. files →
3. output folder → 4. validation (pass/fail table) → 5. mode
(eID/image/Azure + PAdES level selector, default b-lta; in Azure mode a
panel offers the vault settings and a "Sign in with Microsoft" action) →
6. page + position (actual page preview, click to
place) → 7. launch → 8. per-document summary.
The project compiles into two standalone binaries per OS (a windowed GUI
cachet, a console CLI cachet-cli) — no Python required on the target
machine. Official builds are published as GitHub Releases: merging
develop into main automatically tags v{version} and attaches the
Linux/Windows packages (see Release process in BUILD.md). See BUILD.md for all the routes (native Linux, native
Windows, Wine, and GitHub Actions CI).
./build_linux.sh # Linux -> dist/cachet , dist/cachet-cli
build_windows.bat # Windows -> dist\cachet.exe , dist\cachet-cli.exeThe eID middleware remains a runtime dependency (beid mode) and is never bundled; the page preview, for its part, works without installing anything (PDFium bundled via pypdfium2).
Headless unittest suite (no card, no tkinter):
./venv/bin/python -m unittest -v| File | Role |
|---|---|
sign_pdfs_beid.py |
core + CLI entry point (business logic, importable without tkinter). |
trust.py |
EU trusted-list (LOTL) trust provider: anchors for LTV in beid mode, with local cache. |
azure_signer.py |
azure mode: Entra ID login, per-user Key Vault key/cert resolution, pyHanko signer (digest-only signing). |
gui.py |
CustomTkinter interface (façade over the core). |
gui_main.py |
entry point of the windowed binary (opens the GUI). |
test_sign_pdfs_beid.py, test_trust.py, test_azure.py |
unittest test suites. |
cachet.spec |
PyInstaller recipe (two binaries). |
build_*.sh / build_windows.bat |
build scripts. |
.github/workflows/build.yml |
CI: Windows + Linux binaries as artifacts. |
BUILD.md |
detailed packaging guide. |