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:
parent
861b7469d2
commit
8856d8773e
10 changed files with 402 additions and 42 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue