Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
DssBuilder
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
8 / 8
19
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addCertificate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addOcspResponse
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addCrl
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addVriEntry
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
9
 computeVriKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createStream
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Core\Document;
6
7use Phpdftk\Pdf\Core\File\IncrementalWriter;
8use Phpdftk\Pdf\Core\PdfArray;
9use Phpdftk\Pdf\Core\PdfDictionary;
10use Phpdftk\Pdf\Core\PdfName;
11use Phpdftk\Pdf\Core\PdfObject;
12use Phpdftk\Pdf\Core\PdfReference;
13use Phpdftk\Pdf\Core\PdfStream;
14use Phpdftk\Pdf\Core\PdfString;
15
16/**
17 * Builder for the Document Security Store (DSS) — ISO 32000-2 §12.8.4.3.
18 *
19 * Registers certificate, OCSP response, and CRL streams as indirect objects
20 * via an {@see IncrementalWriter}, builds VRI (Validation Related Information)
21 * entries keyed by signature hash, and produces a populated {@see DSS}.
22 *
23 * Deduplicates identical data by SHA-256 hash.
24 */
25final class DssBuilder
26{
27    /** @var array<string, PdfReference> sha256(cert) => reference */
28    private array $certRefs = [];
29
30    /** @var array<string, PdfReference> sha256(ocsp) => reference */
31    private array $ocspRefs = [];
32
33    /** @var array<string, PdfReference> sha256(crl) => reference */
34    private array $crlRefs = [];
35
36    /** @var array<string, array{certs: list<PdfReference>, ocsps: list<PdfReference>, crls: list<PdfReference>}> */
37    private array $vriEntries = [];
38
39    public function __construct(
40        private readonly IncrementalWriter $writer,
41    ) {}
42
43    /**
44     * Add a DER-encoded certificate to the DSS.
45     *
46     * Creates a PdfStream indirect object and registers it with the writer.
47     * Deduplicates: identical certificates (by SHA-256) return the same reference.
48     */
49    public function addCertificate(string $derCert): PdfReference
50    {
51        $hash = hash('sha256', $derCert);
52        if (isset($this->certRefs[$hash])) {
53            return $this->certRefs[$hash];
54        }
55
56        $stream = $this->createStream($derCert);
57        $ref = $this->writer->addNewObject($stream);
58        $this->certRefs[$hash] = $ref;
59        return $ref;
60    }
61
62    /**
63     * Add a DER-encoded OCSP response to the DSS.
64     */
65    public function addOcspResponse(string $derOcsp): PdfReference
66    {
67        $hash = hash('sha256', $derOcsp);
68        if (isset($this->ocspRefs[$hash])) {
69            return $this->ocspRefs[$hash];
70        }
71
72        $stream = $this->createStream($derOcsp);
73        $ref = $this->writer->addNewObject($stream);
74        $this->ocspRefs[$hash] = $ref;
75        return $ref;
76    }
77
78    /**
79     * Add a DER-encoded CRL to the DSS.
80     */
81    public function addCrl(string $derCrl): PdfReference
82    {
83        $hash = hash('sha256', $derCrl);
84        if (isset($this->crlRefs[$hash])) {
85            return $this->crlRefs[$hash];
86        }
87
88        $stream = $this->createStream($derCrl);
89        $ref = $this->writer->addNewObject($stream);
90        $this->crlRefs[$hash] = $ref;
91        return $ref;
92    }
93
94    /**
95     * Add a VRI (Validation Related Information) entry for a signature.
96     *
97     * @param string $sigContentsHash Uppercase hex SHA-256 of the raw signature
98     *                                 bytes (hex-decoded /Contents value)
99     * @param list<PdfReference> $certRefs References to certificate streams
100     * @param list<PdfReference> $ocspRefs References to OCSP response streams
101     * @param list<PdfReference> $crlRefs  References to CRL streams
102     */
103    public function addVriEntry(
104        string $sigContentsHash,
105        array $certRefs = [],
106        array $ocspRefs = [],
107        array $crlRefs = [],
108    ): void {
109        $this->vriEntries[$sigContentsHash] = [
110            'certs' => $certRefs,
111            'ocsps' => $ocspRefs,
112            'crls' => $crlRefs,
113        ];
114    }
115
116    /**
117     * Build the populated DSS object and register it with the writer.
118     */
119    public function build(): DSS
120    {
121        $dss = new DSS();
122
123        // Global certificate list
124        $allCerts = array_values($this->certRefs);
125        if (!empty($allCerts)) {
126            $dss->certs = new PdfArray($allCerts);
127        }
128
129        // Global OCSP list
130        $allOcsps = array_values($this->ocspRefs);
131        if (!empty($allOcsps)) {
132            $dss->ocsps = new PdfArray($allOcsps);
133        }
134
135        // Global CRL list
136        $allCrls = array_values($this->crlRefs);
137        if (!empty($allCrls)) {
138            $dss->crls = new PdfArray($allCrls);
139        }
140
141        // VRI dictionary
142        if (!empty($this->vriEntries)) {
143            $vriDict = new PdfDictionary();
144            foreach ($this->vriEntries as $hash => $entry) {
145                $vriEntry = new PdfDictionary();
146                if (!empty($entry['certs'])) {
147                    $vriEntry->set('Cert', new PdfArray($entry['certs']));
148                }
149                if (!empty($entry['ocsps'])) {
150                    $vriEntry->set('OCSP', new PdfArray($entry['ocsps']));
151                }
152                if (!empty($entry['crls'])) {
153                    $vriEntry->set('CRL', new PdfArray($entry['crls']));
154                }
155                // /TU — time of validation (optional but recommended)
156                $vriEntry->set('TU', new PdfString(gmdate('D:YmdHis\Z')));
157
158                $vriDict->set($hash, $vriEntry);
159            }
160            $dss->vri = $vriDict;
161        }
162
163        return $dss;
164    }
165
166    /**
167     * Compute the VRI dictionary key for a signature.
168     *
169     * @param string $rawSignatureBytes The raw DER bytes of the signature
170     *                                   (hex-decoded /Contents value)
171     * @return string Uppercase hex SHA-256 hash
172     */
173    public static function computeVriKey(string $rawSignatureBytes): string
174    {
175        return strtoupper(hash('sha256', $rawSignatureBytes));
176    }
177
178    /**
179     * Create a PdfStream containing binary data.
180     */
181    private function createStream(string $data): PdfStream
182    {
183        return new class ($data) extends PdfStream {
184            public function __construct(string $data)
185            {
186                parent::__construct(new PdfDictionary(), $data);
187            }
188        };
189    }
190}