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

/**
 * CMS SignedData implementation.
 *
 * <pre>
 *  SignedData ::= SEQUENCE {
 *        version               CMSVersion,
 *        digestAlgorithms      DigestAlgorithmIdentifiers,
 *        encapContentInfo      EncapsulatedContentInfo,
 *        certificates      [0] IMPLICIT CertificateSet OPTIONAL,
 *        crls              [1] IMPLICIT CertificateRevocationLists OPTIONAL,
 *        signerInfos           SignerInfos
 * }
 * </pre>
 *
 * @package asn1
 * @subpackage cms
 *
 * @link http://tools.ietf.org/html/rfc3852#section-5 RFC 3852: Cryptographic Message Syntax
 */
class CMSSignedData implements ASN1DEREncodable {

    /**
     * Object identifier for the Signed-Data content type.
     */
    const OID = "1.2.840.113549.1.7.2";

    private $version;
    private $digestAlgorithms;
    private $encapsulatedContent;
    private $certificates;
    private $signerInfos;

    /**
     * Constructs a new instance of CMSSignedData.
     *
     */
    public function __construct() {
    }

    /**
     * Decodes the given ASN1Sequence as CMSSignedData.
     *
     * @throws GTException
     * @param  ASN1Sequence $object CMSSignedData encoded as an ASN1Sequence.
     * @return void
     */
    public function decode($object) {

        if (!$object instanceof ASN1Sequence) {
            throw new GTException("Expecting an ASN1Sequence");
        }

        $size = $object->getObjectCount();

        if ($size < 4 || $size > 6) {
            throw new GTException("Invalid sequence size: {$size}");
        }

        $version = $object->getObjectAt(0);

        if (!$version instanceof ASN1Integer) {
            throw new GTException("Expecting an ASN1Integer");
        }

        $this->version = (int) $version->getValue();

        if ($this->version !== 3 && $this->version != 1) {
            throw new GTException("Invalid version: {$this->version}");
        }

        $set = $object->getObjectAt(1);

        if (!$set instanceof ASN1Set) {
            throw new GTException("Expecting an ASN1Set");
        }

        $digestAlgorithms = array();

        foreach ($set->getObjects() as $item) {

            $digestAlgorithm = new X509AlgorithmIdentifier();
            $digestAlgorithm->decode($item);

            array_push($digestAlgorithms, $digestAlgorithm);
        }

        $this->digestAlgorithms = $digestAlgorithms;

        $encapsulatedContent = new CMSEncapsulatedContentInfo();
        $encapsulatedContent->decode($object->getObjectAt(2));

        $this->encapsulatedContent = $encapsulatedContent;

        for ($i = 3; $i < $object->getObjectCount(); $i++) {

            $item = $object->getObjectAt($i);

            if ($item instanceof ASN1Tag) {

                switch ($item->getTagValue()) {

                    case 0:
                        // certificates

                        $this->certificates = array();

                        $set = $item->getObjectAs(ASN1_TAG_SET);

                        if ($set->getObjectCount() == 0) {
                            throw new GTException("Certificates size: 0");
                        }

                        foreach ($set->getObjects() as $certificate) {

                            if (!$certificate instanceof ASN1Sequence) {
                                throw new GTException("Certificate should be encoded as an ASN1Sequence");
                            }

                            if ($certificate->getObjectCount() == 0) {
                                throw new GTException("Certificate content missing");
                            }

                            array_push($this->certificates, $certificate);

                        }

                        break;

                    case 1:
                        // certificate revocation list

                        throw new GTException("Certificate revocation lists not implemented");

                        break;

                    default:
                        throw new GTException("Unknown tag value: {$item->getTagValue()}");

                }

            } else if ($item instanceof ASN1Set) {

                $this->signerInfos = array();

                if ($item->getObjectCount() == 0) {
                    throw new GTException("SignerInfos size: 0");
                }

                foreach ($item->getObjects() as $info) {

                    $signerInfo = new CMSSignerInfo();
                    $signerInfo->decode($info);

                    array_push($this->signerInfos, $signerInfo);
                }

            } else {
                throw new GTException("Unkonwn child of CMSSignedData: " . get_class($item));
            }

        }

    }

    /**
     * Encodes this CMSSignedData using DER.
     *
     * @return array byte array that contains the DER encoding of this CMSSignedData
     */
    public function encodeDER() {

        $sequence = new ASN1Sequence();
        $sequence->add(new ASN1Integer((int) $this->version));

        $digestAlgorithms = new ASN1Set();

        foreach ($this->digestAlgorithms as $digestAlgorithm) {
            $digestAlgorithms->add($digestAlgorithm);
        }

        $sequence->add($digestAlgorithms);
        $sequence->add($this->encapsulatedContent);

        if (!empty($this->certificates)) {

            $certificates = new ASN1Set();

            foreach ($this->certificates as $certificate) {
                $certificates->add($certificate);
            }

            $certificatesTag = new ASN1Tag();
            $certificatesTag->setExplicit(false);
            $certificatesTag->setTagClass(ASN1_TAG_CONTEXT);
            $certificatesTag->setTagType(ASN1_TAG_CONSTRUCTED);
            $certificatesTag->setTagValue(0);
            $certificatesTag->setObject($certificates);

            $sequence->add($certificatesTag);
        }

        $signerInfos = new ASN1Set();

        foreach ($this->signerInfos as $signerInfo) {
            $signerInfos->add($signerInfo);
        }

        $sequence->add($signerInfos);

        return $sequence->encodeDER();
    }

    /**
     * Gets the digest algorithms.
     *
     * @return array array of X509AlgorithmIdentifer
     */
    public function getDigestAlgorithms() {
        return $this->digestAlgorithms;
    }

    /**
     * Gets the encapsulated content.
     *
     * @return CMSEncapsulatedContentInfo the encapsulated content
     */
    public function getEncapsulatedContent() {
        return $this->encapsulatedContent;
    }

    /**
     * Gets the certificates.
     *
     * @return array certificates as raw unparsed ASN1Sequence instances
     */
    public function getCertificates() {
        return $this->certificates;
    }

    /**
     * Sets the certificates.
     *
     * @param  array $certificates certufificates as raw unparsed ASN1Sequence instances
     * @return void
     */
    public function setCertificates($certificates) {
        $this->certificates = $certificates;
    }

    /**
     * Gets the signer infos.
     *
     * @return array of CMSSignerInfo
     */
    public function getSignerInfos() {
        return $this->signerInfos;
    }

    /**
     * Convenience method for timestamps to get the single signer info.
     *
     *
     * @return CMSSignerInfo the first signer info
     */
    public function getSignerInfo() {

        if (isset($this->signerInfos[0])) {
            return $this->signerInfos[0];
        }

        return null;
    }

    /**
     * Gets the version.
     *
     * @return int version
     */
    public function getVersion() {
        return $this->version;
    }
}

?>
