From ac3821c949bad0db3d6f9ef9c0c90d493867d238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Fri, 22 Nov 2024 08:40:26 +0100 Subject: [PATCH] feat: Implement new services required or OpenSSL certificate creation --- .../de/mlessmann/certassist/Constants.java | 10 +++ ....java => CertificatePasswordProvider.java} | 2 +- .../openssl/OpenSSLCertificateCreator.java | 6 +- .../repositories/CertificateRepository.java | 4 +- .../service/CertificateProviderImpl.java | 68 +++++++++++++++++++ .../TestOpenSSLCertificateCreator.java | 4 +- .../InMemoryCertificatePasswordProvider.java | 40 +++++++++++ 7 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/Constants.java rename src/main/java/de/mlessmann/certassist/openssl/{CertPasswordProvider.java => CertificatePasswordProvider.java} (81%) create mode 100644 src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java create mode 100644 src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java diff --git a/src/main/java/de/mlessmann/certassist/Constants.java b/src/main/java/de/mlessmann/certassist/Constants.java new file mode 100644 index 0000000..90d2998 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/Constants.java @@ -0,0 +1,10 @@ +package de.mlessmann.certassist; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Constants { + + public static final String CERTASSIST_TMP_PREFIX = "certassist_"; +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertPasswordProvider.java b/src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java similarity index 81% rename from src/main/java/de/mlessmann/certassist/openssl/CertPasswordProvider.java rename to src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java index b069821..4bdb955 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertPasswordProvider.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java @@ -1,6 +1,6 @@ package de.mlessmann.certassist.openssl; -public interface CertPasswordProvider { +public interface CertificatePasswordProvider { String generateNewPassword(); String getPasswordFor(String certificateFingerprint); diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 97932e1..55f6cae 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -1,5 +1,7 @@ package de.mlessmann.certassist.openssl; +import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX; + import de.mlessmann.certassist.ExecutableResolver; import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.UnresolvableCLIDependency; @@ -46,7 +48,7 @@ public class OpenSSLCertificateCreator { ); private final ExecutableResolver executableResolver; - private final CertPasswordProvider passwordProvider; + private final CertificatePasswordProvider passwordProvider; private final CertificateProvider certificateProvider; private static String buildSubjectArg(CertificateRequest request) { @@ -72,7 +74,7 @@ public class OpenSSLCertificateCreator { throws CommandLineOperationException, InterruptedException { Path tmpDir; try { - tmpDir = Files.createTempDirectory("certassist"); + tmpDir = Files.createTempDirectory(CERTASSIST_TMP_PREFIX); } catch (IOException e) { throw new CommandLineOperationException("Could not create temporary directory for certificate creation", e); } diff --git a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java index fed956f..7976856 100644 --- a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java +++ b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java @@ -5,4 +5,6 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository -public interface CertificateRepository extends CrudRepository {} +public interface CertificateRepository extends CrudRepository { + Certificate findByFingerprintIs(String fingerprint); +} diff --git a/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java new file mode 100644 index 0000000..163e835 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java @@ -0,0 +1,68 @@ +package de.mlessmann.certassist.service; + +import de.mlessmann.certassist.Constants; +import de.mlessmann.certassist.DeleteRecursiveFileVisitor; +import de.mlessmann.certassist.openssl.CertificateProvider; +import de.mlessmann.certassist.openssl.CertificateUsage; +import de.mlessmann.certassist.repositories.CertificateRepository; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CertificateProviderImpl implements CertificateProvider { + + private static final OpenOption[] CREATE_TRUNCATE = new OpenOption[] { + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + }; + private final CertificateRepository certificateRepository; + + @Override + public CertificateUsage requestCertificateUsage(String fingerprint) { + var certFromDB = certificateRepository.findByFingerprintIs(fingerprint); + if (certFromDB == null) { + throw new IllegalArgumentException("Unknown fingerprint"); + } + + try { + Path tempDirectory = Files.createTempDirectory(Constants.CERTASSIST_TMP_PREFIX); + Files.write(tempDirectory.resolve("key.pem"), certFromDB.getPrivateKey(), CREATE_TRUNCATE); + Files.write(tempDirectory.resolve("cert.pem"), certFromDB.getCert(), CREATE_TRUNCATE); + return new ExtractedCert(tempDirectory, certFromDB.getFingerprint()); + } catch (IOException e) { + // TODO: Better exception definitions + throw new RuntimeException("Unable to temporarily store certificate for use.", e); + } + } + + @Slf4j + private record ExtractedCert(Path tempDir, String fingerprint) implements CertificateUsage { + @Override + public Path certificateKeyPath() { + return this.tempDir.resolve("key.pem"); + } + + @Override + public Path certificatePath() { + return this.tempDir.resolve("cert.pem"); + } + + @Override + public void close() { + try { + Files.walkFileTree(this.tempDir, new DeleteRecursiveFileVisitor()); + Files.deleteIfExists(this.tempDir); + } catch (IOException e) { + log.error("Unable to clean up temporary directory: " + this.tempDir, e); + } + } + } +} diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 531818c..224be51 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -10,11 +10,11 @@ import org.junit.jupiter.api.Test; class TestOpenSSLCertificateCreator { - private CertPasswordProvider passwordProvider; + private CertificatePasswordProvider passwordProvider; @BeforeEach void setUp() { - passwordProvider = mock(CertPasswordProvider.class); + passwordProvider = mock(CertificatePasswordProvider.class); when(passwordProvider.generateNewPassword()).thenReturn("ABC-123"); } diff --git a/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java b/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java new file mode 100644 index 0000000..9ffae8b --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java @@ -0,0 +1,40 @@ +package de.mlessmann.certassist.service; + +import de.mlessmann.certassist.openssl.CertificatePasswordProvider; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.stereotype.Service; + +@Service +public class InMemoryCertificatePasswordProvider implements CertificatePasswordProvider { + + private final Map passwords = new ConcurrentHashMap<>(); + private static final String CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?"; + private static final int PASSPHRASE_LENGTH = 16; + + @Override + public String generateNewPassword() { + SecureRandom random = new SecureRandom(); + StringBuilder passphrase = new StringBuilder(PASSPHRASE_LENGTH); + for (int i = 0; i < PASSPHRASE_LENGTH; i++) { + passphrase.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length()))); + } + return passphrase.toString(); + } + + @Override + public String getPasswordFor(String certificateFingerprint) { + return Optional.ofNullable(passwords.get(certificateFingerprint)).orElseThrow(); + } + + @Override + public void setPasswordFor(String certificateFingerprint, String password) { + Objects.requireNonNull(certificateFingerprint); + Objects.requireNonNull(password); + passwords.put(certificateFingerprint, password); + } +}