-
Notifications
You must be signed in to change notification settings - Fork 0
Digital Signatures
Apply cryptographic signatures to a PDF document using the openssl PHP extension and a PKCS#12 credential. The library serializes the file with /ByteRange and /Contents placeholders, computes a detached CMS signature over the document bytes, patches it in, and produces a file that validates in Adobe Acrobat / Reader.
First place a signature field on the page (or use an invisible one), then call Document::sign().
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Form\SignatureField;
use DragonOfMercy\PhpPdf\Signature\SigningCertificate;
use DragonOfMercy\PhpPdf\Signature\Tsa;
$doc = new Document();
$page = $doc->addPage();
// Visible signature field
$page->field(SignatureField::visible(20, 20, 80, 20, name: 'approval'));
$doc->sign(
SigningCertificate::fromPkcs12('cert.p12', 'password'),
field: 'approval',
reason: 'I approve this document',
location: 'Geneva',
contactInfo: 'signer@example.com',
timestamp: Tsa::http('https://freetsa.org/tsr'),
);
$doc->save('signed.pdf');SigningCertificate::fromPkcs12(string $path, string $password) reads the .p12 / .pfx bundle from disk. Use SigningCertificate::fromPkcs12Bytes(string $bytes, string $password) when you already have the raw bytes (e.g. loaded from a database).
| Parameter | Type | Default | Description |
|---|---|---|---|
$certificate |
SigningCertificate |
- | Signing credential loaded from PKCS#12. |
$field |
string |
- | Name of an existing SignatureField on the document. |
$reason |
string|null |
null |
Reason for signing (appears in the PDF signature panel). |
$location |
string|null |
null |
Location of signing. |
$contactInfo |
string|null |
null |
Contact information for the signer. |
$signedAt |
DateTimeImmutable|null |
now | Signing date and time. |
$maxSignatureBytes |
int |
16384 | Size of the /Contents placeholder. Raise if the signature does not fit (e.g. with a large TSA token). |
$timestamp |
Tsa|null |
null |
RFC 3161 timestamp authority. |
$format |
SignatureFormat |
Pkcs7Detached |
CMS subfilter (adbe.pkcs7.detached or ETSI.CAdES.detached). |
The default signature container is adbe.pkcs7.detached, a detached PKCS#7 / CMS envelope computed with SHA-256.
Tsa::http() configures the built-in HTTP transport for RFC 3161 timestamps. The token is embedded as an id-aa-timeStampToken unsigned attribute in the CMS envelope and adds roughly 1.5-4 KB, so raise maxSignatureBytes if needed.
use DragonOfMercy\PhpPdf\Signature\Tsa;
use DragonOfMercy\PhpPdf\Signature\TsaBasicAuth;
use DragonOfMercy\PhpPdf\Signature\TsaHashAlgorithm;
// No authentication, default SHA-256 imprint
$tsa = Tsa::http('https://freetsa.org/tsr');
// With HTTP Basic Auth and SHA-512 imprint
$tsa = Tsa::http(
'https://tsa.example.com/ts',
new TsaBasicAuth('user', 'password'),
TsaHashAlgorithm::SHA512,
);
// Custom TsaClient (for tests or non-HTTP transports)
$tsa = Tsa::withClient($myClient, TsaHashAlgorithm::SHA256);TsaHashAlgorithm cases: SHA256 (default), SHA384, SHA512.
The SignatureFormat enum selects the CMS subfilter. Pass it as the format: named argument to sign() or addSignature().
| Case | Subfilter | Description |
|---|---|---|
SignatureFormat::Pkcs7Detached |
adbe.pkcs7.detached |
Default. Standard PKCS#7 detached signature, SHA-256, RSA. Compatible with Acrobat and most validators. |
SignatureFormat::EtsiCadesDetached |
ETSI.CAdES.detached |
Strict PAdES. CMS built by hand with signed attributes contentType, messageDigest, and signingCertificateV2 (ESS, RFC 5035) binding the certificate. RSA keys only. |
EtsiCadesDetached produces PAdES-B-B on its own. Add a Tsa to get PAdES-B-T. Call enableLtv() on top for PAdES-B-LT / B-LTA.
use DragonOfMercy\PhpPdf\Signature\SignatureFormat;
$doc->sign(
SigningCertificate::fromPkcs12('cert.p12', 'password'),
field: 'approval',
format: SignatureFormat::EtsiCadesDetached,
timestamp: Tsa::http('https://freetsa.org/tsr'),
);addSignature() appends an incremental revision to the document so that each subsequent signature cryptographically covers all prior bytes. Chain sign() (for the initial signature field) with as many addSignature() calls as needed. Auto-named invisible fields (Signature1, Signature2, ...) are created automatically.
use DragonOfMercy\PhpPdf\Signature\SigningCertificate;
use DragonOfMercy\PhpPdf\Signature\Tsa;
$cred1 = SigningCertificate::fromPkcs12('signer1.p12', 'pass1');
$cred2 = SigningCertificate::fromPkcs12('signer2.p12', 'pass2');
$page->field(SignatureField::invisible(name: 'approval'));
$doc->sign($cred1, field: 'approval', reason: 'Author approval')
->addSignature($cred2, reason: 'Co-approver')
->save('multi-signed.pdf');addSignature() accepts the same optional parameters as sign() (reason, location, contactInfo, signedAt, maxSignatureBytes, timestamp, format) but no field parameter - the field is created automatically.
addDocumentTimestamp() appends a /DocTimeStamp incremental revision (ETSI.RFC3161 subfilter) without any signer certificate, covering all prior document bytes. Use it after sign() / addSignature() to prove the signed content existed at a given point in time.
$doc->sign($cred, field: 'approval')
->addDocumentTimestamp(Tsa::http('https://freetsa.org/tsr'))
->save('timestamped.pdf');enableLtv() embeds the signer certificate chain and revocation data (CRLs or OCSP responses) in a Document Security Store (/DSS) appended as an incremental revision, optionally covered by a document timestamp. This makes a signature verifiable after the signer certificate expires.
Call enableLtv() after sign() and any addSignature() calls.
use DragonOfMercy\PhpPdf\Signature\Ltv\HttpCrlValidationDataSource;
use DragonOfMercy\PhpPdf\Signature\Ltv\HttpOcspValidationDataSource;
use DragonOfMercy\PhpPdf\Signature\Tsa;
// PAdES-B-LT: DSS with CRL revocation + covering document timestamp
$doc->sign($cred, field: 'approval')
->enableLtv(
new HttpCrlValidationDataSource(), // fetch CRLs from distribution points
Tsa::http('https://freetsa.org/tsr'), // covering document timestamp
)
->save('ltv.pdf');
// PAdES-B-LTA: also embeds TSA certificate revocation so the timestamp itself is LTV
$tsaChainPem = file_get_contents('tsa-chain.pem');
$doc->sign($cred, field: 'approval')
->enableLtv(
new HttpCrlValidationDataSource(),
Tsa::http('https://freetsa.org/tsr'),
[[$tsaChainPem]], // third argument: list of TSA certificate chains
)
->save('lta.pdf');| Parameter | Type | Default | Description |
|---|---|---|---|
$source |
ValidationDataSource|null |
HttpCrlValidationDataSource |
Revocation data source (see below). |
$timestamp |
Tsa|null |
null |
TSA to use for the covering /DocTimeStamp. Without this, only a /DSS is appended (B-LT without the timestamp). |
$timestampCertificateChains |
list<list<string>> |
[] |
PEM chains for the TSA certificate. When provided, the TSA certificate's revocation is also embedded, making the document timestamp itself LTV (PAdES-B-LTA). |
| Class | Description |
|---|---|
HttpCrlValidationDataSource |
Default. Fetches CRL files from the CDP extension of each certificate. |
HttpOcspValidationDataSource |
Fetches OCSP responses from the AIA responder of each certificate. |
StaticValidationDataSource |
Supply pre-fetched CRLs or OCSP responses directly (useful in tests or air-gapped environments). |
All three are in the DragonOfMercy\PhpPdf\Signature\Ltv namespace.
- RSA keys only. ECDSA signing keys are not yet supported.
- Certifying (author / MDP) signatures are not yet supported.
- Signing and encryption cannot be combined on the same document. Configuring both throws a
PdfExceptionat output time. - The per-signature
/VRIentry is not emitted (a global/DSSis used instead, which modern validators accept). - Archive timestamp renewal (stacking further
/DocTimeStamprevisions after certificate expiry) is out of scope.
MIT licensed. Source on GitHub - if phppdf helps you, you can buy me a coffee.