/************************************************************************* * * * 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.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.bouncycastle.asn1.cmp.PKIBody; import org.bouncycastle.asn1.cmp.PKIHeader; import org.bouncycastle.asn1.cmp.PKIMessage; import org.bouncycastle.asn1.cmp.PKIMessages; import org.bouncycastle.asn1.util.ASN1Dump; import org.cesecore.authentication.tokens.AuthenticationToken; import org.cesecore.authorization.AuthorizationDeniedException; import org.cesecore.certificates.ca.CAInfo; import org.cesecore.certificates.ca.CaSessionLocal; import org.cesecore.certificates.certificate.request.FailInfo; import org.cesecore.certificates.certificate.request.ResponseMessage; import org.cesecore.configuration.GlobalConfigurationSessionLocal; import org.cesecore.jndi.JndiConstants; import org.cesecore.keys.token.CryptoTokenSessionLocal; import org.ejbca.config.CmpConfiguration; import org.ejbca.core.ejb.EjbBridgeSessionLocal; import org.ejbca.core.ejb.ra.CertificateRequestSessionLocal; import org.ejbca.core.model.InternalEjbcaResources; import org.ejbca.core.protocol.NoSuchAliasException; /** * Class that receives a CMP message and passes it on to the correct message handler. * * ----- * This processes does the following: * 1. receive a CMP message * 2. check which message type it is * 3. dispatch to the correct message handler * 4. send back the response received from the handler * ----- * * Messages supported (see RFC4210 5.3 Operation-Specific Data Structures): * * Implemented: * - 5.3.1 Initialization Request - will return an Initialization Response (-> CertRepMessage). * - 5.3.9 Revocation Request / Response (-> RevRepContent) * - 5.3.17 PKI Confirmation - same as certificate confirmation accept - will return a PKI Confirmation Content (-> PKIConfirmContent) * - 5.3.18 Certificate Confirmation - accept or reject by client - will return a PKI Confirmation Content (-> PKIConfirmContent) * - 5.3.3 Certificate Request / Response (-> CertRepMessage) * - 5.3.5 Key Update Request / Response (-> CertRepMessage) * * Responses of type 'CertRepMessage' may contain additional CA certificates in its 'caPubs' field * which can be configured in the CMP configuration ({@link CmpConfiguration#getResponseCaPubsCA(String)}. * * @version $Id: CmpMessageDispatcherSessionBean.java 28471 2018-03-12 15:11:18Z henriks $ */ @Stateless(mappedName = JndiConstants.APP_JNDI_PREFIX + "CmpMessageDispatcherSessionRemote") public class CmpMessageDispatcherSessionBean implements CmpMessageDispatcherSessionLocal, CmpMessageDispatcherSessionRemote { private static final Logger log = Logger.getLogger(CmpMessageDispatcherSessionBean.class); /** Internal localization of logs and errors */ private static final InternalEjbcaResources intres = InternalEjbcaResources.getInstance(); @EJB private EjbBridgeSessionLocal ejbBridgeSession; @EJB private CertificateRequestSessionLocal certificateRequestSession; @EJB private CryptoTokenSessionLocal cryptoTokenSession; @EJB private CaSessionLocal caSession; @EJB private GlobalConfigurationSessionLocal globalConfigSession; @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public byte[] dispatchRequest(final AuthenticationToken authenticationToken, final byte[] pkiMessageBytes, final String cmpConfigurationAlias) throws NoSuchAliasException { try { final CmpConfiguration cmpConfiguration = (CmpConfiguration) this.globalConfigSession.getCachedConfiguration(CmpConfiguration.CMP_CONFIGURATION_ID); if (!cmpConfiguration.aliasExists(cmpConfigurationAlias)) { final String msg = intres.getLocalizedMessage("protocol.nosuchalias", "CMP", cmpConfigurationAlias); log.info(msg); throw new NoSuchAliasException(msg); } final PKIMessage pkiMessage = CmpMessageHelper.getPkiMessageFromBytes(pkiMessageBytes, false); if (pkiMessage == null) { // Log that we handled a bad request and respond to the client final String msg = intres.getLocalizedMessage("cmp.errornotcmpmessage"); log.info(msg); return CmpMessageHelper.createUnprotectedErrorMessage(msg); } final ResponseMessage responseMessage = dispatch(authenticationToken, pkiMessage, pkiMessage.getHeader(), cmpConfiguration, cmpConfigurationAlias, /*levelOfNesting=*/0); return responseMessage == null ? null : responseMessage.getResponseMessage(); } catch (CertificateEncodingException e) { log.warn("Could not retrieve byte representation of from org.cesecore.certificates.certificate.request.ResponseMessage", e); return null; } } /** * The message may have been received by any transport protocol, and is passed here in it's binary ASN.1 form. * * @param authenticationToken the authentication token. * @param pkiMessage DER encoded CMP message received from the client. * @param pkiHeader DER encoded PKI header of the original CMP message received from the client. * @param cmpConfigurationAlias the CMP alias we want to use for this request. * @param levelOfNesting the level of nesting depth. * @return A response message containing the CMP response message or null if there is no message to send back or some internal error has occurred */ private ResponseMessage dispatch(final AuthenticationToken authenticationToken, final PKIMessage pkiMessage, final PKIHeader pkiHeader, final CmpConfiguration cmpConfiguration, String cmpConfigurationAlias, final int levelOfNesting) { if (levelOfNesting > CmpMessageHelper.MAX_LEVEL_OF_NESTING) { return CmpMessageHelper.createUnprotectedErrorMessage(pkiHeader, FailInfo.BAD_REQUEST, "Rejected request due to unreasonable level of nesting."); } final boolean authenticated = levelOfNesting > 0; try { final PKIBody pkiBody = pkiMessage.getBody(); final int tagno = pkiBody.getType(); if (log.isDebugEnabled()) { final String message = "Received CMP message with pvno=" + pkiHeader.getPvno() + ", sender=" + pkiHeader.getSender().toString() + ", recipient=" + pkiHeader.getRecipient().toString() + System.lineSeparator() + "Cmp configuration alias: " + cmpConfigurationAlias + System.lineSeparator() + "The CMP message is already authenticated: " + authenticated + System.lineSeparator() + "Body is of type: " + tagno + System.lineSeparator() + "Transaction ID: " + pkiHeader.getTransactionID(); log.debug(message); if (log.isTraceEnabled()) { log.trace(ASN1Dump.dumpAsString(pkiMessage)); } } log.info("Dispatching message with transaction ID: " + pkiHeader.getTransactionID()); BaseCmpMessage cmpMessage = null; ICmpMessageHandler handler = null; int unknownMessageType = -1; switch (tagno) { case PKIBody.TYPE_INIT_REQ: // 0: ir, Initialization Request and 2 (cr, Certification Req) are both certificate requests case PKIBody.TYPE_CERT_REQ: // 2: handler = new CrmfMessageHandler(authenticationToken, cmpConfiguration, cmpConfigurationAlias, ejbBridgeSession, certificateRequestSession); cmpMessage = new CrmfRequestMessage(pkiMessage, cmpConfiguration.getCMPDefaultCA(cmpConfigurationAlias), cmpConfiguration.getAllowRAVerifyPOPO(cmpConfigurationAlias), cmpConfiguration.getExtractUsernameComponent(cmpConfigurationAlias)); break; case PKIBody.TYPE_KEY_UPDATE_REQ: // 7: Key Update request (kur, Key Update Request) handler = new CrmfKeyUpdateHandler(authenticationToken, cmpConfiguration, cmpConfigurationAlias, ejbBridgeSession); cmpMessage = new CrmfRequestMessage(pkiMessage, cmpConfiguration.getCMPDefaultCA(cmpConfigurationAlias), cmpConfiguration.getAllowRAVerifyPOPO(cmpConfigurationAlias), cmpConfiguration.getExtractUsernameComponent(cmpConfigurationAlias)); break; case PKIBody.TYPE_CONFIRM: // 19: PKI confirm (pkiconf, Confirmation) case PKIBody.TYPE_CERT_CONFIRM: // 24: Certificate confirmation (certConf, Certificate confirm) handler = new ConfirmationMessageHandler(authenticationToken, cmpConfiguration, cmpConfigurationAlias, ejbBridgeSession, cryptoTokenSession); cmpMessage = new GeneralCmpMessage(pkiMessage); break; case PKIBody.TYPE_REVOCATION_REQ: // 11: Revocation request (rr, Revocation Request) handler = new RevocationMessageHandler(authenticationToken, cmpConfiguration, cmpConfigurationAlias, ejbBridgeSession, cryptoTokenSession); cmpMessage = new GeneralCmpMessage(pkiMessage); break; case PKIBody.TYPE_NESTED: // 20: NestedMessageContent (nested) if (log.isDebugEnabled()) { log.debug("Received a NestedMessageContent"); } final NestedMessageContent nestedMessage = new NestedMessageContent(pkiMessage, cmpConfiguration, cmpConfigurationAlias); if (nestedMessage.verify()) { if (log.isDebugEnabled()) { log.debug("The NestedMessageContent was verified successfully"); } try { final PKIMessages nestedPkiMessages = PKIMessages.getInstance(pkiMessage.getBody().getContent()); final PKIMessage nestedPkiMessage = nestedPkiMessages.toPKIMessageArray()[0]; return dispatch(authenticationToken, nestedPkiMessage, pkiHeader, cmpConfiguration, cmpConfigurationAlias, levelOfNesting+1); } catch (IllegalArgumentException e) { final String errMsg = e.getMessage(); log.info(errMsg, e); return CmpMessageHelper.createUnprotectedErrorMessage(pkiHeader, FailInfo.BAD_REQUEST, errMsg); } } final String errMsg = "Could not verify the RA, signature verification on NestedMessageContent failed."; log.info(errMsg); return CmpMessageHelper.createUnprotectedErrorMessage(pkiHeader, FailInfo.BAD_REQUEST, errMsg); default: unknownMessageType = tagno; log.info("Received an unknown message type, tagno=" + tagno); break; } addAdditionalResponseCaPubsCertificates(authenticationToken, cmpConfiguration, cmpConfigurationAlias, cmpMessage); addAdditionalResponseExtraCertsCertificates(authenticationToken, cmpConfiguration, cmpConfigurationAlias, cmpMessage); if (handler == null || cmpMessage == null) { if (unknownMessageType > -1) { final String eMsg = intres.getLocalizedMessage("cmp.errortypenohandle", Integer.valueOf(unknownMessageType)); log.error(eMsg); return CmpMessageHelper.createUnprotectedErrorMessage(pkiHeader, FailInfo.BAD_REQUEST, eMsg); } throw new IllegalStateException("Something is null! Handler=" + handler + ", cmpMessage=" + cmpMessage); } final ResponseMessage ret = handler.handleMessage(cmpMessage, authenticated); if (ret == null) { log.error(intres.getLocalizedMessage("cmp.errorresponsenull")); } else { if (log.isDebugEnabled()) { log.debug("Received a response message of type '" + ret.getClass().getName() + "' from CmpMessageHandler."); } } return ret; } catch (RuntimeException e) { log.error(intres.getLocalizedMessage("cmp.errorprocess"), e); return null; } } /** * Adds the list of additional CA certificates to the user certificates signing CA certificate to be * returned with the CMP response 'CertRepMessage.caPubs' field. * * @param admin the authentication token. * @param cmpConfiguration the CMP configuration list. * @param alias the CMP configuration alias. * @param message the request message. */ private void addAdditionalResponseCaPubsCertificates(final AuthenticationToken admin, final CmpConfiguration cmpConfiguration, final String alias, final BaseCmpMessage message) { if (message != null) { final String casToAdd = cmpConfiguration.getResponseCaPubsCA(alias); if (log.isDebugEnabled()) { log.debug("Add CA certificates of CAs '" + casToAdd + "' to the CMP response message caPubs field."); } message.setAdditionalCaCertificates(getCaCertificates(admin, casToAdd)); } } /** * Adds the list of additional CA certificates to the message signing CA certificate returned * with the outer PKI response message 'PKIMessage.extraCerts' field. * * @param admin the authentication token. * @param cmpConfiguration the CMP configuration list. * @param alias the CMP configuration alias. * @param message the request message. */ private void addAdditionalResponseExtraCertsCertificates(final AuthenticationToken admin, final CmpConfiguration cmpConfiguration, final String alias, final BaseCmpMessage message) { if (message != null) { final String casToAdd = cmpConfiguration.getResponseExtraCertsCA(alias); if (log.isDebugEnabled()) { log.debug("Add CA certificates of CAs '" + casToAdd + "' to the PKI response message extraCerts field."); } message.setAdditionalExtraCertsCertificates(getCaCertificates(admin, casToAdd)); } } /** * Gets the CA certificates by the semicolon separated string of CA names. * * @param admin the authentication token. * @param caListString the semicolon separated string of CA IDs * @return the list of CA certificates in the order, the CA IDs were given. */ private List getCaCertificates(final AuthenticationToken admin, final String caListString) { final List result = new ArrayList(); CAInfo cainfo = null; Certificate cacert; if (StringUtils.isNotBlank(caListString)) { int caId; for(String ca : StringUtils.split(caListString, ";")) { caId = Integer.parseInt(ca); if(log.isDebugEnabled()) { log.debug("Get CA by ID: " + caId); } try { cainfo = caSession.getCAInfo(admin, caId); if (cainfo != null && CollectionUtils.isNotEmpty(cainfo.getCertificateChain())) { cacert = (X509Certificate) cainfo.getCertificateChain().get(0); if (!result.contains(cacert)) { result.add((X509Certificate) cacert); } } else { // Should never happen. log.info("Cannot find CA: " + ca); } } catch (AuthorizationDeniedException e) { if(log.isDebugEnabled()) { log.debug(e.getMessage()); } } } } return result; } }