feat: Unify Trust- and KeyStoreManager services

This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-24 10:17:45 +01:00
parent 335eb3ed8d
commit 286c9dcf28
4 changed files with 69 additions and 89 deletions

View file

@ -17,10 +17,4 @@ public class AutoBootKeyStoreManagement {
) { ) {
return new KeyStoreManager(certificateCreator, passwordProvider); return new KeyStoreManager(certificateCreator, passwordProvider);
} }
@Bean
@ConditionalOnMissingBean(TruststoreManager.class)
public TruststoreManager truststoreProvider() {
return new TruststoreManager();
}
} }

View file

@ -13,8 +13,10 @@ import java.security.*;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -29,23 +31,19 @@ public class KeyStoreManager {
}; };
private final OpenSSLService certificateCreator; private final OpenSSLService certificateCreator;
private final CertificatePasswordProvider passwordProvider; private final CertificatePasswordProvider passwordProvider;
private final AtomicReference<CertificateFactory> certFactory = new AtomicReference<>();
public KeystoreUsage createKeyStore(String keyStorePassphrase, CertificateUsage... serverCerts) public KeystoreUsage createKeyStore(String keyStorePassphrase, CertificateUsage... serverCerts)
throws JavaSecurityException { throws JavaSecurityException {
try { try {
Path keystorePath = Files.createTempFile("keystore", ".jks"); Path keystorePath = Files.createTempFile("keystore", ".jks");
keystorePath.toFile().deleteOnExit();
// Load the keystore log.debug("Creating keyStore at {}", keystorePath);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null); keystore.load(null, keyStorePassphrase.toCharArray());
for (CertificateUsage serverCert : serverCerts) { for (CertificateUsage serverCert : serverCerts) {
PrivateKey privateKey = loadPrivateKey( loadPrivateKeyIntoStore(keystore, serverCert);
serverCert.certificateKeyPath(),
passwordProvider.getPasswordFor(serverCert.fingerprint())
);
Certificate[] certChain = loadCertificateChain(serverCert.fullchainPath());
keystore.setKeyEntry(serverCert.fingerprint(), privateKey, null, certChain);
} }
// Save the keystore // Save the keystore
@ -58,6 +56,48 @@ public class KeyStoreManager {
} }
} }
public KeystoreUsage createTruststore(String truststorePassphrase, CertificateUsage... trustedCertificates) {
try {
Path truststorePath = Files.createTempFile("truststore", ".jks");
truststorePath.toFile().deleteOnExit();
log.debug("Creating truststore at {}", truststorePath);
KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
truststore.load(null, truststorePassphrase.toCharArray());
for (CertificateUsage serverCert : trustedCertificates) {
loadPublicKeyIntoTrust(truststore, serverCert);
}
try (var outputStream = Files.newOutputStream(truststorePath, CREATE_TRUNCATE)) {
truststore.store(outputStream, truststorePassphrase.toCharArray());
}
return new KeystoreResult(truststorePath);
} catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Failed to create truststore!", e);
}
}
public void loadPrivateKeyIntoStore(KeyStore keyStore, CertificateUsage cert) throws KeyStoreException {
String passphrase = passwordProvider.getPasswordFor(cert.fingerprint());
var pKey = loadPrivateKey(cert.certificateKeyPath(), passphrase);
var certChain = loadCertificateChain(cert.fullchainPath());
keyStore.setKeyEntry("KEY;%s".formatted(cert.fingerprint()), pKey, null, certChain);
}
public void loadPublicKeyIntoTrust(KeyStore keyStore, CertificateUsage cert) throws KeyStoreException {
try (var inputStream = Files.newInputStream(cert.certificatePath())) {
var parsedCert = getX509Factory().generateCertificate(inputStream);
if (!(parsedCert instanceof X509Certificate x509cert)) {
throw new IllegalStateException("CertificateUsage did not parse to X509 certificate??");
}
keyStore.setCertificateEntry("TRUST;%s".formatted(cert.fingerprint()), x509cert);
} catch (IOException e) {
throw new IllegalStateException("CertificateUsage could not be read??", e);
} catch (CertificateException e) {
throw new IllegalStateException("CertificateUsage is not a parsable certificate??");
}
}
@SneakyThrows @SneakyThrows
private PrivateKey loadPrivateKey(Path privateKey, String passphrase) { private PrivateKey loadPrivateKey(Path privateKey, String passphrase) {
String pemContent; String pemContent;
@ -67,14 +107,12 @@ public class KeyStoreManager {
pemContent = Files.readString(privateKey); pemContent = Files.readString(privateKey);
} }
try (var fis = Files.newInputStream(privateKey)) { String privateKeyPEM = pemContent
String privateKeyPEM = pemContent .replaceAll(".*?-----BEGIN PRIVATE KEY-----", "")
.replaceAll(".*?-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", "");
.replaceAll("\\s", ""); byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM); return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
} }
@SneakyThrows @SneakyThrows
@ -85,4 +123,17 @@ public class KeyStoreManager {
return certFactory.generateCertificates(fis).toArray(Certificate[]::new); return certFactory.generateCertificates(fis).toArray(Certificate[]::new);
} }
} }
protected final synchronized CertificateFactory getX509Factory() {
CertificateFactory instance = certFactory.get();
if (instance == null) {
try {
instance = CertificateFactory.getInstance("X.509");
certFactory.set(instance);
} catch (CertificateException e) {
throw new IllegalStateException("X.509 factory missing??", e);
}
}
return instance;
}
} }

