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