<?php
/*
 * Copyright 2008-2010 GuardTime AS
 *
 * This file is part of the GuardTime PHP SDK.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @package tsp
 */

/**
 * Timestamp object.
 *
 * GuardTime timestamps can be either PKI-signed or hash-linked.
 *
 * PKI-signed timestamps can be verified using GuardTime public key certificate and thus
 * are directly usable only until the certificate expires.
 *
 * Hash-linked (extended) timestamps can be verified independently from GuardTime using control
 * publications. There are no time limitations for hash-linked timestamps, so those are more
 * suitable for long-term archival.
 *
 * GuardTime timestamps are internally represented in ASN.1 and serialized and stored in DER
 * encoding. This class is a wrapper to decode, format and otherwise process a timestmap object.
 *
 * @package tsp
 */
class GTTimestamp implements ASN1DEREncodable {

    const CONTENT_TYPE_ID = "1.2.840.113549.1.9.3"; // id-contentType
    const CONTENT_TYPE = "1.2.840.113549.1.9.16.1.4"; // id-ct-TSTInfo
    const MESSAGE_DIGEST_ID = "1.2.840.113549.1.9.4"; // id-messageDigest

    const PK_SIG_ALGO_ID = "1.2.840.113549.1.1.11"; // id-sha256WithRSAEncryption

    /**
     * Name of the property to retrieve the claimed accuracy of the request time field.
     *
     * @see getProperty
     * @see REQUEST_TIME
     */
    const ACCURACY = "issuer.accuracy";
    /**
     * Name of the property to retrieve the algorithm used to hash the timestamped document.
     *
     * @see getProperty
     * @see HASHED_MESSAGE
     */
    const HASH_ALGORITHM = "hashAlgorithm";
    /**
     * Name of the property to retrieve the hash value of the timestamped document.
     *
     * @see getProperty
     * @see HASH_ALGORITHM
     */
    const HASHED_MESSAGE = "hashedMessage";
    /**
     * Name of the property to retrieve the history identifier of the timestamp.
     *
     * This is the number of seconds from 1970-01-01 00:00:00 UTC to the moment
     * the timestamp was registered in the GuardTime Calendar.
     *
     * @see getProperty
     * @see REGISTERED_TIME
     */
    const HISTORY_ID = "history.id";
    /**
     * Name of the property to retrieve the issue of the timestamp.
     *
     * For GuardTime timestamps, this is the DNS name of the issuing Gateway.
     *
     * @see getProperty
     * @see LOCATION_ID
     */
    const ISSUER_NAME = "issuer.name";
    /**
     * Name of the property to retrieve the identifier of the issuing Gateway
     * within the GuardTime aggregation and distribution network.
     *
     * At the moment, this is mostly for troubleshooting.
     *
     * @see getProperty
     * @see ISSUER_NAME
     */
    const LOCATION_ID = "location.id";
    /**
     * Name of the property to retrieve the policy under which the timestamp was issued.
     *
     * @see getProperty
     */
    const POLICY_ID = "policy.id";
    /**
     * Name of the property to retrieve the control publication for the timestamp.
     *
     * This is available for extended timestamps only.
     *
     * @see getProperty
     * @see PUBLICATION_ID, PUBLICATION_TIME, PUBLICATION_REFERENCE
     */
    const PUBLICATION = "publication.value";
    /**
     * Name of the property to retrieve the identifier of the control publication for the timestamp.
     *
     * This is the number of seconds from 1970-01-01 00:00:00 UTC to the moment
     * the control publicatin was generated in the GuardTime Calendar.
     *
     * This is available for extended timestamps only.
     *
     * @see getProperty
     * @see PUBLICATION, PUBLICATION_TIME, PUBLICATION_REFERENCE
     */
    const PUBLICATION_ID = "publication.id";
    /**
     * Name of the property to retrieve the time of the control publication for the timestamp.
     *
     * This is the moment the control publicatin was generated in the GuardTime Calendar,
     * given as UTC time and date.
     *
     * This is available for extended timestamps only.
     *
     * @see getProperty
     * @see PUBLICATION, PUBLICATION_ID, PUBLICATION_REFERENCE
     */
    const PUBLICATION_TIME = "publication.time";
    /**
     * Name of the property to retrieve the bibliographic references for the control publication for the timestamp.
     *
     * This is currently empty for all timestamps, and in the future will be available for extended timestamps only.
     *
     * @see getProperty
     * @see PUBLICATION, PUBLICATION_ID, PUBLICATION_TIME
     */
    const PUBLICATION_REFERENCE = "publication.references";
    /**
     * Name of the property to retrieve the registration time of the timestamp.
     *
     * This is the moment the timestamp request was registered in the GuardTime Calendar,
     * given as UTC time and date.
     *
     * @see getProperty
     * @see HISTORY_ID
     */
    const REGISTERED_TIME = "registeredTime";
    /**
     * Name of the property to retrieve the time when the GuardTime Gateway received the
     * timestamp request.
     *
     * This is given as UTC time and date according to the Gateway's local clock.
     *
     * @see getProperty
     * @see ACCURACY
     */
    const REQUEST_TIME = "issuer.genTime";
    /**
     * Name of the property to retrieve the unique serial number of the timestamp.
     *
     * @see getProperty
     */
    const SERIAL_NUMBER = "issuer.serialNumber";

