/*
 * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE file for licensing information.
 */
package eu.emi.security.authn.x509.proxy;

import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;

import eu.emi.security.authn.x509.helpers.proxy.DraftRFCProxyCertInfoExtension;
import eu.emi.security.authn.x509.helpers.proxy.ProxyAddressRestrictionData;
import eu.emi.security.authn.x509.helpers.proxy.ProxyCSRImpl;
import eu.emi.security.authn.x509.helpers.proxy.ProxyCertInfoExtension;
import eu.emi.security.authn.x509.helpers.proxy.ProxyGeneratorHelper;
import eu.emi.security.authn.x509.helpers.proxy.ProxySAMLExtension;
import eu.emi.security.authn.x509.helpers.proxy.ProxyTracingExtension;
import eu.emi.security.authn.x509.helpers.proxy.RFCProxyCertInfoExtension;
import eu.emi.security.authn.x509.impl.CertificateUtils;

/**
 * Generates a proxy certificate signing request. The request parameters may contain
 * extensions which are passed in the generated Certificate Signing Request. 
 * Of course the peer issuing the proxy certificate may ignore them.
 * <p>
 * The following rules are applied basing on the parameters object:
 * <ul>
 * <li> [RFC proxy only] If the serial number is set then it is used as requested CN part of the proxy. 
 * Otherwise the CN part is set to the serial number of the issuing certificate. Note that 
 * this value most probably will be ignored anyway by the part which issues the actual proxy,
 * as this is this party responsibility to guarantee uniqueness of serial numbers across all proxies
 * issued.
 * <li> All additional extensions, SAML, tracing and address restrictions are added as Attributes
 * of extensionRequest type (PKCS 9) if are set.
 * <li> Proxy path limit and policy (if set) are wrapped into the proxy extension and then included in
 * the Attributes list (as above). If only one of the values is set then the second receives the default
 * value. If the type is set to the legacy proxy then those parameters are ignored.
 * <li>  There is no way to request a validity time of the generated proxy, therefore the lifetime
 * parameter is ignored.
 * </ul>
 * 
 * @author K. Benedyczak
 */
public class ProxyCSRGenerator
{
	static 
	{
		CertificateUtils.configureSecProvider();
	}

