From 0893a5db461785def3e00519fae226e53d35cbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Mon, 4 Nov 2024 21:53:12 +0100 Subject: [PATCH 01/11] :construction: Start work on executing openssl commands --- .../certassist/CertificateRequest.java | 35 ++++++++++++ .../certassist/ExecutableResolver.java | 55 +++++++++++++++++++ .../certassist/OpenSSLCertificateCreator.java | 39 +++++++++++++ .../except/UnresolvableCLIDependency.java | 17 ++++++ .../TestOpenSSLCertificateCreator.java | 28 ++++++++++ 5 files changed, 174 insertions(+) create mode 100644 src/main/java/de/mlessmann/certassist/CertificateRequest.java create mode 100644 src/main/java/de/mlessmann/certassist/ExecutableResolver.java create mode 100644 src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java create mode 100644 src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java create mode 100644 src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java diff --git a/src/main/java/de/mlessmann/certassist/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/CertificateRequest.java new file mode 100644 index 0000000..12e6599 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/CertificateRequest.java @@ -0,0 +1,35 @@ +package de.mlessmann.certassist; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + + +@Builder +public class CertificateRequest { + + @Getter + @Setter + @Builder.Default + private String oid = UUID.randomUUID().toString(); + + @Getter + @Setter + private RequestType type; + + @Getter + @Setter + private String commonName; + + @Getter + @Setter + private String trustingAuthority; + + public enum RequestType { + ROOT_AUTHORITY, + STANDALONE_CERTIFICATE, + NORMAL_CERTIFICATE + } +} diff --git a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java new file mode 100644 index 0000000..abf54a2 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java @@ -0,0 +1,55 @@ +package de.mlessmann.certassist; + +import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import lombok.Setter; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +@Service +public class ExecutableResolver { + + private static final Logger LOGGER = getLogger(ExecutableResolver.class); + + @Value("${openssl.path:#{null}}") + private String opensslPath; + + public String getOpenSSLPath() throws UnresolvableCLIDependency { + if (opensslPath == null) { + LOGGER.atDebug().log("No openssl path configured, falling back to resolving by shell."); + var optSSLPath = searchCommandFromPath("openssl"); + opensslPath = optSSLPath.orElseThrow(() -> new UnresolvableCLIDependency("openssl", "openssl.path")); + } + + Path configuredPath = new File(opensslPath).toPath(); + if (!Files.isRegularFile(configuredPath)) { + throw new UnresolvableCLIDependency("openssl", "openssl.path"); + } + + return opensslPath; + } + + private Optional searchCommandFromPath(String executableName) { + String envPath = System.getenv("PATH"); + Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!"); + String[] pathEntries = envPath.split(File.pathSeparator); + + for (String pathEntry : pathEntries) { + Path executablePath = Path.of(pathEntry, executableName); + if (Files.isRegularFile(executablePath) && Files.isExecutable(executablePath)) { + return Optional.of(executablePath.toString()); + } + } + + LOGGER.error("Could not find executable '{}' in PATH. Make sure that it exists on the of the directory and is executable.", executableName); + return Optional.empty(); + } +} diff --git a/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java new file mode 100644 index 0000000..e410745 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java @@ -0,0 +1,39 @@ +package de.mlessmann.certassist; + +import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +@Service +public class OpenSSLCertificateCreator { + + private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); + + private final ExecutableResolver executableResolver; + + @Autowired + public OpenSSLCertificateCreator(ExecutableResolver executableResolver) { + this.executableResolver = executableResolver; + } + + public void createCertificate(CertificateRequest request) { + try { + String openSSLPath = executableResolver.getOpenSSLPath(); + + Process process = new ProcessBuilder() + .command(openSSLPath, "--version") + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .start(); + process.waitFor(); + } catch (IOException | InterruptedException e) { + LOGGER.atError().log(e.getMessage()); + } catch (UnresolvableCLIDependency e) { + LOGGER.atError().log(e.getMessage()); + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java new file mode 100644 index 0000000..cbaab23 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java @@ -0,0 +1,17 @@ +package de.mlessmann.certassist.except; + +import lombok.Getter; + +public class UnresolvableCLIDependency extends Exception { + + @Getter + private final String executableName; + @Getter + private final String propertyName; + + public UnresolvableCLIDependency(String executableName, String propertyName) { + super("Could not resolve executable for '%s'. (Use property '%s' to point the application directly to the executable.)".formatted(executableName, propertyName)); + this.executableName = executableName; + this.propertyName = propertyName; + } +} diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java new file mode 100644 index 0000000..66a21b2 --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -0,0 +1,28 @@ +package de.mlessmann.certassist; + +import de.mlessmann.certassist.CertificateRequest.RequestType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +public class TestOpenSSLCertificateCreator { + + private OpenSSLCertificateCreator openSSLCertificateCreator; + + @BeforeEach + void setUp() { + ExecutableResolver executableResolver = new ExecutableResolver(); + openSSLCertificateCreator = new OpenSSLCertificateCreator(executableResolver); + } + + @Test + void testCertificateCreation() { + CertificateRequest certRequest = CertificateRequest.builder() + .commonName("test.home") + .type(RequestType.STANDALONE_CERTIFICATE) + .build(); + + openSSLCertificateCreator.createCertificate(certRequest); + } +} -- 2.45.3 From a51327c7dba135ffc6f73b88833e9f0430398624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 10 Nov 2024 19:06:13 +0100 Subject: [PATCH 02/11] :heavy_plus_sign: Add apache commons for OS utils --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 68ae96d..1251664 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,8 +29,9 @@ repositories { } dependencies { - implementation("org.springframework.boot:spring-boot-autoconfigure") + implementation("org.apache.commons:commons-lang3:3.17.0") + implementation("org.springframework.boot:spring-boot-autoconfigure") implementation("org.springframework.boot:spring-boot-starter-jdbc") implementation("org.xerial:sqlite-jdbc") implementation("org.springframework.boot:spring-boot-starter-data-jpa") -- 2.45.3 From c1ce15c173d38661872e4e6d437895490973897a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 10 Nov 2024 19:07:45 +0100 Subject: [PATCH 03/11] :see_no_evil: Update .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 8af972c..342dd57 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /gradlew text eol=lf *.bat text eol=crlf *.jar binary +*.java text eol=lf \ No newline at end of file -- 2.45.3 From 1671fe29ca802e50374be9342b61748cd2c2e821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 10 Nov 2024 19:08:19 +0100 Subject: [PATCH 04/11] :construction: Work on allowing the creation of OpenSSL certificates --- .../certassist/ExecutableResolver.java | 122 ++++++++++-------- .../certassist/OpenSSLCertificateCreator.java | 39 ------ .../{ => openssl}/CertificateRequest.java | 75 ++++++----- .../openssl/OpenSSLCertificateCreator.java | 62 +++++++++ .../TestOpenSSLCertificateCreator.java | 56 ++++---- 5 files changed, 197 insertions(+), 157 deletions(-) delete mode 100644 src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java rename src/main/java/de/mlessmann/certassist/{ => openssl}/CertificateRequest.java (80%) create mode 100644 src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java diff --git a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java index abf54a2..e2a10b6 100644 --- a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java +++ b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java @@ -1,55 +1,67 @@ -package de.mlessmann.certassist; - -import de.mlessmann.certassist.except.UnresolvableCLIDependency; -import lombok.Setter; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; -import java.util.Optional; - -import static org.slf4j.LoggerFactory.getLogger; - -@Service -public class ExecutableResolver { - - private static final Logger LOGGER = getLogger(ExecutableResolver.class); - - @Value("${openssl.path:#{null}}") - private String opensslPath; - - public String getOpenSSLPath() throws UnresolvableCLIDependency { - if (opensslPath == null) { - LOGGER.atDebug().log("No openssl path configured, falling back to resolving by shell."); - var optSSLPath = searchCommandFromPath("openssl"); - opensslPath = optSSLPath.orElseThrow(() -> new UnresolvableCLIDependency("openssl", "openssl.path")); - } - - Path configuredPath = new File(opensslPath).toPath(); - if (!Files.isRegularFile(configuredPath)) { - throw new UnresolvableCLIDependency("openssl", "openssl.path"); - } - - return opensslPath; - } - - private Optional searchCommandFromPath(String executableName) { - String envPath = System.getenv("PATH"); - Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!"); - String[] pathEntries = envPath.split(File.pathSeparator); - - for (String pathEntry : pathEntries) { - Path executablePath = Path.of(pathEntry, executableName); - if (Files.isRegularFile(executablePath) && Files.isExecutable(executablePath)) { - return Optional.of(executablePath.toString()); - } - } - - LOGGER.error("Could not find executable '{}' in PATH. Make sure that it exists on the of the directory and is executable.", executableName); - return Optional.empty(); - } -} +package de.mlessmann.certassist; + +import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import lombok.Setter; +import org.apache.commons.lang3.SystemUtils; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +@Service +public class ExecutableResolver { + + private static final Logger LOGGER = getLogger(ExecutableResolver.class); + + @Value("${openssl.path:#{null}}") + private String opensslPath; + + public String getOpenSSLPath() throws UnresolvableCLIDependency { + if (opensslPath == null) { + LOGGER.atDebug().log("No openssl path configured, falling back to resolving by shell."); + var optSSLPath = searchCommandFromPath("openssl"); + opensslPath = optSSLPath.orElseThrow(() -> new UnresolvableCLIDependency("openssl", "openssl.path")); + } + + Path configuredPath = new File(opensslPath).toPath(); + if (!Files.isRegularFile(configuredPath)) { + throw new UnresolvableCLIDependency("openssl", "openssl.path"); + } + + return opensslPath; + } + + private Optional searchCommandFromPath(String executableName) { + String envPath = System.getenv("PATH"); + Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!"); + String[] pathEntries = envPath.split(File.pathSeparator); + + for (String pathEntry : pathEntries) { + for (String fileExtension : getAllowedExtensions()) { + Path executablePath = Path.of(pathEntry, executableName + fileExtension); + if (Files.isRegularFile(executablePath) && Files.isExecutable(executablePath)) { + return Optional.of(executablePath.toString()); + } + } + } + + LOGGER.error("Could not find executable '{}' in PATH. Make sure that it exists on the of the directory and is executable.", executableName); + return Optional.empty(); + } + + public List getAllowedExtensions() { + if (SystemUtils.IS_OS_WINDOWS) { + return List.of(".exe", ".bat", ".cmd"); + } else { + return List.of("", ".sh"); + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java deleted file mode 100644 index e410745..0000000 --- a/src/main/java/de/mlessmann/certassist/OpenSSLCertificateCreator.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.mlessmann.certassist; - -import de.mlessmann.certassist.except.UnresolvableCLIDependency; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.io.IOException; - -import static org.slf4j.LoggerFactory.getLogger; - -@Service -public class OpenSSLCertificateCreator { - - private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); - - private final ExecutableResolver executableResolver; - - @Autowired - public OpenSSLCertificateCreator(ExecutableResolver executableResolver) { - this.executableResolver = executableResolver; - } - - public void createCertificate(CertificateRequest request) { - try { - String openSSLPath = executableResolver.getOpenSSLPath(); - - Process process = new ProcessBuilder() - .command(openSSLPath, "--version") - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .start(); - process.waitFor(); - } catch (IOException | InterruptedException e) { - LOGGER.atError().log(e.getMessage()); - } catch (UnresolvableCLIDependency e) { - LOGGER.atError().log(e.getMessage()); - } - } -} diff --git a/src/main/java/de/mlessmann/certassist/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java similarity index 80% rename from src/main/java/de/mlessmann/certassist/CertificateRequest.java rename to src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index 12e6599..126944c 100644 --- a/src/main/java/de/mlessmann/certassist/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -1,35 +1,40 @@ -package de.mlessmann.certassist; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -import java.util.UUID; - - -@Builder -public class CertificateRequest { - - @Getter - @Setter - @Builder.Default - private String oid = UUID.randomUUID().toString(); - - @Getter - @Setter - private RequestType type; - - @Getter - @Setter - private String commonName; - - @Getter - @Setter - private String trustingAuthority; - - public enum RequestType { - ROOT_AUTHORITY, - STANDALONE_CERTIFICATE, - NORMAL_CERTIFICATE - } -} +package de.mlessmann.certassist.openssl; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + + +@Builder +public class CertificateRequest { + + @Getter + @Setter + @Builder.Default + private String oid = UUID.randomUUID().toString(); + + @Getter + @Setter + private RequestType type; + + @Getter + @Setter + private String commonName; + + @Getter + @Setter + private String trustingAuthority; + + @Getter + @Setter + @Builder.Default + private int requestedKeyLength = 4096; + + public enum RequestType { + ROOT_AUTHORITY, + STANDALONE_CERTIFICATE, + NORMAL_CERTIFICATE + } +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java new file mode 100644 index 0000000..adb7671 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -0,0 +1,62 @@ +package de.mlessmann.certassist.openssl; + +import de.mlessmann.certassist.ExecutableResolver; +import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.slf4j.LoggerFactory.getLogger; + +@Service +public class OpenSSLCertificateCreator { + + private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); + + private final ExecutableResolver executableResolver; + + @Autowired + public OpenSSLCertificateCreator(ExecutableResolver executableResolver) { + this.executableResolver = executableResolver; + } + + public void createCertificate(CertificateRequest request) { + Path tmpDir; + try { + tmpDir = Files.createTempDirectory("certassist"); + } catch (IOException e) { + LOGGER.atError() + .log("Could not create temp directory for openssl generator!", e); + return; + } + + try { + createKeyfile(request, tmpDir); + + } catch (IOException | InterruptedException e) { + LOGGER.atError() + .log(e.getMessage()); + } catch (UnresolvableCLIDependency e) { + LOGGER.atError() + .log(e.getMessage()); + } + } + + private Path createKeyfile(CertificateRequest request, Path tmpDir) throws UnresolvableCLIDependency, IOException, InterruptedException { + Path keyFile = tmpDir.resolve("root.key").toAbsolutePath(); + LOGGER.atDebug().log("Creating root certificate key at: {}", keyFile); + + String openSSLPath = executableResolver.getOpenSSLPath(); + Process createRootKeyProc = new ProcessBuilder() + .command(openSSLPath, "req", "genrsa", "-des3", "-out", keyFile.toString(), + Integer.toString(request.getRequestedKeyLength())) + .inheritIO() + .start(); + createRootKeyProc.waitFor(); + return keyFile; + } +} diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 66a21b2..80dc058 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -1,28 +1,28 @@ -package de.mlessmann.certassist; - -import de.mlessmann.certassist.CertificateRequest.RequestType; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -public class TestOpenSSLCertificateCreator { - - private OpenSSLCertificateCreator openSSLCertificateCreator; - - @BeforeEach - void setUp() { - ExecutableResolver executableResolver = new ExecutableResolver(); - openSSLCertificateCreator = new OpenSSLCertificateCreator(executableResolver); - } - - @Test - void testCertificateCreation() { - CertificateRequest certRequest = CertificateRequest.builder() - .commonName("test.home") - .type(RequestType.STANDALONE_CERTIFICATE) - .build(); - - openSSLCertificateCreator.createCertificate(certRequest); - } -} +package de.mlessmann.certassist; + +import de.mlessmann.certassist.openssl.CertificateRequest; +import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; +import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestOpenSSLCertificateCreator { + + private OpenSSLCertificateCreator openSSLCertificateCreator; + + @BeforeEach + void setUp() { + ExecutableResolver executableResolver = new ExecutableResolver(); + openSSLCertificateCreator = new OpenSSLCertificateCreator(executableResolver); + } + + @Test + void testCertificateCreation() { + CertificateRequest certRequest = CertificateRequest.builder() + .commonName("test.home") + .type(RequestType.STANDALONE_CERTIFICATE) + .build(); + + openSSLCertificateCreator.createCertificate(certRequest); + } +} -- 2.45.3 From 637d94e846a008ea95820ae89f6757fc3ed99836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 10 Nov 2024 21:10:24 +0100 Subject: [PATCH 05/11] :construction: Create disposable generator result & include cli output in Logging --- build.gradle.kts | 4 +- .../except/CommandLineOperationException.java | 16 +++++ .../openssl/OpenSSLCertificateCreator.java | 68 ++++++++++++------- .../openssl/OpenSSLCertificateResult.java | 26 +++++++ src/main/resources/application.properties | 6 +- .../TestOpenSSLCertificateCreator.java | 6 +- 6 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/except/CommandLineOperationException.java create mode 100644 src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java diff --git a/build.gradle.kts b/build.gradle.kts index 1251664..5e3766a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,8 @@ repositories { } dependencies { - implementation("org.apache.commons:commons-lang3:3.17.0") + implementation("org.zeroturnaround:zt-exec:1.12") + implementation("org.apache.commons:commons-lang3:3.17.0") implementation("org.springframework.boot:spring-boot-autoconfigure") implementation("org.springframework.boot:spring-boot-starter-jdbc") @@ -54,4 +55,5 @@ dependencies { tasks.withType { useJUnitPlatform() + testLogging.showStandardStreams = true } diff --git a/src/main/java/de/mlessmann/certassist/except/CommandLineOperationException.java b/src/main/java/de/mlessmann/certassist/except/CommandLineOperationException.java new file mode 100644 index 0000000..10ca729 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/except/CommandLineOperationException.java @@ -0,0 +1,16 @@ +package de.mlessmann.certassist.except; + +public class CommandLineOperationException extends Exception { + + public CommandLineOperationException(String message) { + super(message); + } + + public CommandLineOperationException(String message, Throwable cause) { + super(message, cause); + } + + public CommandLineOperationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index adb7671..42d22fc 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -1,14 +1,20 @@ package de.mlessmann.certassist.openssl; import de.mlessmann.certassist.ExecutableResolver; +import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.UnresolvableCLIDependency; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; +import org.zeroturnaround.exec.ProcessExecutor; +import org.zeroturnaround.exec.StartedProcess; +import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.ExecutionException; import static org.slf4j.LoggerFactory.getLogger; @@ -24,39 +30,51 @@ public class OpenSSLCertificateCreator { this.executableResolver = executableResolver; } - public void createCertificate(CertificateRequest request) { + @Nullable + public OpenSSLCertificateResult createCertificate(CertificateRequest request) throws CommandLineOperationException, InterruptedException { Path tmpDir; try { tmpDir = Files.createTempDirectory("certassist"); } catch (IOException e) { - LOGGER.atError() - .log("Could not create temp directory for openssl generator!", e); - return; + throw new CommandLineOperationException("Could not create temporary directory for certificate creation", e); } + createKeyfile(request, tmpDir); + return new OpenSSLCertificateResult(tmpDir); + } + + private Path createKeyfile(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { + Path keyFile = tmpDir.resolve("root.key") + .toAbsolutePath(); + LOGGER.atDebug() + .log("Writing new certificate key to {}", keyFile); + try { - createKeyfile(request, tmpDir); - - } catch (IOException | InterruptedException e) { - LOGGER.atError() - .log(e.getMessage()); - } catch (UnresolvableCLIDependency e) { - LOGGER.atError() - .log(e.getMessage()); + StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", + keyFile.toString(), + "-passout", "env:KEY_PASS", + Integer.toString(request.getRequestedKeyLength())) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller() + .asDebug()) + .redirectError(Slf4jStream.ofCaller() + .asError()) + .start(); + keygenProc.getFuture() + .get(); + } catch (IOException e) { + throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); + } catch (ExecutionException e) { + throw new RuntimeException(e); } - } - - private Path createKeyfile(CertificateRequest request, Path tmpDir) throws UnresolvableCLIDependency, IOException, InterruptedException { - Path keyFile = tmpDir.resolve("root.key").toAbsolutePath(); - LOGGER.atDebug().log("Creating root certificate key at: {}", keyFile); - - String openSSLPath = executableResolver.getOpenSSLPath(); - Process createRootKeyProc = new ProcessBuilder() - .command(openSSLPath, "req", "genrsa", "-des3", "-out", keyFile.toString(), - Integer.toString(request.getRequestedKeyLength())) - .inheritIO() - .start(); - createRootKeyProc.waitFor(); return keyFile; } + + private String resolveOpenSSL() throws CommandLineOperationException { + try { + return executableResolver.getOpenSSLPath(); + } catch (UnresolvableCLIDependency e) { + throw new CommandLineOperationException(e); + } + } } diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java new file mode 100644 index 0000000..ce69053 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -0,0 +1,26 @@ +package de.mlessmann.certassist.openssl; + +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.slf4j.LoggerFactory.getLogger; + +public class OpenSSLCertificateResult implements AutoCloseable { + + private static final Logger LOGGER = getLogger(OpenSSLCertificateResult.class); + + private final Path tmpDir; + + OpenSSLCertificateResult(Path tmpDir) { + this.tmpDir = tmpDir; + } + + @Override + public void close() throws IOException { + LOGGER.info("Cleaning up temporary output directory {}", tmpDir); + Files.deleteIfExists(tmpDir); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1b73bda..177ac97 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,4 +8,8 @@ password=admin spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect #TODO: Use flyway for db setup hibernate.hbm2ddl.auto=create-drop -hibernate.show_sql=true \ No newline at end of file +hibernate.show_sql=true + +# Logging +logging.level.root=INFO +logging.level.de.mlessmann.certassist=DEBUG diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 80dc058..4363de8 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -17,12 +17,14 @@ public class TestOpenSSLCertificateCreator { } @Test - void testCertificateCreation() { + void testCertificateCreation() throws Exception { CertificateRequest certRequest = CertificateRequest.builder() .commonName("test.home") .type(RequestType.STANDALONE_CERTIFICATE) .build(); - openSSLCertificateCreator.createCertificate(certRequest); + try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { + System.out.println("Certificate created: " + cert); + } } } -- 2.45.3 From bcab9f78e266339e5768103688107e9ed4f9a2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 16:51:11 +0100 Subject: [PATCH 06/11] :construction: Create method to generate x509 certs --- .../openssl/CertificateRequest.java | 10 +++ .../openssl/CertificateSubject.java | 16 +++++ .../openssl/OpenSSLCertificateCreator.java | 63 ++++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/mlessmann/certassist/openssl/CertificateSubject.java diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index 126944c..6e9ab73 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -32,6 +32,16 @@ public class CertificateRequest { @Builder.Default private int requestedKeyLength = 4096; + @Getter + @Setter + @Builder.Default + private int requestedValidityDays = 365; + + @Getter + @Setter + @Builder.Default + private CertificateSubject subject = CertificateSubject.builder().build(); + public enum RequestType { ROOT_AUTHORITY, STANDALONE_CERTIFICATE, diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateSubject.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateSubject.java new file mode 100644 index 0000000..5884d4a --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateSubject.java @@ -0,0 +1,16 @@ +package de.mlessmann.certassist.openssl; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CertificateSubject { + + private String emailAddress; + private String organization; + private String organizationalUnit; + private String country; + private String state; + private String locality; +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 42d22fc..49d0466 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -3,6 +3,7 @@ package de.mlessmann.certassist.openssl; import de.mlessmann.certassist.ExecutableResolver; import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.Nullable; @@ -21,8 +22,8 @@ import static org.slf4j.LoggerFactory.getLogger; @Service public class OpenSSLCertificateCreator { + public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); - private final ExecutableResolver executableResolver; @Autowired @@ -30,6 +31,31 @@ public class OpenSSLCertificateCreator { this.executableResolver = executableResolver; } + private static String buildSubjectArg(CertificateRequest request) { + String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject() + .getCountry()) + .replace("STATE", request.getSubject() + .getState()) + .replace("LOCALITY", request.getSubject() + .getLocality()) + .replace("ORGANIZATION", request.getSubject() + .getOrganization()) + .replace("COMMON-NAME", request.getCommonName()); + + if (StringUtils.isNotBlank(request.getSubject() + .getOrganizationalUnit())) { + certSubject += "/OU=" + request.getSubject() + .getOrganizationalUnit(); + } + + if (StringUtils.isNotBlank(request.getSubject() + .getEmailAddress())) { + certSubject += "/emailAddress=" + request.getSubject() + .getEmailAddress(); + } + return certSubject; + } + @Nullable public OpenSSLCertificateResult createCertificate(CertificateRequest request) throws CommandLineOperationException, InterruptedException { Path tmpDir; @@ -40,6 +66,7 @@ public class OpenSSLCertificateCreator { } createKeyfile(request, tmpDir); + createCertificate(request, tmpDir); return new OpenSSLCertificateResult(tmpDir); } @@ -70,6 +97,40 @@ public class OpenSSLCertificateCreator { return keyFile; } + private Path createCertificate(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { + Path keyFile = tmpDir.resolve("root.key") + .toAbsolutePath(); + Path certFile = tmpDir.resolve("root.crt") + .toAbsolutePath(); + LOGGER.atDebug() + .log("Writing new certificate file {}", certFile); + + String certSubject = buildSubjectArg(request); + try { + StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "x509", "-new", "-nodes", + "-key", keyFile.toString(), "-sha256", "-days", + Integer.toString( + request.getRequestedValidityDays()), + "-out", + certFile.toString(), + "-passout", "env:KEY_PASS", "-utf8", "-subj", + certSubject) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller() + .asDebug()) + .redirectError(Slf4jStream.ofCaller() + .asError()) + .start(); + keygenProc.getFuture() + .get(); + } catch (IOException e) { + throw new CommandLineOperationException("Failure running OpenSSL req command.", e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + return certFile; + } + private String resolveOpenSSL() throws CommandLineOperationException { try { return executableResolver.getOpenSSLPath(); -- 2.45.3 From 67698d9b0c5b32a3c6bb3507bec5616b9a2d2156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 18:13:50 +0100 Subject: [PATCH 07/11] :construction: Fix issue where cert cleanup fails - Delete temp directory using FileTree visitor recursively - Update CertificateRequestBuilder to accept subject info directly from builder --- .../DeleteRecursiveFileVisitor.java | 47 +++++++++++ .../except/UnresolvableCLIDependency.java | 33 ++++---- .../openssl/CertificateRequest.java | 34 +++----- .../openssl/OpenSSLCertificateCreator.java | 84 +++++++++---------- .../openssl/OpenSSLCertificateResult.java | 4 +- .../TestOpenSSLCertificateCreator.java | 9 +- 6 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java diff --git a/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java b/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java new file mode 100644 index 0000000..f8c501e --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java @@ -0,0 +1,47 @@ +package de.mlessmann.certassist; + +import org.slf4j.Logger; +import org.springframework.lang.NonNull; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +import static org.slf4j.LoggerFactory.getLogger; + +public class DeleteRecursiveFileVisitor implements FileVisitor { + + private static final Logger LOGGER = getLogger(DeleteRecursiveFileVisitor.class); + + @NonNull + @Override + public FileVisitResult preVisitDirectory(Path dir, @NonNull BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @NonNull + @Override + public FileVisitResult visitFile(Path file, @NonNull BasicFileAttributes attrs) throws IOException { + LOGGER.trace("Deleting file {}", file); + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @NonNull + @Override + public FileVisitResult visitFileFailed(Path file, @NonNull IOException exc) throws IOException { + LOGGER.error("Could not delete file {}", file, exc); + throw exc; + } + + @NonNull + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + LOGGER.trace("Deleting directory {}", dir); + Files.delete(dir); + return FileVisitResult.CONTINUE; + } +} diff --git a/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java index cbaab23..908823f 100644 --- a/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java +++ b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java @@ -1,17 +1,16 @@ -package de.mlessmann.certassist.except; - -import lombok.Getter; - -public class UnresolvableCLIDependency extends Exception { - - @Getter - private final String executableName; - @Getter - private final String propertyName; - - public UnresolvableCLIDependency(String executableName, String propertyName) { - super("Could not resolve executable for '%s'. (Use property '%s' to point the application directly to the executable.)".formatted(executableName, propertyName)); - this.executableName = executableName; - this.propertyName = propertyName; - } -} +package de.mlessmann.certassist.except; + +import lombok.Getter; + +@Getter +public class UnresolvableCLIDependency extends Exception { + + private final String executableName; + private final String propertyName; + + public UnresolvableCLIDependency(String executableName, String propertyName) { + super("Could not resolve executable for '%s'. (Use property '%s' to point the application directly to the executable.)".formatted(executableName, propertyName)); + this.executableName = executableName; + this.propertyName = propertyName; + } +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index 6e9ab73..323a5cb 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -1,50 +1,36 @@ package de.mlessmann.certassist.openssl; import lombok.Builder; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; import java.util.UUID; - +@Data @Builder public class CertificateRequest { - @Getter - @Setter @Builder.Default private String oid = UUID.randomUUID().toString(); - @Getter - @Setter private RequestType type; - - @Getter - @Setter private String commonName; - - @Getter - @Setter private String trustingAuthority; - - @Getter - @Setter @Builder.Default private int requestedKeyLength = 4096; - - @Getter - @Setter @Builder.Default private int requestedValidityDays = 365; - - @Getter - @Setter - @Builder.Default - private CertificateSubject subject = CertificateSubject.builder().build(); + private CertificateSubject subject; public enum RequestType { ROOT_AUTHORITY, STANDALONE_CERTIFICATE, NORMAL_CERTIFICATE } + + public static class CertificateRequestBuilder { + public CertificateRequestBuilder subject(CertificateSubject.CertificateSubjectBuilder builder) { + this.subject = builder.build(); + return this; + } + } } diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 49d0466..2c43260 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -33,25 +33,25 @@ public class OpenSSLCertificateCreator { private static String buildSubjectArg(CertificateRequest request) { String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject() - .getCountry()) - .replace("STATE", request.getSubject() - .getState()) - .replace("LOCALITY", request.getSubject() - .getLocality()) - .replace("ORGANIZATION", request.getSubject() - .getOrganization()) - .replace("COMMON-NAME", request.getCommonName()); + .getCountry()) + .replace("STATE", request.getSubject() + .getState()) + .replace("LOCALITY", request.getSubject() + .getLocality()) + .replace("ORGANIZATION", request.getSubject() + .getOrganization()) + .replace("COMMON-NAME", request.getCommonName()); if (StringUtils.isNotBlank(request.getSubject() - .getOrganizationalUnit())) { + .getOrganizationalUnit())) { certSubject += "/OU=" + request.getSubject() - .getOrganizationalUnit(); + .getOrganizationalUnit(); } if (StringUtils.isNotBlank(request.getSubject() - .getEmailAddress())) { + .getEmailAddress())) { certSubject += "/emailAddress=" + request.getSubject() - .getEmailAddress(); + .getEmailAddress(); } return certSubject; } @@ -72,23 +72,23 @@ public class OpenSSLCertificateCreator { private Path createKeyfile(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { Path keyFile = tmpDir.resolve("root.key") - .toAbsolutePath(); + .toAbsolutePath(); LOGGER.atDebug() - .log("Writing new certificate key to {}", keyFile); + .log("Writing new certificate key to {}", keyFile); try { StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", - keyFile.toString(), - "-passout", "env:KEY_PASS", - Integer.toString(request.getRequestedKeyLength())) - .environment("KEY_PASS", request.getOid()) - .redirectOutput(Slf4jStream.ofCaller() - .asDebug()) - .redirectError(Slf4jStream.ofCaller() - .asError()) - .start(); + keyFile.toString(), + "-passout", "env:KEY_PASS", + Integer.toString(request.getRequestedKeyLength())) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller() + .asDebug()) + .redirectError(Slf4jStream.ofCaller() + .asError()) + .start(); keygenProc.getFuture() - .get(); + .get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); } catch (ExecutionException e) { @@ -99,30 +99,30 @@ public class OpenSSLCertificateCreator { private Path createCertificate(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { Path keyFile = tmpDir.resolve("root.key") - .toAbsolutePath(); + .toAbsolutePath(); Path certFile = tmpDir.resolve("root.crt") - .toAbsolutePath(); + .toAbsolutePath(); LOGGER.atDebug() - .log("Writing new certificate file {}", certFile); + .log("Writing new certificate file {}", certFile); String certSubject = buildSubjectArg(request); try { - StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "x509", "-new", "-nodes", - "-key", keyFile.toString(), "-sha256", "-days", - Integer.toString( - request.getRequestedValidityDays()), - "-out", - certFile.toString(), - "-passout", "env:KEY_PASS", "-utf8", "-subj", - certSubject) - .environment("KEY_PASS", request.getOid()) - .redirectOutput(Slf4jStream.ofCaller() - .asDebug()) - .redirectError(Slf4jStream.ofCaller() - .asError()) - .start(); + StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-nodes", + "-key", keyFile.toString(), "-sha256", "-days", + Integer.toString( + request.getRequestedValidityDays()), + "-out", + certFile.toString(), + "-passout", "env:KEY_PASS", "-utf8", "-subj", + certSubject) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller() + .asDebug()) + .redirectError(Slf4jStream.ofCaller() + .asError()) + .start(); keygenProc.getFuture() - .get(); + .get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL req command.", e); } catch (ExecutionException e) { diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java index ce69053..04a4833 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -1,10 +1,12 @@ package de.mlessmann.certassist.openssl; +import de.mlessmann.certassist.DeleteRecursiveFileVisitor; import org.slf4j.Logger; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Set; import static org.slf4j.LoggerFactory.getLogger; @@ -21,6 +23,6 @@ public class OpenSSLCertificateResult implements AutoCloseable { @Override public void close() throws IOException { LOGGER.info("Cleaning up temporary output directory {}", tmpDir); - Files.deleteIfExists(tmpDir); + Files.walkFileTree(tmpDir, Set.of(), Integer.MAX_VALUE, new DeleteRecursiveFileVisitor()); } } diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 4363de8..70d38ae 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -2,6 +2,7 @@ package de.mlessmann.certassist; import de.mlessmann.certassist.openssl.CertificateRequest; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; +import de.mlessmann.certassist.openssl.CertificateSubject; import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,9 +20,11 @@ public class TestOpenSSLCertificateCreator { @Test void testCertificateCreation() throws Exception { CertificateRequest certRequest = CertificateRequest.builder() - .commonName("test.home") - .type(RequestType.STANDALONE_CERTIFICATE) - .build(); + .commonName("test.home") + .type(RequestType.STANDALONE_CERTIFICATE) + .subject(CertificateSubject.builder().country("DE").state("SH") + .locality("").organization("Crazy-Cats")) + .build(); try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { System.out.println("Certificate created: " + cert); -- 2.45.3 From 885748e46d27598d058c99cbfe5365f62f410791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 19:45:24 +0100 Subject: [PATCH 08/11] fix: Delete tmpDir during cleanup too --- .../mlessmann/certassist/openssl/OpenSSLCertificateResult.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java index 04a4833..a5de8e9 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -24,5 +24,6 @@ public class OpenSSLCertificateResult implements AutoCloseable { public void close() throws IOException { LOGGER.info("Cleaning up temporary output directory {}", tmpDir); Files.walkFileTree(tmpDir, Set.of(), Integer.MAX_VALUE, new DeleteRecursiveFileVisitor()); + Files.deleteIfExists(tmpDir); } } -- 2.45.3 From 1238027487c1573ee4695964b65695822afe9daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 20:10:23 +0100 Subject: [PATCH 09/11] feat: Implement capability of signing certs --- .../openssl/CertificateRequest.java | 6 + .../openssl/CertificateRequestExtension.java | 20 +++ .../openssl/OpenSSLCertificateCreator.java | 129 ++++++++++-------- .../TestOpenSSLCertificateCreator.java | 9 +- 4 files changed, 98 insertions(+), 66 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index 323a5cb..d6aa45b 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -20,6 +20,7 @@ public class CertificateRequest { @Builder.Default private int requestedValidityDays = 365; private CertificateSubject subject; + private CertificateRequestExtension extension; public enum RequestType { ROOT_AUTHORITY, @@ -32,5 +33,10 @@ public class CertificateRequest { this.subject = builder.build(); return this; } + + public CertificateRequestBuilder extension(CertificateRequestExtension.CertificateRequestExtensionBuilder builder) { + this.extension = builder.build(); + return this; + } } } diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java new file mode 100644 index 0000000..f28e94e --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java @@ -0,0 +1,20 @@ +package de.mlessmann.certassist.openssl; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class CertificateRequestExtension { + + private List alternativeNames; + + public static class CertificateRequestExtensionBuilder { + public CertificateRequestExtensionBuilder alternativeNames(String... altNames) { + this.alternativeNames = List.of(altNames); + return this; + } + } +} diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 2c43260..c571ae5 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -13,8 +13,12 @@ import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import static org.slf4j.LoggerFactory.getLogger; @@ -24,6 +28,15 @@ public class OpenSSLCertificateCreator { public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); + private static final String CSR_EXT_TEMPLATE = """ + authorityKeyIdentifier=keyid,issuer + basicConstraints=CA:FALSE + keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment + subjectAltName = @alt_names + + [alt_names] + """; + private final ExecutableResolver executableResolver; @Autowired @@ -32,26 +45,14 @@ public class OpenSSLCertificateCreator { } private static String buildSubjectArg(CertificateRequest request) { - String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject() - .getCountry()) - .replace("STATE", request.getSubject() - .getState()) - .replace("LOCALITY", request.getSubject() - .getLocality()) - .replace("ORGANIZATION", request.getSubject() - .getOrganization()) - .replace("COMMON-NAME", request.getCommonName()); + String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject().getCountry()).replace("STATE", request.getSubject().getState()).replace("LOCALITY", request.getSubject().getLocality()).replace("ORGANIZATION", request.getSubject().getOrganization()).replace("COMMON-NAME", request.getCommonName()); - if (StringUtils.isNotBlank(request.getSubject() - .getOrganizationalUnit())) { - certSubject += "/OU=" + request.getSubject() - .getOrganizationalUnit(); + if (StringUtils.isNotBlank(request.getSubject().getOrganizationalUnit())) { + certSubject += "/OU=" + request.getSubject().getOrganizationalUnit(); } - if (StringUtils.isNotBlank(request.getSubject() - .getEmailAddress())) { - certSubject += "/emailAddress=" + request.getSubject() - .getEmailAddress(); + if (StringUtils.isNotBlank(request.getSubject().getEmailAddress())) { + certSubject += "/emailAddress=" + request.getSubject().getEmailAddress(); } return certSubject; } @@ -65,30 +66,22 @@ public class OpenSSLCertificateCreator { throw new CommandLineOperationException("Could not create temporary directory for certificate creation", e); } - createKeyfile(request, tmpDir); - createCertificate(request, tmpDir); + Path keyFile = createKeyfile(request, tmpDir.resolve("root.key")); + Path rootCert = createCertificate(request, keyFile, tmpDir.resolve("root.crt")); + + Path childKey = createKeyfile(request, tmpDir.resolve("child.key")); + Path unsignedCert = createCertificate(request, childKey, tmpDir.resolve("child.csr")); + Path signedCert = signCertificate(request, rootCert, keyFile, unsignedCert); return new OpenSSLCertificateResult(tmpDir); } - private Path createKeyfile(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { - Path keyFile = tmpDir.resolve("root.key") - .toAbsolutePath(); - LOGGER.atDebug() - .log("Writing new certificate key to {}", keyFile); + private Path createKeyfile(CertificateRequest request, Path outFile) throws CommandLineOperationException, InterruptedException { + Path keyFile = outFile.toAbsolutePath(); + LOGGER.atDebug().log("Writing new certificate key to {}", keyFile); try { - StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", - keyFile.toString(), - "-passout", "env:KEY_PASS", - Integer.toString(request.getRequestedKeyLength())) - .environment("KEY_PASS", request.getOid()) - .redirectOutput(Slf4jStream.ofCaller() - .asDebug()) - .redirectError(Slf4jStream.ofCaller() - .asError()) - .start(); - keygenProc.getFuture() - .get(); + StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", keyFile.toString(), "-passout", "env:KEY_PASS", Integer.toString(request.getRequestedKeyLength())).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + keygenProc.getFuture().get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); } catch (ExecutionException e) { @@ -97,38 +90,54 @@ public class OpenSSLCertificateCreator { return keyFile; } - private Path createCertificate(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { - Path keyFile = tmpDir.resolve("root.key") - .toAbsolutePath(); - Path certFile = tmpDir.resolve("root.crt") - .toAbsolutePath(); - LOGGER.atDebug() - .log("Writing new certificate file {}", certFile); + private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile) throws CommandLineOperationException, InterruptedException { + LOGGER.atDebug().log("Writing new certificate file {}", outFile); String certSubject = buildSubjectArg(request); try { - StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-nodes", - "-key", keyFile.toString(), "-sha256", "-days", - Integer.toString( - request.getRequestedValidityDays()), - "-out", - certFile.toString(), - "-passout", "env:KEY_PASS", "-utf8", "-subj", - certSubject) - .environment("KEY_PASS", request.getOid()) - .redirectOutput(Slf4jStream.ofCaller() - .asDebug()) - .redirectError(Slf4jStream.ofCaller() - .asError()) - .start(); - keygenProc.getFuture() - .get(); + StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-passin", "env:KEY_PASS", "-key", keyFile.toString(), "-sha256", "-days", Integer.toString(request.getRequestedValidityDays()), "-out", outFile.toString(), "-passout", "env:KEY_PASS", "-utf8", "-subj", certSubject).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + certGenProc.getFuture().get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL req command.", e); } catch (ExecutionException e) { throw new RuntimeException(e); } - return certFile; + return outFile; + } + + private Path signCertificate(CertificateRequest request, Path caCert, Path caKey, Path csrFile) throws CommandLineOperationException, InterruptedException { + Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt")); + LOGGER.atDebug().log("Writing new signed certificate file {}", outFile); + Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext")); + + try { + String extContent = CSR_EXT_TEMPLATE; + List altNames = Optional.ofNullable(request.getExtension()) + .map(CertificateRequestExtension::getAlternativeNames) + .orElse(List.of()); + if (!altNames.isEmpty()) { + String altNamesContent = String.join("\n", altNames); + extContent = extContent.replaceAll("\\[alt_names]\n?, ", "[alt_names]\n" + altNamesContent); + } else { + extContent = extContent.replaceAll("\\s*subjectAltName\\s+=\\s+@alt_names\n?", ""); + extContent = extContent.replaceAll("\\[alt_names]\n?, ", ""); + } + + LOGGER.debug("Writing extension file content: \n {}", extContent); + Files.writeString(extFile, extContent, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + + try { + StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "x509", "-req", "-days", Integer.toString(request.getRequestedValidityDays()), "-in", csrFile.toString(), "-CA", caCert.toString(), "-CAkey", caKey.toString(), "-CAcreateserial", "-out", outFile.toString(), "-extfile", extFile.toString()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + certGenProc.getFuture().get(); + } catch (IOException e) { + throw new CommandLineOperationException("Failure running OpenSSL x509 command.", e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + return outFile; } private String resolveOpenSSL() throws CommandLineOperationException { diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 70d38ae..9423509 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -2,6 +2,7 @@ package de.mlessmann.certassist; import de.mlessmann.certassist.openssl.CertificateRequest; 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; @@ -19,15 +20,11 @@ public class TestOpenSSLCertificateCreator { @Test void testCertificateCreation() throws Exception { - CertificateRequest certRequest = CertificateRequest.builder() - .commonName("test.home") - .type(RequestType.STANDALONE_CERTIFICATE) - .subject(CertificateSubject.builder().country("DE").state("SH") - .locality("").organization("Crazy-Cats")) - .build(); + CertificateRequest certRequest = CertificateRequest.builder().commonName("test.home").type(RequestType.STANDALONE_CERTIFICATE).subject(CertificateSubject.builder().country("DE").state("SH").locality("HH").organization("Crazy-Cats")).extension(CertificateRequestExtension.builder().alternativeNames("test2.home", "test3.home")).build(); try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { System.out.println("Certificate created: " + cert); } + throw new RuntimeException("Test not implemented"); } } -- 2.45.3 From 9f94e76a1d5631f19c152c823a98c490c8dc0185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 20:20:58 +0100 Subject: [PATCH 10/11] chore: Run spotless --- .../DeleteRecursiveFileVisitor.java | 7 +- .../certassist/ExecutableResolver.java | 19 +-- .../except/UnresolvableCLIDependency.java | 7 +- .../openssl/CertificateRequest.java | 13 +- .../openssl/CertificateRequestExtension.java | 4 +- .../openssl/OpenSSLCertificateCreator.java | 138 ++++++++++++++---- .../openssl/OpenSSLCertificateResult.java | 7 +- .../TestOpenSSLCertificateCreator.java | 8 +- 8 files changed, 146 insertions(+), 57 deletions(-) diff --git a/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java b/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java index f8c501e..8310eca 100644 --- a/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java +++ b/src/main/java/de/mlessmann/certassist/DeleteRecursiveFileVisitor.java @@ -1,7 +1,6 @@ package de.mlessmann.certassist; -import org.slf4j.Logger; -import org.springframework.lang.NonNull; +import static org.slf4j.LoggerFactory.getLogger; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -9,8 +8,8 @@ import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; - -import static org.slf4j.LoggerFactory.getLogger; +import org.slf4j.Logger; +import org.springframework.lang.NonNull; public class DeleteRecursiveFileVisitor implements FileVisitor { diff --git a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java index e2a10b6..8ae0cd0 100644 --- a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java +++ b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java @@ -1,20 +1,18 @@ package de.mlessmann.certassist; -import de.mlessmann.certassist.except.UnresolvableCLIDependency; -import lombok.Setter; -import org.apache.commons.lang3.SystemUtils; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; +import static org.slf4j.LoggerFactory.getLogger; +import de.mlessmann.certassist.except.UnresolvableCLIDependency; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Objects; import java.util.Optional; - -import static org.slf4j.LoggerFactory.getLogger; +import org.apache.commons.lang3.SystemUtils; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; @Service public class ExecutableResolver { @@ -53,7 +51,10 @@ public class ExecutableResolver { } } - LOGGER.error("Could not find executable '{}' in PATH. Make sure that it exists on the of the directory and is executable.", executableName); + LOGGER.error( + "Could not find executable '{}' in PATH. Make sure that it exists on the of the directory and is executable.", + executableName + ); return Optional.empty(); } diff --git a/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java index 908823f..08d106d 100644 --- a/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java +++ b/src/main/java/de/mlessmann/certassist/except/UnresolvableCLIDependency.java @@ -9,7 +9,12 @@ public class UnresolvableCLIDependency extends Exception { private final String propertyName; public UnresolvableCLIDependency(String executableName, String propertyName) { - super("Could not resolve executable for '%s'. (Use property '%s' to point the application directly to the executable.)".formatted(executableName, propertyName)); + super( + "Could not resolve executable for '%s'. (Use property '%s' to point the application directly to the executable.)".formatted( + executableName, + propertyName + ) + ); this.executableName = executableName; this.propertyName = propertyName; } diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java index d6aa45b..4d6ab9d 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequest.java @@ -1,10 +1,9 @@ package de.mlessmann.certassist.openssl; +import java.util.UUID; import lombok.Builder; import lombok.Data; -import java.util.UUID; - @Data @Builder public class CertificateRequest { @@ -15,26 +14,32 @@ public class CertificateRequest { private RequestType type; private String commonName; private String trustingAuthority; + @Builder.Default private int requestedKeyLength = 4096; + @Builder.Default private int requestedValidityDays = 365; + private CertificateSubject subject; private CertificateRequestExtension extension; public enum RequestType { ROOT_AUTHORITY, STANDALONE_CERTIFICATE, - NORMAL_CERTIFICATE + NORMAL_CERTIFICATE, } public static class CertificateRequestBuilder { + public CertificateRequestBuilder subject(CertificateSubject.CertificateSubjectBuilder builder) { this.subject = builder.build(); return this; } - public CertificateRequestBuilder extension(CertificateRequestExtension.CertificateRequestExtensionBuilder builder) { + public CertificateRequestBuilder extension( + CertificateRequestExtension.CertificateRequestExtensionBuilder builder + ) { this.extension = builder.build(); return this; } diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java index f28e94e..4350717 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateRequestExtension.java @@ -1,10 +1,9 @@ package de.mlessmann.certassist.openssl; +import java.util.List; import lombok.Builder; import lombok.Getter; -import java.util.List; - @Getter @Builder public class CertificateRequestExtension { @@ -12,6 +11,7 @@ public class CertificateRequestExtension { private List alternativeNames; public static class CertificateRequestExtensionBuilder { + public CertificateRequestExtensionBuilder alternativeNames(String... altNames) { this.alternativeNames = List.of(altNames); return this; diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index c571ae5..1649335 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -1,8 +1,18 @@ package de.mlessmann.certassist.openssl; +import static org.slf4j.LoggerFactory.getLogger; + import de.mlessmann.certassist.ExecutableResolver; import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.UnresolvableCLIDependency; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -12,30 +22,21 @@ import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutionException; - -import static org.slf4j.LoggerFactory.getLogger; - @Service public class OpenSSLCertificateCreator { - public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; + public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = + "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); - private static final String CSR_EXT_TEMPLATE = """ - authorityKeyIdentifier=keyid,issuer - basicConstraints=CA:FALSE - keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment - subjectAltName = @alt_names - - [alt_names] - """; + private static final String CSR_EXT_TEMPLATE = + """ + authorityKeyIdentifier=keyid,issuer + basicConstraints=CA:FALSE + keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment + subjectAltName = @alt_names + + [alt_names] + """; private final ExecutableResolver executableResolver; @@ -45,7 +46,12 @@ public class OpenSSLCertificateCreator { } private static String buildSubjectArg(CertificateRequest request) { - String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject().getCountry()).replace("STATE", request.getSubject().getState()).replace("LOCALITY", request.getSubject().getLocality()).replace("ORGANIZATION", request.getSubject().getOrganization()).replace("COMMON-NAME", request.getCommonName()); + String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE + .replace("ISO-COUNTRY", request.getSubject().getCountry()) + .replace("STATE", request.getSubject().getState()) + .replace("LOCALITY", request.getSubject().getLocality()) + .replace("ORGANIZATION", request.getSubject().getOrganization()) + .replace("COMMON-NAME", request.getCommonName()); if (StringUtils.isNotBlank(request.getSubject().getOrganizationalUnit())) { certSubject += "/OU=" + request.getSubject().getOrganizationalUnit(); @@ -58,7 +64,8 @@ public class OpenSSLCertificateCreator { } @Nullable - public OpenSSLCertificateResult createCertificate(CertificateRequest request) throws CommandLineOperationException, InterruptedException { + public OpenSSLCertificateResult createCertificate(CertificateRequest request) + throws CommandLineOperationException, InterruptedException { Path tmpDir; try { tmpDir = Files.createTempDirectory("certassist"); @@ -75,12 +82,26 @@ public class OpenSSLCertificateCreator { return new OpenSSLCertificateResult(tmpDir); } - private Path createKeyfile(CertificateRequest request, Path outFile) throws CommandLineOperationException, InterruptedException { + private Path createKeyfile(CertificateRequest request, Path outFile) + throws CommandLineOperationException, InterruptedException { Path keyFile = outFile.toAbsolutePath(); LOGGER.atDebug().log("Writing new certificate key to {}", keyFile); try { - StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", keyFile.toString(), "-passout", "env:KEY_PASS", Integer.toString(request.getRequestedKeyLength())).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + StartedProcess keygenProc = new ProcessExecutor() + .command( + resolveOpenSSL(), + "genrsa", + "-out", + keyFile.toString(), + "-passout", + "env:KEY_PASS", + Integer.toString(request.getRequestedKeyLength()) + ) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller().asDebug()) + .redirectError(Slf4jStream.ofCaller().asError()) + .start(); keygenProc.getFuture().get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); @@ -90,12 +111,36 @@ public class OpenSSLCertificateCreator { return keyFile; } - private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile) throws CommandLineOperationException, InterruptedException { + private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile) + throws CommandLineOperationException, InterruptedException { LOGGER.atDebug().log("Writing new certificate file {}", outFile); String certSubject = buildSubjectArg(request); try { - StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-passin", "env:KEY_PASS", "-key", keyFile.toString(), "-sha256", "-days", Integer.toString(request.getRequestedValidityDays()), "-out", outFile.toString(), "-passout", "env:KEY_PASS", "-utf8", "-subj", certSubject).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + StartedProcess certGenProc = new ProcessExecutor() + .command( + resolveOpenSSL(), + "req", + "-new", + "-passin", + "env:KEY_PASS", + "-key", + keyFile.toString(), + "-sha256", + "-days", + Integer.toString(request.getRequestedValidityDays()), + "-out", + outFile.toString(), + "-passout", + "env:KEY_PASS", + "-utf8", + "-subj", + certSubject + ) + .environment("KEY_PASS", request.getOid()) + .redirectOutput(Slf4jStream.ofCaller().asDebug()) + .redirectError(Slf4jStream.ofCaller().asError()) + .start(); certGenProc.getFuture().get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL req command.", e); @@ -105,16 +150,18 @@ public class OpenSSLCertificateCreator { return outFile; } - 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) + throws CommandLineOperationException, InterruptedException { Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt")); LOGGER.atDebug().log("Writing new signed certificate file {}", outFile); Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext")); try { String extContent = CSR_EXT_TEMPLATE; - List altNames = Optional.ofNullable(request.getExtension()) - .map(CertificateRequestExtension::getAlternativeNames) - .orElse(List.of()); + List altNames = Optional + .ofNullable(request.getExtension()) + .map(CertificateRequestExtension::getAlternativeNames) + .orElse(List.of()); if (!altNames.isEmpty()) { String altNamesContent = String.join("\n", altNames); extContent = extContent.replaceAll("\\[alt_names]\n?, ", "[alt_names]\n" + altNamesContent); @@ -124,13 +171,40 @@ public class OpenSSLCertificateCreator { } LOGGER.debug("Writing extension file content: \n {}", extContent); - Files.writeString(extFile, extContent, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + Files.writeString( + extFile, + extContent, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ); } catch (IOException e) { throw new RuntimeException(e); } try { - StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "x509", "-req", "-days", Integer.toString(request.getRequestedValidityDays()), "-in", csrFile.toString(), "-CA", caCert.toString(), "-CAkey", caKey.toString(), "-CAcreateserial", "-out", outFile.toString(), "-extfile", extFile.toString()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start(); + StartedProcess certGenProc = new ProcessExecutor() + .command( + resolveOpenSSL(), + "x509", + "-req", + "-days", + Integer.toString(request.getRequestedValidityDays()), + "-in", + csrFile.toString(), + "-CA", + caCert.toString(), + "-CAkey", + caKey.toString(), + "-CAcreateserial", + "-out", + outFile.toString(), + "-extfile", + extFile.toString() + ) + .redirectOutput(Slf4jStream.ofCaller().asDebug()) + .redirectError(Slf4jStream.ofCaller().asError()) + .start(); certGenProc.getFuture().get(); } catch (IOException e) { throw new CommandLineOperationException("Failure running OpenSSL x509 command.", e); diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java index a5de8e9..82d4860 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -1,14 +1,13 @@ package de.mlessmann.certassist.openssl; -import de.mlessmann.certassist.DeleteRecursiveFileVisitor; -import org.slf4j.Logger; +import static org.slf4j.LoggerFactory.getLogger; +import de.mlessmann.certassist.DeleteRecursiveFileVisitor; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; - -import static org.slf4j.LoggerFactory.getLogger; +import org.slf4j.Logger; public class OpenSSLCertificateResult implements AutoCloseable { diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 9423509..be4fd7a 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -20,7 +20,13 @@ public class TestOpenSSLCertificateCreator { @Test void testCertificateCreation() throws Exception { - CertificateRequest certRequest = CertificateRequest.builder().commonName("test.home").type(RequestType.STANDALONE_CERTIFICATE).subject(CertificateSubject.builder().country("DE").state("SH").locality("HH").organization("Crazy-Cats")).extension(CertificateRequestExtension.builder().alternativeNames("test2.home", "test3.home")).build(); + CertificateRequest certRequest = CertificateRequest + .builder() + .commonName("test.home") + .type(RequestType.STANDALONE_CERTIFICATE) + .subject(CertificateSubject.builder().country("DE").state("SH").locality("HH").organization("Crazy-Cats")) + .extension(CertificateRequestExtension.builder().alternativeNames("test2.home", "test3.home")) + .build(); try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { System.out.println("Certificate created: " + cert); -- 2.45.3 From 9c1f1a27e83b327f1baa20a1d064a6f61e890a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 17 Nov 2024 20:33:12 +0100 Subject: [PATCH 11/11] fix: Remove dummy exception from test --- .../de/mlessmann/certassist/TestOpenSSLCertificateCreator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index be4fd7a..ee6bf0d 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -31,6 +31,5 @@ public class TestOpenSSLCertificateCreator { try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { System.out.println("Certificate created: " + cert); } - throw new RuntimeException("Test not implemented"); } } -- 2.45.3