<?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
 */

/**
 * Class containing static helper methods for timestamp verification.
 *
 * @package tsp
 */
class GTVerifier {

    /**
     * Verifies a short term timestamp using the given public key.
     *
     * @static
     * @throws GTException
     * @param  CMSContentInfo $content timestamp token
     * @param  GTDataHash $dataHash datahash to verify the timestamp against
     * @param  resource $publicKey openssl public key to use for verification
     * @return GTVerificationResult timestamp verification result
     */
    public static function verifyWithSignature(CMSContentInfo $content, GTDataHash $dataHash, $publicKey) {

        $result = new GTVerificationResult();
        $result->update(GTVerifier::verifyCommon($content, $dataHash));

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

        $signedData = $content->getContent();
        $signerInfo = $signedData->getSignerInfo();

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

        $certificates = $signedData->getCertificates();

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

        $certificate = $certificates[0];

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

        $historyChain = GTHashChain::getHistoryInstance($timeSignature->getHistory());
        $historyId = $historyChain->computeHistoryId($publicationId);
        $historyTime = $historyId->getValue();

        $result->update(GTVerifier::verifyCertificate($certificate, $historyTime, $publicKey));

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

        $result->update(GTVerifier::verifyPkSignature($timeSignature, $publicKey));

        return $result;
    }

    /**
     * Verifies an extended timestamp against the given publication.
     *
     * @static
     * @throws GTException
     * @param  CMSContentInfo $content timestamp token
     * @param  GTDataHash $dataHash datahash to verify this timestamp against
     * @param  string $publication base32 encoded publication to use for timestamp verification
     * @return GTVerificationResult timestamp verification result
     */
    public static function verifyWithPublication(CMSContentInfo $content, GTDataHash $dataHash, $publication) {

        $result = new GTVerificationResult();
        $result->update(GTVerifier::verifyCommon($content, $dataHash));

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

        $signedData = $content->getContent();
        $signerInfo = $signedData->getSignerInfo();

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

        if (!$timeSignature->isExtended()) {
            throw new GTException("Unable to verify timeSignature that is not extended with publication");
        }

        $result->update(GTVerifier::verifyPublication($timeSignature, $publication));

        return $result;
    }

