content = $bytes; // get header if (count($this->content) < self::HEADER_SIZE) { throw new GTException("Invalid publications file length: " + count($this->content)); } // get and check version $version = GTUtil::readShort($this->content, self::VERSION_POS); if ($version != self::VERSION) { throw new GTException("Unsupported publications file version: " . $version); } // get and check publication block params $publicationBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_BLOCK_BEGIN_POS); if ($publicationBlockBegin != self::HEADER_SIZE) { throw new GTException("Invalid publications block offset: " . $publicationBlockBegin); } $publicationCellSize = GTUtil::readShort($this->content, self::PUBLICATION_CELL_SIZE_POS); $publicationCount = GTUtil::readInt($this->content, self::PUBLICATION_COUNT_POS); // get and check public key block params $publicKeyBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_KEY_BLOCK_BEGIN_POS); if ($publicKeyBlockBegin != $publicationBlockBegin + ($publicationCellSize * $publicationCount)) { throw new GTException("Invalid publications block offset: " . $publicKeyBlockBegin); } $publicKeyCellSize = GTUtil::readShort($this->content, self::PUBLICATION_KEY_CELL_SIZE_POS); $publicKeyCount = GTUtil::readShort($this->content, self::PUBLICATION_KEY_COUNT_POS); // get and check publication references' block params $publicationReferenceBlockBegin = GTUtil::readInt($this->content, self::PUBLICATION_REFERENCES_BLOCK_BEGIN_POS); if ($publicationReferenceBlockBegin >= count($this->content)) { throw new GTException("Invalid publication reference block offset: " . $publicationReferenceBlockBegin); } // get and check signature block params $signatureBlockBegin = GTUtil::readInt($this->content, self::SIGNATURE_BLOCK_BEGIN_POS); if ($signatureBlockBegin >= count($this->content)) { throw new GTException("Invalid signature block offset: " . $signatureBlockBegin); } $this->publicationBlockBegin = $publicationBlockBegin; $this->publicationCellSize = $publicationCellSize; $this->publicationCount = $publicationCount; $this->publicKeyBlockBegin = $publicKeyBlockBegin; $this->publicKeyCellSize = $publicKeyCellSize; $this->publicKeyCount = $publicKeyCount; $this->publicationReferenceBlockBegin = $publicationReferenceBlockBegin; $this->signatureBlockBegin = $signatureBlockBegin; $this->verified = false; $this->verificationResult = new GTVerificationResult(); } /** * Gets the encoded publications file. * * @return array byte array containing the encoded publications file */ public function getEncoded() { return $this->content; } /** * Saves this publications file to the specified file. * * @param string $file file name * @return void */ public function save($file) { GTUtil::write($file, $this->getEncoded()); } /** * Loads a publications file. * * @static * @param string $file file name * @return GTPublicationsFile loaded content */ public static function load($file) { $bytes = GTUtil::read($file); return new GTPublicationsFile($bytes); } /** * Checks if this publications file contains the given publication. * * @param string $publicationString Base-32 encoded publication * @return bool true if this publications file contains the publication, false otherwise */ public function contains($publicationString) { $publicationId = self::getPublicationId($publicationString); if ($this->getPublicationById($publicationId) == $publicationString) { return true; } else { return false; } } /** * Gets the earliest publications time. * * @return string first publication time */ public function getFirstPublicationTime() { $result = GTUtil::readLong($this->content, $this->publicationBlockBegin); $result = $result->getValue(); return $result; } /** * Gets the latest publication time. * * @return string last publication time */ public function getLastPublicationTime() { $result = GTUtil::readLong($this->content, $this->publicationBlockBegin + ($this->publicationCellSize * ($this->publicationCount - 1))); $result = $result->getValue(); return $result; } /** * Retrieves publication by given publicationId. * * @param int $publicationId publication id * @return null|string base32 encoded publication if found, null otherwise */ public function getPublicationById($publicationId) { $low = 0; $high = $this->publicationCount - 1; while ($low <= $high) { $index = $low + (int) (($high - $low) / 2); $offset = $this->publicationBlockBegin + ($index * $this->publicationCellSize); $id = GTUtil::readLong($this->content, $offset); $id = $id->getValue(); if ($id > $publicationId) { $high = $index - 1; } else if ($id < $publicationId) { $low = $index + 1; } else { return $this->getEncodedPublication($offset); } } return null; } /** * Gets publication by given publicationTime. * * @param int $publicationTime publication time * @return null|string base32 encoded publication if found, null otherwise */ public function getPublicationByTime($publicationTime) { return $this->getPublicationById($publicationTime); } /** * Returns the number of publications in this publication file. * * @return int number of publications in this publication file */ public function getPublicationCount() { return $this->publicationCount; } /** * Returns an array of all publications contained in this publications file. * * @return array publications in this publications file */ public function getPublicationList() { $result = array(); $offset = $this->publicationBlockBegin; for ($i = 0; $i < $this->publicationCount; $i++) { array_push($result, $this->getEncodedPublication($offset)); $offset += $this->publicationCellSize; } return $result; } /** * Checks if this publications file contains hash of the given public key. * * @throws GTException * @param GTDataHash $publicKey public key hash * @return bool true if this publications file contains the specified public key hash, false otherwise * * @see GTDataHash */ public function containsPublicKey(GTDataHash $publicKey) { if (!$publicKey instanceof GTDataHash) { throw new GTException("publicKey must be an instance of GTDataHash"); } foreach ($this->getPublicKeyList() as $hash) { if ($hash->getHashedMessage() == $publicKey->getHashedMessage() && $hash->getHashAlgorithm()->getOid() == $publicKey->getHashAlgorithm()->getOid()) { return true; } } return false; } /** * Gets the number of public keys contained in this publications file. * * @return int number of public keys contained in this publications file */ public function getPublicKeyCount() { return $this->publicKeyCount; } /** * Returns an array of all public key hashes contained in this publications file. * * @return array public key hashed contained in this publications file. * * @see GTDataHash */ public function getPublicKeyList() { if ($this->publicKeys === null) { $this->publicKeys = array(); $offset = $this->publicKeyBlockBegin; for ($i = 0; $i < $this->publicKeyCount; $i++) { array_push($this->publicKeys, $this->getPublicKeyHash($offset)); $offset += $this->publicKeyCellSize; } } return $this->publicKeys; } /** * Verifies publications file signature. * * * @param null|string|array $cainfo if cainfo is null the bundled root certificate is used, otherwise the * path specified in cainfo is used for root certificated. * * @return GTVerificationResult publications file verification result * * @see openssl_verify */ public function verify($cainfo = null) { if ($cainfo === null) { $cainfo = dirname(__FILE__) . '/GTPublicationsFile.pem'; } if ($this->verified && $this->verificationResult->getCainfo() === $cainfo) { return $this->verificationResult; } $this->verified = false; $this->verificationResult = new GTVerificationResult(); // extract content info $bytes = array_slice($this->content, $this->signatureBlockBegin, count($this->content) - $this->signatureBlockBegin); // extract data $data = array_slice($this->content, 0, $this->signatureBlockBegin); $contentInfo = new CMSContentInfo(); $contentInfo->decode(ASN1DER::decode($bytes)); $signedData = $contentInfo->getContent(); try { $this->verifySignature($signedData, $data); $this->verifyCertificates($signedData->getCertificates(), $cainfo); } catch (GTException $e) { $this->verificationResult->updateErrors(GTVerificationResult::PUBFILE_SIGNATURE_FAILURE); } $this->verified = true; $this->verificationResult->updateStatus(GTVerificationResult::PUBFILE_SIGNATURE_VERIFIED); return $this->verificationResult; } /** * Verifies publications file signature. * * @throws GTException thrown when signature verification fails * @param CMSSignedData $signedData signed data * @param array $data data byes * @return void */ private function verifySignature(CMSSignedData $signedData, array $data) { // check digest algorithm $digestAlgorithms = $signedData->getDigestAlgorithms(); if (count($digestAlgorithms) != 1) { throw new GTException("Publications file signature algorithm check failed"); } $digestAlgorithm = $digestAlgorithms[0]; if ($digestAlgorithm->getAlgorithm() != GTHashAlgorithm::getByName('SHA256')->getOid()) { throw new GTException("Unsupported publications file signature algorithm: " . $digestAlgorithm->getAlgorithm()); } if (is_null($signedData->getSignerInfo())) { throw new GTException("Invalid publications file, no signatures"); } if (count($signedData->getCertificates()) < 1) { throw new GTException("Invalid publications file, no certificates"); } $signature = $signedData->getSignerInfo()->getSignature(); $certificates = $signedData->getCertificates(); $certificate = new X509Certificate($certificates[0]); if (!$certificate->verifySignature($data, $signature)) { throw new GTException("Publications file signature signer info's verification failed"); } } /** * Verifies publications file certificates. * * @throws GTException thrown when certificate verification fails * @param array $certificates array of raw certificate bytes * @param string|array $cainfo path to root certificates to use for verification * @return void * * @see openssl_verify */ private function verifyCertificates(array $certificates, $cainfo) { $signingCertificate = null; $validCertificates = array(); $now = time(); foreach ($certificates as $certificate) { $certificate = new X509Certificate($certificate); $params = $certificate->getParameters(); if ($params["validFrom_time_t"] > $now) { continue; // not yet valid } if ($params["validTo_time_t"] < $now) { continue; // no longer valid } if (isset($params['subject']['emailAddress']) && $params['subject']['emailAddress'] == 'publications@guardtime.com') { $signingCertificate = $certificate; } else { array_push($validCertificates, $certificate); } } if ($signingCertificate === null) { throw new GTException("No valid signing certificate found in publications file"); } if (!$signingCertificate->isValid(X509_PURPOSE_ANY, $validCertificates, $cainfo)) { throw new GTException("Certificate verification failed"); } } /** * Gets publicationId from the given publicationString. * * @static * @throws GTException * @param string $publicationString base32 encoded publication * @return string publication id */ public static function getPublicationId($publicationString) { if ($publicationString == null) { throw new GTException("Invalid publication: null"); } $bytes = GTBase32::decode($publicationString); $result = GTUtil::readLong($bytes, 0); $result = $result->getValue(); return $result; } /** * Extracts encoded publication from the given offset. * * @param int $offset offset to start reading publication from * @return string base32 encoded publication */ private function getEncodedPublication($offset) { $hashAlgorithm = GTHashAlgorithm::getByGtid($this->content[$offset + self::TIME_SIZE]); $bytes = array_slice($this->content, $offset, self::TIME_SIZE + 1 + $hashAlgorithm->getLength()); $bytes = GTUtil::addCrc32($bytes); return GTBase32::encodeWithDashes($bytes); } /** * Extracts public key hash from the given offset. * * @param int $offset offset to start reading public key hash from * @return GTDataHash public key hash */ private function getPublicKeyHash($offset) { $offset += self::TIME_SIZE; $hashAlgorithm = GTHashAlgorithm::getByGtid($this->content[$offset]); $hashLength = $hashAlgorithm->getLength() + 1; $bytes = array_slice($this->content, $offset, $hashLength); return GTDataHash::getInstance($bytes); } } ?>