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.

Index · Code complet

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.

Résultat PDF signed-document : page 1 sur 1
Page 1 sur 1
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;

Source de ce guide au commit documenté ↗