diff --git a/.gitattributes b/.gitattributes index 342dd57..2811f57 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /gradlew text eol=lf *.bat text eol=crlf *.jar binary -*.java text eol=lf \ No newline at end of file +*.java text eol=lf +*.config text eol=lf \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f6c7c81..12fdfda 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.assertj:assertj-core") + testImplementation("org.mockito:mockito-core") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } @@ -58,5 +59,6 @@ tasks.withType { useJUnitPlatform() systemProperty("spring.profiles.active", "test") + jvmArgs("-XX:+EnableDynamicAgentLoading") // DynamicAgentLoading for byteBuddy within Mockito testLogging.showStandardStreams = true } diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..078ebef --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ + lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value + lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Lazy \ No newline at end of file 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/models/Passphrase.java b/src/main/java/de/mlessmann/certassist/models/Passphrase.java new file mode 100644 index 0000000..30060fd --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/models/Passphrase.java @@ -0,0 +1,49 @@ +package de.mlessmann.certassist.models; + +import jakarta.persistence.*; +import java.time.ZonedDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +/** + * Database entity representing a passphrase. + * Unlike passwords which would be stored using BCrypt, passphrases are supposed to be reversible and are encrypted. + * @see de.mlessmann.certassist.service.PassphraseService + */ +@Entity +@Getter +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "identifier" }) }) +public class Passphrase { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Setter(AccessLevel.NONE) + private String id; + + @Setter + @Column(nullable = false) + private String identifier; + + @Setter + @Column(nullable = false) + private String encPassphrase; + + @Setter + @Column(nullable = false) + private String salt; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private ZonedDateTime created; + + @UpdateTimestamp + @Column(nullable = false) + private ZonedDateTime updated; + + @Setter + @Transient + private String passphrase; +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java b/src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java new file mode 100644 index 0000000..4bdb955 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificatePasswordProvider.java @@ -0,0 +1,9 @@ +package de.mlessmann.certassist.openssl; + +public interface CertificatePasswordProvider { + String generateNewPassword(); + + String getPasswordFor(String certificateFingerprint); + + void setPasswordFor(String certificateFingerprint, String password); +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateProvider.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateProvider.java new file mode 100644 index 0000000..23df5ae --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateProvider.java @@ -0,0 +1,11 @@ +package de.mlessmann.certassist.openssl; + +/** + * Accessor interface to the certificate storage. + */ +public interface CertificateProvider { + /** + * A stored certificate is needed (e.g. for command line operations when signing sub certificates). + */ + CertificateUsage requestCertificateUsage(String fingerprint); +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index 54befcd..e532449 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -1,6 +1,5 @@ package de.mlessmann.certassist.openssl; -import java.util.UUID; import lombok.Builder; import lombok.Data; @@ -8,10 +7,6 @@ import lombok.Data; @Builder public class CertificateRequest { - @Builder.Default - @Deprecated - private String oid = UUID.randomUUID().toString(); - private RequestType type; private String commonName; private String trustingAuthority; diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java new file mode 100644 index 0000000..737fd86 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java @@ -0,0 +1,17 @@ +package de.mlessmann.certassist.openssl; + +import java.nio.file.Path; + +/** + * Instance of a certificate that is temporarily stored on disk to be available for use in command line calls. + * The instance implements AutoCloseable to enable cleanup after the stored files are no longer needed. + * @implSpec The files should be removed from disk when the instance is closed, UNLESS the provided paths are the permanent storage location for the certificate files. + */ +public interface CertificateUsage extends AutoCloseable { + Path certificatePath(); + Path certificateKeyPath(); + String fingerprint(); + + @Override + void close(); +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index cd41d56..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,6 +48,8 @@ public class OpenSSLCertificateCreator { ); private final ExecutableResolver executableResolver; + private final CertificatePasswordProvider passwordProvider; + private final CertificateProvider certificateProvider; private static String buildSubjectArg(CertificateRequest request) { String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE @@ -70,28 +74,38 @@ 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); } - Path keyFile = createKeyfile(request, tmpDir.resolve("root.key")); - Path rootCert = createCertificate(request, keyFile, tmpDir.resolve("root.crt")); + String certPassword = passwordProvider.generateNewPassword(); + Path keyFile = createKeyfile(request, tmpDir.resolve("certificate.key"), certPassword); if ( request.getType() == RequestType.ROOT_AUTHORITY || request.getType() == RequestType.STANDALONE_CERTIFICATE ) { - String fingerprint = getCertificateFingerprint(rootCert); - return new OpenSSLCertificateResult(tmpDir, rootCert, keyFile, fingerprint); + Path certificate = createCertificate(request, keyFile, tmpDir.resolve("certificate.crt"), certPassword); + String fingerprint = getCertificateFingerprint(certificate); + passwordProvider.setPasswordFor(fingerprint, certPassword); + return new OpenSSLCertificateResult(tmpDir, certificate, keyFile, fingerprint); } - Path childKey = createKeyfile(request, tmpDir.resolve("child.key")); - Path unsignedCert = createSigningRequest(request, childKey, tmpDir.resolve("child.csr")); - Path signedCert = signCertificate(request, rootCert, keyFile, unsignedCert); - String fingerPrint = getCertificateFingerprint(signedCert); - return new OpenSSLCertificateResult(tmpDir, signedCert, childKey, fingerPrint); + try (var certAuthority = certificateProvider.requestCertificateUsage(request.getTrustingAuthority())) { + Path unsignedCert = createSigningRequest(request, keyFile, tmpDir.resolve("child.csr"), certPassword); + Path signedCert = signCertificate( + request, + certAuthority.certificatePath(), + certAuthority.certificateKeyPath(), + unsignedCert, + certPassword + ); + String fingerprint = getCertificateFingerprint(signedCert); + passwordProvider.setPasswordFor(fingerprint, certPassword); + return new OpenSSLCertificateResult(tmpDir, signedCert, keyFile, fingerprint); + } } - private Path createKeyfile(CertificateRequest request, Path outFile) + private Path createKeyfile(CertificateRequest request, Path outFile, String filePassword) throws CommandLineOperationException, InterruptedException { Path keyFile = outFile.toAbsolutePath(); log.atDebug().log("Writing new certificate key to {}", keyFile); @@ -107,7 +121,7 @@ public class OpenSSLCertificateCreator { "env:KEY_PASS", Integer.toString(request.getRequestedKeyLength()) ) - .environment("KEY_PASS", request.getOid()) + .environment("KEY_PASS", filePassword) .redirectOutput(Slf4jStream.ofCaller().asDebug()) .redirectError(Slf4jStream.ofCaller().asError()) .start(); @@ -120,7 +134,7 @@ public class OpenSSLCertificateCreator { return keyFile; } - private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile) + private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile, String certPassword) throws CommandLineOperationException, InterruptedException { log.atDebug().log("Writing new certificate file {}", outFile); @@ -147,7 +161,7 @@ public class OpenSSLCertificateCreator { "-subj", certSubject ) - .environment("KEY_PASS", request.getOid()) + .environment("KEY_PASS", certPassword) .redirectOutput(Slf4jStream.ofCaller().asDebug()) .redirectError(Slf4jStream.ofCaller().asError()) .start(); @@ -160,7 +174,7 @@ public class OpenSSLCertificateCreator { return outFile; } - private Path createSigningRequest(CertificateRequest request, Path keyFile, Path outFile) + private Path createSigningRequest(CertificateRequest request, Path keyFile, Path outFile, String certPassword) throws CommandLineOperationException, InterruptedException { log.atDebug().log("Writing new certificate signing request file {}", outFile); @@ -184,7 +198,7 @@ public class OpenSSLCertificateCreator { "-subj", certSubject ) - .environment("KEY_PASS", request.getOid()) + .environment("KEY_PASS", certPassword) .redirectOutput(Slf4jStream.ofCaller().asDebug()) .redirectError(Slf4jStream.ofCaller().asError()) .start(); @@ -211,8 +225,13 @@ public class OpenSSLCertificateCreator { } } - private Path signCertificate(CertificateRequest request, Path caCert, Path caKey, Path csrFile) - throws CommandLineOperationException, InterruptedException { + private Path signCertificate( + CertificateRequest request, + Path caCert, + Path caKey, + Path csrFile, + String certPassword + ) throws CommandLineOperationException, InterruptedException { Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt")); log.atDebug().log("Writing new signed certificate file {}", outFile); Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext")); diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java index 3c72222..37cc359 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -6,14 +6,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import lombok.AccessLevel; -import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor(access = AccessLevel.PACKAGE) -@Getter -public class OpenSSLCertificateResult implements AutoCloseable { +public class OpenSSLCertificateResult implements CertificateUsage { private final Path tmpDir; private final Path certificatePath; @@ -21,9 +19,28 @@ public class OpenSSLCertificateResult implements AutoCloseable { private final String certificateFingerPrint; @Override - public void close() throws IOException { - log.info("Cleaning up temporary output directory {}", tmpDir); - Files.walkFileTree(tmpDir, Set.of(), Integer.MAX_VALUE, new DeleteRecursiveFileVisitor()); - Files.deleteIfExists(tmpDir); + public Path certificatePath() { + return certificatePath; + } + + @Override + public Path certificateKeyPath() { + return privateKeyPath; + } + + @Override + public String fingerprint() { + return certificateFingerPrint; + } + + @Override + public void close() { + try { + log.info("Cleaning up temporary output directory {}", tmpDir); + Files.walkFileTree(tmpDir, Set.of(), Integer.MAX_VALUE, new DeleteRecursiveFileVisitor()); + Files.deleteIfExists(tmpDir); + } catch (IOException e) { + log.error("Failed to clean up temporary output directory {}!", tmpDir, 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/repositories/PassphraseRepository.java b/src/main/java/de/mlessmann/certassist/repositories/PassphraseRepository.java new file mode 100644 index 0000000..956dd94 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/repositories/PassphraseRepository.java @@ -0,0 +1,8 @@ +package de.mlessmann.certassist.repositories; + +import de.mlessmann.certassist.models.Passphrase; +import org.springframework.data.repository.CrudRepository; + +public interface PassphraseRepository extends CrudRepository { + Passphrase findByIdentifierEquals(String identifier); +} diff --git a/src/main/java/de/mlessmann/certassist/service/CertificateCreationService.java b/src/main/java/de/mlessmann/certassist/service/CertificateCreationService.java index a2b6988..39087de 100644 --- a/src/main/java/de/mlessmann/certassist/service/CertificateCreationService.java +++ b/src/main/java/de/mlessmann/certassist/service/CertificateCreationService.java @@ -47,8 +47,8 @@ public class CertificateCreationService { certificateRequest ); ) { - certificate.setPrivateKey(Files.readAllBytes(certificateCreatorResult.getPrivateKeyPath())); - certificate.setCert(Files.readAllBytes(certificateCreatorResult.getCertificatePath())); + certificate.setPrivateKey(Files.readAllBytes(certificateCreatorResult.certificateKeyPath())); + certificate.setCert(Files.readAllBytes(certificateCreatorResult.certificatePath())); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Interrupted exception", e); diff --git a/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java b/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java new file mode 100644 index 0000000..88fc885 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java @@ -0,0 +1,27 @@ +package de.mlessmann.certassist.service; + +import de.mlessmann.certassist.openssl.CertificatePasswordProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CertificatePasswordProviderImpl implements CertificatePasswordProvider { + + private final PassphraseService passphraseService; + + @Override + public String generateNewPassword() { + return PassphraseService.generateNewPassword(16); + } + + @Override + public String getPasswordFor(String certificateFingerprint) { + return passphraseService.readPassphrase("cert:" + certificateFingerprint).getPassphrase(); + } + + @Override + public void setPasswordFor(String certificateFingerprint, String password) { + passphraseService.storePassphrase("cert:" + certificateFingerprint, password); + } +} 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..8e511aa --- /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/main/java/de/mlessmann/certassist/service/PassphraseService.java b/src/main/java/de/mlessmann/certassist/service/PassphraseService.java new file mode 100644 index 0000000..0c2b072 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/service/PassphraseService.java @@ -0,0 +1,57 @@ +package de.mlessmann.certassist.service; + +import de.mlessmann.certassist.models.Passphrase; +import de.mlessmann.certassist.repositories.PassphraseRepository; +import java.security.SecureRandom; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PassphraseService { + + private static final String CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?"; + + public static String generateNewPassword(int length) { + SecureRandom random = new SecureRandom(); + StringBuilder passphrase = new StringBuilder(length); + for (int i = 0; i < length; i++) { + passphrase.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length()))); + } + return passphrase.toString(); + } + + @Value("${APP_KEY}") + private final String appKey; + + private final PassphraseRepository passphraseRepository; + + @NonNull + public Passphrase storePassphrase(@NonNull String identifier, @NonNull String passphrase) { + String salt = KeyGenerators.string().generateKey(); + String encPassphrase = Encryptors.delux(appKey, salt).encrypt(passphrase); + + var phrase = new Passphrase(); + phrase.setIdentifier(identifier); + phrase.setSalt(salt); + phrase.setEncPassphrase(encPassphrase); + return passphraseRepository.save(phrase); + } + + @NonNull + public Passphrase readPassphrase(String identifier) { + var passphrase = passphraseRepository.findByIdentifierEquals(identifier); + if (passphrase == null) { + throw new IllegalArgumentException("No passphrase found for identifier: " + identifier); + } + + String unencPassphrase = Encryptors.delux(appKey, passphrase.getSalt()).decrypt(passphrase.getEncPassphrase()); + passphrase.setPassphrase(unencPassphrase); + return passphrase; + } +} diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index e3be9e6..224be51 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -1,27 +1,33 @@ package de.mlessmann.certassist; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; -import de.mlessmann.certassist.openssl.CertificateRequest; +import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; -import de.mlessmann.certassist.openssl.CertificateRequestExtension; -import de.mlessmann.certassist.openssl.CertificateSubject; -import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class TestOpenSSLCertificateCreator { - private OpenSSLCertificateCreator openSSLCertificateCreator; + private CertificatePasswordProvider passwordProvider; @BeforeEach void setUp() { - ExecutableResolver executableResolver = new ExecutableResolver(); - openSSLCertificateCreator = new OpenSSLCertificateCreator(executableResolver); + passwordProvider = mock(CertificatePasswordProvider.class); + when(passwordProvider.generateNewPassword()).thenReturn("ABC-123"); } @Test void testCertificateCreation() throws Exception { + CertificateProvider certificateProvider = mock(CertificateProvider.class); + ExecutableResolver executableResolver = new ExecutableResolver(); + var certificateCreator = new OpenSSLCertificateCreator( + executableResolver, + passwordProvider, + certificateProvider + ); + CertificateRequest certRequest = CertificateRequest .builder() .commonName("test.home") @@ -30,9 +36,27 @@ class TestOpenSSLCertificateCreator { .extension(CertificateRequestExtension.builder().alternativeNames("test2.home", "test3.home")) .build(); - try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { - assertThat(openSSLCertificateCreator.verifyCertificate(cert.getCertificatePath())).isEqualTo(true); + try (var cert = certificateCreator.createCertificate(certRequest)) { + assertThat(certificateCreator.verifyCertificate(cert.certificatePath())).isEqualTo(true); System.out.println("Certificate created: " + cert); + + CertificateRequest childRequest = CertificateRequest + .builder() + .commonName("test.local") + .type(RequestType.NORMAL_CERTIFICATE) + .trustingAuthority(cert.fingerprint()) + .subject( + CertificateSubject.builder().country("DE").state("SH").locality("HH").organization("Crazy-Cats") + ) + .build(); + + var spiedCert = spy(cert); + doNothing().when(spiedCert).close(); + when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert); + try (var childCert = certificateCreator.createCertificate(childRequest)) { + System.out.println("Child certificate created: " + childCert); + assertThat(certificateCreator.verifyCertificate(childCert.certificatePath())).isEqualTo(true); + } } } } diff --git a/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java b/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java new file mode 100644 index 0000000..841a487 --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java @@ -0,0 +1,20 @@ +package de.mlessmann.certassist.config; + +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; + +@Configuration +@Profile("test") +public class TestAppEnvConfig implements EnvironmentAware { + + public static final String TEST_APP_KEY = "5i4Y)Ja0{YCl"; + + @Override + public void setEnvironment(Environment environment) { + if (environment.getProperty("APP_KEY") == null) { + System.setProperty("APP_KEY", TEST_APP_KEY); + } + } +} 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..cecc028 --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java @@ -0,0 +1,34 @@ +package de.mlessmann.certassist.service; + +import de.mlessmann.certassist.openssl.CertificatePasswordProvider; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnMissingBean(CertificatePasswordProvider.class) +public class InMemoryCertificatePasswordProvider implements CertificatePasswordProvider { + + private final Map passwords = new ConcurrentHashMap<>(); + private static final int PASSPHRASE_LENGTH = 16; + + @Override + public String generateNewPassword() { + return PassphraseService.generateNewPassword(PASSPHRASE_LENGTH); + } + + @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); + } +}