/*
* Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package eu.emi.security.authn.x509.helpers.proxy;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import eu.emi.security.authn.x509.helpers.CertificateHelpers;
import eu.emi.security.authn.x509.proxy.BaseProxyCertificateOptions;
import eu.emi.security.authn.x509.proxy.CertificateExtension;
import eu.emi.security.authn.x509.proxy.ProxyCertificate;
import eu.emi.security.authn.x509.proxy.ProxyCertificateOptions;
import eu.emi.security.authn.x509.proxy.ProxyGenerator;
import eu.emi.security.authn.x509.proxy.ProxyPolicy;
import eu.emi.security.authn.x509.proxy.ProxyRequestOptions;
import eu.emi.security.authn.x509.proxy.ProxyType;
/**
* Actual implementation of the Proxy generation. The object is for one use only,
* i.e. it should not be reused to generate first certificate. It is strongly suggested
* to use {@link ProxyGenerator}.
*
* @author K. Benedyczak
*/
public class ProxyGeneratorHelper
{
private SubjectPublicKeyInfo proxyPublicKeyInfo = null;
private transient PrivateKey proxyPrivateKey = null;
private X509v3CertificateBuilder certBuilder;
private X509Certificate proxy;
/**
* Generate the proxy certificate object from the local certificate.
*
* @param param proxy parameters
* @param privateKey key to sign the proxy
* @return a newly created proxy certificate, wrapped together with a private key
* if it was also generated.
* @throws InvalidKeyException invalid key exception
* @throws SignatureException signature exception
* @throws NoSuchAlgorithmException no such algorithm exception
* @throws CertificateParsingException certificate parsing exception
* @throws IOException IO exception
*/
public ProxyCertificate generate(ProxyCertificateOptions param,
PrivateKey privateKey) throws InvalidKeyException,
SignatureException, NoSuchAlgorithmException,
CertificateParsingException, IOException
{
establishKeys(param);
return generateCommon(param, privateKey);
}
/**
* Generate the proxy certificate object from the received Certificate Signing Request.
*
* @param param proxy parameters
* @param privateKey key to sign the proxy
* @return chain with the new proxy on the first position
* @throws InvalidKeyException invalid key exception
* @throws SignatureException signature exception
* @throws NoSuchAlgorithmException no such algorithm exception
* @throws CertificateParsingException certificate encoding exception
* @throws IOException IO exception
*/
public X509Certificate[] generate(ProxyRequestOptions param,
PrivateKey privateKey) throws InvalidKeyException,
SignatureException, NoSuchAlgorithmException,
CertificateParsingException, IOException
{
PKCS10CertificationRequest csr = param.getProxyRequest();
proxyPublicKeyInfo = csr.getSubjectPublicKeyInfo();
return generateCommon(param, privateKey).getCertificateChain();
}
private ProxyCertificate generateCommon(BaseProxyCertificateOptions param,
PrivateKey privateKey) throws InvalidKeyException,
SignatureException, NoSuchAlgorithmException,
CertificateParsingException, IOException
{
setupCertBuilder(param);
addExtensions(param);
try
{
buildCertificate(param.getParentCertChain()[0], privateKey);
} catch (NoSuchProviderException e)
{
throw new RuntimeException("Default signature provider " +
"is not available? A bug or serious JDK misconfiguration.", e);
} catch (IOException e)
{
throw new CertificateParsingException("Can not encode the certificate " +
"to the binary DER form", e);
}
return wrapResult(param.getParentCertChain());
}
private void establishKeys(ProxyCertificateOptions param) throws InvalidKeyException
{
PublicKey proxyPublicKey = param.getPublicKey();
proxyPrivateKey = null;
if (proxyPublicKey == null)
{
KeyPair pair = ProxyGeneratorHelper.generateKeyPair(param.getKeyLength());
proxyPublicKey = pair.getPublic();
proxyPrivateKey = pair.getPrivate();
}
try
{
ASN1InputStream asn1IS = new ASN1InputStream(proxyPublicKey.getEncoded());
proxyPublicKeyInfo = SubjectPublicKeyInfo.getInstance(asn1IS.readObject());
asn1IS.close();
} catch (IOException e)
{
throw new InvalidKeyException("Can not parse the public key" +
"being included in the proxy certificate", e);
}
}
private void setupCertBuilder(BaseProxyCertificateOptions param) throws InvalidKeyException
{
X509Certificate issuingCert = param.getParentCertChain()[0];
Date notBefore = param.getNotBefore();
Date notAfter = new Date(notBefore.getTime() + param.getLifetime()*1000L);
BigInteger serial = establishSerial(param);
X500Name issuer = CertificateHelpers.toX500Name(issuingCert.getSubjectX500Principal());
X500Name subject = ProxyGeneratorHelper.generateDN(issuingCert.getSubjectX500Principal(),
param.getType(), param.isLimited(), serial);
certBuilder = new X509v3CertificateBuilder(
issuer,
serial,
notBefore,
notAfter,
subject,
proxyPublicKeyInfo);
}
/**
* If the input chain has no KeyUsage extension null is returned. If at least one certificate in the chain
* has the Key Usage extension then a KeyUsage is returned which contains bitwise AND of KeyUsage flags
* from all certificates.
* The CA certificates are ignored in the computation.
* @param chain certificate chain
* @return chain key usage
*/
public static Integer getChainKeyUsage(X509Certificate[] chain)
{
int flags = 0xFF | KeyUsage.decipherOnly;
boolean found = false;
for (X509Certificate cert: chain)
{
//skip certs which are cA
if (cert.getBasicConstraints() != -1)
continue;
boolean[] certKu = cert.getKeyUsage();
if (certKu == null)
continue;
found = true;
int certKuInt = 0;
for (int i=0; i<certKu.length; i++)
{
if (!certKu[i])
continue;
int bit = (i == 8) ? KeyUsage.decipherOnly : (1 << (7-i));
certKuInt |= bit;
}
flags &= certKuInt;
}
return found ? flags : null;
}
private KeyUsage establishKeyUsage(BaseProxyCertificateOptions param)
{
Integer parentKU = getChainKeyUsage(param.getParentCertChain());
int retMask;
if (parentKU == null)
{
retMask = param.getProxyKeyUsageMask() < 0 ? BaseProxyCertificateOptions.DEFAULT_KEY_USAGE :
param.getProxyKeyUsageMask();
} else
{
retMask = param.getProxyKeyUsageMask() < 0 ? parentKU :
param.getProxyKeyUsageMask() & parentKU;
}
return new KeyUsage(retMask);
}
private void addExtensions(BaseProxyCertificateOptions param) throws IOException
{
KeyUsage ks = establishKeyUsage(param);
certBuilder.addExtension(Extension.keyUsage, true, ks);
if (param.getType() != ProxyType.LEGACY)
{
ProxyPolicy policy = param.getPolicy();
if (policy == null)
policy = new ProxyPolicy(ProxyPolicy.INHERITALL_POLICY_OID);
String oid;
ASN1Object extValue;
if (param.getType() == ProxyType.DRAFT_RFC)
{
oid = DraftRFCProxyCertInfoExtension.DRAFT_EXTENSION_OID;
extValue = new DraftRFCProxyCertInfoExtension(param.getProxyPathLimit(), policy);
} else
{
oid = RFCProxyCertInfoExtension.RFC_EXTENSION_OID;
extValue = new RFCProxyCertInfoExtension(param.getProxyPathLimit(), policy);
}
certBuilder.addExtension(new ASN1ObjectIdentifier(oid),
true, extValue);
}
if (param.getProxyTracingIssuer() != null)
{
ProxyTracingExtension extValue = new ProxyTracingExtension(param.getProxyTracingIssuer());
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxyTracingExtension.PROXY_TRACING_ISSUER_EXTENSION_OID),
false, extValue);
}
if (param.getProxyTracingSubject() != null)
{
ProxyTracingExtension extValue = new ProxyTracingExtension(param.getProxyTracingSubject());
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxyTracingExtension.PROXY_TRACING_SUBJECT_EXTENSION_OID),
false, extValue);
}
if (param.getSAMLAssertion() != null)
{
ProxySAMLExtension extValue = new ProxySAMLExtension(param.getSAMLAssertion());
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxySAMLExtension.SAML_OID),
false, extValue);
}
if (param.getAttributeCertificates() != null)
{
ProxyACExtension extValue = new ProxyACExtension(param.getAttributeCertificates());
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxyACExtension.AC_OID),
false, extValue);
}
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);
}
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxyAddressRestrictionData.SOURCE_RESTRICTION_OID),
false, extValue);
}
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);
}
certBuilder.addExtension(new ASN1ObjectIdentifier(ProxyAddressRestrictionData.TARGET_RESTRICTION_OID),
false, extValue);
}
List<CertificateExtension> additionalExts = param.getExtensions();
for (CertificateExtension ext: additionalExts)
certBuilder.addExtension(new ASN1ObjectIdentifier(ext.getOid()),
ext.isCritical(), ext.getValue());
}
private void buildCertificate(X509Certificate issuingCert, PrivateKey privateKey)
throws CertificateParsingException, InvalidKeyException, NoSuchProviderException, NoSuchAlgorithmException, SignatureException, IOException
{
AlgorithmIdentifier sigAlg;
try
{
sigAlg = X509v3CertificateBuilder.extractAlgorithmId(
issuingCert);
} catch (IOException e)
{
throw new CertificateParsingException("Can not parse parameters of the " +
"public key contained in the issuer certificate", e);
}
String sigAlgName = issuingCert.getSigAlgName();
proxy = certBuilder.build(privateKey,
sigAlg,
sigAlgName,
null,
null);
}
private ProxyCertificate wrapResult(X509Certificate []originalChain)
throws InvalidKeyException
{
X509Certificate []extendedChain = new X509Certificate[originalChain.length + 1];
for (int i=0; i<originalChain.length; i++)
extendedChain[i+1] = originalChain[i];
extendedChain[0] = proxy;
if (proxyPrivateKey != null)
{
try
{
return new ProxyCertificateImpl(extendedChain, proxyPrivateKey);
} catch (KeyStoreException e)
{
throw new InvalidKeyException("The generated private key is unsupported, bug?", e);
}
} else
return new ProxyCertificateImpl(extendedChain);
}
/**
* For LEGACY proxies returns the serial from the issuing certificate.
* For the Draft/rfc proxies returns the manually set serial, or generateas a
* random one if not set.
* @param param proxy certificate options
* @return serial number
*/
public static BigInteger establishSerial(BaseProxyCertificateOptions param)
{
if (param.getType() == ProxyType.LEGACY)
return param.getParentCertChain()[0].getSerialNumber();
if (param.getSerialNumber() != null)
return param.getSerialNumber();
SecureRandom rand = new SecureRandom();
return BigInteger.valueOf(rand.nextInt()).abs();
}
/**
* Generate a correct DN for the proxy, depending on its type.
* @param parentSubject parent subject
* @param type proxy type
* @param limited true if limited proxy
* @param serial serial number
* @return generated proxy DN
*/
public static X500Name generateDN(X500Principal parentSubject, ProxyType type, boolean limited,
BigInteger serial)
{
String cn;
if (type == ProxyType.LEGACY)
cn = limited ? "limited proxy" : "proxy";
else
cn = serial.toString();
X500Name dn = CertificateHelpers.toX500Name(parentSubject);
AttributeTypeAndValue ava = new AttributeTypeAndValue(BCStyle.CN, new DERPrintableString(cn));
RDN added = new RDN(ava);
RDN []orig = dn.getRDNs();
RDN []proxyRDNs = new RDN[orig.length + 1];
for (int i=0; i<orig.length; i++)
proxyRDNs[i] = orig[i];
proxyRDNs[orig.length] = added;
return new X500Name(proxyRDNs);
}
public static KeyPair generateKeyPair(int len)
{
KeyPairGenerator kpGen;
try
{
kpGen = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e)
{
throw new IllegalStateException("RSA algorithm not supported!?", e);
}
kpGen.initialize(len, new SecureRandom());
return kpGen.generateKeyPair();
}
}