feat: Implement import of CA bundles to keystores
This commit is contained in:
parent
a4f495ab91
commit
6b1c969ce6
8 changed files with 207 additions and 7 deletions
|
@ -15,7 +15,9 @@ import java.security.cert.CertificateException;
|
|||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
@ -30,7 +32,7 @@ public class KeyStoreManager {
|
|||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
};
|
||||
private final OpenSSLService certificateCreator;
|
||||
private final OpenSSLService openSSLService;
|
||||
private final CertificatePasswordProvider passwordProvider;
|
||||
private final AtomicReference<CertificateFactory> certFactory = new AtomicReference<>();
|
||||
|
||||
|
@ -57,7 +59,15 @@ 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();
|
||||
|
@ -90,9 +100,15 @@ public class KeyStoreManager {
|
|||
|
||||
public void loadPublicKeyIntoTrust(KeyStore keyStore, CertificateUsage cert) throws KeyStoreException {
|
||||
try (var inputStream = Files.newInputStream(cert.certificatePath())) {
|
||||
var parsedCert = getX509Factory().generateCertificate(inputStream);
|
||||
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 certificate??");
|
||||
throw new IllegalStateException("CertificateUsage did not parse to X509 format??");
|
||||
}
|
||||
keyStore.setCertificateEntry("TRUST;%s".formatted(cert.fingerprint()), x509cert);
|
||||
} catch (IOException e) {
|
||||
|
@ -105,8 +121,8 @@ public class KeyStoreManager {
|
|||
@SneakyThrows
|
||||
private PrivateKey loadPrivateKey(Path privateKey, String passphrase) {
|
||||
String pemContent;
|
||||
if (certificateCreator.isKeyEncrypted(privateKey)) {
|
||||
pemContent = certificateCreator.readDecryptedKey(privateKey, passphrase);
|
||||
if (openSSLService.isKeyEncrypted(privateKey)) {
|
||||
pemContent = openSSLService.readDecryptedKey(privateKey, passphrase);
|
||||
} else {
|
||||
pemContent = Files.readString(privateKey);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) })
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public class Certificate {
|
||||
|
@ -57,5 +58,6 @@ public class Certificate {
|
|||
@Column
|
||||
private byte[] fullchain;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String fingerprint;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
@ -30,6 +31,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -535,6 +537,51 @@ public class OpenSSLService {
|
|||
}
|
||||
}
|
||||
|
||||
@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.");
|
||||
Path tmpFile = Files.createTempFile(CERTASSIST_TMP_PREFIX, ".pem");
|
||||
try {
|
||||
Files.writeString(tmpFile, pemContent);
|
||||
return getCertificateFingerprint(tmpFile);
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(tmpFile);
|
||||
} catch (IOException e) {
|
||||
log.warn("Unable to delete temporary file, adding to shutdown hook. {}", tmpFile);
|
||||
tmpFile.toFile().deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@SneakyThrows
|
||||
public CertificateInfo getCertificateInfo(String pemContent) {
|
||||
Path tmpFile = Files.createTempFile(CERTASSIST_TMP_PREFIX, ".pem");
|
||||
try {
|
||||
Files.writeString(tmpFile, pemContent);
|
||||
return getCertificateInfo(tmpFile);
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(tmpFile);
|
||||
} catch (IOException e) {
|
||||
log.warn("Unable to delete temporary file, adding to shutdown hook. {}", tmpFile);
|
||||
tmpFile.toFile().deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
|
||||
requireNonNull(path, "Certificate file must be provided to read the info.");
|
||||
|
|
|
@ -7,13 +7,20 @@ import de.mlessmann.certassist.repositories.CertificateRepository;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
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.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CertificateCreationService {
|
||||
|
@ -21,12 +28,15 @@ public class CertificateCreationService {
|
|||
private final CertificateRepository certificateRepository;
|
||||
private final OpenSSLService openSSLService;
|
||||
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) {
|
||||
final Certificate certificate = createEntityFromRequest(certificateInfo);
|
||||
|
||||
try (OpenSSLCertificateResult certificateCreatorResult = openSSLService.createCertificate(certificateInfo);) {
|
||||
Path keyPath = certificateCreatorResult.certificateKeyPath();
|
||||
certificate.setFingerprint(certificateCreatorResult.fingerprint());
|
||||
if (keyPath != null) {
|
||||
certificate.setPrivateKey(Files.readAllBytes(keyPath));
|
||||
}
|
||||
|
@ -74,6 +84,7 @@ public class CertificateCreationService {
|
|||
String fingerprint = openSSLService.getCertificateFingerprint(certificate);
|
||||
var generatedRequest = openSSLService.getCertificateInfo(certificate);
|
||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||
entity.setFingerprint(fingerprint);
|
||||
entity.setCert(Files.readAllBytes(certificate));
|
||||
if (keyFile != null) {
|
||||
entity.setPrivateKey(Files.readAllBytes(keyFile));
|
||||
|
@ -87,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) {
|
||||
return switch (requestType) {
|
||||
case ROOT_AUTHORITY -> CertificateType.ROOT_CA;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package de.mlessmann.certassist.service;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import de.mlessmann.certassist.Constants;
|
||||
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
||||
import de.mlessmann.certassist.models.Certificate;
|
||||
|
@ -29,7 +31,14 @@ public class CertificateProviderImpl implements CertificateProvider {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
throw new IllegalArgumentException("Unknown fingerprint");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue