From 0543c68c74af7e92d9bf1d350b850e612bd4028a Mon Sep 17 00:00:00 2001 From: Neil Crossley Date: Mon, 19 Aug 2019 12:55:01 +0200 Subject: [PATCH] Add pre-existing code for certificate management protocol. --- ssa-server/server/pom.xml | 6 + .../java/reqesidta/ssa/api/SsaService.java | 3 + .../ssa/sa/CertificateAuthorityClient.java | 357 ++++++++++++++++++ .../config/CertificateAuthorityConfig.java | 72 ++++ .../ssa/server/config/SSAConfig.java | 26 +- .../server/src/main/resources/reference.conf | 10 +- 6 files changed, 464 insertions(+), 10 deletions(-) create mode 100644 ssa-server/server/src/main/java/reqesidta/ssa/sa/CertificateAuthorityClient.java create mode 100644 ssa-server/server/src/main/java/reqesidta/ssa/server/config/CertificateAuthorityConfig.java diff --git a/ssa-server/server/pom.xml b/ssa-server/server/pom.xml index 5c5517d..4aae294 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 44d4eca..d03602b 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 0000000..94559d3 --- /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 0000000..0edae19 --- /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 5c1cd45..7fc54c0 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 d941c00..4db4425 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 -- GitLab