diff --git a/ssa-server/server/pom.xml b/ssa-server/server/pom.xml
index 5c5517da55fd9e3145e6f58c857eef923ef17b78..4aae294dec27adbfb10338dd2d9955aeff6ce0f8 100644
--- a/ssa-server/server/pom.xml
+++ b/ssa-server/server/pom.xml
@@ -87,6 +87,12 @@
runtime
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ 1.62
+
+
org.testng
diff --git a/ssa-server/server/src/main/java/reqesidta/ssa/api/SsaService.java b/ssa-server/server/src/main/java/reqesidta/ssa/api/SsaService.java
index 44d4eca09f5dd09f65ff2c313abaf735cb825b01..d03602b626e910dda7f97f91abfe1b5f5e1f3c3d 100644
--- a/ssa-server/server/src/main/java/reqesidta/ssa/api/SsaService.java
+++ b/ssa-server/server/src/main/java/reqesidta/ssa/api/SsaService.java
@@ -20,6 +20,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import reqesidta.ssa.sa.CertificateAuthorityClient;
import reqesidta.ssa.server.config.SSAConfig;
/**
@@ -33,6 +34,8 @@ public class SsaService {
@Inject SSAConfig config;
+ @Inject CertificateAuthorityClient caClient;
+
public SsaService() {
JsonbConfig config = new JsonbConfig()
.withBinaryDataStrategy(BinaryDataStrategy.BASE_64);
diff --git a/ssa-server/server/src/main/java/reqesidta/ssa/sa/CertificateAuthorityClient.java b/ssa-server/server/src/main/java/reqesidta/ssa/sa/CertificateAuthorityClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..94559d3d41347a33a1a431c8a9467a356de30a8c
--- /dev/null
+++ b/ssa-server/server/src/main/java/reqesidta/ssa/sa/CertificateAuthorityClient.java
@@ -0,0 +1,357 @@
+/** **************************************************************************
+ * Copyright (C) 2019 ecsec GmbH.
+ * All rights reserved.
+ * Contact: ecsec GmbH (info@ecsec.de)
+ *
+ * This file is part of SkIDentity.
+ *
+ * This file may be used in accordance with the terms and conditions
+ * contained in a signed written agreement between you and ecsec GmbH.
+ *
+ ************************************************************************** */
+package reqesidta.ssa.sa;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.Key;
+import java.security.NoSuchProviderException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Optional;
+import java.util.Random;
+import javax.inject.Inject;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+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.DEROutputStream;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.DERUTF8String;
+import org.bouncycastle.asn1.cmp.CMPCertificate;
+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.PKIBody;
+import org.bouncycastle.asn1.cmp.PKIMessage;
+import org.bouncycastle.asn1.cmp.RevDetails;
+import org.bouncycastle.asn1.cmp.RevReqContent;
+import org.bouncycastle.asn1.crmf.CertTemplateBuilder;
+import org.bouncycastle.asn1.util.ASN1Dump;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.CRLReason;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.ExtensionsGenerator;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.cmp.CMPException;
+import org.bouncycastle.cert.cmp.ProtectedPKIMessage;
+import org.bouncycastle.cert.cmp.ProtectedPKIMessageBuilder;
+import org.bouncycastle.cert.crmf.CRMFException;
+import org.bouncycastle.cert.crmf.CertificateRequestMessage;
+import org.bouncycastle.cert.crmf.CertificateRequestMessageBuilder;
+import org.bouncycastle.cert.crmf.PKMACBuilder;
+import org.bouncycastle.cert.crmf.jcajce.JcePKMACValuesCalculator;
+import org.bouncycastle.operator.MacCalculator;
+import reqesidta.ssa.server.config.CertificateAuthorityConfig;
+import reqesidta.ssa.server.config.SSAConfig;
+
+/**
+ *
+ * @author Neil Crossley
+ */
+public class CertificateAuthorityClient {
+
+ private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(CertificateAuthorityClient.class);
+
+ private CertificateAuthorityConfig caConfig;
+
+ @Inject
+ public CertificateAuthorityClient(SSAConfig config) {
+ this.caConfig = config.getCaConfig();
+ }
+
+ public X509Certificate createCertificateForUser(Key pubKey, String KeyId, Optional validUntil) {
+ return this.CMPRAWithSharedSecret(pubKey, KeyId, this.caConfig.getCertUserCn(), validUntil);
+ }
+
+ public void revoke(BigInteger serialNo, int reasonCode) {
+ try {
+ LOG.debug("Revoke certificate: " + serialNo);
+ //https://www.ejbca.org/docs/adminguide.html#Interoperability
+
+ byte[] r1 = new byte[20];
+ new Random().nextBytes(r1);
+ byte[] r2 = new byte[20];
+ new Random().nextBytes(r2);
+
+ final byte[] senderNonce = r1;
+ final byte[] transactionId = r2;
+ X500Name issuerDN = new X500Name("CN=" + this.caConfig.getCaName());
+ X500Name userDN = new X500Name("CN=sender");
+ // Cert template too tell which cert we want to revoke
+ CertTemplateBuilder myCertTemplate = new CertTemplateBuilder();
+ myCertTemplate.setIssuer(issuerDN);
+ myCertTemplate.setSubject(userDN);
+ myCertTemplate.setSerialNumber(new ASN1Integer(serialNo));
+ // Extension telling revocation reason
+ ExtensionsGenerator extgen = new ExtensionsGenerator();
+ CRLReason crlReason = CRLReason.lookup(reasonCode);
+ extgen.addExtension(Extension.reasonCode, false, crlReason);
+ Extensions exts = extgen.generate();
+ ASN1EncodableVector v = new ASN1EncodableVector();
+ v.add(myCertTemplate.build());
+ v.add(exts);
+ ASN1Sequence seq = new DERSequence(v);
+ RevDetails myRevDetails = RevDetails.getInstance(seq);
+ RevReqContent myRevReqContent = new RevReqContent(myRevDetails);
+ PKIBody myPKIBody = new PKIBody(PKIBody.TYPE_REVOCATION_REQ, myRevReqContent); // revocation request
+ // Message protection and final message
+ GeneralName sender = new GeneralName(userDN);
+ GeneralName recipient = new GeneralName(issuerDN);
+ ProtectedPKIMessageBuilder pbuilder = new ProtectedPKIMessageBuilder(sender, recipient);
+ pbuilder.setMessageTime(new Date());
+ // senderNonce
+ pbuilder.setSenderNonce(senderNonce);
+ // TransactionId
+ pbuilder.setTransactionID(transactionId);
+ // Key Id used (required) by the recipient to do a lot of stuff
+ pbuilder.setSenderKID("KeyId".getBytes());
+ pbuilder.setBody(myPKIBody);
+ JcePKMACValuesCalculator jcePkmacCalc = new JcePKMACValuesCalculator();
+ final AlgorithmIdentifier digAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.3.14.3.2.26")); // SHA1
+ final AlgorithmIdentifier macAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.2.7")); // HMAC/SHA1
+ jcePkmacCalc.setup(digAlg, macAlg);
+ PKMACBuilder macbuilder = new PKMACBuilder(jcePkmacCalc);
+ MacCalculator macCalculator = macbuilder.build(getCmpPassword());
+ ProtectedPKIMessage message = pbuilder.build(macCalculator);
+
+ ByteArrayOutputStream bao = new ByteArrayOutputStream();
+ DEROutputStream out = new DEROutputStream(bao);
+ out.writeObject(message.toASN1Structure());
+ byte[] ba = bao.toByteArray();
+
+ byte[] ret = sendCmpHttp(ba, this.caConfig.getCmpAlias());
+
+ // check response message
+ try (ASN1InputStream asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(ret))) {
+ PKIMessage respObject = PKIMessage.getInstance(asn1InputStream.readObject());
+ PKIBody body = respObject.getBody();
+
+ ASN1Sequence seqOuter = ASN1Sequence.getInstance(body.getContent().toASN1Primitive());
+ ASN1Sequence seqIn = ASN1Sequence.getInstance(seqOuter.getObjectAt(0).toASN1Primitive());
+ ASN1Sequence seqInIn = ASN1Sequence.getInstance(seqIn.getObjectAt(0).toASN1Primitive());
+ ASN1Integer type = ASN1Integer.getInstance(seqInIn.getObjectAt(0));
+
+ // see: https://www.ejbca.org/older_releases/ejbca_6_5/htdocs/docs/adminguide.html CMP Error Messages
+ switch (type.getValue().intValue()) {
+ case 0:
+ String msg = String.format("Revoking of certificate with serial number '%s' was successfull.",
+ serialNo.toString());
+ LOG.debug(msg);
+ break;
+ case 2:
+ ASN1Sequence seqMsg = ASN1Sequence.getInstance(seqInIn.getObjectAt(1));
+ DERUTF8String nameData = DERUTF8String.getInstance(seqMsg.getObjectAt(0));
+ String errorMsg = String.format("Revoking certificate, BAD_REQUEST: %s", nameData.getString());
+ LOG.error(errorMsg);
+ break;
+ default:
+ String errorMsg2 = String.format("Revoking certificate, ERROR: %s", ASN1Dump.dumpAsString(seqInIn));
+ LOG.error(errorMsg2);
+ break;
+ }
+ }
+ } catch (IOException | CMPException | CRMFException | URISyntaxException e) {
+ LOG.error("EJBCA-Error: Unable to revoke Certificate: {}", serialNo, e);
+
+ throw createInternalError();
+ }
+ }
+
+ private static WebApplicationException createInternalError() {
+ return new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build());
+ }
+
+ private char[] getCmpPassword() {
+ return this.caConfig.getCmpPassword().toCharArray();
+ }
+
+ private X509Certificate CMPRAWithSharedSecret(Key pubKey, String KeyId, String name, Optional validUntil) {
+ try {
+ final BigInteger certReqId = BigInteger.valueOf(1);
+ byte[] r1 = new byte[20];
+ new Random().nextBytes(r1);
+ byte[] r2 = new byte[20];
+ new Random().nextBytes(r2);
+ final byte[] senderNonce = r1;
+ final byte[] transactionId = r2;
+
+ CertificateRequestMessageBuilder msgbuilder = new CertificateRequestMessageBuilder(certReqId);
+ X500Name issuerDN = new X500Name("CN=" + this.caConfig.getCaName());
+ X500Name subjectDN = new X500Name("CN=" + this.caConfig.getCertPrefixCn() + name);
+
+ msgbuilder.setSubject(new X500Name("CN=" + this.caConfig.getCertPrefixCn() + name));
+
+ validUntil.ifPresent(vu -> msgbuilder.setValidity(null, vu));
+
+ final byte[] bytes = pubKey.getEncoded();
+ final ByteArrayInputStream bIn = new ByteArrayInputStream(bytes);
+ SubjectPublicKeyInfo keyInfo = null;
+ try (ASN1InputStream dIn = new ASN1InputStream(bIn)) {
+ keyInfo = SubjectPublicKeyInfo.getInstance(dIn.readObject());
+ } catch (IOException ex) {
+ String msg = "Unable ro read PublicKey for: " + KeyId;
+ LOG.error(msg, ex);
+
+ throw createInternalError();
+ }
+ msgbuilder.setPublicKey(keyInfo);
+
+ GeneralName sender = new GeneralName(subjectDN);
+ msgbuilder.setAuthInfoSender(sender);
+ msgbuilder.setProofOfPossessionRaVerified();
+
+ CertificateRequestMessage msg = msgbuilder.build();
+ org.bouncycastle.asn1.crmf.CertReqMessages msgs = new org.bouncycastle.asn1.crmf.CertReqMessages(msg.toASN1Structure());
+ org.bouncycastle.asn1.cmp.PKIBody pkibody = new org.bouncycastle.asn1.cmp.PKIBody(org.bouncycastle.asn1.cmp.PKIBody.TYPE_INIT_REQ, msgs);
+ GeneralName recipient = new GeneralName(issuerDN);
+ ProtectedPKIMessageBuilder pbuilder = new ProtectedPKIMessageBuilder(sender, recipient);
+ pbuilder.setMessageTime(new Date());
+ pbuilder.setSenderNonce(senderNonce);
+ pbuilder.setTransactionID(transactionId);
+ pbuilder.setSenderKID(KeyId.getBytes());
+ pbuilder.setBody(pkibody);
+ JcePKMACValuesCalculator jcePkmacCalc = new JcePKMACValuesCalculator();
+ final AlgorithmIdentifier digAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.3.14.3.2.26"));
+ final AlgorithmIdentifier macAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.2.7"));
+ jcePkmacCalc.setup(digAlg, macAlg);
+ PKMACBuilder macbuilder = new PKMACBuilder(jcePkmacCalc);
+ MacCalculator macCalculator = macbuilder.build(getCmpPassword());
+ ProtectedPKIMessage message = pbuilder.build(macCalculator);
+ return getCertificate(message, this.caConfig.getCmpAlias());
+ } catch (CRMFException | CMPException ex) {
+ String msg = "EJBCA-Error: Unable to create Certificate for key "
+ + KeyId
+ + " with Algorithm "
+ + pubKey.getAlgorithm();
+ LOG.error(msg, ex);
+
+ throw createInternalError();
+ }
+ }
+
+ private X509Certificate getCertificate(PKIMessage message, String alias) {
+ try {
+ ByteArrayOutputStream bao = new ByteArrayOutputStream();
+ DEROutputStream out = new DEROutputStream(bao);
+ out.writeObject(message);
+ byte[] ba = bao.toByteArray();
+
+ byte[] ret = sendCmpHttp(ba, alias);
+
+ ASN1InputStream asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(ret));
+ PKIMessage respObject = null;
+ try {
+ respObject = PKIMessage.getInstance(asn1InputStream.readObject());
+ } finally {
+ asn1InputStream.close();
+ }
+
+ PKIBody body = respObject.getBody();
+
+ if (body.getContent() instanceof CertRepMessage) {
+ CertRepMessage c = (CertRepMessage) body.getContent();
+ CertResponse resp = c.getResponse()[0];
+
+ CertifiedKeyPair kp = resp.getCertifiedKeyPair();
+ CertOrEncCert cc = kp.getCertOrEncCert();
+
+ final CMPCertificate cmpcert = cc.getCertificate();
+
+ CertificateFactory instance = CertificateFactory.getInstance("X.509", "BC");
+ Certificate generateCertificate = instance.generateCertificate(new ByteArrayInputStream(cmpcert.getEncoded()));
+ final X509Certificate cert = (X509Certificate) generateCertificate;
+ LOG.info("Certificate: ------------------");
+ LOG.info("Subject: {}", cert.getSubjectDN().getName());
+ LOG.info("Issuer: {}", cert.getIssuerDN().getName());
+ LOG.info("Validity: {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSSX").format(cert.getNotAfter()));
+ LOG.info("Used algorithm: {}", cert.getSigAlgName());
+ LOG.info("SerialNumber: {}", cert.getSerialNumber().toString());
+ return cert;
+ } else {
+ ErrorMsgContent error = (ErrorMsgContent) body.getContent();
+
+ String msg = "EJBCA-Error: " + error.getPKIStatusInfo().getStatusString().getStringAt(0).getString();
+ LOG.error(msg);
+
+ throw createInternalError();
+ }
+
+ } catch (IOException | CertificateParsingException ex) {
+ String msg = "EJBCA-Error: Can't read received CMP message.";
+ LOG.error(msg, ex);
+
+ throw createInternalError();
+ } catch (CertificateException | NoSuchProviderException ex) {
+ String msg = "EJBCA-Error: Unable to parse received certificate.";
+ LOG.error(msg, ex);
+
+ throw createInternalError();
+ } catch (URISyntaxException ex) {
+ LOG.error("Unable to parse given certificate authority url.", ex);
+
+ throw createInternalError();
+ }
+ }
+
+ private X509Certificate getCertificate(ProtectedPKIMessage message, String alias) {
+ return getCertificate(message.toASN1Structure(), alias);
+ }
+
+ private byte[] sendCmpHttp(byte[] message, String alias) throws IOException, URISyntaxException {
+ final var targetUri = new URI(this.caConfig.getBaseUrl()).resolve("./publicweb/cmp/" + alias);
+
+ URL url = targetUri.toURL();
+ final HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setDoOutput(true);
+ con.setRequestMethod("POST");
+ con.setRequestProperty("Content-type", "application/pkixcmp");
+ con.connect();
+ try (OutputStream os = con.getOutputStream()) {
+ os.write(message);
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (InputStream in = con.getInputStream()) {
+ int b = in.read();
+ while (b != -1) {
+ baos.write(b);
+ b = in.read();
+ }
+ baos.flush();
+ }
+ byte[] respBytes = baos.toByteArray();
+ return respBytes;
+ }
+
+}
diff --git a/ssa-server/server/src/main/java/reqesidta/ssa/server/config/CertificateAuthorityConfig.java b/ssa-server/server/src/main/java/reqesidta/ssa/server/config/CertificateAuthorityConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..0edae1902f7f18c59a739981ced23484c62457cd
--- /dev/null
+++ b/ssa-server/server/src/main/java/reqesidta/ssa/server/config/CertificateAuthorityConfig.java
@@ -0,0 +1,72 @@
+/****************************************************************************
+ * Copyright (C) 2019 ecsec GmbH.
+ * All rights reserved.
+ * Contact: ecsec GmbH (info@ecsec.de)
+ *
+ * This file may be used in accordance with the terms and conditions
+ * contained in a signed written agreement between you and ecsec GmbH.
+ *
+ ***************************************************************************/
+package reqesidta.ssa.server.config;
+
+/**
+ *
+ * @author Neil Crossley
+ */
+public class CertificateAuthorityConfig {
+
+ private String caName;
+ private String cmpAlias;
+ private String cmpPassword;
+ private String baseUrl;
+ private String certUserCn;
+ private String certPrefixCn;
+
+ public String getCaName() {
+ return caName;
+ }
+
+ public void setCaName(String caName) {
+ this.caName = caName;
+ }
+
+ public String getCmpAlias() {
+ return cmpAlias;
+ }
+
+ public void setCmpAlias(String cmpAlias) {
+ this.cmpAlias = cmpAlias;
+ }
+
+ public String getCmpPassword() {
+ return cmpPassword;
+ }
+
+ public void setCmpPassword(String cmpPassword) {
+ this.cmpPassword = cmpPassword;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getCertUserCn() {
+ return certUserCn;
+ }
+
+ public void setCertUserCn(String certUserCn) {
+ this.certUserCn = certUserCn;
+ }
+
+ public String getCertPrefixCn() {
+ return certPrefixCn;
+ }
+
+ public void setCertPrefixCn(String certPrefixCn) {
+ this.certPrefixCn = certPrefixCn;
+ }
+}
diff --git a/ssa-server/server/src/main/java/reqesidta/ssa/server/config/SSAConfig.java b/ssa-server/server/src/main/java/reqesidta/ssa/server/config/SSAConfig.java
index 5c1cd45e10ce95d2baa0131174e2b1e6882062ba..7fc54c03296adb070bca07acd50c428b4e9369d9 100644
--- a/ssa-server/server/src/main/java/reqesidta/ssa/server/config/SSAConfig.java
+++ b/ssa-server/server/src/main/java/reqesidta/ssa/server/config/SSAConfig.java
@@ -1,4 +1,4 @@
-/****************************************************************************
+/** **************************************************************************
* Copyright (C) 2019 ecsec GmbH.
* All rights reserved.
* Contact: ecsec GmbH (info@ecsec.de)
@@ -6,7 +6,7 @@
* This file may be used in accordance with the terms and conditions
* contained in a signed written agreement between you and ecsec GmbH.
*
- ***************************************************************************/
+ ************************************************************************** */
package reqesidta.ssa.server.config;
/**
@@ -15,14 +15,22 @@ package reqesidta.ssa.server.config;
*/
public class SSAConfig {
- private int testInt;
+ private int testInt;
+ private CertificateAuthorityConfig caConfig;
- public int getTestInt() {
- return testInt;
- }
+ public int getTestInt() {
+ return testInt;
+ }
- public void setTestInt(int testInt) {
- this.testInt = testInt;
- }
+ public void setTestInt(int testInt) {
+ this.testInt = testInt;
+ }
+ public CertificateAuthorityConfig getCaConfig() {
+ return caConfig;
+ }
+
+ public void setCaConfig(CertificateAuthorityConfig caConfig) {
+ this.caConfig = caConfig;
+ }
}
diff --git a/ssa-server/server/src/main/resources/reference.conf b/ssa-server/server/src/main/resources/reference.conf
index d941c001e53d839fe94824128d2c6f6d13abb4f2..4db4425e55b6a201c9aa3ef235459669fe1b66b9 100644
--- a/ssa-server/server/src/main/resources/reference.conf
+++ b/ssa-server/server/src/main/resources/reference.conf
@@ -1,3 +1,11 @@
ssa-config {
- test-int: 1
+ test-int: 1,
+ ca-config: {
+ caName: 'dummy-caName',
+ cmpAlias: 'dummy-cmp-alias',
+ cmpPassword: 'dummy-cmp-password',
+ baseUrl: 'dummy-baseUrl',
+ certUserCn: 'dummy-certUserCn',
+ certPrefixCn: 'dummy-certPrefixCn'
+ }
}
\ No newline at end of file