Skip to content

Digital Signatures

Dragon edited this page Jun 3, 2026 · 1 revision

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.

Signing a document

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).

Document::sign() parameters

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.

Timestamps

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.

Signature formats

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'),
);

Multiple signatures

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.

Document timestamps

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');

Long-term validation (LTV)

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');

enableLtv() parameters

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).

Revocation data sources

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.

Current limits

  • 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 PdfException at output time.
  • The per-signature /VRI entry is not emitted (a global /DSS is used instead, which modern validators accept).
  • Archive timestamp renewal (stacking further /DocTimeStamp revisions after certificate expiry) is out of scope.

See also

Clone this wiki locally