    private $token;
    private $verificationResult;
    private $properties;
    private $registeredTime;
    private $dataHash;

    /**
     * Constructs a new GTTimestamp instance.
     *
     * @param  CMSContentInfo $token timestamp token
     */
    public function __construct(CMSContentInfo $token) {
        $this->token = $token;
        $this->verificationResult = null;

        $this->updateProperties();
    }

    /**
     * Parses timestamp contents and checks vital parameters.
     *
     * This method performs basic timestamp syntactic checks.
     *
     * @throws GTException
     * @return void
     */
    protected function updateProperties() {
        $this->properties = array();

        $signedData = $this->token->getContent();

        $tstInfo = $signedData->getEncapsulatedContent()->getContent();

        $hashAlgorithm = $tstInfo->getMessageImprint()->getHashAlgorithm()->getAlgorithm();
        $hashedMessage = $tstInfo->getMessageImprint()->getHashedMessage();
        $this->dataHash = new GTDataHash(GTHashAlgorithm::getByOid($hashAlgorithm), $hashedMessage);

        // timestamps should only have 1 signerinfo
        if (count($signedData->getSignerInfos()) != 1) {
            throw new GTException("Invalid signerInfos size: " . count($signedData->getSignerInfos()));
        }

        $signerInfo = $signedData->getSignerInfo();
        $signerInfoSid = $signerInfo->getSid();

        if (empty($signerInfoSid)) {
            throw new GTException("Missing signerInfo.sid");
        }

        // verify that sid is of type issuerAndSerialNumber for timestamps, not subjectKeyIdentifier
        $issuerAndSerialNumber = $signerInfoSid->getIssuerAndSerialNumber();

        if (empty($issuerAndSerialNumber)) {
            throw new GTException("Missing signerInfo.sid.issuerAndSerialNumber");
        }

        // check digest alg is SHA-256
        if ($signerInfo->getDigestAlgorithm()->getAlgorithm() != GTHashAlgorithm::getByName('SHA256')->getOid()) {
            throw new GTException("Unsupported digestAlgorithm: {$signerInfo->getDigestAlgorithm()->getAlgorithm()}");
        }

        // check signature alg is GTTimeSignature::OID
        if ($signerInfo->getSignatureAlgorithm()->getAlgorithm() != GTTimeSignature::OID) {
            throw new GTException("Unsupported signatureAlgorithm: {$signerInfo->getSignatureAlgorithm()->getAlgorithm()}");
        }

        $signedAttributes = $signerInfo->getSignedAttrs();

        if (empty($signedAttributes)) {
            throw new GTException("Signed attributes missing");
        }

        if (count($signedAttributes) != 2) {
            throw new GTException("Signed attributes invalid size: " . count($signedAttributes));
        }

        $contentType = $signedAttributes[0];
        $messageDigest = $signedAttributes[1];

        if ($contentType->getType() != self::CONTENT_TYPE_ID) {
            throw new GTException("Expecting content-type signedAttr, but found: " . $contentType->getType());
        }

        if ($messageDigest->getType() != self::MESSAGE_DIGEST_ID) {
            throw new GTException("Expecting message-digest signedAttr, but found: " . $messageDigest->getType());
        }

        $contentTypeValues = $contentType->getValues();
        $messageDigestValues = $messageDigest->getValues();

        if (count($contentTypeValues) != 1) {
            throw new GTException("Multiple values not allowed for signedAttr content-type");
        }

        if (count($messageDigestValues) != 1) {
            throw new GTException("Multiple values not allowed for signedAttr message-digest");
        }

        $contentTypeValue = $contentTypeValues[0]->getValue();
        $messageDigestValue = $messageDigestValues[0];

        if ($contentTypeValue != self::CONTENT_TYPE) {
            throw new GTException("Invalid signedAttr content-type: " . $contentTypeValue);
        }

        if (!$messageDigestValue instanceof ASN1OctetString) {
            throw new GTException("Invalid signedAttr message-digest: " . $messageDigestValue);
        }

        $this->properties[self::HASH_ALGORITHM] = $this->dataHash->getHashAlgorithm()->getOid();
        $this->properties[self::HASHED_MESSAGE] = GTBase16::encode($this->dataHash->getHashedMessage());

        $this->properties[self::POLICY_ID] = $tstInfo->getPolicy();
        $this->properties[self::SERIAL_NUMBER] = $tstInfo->getSerialNumber();

        $this->properties[self::REQUEST_TIME] = GTUtil::formatTime(GTUtil::decodeTime($tstInfo->getGenTime()));

        $accuracy = $tstInfo->getAccuracy();

        if ($accuracy != null) {
            $this->properties[self::ACCURACY] = $accuracy->getFormatted();
        }

        $tsa = $tstInfo->getFormattedTsa();

        if ($tsa != null) {
            $this->properties[self::ISSUER_NAME] = $tsa;
        }

        $timeSignature = new GTTimeSignature();
        $timeSignature->decode($signedData->getSignerInfo()->getSignature());

        $publicationId = new GTBigInteger($timeSignature->getPublishedData()->getPublicationIdentifier());

        $historyChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
        $locationChain = GTHashChain::getLocationInstance($timeSignature->getLocation());

        $historyId = $historyChain->computeHistoryId($publicationId);
        $locationId = $locationChain->computeLocationId();

        $this->properties[self::HISTORY_ID] = $historyId->getValue();
        $this->properties[self::LOCATION_ID] = $locationId->getValue();

        $this->registeredTime = $historyId;

        $this->properties[self::REGISTERED_TIME] = GTUtil::formatTime($this->registeredTime->getValue(), 'UTC');

        if ($timeSignature->isExtended()) {

            $publishedData = $timeSignature->getPublishedData();

            $this->properties[self::PUBLICATION_ID] = $publicationId->getValue();
            $this->properties[self::PUBLICATION_TIME] = GTUtil::formatTime($publicationId->getValue(), 'UTC');
            $this->properties[self::PUBLICATION] = $publishedData->getEncodedPublication();
            $this->properties[self::PUBLICATION_REFERENCE] = $timeSignature->getEncodedPublicationReferences();

        } else {

            $pkSignature = $timeSignature->getPkSignature();
            $pkSignatureAlgorithm = $pkSignature->getSignatureAlgorithm();

            if (empty($pkSignatureAlgorithm)) {
                throw new GTException("Missing pkSignatureAlgorithm");
            }

            if ($pkSignatureAlgorithm->getAlgorithm() != self::PK_SIG_ALGO_ID) {
                throw new GTException("Invalid pkSignatureAlgorithm: {$pkSignatureAlgorithm->getAlgorithm()}");
            }
        }

    }

