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 * Timestamp object.
26 *
27 * GuardTime timestamps can be either PKI-signed or hash-linked.
28 *
29 * PKI-signed timestamps can be verified using GuardTime public key certificate and thus
30 * are directly usable only until the certificate expires.
31 *
32 * Hash-linked (extended) timestamps can be verified independently from GuardTime using control
33 * publications. There are no time limitations for hash-linked timestamps, so those are more
34 * suitable for long-term archival.
35 *
36 * GuardTime timestamps are internally represented in ASN.1 and serialized and stored in DER
37 * encoding. This class is a wrapper to decode, format and otherwise process a timestmap object.
38 *
39 * @package tsp
40 */
41class GTTimestamp implements ASN1DEREncodable {
42
43    const CONTENT_TYPE_ID = "1.2.840.113549.1.9.3"; // id-contentType
44    const CONTENT_TYPE = "1.2.840.113549.1.9.16.1.4"; // id-ct-TSTInfo
45    const MESSAGE_DIGEST_ID = "1.2.840.113549.1.9.4"; // id-messageDigest
46
47    const PK_SIG_ALGO_ID = "1.2.840.113549.1.1.11"; // id-sha256WithRSAEncryption
48
49    /**
50     * Name of the property to retrieve the claimed accuracy of the request time field.
51     *
52     * @see getProperty
53     * @see REQUEST_TIME
54     */
55    const ACCURACY = "issuer.accuracy";
56    /**
57     * Name of the property to retrieve the algorithm used to hash the timestamped document.
58     *
59     * @see getProperty
60     * @see HASHED_MESSAGE
61     */
62    const HASH_ALGORITHM = "hashAlgorithm";
63    /**
64     * Name of the property to retrieve the hash value of the timestamped document.
65     *
66     * @see getProperty
67     * @see HASH_ALGORITHM
68     */
69    const HASHED_MESSAGE = "hashedMessage";
70    /**
71     * Name of the property to retrieve the history identifier of the timestamp.
72     *
73     * This is the number of seconds from 1970-01-01 00:00:00 UTC to the moment
74     * the timestamp was registered in the GuardTime Calendar.
75     *
76     * @see getProperty
77     * @see REGISTERED_TIME
78     */
79    const HISTORY_ID = "history.id";
80    /**
81     * Name of the property to retrieve the issue of the timestamp.
82     *
83     * For GuardTime timestamps, this is the DNS name of the issuing Gateway.
84     *
85     * @see getProperty
86     * @see LOCATION_ID
87     */
88    const ISSUER_NAME = "issuer.name";
89    /**
90     * Name of the property to retrieve the identifier of the issuing Gateway
91     * within the GuardTime aggregation and distribution network.
92     *
93     * At the moment, this is mostly for troubleshooting.
94     *
95     * @see getProperty
96     * @see ISSUER_NAME
97     */
98    const LOCATION_ID = "location.id";
99    /**
100     * Name of the property to retrieve the policy under which the timestamp was issued.
101     *
102     * @see getProperty
103     */
104    const POLICY_ID = "policy.id";
105    /**
106     * Name of the property to retrieve the control publication for the timestamp.
107     *
108     * This is available for extended timestamps only.
109     *
110     * @see getProperty
111     * @see PUBLICATION_ID, PUBLICATION_TIME, PUBLICATION_REFERENCE
112     */
113    const PUBLICATION = "publication.value";
114    /**
115     * Name of the property to retrieve the identifier of the control publication for the timestamp.
116     *
117     * This is the number of seconds from 1970-01-01 00:00:00 UTC to the moment
118     * the control publicatin was generated in the GuardTime Calendar.
119     *
120     * This is available for extended timestamps only.
121     *
122     * @see getProperty
123     * @see PUBLICATION, PUBLICATION_TIME, PUBLICATION_REFERENCE
124     */
125    const PUBLICATION_ID = "publication.id";
126    /**
127     * Name of the property to retrieve the time of the control publication for the timestamp.
128     *
129     * This is the moment the control publicatin was generated in the GuardTime Calendar,
130     * given as UTC time and date.
131     *
132     * This is available for extended timestamps only.
133     *
134     * @see getProperty
135     * @see PUBLICATION, PUBLICATION_ID, PUBLICATION_REFERENCE
136     */
137    const PUBLICATION_TIME = "publication.time";
138    /**
139     * Name of the property to retrieve the bibliographic references for the control publication for the timestamp.
140     *
141     * This is currently empty for all timestamps, and in the future will be available for extended timestamps only.
142     *
143     * @see getProperty
144     * @see PUBLICATION, PUBLICATION_ID, PUBLICATION_TIME
145     */
146    const PUBLICATION_REFERENCE = "publication.references";
147    /**
148     * Name of the property to retrieve the registration time of the timestamp.
149     *
150     * This is the moment the timestamp request was registered in the GuardTime Calendar,
151     * given as UTC time and date.
152     *
153     * @see getProperty
154     * @see HISTORY_ID
155     */
156    const REGISTERED_TIME = "registeredTime";
157    /**
158     * Name of the property to retrieve the time when the GuardTime Gateway received the
159     * timestamp request.
160     *
161     * This is given as UTC time and date according to the Gateway's local clock.
162     *
163     * @see getProperty
164     * @see ACCURACY
165     */
166    const REQUEST_TIME = "issuer.genTime";
167    /**
168     * Name of the property to retrieve the unique serial number of the timestamp.
169     *
170     * @see getProperty
171     */
172    const SERIAL_NUMBER = "issuer.serialNumber";
173
174    private $token;
175    private $verificationResult;
176    private $properties;
177    private $registeredTime;
178    private $dataHash;
179
180    /**
181     * Constructs a new GTTimestamp instance.
182     *
183     * @param  CMSContentInfo $token timestamp token
184     */
185    public function __construct(CMSContentInfo $token) {
186        $this->token = $token;
187        $this->verificationResult = null;
188
189        $this->updateProperties();
190    }
191
192    /**
193     * Parses timestamp contents and checks vital parameters.
194     *
195     * This method performs basic timestamp syntactic checks.
196     *
197     * @throws GTException
198     * @return void
199     */
200    protected function updateProperties() {
201        $this->properties = array();
202
203        $signedData = $this->token->getContent();
204
205        $tstInfo = $signedData->getEncapsulatedContent()->getContent();
206
207        $hashAlgorithm = $tstInfo->getMessageImprint()->getHashAlgorithm()->getAlgorithm();
208        $hashedMessage = $tstInfo->getMessageImprint()->getHashedMessage();
209        $this->dataHash = new GTDataHash(GTHashAlgorithm::getByOid($hashAlgorithm), $hashedMessage);
210
211        // timestamps should only have 1 signerinfo
212        if (count($signedData->getSignerInfos()) != 1) {
213            throw new GTException("Invalid signerInfos size: " . count($signedData->getSignerInfos()));
214        }
215
216        $signerInfo = $signedData->getSignerInfo();
217        $signerInfoSid = $signerInfo->getSid();
218
219        if (empty($signerInfoSid)) {
220            throw new GTException("Missing signerInfo.sid");
221        }
222
223        // verify that sid is of type issuerAndSerialNumber for timestamps, not subjectKeyIdentifier
224        $issuerAndSerialNumber = $signerInfoSid->getIssuerAndSerialNumber();
225
226        if (empty($issuerAndSerialNumber)) {
227            throw new GTException("Missing signerInfo.sid.issuerAndSerialNumber");
228        }
229
230        // check digest alg is SHA-256
231        if ($signerInfo->getDigestAlgorithm()->getAlgorithm() != GTHashAlgorithm::getByName('SHA256')->getOid()) {
232            throw new GTException("Unsupported digestAlgorithm: {$signerInfo->getDigestAlgorithm()->getAlgorithm()}");
233        }
234
235        // check signature alg is GTTimeSignature::OID
236        if ($signerInfo->getSignatureAlgorithm()->getAlgorithm() != GTTimeSignature::OID) {
237            throw new GTException("Unsupported signatureAlgorithm: {$signerInfo->getSignatureAlgorithm()->getAlgorithm()}");
238        }
239
240        $signedAttributes = $signerInfo->getSignedAttrs();
241
242        if (empty($signedAttributes)) {
243            throw new GTException("Signed attributes missing");
244        }
245
246        if (count($signedAttributes) != 2) {
247            throw new GTException("Signed attributes invalid size: " . count($signedAttributes));
248        }
249
250        $contentType = $signedAttributes[0];
251        $messageDigest = $signedAttributes[1];
252
253        if ($contentType->getType() != self::CONTENT_TYPE_ID) {
254            throw new GTException("Expecting content-type signedAttr, but found: " . $contentType->getType());
255        }
256
257        if ($messageDigest->getType() != self::MESSAGE_DIGEST_ID) {
258            throw new GTException("Expecting message-digest signedAttr, but found: " . $messageDigest->getType());
259        }
260
261        $contentTypeValues = $contentType->getValues();
262        $messageDigestValues = $messageDigest->getValues();
263
264        if (count($contentTypeValues) != 1) {
265            throw new GTException("Multiple values not allowed for signedAttr content-type");
266        }
267
268        if (count($messageDigestValues) != 1) {
269            throw new GTException("Multiple values not allowed for signedAttr message-digest");
270        }
271
272        $contentTypeValue = $contentTypeValues[0]->getValue();
273        $messageDigestValue = $messageDigestValues[0];
274
275        if ($contentTypeValue != self::CONTENT_TYPE) {
276            throw new GTException("Invalid signedAttr content-type: " . $contentTypeValue);
277        }
278
279        if (!$messageDigestValue instanceof ASN1OctetString) {
280            throw new GTException("Invalid signedAttr message-digest: " . $messageDigestValue);
281        }
282
283        $this->properties[self::HASH_ALGORITHM] = $this->dataHash->getHashAlgorithm()->getOid();
284        $this->properties[self::HASHED_MESSAGE] = GTBase16::encode($this->dataHash->getHashedMessage());
285
286        $this->properties[self::POLICY_ID] = $tstInfo->getPolicy();
287        $this->properties[self::SERIAL_NUMBER] = $tstInfo->getSerialNumber();
288
289        $this->properties[self::REQUEST_TIME] = GTUtil::formatTime(GTUtil::decodeTime($tstInfo->getGenTime()));
290
291        $accuracy = $tstInfo->getAccuracy();
292
293        if ($accuracy != null) {
294            $this->properties[self::ACCURACY] = $accuracy->getFormatted();
295        }
296
297        $tsa = $tstInfo->getFormattedTsa();
298
299        if ($tsa != null) {
300            $this->properties[self::ISSUER_NAME] = $tsa;
301        }
302
303        $timeSignature = new GTTimeSignature();
304        $timeSignature->decode($signedData->getSignerInfo()->getSignature());
305
306        $publicationId = new GTBigInteger($timeSignature->getPublishedData()->getPublicationIdentifier());
307
308        $historyChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
309        $locationChain = GTHashChain::getLocationInstance($timeSignature->getLocation());
310
311        $historyId = $historyChain->computeHistoryId($publicationId);
312        $locationId = $locationChain->computeLocationId();
313
314        $this->properties[self::HISTORY_ID] = $historyId->getValue();
315        $this->properties[self::LOCATION_ID] = $locationId->getValue();
316
317        $this->registeredTime = $historyId;
318
319        $this->properties[self::REGISTERED_TIME] = GTUtil::formatTime($this->registeredTime->getValue(), 'UTC');
320
321        if ($timeSignature->isExtended()) {
322
323            $publishedData = $timeSignature->getPublishedData();
324
325            $this->properties[self::PUBLICATION_ID] = $publicationId->getValue();
326            $this->properties[self::PUBLICATION_TIME] = GTUtil::formatTime($publicationId->getValue(), 'UTC');
327            $this->properties[self::PUBLICATION] = $publishedData->getEncodedPublication();
328            $this->properties[self::PUBLICATION_REFERENCE] = $timeSignature->getEncodedPublicationReferences();
329
330        } else {
331
332            $pkSignature = $timeSignature->getPkSignature();
333            $pkSignatureAlgorithm = $pkSignature->getSignatureAlgorithm();
334
335            if (empty($pkSignatureAlgorithm)) {
336                throw new GTException("Missing pkSignatureAlgorithm");
337            }
338
339            if ($pkSignatureAlgorithm->getAlgorithm() != self::PK_SIG_ALGO_ID) {
340                throw new GTException("Invalid pkSignatureAlgorithm: {$pkSignatureAlgorithm->getAlgorithm()}");
341            }
342        }
343
344    }
345
346    /**
347     * Extends this timestamp using the given extension response.
348     *
349     * @throws GTException
350     * @param  GTCertTokenResponse $certTokenResponse extension response
351     * @return void
352     *
353     * @see GTHttpClient
354     * @see GTCertTokenResponse
355     */
356    public function extend(GTCertTokenResponse $certTokenResponse) {
357
358        if (empty($certTokenResponse)) {
359            throw new GTException("Parameter certTokenResponse is required");
360        }
361
362        if (!$certTokenResponse instanceof GTCertTokenResponse) {
363            throw new GTException("Expecting an GTCertTokenResponse");
364        }
365
366        $statusCode = $certTokenResponse->getStatusCode();
367
368        if ($statusCode < 0 || $statusCode > 1) {
369            throw new GTException("Extending not possible: " . $certTokenResponse->getStatus()->getStatusMessage());
370        }
371
372        $certToken = $certTokenResponse->getToken();
373
374        $signedData = $this->token->getContent();
375        $signerInfo = $signedData->getSignerInfo();
376
377        $timeSignature = new GTTimeSignature();
378        $timeSignature->decode($signerInfo->getSignature());
379
380        $oldChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
381        $newChain = GTHashChain::getHistoryInstance($certToken->getHistory());
382
383        if (!$oldChain->checkPastEntries($newChain)) {
384            throw new GTException("Extending failed: past history chains do not match in timestamp and response");
385        }
386
387        $timeSignature->setPkSignature(null);
388        $timeSignature->setHistory($certToken->getHistory());
389        $timeSignature->setPublishedData($certToken->getPublishedData());
390        $timeSignature->setPubReference($certToken->getPubReference());
391
392        $signerInfo->setSignature($timeSignature->encodeDER());
393        $signedData->setCertificates(null);
394
395        $this->updateProperties();
396    }
397
398    /**
399     * Checks if this timestamp is extendable.
400     *
401     * @param  GTPublicationsFile $publicationsFile the publications file to check agains
402     * @return bool true if this timestamp is extendable
403     */
404    public function isExtendable(GTPublicationsFile $publicationsFile) {
405
406        if ($publicationsFile == null) {
407            return false;
408        }
409
410        if ($this->isExtended()) {
411            return false;
412        }
413
414        $regTime = $this->getRegisteredTime();
415        $pubTime = $publicationsFile->getLastPublicationTime();
416
417        return $regTime <= $pubTime;
418    }
419
420    /**
421     * Verifies this timestamp using the given data hash and publications file.
422     *
423     * @throws GTException
424     * @param  GTDataHash $dataHash data hash to use for verification
425     * @param  GTPublicationsFile $publicationsFile publications file to use for verification
426     * @return GTVerificationResult timestamp verification result
427     */
428    public function verify(GTDataHash $dataHash, GTPublicationsFile $publicationsFile) {
429
430        if ($publicationsFile == null) {
431            throw new GTException("Parameter publicationsFile is required");
432        }
433
434        if (!$publicationsFile instanceof GTPublicationsFile) {
435            throw new GTException("Parameter publicationsFile must be an instance of GTPublicationsFile");
436        }
437
438        $this->verificationResult = new GTVerificationResult();
439        $this->verificationResult->update($publicationsFile->verify());
440
441        if (!$this->verificationResult->isValid()) {
442            return $this->verificationResult;
443        }
444
445        if ($this->isExtended()) {
446
447            $publication = $this->getProperty(self::PUBLICATION);
448
449            if (!$publicationsFile->contains($publication)) {
450                $this->verificationResult->updateErrors(GTVerificationResult::PUBLICATION_FAILURE);
451            }
452
453            if (!$this->verificationResult->isValid()) {
454                return $this->verificationResult;
455            }
456
457            $this->verificationResult->update(GTVerifier::verifyWithPublication($this->token, $dataHash, $publication));
458
459        } else {
460
461            $certificates = $this->token->getContent()->getCertificates();
462
463            if (count($certificates) < 1) {
464                throw new GTException("Invalid timestamp: no certificates");
465            }
466
467            $certificate = $certificates[0];
468            $certificate = new X509Certificate($certificate);
469
470            $hash = X509Certificate::getPublicKeyHash($certificate->getPublicKey());
471
472            if ($publicationsFile->containsPublicKey($hash)) {
473                $this->verificationResult->updateStatus(GTVerificationResult::PUBLICATION_CHECKED);
474
475            } else {
476
477                $this->verificationResult->updateErrors(GTVerificationResult::PUBLIC_KEY_FAILURE);
478
479                if (!$this->verificationResult->isValid()) {
480                    return $this->verificationResult;
481                }
482
483            }
484
485            $this->verificationResult->update(GTVerifier::verifyWithSignature($this->token, $dataHash, $certificate->getPublicKey()));
486        }
487
488        return $this->verificationResult;
489
490    }
491
492    /**
493     * Checks if this timestamp is extended (hash-linked).
494     *
495     * @return bool true if this timestamp is extended, false otherwise
496     */
497    public function isExtended() {
498
499        $timeSignature = new GTTimeSignature();
500        $timeSignature->decode($this->token->getContent()->getSignerInfo()->getSignature());
501
502        return $timeSignature->isExtended();
503    }
504
505    /**
506     * Gets the timestamp property by the given name.
507     *
508     * @param  string $name property name
509     * @return null|object property value or null if not set
510     */
511    public function getProperty($name) {
512
513        if (!isset($this->properties[$name])) {
514            return null;
515
516        } else {
517            return $this->properties[$name];
518
519        }
520    }
521
522    /**
523     * Gets the hash algorithm used to hash data which this timestamp was created for.
524     *
525     * @return GTHashAlgorithm hash algorithm
526     */
527    public function getHashAlgorithm() {
528        return $this->dataHash->getHashAlgorithm();
529    }
530
531    /**
532     * Gets the time this timestamp was registered at.
533     *
534     * @return GTBigInteger timestamp registration time
535     */
536    public function getRegisteredTime() {
537        return $this->registeredTime;
538    }
539
540    /**
541     * Encodes the contents of this timestamp using DER.
542     *
543     * @return array byte array containing the contents of this timestamp using DER encoding
544     */
545    public function encodeDER() {
546        return $this->token->encodeDER();
547    }
548
549    /**
550     * Saves this timestamp to the specified file.
551     *
552     * @param  string $file file name
553     * @return void
554     */
555    public function save($file) {
556        GTUtil::write($file, $this->encodeDER());
557    }
558
559    /**
560     * Loads a GuardTime timestamp from the given file.
561     *
562     * @static
563     * @param  string $file file name
564     * @return GTTimestamp loaded timestamp
565     */
566    public static function load($file) {
567
568        $bytes = GTUtil::read($file);
569
570        $content = new CMSContentInfo();
571        $content->decode(ASN1DER::decode($bytes));
572
573        return new GTTimestamp($content);
574    }
575
576}
577
578?>
579