<?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 asn1
 * @subpackage x509
 */

/**
 * X.509 Certificate implementation.
 *
 * This class treats X.509 certificates as BLOBs and doesn't
 * actually fully implement ASN.1 decoding/encoding of X509Certificates.
 *
 * Wrapper methods are provided for PHP's openssl_xxx functions.
 *
 * @package asn1
 * @subpackage x509
 */
class X509Certificate {

    private $bytes;

    private $cert;
    private $data;
    private $pkey;

    /**
     * Constructs a new instance of X509Certificate.
     *
     * @throws GTException
     * @param  array|ASN1DEREncodable $data
     * @return void
     */
    public function __construct($data) {

        if (is_array($data)) {
            $this->bytes = $data;

        } else if ($data instanceof ASN1DEREncodable) {
            $this->bytes = $data->encodeDER();

        } else {
            throw new GTException("paramater data must be an array of bytes or an instance of ASN1DEREncodable");

        }
    }

    /**
     * Destructs this X509Certificate.
     *
     * This method frees any allocated OpenSSL resources.
     *
     */
    public function __destruct() {

        if ($this->pkey !== null && $this->pkey !== false) {
            openssl_pkey_free($this->pkey);
        }

        if ($this->cert !== null && $this->cert !== false) {
            openssl_x509_free($this->cert);
        }

    }

    /**
     * PEM encodes this certificate.
     *
     * @return string pem encoded certificate
     */
    public function encodePEM() {

        $pem = "";
        $pem .= "-----BEGIN CERTIFICATE-----\r\n";

        $body = GTBase64::encode($this->bytes);
        $body = wordwrap($body, 64, "\r\n", true);

        if ($body{strlen($body) - 1} != "\n") {
            $body .= "\r\n";
        }

        $pem .= $body;
        $pem .= "-----END CERTIFICATE-----\r\n";

        return $pem;

    }

    /**
     * Verifies the given signature using this certificate's public key.
     *
     * @param  array $data byte array containing the data bytes that were signed
     * @param  array $sign byte array containing the signature bytes
     * @param  string $algorithm the hash algorithm to use
     * @return bool true if the signature is valid
     */
    public function verifySignature($data, $sign, $algorithm = 'sha256') {

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

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

        if ($this->cert === null || $this->cert === false) {
            $this->cert = openssl_x509_read($this->encodePEM());
        }

        if ($this->pkey == null || $this->pkey === false) {
            $this->pkey = openssl_pkey_get_public($this->cert);
        }

        return X509Certificate::verifyPublicKeySignature($this->pkey, $data, $sign, $algorithm);

    }

    /**
     * Gets the certificate parameters.
     *
     * @return array containing the certificate parameters
     * @see openssl_x509_parse
     */
    public function getParameters() {

        if ($this->cert === null || $this->cert === false) {
            $this->cert = openssl_x509_read($this->encodePEM());
        }

        if ($this->data === null) {
            $this->data = openssl_x509_parse($this->cert);
        }

        return $this->data;

    }

    /**
     * Gets this certificate's public key.
     *
     * @return resource OpenSSL public key resource
     * @see openssl_pkey_get_public
     *
     */
    public function getPublicKey() {

        if ($this->cert === null || $this->cert === false) {
            $this->cert = openssl_x509_read($this->encodePEM());
        }

        if ($this->pkey === null || $this->pkey === false) {
            $this->pkey = openssl_pkey_get_public($this->cert);
        }

        return $this->pkey;
    }

    /**
     * Checks if this certificate is valid for given purpose.
     *
     * @throws GTException
     * @param  int $purpose purpose
     * @param  array $chain certificate chain
     * @param  array $cainfo root certificates
     * @return bool true if this certificate is valid
     *
     * @see openssl_x509_checkpurpose
     */
    public function isValid($purpose, $chain = array(), $cainfo = array()) {

        if ($this->cert === null || $this->cert === false) {
            $this->cert = openssl_x509_parse($this->encodePEM());
        }

        // $untrustedFile = tempnam(sys_get_temp_dir(), "guardtime_ca");
        $untrustedFile = tempnam(DOKU_INC.'/data/tmp/', "guardtime_ca");


        foreach ($chain as $cert) {
            GTUtil::write($untrustedFile, GTUtil::toByteArray($cert->encodePEM()));
        }

        if ($cainfo == null) {
            $cainfo = array();

        } else if (is_string($cainfo)) {

            if (!is_file($cainfo)) {
                throw new GTException("Specified cainfo file does not exist: {$cainfo}");
            }

            $cainfo = array($cainfo);

        } else {
            throw new GTException("Invalid cainfo specified: " . $cainfo);

        }

        $result = openssl_x509_checkpurpose($this->cert, $purpose, $cainfo, $untrustedFile) === true;

        unlink($untrustedFile);

        return $result;

    }

    /**
     * Gets the public key hash for given public key.
     *
     * @static
     * @throws GTException
     * @param  resource $pkey OpenSSL public key resource
     * @return GTDataHash public key hash
     */
    public static function getPublicKeyHash($pkey) {

        if (!is_resource($pkey)) {
            throw new GTException("Parameter pkey must be a valid resource of type OpenSSL key");
        }

        $params = openssl_pkey_get_details($pkey);

        $string = $params["key"];
        $string = str_replace("-----BEGIN PUBLIC KEY-----", "", $string);
        $string = str_replace("-----END PUBLIC KEY-----", "", $string);
        $string = str_replace("\r", "", $string);
        $string = str_replace("\n", "", $string);

        $bytes = GTBase64::decode($string);

        $hash = new GTDataHash(GTHashAlgorithm::getByName('SHA256'));
        $hash->update($bytes);
        $hash->close();

        return $hash;
    }


    /**
     * Verifies public key signature.
     *
     * @static
     * @throws GTException
     * @param  resource $pkey public key to use for verification
     * @param  array $data array of data bytes
     * @param  array $sign array of signature bytes
     * @param  string $algorithm the hash algorithm to use
     * @return bool true if signature is valid
     */
    public static function verifyPublicKeySignature($pkey, $data, $sign, $algorithm = 'sha256') {

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

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

        if (!is_resource($pkey)) {
            throw new GTException("Parameter pkey must be a valid resource of type OpenSSL key");
        }

        $data = GTUtil::fromByteArray($data);
        $sign = GTUtil::fromByteArray($sign);

        return openssl_verify($data, $sign, $pkey, $algorithm) === 1;

    }
}

?>