    /**
     * Extends this timestamp using the given extension response.
     *
     * @throws GTException
     * @param  GTCertTokenResponse $certTokenResponse extension response
     * @return void
     *
     * @see GTHttpClient
     * @see GTCertTokenResponse
     */
    public function extend(GTCertTokenResponse $certTokenResponse) {

        if (empty($certTokenResponse)) {
            throw new GTException("Parameter certTokenResponse is required");
        }

        if (!$certTokenResponse instanceof GTCertTokenResponse) {
            throw new GTException("Expecting an GTCertTokenResponse");
        }

        $statusCode = $certTokenResponse->getStatusCode();

        if ($statusCode < 0 || $statusCode > 1) {
            throw new GTException("Extending not possible: " . $certTokenResponse->getStatus()->getStatusMessage());
        }

        $certToken = $certTokenResponse->getToken();

        $signedData = $this->token->getContent();
        $signerInfo = $signedData->getSignerInfo();

        $timeSignature = new GTTimeSignature();
        $timeSignature->decode($signerInfo->getSignature());

        $oldChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
        $newChain = GTHashChain::getHistoryInstance($certToken->getHistory());

        if (!$oldChain->checkPastEntries($newChain)) {
            throw new GTException("Extending failed: past history chains do not match in timestamp and response");
        }

        $timeSignature->setPkSignature(null);
        $timeSignature->setHistory($certToken->getHistory());
        $timeSignature->setPublishedData($certToken->getPublishedData());
        $timeSignature->setPubReference($certToken->getPubReference());

        $signerInfo->setSignature($timeSignature->encodeDER());
        $signedData->setCertificates(null);

        $this->updateProperties();
    }

