feat: Unified keystore handling, pem bundle import & inline OpenSSL operations #19
14 changed files with 499 additions and 190 deletions
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,13 @@ 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.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
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;
|
||||||
|
@ -27,25 +32,21 @@ public class KeyStoreManager {
|
||||||
StandardOpenOption.CREATE,
|
StandardOpenOption.CREATE,
|
||||||
StandardOpenOption.TRUNCATE_EXISTING,
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
};
|
};
|
||||||
private final OpenSSLService certificateCreator;
|
private final OpenSSLService openSSLService;
|
||||||
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,16 +59,74 @@ public class KeyStoreManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public KeystoreUsage createTruststore(String truststorePassphrase, CertificateUsage... trustedCertificates) {
|
||||||
|
return createTruststore(truststorePassphrase, Arrays.asList(trustedCertificates));
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeystoreUsage createTruststore(
|
||||||
|
String truststorePassphrase,
|
||||||
|
Collection<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(
|
||||||
|
Objects.requireNonNull(cert.certificateKeyPath(), "Certificate does not have a key to import."),
|
||||||
|
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())) {
|
||||||
|
Collection<? extends Certificate> parsedCerts = getX509Factory().generateCertificates(inputStream);
|
||||||
|
if (parsedCerts.size() != 1) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"CertificateUsage parsed to not exactly one result! " + parsedCerts.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Certificate parsedCert = parsedCerts.stream().toList().getFirst();
|
||||||
|
if (!(parsedCert instanceof X509Certificate x509cert)) {
|
||||||
|
throw new IllegalStateException("CertificateUsage did not parse to X509 format??");
|
||||||
|
}
|
||||||
|
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;
|
||||||
if (certificateCreator.isKeyEncrypted(privateKey)) {
|
if (openSSLService.isKeyEncrypted(privateKey)) {
|
||||||
pemContent = certificateCreator.readDecryptedKey(privateKey, passphrase);
|
pemContent = openSSLService.readDecryptedKey(privateKey, passphrase);
|
||||||
} else {
|
} else {
|
||||||
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-----", "")
|
||||||
|
@ -75,7 +134,6 @@ public class KeyStoreManager {
|
||||||
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
|
||||||
private Certificate[] loadCertificateChain(Path certChainPath) {
|
private Certificate[] loadCertificateChain(Path certChainPath) {
|
||||||
|
@ -85,4 +143,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) })
|
||||||
@Data
|
@Data
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class Certificate {
|
public class Certificate {
|
||||||
|
@ -50,12 +51,13 @@ public class Certificate {
|
||||||
private byte[] cert = new byte[0];
|
private byte[] cert = new byte[0];
|
||||||
|
|
||||||
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
|
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
|
||||||
@Column(nullable = false)
|
@Column
|
||||||
private byte[] privateKey = new byte[0];
|
private byte[] privateKey = new byte[0];
|
||||||
|
|
||||||
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
|
//@Lob - Cannot annotate column: https://github.com/xerial/sqlite-jdbc/issues/135
|
||||||
@Column
|
@Column
|
||||||
private byte[] fullchain;
|
private byte[] fullchain;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
private String fingerprint;
|
private String fingerprint;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,11 @@ public interface CertificateUsage extends AutoCloseable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path to the private key file (on disk, potentially temporary depending on the storage implementation).
|
* Returns the path to the private key file (on disk, potentially temporary depending on the storage implementation).
|
||||||
* This file should also be encrypted.
|
* This file should also be encrypted as the file system is considered in-flight.
|
||||||
* @see CertificatePasswordProvider
|
* @see CertificatePasswordProvider
|
||||||
|
* @apiNote Return value can be null, when there is no private key (e.g. imported certificates for trust only)
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
Path certificateKeyPath();
|
Path certificateKeyPath();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package de.mlessmann.certassist.openssl;
|
package de.mlessmann.certassist.openssl;
|
||||||
|
|
||||||
import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX;
|
import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.concurrent.TimeUnit.*;
|
import static java.util.concurrent.TimeUnit.*;
|
||||||
|
import static lombok.AccessLevel.PRIVATE;
|
||||||
import static org.slf4j.LoggerFactory.getLogger;
|
import static org.slf4j.LoggerFactory.getLogger;
|
||||||
|
|
||||||
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
||||||
|
@ -13,14 +15,14 @@ import de.mlessmann.certassist.models.CertificateInfoExtension;
|
||||||
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
||||||
import de.mlessmann.certassist.models.CertificateInfoSubject.CertificateInfoSubjectBuilder;
|
import de.mlessmann.certassist.models.CertificateInfoSubject.CertificateInfoSubjectBuilder;
|
||||||
import de.mlessmann.certassist.service.ExecutableResolver;
|
import de.mlessmann.certassist.service.ExecutableResolver;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.List;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Optional;
|
import java.util.*;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
@ -28,11 +30,14 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.zeroturnaround.exec.ProcessExecutor;
|
import org.zeroturnaround.exec.ProcessExecutor;
|
||||||
|
@ -45,9 +50,9 @@ import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OpenSSLService {
|
public class OpenSSLService {
|
||||||
|
|
||||||
private static final Logger openSSLLogger = getLogger("OpenSSL-Logger");
|
|
||||||
public static final String OPENSSL_CERT_SUBJECT_TEMPLATE =
|
public static final String OPENSSL_CERT_SUBJECT_TEMPLATE =
|
||||||
"/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME";
|
"/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME";
|
||||||
|
private static final Logger openSSLLogger = getLogger("OpenSSL-Logger");
|
||||||
private static final String CSR_EXT_TEMPLATE =
|
private static final String CSR_EXT_TEMPLATE =
|
||||||
"""
|
"""
|
||||||
authorityKeyIdentifier=keyid,issuer
|
authorityKeyIdentifier=keyid,issuer
|
||||||
|
@ -105,6 +110,12 @@ public class OpenSSLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNotBlank(@Nullable String str, @NonNull String failMessage) {
|
||||||
|
if (StringUtils.isBlank(str)) {
|
||||||
|
throw new IllegalArgumentException(failMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public OpenSSLCertificateResult createCertificate(CertificateInfo request) throws CommandLineOperationException {
|
public OpenSSLCertificateResult createCertificate(CertificateInfo request) throws CommandLineOperationException {
|
||||||
Path tmpDir;
|
Path tmpDir;
|
||||||
|
@ -126,11 +137,16 @@ public class OpenSSLService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try (var certAuthority = certificateProvider.requestCertificateUsage(request.getTrustingAuthority())) {
|
try (var certAuthority = certificateProvider.requestCertificateUsage(request.getTrustingAuthority())) {
|
||||||
|
Path pKeyPath = requireNonNull(
|
||||||
|
certAuthority.certificateKeyPath(),
|
||||||
|
"Cannot sign a certificate with a CA for which the key is not available!"
|
||||||
|
);
|
||||||
|
|
||||||
Path signingRequest = createSigningRequest(request, keyFile, tmpDir.resolve("child.csr"), keyPassphrase);
|
Path signingRequest = createSigningRequest(request, keyFile, tmpDir.resolve("child.csr"), keyPassphrase);
|
||||||
Path signedCert = signCertificate(
|
Path signedCert = signCertificate(
|
||||||
request,
|
request,
|
||||||
certAuthority.certificatePath(),
|
certAuthority.certificatePath(),
|
||||||
certAuthority.certificateKeyPath(),
|
pKeyPath,
|
||||||
passwordProvider.getPasswordFor(certAuthority.fingerprint()),
|
passwordProvider.getPasswordFor(certAuthority.fingerprint()),
|
||||||
signingRequest
|
signingRequest
|
||||||
);
|
);
|
||||||
|
@ -153,11 +169,14 @@ public class OpenSSLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path createKeyfile(CertificateInfo request, Path outFile, String filePassword)
|
@NonNull
|
||||||
|
private Path createKeyfile(@NonNull CertificateInfo request, @NonNull Path outFile, @NonNull String keyPassphrase)
|
||||||
throws CommandLineOperationException {
|
throws CommandLineOperationException {
|
||||||
|
requireNotBlank(keyPassphrase, "A passphrase must be provided to generate private keys!");
|
||||||
|
requireNonNull(outFile, "Output file must be provided to generate keys!");
|
||||||
Path keyFile = outFile.toAbsolutePath();
|
Path keyFile = outFile.toAbsolutePath();
|
||||||
log.debug("Writing new certificate key to {}", keyFile);
|
|
||||||
|
|
||||||
|
log.debug("Writing new certificate key to {}", keyFile);
|
||||||
StartedProcess keygenProc = null;
|
StartedProcess keygenProc = null;
|
||||||
try {
|
try {
|
||||||
keygenProc =
|
keygenProc =
|
||||||
|
@ -172,7 +191,7 @@ public class OpenSSLService {
|
||||||
OSSL_ARG_KEY_PW,
|
OSSL_ARG_KEY_PW,
|
||||||
Integer.toString(request.getRequestedKeyLength())
|
Integer.toString(request.getRequestedKeyLength())
|
||||||
)
|
)
|
||||||
.environment(OSSL_ENV_KEY_PW, filePassword)
|
.environment(OSSL_ENV_KEY_PW, keyPassphrase)
|
||||||
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
|
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
|
||||||
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
||||||
.start();
|
.start();
|
||||||
|
@ -185,10 +204,18 @@ public class OpenSSLService {
|
||||||
return keyFile;
|
return keyFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path createCertificate(CertificateInfo request, Path keyFile, Path outFile, String keyPassphrase)
|
@NonNull
|
||||||
throws CommandLineOperationException {
|
private Path createCertificate(
|
||||||
log.debug("Writing new certificate file {}", outFile);
|
@NonNull CertificateInfo request,
|
||||||
|
@NonNull Path keyFile,
|
||||||
|
Path outFile,
|
||||||
|
@NonNull String keyPassphrase
|
||||||
|
) throws CommandLineOperationException {
|
||||||
|
requireNonNull(outFile, "Output file must be provided to generate certificates!");
|
||||||
|
requireNonNull(keyFile, "Private key file must be provided to generate certificates!");
|
||||||
|
requireNotBlank(keyPassphrase, "Private key passphrase must be provided to generate certificates.");
|
||||||
|
|
||||||
|
log.debug("Writing new certificate file {}", outFile);
|
||||||
String certSubject = buildSubjectArg(request);
|
String certSubject = buildSubjectArg(request);
|
||||||
StartedProcess certGenProc = null;
|
StartedProcess certGenProc = null;
|
||||||
try {
|
try {
|
||||||
|
@ -225,9 +252,17 @@ public class OpenSSLService {
|
||||||
return outFile;
|
return outFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path createSigningRequest(CertificateInfo request, Path keyFile, Path outFile, String certPassword)
|
@NonNull
|
||||||
throws CommandLineOperationException {
|
private Path createSigningRequest(
|
||||||
log.atDebug().log("Writing new certificate signing request file {}", outFile);
|
@NonNull CertificateInfo request,
|
||||||
|
@NonNull Path keyFile,
|
||||||
|
@NonNull Path outFile,
|
||||||
|
@NonNull String pkeyPassphrase
|
||||||
|
) throws CommandLineOperationException {
|
||||||
|
requireNonNull(keyFile, "Private key must be provided to generate CSR!");
|
||||||
|
requireNonNull(outFile, "Output file must be provided to generate CSR!");
|
||||||
|
requireNotBlank(pkeyPassphrase, "Passphrase for private key must be provided to create CSR.");
|
||||||
|
log.debug("Writing new certificate signing request file {}", outFile);
|
||||||
|
|
||||||
String certSubject = buildSubjectArg(request);
|
String certSubject = buildSubjectArg(request);
|
||||||
StartedProcess certGenProc = null;
|
StartedProcess certGenProc = null;
|
||||||
|
@ -249,7 +284,7 @@ public class OpenSSLService {
|
||||||
"-subj",
|
"-subj",
|
||||||
certSubject
|
certSubject
|
||||||
)
|
)
|
||||||
.environment(OSSL_ENV_KEY_PW, certPassword)
|
.environment(OSSL_ENV_KEY_PW, pkeyPassphrase)
|
||||||
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
|
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
|
||||||
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
||||||
.start();
|
.start();
|
||||||
|
@ -270,10 +305,10 @@ public class OpenSSLService {
|
||||||
public boolean verifyCertificate(@NonNull Path fullChainFile, @NonNull List<Path> trustedCAs)
|
public boolean verifyCertificate(@NonNull Path fullChainFile, @NonNull List<Path> trustedCAs)
|
||||||
throws CommandLineOperationException {
|
throws CommandLineOperationException {
|
||||||
if (CollectionUtils.isEmpty(trustedCAs)) {
|
if (CollectionUtils.isEmpty(trustedCAs)) {
|
||||||
throw new IllegalArgumentException(
|
log.warn("No trusted CA provided for verification. Certificate is untrusted.");
|
||||||
"At least one trusted CA certificate must be provided to run the verification command."
|
return false;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
requireNonNull(fullChainFile, "Cannot verify certificate without fullChainFile");
|
||||||
|
|
||||||
Path tmpDir = null;
|
Path tmpDir = null;
|
||||||
StartedProcess verifyCommand = null;
|
StartedProcess verifyCommand = null;
|
||||||
|
@ -335,10 +370,13 @@ public class OpenSSLService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies a passphrase against a provided key.
|
* Verifies a passphrase against a provided key.
|
||||||
|
*
|
||||||
* @implNote Due to the implementation of the OpenSSL cli, <em>any password</em> will be valid for unencrypted keys. (Check with {@link #isKeyEncrypted(Path).)
|
* @implNote Due to the implementation of the OpenSSL cli, <em>any password</em> will be valid for unencrypted keys. (Check with {@link #isKeyEncrypted(Path).)
|
||||||
*/
|
*/
|
||||||
public boolean verifyKeyPassphrase(@NonNull Path keyFile, @NonNull String passphrase)
|
public boolean verifyKeyPassphrase(@NonNull Path keyFile, @NonNull String passphrase)
|
||||||
throws CommandLineOperationException {
|
throws CommandLineOperationException {
|
||||||
|
requireNonNull(keyFile, "Key file must be provided to check encryption.");
|
||||||
|
requireNonNull(passphrase, "Must provide a passphrase to check encryption.");
|
||||||
StartedProcess verifyCommand = null;
|
StartedProcess verifyCommand = null;
|
||||||
try {
|
try {
|
||||||
verifyCommand =
|
verifyCommand =
|
||||||
|
@ -365,16 +403,22 @@ public class OpenSSLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private Path signCertificate(
|
private Path signCertificate(
|
||||||
CertificateInfo request,
|
CertificateInfo request,
|
||||||
Path caCert,
|
@NonNull Path caCert,
|
||||||
Path caKey,
|
@NonNull Path caKey,
|
||||||
String caKeyPassphrase,
|
@NonNull String caKeyPassphrase,
|
||||||
Path csrFile
|
@NonNull Path csrFile
|
||||||
) throws CommandLineOperationException {
|
) throws CommandLineOperationException {
|
||||||
|
requireNonNull(caCert, "CA certificate must be provided to sign certs!");
|
||||||
|
requireNonNull(caKey, "CA private key must be provided to sign certs!");
|
||||||
|
requireNotBlank(caKeyPassphrase, "CA private key passphrase must be provided to sign certs!");
|
||||||
|
requireNonNull(csrFile, "CSR to be signed must be provided.");
|
||||||
|
|
||||||
Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt"));
|
Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt"));
|
||||||
log.debug("Writing new signed certificate file {}", outFile);
|
|
||||||
Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext"));
|
Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext"));
|
||||||
|
log.debug("Writing new signed certificate file {}", outFile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String extContent = CSR_EXT_TEMPLATE;
|
String extContent = CSR_EXT_TEMPLATE;
|
||||||
|
@ -394,7 +438,7 @@ public class OpenSSLService {
|
||||||
extContent = extContent.replaceAll("\\[alt_names]\n?", "");
|
extContent = extContent.replaceAll("\\[alt_names]\n?", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Writing extension file content: \n {}", extContent);
|
log.debug("Writing extension file {} with content: \n {}", extFile, extContent);
|
||||||
Files.writeString(
|
Files.writeString(
|
||||||
extFile,
|
extFile,
|
||||||
extContent,
|
extContent,
|
||||||
|
@ -449,24 +493,24 @@ public class OpenSSLService {
|
||||||
return outFile;
|
return outFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCertificateFingerprint(Path certificate) throws CommandLineOperationException {
|
@NonNull
|
||||||
StartedProcess fingerprintProc = null;
|
public String getCertificateFingerprint(@NonNull Path certificate) throws CommandLineOperationException {
|
||||||
|
requireNonNull(certificate, "Certificate must be provided to generate fingerprint.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fingerprintProc =
|
return getCertificateFingerprint(Files.readString(certificate));
|
||||||
new ProcessExecutor()
|
} catch (IOException e) {
|
||||||
.command(resolveOpenSSL(), "x509", "-in", certificate.toString(), "-noout", "-fingerprint")
|
throw new CommandLineOperationException("Certificate content could not be read.", e);
|
||||||
.readOutput(true)
|
}
|
||||||
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
}
|
||||||
.start();
|
|
||||||
var fingerprintResult = fingerprintProc.getFuture().get(30, SECONDS);
|
private String extractFingerprintFromOutput(ProcessResult fingerprintResult) throws CommandLineOperationException {
|
||||||
String output = fingerprintResult.getOutput().getUTF8();
|
String output = fingerprintResult.getOutput().getUTF8();
|
||||||
|
|
||||||
if (fingerprintResult.getExitValue() != 0) {
|
if (fingerprintResult.getExitValue() != 0) {
|
||||||
log.debug("Fingerprint command output:\n{}", output);
|
log.debug("Fingerprint command output:\n{}", output);
|
||||||
throw new CommandLineOperationException(
|
throw new CommandLineOperationException(
|
||||||
"Failed to get fingerprint of certificate. Exit code: %d".formatted(
|
"Failed to get fingerprint of certificate. Exit code: %d".formatted(fingerprintResult.getExitValue())
|
||||||
fingerprintResult.getExitValue()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,6 +529,28 @@ public class OpenSSLService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "%s;%s".formatted(algorithm, fingerprint);
|
return "%s;%s".formatted(algorithm, fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@SneakyThrows
|
||||||
|
public String getCertificateFingerPrint(X509Certificate jdkCert) {
|
||||||
|
String pemContent =
|
||||||
|
"-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----".formatted(
|
||||||
|
new String(jdkCert.getEncoded(), StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
return getCertificateFingerprint(pemContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@SneakyThrows
|
||||||
|
public String getCertificateFingerprint(@NonNull String pemContent) throws CommandLineOperationException {
|
||||||
|
requireNonNull(pemContent, "Certificate PEM content must be provided to generate fingerprint from string.");
|
||||||
|
|
||||||
|
StartedProcess fingerprintProc = null;
|
||||||
|
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||||
|
fingerprintProc = Commands.fingerprintCommand(resolveOpenSSL()).redirectInput(input).start();
|
||||||
|
var fingerprintResult = fingerprintProc.getFuture().get(30, SECONDS);
|
||||||
|
return extractFingerprintFromOutput(fingerprintResult);
|
||||||
} catch (IOException | ExecutionException | TimeoutException | InterruptedException e) {
|
} catch (IOException | ExecutionException | TimeoutException | InterruptedException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -492,37 +558,12 @@ public class OpenSSLService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
|
@NonNull
|
||||||
|
@SneakyThrows
|
||||||
|
public CertificateInfo getCertificateInfo(String pemContent) {
|
||||||
StartedProcess infoProc = null;
|
StartedProcess infoProc = null;
|
||||||
try {
|
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||||
infoProc =
|
infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start();
|
||||||
new ProcessExecutor()
|
|
||||||
.command(
|
|
||||||
resolveOpenSSL(),
|
|
||||||
"x509",
|
|
||||||
"-in",
|
|
||||||
path.toString(),
|
|
||||||
"-noout",
|
|
||||||
"-dateopt",
|
|
||||||
"iso_8601",
|
|
||||||
"-fingerprint",
|
|
||||||
"-subject",
|
|
||||||
"-issuer",
|
|
||||||
"-serial",
|
|
||||||
"-dates",
|
|
||||||
"-alias",
|
|
||||||
"-email",
|
|
||||||
"-purpose",
|
|
||||||
"-ext",
|
|
||||||
"subjectAltName",
|
|
||||||
"-nameopt",
|
|
||||||
"sep_multiline",
|
|
||||||
"-nameopt",
|
|
||||||
"lname"
|
|
||||||
)
|
|
||||||
.readOutput(true)
|
|
||||||
.redirectError(Slf4jStream.of(openSSLLogger).asError())
|
|
||||||
.start();
|
|
||||||
var infoResult = infoProc.getFuture().get(30, SECONDS);
|
var infoResult = infoProc.getFuture().get(30, SECONDS);
|
||||||
String output = infoResult.getOutput().getUTF8();
|
String output = infoResult.getOutput().getUTF8();
|
||||||
if (infoResult.getExitValue() != 0) {
|
if (infoResult.getExitValue() != 0) {
|
||||||
|
@ -534,9 +575,22 @@ public class OpenSSLService {
|
||||||
return getCertificateInfo(output.lines().toArray(String[]::new));
|
return getCertificateInfo(output.lines().toArray(String[]::new));
|
||||||
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
|
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
killIfActive(infoProc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
|
||||||
|
requireNonNull(path, "Certificate file must be provided to read the info.");
|
||||||
|
try {
|
||||||
|
return getCertificateInfo(Files.readString(path));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CommandLineOperationException("Failed to read certificate file.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private String resolveOpenSSL() throws CommandLineOperationException {
|
private String resolveOpenSSL() throws CommandLineOperationException {
|
||||||
try {
|
try {
|
||||||
String path = executableResolver.getOpenSSLPath();
|
String path = executableResolver.getOpenSSLPath();
|
||||||
|
@ -621,7 +675,12 @@ public class OpenSSLService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public String readDecryptedKey(Path keyFile, String passphrase) throws CommandLineOperationException {
|
@NonNull
|
||||||
|
public String readDecryptedKey(@NonNull Path keyFile, @NonNull String passphrase)
|
||||||
|
throws CommandLineOperationException {
|
||||||
|
requireNonNull(keyFile, "Key to be decrypted must be provided!");
|
||||||
|
requireNotBlank(passphrase, "Passphrase for private key must be provided to run decryption.");
|
||||||
|
|
||||||
StartedProcess keyReadProc = null;
|
StartedProcess keyReadProc = null;
|
||||||
try {
|
try {
|
||||||
keyReadProc =
|
keyReadProc =
|
||||||
|
@ -646,4 +705,48 @@ public class OpenSSLService {
|
||||||
killIfActive(keyReadProc);
|
killIfActive(keyReadProc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NoArgsConstructor(access = PRIVATE)
|
||||||
|
private static class Commands {
|
||||||
|
|
||||||
|
private static ProcessExecutor infoCommand(String openSSL, String... additArgs) {
|
||||||
|
List<String> command = new ArrayList<>(
|
||||||
|
List.of(
|
||||||
|
openSSL,
|
||||||
|
"x509",
|
||||||
|
"-noout",
|
||||||
|
"-dateopt",
|
||||||
|
"iso_8601",
|
||||||
|
"-fingerprint",
|
||||||
|
"-subject",
|
||||||
|
"-issuer",
|
||||||
|
"-serial",
|
||||||
|
"-dates",
|
||||||
|
"-alias",
|
||||||
|
"-email",
|
||||||
|
"-purpose",
|
||||||
|
"-ext",
|
||||||
|
"subjectAltName",
|
||||||
|
"-nameopt",
|
||||||
|
"sep_multiline",
|
||||||
|
"-nameopt",
|
||||||
|
"lname"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
command.addAll(Arrays.asList(additArgs));
|
||||||
|
return new ProcessExecutor()
|
||||||
|
.command(command)
|
||||||
|
.readOutput(true)
|
||||||
|
.redirectError(Slf4jStream.of(openSSLLogger).asError());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProcessExecutor fingerprintCommand(String openSSL, String... additArgs) {
|
||||||
|
List<String> command = new ArrayList<>(List.of(openSSL, "x509", "-noout", "-fingerprint"));
|
||||||
|
command.addAll(Arrays.asList(additArgs));
|
||||||
|
return new ProcessExecutor()
|
||||||
|
.command(command)
|
||||||
|
.readOutput(true)
|
||||||
|
.redirectError(Slf4jStream.of(openSSLLogger).asError());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,20 @@ import de.mlessmann.certassist.repositories.CertificateRepository;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CertificateCreationService {
|
public class CertificateCreationService {
|
||||||
|
@ -19,12 +28,18 @@ public class CertificateCreationService {
|
||||||
private final CertificateRepository certificateRepository;
|
private final CertificateRepository certificateRepository;
|
||||||
private final OpenSSLService openSSLService;
|
private final OpenSSLService openSSLService;
|
||||||
private final PassphraseService passphraseService;
|
private final PassphraseService passphraseService;
|
||||||
|
private final Pattern CERT_START_PATTERN = Pattern.compile("-+BEGIN CERTIFICATE-+");
|
||||||
|
private final Pattern CERT_END_PATTERN = Pattern.compile("-+END CERTIFICATE-+");
|
||||||
|
|
||||||
public Certificate createCertificate(final CertificateInfo certificateInfo) {
|
public Certificate createCertificate(final CertificateInfo certificateInfo) {
|
||||||
final Certificate certificate = createEntityFromRequest(certificateInfo);
|
final Certificate certificate = createEntityFromRequest(certificateInfo);
|
||||||
|
|
||||||
try (OpenSSLCertificateResult certificateCreatorResult = openSSLService.createCertificate(certificateInfo);) {
|
try (OpenSSLCertificateResult certificateCreatorResult = openSSLService.createCertificate(certificateInfo);) {
|
||||||
certificate.setPrivateKey(Files.readAllBytes(certificateCreatorResult.certificateKeyPath()));
|
Path keyPath = certificateCreatorResult.certificateKeyPath();
|
||||||
|
certificate.setFingerprint(certificateCreatorResult.fingerprint());
|
||||||
|
if (keyPath != null) {
|
||||||
|
certificate.setPrivateKey(Files.readAllBytes(keyPath));
|
||||||
|
}
|
||||||
certificate.setCert(Files.readAllBytes(certificateCreatorResult.certificatePath()));
|
certificate.setCert(Files.readAllBytes(certificateCreatorResult.certificatePath()));
|
||||||
} catch (CommandLineOperationException | IOException e) {
|
} catch (CommandLineOperationException | IOException e) {
|
||||||
throw new IllegalStateException("Failed to create certificate!", e);
|
throw new IllegalStateException("Failed to create certificate!", e);
|
||||||
|
@ -59,15 +74,23 @@ public class CertificateCreationService {
|
||||||
return certificate;
|
return certificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Certificate importCertificate(Path certificate, Path keyFile, String passphrase) {
|
@NonNull
|
||||||
|
public Certificate importCertificate(
|
||||||
|
@NonNull Path certificate,
|
||||||
|
@Nullable Path keyFile,
|
||||||
|
@Nullable String keyPassphrase
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
String fingerprint = openSSLService.getCertificateFingerprint(certificate);
|
String fingerprint = openSSLService.getCertificateFingerprint(certificate);
|
||||||
var generatedRequest = openSSLService.getCertificateInfo(certificate);
|
var generatedRequest = openSSLService.getCertificateInfo(certificate);
|
||||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||||
|
entity.setFingerprint(fingerprint);
|
||||||
entity.setCert(Files.readAllBytes(certificate));
|
entity.setCert(Files.readAllBytes(certificate));
|
||||||
|
if (keyFile != null) {
|
||||||
entity.setPrivateKey(Files.readAllBytes(keyFile));
|
entity.setPrivateKey(Files.readAllBytes(keyFile));
|
||||||
if (StringUtils.isNotBlank(passphrase)) {
|
}
|
||||||
passphraseService.storePassphrase("cert:" + fingerprint, passphrase);
|
if (StringUtils.isNotBlank(keyPassphrase)) {
|
||||||
|
passphraseService.storePassphrase("cert:" + fingerprint, keyPassphrase);
|
||||||
}
|
}
|
||||||
return certificateRepository.save(entity);
|
return certificateRepository.save(entity);
|
||||||
} catch (CommandLineOperationException | IOException e) {
|
} catch (CommandLineOperationException | IOException e) {
|
||||||
|
@ -75,6 +98,36 @@ public class CertificateCreationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Certificate> importCertificateTrustBundle(@NonNull Path bundleFile) {
|
||||||
|
try {
|
||||||
|
Map<String, Certificate> certsInBundle = new HashMap<>();
|
||||||
|
String pemContent = Files.readString(bundleFile);
|
||||||
|
Matcher beginMatcher = CERT_START_PATTERN.matcher(pemContent);
|
||||||
|
|
||||||
|
while (beginMatcher.find()) {
|
||||||
|
int startIdx = beginMatcher.start();
|
||||||
|
Matcher endMatcher = CERT_END_PATTERN.matcher(pemContent);
|
||||||
|
if (!endMatcher.find(startIdx)) {
|
||||||
|
throw new IllegalStateException("Certificate has a startIdx but not an end??");
|
||||||
|
}
|
||||||
|
int endIdx = endMatcher.end();
|
||||||
|
String singleCert = pemContent.substring(startIdx, endIdx);
|
||||||
|
String fingerprint = openSSLService.getCertificateFingerprint(singleCert);
|
||||||
|
var generatedRequest = openSSLService.getCertificateInfo(singleCert);
|
||||||
|
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||||
|
entity.setFingerprint(fingerprint);
|
||||||
|
entity.setCert(singleCert.getBytes());
|
||||||
|
certsInBundle.put(fingerprint, entity);
|
||||||
|
log.debug("Found certificate in bundle at {} to {}: {}", startIdx, endIdx, fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveResult = certificateRepository.saveAll(certsInBundle.values());
|
||||||
|
return StreamSupport.stream(saveResult.spliterator(), false).toList();
|
||||||
|
} catch (CommandLineOperationException | IOException e) {
|
||||||
|
throw new RuntimeException("Unable to import certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CertificateType mapCertificateRequestType(CertificateInfo.RequestType requestType) {
|
private CertificateType mapCertificateRequestType(CertificateInfo.RequestType requestType) {
|
||||||
return switch (requestType) {
|
return switch (requestType) {
|
||||||
case ROOT_AUTHORITY -> CertificateType.ROOT_CA;
|
case ROOT_AUTHORITY -> CertificateType.ROOT_CA;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package de.mlessmann.certassist.service;
|
package de.mlessmann.certassist.service;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
import de.mlessmann.certassist.Constants;
|
import de.mlessmann.certassist.Constants;
|
||||||
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
||||||
import de.mlessmann.certassist.models.Certificate;
|
import de.mlessmann.certassist.models.Certificate;
|
||||||
|
@ -29,7 +31,14 @@ public class CertificateProviderImpl implements CertificateProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CertificateUsage requestCertificateUsage(String fingerprint) {
|
public CertificateUsage requestCertificateUsage(String fingerprint) {
|
||||||
Certificate certFromDB = certificateRepository.findByFingerprintIs(fingerprint);
|
requireNonNull(fingerprint, "Fingerprint must be provided.");
|
||||||
|
Certificate certFromDB;
|
||||||
|
try {
|
||||||
|
certFromDB = certificateRepository.findByFingerprintIs(fingerprint);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.error("Failed to retrieve certificate from database by fingerprint: {}", fingerprint);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
if (certFromDB == null) {
|
if (certFromDB == null) {
|
||||||
throw new IllegalArgumentException("Unknown fingerprint");
|
throw new IllegalArgumentException("Unknown fingerprint");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,18 @@ 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.models.Certificate;
|
||||||
import de.mlessmann.certassist.models.CertificateInfo;
|
import de.mlessmann.certassist.models.CertificateInfo;
|
||||||
import de.mlessmann.certassist.models.CertificateInfoExtension;
|
import de.mlessmann.certassist.models.CertificateInfoExtension;
|
||||||
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
||||||
|
import de.mlessmann.certassist.openssl.CertificateProvider;
|
||||||
|
import de.mlessmann.certassist.openssl.CertificateUsage;
|
||||||
|
import de.mlessmann.certassist.repositories.CertificateRepository;
|
||||||
import de.mlessmann.certassist.service.CertificateCreationService;
|
import de.mlessmann.certassist.service.CertificateCreationService;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
@ -14,8 +21,24 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
public class CertificateServiceTest {
|
public class CertificateServiceTest {
|
||||||
|
|
||||||
|
public static final Path LETSENCRYPT_CERT_PATH = Path.of("src/test/resources/letsencrypt");
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CertificateCreationService certificateService;
|
CertificateRepository certificateRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
CertificateCreationService certificateService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
KeyStoreManager keyStoreManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
CertificateProvider certificateProvider;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
certificateRepository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCanCreateCertificate() {
|
void testCanCreateCertificate() {
|
||||||
|
@ -49,4 +72,30 @@ public class CertificateServiceTest {
|
||||||
assertThat(importedCert).isNotNull();
|
assertThat(importedCert).isNotNull();
|
||||||
assertThat(importedCert.getId()).isGreaterThan("0");
|
assertThat(importedCert.getId()).isGreaterThan("0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCanImportTrustCertificate() {
|
||||||
|
Path cert = LETSENCRYPT_CERT_PATH.resolve("2024-11-isrgrootx1.pem");
|
||||||
|
var importedCert = certificateService.importCertificate(cert, null, null);
|
||||||
|
assertThat(importedCert).isNotNull();
|
||||||
|
assertThat(importedCert.getId()).isGreaterThan("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCanImportTrustBundles() throws Exception {
|
||||||
|
Path cert = LETSENCRYPT_CERT_PATH.resolve("2024-11-letsencrypt.bundle.pem");
|
||||||
|
List<Certificate> importedCerts = certificateService.importCertificateTrustBundle(cert);
|
||||||
|
List<CertificateUsage> tmpUsableCerts = importedCerts
|
||||||
|
.stream()
|
||||||
|
.map(Certificate::getFingerprint)
|
||||||
|
.map(certificateProvider::requestCertificateUsage)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
try (var store = keyStoreManager.createTruststore("changeit", tmpUsableCerts)) {
|
||||||
|
assertThat(store).isNotNull();
|
||||||
|
assertThat(store.truststorePath()).isRegularFile();
|
||||||
|
} finally {
|
||||||
|
tmpUsableCerts.forEach(CertificateUsage::close);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -53,6 +53,7 @@ class CertificateRepositoryTest {
|
||||||
@Transactional
|
@Transactional
|
||||||
void findCertificateWithExtension() {
|
void findCertificateWithExtension() {
|
||||||
final Certificate certificate = getCertificate();
|
final Certificate certificate = getCertificate();
|
||||||
|
certificate.setFingerprint("test-fingerprint");
|
||||||
CertificateExtension extension = new CertificateExtension();
|
CertificateExtension extension = new CertificateExtension();
|
||||||
extension.setValue("test-ext-value");
|
extension.setValue("test-ext-value");
|
||||||
certificate.setCertificateExtension(List.of(extension));
|
certificate.setCertificateExtension(List.of(extension));
|
||||||
|
|
14
src/test/resources/letsencrypt/2024-11-isrg-root-x2.pem
Normal file
14
src/test/resources/letsencrypt/2024-11-isrg-root-x2.pem
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
|
||||||
|
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
|
||||||
|
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
|
||||||
|
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
|
||||||
|
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
|
||||||
|
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
|
||||||
|
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
|
||||||
|
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
|
||||||
|
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
|
||||||
|
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
|
||||||
|
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
|
||||||
|
/q4AaOeMSQ+2b1tbFfLn
|
||||||
|
-----END CERTIFICATE-----
|
31
src/test/resources/letsencrypt/2024-11-isrgrootx1.pem
Normal file
31
src/test/resources/letsencrypt/2024-11-isrgrootx1.pem
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,45 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
|
||||||
|
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
|
||||||
|
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
|
||||||
|
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
|
||||||
|
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
|
||||||
|
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
|
||||||
|
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
|
||||||
|
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
|
||||||
|
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
|
||||||
|
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
|
||||||
|
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
|
||||||
|
/q4AaOeMSQ+2b1tbFfLn
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
Loading…
Add table
Reference in a new issue