Table of contents
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 signature itself is embedded in the signed revision; LTV material and archive timestamps are written in further incremental revisions. The cryptography lives in the companion package tc-lib-pdf-sign, while the host library keeps the ByteRange placement, the incremental update writer, and the public facade.
Signature Profiles
The profile option selects the signature format. Each level builds on the previous one:
| 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, and 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.
Both settings are also defined as backed enums in tc-lib-pdf-sign, \Com\Tecnick\Pdf\Sign\SignatureProfile and \Com\Tecnick\Pdf\Sign\DigestAlgorithm, so the accepted values can be referenced symbolically instead of being retyped as literals:
use Com\Tecnick\Pdf\Sign\DigestAlgorithm;
use Com\Tecnick\Pdf\Sign\SignatureProfile;
$pdf->signature()->configure([
'profile' => SignatureProfile::PadesBT->value, // 'pades-b-t'
'digest_algorithm' => DigestAlgorithm::Sha256->value, // 'sha256'
// ...
]);
The configure() array carries string values; the low-level \Com\Tecnick\Pdf\Sign\Config constructor accepts the enum cases directly.
Runnable Examples
- Basic detached signature (PAdES-BASELINE-B): /examples/E007_signature_basic/
- Signature with timestamp (PAdES-BASELINE-T): /examples/E008_signature_timestamp/
- Signature with LTV material (PAdES-BASELINE-LT): /examples/E009_signature_ltv/
- Archive timestamp (PAdES-BASELINE-LTA): /examples/E081_signature_pades_lta/
- External/remote signing (HSM) with ByteRange digest export: /examples/E075_external_signature_injection/
- Two-phase signing, where a key service signs the CMS signed attributes: /examples/E084_signature_two_phase_cms/
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 Material 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
],
]);
When LTV is enabled, the output PDF contains /DSS, /VRI, /OCSPs, /CRLs, and /Certs objects so long-term validation can be performed without external retrieval during verification.
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. See /examples/E075_external_signature_injection/. - 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 DERSET OFsigned 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. See /examples/E084_signature_two_phase_cms/.
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 Test Certificate
The bundled examples/data/cert/tcpdf.crt is a self-signed demo certificate holding both the certificate and the 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 certificate and key into a single file, 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.
Related Guides
- Signature primitives and low-level API: /projects/tc-lib-pdf-sign/
- Remote resource policy for network retrieval: /docs/remote-resources/
- Standards and archival or accessibility modes: /docs/standards/
- PDF import limitations around signed source files: /docs/pdf-import/
Previous: /docs/remote-resources/
Overview: /docs/
Next: /docs/standards/