// SigSuiteDSS.java
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

// The source code is jointly developed by W3C and some of its members
// participating in the Digital Signature Initiative.  The authors are:
//    Mark Champine, HP, <champine@apollo.hp.com>
//    Yang-hua Chu, MIT/W3C, <yhchu@w3.org>
//    Vasanthan Dasan, Sun, <vasanthan.dasan@central.sun.com>
//    Peter Lipp, University of Technology, Graz <plipp@iaik.tu-graz.ac.at>
//    Andreas Sterbenz, U. of Technology, Graz <sterbenz@iaik.tu-graz.ac.at>

package w3c.www.dsig;

import java.security.*;
import java.security.interfaces.*;
import java.math.BigInteger;
import java.util.Dictionary;
import java.util.Vector;
import w3c.www.dsig.*;
import w3c.tools.codec.*;

/**
 * This class implements the DSS signature suite for the W3C's DSig
 * as defined in <TT>http://www.w3.org/TR/1998/REC-DSig-label/DSS-1_0</TT>.
 * <P>
 * Application programmers should never access this class via its name
 * <CODE>w3c.www.dsig.SigSuiteDSS</CODE>. You should use
 * <CODE>SigSuiteRegistry.getInstance()</CODE>,
 * <CODE>sigblock.getSigSuite()</CODE>, etc.
 * <P>
 * This class does not the implement the cryptographic DSS algorithm itself,
 * it requires that a Java security provider implementing it is already
 * installed.
 *
 * @author Andreas Sterbenz
 * @version 1.0beta (last modified 19-December-1997)
 */
public final class SigSuiteDSS extends SignatureSuite
{
  private static Class dssprovider = null;
  /**
   * Internal flag indicating debug mode.
   */
  private final static boolean DEBUG = false;

  private int keyLength = -1;

  /**
   * The string defined to indentify the DSS (DSA with SHA-1)
   * signature suite implemented by this class,
   * <TT>http://www.w3.org/TR/1998/REC-DSig-label/DSS-1_0</TT>.
   */
  public final static String DSSURL = 
  "http://www.w3.org/TR/1998/REC-DSig-label/DSS-1_0";

  static {
    initialize();
  }

  /**
   * Initialize function that makes sure that provider implementing
   * DSS is present and stores the Class object of that class.
   */
  private static void initialize()
  {
    if( dssprovider == null ) {
      try {
        Signature dss = Signature.getInstance("DSA");
        dssprovider = dss.getClass();
        if(DEBUG) System.out.println("SigSuiteDSS: Using DSA implementation from " + dssprovider.getName());
      } catch( NoSuchAlgorithmException e ) {
        if(DEBUG) System.err.println("Could not find algorithm DSA!");
      }
    }
  }

  /**
   * Constructor for use by
   * <CODE>w3c.www.dsig.SigSuiteRegistry.getInstance()</CODE>.
   * Application programmers should not need to access this
   * constructor directly.
   * @exception NoSuchAlgorithmException if the signature algorithm DSA
   * cannot be found
   */
  public SigSuiteDSS() throws NoSuchAlgorithmException
  {
    super(DSSURL, "DSA");
    initialize();
    if( dssprovider == null ) throw new NoSuchAlgorithmException();
  }

  /**
   * Return the length of the key used to generate this signature.
   * To be more precise, return the length of the number p, which is only
   * one part of the key. Note that the keylength will only be available
   * after <CODE>sign()</CODE> or <CODE>verify()</CODE> has been called,
   * otherwise this method will return -1.
   */
  public int getKeyLength()
  {
    return keyLength;
  }
  