View file

@ -1,62 +0,0 @@
package de.mlessmann.certassist.keystore;
import de.mlessmann.certassist.openssl.CertificateUsage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class TruststoreManager {
private static final OpenOption[] CREATE_TRUNCATE = {
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
};
public KeystoreUsage createTruststore(String truststorePassphrase, CertificateUsage... trustedCertificates) {
try {
Path truststorePath = Files.createTempFile("truststore", ".jks");
truststorePath.toFile().deleteOnExit();
log.debug("Creating truststore at {}", truststorePath);
KeyStore truststore = buildTruststore(trustedCertificates);
try (var outputStream = Files.newOutputStream(truststorePath, CREATE_TRUNCATE)) {
truststore.store(outputStream, truststorePassphrase.toCharArray());
}
return new KeystoreResult(truststorePath);
} catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Failed to create truststore!", e);
}
}
@SneakyThrows
private static KeyStore buildTruststore(CertificateUsage[] trustedCertificates) {
KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
truststore.load(null, null);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
if (trustedCertificates == null || trustedCertificates.length == 0) {
log.warn("No trusted certificates provided, truststore will be empty!");
} else {
for (CertificateUsage trustedCertificate : trustedCertificates) {
try (var inputStream = Files.newInputStream(trustedCertificate.certificatePath())) {
X509Certificate jdkCert = (X509Certificate) certificateFactory.generateCertificate(inputStream);
truststore.setCertificateEntry(trustedCertificate.fingerprint(), jdkCert);
}
}
}
return truststore;
}
}

View file

@ -3,7 +3,6 @@ package de.mlessmann.certassist;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import de.mlessmann.certassist.keystore.KeyStoreManager; import de.mlessmann.certassist.keystore.KeyStoreManager;
import de.mlessmann.certassist.keystore.TruststoreManager;
import de.mlessmann.certassist.openssl.CertificateProvider; import de.mlessmann.certassist.openssl.CertificateProvider;
import de.mlessmann.certassist.openssl.CertificateUsage; import de.mlessmann.certassist.openssl.CertificateUsage;
import de.mlessmann.certassist.openssl.InMemoryCertificatePasswordProvider; import de.mlessmann.certassist.openssl.InMemoryCertificatePasswordProvider;
@ -53,13 +52,12 @@ public class TestKeystoreCreation {
var certificateProvider = Mockito.mock(CertificateProvider.class); var certificateProvider = Mockito.mock(CertificateProvider.class);
var opensslCertCreator = new OpenSSLService(new ExecutableResolver(), passwordProvider, certificateProvider); var opensslCertCreator = new OpenSSLService(new ExecutableResolver(), passwordProvider, certificateProvider);
var truststoreManager = new TruststoreManager();
var keyStoreManager = new KeyStoreManager(opensslCertCreator, passwordProvider); var keyStoreManager = new KeyStoreManager(opensslCertCreator, passwordProvider);
AtomicBoolean serverAccepted = new AtomicBoolean(false); AtomicBoolean serverAccepted = new AtomicBoolean(false);
AtomicBoolean clientAccepted = new AtomicBoolean(false); AtomicBoolean clientAccepted = new AtomicBoolean(false);
try ( try (
var tmpTruststore = truststoreManager.createTruststore(STORE_PASSPHRASE, dummyCert); var tmpTruststore = keyStoreManager.createTruststore(STORE_PASSPHRASE, dummyCert);
var tmpKeyStore = keyStoreManager.createKeyStore(STORE_PASSPHRASE, dummyCert) var tmpKeyStore = keyStoreManager.createKeyStore(STORE_PASSPHRASE, dummyCert)
) { ) {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
@ -69,7 +67,6 @@ public class TestKeystoreCreation {
SSLContext tlsSrvContext = SSLContext.getInstance("TLS"); SSLContext tlsSrvContext = SSLContext.getInstance("TLS");
tlsSrvContext.init(keyManagerFactory.getKeyManagers(), null, TEST_RANDOM); tlsSrvContext.init(keyManagerFactory.getKeyManagers(), null, TEST_RANDOM);
int serverPort = 1024 + TEST_RANDOM.nextInt(22_000);
ServerSocket serverSocket = tlsSrvContext.getServerSocketFactory().createServerSocket(0); ServerSocket serverSocket = tlsSrvContext.getServerSocketFactory().createServerSocket(0);
var serverThread = Thread.startVirtualThread(() -> { var serverThread = Thread.startVirtualThread(() -> {