The complete script produces a visible page and a detached CMS signature associated with an invisible /Sig field.

php examples/signed-document.php signed-document.pdf

The PHP OpenSSL extension is required. The script's certificate and key are public and for testing only: they make a signature reproducible without proving identity or trust.

X509Certificate, CertificateChain and OpenSslIdentity receive bytes. The identity checks that certificate and key match; the backend searches for no files or URLs. The script chooses fixtures. A real application supplies its own credentials and trust policy.

PendingSignature reserves capacity for /Contents and /ByteRange. SignatureField associates it with /V; AcroForm references the field. A visible appearance is a separate object and is not required for this association.

SignatureWriter::prepare() writes the file and fixes ByteRange before the cryptographic calculation. Ranges cover the entire revision except the Contents token and its delimiters. PreparedSignature::sign() signs those bytes and fills only the reservation. Oversized signatures are rejected; offsets and file length stay fixed.

SignatureBackend permits choosing another component. The three ISO formats have their own requirements. prepareRevision() can sign an incremental revision while preserving the first signature's bytes.

The PHP OpenSSL backend uses SHA-256 for CMS messages and SHA-1 for the offered PKCS#1 format. Its API cannot select every digest listed in the specification. Key parameters and the effective PDF version are checked: this CMS backend requires PDF 1.6; an RSA key larger than 1,024 bits requires at least PDF 1.5. The catalog can raise the header's version.

Required seeds and field locks use the SignatureFieldSigning orchestration presented in lesson 26. This API checks written policies and the actual cryptographic result; the simple signature in this lesson rejects requirements needing that contract. General comparison of revision changes under DocMDP, FieldMDP or UR remains a separate mechanism.

Mathematical verification and trust are distinct. Certificates, revocation, timestamps and revision permissions require their own checks. CI independently verifies ByteRange, CMS, digests and RSA signatures, and rejects a modified message.

Reference: ISO 12.8, physical pages 474–487.

Index · Complete code

View the PDF result

This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.

PDF result signed-document: page 1 of 1
Page 1 of 1
Use the browser PDF viewer

This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.

Complete source code

Code from roukmoute/pdf · View the source at the documented commit ↗

Read the commented script: signed-document.php
<?php

declare(strict_types=1);

use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\Forms\AcroForm;
use PDF\Document\Forms\FieldTree;
use PDF\Document\Forms\PartialFieldName;
use PDF\Document\Forms\SignatureField;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Signatures\PendingSignature;
use PDF\Document\Signatures\SignatureCapacity;
use PDF\Document\Signatures\SignatureFormat;
use PDF\FileStructure;
use PDF\Security\CertificateChain;
use PDF\Security\OpenSslIdentity;
use PDF\Security\OpenSslSignatureBackend;
use PDF\Security\X509Certificate;
use PDF\Serialization\SignatureWriter;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\HexadecimalString;
use PDF\Syntax\Object\StringObject\LiteralString;

require dirname(__DIR__) . '/vendor/autoload.php';

// 1. These keys are PUBLIC and for testing only. They prove no identity
// and must never sign production documents. The script supplies
// bytes: the signature API interprets no paths.
$fixtures = dirname(__DIR__) . '/tests/Fixtures/signing/';
$certificateBytes = file_get_contents($fixtures . 'signer.der');
$keyBytes = file_get_contents($fixtures . 'signer-private-key.pem');
if ($certificateBytes === false || $keyBytes === false) {
    throw new RuntimeException('The public signing fixtures are unavailable.');
}
$certificate = new X509Certificate(new HexadecimalString($certificateBytes));
$chain = new CertificateChain($certificate);
$identity = new OpenSslIdentity($chain, $keyBytes);
$backend = new OpenSslSignatureBackend($identity);

// 2. The dictionary reserves Contents binary capacity. Field Sig gives
// it a document association V; a visible widget is another object.
$pending = new PendingSignature(
    new SignatureCapacity(8192),
    SignatureFormat::Pkcs7Detached,
    entries: new Entry(new NameType('Reason'), new LiteralString('Public educational fixture'))
);
$fields = (new FieldTree())->addRoot(new SignatureField(new PartialFieldName(new TextString('Demonstration')), value: new IndirectReference($pending)));
$form = new AcroForm($fields, entries: new Entry(new NameType('SigFlags'), new IntegerType(3)));
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference(new Type1Font(StandardFont::Helvetica))))));
$content = new StreamObject(new ContentStreams(
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(18)),
    new MoveTextPosition(new NumericType(48), new NumericType(740)),
    new ShowText(new LiteralString('Un document signe objet par objet')),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(11)),
    new MoveTextPosition(new NumericType(0), new NumericType(-48)),
    new ShowText(new LiteralString('ByteRange couvre le fichier, sauf la reservation Contents.')),
    new MoveTextPosition(new NumericType(0), new NumericType(-24)),
    new ShowText(new LiteralString('Certificat et cle publics de test : aucune confiance attribuee.')),
    new EndTextObject(),
));
$page = new PageObject(new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)), $resources, $content);
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page), new Entry(new NameType('AcroForm'), new IndirectReference($form)));
$file = new FileStructure(new FileHeader(), new FileBody($catalog));

// 3. prepare() fixes ByteRange before CMS; sign() fills only Contents
// within its reservation. xref offsets and document length stay fixed.
$prepared = (new SignatureWriter())->prepare($file, $pending);
$signed = $prepared->sign($backend);
$path = $argv[1] ?? __DIR__ . '/signed-document.pdf';
$signed->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;

Guide source at the documented commit ↗