  /**
   * Sign the given label using the given private key, which
   * needs to be an instance of a DSAPublicKey. Signing automatically
   * sets the ByName property to "[anonymous]", that should be
   * set appropriately after signing. If this signature suite
   * object already contains a signature, it will be replaced by
   * the new one.
   */
  public DSigLabelInterface sign(DSigLabelInterface label, PrivateKey privkey)
  {
    if (DEBUG) System.out.println("SigSuiteDSS: sign called");
    try {
      Signature dss = (Signature)dssprovider.newInstance();
      DSAPrivateKey dsaprivkey = (DSAPrivateKey)privkey;
      dss.initSign(dsaprivkey);
      performHashing(dss, label);
      // add date (?)
      byte[] sig = dss.sign();      // sign returns an ASN.1 encoded signature
      BigInteger[] rs = ASN1toRS(sig);
      if(DEBUG) System.out.println("r = " + rs[0]);
      if(DEBUG) System.out.println("s = " + rs[1]);
      addValuePair("SigCrypto", "R", BigIntToBase64(rs[0]));
      addValuePair("SigCrypto", "S", BigIntToBase64(rs[1]));
      // yc-d: label.getSigBlock().addSigSuite(this);
      setByName("[anonymous]");
      keyLength = dsaprivkey.getParams().getP().bitLength();
      return label;
    } catch( Exception e ) {
      if (DEBUG) {
	System.out.println("SigSuiteDSS: sign failed");
	e.printStackTrace();
      }
      return null;
    }
  }

  /**
   * Verify if this signature suite object correctly signs the
   * given label with the given public key. pubkey must either
   * be an instance of DSAPublicKey or null. In that case the
   * public key will be extracted from the signature suite
   * if it is specified either as ByKey or as ByName (trying
   * a certificate lookup). If no valid public key can be found,
   * verify returns unknown. Else the signature will be verified
   * using that public key returning unknown if there was any
   * other problem verifying the signature or one of true/ false
   * to indicate the result.
   */
  public Trivalue verify(DSigLabelInterface label, PublicKey pubkey)
  {
    DSAPublicKey dsapubkey;

    // check date (?)
    if( pubkey != null ) {
      if( !(pubkey instanceof DSAPublicKey) ) {
        if(DEBUG) System.out.println("SigSuiteDSS: invalid public key parameter!");
        return Trivalue.newUnknown();
      }
      if(DEBUG) System.out.println("SigSuiteDSS: verifying using the specified public key");
      dsapubkey = (DSAPublicKey)pubkey;
      return doVerify(label, dsapubkey);
    } else { // pubkey == null, extract key from ByX in signature...
      String type = getByType();
      if( type.equals("ByKey") ) {
        if(DEBUG) System.out.println("SigSuiteDSS: verifying using ByKey");
        dsapubkey = (DSAPublicKey)getBy();
        return doVerify(label, dsapubkey);
      } else if( type.equals("ByName") ) {
        String bynamestr = (String)getBy();
        if(DEBUG) System.out.println("SigSuiteDSS: verifying using ByName");
        if(DEBUG) System.out.println("SigSuiteDSS: Signer name is '" + bynamestr + "'");
        if(DEBUG) System.out.println("SigSuiteDSS: trying to obtain certificate for signer...");
        Certificate[] certs = CertDatabase.certForName(bynamestr);
        if(DEBUG) System.out.println("SigSuiteDSS: " + certs.length + " certificates found for '" + bynamestr + "'");
        // try all certificates available for the individual
        for( int i=0; i<certs.length; i++ ) {
          try {
            dsapubkey = (DSAPublicKey)certs[i].getPublicKey();
            Trivalue res = doVerify(label, dsapubkey);
            // if result is true, we are done
            // if result is unknown, we are done, too, because that
            // only happens if the DSS algorithm is not available at all
            // or if the signature is invalid
            // if result is false, we might have tried the wrong
            // DSA key for that individual and need to try again
            if( ! res.isFalse() ) return res;
          } catch( ClassCastException e ) {
            // ignore
          }
        }
        if(DEBUG) System.out.println("SigSuiteDSS: no certificate with a DSA key found for this signer, cannot verify.");
        return Trivalue.newUnknown();
      } else if( type.equals("ByHash") ) {
        if(DEBUG) System.out.println("SigSuiteDSS: verifying using ByHash (not implemented!)");
        // should somehow try to find a key matching that key-hash...
        return Trivalue.newUnknown();
      } else {
        if(DEBUG) System.out.println("SigSuiteDSS: no by option found for signature, cannot verify");
        return Trivalue.newUnknown();
      }
    }
    // this point can never be reached
  }

  /**
   * Internal verify function, pubkey is never null and always
   * an instance of DSAPublicKey.
   */
  private Trivalue doVerify(DSigLabelInterface label, DSAPublicKey pubkey)
  {
    keyLength = pubkey.getParams().getP().bitLength();
    if (DEBUG) System.out.println("SigSuiteDSS: doVerify called");
    try {
      Signature dss = (Signature)dssprovider.newInstance();
      dss.initVerify(pubkey);
      performHashing(dss, label);
      String rbase64 = (String) ((Dictionary)suiteValues.get("SigCrypto")).get("R");
      String sbase64 = (String) ((Dictionary)suiteValues.get("SigCrypto")).get("S");
      BigInteger r = Base64ToBigInt(rbase64);
      BigInteger s = Base64ToBigInt(sbase64);
      if(DEBUG) System.out.println("r = " + r);
      if(DEBUG) System.out.println("s = " + s);
      byte[] sig = RStoASN1(r, s);
      Trivalue result = new Trivalue(dss.verify(sig));
      if(DEBUG) System.out.println("SigSuiteDSS: result is " + result);
      return result;
    } catch( Exception e ) { // NoSuchAlgorithmException, Base64FormatException
      return Trivalue.newUnknown();
    }
  }

  /**
   * Perform the hashing of the label data as defined in the DSS
   * signature suite specification. This includes stripping
   * the sigblock and other parts of the label as specified in
   * its exclude extension. The data is hashed into the given
   * signature object using the update method.
   * Internal use only.
   */
  private void performHashing(Signature dss, DSigLabelInterface label) 
       throws SignatureException, DSigException
  {
    // simply hash the excluded label
//     label.updateLabel();
//     PICSLabel excludedLabel = exclude(label);
//     if(DEBUG) System.out.println("\nThe excluded label is :" + excludedLabel.toString());
//     dss.update((excludedLabel.toString()).getBytes());
    label.updateDSig();
    String excludedLabel = label.toExcludedDSigString(super.getSuiteValues());
    if (DEBUG) System.out.println("\nThe excluded label is :" + excludedLabel);
    dss.update(excludedLabel.getBytes());
  }

  /**
   * Return the type of the By information given in this signature suite.
   * This will return either one of the string  "ByName", "ByKey", or
   * "ByHash" or null.
   */
  public String getByType()
  {
    Object byobj;
    if( (byobj = suiteValues.get("ByName")) != null ) return "ByName";
    if( (byobj = suiteValues.get("ByKey")) != null ) return "ByKey";
    if( (byobj = suiteValues.get("ByHash")) != null ) return "ByHash";
    return null;
  }
  
  /**
   * Return the By information itself. For ByName, this will return the
   * name as string, for ByKey it will return the name as a DSAPublicKey,
   * and for ByHash it will return the hash data as a byte array. In case
   * of any error or if no known by information was found this method
   * return null.
   */
  public Object getBy()
  {
    String type = getByType();
    if( type != null ) {
      if( type.equals("ByName") ) return getByName();
      if( type.equals("ByKey") ) return getByKey();
      if( type.equals("ByHash") ) return getByHash();
      // no ByCert at the moment
    }
    return null;
  }
  
  private String getByName()
  {
    Object byobj; 
    if( (byobj = suiteValues.get("ByName")) != null ) {
      try {
        String bynamestr = (String)byobj; 
        return bynamestr;
      } catch( ClassCastException e ) {
      }
    }
    return null;
  }
  
