Le script complet écrit un PDF lisible contenant deux véritables horodatages RFC3161. Entre eux, il ajoute des certificats dans un Document Security Store (DSS) et des Validation Related Information (VRI) propres à la signature. La page visible énumère ces étapes ; les preuves se trouvent dans les objets PDF et les plages d’octets historiques signées.
L’autorité de démonstration fonctionne hors ligne avec une clé publique de test. Ses signatures mathématiques sont réelles, mais son identité et son horloge locale ne constituent pas un service d’horodatage de confiance. L’application choisit explicitement son fournisseur et sa politique concernant les certificats, le temps et la révocation.
Exécuter l’exemple
Depuis une copie du core 0.7.1 avec ses dépendances de développement verrouillées, fournissez explicitement l’exécutable OpenSSL :
php examples/pdf20-timestamp-dss.php timestamps.pdf /usr/bin/openssl
L’exemple fournit Courier avec des métriques explicites : FirstChar/LastChar, largeurs ASCII et FontDescriptor. PDF 2.0 ne permet plus d’omettre ces entrées pour les quatorze polices standard. Cette construction n’incorpore pas de programme de fonte ; un profil exigeant l’incorporation demande une fonte explicitement incorporée.
Sous Windows, placez entre guillemets un chemin contenant des espaces. L’exemple accepte aussi PDFTOOLKIT_OPENSSL à la place du second argument. Il ne télécharge aucun certificat, clé privée, réponse de révocation ou horodatage sur le réseau.
Préparer le premier horodatage de document
PendingDocumentTimestamp est distinct de PendingSignature. Il possède /Type /DocTimeStamp, /SubFilter /ETSI.RFC3161, /ByteRange et la réservation /Contents. Son identité indirecte exacte est reliée à un SignatureField de l’AcroForm.
$pending = new PendingDocumentTimestamp(new SignatureCapacity(8192));
$field = new SignatureField(
new PartialFieldName(new TextString('FirstTimestamp')),
value: new IndirectReference($pending),
);
SignatureWriter prépare la plage couvrant tout le fichier. Rfc3161TimestampBackend calcule le condensat de ces octets et construit une TimestampRequest contenant l’empreinte et un nonce. Le fournisseur reçoit cette requête plutôt que le PDF entier. Avant d’insérer le jeton reçu, le backend vérifie l’empreinte, le nonce, la politique lorsqu’elle est demandée, la signature CMS et la liaison au certificat de l’autorité choisie. Le certificat doit posséder l’usage de clé étendu d’horodatage.
LocalTimestampAuthority reçoit explicitement l’exécutable, la chaîne de certificats, la clé privée, la source de numéros de série et l’OID de politique. Une intégration de production peut implémenter TimestampTokenProvider sans changer le dictionnaire PDF ni le mécanisme de plages d’octets.
Ajouter les données de validation dans une autre révision
Le premier horodatage signe le fichier initial. Son VRI doit donc être ajouté dans une révision incrémentale ultérieure, après une décision de validation réussie :
$information = new ValidationRelatedInformation(
$first,
$policy,
certificates: $references,
);
$store = new DocumentSecurityStore(
certificates: $references,
validation: $information,
);
SignatureValidationEvidence constitue la frontière explicite de politique de l’application. La politique d’exemple vérifie mathématiquement la fixture publique. En production, elle doit aussi établir les faits requis concernant la confiance dans les certificats, leur validité temporelle et leur révocation. Incorporer un certificat n’établit pas ces faits.
ValidationDataStream conserve les octets DER fournis ; ValidationDataReferences associe les preuves indirectes à leur nature. DSS accepte des tableaux de certificats, CRL et OCSP. VRI référence les preuves correspondantes déjà présentes dans ces tableaux DSS. Sa clé est le SHA-1 en majuscules des /Contents réels de la signature antérieure, bourrage réservé compris. Ce n’est pas le condensat du jeton CMS sans bourrage ni celui du document entier.
Le writer examine les véritables octets historiques figés et refuse un VRI sans signature antérieure complète correspondante. Il refuse aussi les natures de preuve incompatibles et les références VRI absentes du stockage documentaire. La modification du catalogue est préparée explicitement avec IndirectObject et l’identifiant existant du catalogue ; les anciens octets restent intacts.
Sceller les preuves ajoutées
Le script ajoute un second champ d’horodatage à l’AcroForm effectif, puis prépare et signe une troisième révision. Cet horodatage couvre le document précédent, le premier horodatage et l’ajout DSS/VRI. Le premier horodatage valide toujours son préfixe d’origine ; il ne couvre pas rétroactivement les preuves ajoutées.
ISO 32000-2 permet d’ignorer les horodatages et le DSS lors de l’évaluation des restrictions DocMDP. L’exception native P1 exige une comparaison stricte de la révision de maintenance : elle ne peut pas masquer des modifications de page, de contenu, de widget ou de signature ordinaire. Les restrictions FieldMDP et les verrous de champs supplémentaires restent actifs. Le validateur indépendant exerce séparément des historiques certifiés P1 ; l’exemple visible se concentre sur les trois étapes de conservation des preuves.
Vérifier indépendamment les octets historiques
tools/validate_pdf20_timestamps.py valide chaque préfixe historique signé avec pyHanko et OpenSSL. Il examine les empreintes RFC3161 réelles, signatures CMS, identifiants de certificat ESS, clés VRI et références de preuves. La matrice comprend les références croisées classiques, en flux et hybrides, la linéarisation, les fichiers clairs/R=6/R=7, PDF MAC et les historiques certifiés.
python tools/validate_pdf20_timestamps.py \
--php php --qpdf qpdf --openssl /usr/bin/openssl \
--work-dir /tmp/pdf20-timestamps
Les dépendances de validation verrouillées appartiennent aux outils de développement. Aucune ne devient une dépendance Composer de production. Contrôler uniquement la syntaxe du fichier ou sa dernière signature ne vérifierait pas tout l’historique ni la politique de confiance de l’application.
Références
Les paragraphes 12.8.4 et 12.8.5 d’ISO 32000-2:2020 décrivent DSS/VRI et les horodatages ; la page physique 606 précise leur traitement DocMDP. La norme principale conservée et le catalogue du bundle identifient la source fournie par la PDF Association et sa collection de corrections. TimestampRequest, TimestampResponse et TimestampToken représentent explicitement les données RFC3161.
Parcours des tutoriels · Signatures modernes · Guide du bundle
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é : pdf20-timestamp-dss.php
<?php
declare(strict_types=1);
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\DocumentSecurityStore;
use PDF\Document\Signatures\PendingDocumentTimestamp;
use PDF\Document\Signatures\PreparedSignature;
use PDF\Document\Signatures\Rfc3161TimestampBackend;
use PDF\Document\Signatures\SignatureCapacity;
use PDF\Document\Signatures\SignatureValidationEvidence;
use PDF\Document\Signatures\SignatureVerificationInput;
use PDF\Document\Signatures\SignedRevision;
use PDF\Document\Signatures\TimestampSerialSequence;
use PDF\Document\Signatures\ValidationDataKind;
use PDF\Document\Signatures\ValidationDataReferences;
use PDF\Document\Signatures\ValidationDataStream;
use PDF\Document\Signatures\ValidationRelatedInformation;
use PDF\FileStructure;
use PDF\Security\CertificateChain;
use PDF\Security\LocalTimestampAuthority;
use PDF\Security\OpenSslTimestampTokenVerifier;
use PDF\Security\X509Certificate;
use PDF\Serialization\SignatureWriter;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators as Op;
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\FileStructure\IncrementalUpdate;
use PDF\Syntax\Object\ArrayType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectObject;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StringObject\HexadecimalString;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// 1. The example application explicitly selects an OFFLINE authority provider.
// Its private key and certificate are PUBLIC TEST DATA, not a trusted service.
// Set PDFTOOLKIT_OPENSSL or pass the executable as the second argument on Windows.
$fixtureBytes = file_get_contents(dirname(__DIR__) . '/tests/Fixtures/iso32000-crypto/timestamp.json');
if ($fixtureBytes === false) {
throw new RuntimeException('The public timestamp fixture is unavailable.');
}
$fixture = json_decode($fixtureBytes, true, flags: JSON_THROW_ON_ERROR);
$certificateBytes = base64_decode($fixture['certificate'], true);
if ($certificateBytes === false) {
throw new RuntimeException('Invalid public timestamp certificate encoding.');
}
$chain = new CertificateChain(new X509Certificate(new HexadecimalString($certificateBytes)));
$openssl = $argv[2] ?? (getenv('PDFTOOLKIT_OPENSSL') ?: throw new InvalidArgumentException('Supply the OpenSSL executable as argument two or PDFTOOLKIT_OPENSSL.'));
$authority = new LocalTimestampAuthority($openssl, $chain, $fixture['private_key'], new TimestampSerialSequence(), '1.2.3.4.1');
$backend = new Rfc3161TimestampBackend($authority, new OpenSslTimestampTokenVerifier($chain));
// 2. A document timestamp has its own /DocTimeStamp dictionary and /ETSI.RFC3161
// SubFilter. The provider receives only a message imprint and nonce. The returned
// token must authenticate that exact request and the selected authority key.
$pending = new PendingDocumentTimestamp(new SignatureCapacity(8192));
$field = new SignatureField(new PartialFieldName(new TextString('FirstTimestamp')), value: new IndirectReference($pending));
$form = new AcroForm((new FieldTree())->addRoot($field));
// PDF 2.0 requires explicit metrics and a descriptor even for standard fonts.
$font = require __DIR__ . '/fixtures/pdf20-courier-font.php';
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))));
$content = new \PDF\Syntax\Object\StreamObject(new ContentStreams(
new Op\BeginTextObject(),
new Op\SetTextFontAndSize(new NameType('F1'), new NumericType(18)),
new Op\MoveTextPosition(new NumericType(48), new NumericType(740)),
new Op\ShowText(new LiteralString('PDF 2.0: timestamps and validation evidence')),
new Op\SetTextFontAndSize(new NameType('F1'), new NumericType(12)),
new Op\MoveTextPosition(new NumericType(0), new NumericType(-48)),
new Op\ShowText(new LiteralString('1. Timestamp the exact document byte range.')),
new Op\MoveTextPosition(new NumericType(0), new NumericType(-28)),
new Op\ShowText(new LiteralString('2. Append a DSS and signature-specific VRI evidence.')),
new Op\MoveTextPosition(new NumericType(0), new NumericType(-28)),
new Op\ShowText(new LiteralString('3. Append a second timestamp over the updated document.')),
new Op\MoveTextPosition(new NumericType(0), new NumericType(-40)),
new Op\ShowText(new LiteralString('PUBLIC TEST AUTHORITY: no production trust is asserted.')),
new Op\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)));
$first = (new SignatureWriter())->prepare(new FileStructure(new FileHeader('2.0'), new FileBody($catalog)), $pending)->sign($backend);
// 3. A VRI entry is permitted only after successful validation under an explicit
// caller policy. This educational policy accepts the public fixture authority
// after mathematical verification; production policies also establish trust,
// certificate validity and revocation. Merely embedding a certificate proves none.
$policy = new class ($backend, $chain) implements SignatureValidationEvidence {
public function __construct(private Rfc3161TimestampBackend $backend, private CertificateChain $chain)
{
}
public function assertSuccessful(SignedRevision $signature): void
{
$this->backend->verify(new SignatureVerificationInput($signature->range->coveredBytes($signature->revision->bytes), $signature->value, $signature->format, $this->chain));
}
};
$references = new ValidationDataReferences(ValidationDataKind::Certificate, new IndirectReference(new ValidationDataStream(ValidationDataKind::Certificate, $certificateBytes)));
$information = new ValidationRelatedInformation($first, $policy, certificates: $references);
$store = new DocumentSecurityStore(certificates: $references, validation: $information);
$catalogRecord = $first->revision->binding($catalog);
$updatedCatalog = ($catalogRecord->dictionary ?? throw new LogicException('Missing catalog.'))->with(new Entry(new NameType('DSS'), new IndirectReference($store)));
$withEvidence = (new IncrementalUpdate($first->revision, new FileBody(new IndirectObject($updatedCatalog, $catalogRecord->identifier))))->toRevision();
// 4. A separate timestamp field is appended in a new revision. The earlier
// document bytes remain identical. This new seal also covers the DSS/VRI update.
$secondPending = new PendingDocumentTimestamp(new SignatureCapacity(8192));
$secondField = new SignatureField(new PartialFieldName(new TextString('SecondTimestamp')), value: new IndirectReference($secondPending));
$formRecord = $withEvidence->binding($form);
$formDictionary = $formRecord->dictionary ?? throw new LogicException('Missing AcroForm.');
$roots = $formDictionary->get('Fields');
if (!$roots instanceof ArrayType) {
throw new LogicException('An AcroForm has its explicit field roots.');
}
$secondForm = $formDictionary->with(new Entry(new NameType('Fields'), new ArrayType(...[...$roots->items, new IndirectReference($secondField)])));
$updated = (new IncrementalUpdate($withEvidence, new FileBody(new IndirectObject($secondForm, $formRecord->identifier))))->toRevision();
$final = (new PreparedSignature($updated, $secondPending))->sign($backend);
$path = $argv[1] ?? __DIR__ . '/pdf20-timestamp-dss.pdf';
$final->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;
