EJBCA Sample Plugin
Overview
Signable Material Retriever servce (SMR) ships with default CA and Non CA plugin for the CENM provided Signing Service. These plugins demonstrate the connectivity to the CENM Signing Service which doesn’t persist the signable materials requests, and the data needs to be stored on the plugin’s side (SMR Service). To illustrate the reverse setup, where a signing infrastructure (the replacement of CENM Signing Service) stores internally signable material requests, we provide instruction how to setup EJBCA - the Open Source Certificate Authority, and sample plugin implementation.
EJBCA Web Service Setup
Web service setup follows the same steps as the official documentation. We will setup a local environment as specified in Quick Start Guide. After this step you will be able to access EJBCA Administration UI on specified address.
Next step is to import Corda’s CA. This is done by accessing Certification Authorities tab and using
Import CA keystore… option. The keystore you will need to import for successful CA material signing is
corda-identity-manager-keys.jks
which is contained in Signing Service’s certificates
directory. Keystore’s
password is password
and Alias of signature key is cordaidentitymanagerca
. Leave Alias of encryption key
field empty.
- OID -> 1.3.6.1.4.1.50530.1.1
- Label -> X509_EXTENSION_CORDA_ROLE
- Critical -> No
- Required -> Yes
- Encoding -> DERINTEGER
- Dynamic -> True
- Value -> 4
Now we must set up new certificate profile in order to support Corda compatible certificate issuance. This is done by accessing Certificate Profiles tab and adding new profile. After that edit profile to have following properties set up:
- Type -> Sub CA
- Allow Extension Override -> True
- Allow certificate serial number override -> True
- Allow Key Usage Override -> True
- Key Usage -> Use.., Critical, Digital Signature, Non-repudiation, Key encipherement, Key certificate sign, CRL sign
- CRL Distribution Points -> Use..
- Use CA defined CRL Distribution Point -> Use..
- Used Custom Certificate Extensions -> certificate role you’ve specified before
- Available CAs -> Corda’s CA
At the end we must set up new end entity profile. This is done by accessing End Entity Profiles and adding new profile. After that edit profile to have following properties set up:
- Subject DN Attributes -> Add OU, O, L, C, ST, DC
- Default Certificate Profile -> The one you’ve set up
- Available Certificate Profiles -> The one you’ve set up
- Default CA -> The one you’ve set up
- Available CAs -> The one you’ve set up
- Custom certificate serial number -> Use
- Custom certificate extension data -> Use
Implementation
EJBCA is oriented on CA related type of signable material. This is why the sample plugin implements CASigningPlugin
interface. It also implements ENMLoggable
interface which is our internal logging interface. However this is optional
and it’s added for convenience.
The sample implementation follows the same steps as the official documentation. The suggested approach would be to follow the given link and this document at the same time to fill in the gaps.
start()
method initialises communication with EJBCA Web Service set up prior and EjbcaWS
typed member
ejbcaraws
is used for client methods invocation. Keep in mind you have to export keystore for the user which has
permissions to invoke Web Service API methods. Creation of that user and the keystore export is done via EJBCA
administration UI.
submitCSR()
method takes CertificateSigningRequest
typed argument csr
and based on its contents creates user
for which certificate will be created. Certificate request is done and after that we collect generated certifciate’s
chain since node will only accept chains which root certificate matches the one provided in network root truststore.
submitCRL()
method takes current CRL in form of crl
argument and a Certificate Revocation List to be updated. First of all, revocation of all new revocation requests is performed. After that CRL is updated and
fetched. At the end we form response as specified in interface.
package com.r3.enm.smrplugins.ejbcaplugin;
import com.r3.enm.logging.ENMLoggable;
import com.r3.enm.logging.ILoggingContextWrapper;
import com.r3.enm.logging.LoggingContext;
import com.r3.enm.logging.LoggingContextWrapperFactory;
import com.r3.enm.model.CertificateRevocationRequest;
import com.r3.enm.model.CertificateSigningRequest;
import com.r3.enm.smrpluginapi.ca.CASigningPlugin;
import com.r3.enm.smrpluginapi.ca.CRLResponse;
import com.r3.enm.smrpluginapi.ca.CRLSigningData;
import com.r3.enm.smrpluginapi.ca.CSRResponse;
import com.r3.enm.smrpluginapi.ca.CSRSigningData;
import com.r3.enm.smrpluginapi.common.SMRPluginTerminalException;
import com.r3.enm.smrpluginapi.common.SigningStatus;
import org.cesecore.util.Base64;
import org.cesecore.util.CryptoProviderTools;
import org.ejbca.core.protocol.ws.client.gen.Certificate;
import org.ejbca.core.protocol.ws.client.gen.EjbcaWS;
import org.ejbca.core.protocol.ws.client.gen.EjbcaWSService;
import org.ejbca.core.protocol.ws.client.gen.UserDataVOWS;
import org.ejbca.core.protocol.ws.common.CertificateHelper;
import org.jetbrains.annotations.NotNull;
import net.corda.core.crypto.CryptoUtils;
import javax.annotation.Nullable;
import javax.xml.namespace.QName;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class EJBCASigningPlugin implements CASigningPlugin, ENMLoggable {
private static final String EJBCA_URL = "https://localhost:8443/ejbca/ejbcaws/ejbcaws?wsdl";
// keystore from EJBCA user under role which has permissions to communicate with CA related web service APIs
private static final String KEYSTORE_PATH = "p12/keystore.jks";
private static final String KEYSTORE_PASSWORD = "password";
private static final String CA_NAME = "CordaCA";
private static final String END_ENTITY_PROFILE_NAME = "CENM";
private static final String CERTIFICATE_PROFILE_NAME = "CENM";
private static final String SIGNER_NAMES = "EJBCA";
private static CertificateFactory certificateFactory;
private EjbcaWS ejbcaraws;
static {
try {
certificateFactory = CertificateFactory.getInstance("X509");
} catch (CertificateException e) {
throw new RuntimeException(e);
}
}
@Override
public void start() {
CryptoProviderTools.installBCProvider();
System.setProperty("javax.net.ssl.trustStore", KEYSTORE_PATH);
System.setProperty("javax.net.ssl.trustStorePassword", KEYSTORE_PASSWORD);
System.setProperty("javax.net.ssl.keyStore", KEYSTORE_PATH);
System.setProperty("javax.net.ssl.keyStorePassword", KEYSTORE_PASSWORD);
QName qname = new QName("http://ws.protocol.core.ejbca.org/", "EjbcaWSService");
try {
EjbcaWSService service = new EjbcaWSService(new URL(EJBCA_URL), qname);
ejbcaraws = service.getEjbcaWSPort();
} catch (MalformedURLException e) {
getOpsLogger().error(() -> "Malformed URL provided, shutting down SMR");
throw new SMRPluginTerminalException(e);
} catch (Exception e) {
throw new SMRPluginTerminalException(e);
}
getCtx().withLoggers(getConsoleLogger(), getOpsLogger()).forEach(logger -> logger.info(() -> "EJBCA plugin started"));
}
@Override
public CSRResponse submitCSR(CertificateSigningRequest csr) {
// fill user data
final UserDataVOWS user = new UserDataVOWS();
user.setUsername(csr.getLegalName().toString());
user.setSubjectDN(csr.getLegalName().toString());
user.setCaName(CA_NAME);
user.setEndEntityProfileName(END_ENTITY_PROFILE_NAME);
user.setCertificateProfileName(CERTIFICATE_PROFILE_NAME);
user.setCertificateSerialNumber(BigInteger.valueOf(CryptoUtils.random63BitValue()));
// submit certificate signing request
try {
ejbcaraws.certificateRequest(
user,
new String(Base64.encode(csr.getPkcS10CertificationRequest().getEncoded())),
CertificateHelper.CERT_REQ_TYPE_PKCS10,
null,
CertificateHelper.RESPONSETYPE_CERTIFICATE
);
} catch (Exception e) {
getOpsLogger().error(() -> "Failed to create certificate for " + csr.getLegalName().toString());
throw new RuntimeException(e);
}
getConsoleLogger().debug(() -> "Received certificate for " + csr.getLegalName().toString());
getOpsLogger().info(() -> "Received certificate for " + csr.getLegalName().toString());
// retrieve certificate chain for generated certificate
List<Certificate> certChain;
try {
certChain = ejbcaraws.getLastCertChain(user.getUsername());
} catch (Exception e) {
getOpsLogger().error(() -> "Failed to retrieve certificate chain for certificate under name " +
csr.getLegalName().toString());
throw new RuntimeException(e);
}
getConsoleLogger().debug(() -> "Received certificate chain for certificate under name " + csr.getLegalName().toString());
getOpsLogger().info(() -> "Received certificate chain for certificate under name " + csr.getLegalName().toString());
// form proper response to be passed to SMR
try {
return new CSRResponse(SigningStatus.COMPLETED, new CSRSigningData(
certificateFactory.generateCertPath(certChain.stream().map(cert -> {
try {
return certificateFactory.generateCertificate(new ByteArrayInputStream(cert.getRawCertificateData()));
} catch (CertificateException e) {
getOpsLogger().error(() -> "Failed to generate certificate from raw data");
throw new RuntimeException(e);
}
}
).collect(Collectors.toList())),
SIGNER_NAMES
));
} catch (CertificateException e) {
getOpsLogger().error(() -> "Failed to generate certificate path from raw data");
throw new RuntimeException(e);
}
}
@Override
public CRLResponse submitCRL(@Nullable X509CRL crl, Set<CertificateRevocationRequest> newCRRs) {
// revoke certificates
Set<CertificateRevocationRequest> revokedRequests = new HashSet<>();
newCRRs.forEach(crr -> {
try {
ejbcaraws.revokeCert(
crr.getCertificate().getIssuerDN().getName(),
crr.getCertificate().getSubjectDN().getName(),
crr.getReason().ordinal()
);
revokedRequests.add(crr);
getOpsLogger().info(() -> "Revoked certificate under name " + crr.getCertificate().getSubjectDN().getName() +
" issued by " + crr.getCertificate().getIssuerDN().getName());
} catch (Exception e) {
getOpsLogger().info(() -> "Failed to revoke certificate under name " + crr.getCertificate().getSubjectDN().getName() +
" issued by " + crr.getCertificate().getIssuerDN().getName() + " due to " + e.getMessage());
}
});
// create new certificate revocation list
try {
ejbcaraws.createCRL(CA_NAME);
} catch (Exception e) {
getOpsLogger().error(() -> "Failed to generate new CRL");
throw new RuntimeException(e);
}
getConsoleLogger().debug(() -> "CRL generated");
getOpsLogger().info(() -> "CRL generated");
// retrieve created certificate revocation list
byte[] newCrl;
try {
newCrl = ejbcaraws.getLatestCRL(CA_NAME, false);
} catch (Exception e) {
getOpsLogger().error(() -> "Failed to fetch CRL");
throw new RuntimeException(e);
}
getConsoleLogger().debug(() -> "CRL fetched");
getOpsLogger().info(() -> "CRL fetched");
// form proper response to be passed to SMR
try {
return new CRLResponse(SigningStatus.COMPLETED, new CRLSigningData(
(X509CRL) certificateFactory.generateCRL(new ByteArrayInputStream(newCrl)),
SIGNER_NAMES,
Instant.now(),
revokedRequests
));
} catch (CRLException e) {
getOpsLogger().error(() -> "Failed to generate CRL from raw data");
throw new RuntimeException(e);
}
}
@NotNull
@Override
public LoggingContext getCtx() {
return new LoggingContext("EJBCAPlugin", "", null, new LoggingContextWrapperFactory());
}
@NotNull
@Override
public ILoggingContextWrapper getOpsLogger() {
return getCtx().getOps();
}
@NotNull
@Override
public ILoggingContextWrapper getAuditLogger() {
return getCtx().getAudit();
}
@NotNull
@Override
public ILoggingContextWrapper getDevLogger() {
return getCtx().getDev();
}
@NotNull
@Override
public ILoggingContextWrapper getConsoleLogger() {
return getCtx().getConsole();
}
}
Running EJBCA plugin
To run the plugin you simply need to specify its JAR path for CSR and CRL material management tasks in SMR’s
configuration. The class name to configure is com.r3.enm.smrplugins.ejbcaplugin.EJBCASigningPlugin
.
You run SMR as per usual with following command:
java -jar smr-<VERSION>.jar --config-file <CONFIG_FILE>
On success you should see a message similar to:
EJBCA plugin started
SMR Service started
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.