    /**
     * Common verification, used by both publication and signature verification.
     *
     * @static
     * @throws GTException
     * @param  CMSContentInfo $content timestamp token
     * @param  GTDataHash $dataHash data hash to verify this timestamp against
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyCommon(CMSContentInfo $content, GTDataHash $dataHash) {

        $result = new GTVerificationResult();

        $signedData = $content->getContent();
        $signerInfo = $signedData->getSignerInfo();

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

        if ($timeSignature->getPubReference() != null) {
            $result->updateStatus(GTVerificationResult::PUBLICATION_REFERENCE_PRESENT);
        }

        if ($timeSignature->getPkSignature() != null) {
            $result->updateStatus(GTVerificationResult::PUBLIC_KEY_SIGNATURE_PRESENT);
        }

        $messageImprint = $signedData->getEncapsulatedContent()->getContent()->getMessageImprint();

        $result->update(GTVerifier::verifyDataHash($messageImprint, $dataHash));

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

        $digestAlgorithm = GTHashAlgorithm::getByOid($signerInfo->getDigestAlgorithm()->getAlgorithm());

        $signedAttrs = $signerInfo->getSignedAttrs();

        if ($signedAttrs == null) {
            throw new GTException("Invalid signed attributes: null");
        }

        // GTTimestamp already checks that the messageDigest attribute is
        // the second signed attribute and has a single OCTECT STRING value
        $messageDigest = $signedAttrs[1]->getValues();
        $messageDigest = $messageDigest[0]->getValue();

        // verify that signedAttrs.messageDigest == signerInfo.digestAlgorithm(TSTInfo.encodeDER())

        $dataHash = new GTDataHash($digestAlgorithm);
        $dataHash->update($signedData->getEncapsulatedContent()->getContentRaw());
        $dataHash->close();

        if ($messageDigest !== $dataHash->getHashedMessage()) {
            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
        }

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

        // now feed the hash of signed attributes to the hash chains

        $result->update(GTVerifier::verifyHashChains($timeSignature, $digestAlgorithm, $signedAttrs));

        return $result;
    }

    /**
     * Data hash verification against the message imprint.
     *
     * @static
     * @throws GTException
     * @param  TSPMessageImprint $messageImprint message imprint
     * @param  GTDataHash $dataHash data hash
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyDataHash(TSPMessageImprint $messageImprint, GTDataHash $dataHash) {

        $result = new GTVerificationResult();

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

        if ($dataHash == null) {
            return $result;
        }

        if ($dataHash->getHashAlgorithm()->getOid() != $messageImprint->getHashAlgorithm()->getAlgorithm()) {
            $result->updateErrors(GTVerificationResult::WRONG_DOCUMENT_FAILURE);

        } else if ($dataHash->getHashedMessage() != $messageImprint->getHashedMessage()) {
            $result->updateErrors(GTVerificationResult::WRONG_DOCUMENT_FAILURE);

        }

        $result->updateStatus(GTVerificationResult::DATA_HASH_CHECKED);

        return $result;
    }


    /**
     * Hash chains verification method.
     *
     * @static
     * @param  GTTimeSignature $timeSignature
     * @param  GTHashAlgorithm $digestAlgorithm
     * @param  array $signedAttrs
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyHashChains(GTTimeSignature $timeSignature, GTHashAlgorithm $digestAlgorithm, array $signedAttrs) {

        $result = new GTVerificationResult();

        $locationChainBytes = $timeSignature->getLocation();
        $historyChainBytes = $timeSignature->getHistory();

        $locationChain = null;
        $historyChain = null;

        try {

            $locationChain = GTHashChain::getLocationInstance($locationChainBytes);
            $historyChain = GTHashChain::getHistoryInstance($historyChainBytes);

        } catch (GTException $e) {
            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
            return $result;
        }

        $publicationImprint = $timeSignature->getPublishedData()->getPublicationImprint();
        $publicationImprintAlg = GTHashAlgorithm::getByGtid($publicationImprint[0]);

        if ($publicationImprintAlg->getLength() + 1 != count($publicationImprint)) {
            $result->updateErrors(GTVerificationResult::SYNTACTIC_CHECK_FAILURE);
            return $result;
        }

        $set = new ASN1Set();

        foreach ($signedAttrs as $attr) {
            $set->add($attr);
        }

        $input = new GTDataHash($digestAlgorithm);
        $input->update($set->encodeDER());
        $input->close();

        $input = $input->getDataImprint();

        $locationOutput = $locationChain->computeOutput($input);
        $historyOutput = $historyChain->computeOutput($locationOutput);

        $output = new GTDataHash($publicationImprintAlg);
        $output->update($historyOutput);
        $output->close();

        $output = $output->getDataImprint();

        if ($output != $publicationImprint) {
            $result->updateErrors(GTVerificationResult::HASHCHAIN_VERIFICATION_FAILURE);
        }

        return $result;
    }

    /**
     * Publication verification.
     *
     * @static
     * @throws GTException
     * @param  GTTimeSignature $timeSignature
     * @param  string $publication
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyPublication(GTTimeSignature $timeSignature, $publication) {

        if ($publication == null) {
            throw new GTException("parameter publication is required");
        }

        $result = new GTVerificationResult();

        if ($publication != $timeSignature->getPublishedData()->getEncodedPublication()) {
            $result->updateErrors(GTVerificationResult::PUBLICATION_FAILURE);
        }

        $result->updateStatus(GTVerificationResult::PUBLICATION_CHECKED);

        return $result;
    }

    /**
     * Certificate verification.
     *
     * @static
     * @throws GTException
     * @param  ASN1Sequence|array $certificate
     * @param  string $historyTime
     * @param  resource $publicKey
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyCertificate($certificate, $historyTime, $publicKey) {

        $result = new GTVerificationResult();

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

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

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

        $cert = new X509Certificate($certificate);

        // verify cert was valid when timestamp was created

        $params = $cert->getParameters();

        if ($params["validFrom_time_t"] > $historyTime) {
            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
        }

        if ($params["validTo_time_t"] < $historyTime) {
            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
        }

        // verify public key belongs to cert

        $hash1 = X509Certificate::getPublicKeyHash($publicKey);
        $hash2 = X509Certificate::getPublicKeyHash($cert->getPublicKey());

        if ($hash1 != $hash2) {
            $result->updateErrors(GTVerificationResult::CERTIFICATE_FAILURE);
        }

        return $result;
    }

    /**
     * Public key signature verification.
     *
     * @static
     * @throws GTException
     * @param  GTTimeSignature $timeSignature
     * @param  resource $publicKey
     * @return GTVerificationResult timestamp verification result
     */
    private static function verifyPkSignature(GTTimeSignature $timeSignature, $publicKey) {

        $result = new GTVerificationResult();

        $signatureAlgorithm = $timeSignature->getPkSignature()->getSignatureAlgorithm()->getAlgorithm();

        switch ($signatureAlgorithm) {

            case '1.2.840.113549.1.1.11':
                $signatureAlgorithm = 'sha256';
                break;

            default:
                throw new GTException("Unsupported signature algorithm: {$signatureAlgorithm}");
        }

        $sign = $timeSignature->getPkSignature()->getSignatureValue();
        $data = $timeSignature->getPublishedData()->encodeDER();

        if (!X509Certificate::verifyPublicKeySignature($publicKey, $data, $sign, $signatureAlgorithm)) {
            $result->updateErrors(GTVerificationResult::PUBLIC_KEY_SIGNATURE_FAILURE);
        }

        return $result;
    }

}

?>
