/*************************************************************************
* *
* EJBCA Community: 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.protocol.cmp;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CRL;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.cmp.CMPCertificate;
import org.bouncycastle.asn1.cmp.CMPObjectIdentifiers;
import org.bouncycastle.asn1.cmp.CertOrEncCert;
import org.bouncycastle.asn1.cmp.CertRepMessage;
import org.bouncycastle.asn1.cmp.CertResponse;
import org.bouncycastle.asn1.cmp.CertifiedKeyPair;
import org.bouncycastle.asn1.cmp.ErrorMsgContent;
import org.bouncycastle.asn1.cmp.InfoTypeAndValue;
import org.bouncycastle.asn1.cmp.PKIBody;
import org.bouncycastle.asn1.cmp.PKIFreeText;
import org.bouncycastle.asn1.cmp.PKIHeader;
import org.bouncycastle.asn1.cmp.PKIHeaderBuilder;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.cmp.PKIStatus;
import org.bouncycastle.asn1.cmp.PKIStatusInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.crmf.CRMFException;
import org.bouncycastle.cert.crmf.jcajce.JceCRMFEncryptorBuilder;
import org.bouncycastle.cms.CMSAlgorithm;
import org.bouncycastle.cms.CMSSignedGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.jcajce.JceAsymmetricKeyWrapper;
import org.cesecore.certificates.certificate.Base64CertData;
import org.cesecore.certificates.certificate.CertificateData;
import org.cesecore.certificates.certificate.request.CertificateResponseMessage;
import org.cesecore.certificates.certificate.request.FailInfo;
import org.cesecore.certificates.certificate.request.RequestMessage;
import org.cesecore.certificates.certificate.request.ResponseStatus;
import org.cesecore.util.CertTools;
/**
* CMP certificate response message
*
* @version $Id: CmpResponseMessage.java 28875 2018-05-08 13:44:40Z anatom $
*/
public class CmpResponseMessage implements CertificateResponseMessage {
/**
* 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 details.
*
*/
static final long serialVersionUID = 10003L;
private static final Logger log = Logger.getLogger(CmpResponseMessage.class);
/** The encoded response message */
private byte[] responseMessage = null;
/** status for the response */
private ResponseStatus status = ResponseStatus.SUCCESS;
/** Possible fail information in the response. Defaults to 'badRequest (2)'. */
private FailInfo failInfo = FailInfo.BAD_REQUEST;
/** Possible clear text error information in the response. Defaults to null. */
private String failText = null;
/**
* SenderNonce. This is base64 encoded bytes
*/
private String senderNonce = null;
/**
* RecipientNonce in a response is the senderNonce from the request. This is base64 encoded bytes
*/
private String recipientNonce = null;
/** transaction id */
private String transactionId = null;
/** Default digest algorithm for CMP response message, can be overridden */
private String digest = CMSSignedGenerator.DIGEST_SHA1;
/** The default provider is BC, if nothing else is specified when setting SignKeyInfo */
private String provider = BouncyCastleProvider.PROVIDER_NAME;
/** Certificate to be in certificate response message, not serialized */
private transient Certificate cert = null;
/** The CA certificate to be included in the response message to be used to verify the end entity certificate */
private transient List cacert = new ArrayList();
/** Certificate for the signer of the response message (CA) */
private transient Collection signCertChain = null;
/** Additions CA certificate for the outer PKI response message extraCerts field. */
private transient Collection extraCerts = new ArrayList();
/** Private key used to sign the response message */
private transient PrivateKey signKey = null;
/** The request message this response is for */
private transient ICrmfRequestMessage reqMsg;
/** used to choose response body type */
private transient int requestType;
/** used to match request with response */
private transient int requestId;
private transient int pbeIterationCount = 1024;
private transient String pbeDigestAlg = null;
private transient String pbeMacAlg = null;
private transient String pbeKeyId = null;
private transient String pbeKey = null;
private transient boolean implicitConfirm = false;
private transient CertificateData certificateData;
private transient Base64CertData base64CertData;
@Override
public CertificateData getCertificateData() {
return certificateData;
}
@Override
public void setCertificateData(CertificateData certificateData) {
if (certificateData != null) {
this.certificateData = new CertificateData(certificateData);
} else {
this.certificateData = null;
}
}
@Override
public Base64CertData getBase64CertData() {
return base64CertData;
}
@Override
public void setBase64CertData(final Base64CertData base64CertData) {
if (base64CertData != null) {
this.base64CertData = new Base64CertData(base64CertData);
} else {
this.base64CertData = null;
}
}
@Override
public Certificate getCertificate() {
try {
return CertTools.getCertfromByteArray(cert.getEncoded(), Certificate.class);
} catch (CertificateEncodingException e) {
throw new IllegalStateException("Could not encode certificate. This should not happen", e);
} catch (CertificateException e) {
throw new IllegalStateException("Response was created without containing valid certificate. This should not happen", e);
}
}
@Override
public void setCertificate(Certificate cert) {
this.cert = cert;
}
@Override
public void setCrl(CRL crl) {
}
@Override
public void setIncludeCACert(boolean incCACert) {
}
@Override
public void setCACert(Certificate cACert) {
this.cacert.clear();
this.cacert.add(cACert);
}
@Override
public byte[] getResponseMessage() throws CertificateEncodingException {
return responseMessage;
}
@Override
public void setStatus(ResponseStatus status) {
this.status = status;
}
@Override
public ResponseStatus getStatus() {
return status;
}
@Override
public void setFailInfo(FailInfo failInfo) {
this.failInfo = failInfo;
}
@Override
public FailInfo getFailInfo() {
return failInfo;
}
@Override
public void setFailText(String failText) {
this.failText = failText;
}
@Override
public String getFailText() {
return this.failText;
}
@Override
public boolean create() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException {
boolean ret = false;
// Some general stuff, common for all types of messages
String issuer = null;
String subject = null;
Certificate signCert = null;
if (CollectionUtils.isNotEmpty(signCertChain)) {
signCert = signCertChain.iterator().next();
}
if (cert != null) {
final X509Certificate x509cert = (X509Certificate) cert;
issuer = x509cert.getIssuerDN().getName();
subject = x509cert.getSubjectDN().getName();
} else if (signCert != null) {
issuer = ((X509Certificate) signCert).getSubjectDN().getName();
subject = "CN=fooSubject";
} else {
issuer = "CN=fooIssuer";
subject = "CN=fooSubject";
}
final GeneralName issuerName = new GeneralName(new X500Name(issuer));
final GeneralName subjectName = new GeneralName(new X500Name(subject));
final PKIHeaderBuilder myPKIHeader = CmpMessageHelper.createPKIHeaderBuilder(issuerName, subjectName, senderNonce, recipientNonce, transactionId);
PKIBody myPKIBody = null;
final PKIMessage myPKIMessage;
try {
if (status.equals(ResponseStatus.SUCCESS)) {
if (cert != null) {
if (log.isDebugEnabled()) {
log.debug("Creating a CertRepMessage 'accepted'");
}
PKIStatusInfo myPKIStatusInfo = new PKIStatusInfo(PKIStatus.granted); // 0 = accepted
ASN1InputStream certASN1InputStream = new ASN1InputStream(new ByteArrayInputStream(cert.getEncoded()));
try {
try {
CMPCertificate cmpcert = CMPCertificate.getInstance(certASN1InputStream.readObject());
CertOrEncCert retCert = new CertOrEncCert(cmpcert);
CertifiedKeyPair myCertifiedKeyPair;
// If the requestMessage has a server generated key pair, and the requestMessage had a public key
// "controls.protocolEncrKey" to encrypt the private key with
if (reqMsg != null && reqMsg.getServerGenKeyPair() != null && reqMsg.getProtocolEncrKey() != null) {
log.debug("CMP request had a server generated key pair and controls.protocolEncrKey which we will use to encrypt the private key in the response");
final KeyPair kp = reqMsg.getServerGenKeyPair();
final PublicKey protocolEncrKey = reqMsg.getProtocolEncrKey();
if (!protocolEncrKey.getAlgorithm().equals("RSA")) {
final String msg = "CMP request had a controls.protocolEncrKey that is not an RSA key, can not create response: "+protocolEncrKey.getAlgorithm();
log.debug(msg);
throw new InvalidKeyException(msg);
}
// JceAsymmetricKeyWrapper sets kp.getPublic to be the key used for wrapping
// JceCRMFEncryptorBuilder sets AES256 CBC to be the symmetric encryption algorithm used
JcaEncryptedValueBuilder encBldr = new JcaEncryptedValueBuilder(
new JceAsymmetricKeyWrapper(protocolEncrKey).setProvider(BouncyCastleProvider.PROVIDER_NAME),
new JceCRMFEncryptorBuilder(CMSAlgorithm.AES256_CBC).setProvider(BouncyCastleProvider.PROVIDER_NAME).build());
// encBldr.build encrypts the privateKey using the wrapper above, i.e. encrypted with AES128_CBC with the symmkey wrapped with kp.getPublic
myCertifiedKeyPair = new CertifiedKeyPair(retCert, encBldr.build(kp.getPrivate()), null);
} else if (reqMsg != null && reqMsg.getServerGenKeyPair() != null && reqMsg.getProtocolEncrKey() == null) {
// We should actually check this in the outer CMP layers before trying to create a real certificate response, but of course we have to check in here as well
final String msg = "CMP request had a server generated key pair but no controls.protocolEncrKey, can not create response";
log.debug(msg);
throw new InvalidKeyException(msg);
} else {
myCertifiedKeyPair = new CertifiedKeyPair(retCert);
}
// If we have server generated keys, add privateKey
final CertResponse certResponse = new CertResponse(new ASN1Integer(requestId), myPKIStatusInfo, myCertifiedKeyPair, null);
final CertResponse[] certResponses = { certResponse };
// Add the user certificates signing CA certificate (at index 0) and the others by the CMP configuration to the CMP
// response 'caPubs' field (added previously to the response with CertificateResponseMessage.addAdditionalCaCertificates().
final List caPubs = new ArrayList();
for (Certificate certificate : this.cacert) {
try (ASN1InputStream stream = new ASN1InputStream(new ByteArrayInputStream(certificate.getEncoded()));) {
caPubs.add(CMPCertificate.getInstance(stream.readObject()));
}
}
final CertRepMessage myCertRepMessage = new CertRepMessage(caPubs.toArray( new CMPCertificate[] {}), certResponses);
int respType = requestType + 1; // 1 = intitialization response, 3 = certification response etc
if (log.isDebugEnabled()) {
log.debug("Creating response body of type " + respType);
}
myPKIBody = new PKIBody(respType, myCertRepMessage);
// All good, see if we should add implicitConfirm
if (implicitConfirm) {
if (log.isDebugEnabled()) {
log.debug("Adding implicitConform to CMP response message with transId: "+transactionId);
}
final InfoTypeAndValue genInfo = new InfoTypeAndValue(CMPObjectIdentifiers.it_implicitConfirm);
myPKIHeader.setGeneralInfo(genInfo);
}
} finally {
certASN1InputStream.close();
}
} catch (IOException e) {
throw new IllegalStateException("Unexpected IOException caught.", e);
}
}
} else if (status.equals(ResponseStatus.FAILURE)) {
if (log.isDebugEnabled()) {
log.debug("Creating a CertRepMessage 'rejected'");
}
// Create a failure message
ASN1EncodableVector statusInfoV = new ASN1EncodableVector();
statusInfoV.add(ASN1Integer.getInstance(PKIStatus.rejection.toASN1Primitive()));
if (failText != null) {
statusInfoV.add(new PKIFreeText(new DERUTF8String(failText)));
}
statusInfoV.add(CmpMessageHelper.getPKIFailureInfo(failInfo.intValue()));
PKIStatusInfo myPKIStatusInfo = PKIStatusInfo.getInstance(ASN1Sequence.getInstance(new DERSequence(statusInfoV)));
myPKIBody = CmpMessageHelper.createCertRequestRejectBody(myPKIStatusInfo, requestId, requestType);
} else {
if (log.isDebugEnabled()) {
log.debug("Creating a 'waiting' message?");
}
// Not supported, lets create a PKIError failure instead
// Create a failure message
ASN1EncodableVector statusInfoV = new ASN1EncodableVector();
statusInfoV.add(PKIStatus.rejection); // 2 = rejection
if (failText != null) {
statusInfoV.add(new PKIFreeText(new DERUTF8String(failText)));
}
statusInfoV.add(CmpMessageHelper.getPKIFailureInfo(failInfo.intValue()));
PKIStatusInfo myPKIStatusInfo = PKIStatusInfo.getInstance(new DERSequence(statusInfoV));
ErrorMsgContent myErrorContent = new ErrorMsgContent(myPKIStatusInfo);
myPKIBody = new PKIBody(23, myErrorContent); // 23 = error
}
if ((pbeKeyId != null) && (pbeKey != null) && (pbeDigestAlg != null) && (pbeMacAlg != null)) {
myPKIHeader.setProtectionAlg(new AlgorithmIdentifier(CMPObjectIdentifiers.passwordBasedMac));
PKIHeader header = myPKIHeader.build();
myPKIMessage = new PKIMessage(header, myPKIBody);
responseMessage = CmpMessageHelper.protectPKIMessageWithPBE(myPKIMessage, pbeKeyId, pbeKey, pbeDigestAlg, pbeMacAlg,
pbeIterationCount);
} else {
myPKIHeader.setProtectionAlg(new AlgorithmIdentifier(new ASN1ObjectIdentifier(digest)));
if (signCert != null) {
// set sender Key ID as well when the response is signed, so the signer (CA) can have multiple certificates out there
// with the same DN but different keys
myPKIHeader.setSenderKID(CertTools.getSubjectKeyId(signCert));
}
PKIHeader header = myPKIHeader.build();
final Collection extraCertsList = new ArrayList(signCertChain);
for (Certificate extraCert : extraCerts) {
if (!extraCertsList.contains(extraCert)) {
extraCertsList.add(extraCert);
}
}
myPKIMessage = new PKIMessage(header, myPKIBody);
responseMessage = CmpMessageHelper.signPKIMessage(myPKIMessage, extraCertsList, signKey, digest, provider);
}
ret = true;
} catch (CertificateEncodingException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (InvalidKeyException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (NoSuchProviderException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (NoSuchAlgorithmException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (SecurityException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (SignatureException e) {
log.error("Error creating CertRepMessage: ", e);
} catch (CRMFException e) {
log.error("Error creating CertRepMessage: ", e);
}
return ret;
}
@Override
public boolean requireSignKeyInfo() {
return true;
}
@Override
public void setSignKeyInfo(Collection certs, PrivateKey key, String provider) {
this.signCertChain = certs;
this.signKey = key;
if (provider != null) {
this.provider = provider;
}
}
@Override
public void setSenderNonce(String senderNonce) {
this.senderNonce = senderNonce;
}
@Override
public void setRecipientNonce(String recipientNonce) {
this.recipientNonce = recipientNonce;
}
@Override
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
@Override
public void setRecipientKeyInfo(byte[] recipientKeyInfo) {
}
@Override
public void setPreferredDigestAlg(String digest){
if(!StringUtils.isEmpty(digest)) {
this.digest = digest;
}
}
@Override
public void setRequestType(int reqtype) {
this.requestType = reqtype;
}
@Override
public void setRequestId(int reqid) {
this.requestId = reqid;
}
@Override
public void setProtectionParamsFromRequest(RequestMessage reqMsg) {
if (reqMsg instanceof ICrmfRequestMessage) {
ICrmfRequestMessage crmf = (ICrmfRequestMessage) reqMsg;
this.reqMsg = crmf;
this.pbeIterationCount = crmf.getPbeIterationCount();
this.pbeDigestAlg = crmf.getPbeDigestAlg();
this.pbeMacAlg = crmf.getPbeMacAlg();
this.pbeKeyId = crmf.getPbeKeyId();
this.pbeKey = crmf.getPbeKey();
this.implicitConfirm = crmf.isImplicitConfirm();
}
}
@Override
public void addAdditionalCaCertificates(final List certificates) {
if (CollectionUtils.isNotEmpty(certificates)) {
for (Certificate certificate : certificates) {
if (!this.cacert.contains(certificate)) {
this.cacert.add( certificate);
}
}
}
}
@Override
public void addAdditionalResponseExtraCertsCertificates(final List certificates) {
if (CollectionUtils.isNotEmpty(certificates)) {
for (Certificate certificate : certificates) {
if (!this.extraCerts.contains(certificate)) {
this.extraCerts.add(certificate);
}
}
}
}
}