//////////////////////////////////////////////////////////////////////////// 
// 
// Copyright (C) DSTC Pty Ltd (ACN 052 372 577) 1993, 1994, 1995.
// Unpublished work.  All Rights Reserved.
// 
// The software contained on this media is the property of the
// DSTC Pty Ltd.  Use of this software is strictly in accordance
// with the license agreement in the accompanying LICENSE.DOC 
// file. If your distribution of this software does not contain 
// a LICENSE.DOC file then you have no rights to use this 
// software in any manner and should contact DSTC at the address 
// below to determine an appropriate licensing arrangement.
// 
//      DSTC Pty Ltd
//      Level 7, GP South
//      University of Queensland
//      St Lucia, 4072
//      Australia
//      Tel: +61 7 3365 4310
//      Fax: +61 7 3365 4311
//      Email: jcsi@dstc.qut.edu.au
// 
// This software is being provided "AS IS" without warranty of
// any kind.  In no event shall DSTC Pty Ltd be liable for
// damage of any kind arising out of or in connection with
// the use or performance of this software.
// 
//////////////////////////////////////////////////////////////////////////// 

package com.dstc.security.cms;

import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.Signature;

import com.dstc.security.provider.OID;
import com.dstc.security.x509.Attribute;
import com.dstc.security.x509.AlgorithmId;
import com.dstc.security.cms.v1.SignerInfo;
import com.dstc.security.cms.v1.SignedAttributes;
import com.dstc.security.cms.v1.IssuerAndSerialNumber;
import com.dstc.security.cms.atts.MessageDigest;
import com.dstc.security.asn1.OctetString;

/**
 * <p>A class representing a signer of a message accoring to CMS.
 *
 * <p>A Signer object is passed in the constructor of a SignedMessage.
 * To construct a Signer object, pass in the signing Private Key, the
 * associated X509Certificate and any extra certificates to include in
 * the SignedMessage to facilitate cert path processing for the verifier.
 *
 * <p>Next, set the digest and signature algorithms this Signer will use
 * by calling setDigestAlgorithm() and setSignatureAlgorithm(). Finally,
 * call setSignedAttributes() and setUnsignedAttributes(), as necessary,
 * to include any signed and/or unsigned attributes this Signer will 
 * include in the signing process. This Signer object is now ready to
 * be passed to a SignedMessage.
 *
 * @version 0.99, 99/01/26
 * @author Ming Yung
 *
 */

public class Signer 
{
  private Set certs;
  private X509Certificate signerCert;
  private CertPath cpc = null;
  private PrivateKey privKey;
  private Set signedAttributes = null;
  private Set unsignedAttributes = null;
  private String digestAlg = "SHA";
  private String sigAlg = null;
  private byte[] toBeSigned = null;
  private byte[] signature = null;
  private String provider = null;

  /**
   * Constructs a Signer from a signer Certificate, a set of other
   * certificates contributing towards a Certificate chain and a 
   * PrivateKey
   */
  public Signer(X509Certificate cert, Set certs, PrivateKey key)
  {
    this.signerCert = cert;

    if (this.certs == null)
      this.certs = new HashSet();
    else
      this.certs = certs;

    this.certs.add(cert);
    this.privKey = key;
    this.signedAttributes = new HashSet();

    try
    {
      this.cpc = new CertPath(this.certs, new IssuerAndSerialNumber(cert));
    }
    catch (CMSException e)
    {
      e.printStackTrace();
    }
  }

  /**
   * Constructs a Signer from a SignerInfo and a Set of X509Certificates
   *
   * (package scope: used only by SignedMessage)
   */
  Signer(SignerInfo signerInfo, Set certs) throws CMSException
  {
    this.signedAttributes = signerInfo.getSignedAttributes();
    this.signature = signerInfo.getSignature();

    this.sigAlg = signerInfo.getSignatureAlgorithm();
    if (this.sigAlg.equals("RSA"))
      this.sigAlg = signerInfo.getDigestAlgorithm() + "/RSA";

    this.certs = certs;

    IssuerAndSerialNumber iss = signerInfo.getIssuerAndSerialNumber();
    this.cpc = new CertPath(this.certs, iss);
    this.signerCert = cpc.getSignerCert();
    this.toBeSigned = signerInfo.getToBeSigned();
  }

  /**
   * Returns the Set of Certificates (including the signer cert) for this
   * Signer
   *
   * (package scope: used only by SignedMessage)
   */
  Set getCertificates()
  {
    return this.certs;
  }