  private PublicKey getByKey()
  {
    try {
      Object byobj = suiteValues.get("ByKey");
      Dictionary bykeydict = (Dictionary)byobj;
      BigInteger y, p, q, g;
      y = Base64ToBigInt( (String) bykeydict.get("Y"));
      p = Base64ToBigInt( (String) bykeydict.get("P"));
      q = Base64ToBigInt( (String) bykeydict.get("Q"));
      g = Base64ToBigInt( (String) bykeydict.get("G"));
      return new DSAPublicKeyClass(y, p, q, g);
    } catch( Exception e ) { // ClassCastException, InvalidKeyException
      if(DEBUG) System.out.println("SigSuiteDSS: invalid ByKey element found!");
    }
    return null;
  }
  
  private byte[] getByHash()
  {
    Object byobj; 
    if( (byobj = suiteValues.get("ByHash")) != null ) {
      try {
        String b64digest = (String)byobj; 
        return Base64ToByteArray(b64digest);
      } catch( ClassCastException e ) {
      }
    }
    return null;
  }

  /**
   * Set the By information for this signature suite to the type
   * type and the value to signer. Supported by this signature suite
   * are "ByName", which uses signer.toString() as the value;
   * and "ByKey" and "ByHash", in which cases signer has to be an 
   * instance of DSAPublicKey. The method returns this signature suite
   * itself for success or null if there was an error.
   * <EM>NOTE:</EM> This method has to be called after signing in order
   * to have any effect.
   */
  public SignatureSuite setBy(String type, Object signer)
  {
    if( signer == null ) return null;
    if( type.equals("ByName") ) {
      return setByName(signer.toString());
    }
    if( type.equals("ByKey") && (signer instanceof PublicKey) ) {
      return setByKey((PublicKey)signer); 
    }
    if( type.equals("ByHash") && (signer instanceof PublicKey) ) {
      return setByHash((PublicKey)signer); 
    }
    if( type.equals("ByCert") ) {
      if(DEBUG) System.out.println("SigSuite: ByCert not implemented yet."); 
    }
    if(DEBUG) System.out.println("SigSuite: Unknown type for By '" + type + "'.");
    return null;
  }
  
  /**
   * Set the ByX option for this signature suite to ByKey with the
   * values taken from the given public key. The key has to be an
   * instance of DSAPublicKey and should be the public key matching
   * the private key used for signing for this to make any sense.
   */
  private SignatureSuite setByKey(PublicKey pubkey)
  {
    removeBy();
    try {
      if (DEBUG) System.out.println("Entered setByKey");
      DSAPublicKey dsapubkey = (DSAPublicKey)pubkey;
      BigInteger y = dsapubkey.getY();
      DSAParams params = dsapubkey.getParams();
      BigInteger p = params.getP();
      BigInteger q = params.getQ();
      BigInteger g = params.getG();
      addValuePair("ByKey", "Y", BigIntToBase64(y));
      addValuePair("ByKey", "P", BigIntToBase64(p));
      addValuePair("ByKey", "Q", BigIntToBase64(q));
      addValuePair("ByKey", "G", BigIntToBase64(g));
      if (DEBUG) System.out.println("Exited setByKey");
    } catch( ClassCastException e ) {
      return null;
    }
    return this;
  }