    /**
     * Checks if this timestamp is extendable.
     *
     * @param  GTPublicationsFile $publicationsFile the publications file to check agains
     * @return bool true if this timestamp is extendable
     */
    public function isExtendable(GTPublicationsFile $publicationsFile) {

        if ($publicationsFile == null) {
            return false;
        }

        if ($this->isExtended()) {
            return false;
        }

        $regTime = $this->getRegisteredTime();
        $pubTime = $publicationsFile->getLastPublicationTime();

        return $regTime <= $pubTime;
    }

    /**
     * Verifies this timestamp using the given data hash and publications file.
     *
     * @throws GTException
     * @param  GTDataHash $dataHash data hash to use for verification
     * @param  GTPublicationsFile $publicationsFile publications file to use for verification
     * @return GTVerificationResult timestamp verification result
     */
    public function verify(GTDataHash $dataHash, GTPublicationsFile $publicationsFile) {

        if ($publicationsFile == null) {
            throw new GTException("Parameter publicationsFile is required");
        }

        if (!$publicationsFile instanceof GTPublicationsFile) {
            throw new GTException("Parameter publicationsFile must be an instance of GTPublicationsFile");
        }

        $this->verificationResult = new GTVerificationResult();
        $this->verificationResult->update($publicationsFile->verify());

        if (!$this->verificationResult->isValid()) {
            return $this->verificationResult;
        }

        if ($this->isExtended()) {

            $publication = $this->getProperty(self::PUBLICATION);

            if (!$publicationsFile->contains($publication)) {
                $this->verificationResult->updateErrors(GTVerificationResult::PUBLICATION_FAILURE);
            }

            if (!$this->verificationResult->isValid()) {
                return $this->verificationResult;
            }

            $this->verificationResult->update(GTVerifier::verifyWithPublication($this->token, $dataHash, $publication));

        } else {

            $certificates = $this->token->getContent()->getCertificates();

            if (count($certificates) < 1) {
                throw new GTException("Invalid timestamp: no certificates");
            }

            $certificate = $certificates[0];
            $certificate = new X509Certificate($certificate);

            $hash = X509Certificate::getPublicKeyHash($certificate->getPublicKey());

            if ($publicationsFile->containsPublicKey($hash)) {
                $this->verificationResult->updateStatus(GTVerificationResult::PUBLICATION_CHECKED);

            } else {

                $this->verificationResult->updateErrors(GTVerificationResult::PUBLIC_KEY_FAILURE);

                if (!$this->verificationResult->isValid()) {
                    return $this->verificationResult;
                }

            }

            $this->verificationResult->update(GTVerifier::verifyWithSignature($this->token, $dataHash, $certificate->getPublicKey()));
        }

        return $this->verificationResult;

    }

    /**
     * Checks if this timestamp is extended (hash-linked).
     *
     * @return bool true if this timestamp is extended, false otherwise
     */
    public function isExtended() {

        $timeSignature = new GTTimeSignature();
        $timeSignature->decode($this->token->getContent()->getSignerInfo()->getSignature());

        return $timeSignature->isExtended();
    }

    /**
     * Gets the timestamp property by the given name.
     *
     * @param  string $name property name
     * @return null|object property value or null if not set
     */
    public function getProperty($name) {

        if (!isset($this->properties[$name])) {
            return null;

        } else {
            return $this->properties[$name];

        }
    }

    /**
     * Gets the hash algorithm used to hash data which this timestamp was created for.
     *
     * @return GTHashAlgorithm hash algorithm
     */
    public function getHashAlgorithm() {
        return $this->dataHash->getHashAlgorithm();
    }

    /**
     * Gets the time this timestamp was registered at.
     *
     * @return GTBigInteger timestamp registration time
     */
    public function getRegisteredTime() {
        return $this->registeredTime;
    }

    /**
     * Encodes the contents of this timestamp using DER.
     *
     * @return array byte array containing the contents of this timestamp using DER encoding
     */
    public function encodeDER() {
        return $this->token->encodeDER();
    }

    /**
     * Saves this timestamp to the specified file.
     *
     * @param  string $file file name
     * @return void
     */
    public function save($file) {
        GTUtil::write($file, $this->encodeDER());
    }

    /**
     * Loads a GuardTime timestamp from the given file.
     *
     * @static
     * @param  string $file file name
     * @return GTTimestamp loaded timestamp
     */
    public static function load($file) {

        $bytes = GTUtil::read($file);

        $content = new CMSContentInfo();
        $content->decode(ASN1DER::decode($bytes));

        return new GTTimestamp($content);
    }

}

?>
