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;