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 301579cb87
Signed by: Mark.TwoFive
GPG key ID: 5B5EBCBE331F1E6F
4 changed files with 69 additions and 89 deletions

View file

@ -17,10 +17,4 @@ public class AutoBootKeyStoreManagement {
) {
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.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -29,23 +31,19 @@ public class KeyStoreManager {
};
private final OpenSSLService certificateCreator;
private final CertificatePasswordProvider passwordProvider;
private final AtomicReference<CertificateFactory> certFactory = new AtomicReference<>();
public KeystoreUsage createKeyStore(String keyStorePassphrase, CertificateUsage... serverCerts)
throws JavaSecurityException {
try {
Path keystorePath = Files.createTempFile("keystore", ".jks");
// Load the keystore
keystorePath.toFile().deleteOnExit();
log.debug("Creating keyStore at {}", keystorePath);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null);
keystore.load(null, keyStorePassphrase.toCharArray());
for (CertificateUsage serverCert : serverCerts) {
PrivateKey privateKey = loadPrivateKey(
serverCert.certificateKeyPath(),
passwordProvider.getPasswordFor(serverCert.fingerprint())
);
Certificate[] certChain = loadCertificateChain(serverCert.fullchainPath());
keystore.setKeyEntry(serverCert.fingerprint(), privateKey, null, certChain);
loadPrivateKeyIntoStore(keystore, serverCert);
}
// 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
private PrivateKey loadPrivateKey(Path privateKey, String passphrase) {
String pemContent;
@ -67,14 +107,12 @@ public class KeyStoreManager {
pemContent = Files.readString(privateKey);
}
try (var fis = Files.newInputStream(privateKey)) {
String privateKeyPEM = pemContent
.replaceAll(".*?-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
String privateKeyPEM = pemContent
.replaceAll(".*?-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
@SneakyThrows
@ -85,4 +123,17 @@ public class KeyStoreManager {
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 de.mlessmann.certassist.keystore.KeyStoreManager;
import de.mlessmann.certassist.keystore.TruststoreManager;
import de.mlessmann.certassist.openssl.CertificateProvider;
import de.mlessmann.certassist.openssl.CertificateUsage;
import de.mlessmann.certassist.openssl.InMemoryCertificatePasswordProvider;
@ -53,13 +52,12 @@ public class TestKeystoreCreation {
var certificateProvider = Mockito.mock(CertificateProvider.class);
var opensslCertCreator = new OpenSSLService(new ExecutableResolver(), passwordProvider, certificateProvider);
var truststoreManager = new TruststoreManager();
var keyStoreManager = new KeyStoreManager(opensslCertCreator, passwordProvider);
AtomicBoolean serverAccepted = new AtomicBoolean(false);
AtomicBoolean clientAccepted = new AtomicBoolean(false);
try (
var tmpTruststore = truststoreManager.createTruststore(STORE_PASSPHRASE, dummyCert);
var tmpTruststore = keyStoreManager.createTruststore(STORE_PASSPHRASE, dummyCert);
var tmpKeyStore = keyStoreManager.createKeyStore(STORE_PASSPHRASE, dummyCert)
) {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
@ -69,7 +67,6 @@ public class TestKeystoreCreation {
SSLContext tlsSrvContext = SSLContext.getInstance("TLS");
tlsSrvContext.init(keyManagerFactory.getKeyManagers(), null, TEST_RANDOM);
int serverPort = 1024 + TEST_RANDOM.nextInt(22_000);
ServerSocket serverSocket = tlsSrvContext.getServerSocketFactory().createServerSocket(0);
var serverThread = Thread.startVirtualThread(() -> {