1<?php
2/*
3 * Copyright 2008-2010 GuardTime AS
4 *
5 * This file is part of the GuardTime PHP SDK.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *     http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19
20/**
21 * @package tsp
22 */
23
24/**
25 * Class containing static helper methods for timestamp verification.
26 *
27 * @package tsp
28 */
29class GTVerifier {
30
31    /**
32     * Verifies a short term timestamp using the given public key.
33     *
34     * @static
35     * @throws GTException
36     * @param  CMSContentInfo $content timestamp token
37     * @param  GTDataHash $dataHash datahash to verify the timestamp against
38     * @param  resource $publicKey openssl public key to use for verification
39     * @return GTVerificationResult timestamp verification result
40     */
41    public static function verifyWithSignature(CMSContentInfo $content, GTDataHash $dataHash, $publicKey) {
42
43        $result = new GTVerificationResult();
44        $result->update(GTVerifier::verifyCommon($content, $dataHash));
45
46        if (!$result->isValid()) {
47            return $result;
48        }
49
50        $signedData = $content->getContent();
51        $signerInfo = $signedData->getSignerInfo();
52
53        $timeSignature = new GTTimeSignature();
54        $timeSignature->decode($signerInfo->getSignature());
55
56        $certificates = $signedData->getCertificates();
57
58        if (count($certificates) < 1) {
59            throw new GTException("Invalid timestamp: no certificates");
60        }
61
62        $certificate = $certificates[0];
63
64        $publicationId = new GTBigInteger(
65            $timeSignature->getPublishedData()->getPublicationIdentifier()
66        );
67
68        $historyChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
69        $historyId = $historyChain->computeHistoryId($publicationId);
70        $historyTime = $historyId->getValue();
71
72        $result->update(GTVerifier::verifyCertificate($certificate, $historyTime, $publicKey));
73
74        if (!$result->isValid()) {
75            return $result;
76        }
77
78        $result->update(GTVerifier::verifyPkSignature($timeSignature, $publicKey));
79
80        return $result;
81    }
82
83    /**
84     * Verifies an extended timestamp against the given publication.
85     *
86     * @static
87     * @throws GTException
88     * @param  CMSContentInfo $content timestamp token
89     * @param  GTDataHash $dataHash datahash to verify this timestamp against
90     * @param  string $publication base32 encoded publication to use for timestamp verification
91     * @return GTVerificationResult timestamp verification result
92     */
93    public static function verifyWithPublication(CMSContentInfo $content, GTDataHash $dataHash, $publication) {
94
95        $result = new GTVerificationResult();
96        $result->update(GTVerifier::verifyCommon($content, $dataHash));
97
98        if (!$result->isValid()) {
99            return $result;
100        }
101
102        $signedData = $content->getContent();
103        $signerInfo = $signedData->getSignerInfo();
104
105        $timeSignature = new GTTimeSignature();
106        $timeSignature->decode($signerInfo->getSignature());
107
108        if (!$timeSignature->isExtended()) {
109            throw new GTException("Unable to verify timeSignature that is not extended with publication");
110        }
111
112        $result->update(GTVerifier::verifyPublication($timeSignature, $publication));
113
114        return $result;
115    }
116
117    /**
118     * Common verification, used by both publication and signature verification.
119     *
120     * @static
121     * @throws GTException
122     * @param  CMSContentInfo $content timestamp token
123     * @param  GTDataHash $dataHash data hash to verify this timestamp against
124     * @return GTVerificationResult timestamp verification result
125     */
126    private static function verifyCommon(CMSContentInfo $content, GTDataHash $dataHash) {
127
128        $result = new GTVerificationResult();
129
130        $signedData = $content->getContent();
131        $signerInfo = $signedData->getSignerInfo();
132
133        $timeSignature = new GTTimeSignature();
134        $timeSignature->decode($signerInfo->getSignature());
135
136        if ($timeSignature->getPubReference() != null) {
137            $result->updateStatus(GTVerificationResult::PUBLICATION_REFERENCE_PRESENT);
138        }
139
140        if ($timeSignature->getPkSignature() != null) {
141            $result->updateStatus(GTVerificationResult::PUBLIC_KEY_SIGNATURE_PRESENT);
142        }
143
144        $messageImprint = $signedData->getEncapsulatedContent()->getContent()->getMessageImprint();
145
146        $result->update(GTVerifier::verifyDataHash($messageImprint, $dataHash));
147
148        if (!$result->isValid()) {
149            return $result;
150        }
151
152        $digestAlgorithm = GTHashAlgorithm::getByOid($signerInfo->getDigestAlgorithm()->getAlgorithm());
153
154        $signedAttrs = $signerInfo->getSignedAttrs();
155
156        if ($signedAttrs == null) {
157            throw new GTException("Invalid signed attributes: null");
158        }
159
160        // GTTimestamp already checks that the messageDigest attribute is
161        // the second signed attribute and has a single OCTECT STRING value
162        $messageDigest = $signedAttrs[1]->getValues();
163        $messageDigest = $messageDigest[0]->getValue();
164
165        // verify that signedAttrs.messageDigest == signerInfo.digestAlgorithm(TSTInfo.encodeDER())
166
167        $dataHash = new GTDataHash($digestAlgorithm);
168        $dataHash->update($signedData->getEncapsulatedContent()->getContentRaw());
169        $dataHash->close();
170
171        if ($messageDigest !== $dataHash->getHashedMessage()) {
172            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
173        }
174
175        if (!$result->isValid()) {
176            return $result;
177        }
178
179        // now feed the hash of signed attributes to the hash chains
180
181        $result->update(GTVerifier::verifyHashChains($timeSignature, $digestAlgorithm, $signedAttrs));
182
183        return $result;
184    }
185
186    /**
187     * Data hash verification against the message imprint.
188     *
189     * @static
190     * @throws GTException
191     * @param  TSPMessageImprint $messageImprint message imprint
192     * @param  GTDataHash $dataHash data hash
193     * @return GTVerificationResult timestamp verification result
194     */
195    private static function verifyDataHash(TSPMessageImprint $messageImprint, GTDataHash $dataHash) {
196
197        $result = new GTVerificationResult();
198
199        if ($messageImprint == null) {
200            throw new GTException("Parameter messageImprint is required");
201        }
202
203        if ($dataHash == null) {
204            return $result;
205        }
206
207        if ($dataHash->getHashAlgorithm()->getOid() != $messageImprint->getHashAlgorithm()->getAlgorithm()) {
208            $result->updateErrors(GTVerificationResult::WRONG_DOCUMENT_FAILURE);
209
210        } else if ($dataHash->getHashedMessage() != $messageImprint->getHashedMessage()) {
211            $result->updateErrors(GTVerificationResult::WRONG_DOCUMENT_FAILURE);
212
213        }
214
215        $result->updateStatus(GTVerificationResult::DATA_HASH_CHECKED);
216
217        return $result;
218    }
219
220
221    /**
222     * Hash chains verification method.
223     *
224     * @static
225     * @param  GTTimeSignature $timeSignature
226     * @param  GTHashAlgorithm $digestAlgorithm
227     * @param  array $signedAttrs
228     * @return GTVerificationResult timestamp verification result
229     */
230    private static function verifyHashChains(GTTimeSignature $timeSignature, GTHashAlgorithm $digestAlgorithm, array $signedAttrs) {
231
232        $result = new GTVerificationResult();
233
234        $locationChainBytes = $timeSignature->getLocation();
235        $historyChainBytes = $timeSignature->getHistory();
236
237        $locationChain = null;
238        $historyChain = null;
239
240        try {
241
242            $locationChain = GTHashChain::getLocationInstance($locationChainBytes);
243            $historyChain = GTHashChain::getHistoryInstance($historyChainBytes);
244
245        } catch (GTException $e) {
246            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
247            return $result;
248        }
249
250        $publicationImprint = $timeSignature->getPublishedData()->getPublicationImprint();
251        $publicationImprintAlg = GTHashAlgorithm::getByGtid($publicationImprint[0]);
252
253        if ($publicationImprintAlg->getLength() + 1 != count($publicationImprint)) {
254            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
255            return $result;
256        }
257
258        $set = new ASN1Set();
259
260        foreach ($signedAttrs as $attr) {
261            $set->add($attr);
262        }
263
264        $input = new GTDataHash($digestAlgorithm);
265        $input->update($set->encodeDER());
266        $input->close();
267
268        $input = $input->getDataImprint();
269
270        $locationOutput = $locationChain->computeOutput($input);
271        $historyOutput = $historyChain->computeOutput($locationOutput);
272
273        $output = new GTDataHash($publicationImprintAlg);
274        $output->update($historyOutput);
275        $output->close();
276
277        $output = $output->getDataImprint();
278
279        if ($output != $publicationImprint) {
280            $result->updateErrors(GTVerificationResult::HASHCHAIN_VERIFICATION_FAILURE);
281        }
282
283        return $result;
284    }
285
286    /**
287     * Publication verification.
288     *
289     * @static
290     * @throws GTException
291     * @param  GTTimeSignature $timeSignature
292     * @param  string $publication
293     * @return GTVerificationResult timestamp verification result
294     */
295    private static function verifyPublication(GTTimeSignature $timeSignature, $publication) {
296
297        if ($publication == null) {
298            throw new GTException("parameter publication is required");
299        }
300
301        $result = new GTVerificationResult();
302
303        if ($publication != $timeSignature->getPublishedData()->getEncodedPublication()) {
304            $result->updateErrors(GTVerificationResult::PUBLICATION_FAILURE);
305        }
306
307        $result->updateStatus(GTVerificationResult::PUBLICATION_CHECKED);
308
309        return $result;
310    }
311
312    /**
313     * Certificate verification.
314     *
315     * @static
316     * @throws GTException
317     * @param  ASN1Sequence|array $certificate
318     * @param  string $historyTime
319     * @param  resource $publicKey
320     * @return GTVerificationResult timestamp verification result
321     */
322    private static function verifyCertificate($certificate, $historyTime, $publicKey) {
323
324        $result = new GTVerificationResult();
325
326        if (empty($certificate)) {
327            throw new GTException("Parameter certificate is required");
328        }
329
330        if (empty($historyTime)) {
331            throw new GTException("Parameter historyTime is required");
332        }
333
334        if (empty($publicKey)) {
335            throw new GTException("Parameter publicKey is required");
336        }
337
338        $cert = new X509Certificate($certificate);
339
340        // verify cert was valid when timestamp was created
341
342        $params = $cert->getParameters();
343
344        if ($params["validFrom_time_t"] > $historyTime) {
345            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
346        }
347
348        if ($params["validTo_time_t"] < $historyTime) {
349            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
350        }
351
352        // verify public key belongs to cert
353
354        $hash1 = X509Certificate::getPublicKeyHash($publicKey);
355        $hash2 = X509Certificate::getPublicKeyHash($cert->getPublicKey());
356
357        if ($hash1 != $hash2) {
358            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
359        }
360
361        return $result;
362    }
363
364    /**
365     * Public key signature verification.
366     *
367     * @static
368     * @throws GTException
369     * @param  GTTimeSignature $timeSignature
370     * @param  resource $publicKey
371     * @return GTVerificationResult timestamp verification result
372     */
373    private static function verifyPkSignature(GTTimeSignature $timeSignature, $publicKey) {
374
375        $result = new GTVerificationResult();
376
377        $signatureAlgorithm = $timeSignature->getPkSignature()->getSignatureAlgorithm()->getAlgorithm();
378
379        switch ($signatureAlgorithm) {
380
381            case '1.2.840.113549.1.1.11':
382                $signatureAlgorithm = 'sha256';
383                break;
384
385            default:
386                throw new GTException("Unsupported signature algorithm: {$signatureAlgorithm}");
387        }
388
389        $sign = $timeSignature->getPkSignature()->getSignatureValue();
390        $data = $timeSignature->getPublishedData()->encodeDER();
391
392        if (!X509Certificate::verifyPublicKeySignature($publicKey, $data, $sign, $signatureAlgorithm)) {
393            $result->updateErrors(GTVerificationResult::PUBLIC_KEY_SIGNATURE_FAILURE);
394        }
395
396        return $result;
397    }
398
399}
400
401?>
402