  /**
   * Set the ByX option for this signature suite to ByHash with the
   * values taken from the given public key. Hashing is performed
   * using the SHA-1 message digest algorithm with the key encoded
   * as defined in the DSS Signature Suite Specification.
   * The key has to be an
   * instance of DSAPublicKey and should be the public key matching
   * the private key used for signing for this to make any sense.
   */
  private SignatureSuite setByHash(PublicKey pubkey)
  {
    removeBy();
    try {
      DSAPublicKey dsapubkey = (DSAPublicKey)pubkey;
      BigInteger y = dsapubkey.getY();
      DSAParams params = dsapubkey.getParams();
      BigInteger p = params.getP();
      BigInteger q = params.getQ();
      BigInteger g = params.getG();
      MessageDigest md = HashRegistry.getInstance(HashRegistry.SHA1URL);
      md.update(BigInteger2ByteArrayWithLengthPrefix(y));
      md.update(BigInteger2ByteArrayWithLengthPrefix(p));
      md.update(BigInteger2ByteArrayWithLengthPrefix(q));
      md.update(BigInteger2ByteArrayWithLengthPrefix(g));
      byte[] bindigest = md.digest();
      suiteValues.put("ByHash", ByteArrayToBase64(bindigest));
    } catch( NoSuchAlgorithmException e ) {
      return null;
    } catch( ClassCastException e ) {
      return null;
    }
    return this;
  }

  /**
   * Set the ByX option for this signature suite to ByName with the
   * given name. In order to allow the verifier to successfully
   * verify the signature, the name should match the name of
   * an individual that has a certificate included in the attribution
   * info of the label, although this is not required.
   */
  private SignatureSuite setByName(String name)
  {
    removeBy();
    suiteValues.put("ByName", name);
    return this;
  }

  /**
   * ASN.1 BER object id for a sequence with the constructed bit
   * set.
   */
  private final static byte OBJID_CONSTRUCTED_SEQUENCE = 0x30;
  /**
   * ASN.1 BER object ID for an integer value.
   */
  private final static byte OBJID_INTEGER = 0x02;

  /**
   * Convert the ASN.1 representation of a DSS signature as returned
   * by the Sun implementation of the DSS algorithm into the two
   * integers R and S, that make up the signature. Index 0 of
   * the returned array will contain R, index 1 S. The array will
   * always either be null or contain those to values.
   */
  private static BigInteger[] ASN1toRS(byte[] sig)
  {
    BigInteger[] rs = new BigInteger[2];
    try {
      int i=0;
      if( sig[i++] != OBJID_CONSTRUCTED_SEQUENCE ) return null;
      if( sig[i++] != sig.length-2 ) return null;
      if( sig[i++] != OBJID_INTEGER ) return null;
      int rlen = sig[i++] & 0xff;
      if( rlen > 127 ) return null;
      byte[] rbyte = new byte[rlen];
      System.arraycopy(sig, i, rbyte, 0, rlen);
      i += rlen;
      if( sig[i++] != OBJID_INTEGER ) return null;
      int slen = sig[i++] & 0xff;
      if( slen > 127 ) return null;
      byte[] sbyte = new byte[slen];
      System.arraycopy(sig, i, sbyte, 0, slen);
      i += slen;
      if( i != sig.length ) return null;
      rs[0] = new BigInteger(rbyte);
      rs[1] = new BigInteger(sbyte);
    } catch( RuntimeException e ) {
      return null;
    }
    return rs;
  }

  /**
   * Convert the to BigIntegers R and S of a DSS signature
   * into a ASN.1 BER sequence so that the signature  can be used
   * with the Sun DSS implementation.
   */
  private static byte[] RStoASN1(BigInteger r, BigInteger s)
  {
    byte[] rbyte = r.toByteArray();
    byte[] sbyte = s.toByteArray();
    byte[] asn = new byte[6 + rbyte.length + sbyte.length];
    int i=0;
    asn[i++] = OBJID_CONSTRUCTED_SEQUENCE;
    asn[i++] = (byte)(4 + rbyte.length + sbyte.length);
    asn[i++] = OBJID_INTEGER;
    asn[i++] = (byte)rbyte.length;
    System.arraycopy(rbyte, 0, asn, i, rbyte.length);
    i += rbyte.length;
    asn[i++] = OBJID_INTEGER;
    asn[i++] = (byte)sbyte.length;
    System.arraycopy(sbyte, 0, asn, i, sbyte.length);
    return asn;
  }
}
