Le script complet produit une page visible
et une signature CMS détachée associée à un champ /Sig invisible.
php examples/signed-document.php signed-document.pdf
L'extension PHP OpenSSL est nécessaire. Le certificat et la clé du script sont publics et réservés aux tests : ils permettent de reproduire une signature, sans prouver une identité ni une confiance.
X509Certificate, CertificateChain et OpenSslIdentity reçoivent des octets.
L'identité vérifie la correspondance certificat/clé ; le backend ne cherche aucun
fichier ni URL. Le script choisit les fixtures. Une application réelle fournit
ses propres credentials et sa politique de confiance.
PendingSignature réserve la capacité de /Contents et de /ByteRange.
SignatureField l'associe à /V ; AcroForm référence le champ. Une apparence
visible est un objet séparé et n'est pas nécessaire à cette association.
SignatureWriter::prepare() écrit le fichier puis fixe ByteRange avant le calcul
cryptographique. Les plages couvrent la révision entière, sauf le token Contents
avec ses délimiteurs. PreparedSignature::sign() signe ces octets et remplit
uniquement la réservation. Une signature trop grande est refusée ; les offsets
et la longueur du fichier restent fixes.
SignatureBackend permet de choisir un autre composant. Les trois formats ISO
ont leurs obligations propres. prepareRevision() peut signer une révision
incrémentale en conservant les octets de la première signature.
Le backend PHP OpenSSL utilise SHA-256 pour les messages CMS et SHA-1 pour le format PKCS#1 proposé. Son API ne permet pas de choisir tous les condensats énumérés par la norme. Les paramètres de la clé et la version PDF effective sont contrôlés : ce backend CMS demande PDF 1.6 ; une clé RSA de plus de 1 024 bits demande au moins PDF 1.5. Le catalogue peut relever la version du header.
Les seeds obligatoires et les verrous de champs utilisent l'orchestration
SignatureFieldSigning présentée dans la leçon 26.
Cette API vérifie les politiques écrites et le résultat cryptographique réel ;
la signature simple de cette leçon refuse les exigences qui demandent ce contrat.
La comparaison générale des modifications d'une révision selon DocMDP, FieldMDP
ou UR reste un mécanisme distinct.
La vérification mathématique et la confiance sont distinctes. Certificats, révocation, horodatage et permissions des révisions demandent leurs contrôles. La CI vérifie ByteRange, CMS, condensats et signatures RSA indépendamment, et rejette un message modifié.
Référence : ISO 12.8, pages physiques 474–487.
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : 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;
