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