	/**
	 * Generate the proxy certificate object. Use this method if you want to sign 
	 * the proxy with the key which will be autogenerated together with the public part
	 * which will be included in the proxy itself. This method will throw an exception
	 * if used with parameter which has public key manually set.
	 * 
	 * @param param request creation parameters
	 * @return Proxy certificate signing request
	 * @throws InvalidKeyException invalid key exception
	 * @throws SignatureException signature exception
	 * @throws NoSuchAlgorithmException no such algorithm exception
	 * @throws CertificateEncodingException certificate encoding exception
	 * @throws IllegalArgumentException when signingKey is null and public key was manully set
	 */
	public static ProxyCSR generate(ProxyCertificateOptions param) 
			throws InvalidKeyException, SignatureException, NoSuchAlgorithmException,
			CertificateEncodingException
	{
		return generate(param, null);
	}

	
	/**
	 * Generate the proxy certificate object. Use this method if you want to manually 
	 * specify the CSR signing key. This is normally the case when 
	 * the {@link ProxyCertificateOptions} parameter contains a manually set public key.
	 * 
	 * @param param request creation parameters
	 * @param signingKey private key
	 * @return Proxy certificate signing request
	 * @throws InvalidKeyException invalid key exception
	 * @throws SignatureException signature exception
	 * @throws NoSuchAlgorithmException no such algorithm exception
	 * @throws CertificateEncodingException certificate encoding exception
	 * @throws IllegalArgumentException when signingKey is null and public key was manually set
	 */
	public static ProxyCSR generate(ProxyCertificateOptions param, PrivateKey signingKey) 
			throws InvalidKeyException, SignatureException, NoSuchAlgorithmException,
			CertificateEncodingException
	{
		PublicKey pubKey = param.getPublicKey();
		KeyPair keyPair;
		if (pubKey == null)
			keyPair = ProxyGeneratorHelper.generateKeyPair(param.getKeyLength());
		else
			keyPair = new KeyPair(pubKey, null);
		if (signingKey == null)
			signingKey = keyPair.getPrivate();
		if (signingKey == null)
			throw new IllegalArgumentException("Signing (private) key can not be null " +
					"when using a manually set public key");
			
		X509Certificate []chain = param.getParentCertChain();
		ProxyType type = param.getType();
		BigInteger serial = ProxyGeneratorHelper.establishSerial(param);
		X500Name proxySubjectName = ProxyGeneratorHelper.generateDN(chain[0].getSubjectX500Principal(), type, 
				param.isLimited(), serial);
		List<Attribute> attributes = generateAttributes(param);
		
		PKCS10CertificationRequest req;
		try
		{
			ASN1InputStream is = new ASN1InputStream(keyPair.getPublic().getEncoded());
			SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(is.readObject());
			is.close();
			PKCS10CertificationRequestBuilder builder = new PKCS10CertificationRequestBuilder(
					proxySubjectName, subjectPublicKeyInfo);
			for (Attribute attribute: attributes)
				builder.addAttribute(attribute.getAttrType(), attribute.getAttributeValues());
			AlgorithmIdentifier signatureAi = new AlgorithmIdentifier(OIWObjectIdentifiers.sha1WithRSA);
			AlgorithmIdentifier hashAi = new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1);
			BcRSAContentSignerBuilder csBuilder = new BcRSAContentSignerBuilder(signatureAi, hashAi);
			AsymmetricKeyParameter pkParam = PrivateKeyFactory.createKey(signingKey.getEncoded());
			ContentSigner signer = csBuilder.build(pkParam);
			req = builder.build(signer);
		} catch (IOException e)
		{
			throw new InvalidKeyException("Probelm with the proxy CSR private key", e);
		} catch (OperatorCreationException e)
		{
			throw new SignatureException("Problem signing the proxy CSR", e);
		}
		return new ProxyCSRImpl(req, keyPair.getPrivate());
	}
	
	
	
	private static List<Attribute> generateAttributes(ProxyCertificateOptions param)
	{
		List<Attribute> attributes = new ArrayList<Attribute>();

		List<CertificateExtension> additionalExts = param.getExtensions();
		for (CertificateExtension ext: additionalExts)
			addAttribute(attributes, ext);

		ProxyPolicy policy = param.getPolicy();
		int pathLimit = param.getProxyPathLimit();
		if (param.getType() != ProxyType.LEGACY && (policy != null || pathLimit != -1))
		{
			if (policy == null)
				policy = new ProxyPolicy(ProxyPolicy.INHERITALL_POLICY_OID);
			
			String oid = param.getType() == ProxyType.DRAFT_RFC ? 
					DraftRFCProxyCertInfoExtension.DRAFT_EXTENSION_OID 
					: RFCProxyCertInfoExtension.RFC_EXTENSION_OID;
			ProxyCertInfoExtension extValue = param.getType() == ProxyType.DRAFT_RFC ? 
					new DraftRFCProxyCertInfoExtension(pathLimit, policy) : 
					new RFCProxyCertInfoExtension(pathLimit, policy);
			CertificateExtension ext = new CertificateExtension(oid, extValue, true);
			addAttribute(attributes, ext);
		}
		
		if (param.getProxyTracingIssuer() != null)
		{
			ProxyTracingExtension extValue = new ProxyTracingExtension(param.getProxyTracingIssuer());
			CertificateExtension ext = new CertificateExtension(
					ProxyTracingExtension.PROXY_TRACING_ISSUER_EXTENSION_OID, 
					extValue, false);
			addAttribute(attributes, ext);
		}
		if (param.getProxyTracingSubject() != null)
		{
			ProxyTracingExtension extValue = new ProxyTracingExtension(param.getProxyTracingSubject());
			CertificateExtension ext = new CertificateExtension(
					ProxyTracingExtension.PROXY_TRACING_SUBJECT_EXTENSION_OID, 
					extValue, false);
			addAttribute(attributes, ext);
		}
		
		if (param.getSAMLAssertion() != null)
		{
			ProxySAMLExtension extValue = new ProxySAMLExtension(param.getSAMLAssertion());
			CertificateExtension ext = new CertificateExtension(
					ProxySAMLExtension.SAML_OID, extValue, false);
			addAttribute(attributes, ext);
		}
		
		String[] srcExcl = param.getSourceRestrictionExcludedAddresses();
		String[] srcPerm = param.getSourceRestrictionPermittedAddresses();
		if (srcExcl != null || srcPerm != null)
		{
			ProxyAddressRestrictionData extValue = new ProxyAddressRestrictionData();
			if (srcExcl != null)
			{
				for (String addr: srcExcl)
					extValue.addExcludedIPAddressWithNetmask(addr);
			}
			if (srcPerm != null)
			{
				for (String addr: srcPerm)
					extValue.addPermittedIPAddressWithNetmask(addr);
			}
			CertificateExtension ext = new CertificateExtension(
					ProxyAddressRestrictionData.SOURCE_RESTRICTION_OID, extValue, false);
			addAttribute(attributes, ext);
		}

		String[] tgtExcl = param.getTargetRestrictionExcludedAddresses();
		String[] tgtPerm = param.getTargetRestrictionPermittedAddresses();
		if (tgtExcl != null || tgtPerm != null)
		{
			ProxyAddressRestrictionData extValue = new ProxyAddressRestrictionData();
			if (tgtExcl != null)
			{
				for (String addr: tgtExcl)
					extValue.addExcludedIPAddressWithNetmask(addr);
			}
			if (tgtPerm != null)
			{
				for (String addr: tgtPerm)
					extValue.addPermittedIPAddressWithNetmask(addr);
			}
			CertificateExtension ext = new CertificateExtension(
					ProxyAddressRestrictionData.TARGET_RESTRICTION_OID, extValue, false);
			addAttribute(attributes, ext);
		}
		
		return attributes;
	}

	private static void addAttribute(List<Attribute> attributes, ASN1Encodable ext)
	{
		Attribute a = new Attribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, 
				new DERSet(ext));
		attributes.add(a);
	}
}








