Digital Signatures
August 25, 2026 ยท View on GitHub
Back to root overview: README.md
tc-lib-pdf produces detached CMS (PKCS#7) signatures and PAdES baseline signatures
(ETSI EN 319 142-1) with optional RFC 3161 timestamps, LTV (Long-Term Validation)
material, and archive timestamps. The cryptography lives in the companion package
tecnickcom/tc-lib-pdf-sign.
Signature profiles
The profile option selects the signature format:
| Profile | /SubFilter | What it adds |
|---|---|---|
legacy (default) | adbe.pkcs7.detached | ISO 32000-1 detached CMS with the ESS signing-certificate-v2 attribute. |
pades-b-b | ETSI.CAdES.detached | PAdES-BASELINE-B: CAdES-based CMS; the signing time is carried by the /M dictionary entry (the CMS signing-time attribute is omitted, as the baseline requires). |
pades-b-t | ETSI.CAdES.detached | PAdES-BASELINE-T: B-B plus an RFC 3161 signature timestamp embedded in the CMS. |
pades-b-lt | ETSI.CAdES.detached | PAdES-BASELINE-LT: B-T plus a Document Security Store (/DSS, /VRI) with certificates and, where reachable, OCSP/CRL revocation data. |
pades-b-lta | ETSI.CAdES.detached + ETSI.RFC3161 | PAdES-BASELINE-LTA: B-LT plus a /Type /DocTimeStamp archive timestamp over the whole document. |
digest_algorithm accepts sha256 (default), sha384, or sha512; RSA and ECDSA signing
keys are both supported.
Signature-focused runnable examples:
- examples/E007_signature_basic.php: PAdES-BASELINE-B signature via the fluent
signature()facade. - examples/E008_signature_timestamp.php: PAdES-BASELINE-T signature with an RFC 3161 TSA timestamp.
- examples/E009_signature_ltv.php: PAdES-BASELINE-LT signature with LTV material (
/DSS,/VRI). - examples/E081_signature_pades_lta.php: PAdES-BASELINE-LTA signature with a document archive timestamp via
upgradeToLta(). - examples/E075_external_signature_injection.php: external/remote signing workflow with ByteRange digest export and later CMS signature injection.
- examples/E084_signature_two_phase_cms.php: two-phase signing where a key service signs the CMS signed attributes and the detached CAdES is assembled here.
Fluent API: signature()
The signature() facade is the entry point. Each call is chainable and forwards to the
underlying methods setSignature(), setSignTimeStamp(), setUserRights(),
setSignatureAppearance(), and so on, which are also callable directly.
$pdf->signature()
->configure([
'profile' => 'pades-b-t', // legacy | pades-b-b | pades-b-t | pades-b-lt | pades-b-lta
'digest_algorithm' => 'sha256', // sha256 | sha384 | sha512
'signcert' => 'file:///path/to/cert.pem',
'privkey' => 'file:///path/to/key.pem',
'password' => '',
'extracerts' => 'file:///path/to/chain.pem', // optional issuer chain
'cert_type' => 2,
'info' => [
'Name' => 'Jane Smith',
'Location' => 'London',
'Reason' => 'Document approval',
'ContactInfo' => 'jane@example.com',
],
])
->timestamp([
'enabled' => true,
'host' => 'https://freetsa.org/tsr',
'hash_algorithm' => 'sha256',
'timeout' => 30,
'verify_peer' => true,
'allow_sha1' => true, // this TSA still uses the SHA-1 v1 attribute
]);
$pdf->signature()->appearance()->place(posx: 15, posy: 35, width: 90, height: 20, page: -1, name: 'Signature');
$widgetObjId = $pdf->signature()->widgetObjectId();
A signature field name cannot contain a period: ISO 32000-1 clause 12.7.3.2 makes it the separator between the components of a fully qualified field name, so one is rejected.
Adding a TSA Timestamp (RFC 3161)
For pades-b-t and above a timestamp is required. Configure it with
signature()->timestamp([...]) (or setSignTimeStamp([...])); the RFC 3161 token is
embedded in the CMS as the id-aa-signatureTimeStampToken unsigned attribute:
$pdf->signature()->timestamp([
'enabled' => true,
'host' => 'https://freetsa.org/tsr',
'username' => '',
'password' => '',
'cert' => '',
'hash_algorithm' => 'sha256', // sha256 | sha384 | sha512
'policy_oid' => '', // optional OID string
'nonce_enabled' => true,
'timeout' => 30,
'verify_peer' => true,
'allow_sha1' => false, // accept a SHA-1 token (see below)
]);
The token a TSA returns is verified before it is embedded: its SignerInfo signature
must verify against the TSA certificate the token carries, that certificate must hold
id-kp-timeStamping as its single critical extended key usage and must have covered the
instant the token attests, and the token must answer the request that was sent (same
message imprint under the same digest algorithm, the nonce echoed unchanged, the policy
that was asked for, and a genTime near the moment of the request). A token failing any
of these raises a \Com\Tecnick\Pdf\Exception instead of being written to the document.
allow_sha1 (off by default) relaxes the digest rules for that check: it accepts a token
whose signature, message digest, or ESS certificate hash uses SHA-1. Some public TSAs
name their certificate with the RFC 2634 signing-certificate (v1) attribute, which is
SHA-1 by definition, and are refused without it; freetsa.org, used by the signature
examples, is one of them. A TSA emitting the signing-certificate-v2 attribute
(SHA-256) needs no such setting. The relaxation applies only to the timestamp token, not
to the OCSP responses or CRLs collected for the DSS.
LTV (Long-Term Validation) and archive timestamps
Enable LTV via the ltv key inside configure(). The library fetches OCSP responses and
CRL payloads from the certificate's AIA and CRL-DP extensions and writes a /DSS (with a
/VRI map keyed by the uppercase SHA-1 of the signature /Contents) in a post-signing
incremental revision:
$pdf->signature()->configure([
'profile' => 'pades-b-lt',
'signcert' => 'file:///path/to/cert.pem',
'privkey' => 'file:///path/to/key.pem',
'password' => '',
'ltv' => [
'enabled' => true,
'embed_ocsp' => true, // fetch OCSP responses
'embed_crl' => true, // fetch CRL payloads (fallback)
'embed_certs' => true, // include certificate DER bytes
'include_dss' => true, // emit /DSS in the catalog
'include_vri' => true, // emit /VRI map keyed by signature SHA-1
],
]);
The extracerts bundle is read as the chain above the signing certificate, leaf first up
to the root, one certificate per entry; a bundle that repeats the signing certificate (a
fullchain.pem) is accepted, the repeat being dropped. The order is verified by
signature, so a chain whose entries do not issue one another is rejected with a
\Com\Tecnick\Pdf\Exception.
To reach PAdES-BASELINE-LTA, call upgradeToLta() (it selects the pades-b-lta profile,
forces the DSS on, and adds a /Type /DocTimeStamp archive timestamp over the whole
document in a further incremental revision; a TSA must be configured):
$pdf->signature()->configure([/* pades-b-lt + ltv */])->timestamp([/* TSA */])->upgradeToLta();
A validator only reports the LT/LTA level when the DSS actually contains the revocation data for the chain, so the signing certificate must expose reachable OCSP/CRL responders. A self-signed certificate embeds only its own bytes, so a validator then reports B-T with a DSS present.
What reaches the DSS
Nothing an OCSP responder or a CRL distribution point returns is embedded until it has been verified:
- an OCSP response must have a successful status, a basic response type, a signature
that verifies against a responder the issuer authorised (itself, or a delegate holding
id-kp-OCSPSigning), aCertIDmatching the request, agoodcertificate status, and a validity interval covering the signing time; - a CRL must be one complete list issued and signed by the certificate whose
distribution point it came from, covering the signing time, not narrowed by a
deltaCRLIndicatoror anissuingDistributionPoint, and issued by a CA whosekeyUsageadmitscRLSign; - the digests and signatures accepted are SHA-256 and above; SHA-1 is refused, and RSASSA-PSS is not supported in either direction.
The signing time (the document modification time, the same instant the /M entry
carries) is the moment every response is checked against.
Revocation collection is best-effort: a source that is unreachable, that returns something unusable, or that reports the certificate revoked is skipped, and the rest of the material is still embedded. The certificates carried by the signature timestamp token are collected into the DSS alongside the signer's own chain, and are run through the same OCSP and CRL lookups, as ETSI EN 319 142-1 requires of a B-LT Document Security Store.
Signing with a key this process cannot reach
signature()->external() reserves the signature field without a key: the document is built
and hashed by the library and the signature is made elsewhere. configure() takes the same
options as on the facade, with signcert and privkey left empty; prepare() returns the
prepared document bytes, the /ByteRange tuple, and the digest of the covered bytes;
apply() writes the returned CMS into the reserved /Contents, which has to hold it
(11742 hexadecimal digits, or 31742 with a timestamp enabled).
$pdf->signature()->external()->configure([/* ... */ 'privkey' => '', 'signcert' => '']);
$prepared = $pdf->signature()->external()->prepare('sha256');
$signedPdf = $pdf->signature()->external()->apply(
$prepared['prepared_pdf'],
$prepared['byte_range'],
$cms,
'binary',
);
Two workflows produce $cms:
- a provider that returns a complete CMS is handed the digest and gives back the whole
detached signature, which
apply()only injects. The profile the provider signs under has to match the oneconfigure()was given, since the PDF/SubFiltercomes from it. This is examples/E075_external_signature_injection.php; - a key service that signs bytes, such as an HSM, a KMS, or a remote signing API, never
sees the document.
Com\Tecnick\Pdf\Sign\Signer::prepare()turns the digest and the signing certificate into aSigningRequest,signaturePayload()renders it as the DER SET OF signed attributes the key signs, andbuildFromSignature()rebuilds the same attributes, checks the returned signature against them, and emits the detached CAdES. The request crosses a queue or a second HTTP request throughtoArray()/fromArray(), which take an optional key for an HMAC over the exported state. This is examples/E084_signature_two_phase_cms.php.
Both halves of a two-phase signature take the same Com\Tecnick\Pdf\Sign\Config: it fixes
the digest algorithm and, through the profile, whether the CMS carries the signing-time
attribute. A B-T or higher profile also requires the timestamp client and transport in
buildFromSignature().
Generating a Self-Signed Test Certificate
The bundled examples/data/cert/tcpdf.crt is a self-signed demo certificate (certificate
and RSA private key in one file). Generate your own with:
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -sha256 \
-keyout tcpdf.key -out tcpdf.crt \
-subj "/CN=tc-lib-pdf test certificate"
# combine into a single file (as the bundled demo does), or reference them separately
cat tcpdf.crt tcpdf.key > tcpdf.pem
# convert to PKCS#12 if needed
openssl pkcs12 -export -in tcpdf.crt -inkey tcpdf.key -out tcpdf.p12
For a real PAdES-BASELINE-LT/LTA validation you need a certificate issued by a CA whose OCSP responder (AIA) and CRL distribution point are reachable at signing time.