feat: Implement Truststore/Keystore creation (#17)

* feat: Implement Truststore/Keystore creation
* feat: Update ordering of certificate chains to match what JDK demands
* feat: Implement creating trust- and keystores from certs :)
This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-23 20:24:43 +01:00 committed by GitHub
parent 861b7469d2
commit 8856d8773e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 402 additions and 42 deletions

View file

@ -0,0 +1,20 @@
package de.mlessmann.certassist.except;
public class JavaSecurityException extends Exception {
public JavaSecurityException() {
super();
}
public JavaSecurityException(String message) {
super(message);
}
public JavaSecurityException(String message, Throwable cause) {
super(message, cause);
}
public JavaSecurityException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,88 @@
package de.mlessmann.certassist.keystore;
import de.mlessmann.certassist.except.JavaSecurityException;
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
import de.mlessmann.certassist.openssl.CertificateUsage;
import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator;
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.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class KeyStoreManager {
private static final OpenOption[] CREATE_TRUNCATE = {
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
};
private final OpenSSLCertificateCreator certificateCreator;
private final CertificatePasswordProvider passwordProvider;
public KeystoreUsage createKeyStore(String keyStorePassphrase, CertificateUsage... serverCerts)
throws JavaSecurityException {
try {
Path keystorePath = Files.createTempFile("keystore", ".jks");
// Load the keystore
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null);
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);
}
// Save the keystore
try (var keystoreOut = Files.newOutputStream(keystorePath, CREATE_TRUNCATE)) {
keystore.store(keystoreOut, keyStorePassphrase.toCharArray());
}
return new KeystoreResult(keystorePath);
} catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
throw new JavaSecurityException("Failed to create keystore!", e);
}
}
@SneakyThrows
private PrivateKey loadPrivateKey(Path privateKey, String passphrase) {
String pemContent;
if (certificateCreator.isKeyEncrypted(privateKey)) {
pemContent = certificateCreator.readDecryptedKey(privateKey, passphrase);
} else {
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));
}
}
@SneakyThrows
private Certificate[] loadCertificateChain(Path certChainPath) {
// Load the certificate chain from a PEM file
try (var fis = Files.newInputStream(certChainPath)) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return certFactory.generateCertificates(fis).toArray(Certificate[]::new);
}
}
}

View file

@ -0,0 +1,19 @@
package de.mlessmann.certassist.keystore;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
@Slf4j
record KeystoreResult(Path truststorePath) implements KeystoreUsage {
@Override
public void close() {
try {
Files.deleteIfExists(truststorePath);
} catch (IOException e) {
log.warn("Failed to delete truststore at {}. Scheduling delete on exit.", truststorePath, e);
truststorePath.toFile().deleteOnExit();
}
}
}

View file

@ -0,0 +1,26 @@
package de.mlessmann.certassist.keystore;
import de.mlessmann.certassist.except.JavaSecurityException;
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
public interface KeystoreUsage extends AutoCloseable {
Path truststorePath();
@Override
default void close() {
// Default implementation does nothing - overwrite this if you need to close resources.
}
default KeyStore readAsKeystore(String passphrase) throws JavaSecurityException {
try {
return KeyStore.getInstance(truststorePath().toFile(), passphrase.toCharArray());
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
throw new JavaSecurityException(e);
}
}
}

View file

@ -0,0 +1,62 @@
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

@ -36,5 +36,7 @@ public interface CertificateUsage extends AutoCloseable {
String fingerprint();
@Override
void close();
default void close() {
// Default implementation does nothing - overwrite this if you need to close resources.
}
}

View file

@ -139,8 +139,9 @@ public class OpenSSLCertificateCreator {
Path certAuthFullchain = Optional
.ofNullable(certAuthority.fullchainPath())
.orElse(certAuthority.certificatePath());
Files.write(fullchain, Files.readAllBytes(certAuthFullchain), StandardOpenOption.CREATE);
Files.write(fullchain, Files.readAllBytes(signedCert), StandardOpenOption.APPEND);
// Leaf certificate first, then the CA chain
Files.write(fullchain, Files.readAllBytes(signedCert), StandardOpenOption.CREATE);
Files.write(fullchain, Files.readAllBytes(certAuthFullchain), StandardOpenOption.APPEND);
} catch (IOException e) {
throw new CommandLineOperationException("Failed to create fullchain file.", e);
}
@ -616,4 +617,30 @@ public class OpenSSLCertificateCreator {
default -> throw new IllegalStateException("Unexpected subject key: %s in line: %s".formatted(key, line));
};
}
public String readDecryptedKey(Path keyFile, String passphrase) throws CommandLineOperationException {
StartedProcess keyReadProc = null;
try {
keyReadProc =
new ProcessExecutor()
.command(resolveOpenSSL(), "rsa", "-in", keyFile.toString(), "-passin", OSSL_ARG_KEY_PW)
.environment(OSSL_ENV_KEY_PW, passphrase)
.readOutput(true)
.redirectError(Slf4jStream.of(openSSLLogger).asError())
.start();
var keyReadResult = keyReadProc.getFuture().get(30, SECONDS);
if (keyReadResult.getExitValue() != 0) {
throw new CommandLineOperationException(
"Failed to read decrypted key - is the passphrase correct? Exit code: %d".formatted(
keyReadResult.getExitValue()
)
);
}
return keyReadResult.getOutput().getUTF8();
} catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
} finally {
killIfActive(keyReadProc);
}
}
}