Le script complet produit une page,
un champ texte Reference et un champ signature Approval. La signature CMS
détachée est réellement calculée et vérifiée. Le widget du champ affiche une
apparence explicite, tandis que son verrou FieldMDP sélectionne Reference.
php examples/signature-field-policy.php signature-field-policy.pdf
Ce tutoriel utilise l'API introduite en 0.5.0, PHP 8.3 ou supérieur et l'extension OpenSSL. Le certificat et la clé sont les fixtures publiques de test du dépôt. Ils reproduisent une signature mathématique ; ils n'identifient aucune personne de confiance.
Décrire des obligations, puis choisir comment les satisfaire
SignatureSeedValue représente /SV, les conditions du champ. SignatureSeedFlags
indique lesquelles sont obligatoires. Ici, les quatre bits sélectionnés imposent :
/Filter /Adobe.PPKLite, l'identité du handler choisi ;- le premier
/SubFilterpris en charge, ici/adbe.pkcs7.detached; /DigestMethod [/SHA256]pour la clé RSA réellement utilisée ;- une
/Reasonappartenant à/Reasons, iciÉtudié et approuvé.
$seed = new SignatureSeedValue(
new SignatureSeedFlags(
SignatureSeedFlag::Filter,
SignatureSeedFlag::SubFilter,
SignatureSeedFlag::DigestMethod,
SignatureSeedFlag::Reasons,
),
new Entry(new NameType('Filter'), new NameType('Adobe.PPKLite')),
new Entry(new NameType('SubFilter'), new ArrayType(new NameType('adbe.pkcs7.detached'))),
new Entry(new NameType('DigestMethod'), new ArrayType(new NameType('SHA256'))),
new Entry(new NameType('Reasons'), new ArrayType($reason)),
);
SignatureSigningIntent fournit les choix de l'application : raison, digest
facultatif, certification, verrou documentaire, apparence et demandes de preuves.
Une raison demandée doit être celle qui figure dans le dictionnaire signé. Les
conditions sont aussi relues dans le fichier préparé : modifier un objet de
construction après son écriture ne change pas les obligations déjà émises.
Un tableau SubFilter conserve son ordre : le premier format pris en charge est
choisi, même si la préférence est facultative. Si aucun ne convient et que le bit
est facultatif, un autre format explicitement pris en charge peut être choisi.
Une liste de raisons facultative autorise une autre raison. Avec le bit obligatoire,
l'absence de Reasons ou la valeur spéciale [.] impose l'omission de Reason.
Construire le champ et son verrou
SignatureFieldSigning compose la réservation /Contents, le champ /Sig,
sa valeur /V et les références de transformation nécessaires. Il garde des
objets distincts pour les ressources, apparences, widgets et conditions.
$lock = new SignatureFieldLock(
SignatureLockAction::Include,
new ArrayType(new TextString('Reference')),
);
$signing = new SignatureFieldSigning(
$backend,
new PartialFieldName(new TextString('Approval')),
new SignatureCapacity(8192),
new SignatureSigningIntent(reason: $reason),
$seed,
$lock,
);
Le verrou sélectionne un autre champ du vrai formulaire. L'opération copie ses
entrées Action et Fields dans les paramètres directs de FieldMDP.
All, Include et Exclude conservent leur sens ; sélectionner un nom de groupe
sélectionne également ses descendants.
Le script fournit une FormAppearanceStream et une WidgetAppearance, puis
attache le widget à Approval avec addWidget(). La page référence le widget
dans Annots, et bindToPage() définit son véritable P. Le texte visible ne
remplace pas le CMS contenu dans /V/Contents.
SignatureAppearanceSelection donne un nom applicatif à une apparence fournie.
Le contrat vérifie qu'elle est bien l'apparence normale sélectionnée de chaque
widget du champ, y compris un champ/widget fusionné et les dictionnaires d'états.
Il ne dessine pas une apparence et ne reproduit pas le dialogue d'un lecteur PDF.
L'apparence du champ texte Reference suit aussi la procédure de texte variable
du tutoriel AcroForm : /Tx BMC ... EMC encadre le texte
et le clipping, tandis que le cadre reste à l'extérieur. Elle est distincte de
l'apparence statique de signature. Le marquage ne rend pas le champ modifiable
après signature lorsque le lecteur applique son verrou FieldMDP.
Lier le catalogue, figer les octets et signer
$signing->bindCatalog(new IndirectReference($catalog));
$prepared = (new SignatureWriter())->prepare($file, $signing->signature);
$signed = $signing->sign($prepared);
$signed->writeTo($path);
bindCatalog() ferme explicitement la référence FieldMDP.Data. Elle désigne ce
catalogue et son formulaire ; une référence vers un autre document est refusée.
prepare() écrit le PDF et fixe /ByteRange avant le calcul cryptographique.
Les octets couverts incluent le formulaire, le seed, le verrou, les ressources
et les apparences ; seule la réservation /Contents est exclue.
Le contrat parcourt AcroForm/Fields/Kids dans les snapshots réellement émis.
Il résout les champs hérités, leurs noms complets et les relations Parent,
distingue les widgets séparés et refuse les cycles ou possessions incohérentes.
Un dictionnaire quelconque nommé V, placé hors de cet arbre, ne devient pas
un champ de signature.
Le backend annonce les digests qu'il sait émettre. PHP OpenSSL utilise ici SHA-256 pour CMS ; son API ne sélectionne pas arbitrairement SHA-384 ou SHA-512. Le vérificateur lit le digest du résultat CMS, vérifie mathématiquement le message et identifie le certificat réellement sélectionné. Il lie ses faits aux octets exacts de l'entrée et de la sortie. Un certificat substitué, un CMS modifié ou des faits réutilisés depuis un autre document sont refusés avant finalisation.
Seule cette vérification permet de remplir la réservation. Si le CMS dépasse sa capacité, il faut préparer une nouvelle réservation et signer les nouveaux octets. Les offsets et la longueur du fichier final restent fixes.
Historique et composants externes
Une certification DocMDP doit être le premier champ signé, et le document
en contient au maximum une. Son lien Perms/DocMDP est facultatif : la signature
du champ permet la détection ; ce lien ajoute la prévention des modifications.
certificationPermissions() fournit cette contribution explicite au catalogue.
Chaque signature ultérieure utilise une nouvelle révision incrémentale. Le core
conserve les snapshots précédents : retirer un ancien champ ou libérer l'objet de
sa signature n'efface pas son ordre historique. Une référence de valeur retirée
se résout en null dans la révision courante.
Pour l'opération de signature, un ancien DocMDP ou verrou Adobe P1 interdit
un nouveau V. Un ancien FieldMDP interdit la signature d'un champ sélectionné,
y compris ses descendants. Les permissions Adobe restent le minimum historique :
un nouveau P3 n'augmente pas un ancien P2.
Ces contrôles de l'opération ne réalisent pas encore la comparaison générale de toutes les modifications d'une révision : valeurs de tous les champs, retraits ou déplacements, contenu de pages, ressources partagées, apparences, annotations et instanciation de modèles de pages. L'analyse complète DocMDP/FieldMDP/UR reste un mécanisme à poursuivre. Une signature UR mathématiquement valide ne prouve pas non plus que son signataire est une autorité habilitée à accorder des droits.
PolicySignatureBackend, SignatureOutputVerifier et SignatureSigningEvidence
permettent une intégration externe explicite. finalizeWithPolicy() applique le
même contrat à une signature calculée ailleurs. Un horodatage RFC3161 ou des
preuves OCSP/CRL exigent un composant qui les produit et les vérifie dans le vrai
CMS. Leur capacité annoncée ne remplace pas cette preuve ; le backend natif
refuse ces demandes. Les politiques de confiance PKI restent à fournir par
l'application, et aucun accès réseau n'est exécuté implicitement par le core.
Preuves et références
Le corpus indépendant de politiques inspecte huit PDF et vérifie neuf signatures
mathématiques RSA/DSA avec qpdf, pypdf, ASN.1 et cryptographie indépendante, sur
PHP 8.3, 8.4 et 8.5. Il couvre les conditions réellement écrites, une certification
avec et sans Perms, deux révisions signées, FieldMDP.Data, un verrou Adobe et
une apparence attachée. Le rendu et le formulaire de ce script sont aussi inspectés.
Sources conservées : conditions et certificats, pages455–459,
transformations et signatures, pages474–487,
verrous Adobe et apparences, pages43–45.
La clause DSA/SHA1 de DigestMethod est appliquée lorsque cette entrée du seed
existe ; un seed vide ou Reasons seul ne crée pas une restriction supplémentaire
à la capacité générale DSA/SHA256 de la table257. Cette interprétation est située
dans les deux descriptions, sans être présentée comme un erratum officiel.
Parcours des tutoriels · 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.
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é : signature-field-policy.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\DefaultAppearance;
use PDF\Document\Forms\FieldTree;
use PDF\Document\Forms\FormAppearanceStream;
use PDF\Document\Forms\PartialFieldName;
use PDF\Document\Forms\SignatureFieldLock;
use PDF\Document\Forms\SignatureLockAction;
use PDF\Document\Forms\TextField;
use PDF\Document\Forms\WidgetAnnotation;
use PDF\Document\Forms\WidgetAppearance;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Signatures\SignatureAppearanceSelection;
use PDF\Document\Signatures\SignatureCapacity;
use PDF\Document\Signatures\SignatureFieldSigning;
use PDF\Document\Signatures\SignatureSigningIntent;
use PDF\Document\Signatures\Policies\SignatureSeedFlag;
use PDF\Document\Signatures\Policies\SignatureSeedFlags;
use PDF\Document\Signatures\Policies\SignatureSeedValue;
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\AppendRectangle;
use PDF\Syntax\ContentStreamsResources\Operators\BeginMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\ClipPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\EndMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\EndPath;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\FillPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\RestoreGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SaveGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SetLineWidth;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingGray;
use PDF\Syntax\ContentStreamsResources\Operators\SetStrokingGray;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\ContentStreamsResources\Operators\StrokePath;
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\ArrayType;
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. The application supplies bytes. This certificate and key are PUBLIC
// and for testing only; they prove no production identity.
$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.');
}
$identity = new OpenSslIdentity(new CertificateChain(new X509Certificate(new HexadecimalString($certificateBytes))), $keyBytes);
$backend = new OpenSslSignatureBackend($identity);
// 2. Resources, coordinates and appearances remain explicit. Stream text
// uses ASCII bytes here; Reason/SV metadata use actual TextString values
// and retain their accented characters.
$resources = new ResourceDictionary(new Font(new DictionaryType(
new Entry(new NameType('F1'), new IndirectReference(new Type1Font(StandardFont::Helvetica))),
new Entry(new NameType('Title'), new IndirectReference(new Type1Font(StandardFont::HelveticaBold))),
)));
$rectangle = static fn (int $x, int $y, int $width, int $height): Rectangle => new Rectangle(new NumericType($x), new NumericType($y), new NumericType($x + $width), new NumericType($y + $height));
$text = static fn (string $value, int $x, int $y, int $size, string $font = 'F1'): ContentStreams => new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType($font), new NumericType($size)),
new MoveTextPosition(new NumericType($x), new NumericType($y)),
new ShowText(new LiteralString($value)),
new EndTextObject(),
);
$referenceAppearance = new FormAppearanceStream(new ContentStreams(...[
new SetStrokingGray(new NumericType(0.7)),
new SetLineWidth(new NumericType(1)),
new AppendRectangle(new NumericType(0.5), new NumericType(0.5), new NumericType(498), new NumericType(31)),
new StrokePath(),
// ISO 12.7.3.3, p.444: readers replace only variable text while
// preserving the frame. This appearance stays distinct from the signature.
new BeginMarkedContent(new NameType('Tx')),
new SaveGraphicsState(),
new AppendRectangle(new NumericType(1), new NumericType(1), new NumericType(497), new NumericType(30)),
new ClipPathNonzero(),
new EndPath(),
new SetNonstrokingGray(new NumericType(0)),
...$text('DOSSIER-2026-010', 12, 10, 12)->operators,
new RestoreGraphicsState(),
new EndMarkedContent(),
]), $rectangle(0, 0, 499, 32), $resources);
$signatureAppearance = new FormAppearanceStream(new ContentStreams(
new SetNonstrokingGray(new NumericType(0.95)),
new AppendRectangle(new NumericType(0), new NumericType(0), new NumericType(499), new NumericType(80)),
new FillPathNonzero(),
new SetNonstrokingGray(new NumericType(0)),
...$text('Signature mathematiquement verifiee', 16, 54, 15, 'Title')->operators,
...$text('CMS detache / SHA-256 / certificat public de test', 16, 31, 11)->operators,
...$text('Apparence fournie explicitement par ce script', 16, 13, 9)->operators,
), $rectangle(0, 0, 499, 80), $resources);
// 3. The seed defines obligations. Ff bits require Filter, the first
// supported SubFilter, DigestMethod and the reason actually written.
// Reference: ISO Table234, PDF32000_2008.pdf, pages455-457.
$reason = new TextString('Étudié et approuvé');
$seed = new SignatureSeedValue(
new SignatureSeedFlags(SignatureSeedFlag::Filter, SignatureSeedFlag::SubFilter, SignatureSeedFlag::DigestMethod, SignatureSeedFlag::Reasons),
new Entry(new NameType('Filter'), new NameType('Adobe.PPKLite')),
new Entry(new NameType('SubFilter'), new ArrayType(new NameType('adbe.pkcs7.detached'))),
new Entry(new NameType('DigestMethod'), new ArrayType(new NameType('SHA256'))),
new Entry(new NameType('Reasons'), new ArrayType($reason)),
);
$referenceName = new PartialFieldName(new TextString('Reference'));
$reference = new TextField($referenceName, value: new TextString('DOSSIER-2026-010'));
$lock = new SignatureFieldLock(SignatureLockAction::Include, new ArrayType($referenceName->value));
$signing = new SignatureFieldSigning(
$backend,
new PartialFieldName(new TextString('Approval')),
new SignatureCapacity(8192),
new SignatureSigningIntent(reason: $reason, appearance: new SignatureAppearanceSelection(new TextString('Approbation pédagogique'), new IndirectReference($signatureAppearance))),
$seed,
$lock,
);
// 4. Field Approval's /V designates the reservation. Its /Lock selects
// Reference. SignatureFieldSigning copies Action/Fields into FieldMDP
// without inventing coordinates or changing the target field for the application.
$defaultAppearance = new DefaultAppearance(new SetTextFontAndSize(new NameType('F1'), new NumericType(12)), new SetNonstrokingGray(new NumericType(0)));
$fields = (new FieldTree($defaultAppearance, $resources))->addRoot($reference)->addRoot($signing->field);
$referenceWidget = new WidgetAnnotation($rectangle(48, 610, 499, 32), new WidgetAppearance($referenceAppearance), entries: new Entry(new NameType('F'), new IntegerType(4)));
$signatureWidget = new WidgetAnnotation($rectangle(48, 353, 499, 80), new WidgetAppearance($signatureAppearance), entries: new Entry(new NameType('F'), new IntegerType(4)));
$reference->addWidget($referenceWidget);
$signing->field->addWidget($signatureWidget);
$content = new ContentStreams(...[
...$text('Signer un champ sous conditions', 48, 760, 22, 'Title')->operators,
...$text('Des objets explicites, un contrat applique aux octets ecrits.', 48, 721, 11)->operators,
...$text('1 Le formulaire conserve la reference du dossier', 48, 672, 14, 'Title')->operators,
...$text('Ce champ est selectionne par le verrou FieldMDP de Approval.', 48, 589, 10)->operators,
...$text('2 Le seed impose le format, la raison et le digest', 48, 553, 14, 'Title')->operators,
...$text('Filter : Adobe.PPKLite | SubFilter : adbe.pkcs7.detached', 48, 526, 11)->operators,
...$text('DigestMethod : SHA256 | Reason : Etudie et approuve', 48, 504, 11)->operators,
...$text('3 Le resultat verifie remplit la reservation Contents', 48, 461, 14, 'Title')->operators,
...$text('Le CMS signe la revision entiere, sauf la valeur Contents.', 48, 310, 11)->operators,
...$text('Le champ, son widget et le verrou appartiennent au meme catalogue.', 48, 289, 10)->operators,
new SetNonstrokingGray(new NumericType(0.97)), new AppendRectangle(new NumericType(48), new NumericType(128), new NumericType(499), new NumericType(128)), new FillPathNonzero(),
new SetNonstrokingGray(new NumericType(0)),
...$text('Ce que cette signature confirme', 60, 234, 12, 'Title')->operators,
...$text('Les octets couverts correspondent au certificat et au CMS generes.', 60, 210, 10)->operators,
...$text('Ce certificat et sa cle sont publics et reserves a la demonstration.', 60, 193, 10)->operators,
...$text('Aucune identite PKI, aucun horodatage TSA ni preuve OCSP/CRL.', 60, 176, 10)->operators,
...$text('Le verrou est une declaration PDF ; la comparaison generale des', 60, 159, 10)->operators,
...$text('revisions DocMDP/FieldMDP/UR reste un mecanisme distinct.', 60, 142, 10)->operators,
new SetNonstrokingGray(new NumericType(0.5)),
...$text('PDFToolkit / Tutoriel 26 / PHP 8.3+', 48, 60, 9)->operators,
]);
$page = new PageObject($rectangle(0, 0, 595, 842), $resources, new StreamObject($content), new Entry(new NameType('Annots'), new ArrayType(new IndirectReference($referenceWidget), new IndirectReference($signatureWidget))));
$referenceWidget->bindToPage($page);
$signatureWidget->bindToPage($page);
$catalog = new DocumentCatalog((new PageTreeNode())->addKid($page), new Entry(new NameType('AcroForm'), new IndirectReference(new AcroForm($fields, entries: new Entry(new NameType('SigFlags'), new IntegerType(3))))));
// 5. Data explicitly closes the circular connection to this catalog. prepare()
// writes the file and fixes ByteRange. The contract rereads actually emitted
// snapshots, then verifies the result's mathematical signature, digest and
// certificate before filling Contents. It grants no PKI trust.
$signing->bindCatalog(new IndirectReference($catalog));
$prepared = (new SignatureWriter())->prepare(new FileStructure(new FileHeader('1.7'), new FileBody($catalog)), $signing->signature);
$signed = $signing->sign($prepared);
$path = $argv[1] ?? __DIR__ . '/signature-field-policy.pdf';
$signed->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;
