Commit 0543c68c authored by Neil Crossley's avatar Neil Crossley
Browse files

Add pre-existing code for certificate management protocol.

parent 19e35d72
......@@ -87,6 +87,12 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.62</version>
</dependency>
<!--Test-->
<dependency>
<groupId>org.testng</groupId>
......
......@@ -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);
......
/** **************************************************************************
* 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<Date> 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<Date> 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;
}
}
/****************************************************************************
* 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;
}
}
/****************************************************************************
/** **************************************************************************
* 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;
/**
......@@ -16,6 +16,7 @@ package reqesidta.ssa.server.config;
public class SSAConfig {
private int testInt;
private CertificateAuthorityConfig caConfig;
public int getTestInt() {
return testInt;
......@@ -25,4 +26,11 @@ public class SSAConfig {
this.testInt = testInt;
}
public CertificateAuthorityConfig getCaConfig() {
return caConfig;
}
public void setCaConfig(CertificateAuthorityConfig caConfig) {
this.caConfig = caConfig;
}
}
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
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment