TECHNICAL8 min read

How to Validate a PAdES Signature (a Developer's Walkthrough)

SahlSign Team|

If you are building signature validation into a product — a procurement system that checks contracts before payment, an archive that must prove documents years later, a compliance pipeline — "it shows a green tick in Acrobat" is not an acceptance test. PAdES validation is a specified algorithm with defined inputs, defined checks, and defined failure states. This is that algorithm, in the order a conformant validator runs it, with the byte-level detail you need to implement or audit one.

ETSI EN 319 142-1

Defines the PAdES baseline profiles (B-B, B-T, B-LT, B-LTA) — what structures a compliant PDF signature must contain at each level

ETSI TC ESI

ISO 32000-2 §12.8

Specifies the PDF signature dictionary: the /ByteRange, /Contents, /SubFilter, and how the signature is embedded in the file

ISO/IEC 32000-2:2020

RFC 5652

The Cryptographic Message Syntax (CMS) — the SignedData structure PAdES carries in /Contents as a detached signature

IETF CMS

The signature lives in the PDF's structure

A PAdES signature is a PDF signature dictionary embedded via an incremental update. Two entries do the real work. /ByteRange is an array of four integers — [a b c d] — that defines exactly which bytes of the file the signature covers: bytes a through a+b, then c through c+d. The gap in the middle is the hole where the signature itself sits. /Contents is that hole: a hex-encoded PKCS#7 / CMS SignedData object, detached (the signed content is the ByteRange bytes, not embedded in the CMS).

Getting the ByteRange right is the first place validators fail. If the ByteRange does not cover the entire file except the /Contents hole, an attacker can append content after the signed range — the classic "incremental saving" attack. A correct validator confirms the ByteRange leaves no unsigned bytes other than the signature hole itself.

Step 1

Parse the signature dictionary

Locate the /Sig dictionary, read /ByteRange and /Contents. Confirm /SubFilter is ETSI.CAdES.detached (PAdES) — not the legacy adbe.pkcs7.detached — and that the ByteRange spans the whole file bar the /Contents hole.

Step 2

Recompute and match the message digest

SHA-256 the bytes covered by /ByteRange. Compare against the message-digest signed attribute inside the CMS SignedData. Mismatch means the covered bytes changed since signing.

Step 3

Verify the CMS signature over the signed attributes

The RSA (or ECDSA) signature covers the DER-encoded set of signed attributes, not the raw digest. Verify it with the signer's public key from the embedded certificate.

Step 4

Validate the signing certificate and timestamp

Walk the X.509 chain to a trusted root, check revocation (CRL/OCSP), and validate the RFC 3161 timestamp token in the unsigned attributes over the signature value.

The signed attributes are where PAdES gets strict

A common misconception is that the signature covers the document hash directly. It does not. In CMS, when signed attributes are present (and PAdES requires them), the signature is computed over the DER encoding of the complete SignedAttributes set. Two of those attributes are mandatory and are exactly what a validator must check.

The signed attributes a PAdES validator must confirm

  • message-digest (RFC 5652)

    The SHA-256 of the ByteRange content. The validator recomputes this independently and confirms it equals the attribute value. This is the link between the signature and the actual document bytes.

  • ESS signing-certificate-v2 (RFC 5035)

    A hash of the signing certificate, bound into the signed attributes. This prevents certificate-substitution attacks — swapping the certificate while keeping the signature. PAdES mandates it; a validator that skips it accepts a substituted cert. Note: node-forge's built-in PKCS#7 cannot emit this attribute, which is why robust implementations build the SignedData manually.

  • content-type

    Must be present and consistent (id-data). Part of the DER-sorted attribute set the signature commits to.

  • DER ordering of the attribute set

    SignedAttributes are a SET OF, which DER requires in sorted order. If the signer built them unsorted, the signature won't reproduce. Validators and signers must both canonicalise identically.

The baseline levels decide how long it stays valid

PAdES-B-B is a valid signature today. Whether it is still verifiable in five years depends on which baseline level it reached. The levels are cumulative — each includes the one below — and each adds proof that survives a different kind of decay.

PAdES baseline levels per ETSI EN 319 142-1. A validator must know the target level to judge whether missing data is a defect or expected. SahlSign produces B-T by default.

JurisdictionLawCross-border transfer ruleIntensity
PAdES-B-BBasicSignature + signing certificate. No trusted signing time. Verifiable only while the certificate is valid and you trust the signer's clock.Moderate
PAdES-B-TTimestampAdds an RFC 3161 timestamp over the signature value from a trusted TSA. The signing instant is provable independently of the signer. The practical baseline for legally binding signatures.Restricted
PAdES-B-LTLong-TermEmbeds the full certificate chain plus CRL/OCSP revocation data in a DSS dictionary. The signature validates even after the issuing CA goes offline.Restricted
PAdES-B-LTAArchiveAdds document timestamps that are periodically renewed. Protects against cryptographic algorithm decay for archive-grade retention (10+ years).Strict

Validating the timestamp

At B-T and above, the RFC 3161 timestamp token sits as an unsigned attribute (signature-time-stamp) inside the CMS. It is itself a CMS SignedData, signed by the TSA, whose message imprint is a hash of the signature value — not the document. A validator must confirm the imprint matches, that the TSA certificate chains to a trusted timestamping root, and parse the genTime from the TSTInfo for the asserted signing time.

The timestamp covers the signature value, and it is requested after signing — hash the signature bytes, send that imprint to the TSA, embed the returned token. A validator that tries to verify the timestamp against the document digest instead of the signature value will reject every valid B-T signature it sees.

The ordering that trips up first implementations

Do not roll your own — use the reference validator

Unless you have a specific reason to implement the algorithm yourself, validate with the European Commission's DSS. It implements every ETSI profile, produces a structured report naming the exact sub-check that failed, and runs offline for court-discovery and bulk-audit workflows.

Programmatic PAdES validation

  • EU DSS (open source, Java + REST)

    The reference implementation of the full ETSI validation algorithm. Feed it the PDF and a trust anchor set; it returns a detailed report (Total-passed / Indeterminate / Total-failed) with the reason for each verdict.

  • pyHanko (Python)

    A well-maintained open-source library for both signing and validating PAdES, including LTV and timestamp validation — useful for embedding validation in a Python pipeline.

  • SahlSign /verify/[documentId]

    Every SahlSign document exposes a public verification endpoint that re-checks the integrity hash, audit chain, and TSA timestamp server-side — so a counterparty validates without trusting our UI or installing anything.

How SahlSign builds its PAdES signatures

SahlSign constructs the CMS SignedData manually rather than relying on a library's PKCS#7 helper, precisely so it can emit the ESS signing-certificate-v2 attribute PAdES mandates, DER-sort the signed attributes, and request the RFC 3161 timestamp over the signature value per ETSI EN 319 122-1. The result validates cleanly in Acrobat, EU DSS, and any conformant PAdES validator.

PAdES-B-T

SahlSign's default output: detached PKCS#7 SignedData over the /ByteRange, SHA-256 + RSA, ESS signing-certificate-v2 in DER-sorted signed attributes, and an RFC 3161 timestamp from a EUTL-listed TSA over the signature value.

src/lib/pdf-seal.ts and src/lib/tsa.ts in the SahlSign source

Related reading

Sources

PAdES validationPAdESETSI EN 319 142CMSPKCS#7RFC 5652RFC 3161ByteRangeESS signing-certificate-v2EU DSSISO 32000digital signaturesQatarGCCMENA

Ready to try SahlSign?

Start your free 14-day trial. No credit card required.

Try for Free