/*************************************************************************
* *
* EJBCA: The OpenSource Certificate Authority *
* *
* This software is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or any later version. *
* *
* See terms of license at gnu.org. *
* *
*************************************************************************/
package org.ejbca.core.model.ca.caadmin.extendedcaservices;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.Certificate;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Logger;
import org.bouncycastle.cms.CMSEnvelopedData;
import org.bouncycastle.cms.CMSEnvelopedDataGenerator;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSSignedGenerator;
import org.bouncycastle.cms.RecipientId;
import org.bouncycastle.cms.RecipientInformation;
import org.bouncycastle.cms.RecipientInformationStore;
import org.ejbca.config.EjbcaConfiguration;
import org.ejbca.core.model.InternalResources;
import org.ejbca.core.model.ca.caadmin.CA;
import org.ejbca.core.model.ca.caadmin.IllegalKeyStoreException;
import org.ejbca.core.model.ca.certificateprofiles.XKMSCertificateProfile;
import org.ejbca.core.model.ra.UserDataVO;
import org.ejbca.util.Base64;
import org.ejbca.util.CertTools;
import org.ejbca.util.CryptoProviderTools;
import org.ejbca.util.StringTools;
import org.ejbca.util.keystore.KeyTools;
/** Handles and maintains the CA-part of the CMS message functionality.
* The service have it's own certificate used for signing and encryption
*
* @version $Id: CmsCAService.java 11731 2011-04-13 17:52:27Z jeklund $
*/
public class CmsCAService extends ExtendedCAService implements java.io.Serializable{
/** Determines if a de-serialized file is compatible with this class.
*
* Maintainers must change this value if and only if the new version
* of this class is not compatible with old versions. See Sun docs
* for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
* /serialization/spec/version.doc.html> details. </a>
*/
private static final long serialVersionUID = 5273836489592921586L;
private static Logger m_log = Logger.getLogger(CmsCAService.class);
/** Internal localization of logs and errors */
private static final InternalResources intres = InternalResources.getInstance();
public static final float LATEST_VERSION = 2;
public static final String SERVICENAME = "CMSCASERVICE";
private PrivateKey privKey = null;
private List<Certificate> certificatechain = null;
private CmsCAServiceInfo info = null;
private static final String KEYSTORE = "keystore";
private static final String KEYSPEC = "keyspec";
private static final String KEYALGORITHM = "keyalgorithm";
private static final String SUBJECTDN = "subjectdn";
private static final String SUBJECTALTNAME = "subjectaltname";
private static final String PRIVATESIGNKEYALIAS = "privatesignkeyalias";
public CmsCAService(final ExtendedCAServiceInfo serviceinfo) {
m_log.debug("CmsCAService : constructor " + serviceinfo.getStatus());
CryptoProviderTools.installBCProviderIfNotAvailable();
// Currently only RSA keys are supported
final CmsCAServiceInfo info = (CmsCAServiceInfo) serviceinfo;
data = new HashMap();
data.put(ExtendedCAServiceInfo.IMPLEMENTATIONCLASS, this.getClass().getName()); // For integration with CESeCore
data.put(EXTENDEDCASERVICETYPE, Integer.valueOf(ExtendedCAServiceInfo.TYPE_CMSEXTENDEDSERVICE)); // For current version of EJBCA
data.put(KEYSPEC, info.getKeySpec());
data.put(KEYALGORITHM, info.getKeyAlgorithm());
setSubjectDN(info.getSubjectDN());
setSubjectAltName(info.getSubjectAltName());
setStatus(serviceinfo.getStatus());
data.put(VERSION, new Float(LATEST_VERSION));
}
public CmsCAService(final HashMap data) throws IllegalArgumentException, IllegalKeyStoreException {
CryptoProviderTools.installBCProviderIfNotAvailable();
loadData(data);
if (data.get(KEYSTORE) != null) {
// lookup keystore passwords
final String keystorepass = StringTools.passwordDecryption(EjbcaConfiguration.getCaCmsKeyStorePass(), "ca.cmskeystorepass");
int status = ExtendedCAServiceInfo.STATUS_INACTIVE;
try {
m_log.debug("Loading CMS keystore");
final KeyStore keystore = KeyStore.getInstance("PKCS12", "BC");
keystore.load(new java.io.ByteArrayInputStream(Base64.decode(((String) data.get(KEYSTORE)).getBytes())),keystorepass.toCharArray());
m_log.debug("Finished loading CMS keystore");
this.privKey = (PrivateKey) keystore.getKey(PRIVATESIGNKEYALIAS, null);
// Due to a bug in Glassfish v1 (fixed in v2), we used to have to make sure all certificates in this
// Array were of SUNs own provider, using CertTools.SYSTEM_SECURITY_PROVIDER.
// As of EJBCA 3.9.3 we decided that we don't have to support Glassfish v1 anymore.
this.certificatechain = CertTools.getCertCollectionFromArray(keystore.getCertificateChain(PRIVATESIGNKEYALIAS), null);
status = getStatus();
} catch (Exception e) {
m_log.error("Could not load keystore or certificate for CA CMS service. Perhaps the password was changed? " + e.getMessage());
} finally {
this.info = new CmsCAServiceInfo(status, getSubjectDN(), getSubjectAltName(), (String)data.get(KEYSPEC),
(String) data.get(KEYALGORITHM), this.certificatechain);
}
data.put(EXTENDEDCASERVICETYPE, Integer.valueOf(ExtendedCAServiceInfo.TYPE_CMSEXTENDEDSERVICE));
}
}
@Override
public void init(final CA ca) throws Exception {
m_log.debug("CmsCAService : init");
// lookup keystore passwords
final String keystorepass = StringTools.passwordDecryption(EjbcaConfiguration.getCaCmsKeyStorePass(), "ca.cmskeystorepass");
// Currently only RSA keys are supported
final CmsCAServiceInfo info = (CmsCAServiceInfo) getExtendedCAServiceInfo();
// Create KeyStore
final KeyStore keystore = KeyStore.getInstance("PKCS12", "BC");
keystore.load(null, null);
final KeyPair cmskeys = KeyTools.genKeys(info.getKeySpec(), info.getKeyAlgorithm());
final Certificate certificate =
ca.generateCertificate(new UserDataVO("NOUSERNAME", info.getSubjectDN(), 0, info.getSubjectAltName(), "NOEMAIL", 0,0,0,0, null,null,0,0,null),
cmskeys.getPublic(),
-1, // KeyUsage
ca.getValidity(),
new XKMSCertificateProfile(), // We can use the (simple) XKMS profile, since it uses the same values as we want for CMS
null // sequence
);
certificatechain = new ArrayList<Certificate>();
certificatechain.add(certificate);
certificatechain.addAll(ca.getCertificateChain());
this.privKey = cmskeys.getPrivate();
keystore.setKeyEntry(PRIVATESIGNKEYALIAS,cmskeys.getPrivate(),null,(Certificate[]) certificatechain.toArray(new Certificate[certificatechain.size()]));
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
keystore.store(baos, keystorepass.toCharArray());
data.put(KEYSTORE, new String(Base64.encode(baos.toByteArray())));
// Store KeyStore
setStatus(info.getStatus());
this.info = new CmsCAServiceInfo(info.getStatus(), getSubjectDN(), getSubjectAltName(), (String)data.get(KEYSPEC), (String) data.get(KEYALGORITHM), certificatechain);
}
@Override
public void update(final ExtendedCAServiceInfo serviceinfo, final CA ca) throws Exception {
final CmsCAServiceInfo info = (CmsCAServiceInfo) serviceinfo;
m_log.debug("CmsCAService : update " + serviceinfo.getStatus());
setStatus(serviceinfo.getStatus());
if (info.getRenewFlag()) {
// Renew The Signers certificate.
this.init(ca);
}
// Only status is updated
this.info = new CmsCAServiceInfo(serviceinfo.getStatus(), getSubjectDN(), getSubjectAltName(), (String) data.get(KEYSPEC), (String) data.get(KEYALGORITHM), certificatechain);
}
@Override
public ExtendedCAServiceResponse extendedService(final ExtendedCAServiceRequest request) throws ExtendedCAServiceRequestException, IllegalExtendedCAServiceRequestException,ExtendedCAServiceNotActiveException {
m_log.trace(">extendedService");
if (!(request instanceof CmsCAServiceRequest)) {
throw new IllegalExtendedCAServiceRequestException();
}
if (this.getStatus() != ExtendedCAServiceInfo.STATUS_ACTIVE) {
String msg = intres.getLocalizedMessage("caservice.notactive");
m_log.error(msg);
throw new ExtendedCAServiceNotActiveException(msg);
}
ExtendedCAServiceResponse returnval = null;
final X509Certificate signerCert = (X509Certificate) certificatechain.get(0);
final CmsCAServiceRequest serviceReq = (CmsCAServiceRequest)request;
// Create the signed data
final CMSSignedDataGenerator gen1 = new CMSSignedDataGenerator();
try {
byte[] resp = serviceReq.getDoc();
// Add our signer info and sign the message
if ((serviceReq.getMode() & CmsCAServiceRequest.MODE_SIGN) != 0) {
final CertStore certs = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certificatechain), "BC");
gen1.addCertificatesAndCRLs(certs);
gen1.addSigner(privKey, signerCert, CMSSignedGenerator.DIGEST_SHA1);
final CMSProcessable msg = new CMSProcessableByteArray(resp);
final CMSSignedData s = gen1.generate(msg, true, "BC");
resp = s.getEncoded();
}
if ((serviceReq.getMode() & CmsCAServiceRequest.MODE_ENCRYPT) != 0) {
CMSEnvelopedDataGenerator edGen = new CMSEnvelopedDataGenerator();
edGen.addKeyTransRecipient(getCMSCertificate());
CMSEnvelopedData ed = edGen.generate(new CMSProcessableByteArray(resp),CMSEnvelopedDataGenerator.DES_EDE3_CBC,"BC");
resp = ed.getEncoded();
}
if ((serviceReq.getMode() & CmsCAServiceRequest.MODE_DECRYPT) != 0) {
CMSEnvelopedData ed = new CMSEnvelopedData(resp);
RecipientInformationStore recipients = ed.getRecipientInfos();
RecipientId id = new RecipientId();
id.setIssuer(getCMSCertificate().getIssuerX500Principal());
id.setSerialNumber(getCMSCertificate().getSerialNumber());
RecipientInformation recipient = recipients.get(id);
if (recipient != null) {
resp = recipient.getContent(this.privKey, "BC");
}
}
returnval = new CmsCAServiceResponse(resp);
} catch (InvalidAlgorithmParameterException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
} catch (NoSuchAlgorithmException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
} catch (NoSuchProviderException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
} catch (CertStoreException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
} catch (CMSException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
} catch (IOException e) {
m_log.error("Error in CmsCAService", e);
throw new ExtendedCAServiceRequestException(e);
}
m_log.trace("<extendedService");
return returnval;
}
private X509Certificate cmsCertificate = null;
private X509Certificate getCMSCertificate() {
if(cmsCertificate == null){
Iterator<Certificate> iter = certificatechain.iterator();
while (iter.hasNext()) {
final X509Certificate cert = (X509Certificate) iter.next();
if (cert.getBasicConstraints() == -1) {
cmsCertificate = cert;
break;
}
}
}
return cmsCertificate;
}
@Override
public float getLatestVersion() {
return LATEST_VERSION;
}
@Override
public void upgrade() {
if (Float.compare(LATEST_VERSION, getVersion()) != 0) {
// New version of the class, upgrade
data.put(ExtendedCAServiceInfo.IMPLEMENTATIONCLASS, this.getClass().getName()); // For integration with CESeCore
data.put(VERSION, new Float(LATEST_VERSION));
}
}
@Override
public ExtendedCAServiceInfo getExtendedCAServiceInfo() {
if (info == null) {
info = new CmsCAServiceInfo(getStatus(), getSubjectDN(), getSubjectAltName(), (String) data.get(KEYSPEC), (String) data.get(KEYALGORITHM), certificatechain);
}
return this.info;
}
private String getSubjectDN() {
String retval = null;
final String str = (String)data.get(SUBJECTDN);
try {
retval = new String(Base64.decode((str).getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
m_log.error("Could not decode data from Base64",e);
} catch (ArrayIndexOutOfBoundsException e) {
// This is an old CA, where it's not Base64encoded
m_log.debug("Old non base64 encoded DN: "+str);
retval = str;
}
return retval;
}
private void setSubjectDN(final String dn) {
try {
data.put(SUBJECTDN,new String(Base64.encode(dn.getBytes("UTF-8"),false)));
} catch (UnsupportedEncodingException e) {
m_log.error("Could not encode data from Base64",e);
}
}
private String getSubjectAltName() {
String retval = null;
final String str = (String) data.get(SUBJECTALTNAME);
try {
retval = new String(Base64.decode((str).getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
m_log.error("Could not decode data from Base64",e);
} catch (ArrayIndexOutOfBoundsException e) {
// This is an old CA, where it's not Base64encoded
m_log.debug("Old non base64 encoded altname: "+str);
retval = str;
}
return retval;
}
private void setSubjectAltName(final String dn) {
try {
data.put(SUBJECTALTNAME,new String(Base64.encode(dn.getBytes("UTF-8"), false)));
} catch (UnsupportedEncodingException e) {
m_log.error("Could not encode data from Base64",e);
}
}
}