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 * Publications file object used in timestamp verification.
26 *
27 * Publications file contains all publications released, and hashes of all
28 * public keys used to sign timestamps.
29 *
30 * Publications file should not be trusted unless its signature is
31 * verified successfully.
32 *
33 * @package tsp
34 */
35class GTPublicationsFile {
36
37    const VERSION = 1;
38    const VERSION_POS = 0;
39
40    const PUBLICATION_BLOCK_BEGIN_POS = 10;
41    const PUBLICATION_CELL_SIZE_POS = 14;
42    const PUBLICATION_COUNT_POS = 16;
43
44    const PUBLICATION_KEY_BLOCK_BEGIN_POS = 20;
45    const PUBLICATION_KEY_CELL_SIZE_POS = 24;
46    const PUBLICATION_KEY_COUNT_POS = 26;
47
48    const PUBLICATION_REFERENCES_BLOCK_BEGIN_POS = 28;
49
50    const SIGNATURE_BLOCK_BEGIN_POS = 32;
51
52    const HEADER_SIZE = 36;
53    const TIME_SIZE = 8;
54
55    private $content;
56
57    private $verified;
58    private $verificationResult;
59
60    private $publicationBlockBegin;
61    private $publicationCellSize;
62    private $publicationCount;
63
64    private $publicKeyBlockBegin;
65    private $publicKeyCellSize;
66    private $publicKeyCount;
67    private $publicKeys;
68
69    private $publicationReferenceBlockBegin;
70
71    private $signatureBlockBegin;
72
73    /**
74     * Constructs a new GTPublicationsFile from the given bytes.
75     *
76     * @throws GTException
77     * @param  array $bytes byte array containing encoded publications file
78     */
79    public function __construct(array $bytes) {
80
81        if (empty($bytes)) {
82            throw new GTException("Parameter bytes is required");
83        }
84
85        $this->content = $bytes;
86
87        // get header
88        if (count($this->content) < self::HEADER_SIZE) {
89            throw new GTException("Invalid publications file length: " + count($this->content));
90        }
91
92        // get and check version
93        $version = GTUtil::readShort($this->content, self::VERSION_POS);
94
95        if ($version != self::VERSION) {
96            throw new GTException("Unsupported publications file version: " . $version);
97        }
98
99        // get and check publication block params
100        $publicationBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_BLOCK_BEGIN_POS);
101
102        if ($publicationBlockBegin != self::HEADER_SIZE) {
103            throw new GTException("Invalid publications block offset: " . $publicationBlockBegin);
104        }
105
106        $publicationCellSize = GTUtil::readShort($this->content, self::PUBLICATION_CELL_SIZE_POS);
107        $publicationCount = GTUtil::readInt($this->content, self::PUBLICATION_COUNT_POS);
108
109        // get and check public key block params
110        $publicKeyBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_KEY_BLOCK_BEGIN_POS);
111
112        if ($publicKeyBlockBegin != $publicationBlockBegin + ($publicationCellSize * $publicationCount)) {
113            throw new GTException("Invalid publications block offset: " . $publicKeyBlockBegin);
114        }
115
116        $publicKeyCellSize = GTUtil::readShort($this->content, self::PUBLICATION_KEY_CELL_SIZE_POS);
117        $publicKeyCount = GTUtil::readShort($this->content, self::PUBLICATION_KEY_COUNT_POS);
118
119        // get and check publication references' block params
120        $publicationReferenceBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_REFERENCES_BLOCK_BEGIN_POS);
121
122        if ($publicationReferenceBlockBegin >= count($this->content)) {
123            throw new GTException("Invalid publication reference block offset: " . $publicationReferenceBlockBegin);
124        }
125
126        // get and check signature block params
127        $signatureBlockBegin = GTUtil::readInt($this->content, self::SIGNATURE_BLOCK_BEGIN_POS);
128
129        if ($signatureBlockBegin >= count($this->content)) {
130            throw new GTException("Invalid signature block offset: " . $signatureBlockBegin);
131        }
132
133        $this->publicationBlockBegin = $publicationBlockBegin;
134        $this->publicationCellSize = $publicationCellSize;
135        $this->publicationCount = $publicationCount;
136
137        $this->publicKeyBlockBegin = $publicKeyBlockBegin;
138        $this->publicKeyCellSize = $publicKeyCellSize;
139        $this->publicKeyCount = $publicKeyCount;
140
141        $this->publicationReferenceBlockBegin = $publicationReferenceBlockBegin;
142        $this->signatureBlockBegin = $signatureBlockBegin;
143
144        $this->verified = false;
145        $this->verificationResult = new GTVerificationResult();
146
147    }
148
149    /**
150     * Gets the encoded publications file.
151     *
152     * @return array byte array containing the encoded publications file
153     */
154    public function getEncoded() {
155        return $this->content;
156    }
157
158    /**
159     * Saves this publications file to the specified file.
160     *
161     * @param  string $file file name
162     * @return void
163     */
164    public function save($file) {
165        GTUtil::write($file, $this->getEncoded());
166    }
167
168    /**
169     * Loads a publications file.
170     *
171     * @static
172     * @param  string $file file name
173     * @return GTPublicationsFile loaded content
174     */
175    public static function load($file) {
176
177        $bytes = GTUtil::read($file);
178
179        return new GTPublicationsFile($bytes);
180    }
181
182    /**
183     * Checks if this publications file contains the given publication.
184     *
185     * @param  string $publicationString Base-32 encoded publication
186     * @return bool true if this publications file contains the publication, false otherwise
187     */
188    public function contains($publicationString) {
189
190        $publicationId = self::getPublicationId($publicationString);
191
192        if ($this->getPublicationById($publicationId) == $publicationString) {
193            return true;
194
195        } else {
196            return false;
197        }
198
199    }
200
201    /**
202     * Gets the earliest publications time.
203     *
204     * @return string first publication time
205     */
206    public function getFirstPublicationTime() {
207
208        $result = GTUtil::readLong($this->content, $this->publicationBlockBegin);
209        $result = $result->getValue();
210
211        return $result;
212
213    }
214
215    /**
216     * Gets the latest publication time.
217     *
218     * @return string last publication time
219     */
220    public function getLastPublicationTime() {
221
222        $result = GTUtil::readLong($this->content, $this->publicationBlockBegin + ($this->publicationCellSize * ($this->publicationCount - 1)));
223        $result = $result->getValue();
224
225        return $result;
226    }
227
228    /**
229     * Retrieves publication by given publicationId.
230     *
231     * @param  int $publicationId publication id
232     * @return null|string base32 encoded publication if found, null otherwise
233     */
234    public function getPublicationById($publicationId) {
235
236        $low = 0;
237        $high = $this->publicationCount - 1;
238
239        while ($low <= $high) {
240
241            $index = $low + (int) (($high - $low) / 2);
242            $offset = $this->publicationBlockBegin + ($index * $this->publicationCellSize);
243
244            $id = GTUtil::readLong($this->content, $offset);
245            $id = $id->getValue();
246
247            if ($id > $publicationId) {
248
249                $high = $index - 1;
250
251            } else if ($id < $publicationId) {
252
253                $low = $index + 1;
254
255            } else {
256
257                return $this->getEncodedPublication($offset);
258            }
259        }
260
261        return null;
262
263    }
264
265    /**
266     * Gets publication by given publicationTime.
267     *
268     * @param  int $publicationTime publication time
269     * @return null|string base32 encoded publication if found, null otherwise
270     */
271    public function getPublicationByTime($publicationTime) {
272        return $this->getPublicationById($publicationTime);
273    }
274
275    /**
276     * Returns the number of publications in this publication file.
277     *
278     * @return int number of publications in this publication file
279     */
280    public function getPublicationCount() {
281        return $this->publicationCount;
282    }
283
284    /**
285     * Returns an array of all publications contained in this publications file.
286     *
287     * @return array publications in this publications file
288     */
289    public function getPublicationList() {
290
291        $result = array();
292        $offset = $this->publicationBlockBegin;
293
294        for ($i = 0; $i < $this->publicationCount; $i++) {
295            array_push($result, $this->getEncodedPublication($offset));
296            $offset += $this->publicationCellSize;
297        }
298
299        return $result;
300    }
301
302    /**
303     * Checks if this publications file contains hash of the given public key.
304     *
305     * @throws GTException
306     * @param  GTDataHash $publicKey public key hash
307     * @return bool true if this publications file contains the specified public key hash, false otherwise
308     *
309     * @see GTDataHash
310     */
311    public function containsPublicKey(GTDataHash $publicKey) {
312
313        if (!$publicKey instanceof GTDataHash) {
314            throw new GTException("publicKey must be an instance of GTDataHash");
315        }
316
317        foreach ($this->getPublicKeyList() as $hash) {
318
319            if ($hash->getHashedMessage() == $publicKey->getHashedMessage() &&
320                $hash->getHashAlgorithm()->getOid() == $publicKey->getHashAlgorithm()->getOid()) {
321                return true;
322            }
323
324        }
325
326        return false;
327    }
328
329    /**
330     * Gets the number of public keys contained in this publications file.
331     *
332     * @return int number of public keys contained in this publications file
333     */
334    public function getPublicKeyCount() {
335        return $this->publicKeyCount;
336    }
337
338    /**
339     * Returns an array of all public key hashes contained in this publications file.
340     *
341     * @return array public key hashed contained in this publications file.
342     *
343     * @see GTDataHash
344     */
345    public function getPublicKeyList() {
346
347        if ($this->publicKeys === null) {
348            $this->publicKeys = array();
349
350            $offset = $this->publicKeyBlockBegin;
351
352            for ($i = 0; $i < $this->publicKeyCount; $i++) {
353                array_push($this->publicKeys, $this->getPublicKeyHash($offset));
354                $offset += $this->publicKeyCellSize;
355            }
356        }
357
358        return $this->publicKeys;
359
360    }
361
362    /**
363     * Verifies publications file signature.
364     *
365     *
366     * @param null|string|array $cainfo if cainfo is null the bundled root certificate is used, otherwise the
367     * path specified in cainfo is used for root certificated.
368     *
369     * @return GTVerificationResult publications file verification result
370     *
371     * @see openssl_verify
372     */
373    public function verify($cainfo = null) {
374
375        if ($cainfo === null) {
376            $cainfo = dirname(__FILE__) . '/GTPublicationsFile.pem';
377        }
378
379        if ($this->verified && $this->verificationResult->getCainfo() === $cainfo) {
380            return $this->verificationResult;
381        }
382
383        $this->verified = false;
384        $this->verificationResult = new GTVerificationResult();
385
386        // extract content info
387        $bytes = array_slice($this->content, $this->signatureBlockBegin,
388                count($this->content) - $this->signatureBlockBegin);
389
390        // extract data
391        $data = array_slice($this->content, 0, $this->signatureBlockBegin);
392
393        $contentInfo = new CMSContentInfo();
394        $contentInfo->decode(ASN1DER::decode($bytes));
395
396        $signedData = $contentInfo->getContent();
397
398        try {
399            $this->verifySignature($signedData, $data);
400            $this->verifyCertificates($signedData->getCertificates(), $cainfo);
401
402        } catch (GTException $e) {
403            $this->verificationResult->updateErrors(GTVerificationResult::PUBFILE_SIGNATURE_FAILURE);
404        }
405
406        $this->verified = true;
407        $this->verificationResult->updateStatus(GTVerificationResult::PUBFILE_SIGNATURE_VERIFIED);
408
409        return $this->verificationResult;
410
411    }
412
413    /**
414     * Verifies publications file signature.
415     *
416     * @throws GTException thrown when signature verification fails
417     * @param  CMSSignedData $signedData signed data
418     * @param  array $data data byes
419     * @return void
420     */
421    private function verifySignature(CMSSignedData $signedData, array $data) {
422
423        // check digest algorithm
424        $digestAlgorithms = $signedData->getDigestAlgorithms();
425
426        if (count($digestAlgorithms) != 1) {
427            throw new GTException("Publications file signature algorithm check failed");
428        }
429
430        $digestAlgorithm = $digestAlgorithms[0];
431
432        if ($digestAlgorithm->getAlgorithm() != GTHashAlgorithm::getByName('SHA256')->getOid()) {
433            throw new GTException("Unsupported publications file signature algorithm: " . $digestAlgorithm->getAlgorithm());
434        }
435
436        if (is_null($signedData->getSignerInfo())) {
437            throw new GTException("Invalid publications file, no signatures");
438        }
439
440        if (count($signedData->getCertificates()) < 1) {
441            throw new GTException("Invalid publications file, no certificates");
442        }
443
444        $signature = $signedData->getSignerInfo()->getSignature();
445        $certificates = $signedData->getCertificates();
446
447        $certificate = new X509Certificate($certificates[0]);
448
449        if (!$certificate->verifySignature($data, $signature)) {
450            throw new GTException("Publications file signature signer info's verification failed");
451        }
452    }
453
454    /**
455     * Verifies publications file certificates.
456     *
457     * @throws GTException thrown when certificate verification fails
458     * @param  array $certificates array of raw certificate bytes
459     * @param  string|array $cainfo path to root certificates to use for verification
460     * @return void
461     *
462     * @see openssl_verify
463     */
464    private function verifyCertificates(array $certificates, $cainfo) {
465
466        $signingCertificate = null;
467        $validCertificates = array();
468
469        $now = time();
470
471        foreach ($certificates as $certificate) {
472
473            $certificate = new X509Certificate($certificate);
474
475            $params = $certificate->getParameters();
476
477            if ($params["validFrom_time_t"] > $now) {
478                continue; // not yet valid
479            }
480
481            if ($params["validTo_time_t"] < $now) {
482                continue; // no longer valid
483            }
484
485            if (isset($params['subject']['emailAddress']) && $params['subject']['emailAddress'] == 'publications@guardtime.com') {
486                $signingCertificate = $certificate;
487            } else {
488
489                array_push($validCertificates, $certificate);
490            }
491
492        }
493
494        if ($signingCertificate === null) {
495            throw new GTException("No valid signing certificate found in publications file");
496        }
497
498        if (!$signingCertificate->isValid(X509_PURPOSE_ANY, $validCertificates, $cainfo)) {
499            throw new GTException("Certificate verification failed");
500        }
501    }
502
503    /**
504     * Gets publicationId from the given publicationString.
505     *
506     * @static
507     * @throws GTException
508     * @param  string $publicationString base32 encoded publication
509     * @return string publication id
510     */
511    public static function getPublicationId($publicationString) {
512
513        if ($publicationString == null) {
514            throw new GTException("Invalid publication: null");
515        }
516
517        $bytes = GTBase32::decode($publicationString);
518
519        $result = GTUtil::readLong($bytes, 0);
520        $result = $result->getValue();
521
522        return $result;
523    }
524
525    /**
526     * Extracts encoded publication from the given offset.
527     *
528     * @param  int $offset offset to start reading publication from
529     * @return string base32 encoded publication
530     */
531    private function getEncodedPublication($offset) {
532
533        $hashAlgorithm = GTHashAlgorithm::getByGtid($this->content[$offset + self::TIME_SIZE]);
534
535        $bytes = array_slice($this->content, $offset, self::TIME_SIZE + 1 + $hashAlgorithm->getLength());
536        $bytes = GTUtil::addCrc32($bytes);
537
538        return GTBase32::encodeWithDashes($bytes);
539
540    }
541
542    /**
543     * Extracts public key hash from the given offset.
544     *
545     * @param  int $offset offset to start reading public key hash from
546     * @return GTDataHash public key hash
547     */
548    private function getPublicKeyHash($offset) {
549
550        $offset += self::TIME_SIZE;
551
552        $hashAlgorithm = GTHashAlgorithm::getByGtid($this->content[$offset]);
553        $hashLength = $hashAlgorithm->getLength() + 1;
554
555        $bytes = array_slice($this->content, $offset, $hashLength);
556
557        return GTDataHash::getInstance($bytes);
558    }
559
560}
561
562?>
563