  /**
   * Sets the digest algorithm this signer will use
   */
  public void setDigestAlgorithm(String alg)
  {
    this.digestAlg = alg;
  }

  /**
   * Returns the digest algorithm this signer will use
   *
   * (package scope: used only by SignedMessage)
   */
  String getDigestAlgorithm()
  {
    return this.digestAlg;
  }

  /**
   * Sets the provider for the Signature algorithm
   */
  public void setProvider(String name)
  {
    this.provider = name;
  }

  /**
   * Sets the signature algorithm this signer will use
   */
  public void setSignatureAlgorithm(String alg)
  {
    this.sigAlg = alg;
  }

  /**
   * Returns the signature algorithm this signer will use
   *
   * (package scope: used only by SignedMessage)
   */
  String getSignatureAlgorithm()
  {
    return this.sigAlg;
  }

  /**
   * Sets the Attributes this signer will sign
   */
  public void setSignedAttributes(Set atts)
  {
    this.signedAttributes = atts;
  }

  /**
   * Sets the Attributes this signer will not sign but will include
   * in a SignedMessage
   */
  public void setUnsignedAttributes(Set atts)
  {
    this.unsignedAttributes = atts;
  }
  
  /**
   * Signs a supplied message and returns a SignerInfo for this Signer
   *
   * (package scope: used only by SignedMessage)
   */
  SignerInfo sign(byte[] message) throws CMSException
  {
    if (this.sigAlg == null)
      throw new CMSException("Signature algorithm not set");

    if (this.signedAttributes == null)
      this.signedAttributes = new HashSet();

    this.signedAttributes.add(new MessageDigest(this.digest(message)));

    this.signature = this.sign();

    IssuerAndSerialNumber issuer = new IssuerAndSerialNumber(this.signerCert);

    String funnyOID = this.sigAlg;
    if (this.sigAlg.endsWith("RSA"))
      funnyOID = "RSA";

    return new SignerInfo(issuer, 
      new AlgorithmId(OID.getAlgOid(this.digestAlg)), 
        this.signedAttributes, funnyOID, this.signature);
  }

  /**
   * Verifies the signature for this Signer against the supplied message
   */
  void verify(byte[] message) throws CMSException
  {
    if (!Arrays.equals(this.getDigest(), this.digest(message)))
      throw new CMSException("Bad digest");

    this.verifySignature();
  }

  /**
   * Returns the CertPath for this Signer
   */
  public CertPath getCertPath()
  {
    return this.cpc;
  }

  /**
   * Digests a given message 
   */
  private byte[] digest(byte[] message) throws CMSException
  {
    try
    {
      java.security.MessageDigest md 
        = java.security.MessageDigest.getInstance(this.digestAlg);
      md.update(message);
      return md.digest();
    }
    catch (Exception e)
    {
      throw new CMSException(e.toString());
    }
  }

  /**
   * Signs and returns the signature
   */
  private byte[] sign() throws CMSException
  {
    try
    {
      Signature sig;

      if (this.provider == null)
      {
        sig = Signature.getInstance(this.sigAlg);
      }
      else
      {
        sig = Signature.getInstance(this.sigAlg, this.provider);
      }

      sig.initSign(this.privKey);
      this.toBeSigned = (new SignedAttributes(this.signedAttributes)).encode();
      sig.update(this.toBeSigned);
      return sig.sign();
    }
    catch (Exception e)
    {
      throw new CMSException(e.toString());
    }
  }

  /**
   * Verifies the signature on a supplied byte array of data
   */
  private void verifySignature()
    throws CMSException
  {
    try
    {
      Signature sig = Signature.getInstance(this.sigAlg);
      sig.initVerify(this.signerCert.getPublicKey());
      sig.update(this.toBeSigned);

      boolean truth = sig.verify(this.signature);
      if (!truth)
        throw new CMSException("Bad signature");
    }
    catch (Exception e)
    {
      throw new CMSException(e.getMessage());
    }
  }

  /**
   * Returns the Digest bytes for the associated SignerInfo
   */
  private byte[] getDigest()
  {
    Iterator it = this.signedAttributes.iterator();
    while (it.hasNext())
    {
      Attribute att = (Attribute)it.next();
      if (att.getType().equals(OID.id_messageDigest))
        return ((OctetString)att.getValues().elementAt(0)).getBytes();
    }

    return null;
  }
}
