Skip to content

Commit

Permalink
[HWORKS-795] Create Java keystore and send it back as part of signing…
Browse files Browse the repository at this point in the history
… request response (#1417)

* [HWORKS-795] Create Java keystore and send it back as part of signing request response

[HWORKS-795] Test create keystores

* [HWORKS-795] Remove forgotten imports
  • Loading branch information
kouzant authored Oct 19, 2023
1 parent 6de8168 commit ca3e4c7
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@
@XmlRootElement
@ApiModel(value = "Represents a certificate signing request")
public class CSRView {
private String privateKey;
private String csr;
private String signedCert;
private String intermediateCaCert;
private String rootCaCert;
private String keyStore;
private String trustStore;
private String password;

public CSRView() {

Expand All @@ -74,6 +78,15 @@ public CSRView(String rootCaCert, String intermediateCaCert) {
this.rootCaCert = rootCaCert;
}

@ApiModelProperty(value = "PKCS8 encoded private key used to create Java keystore")
public String getPrivateKey() {
return privateKey;
}

public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}

@ApiModelProperty(value = "String containing the certificate signing request", required = true)
public String getCsr() {
return csr;
Expand Down Expand Up @@ -109,4 +122,32 @@ public String getRootCaCert() {
public void setRootCaCert(String rootCaCert) {
this.rootCaCert = rootCaCert;
}

@ApiModelProperty(value = "Base64 encoded keystore containing signed certificate and intermediate CA locked by " +
"password")
public String getKeyStore() {
return keyStore;
}

public void setKeyStore(String keyStore) {
this.keyStore = keyStore;
}

@ApiModelProperty(value = "Base64 encoded trustore containing Hopsworks root CA certificate locked by password")
public String getTrustStore() {
return trustStore;
}

public void setTrustStore(String trustStore) {
this.trustStore = trustStore;
}

@ApiModelProperty(value = "Randomly generated password to protect keystore and truststore")
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import static io.hops.hopsworks.ca.controllers.CertificateType.HOST;

Expand All @@ -80,6 +81,7 @@
public class HostCertsResource {

private static final String REVOKE_CERTIFICATES_PATTERN = "^%s__.*__[0-9]+.*";
private static final Logger LOGGER = Logger.getLogger(HostCertsResource.class.getName());

@EJB
private NoCacheResponse noCacheResponse;
Expand All @@ -105,6 +107,13 @@ public Response signCSR(CSRView csrView) throws CAException {
String stringifiedCert = pkiUtils.convertToPEM(signedCert);
Pair<String, String> chainOfTrust = pki.getChainOfTrust(pkiUtils.getResponsibleCA(HOST));
CSRView signedCsr = new CSRView(stringifiedCert, chainOfTrust.getLeft(), chainOfTrust.getRight());
if (!Strings.isNullOrEmpty(csrView.getPrivateKey())) {
PKIUtils.KeyStores<String> keyStores = pkiUtils.createB64Keystores(csrView.getPrivateKey(), signedCert,
pkiUtils.loadCertificate(chainOfTrust.getRight()), pkiUtils.loadCertificate(chainOfTrust.getLeft()));
signedCsr.setKeyStore(keyStores.getKeyStore());
signedCsr.setTrustStore(keyStores.getTrustStore());
signedCsr.setPassword(new String(keyStores.getPassword()));
}
GenericEntity<CSRView> csrViewGenericEntity = new GenericEntity<CSRView>(signedCsr) { };
return noCacheResponse.getNoCacheResponseBuilder(Response.Status.OK).entity(csrViewGenericEntity).build();
} catch (IOException | GeneralSecurityException | OperatorCreationException | CAInitializationException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,7 @@
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
Expand Down Expand Up @@ -197,7 +192,6 @@ public class PKI {
private KeyFactory keyFactory;
private JcaX509CertificateConverter converter;
private JcaX509CRLConverter crlConverter;
private JcaPEMKeyConverter pemKeyConverter;
private CAsConfiguration conf;
private static final Map<CAType, X500Name> CA_SUBJECT_NAME = new HashMap<>(3);
private static final String CA_INIT_LOCK = "caInitLock";
Expand Down Expand Up @@ -241,7 +235,6 @@ public void init() {
keyFactory = KeyFactory.getInstance("RSA");
converter = new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider());
crlConverter = new JcaX509CRLConverter().setProvider(new BouncyCastleProvider());
pemKeyConverter = new JcaPEMKeyConverter().setProvider(new BouncyCastleProvider());
configure();
} catch (GeneralSecurityException ex) {
throw new RuntimeException("Failed to initialize PKI", ex);
Expand Down Expand Up @@ -521,7 +514,7 @@ protected Pair<Boolean, KeyPair> loadOrGenerateKeypair(String owner) throws Inva

protected KeyPair loadKeyPairFromFile(String owner) throws IOException {
Path keyPath = getPathToPrivateKey(owner);
return loadKeyPair(keyPath, getPrivateFileKeyPassword(owner));
return pkiUtils.loadKeyPair(keyPath, getPrivateFileKeyPassword(owner));
}

private String getPrivateFileKeyPassword(String owner) {
Expand Down Expand Up @@ -552,22 +545,6 @@ private Path getPathToPrivateKey(String owner) throws FileNotFoundException {
return path;
}

private KeyPair loadKeyPair(Path path, String password) throws IOException {
try (PEMParser pemParser = new PEMParser(new FileReader(path.toFile()))) {
Object object = pemParser.readObject();
KeyPair kp;
if (object instanceof PEMEncryptedKeyPair) {
PEMEncryptedKeyPair ekp = (PEMEncryptedKeyPair) object;
PEMDecryptorProvider decryptorProvider = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
kp = pemKeyConverter.getKeyPair(ekp.decryptKeyPair(decryptorProvider));
} else {
PEMKeyPair ukp = (PEMKeyPair) object;
kp = pemKeyConverter.getKeyPair(ukp);
}
return kp;
}
}

@TransactionAttribute(TransactionAttributeType.NEVER)
protected KeyPair generateKeyPair() {
return keyPairGenerator.generateKeyPair();
Expand Down Expand Up @@ -633,7 +610,7 @@ protected Pair<Boolean, X509Certificate> loadOrGenerateCACertificate(CAType type
@VisibleForTesting
protected X509Certificate loadCACertificate(CAType type) throws IOException, CertificateException {
Path certPath = getCACertificatePath(type);
return loadCertificate(certPath);
return pkiUtils.loadCertificate(certPath);
}

private Path getCACertificatePath(CAType type) throws FileNotFoundException {
Expand All @@ -657,16 +634,6 @@ private Path getCACertificatePath(CAType type) throws FileNotFoundException {
return path;
}

private X509Certificate loadCertificate(Path path) throws IOException, CertificateException {
try (PEMParser pemParser = new PEMParser(new FileReader(path.toFile()))) {
Object object = pemParser.readObject();
if (object instanceof X509CertificateHolder) {
return converter.getCertificate((X509CertificateHolder) object);
}
return null;
}
}

protected Optional<X509Certificate> loadCertificate(String subject) throws IOException, CertificateException {
LOGGER.log(Level.INFO, "Loading certificate with subject " + subject);
Optional<PKICertificate> pkiCert = pkiCertificateFacade.findBySubjectAndStatus(subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,54 @@
import io.hops.hopsworks.persistence.entity.pki.CAType;
import io.hops.hopsworks.persistence.entity.pki.PKICertificate;
import io.hops.hopsworks.restutils.RESTCodes;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStrictStyle;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.util.io.pem.PemObjectGenerator;
import org.bouncycastle.util.io.pem.PemWriter;

import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.naming.InvalidNameException;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CRLException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.cert.X509Extension;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
Expand Down Expand Up @@ -78,8 +104,19 @@ public class PKIUtils {
TIME_SUFFIXES.put("d", ChronoUnit.DAYS);
}
private static final Pattern TIME_CONF_PATTERN = Pattern.compile("([0-9]+)([a-z]+)?");
private static final Base64 B64 = new Base64();
private JcaPEMKeyConverter pemKeyConverter;
private JcaX509CertificateConverter x509Converter;



@PostConstruct
public void init() {
Security.addProvider(new BouncyCastleProvider());
pemKeyConverter = new JcaPEMKeyConverter().setProvider(new BouncyCastleProvider());
x509Converter = new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider());
}

public X500Name parseCertificateSubjectName(String subject, CertificateType certificateType)
throws InvalidNameException {
switch (certificateType) {
Expand Down Expand Up @@ -290,4 +327,111 @@ private ChronoUnit getConfTimeTimeUnit(String configurationTime) {
}
return timeUnitStr == null ? ChronoUnit.MINUTES : TIME_SUFFIXES.get(timeUnitStr.toLowerCase());
}

public KeyPair loadKeyPair(String privateKey, String password) throws IOException {
return loadKeyPair(new StringReader(privateKey), password);
}

public KeyPair loadKeyPair(Path path, String password) throws IOException {
return loadKeyPair(new FileReader(path.toFile()), password);
}

private KeyPair loadKeyPair(Reader reader, String password) throws IOException {
try (PEMParser pemParser = new PEMParser(reader)) {
Object object = pemParser.readObject();
KeyPair kp;
if (object instanceof PEMEncryptedKeyPair) {
PEMEncryptedKeyPair ekp = (PEMEncryptedKeyPair) object;
PEMDecryptorProvider decryptorProvider = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
kp = pemKeyConverter.getKeyPair(ekp.decryptKeyPair(decryptorProvider));
} else if (object instanceof PEMKeyPair) {
// PKCS1
PEMKeyPair ukp = (PEMKeyPair) object;
kp = pemKeyConverter.getKeyPair(ukp);
} else if (object instanceof PrivateKeyInfo) {
// PKCS8
PrivateKey privateKey = pemKeyConverter.getPrivateKey((PrivateKeyInfo) object);
kp = new KeyPair(null, privateKey);
} else {
throw new UnsupportedEncodingException("Unsupported keypair encoding");
}
return kp;
}
}

public X509Certificate loadCertificate(Path path) throws IOException, CertificateException {
return loadCertificate(new FileReader(path.toFile()));
}

public X509Certificate loadCertificate(String certificate) throws IOException, CertificateException {
return loadCertificate(new StringReader(certificate));
}

private X509Certificate loadCertificate(Reader reader) throws IOException, CertificateException {
try (PEMParser pemParser = new PEMParser(reader)) {
Object object = pemParser.readObject();
if (object instanceof X509CertificateHolder) {
return x509Converter.getCertificate((X509CertificateHolder) object);
}
return null;
}
}

public KeyStores<String> createB64Keystores(String privateKey, X509Certificate certificate,
X509Certificate issuer, X509Certificate root) throws GeneralSecurityException, IOException {

try (ByteArrayOutputStream keyStore = new ByteArrayOutputStream();
ByteArrayOutputStream trustStore = new ByteArrayOutputStream()) {
char[] password = createKeystores(privateKey, certificate, issuer, root, keyStore, trustStore);
return new KeyStores<>(B64.encodeToString(keyStore.toByteArray()), B64.encodeToString(trustStore.toByteArray()),
password);
}
}

public char[] createKeystores(String privateKey, X509Certificate certificate, X509Certificate issuer,
X509Certificate root, OutputStream keyStore, OutputStream trustStore)
throws GeneralSecurityException, IOException {
PrivateKey key = loadKeyPair(new StringReader(privateKey), "").getPrivate();
char[] password = RandomStringUtils.randomAlphanumeric(15, 20).toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(null, null);
X509Certificate[] chain = new X509Certificate[2];
chain[0] = certificate;
chain[1] = issuer;
ks.setKeyEntry("own", key, password, chain);
ks.store(keyStore, password);


KeyStore ts = KeyStore.getInstance("JKS");
ts.load(null, null);
ts.setCertificateEntry("hw_root_ca", root);
ts.store(trustStore, password);
keyStore.flush();
trustStore.flush();
return password;
}

public static class KeyStores<T> {
private final T keyStore;
private final T trustStore;
private final char[] password;

public KeyStores(T keyStore, T trustStore, char[] password) {
this.keyStore = keyStore;
this.trustStore = trustStore;
this.password = password;
}

public T getKeyStore() {
return keyStore;
}

public T getTrustStore() {
return trustStore;
}

public char[] getPassword() {
return password;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public Long answer(InvocationOnMock invocationOnMock) throws Throwable {
pki.setCaConf(conf);
}

protected String stringifyCSR(PKCS10CertificationRequest csr) throws IOException {
protected static String stringifyCSR(PKCS10CertificationRequest csr) throws IOException {
try (StringWriter sw = new StringWriter()) {
PemWriter pw = new JcaPEMWriter(sw);
PemObjectGenerator pog = new JcaMiscPEMGenerator(csr);
Expand Down
Loading

0 comments on commit ca3e4c7

Please sign in to comment.