diff --git a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java index baba454..b6322c4 100644 --- a/src/main/java/de/mlessmann/certassist/ExecutableResolver.java +++ b/src/main/java/de/mlessmann/certassist/ExecutableResolver.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; import org.springframework.beans.factory.annotation.Value; @@ -16,6 +17,8 @@ import org.springframework.stereotype.Service; @Slf4j public class ExecutableResolver { + private static final AtomicBoolean loggedPath = new AtomicBoolean(false); + @Value("${openssl.path:#{null}}") private String opensslPath; @@ -39,6 +42,13 @@ public class ExecutableResolver { Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!"); String[] pathEntries = envPath.split(File.pathSeparator); + if (!loggedPath.get()) { + loggedPath.set(true); + for (String pathEntry : pathEntries) { + log.atError().log("Path entry: {}", pathEntry); + } + } + for (String pathEntry : pathEntries) { for (String fileExtension : getAllowedExtensions()) { Path executablePath = Path.of(pathEntry, executableName + fileExtension); diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 55f6cae..85025d7 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -2,6 +2,7 @@ package de.mlessmann.certassist.openssl; import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX; +import de.mlessmann.certassist.DeleteRecursiveFileVisitor; import de.mlessmann.certassist.ExecutableResolver; import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.UnresolvableCLIDependency; @@ -23,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; @@ -211,17 +213,56 @@ public class OpenSSLCertificateCreator { return outFile; } - public boolean verifyCertificate(Path certFile) throws CommandLineOperationException { + public boolean verifyCertificate(@NonNull Path fullChainFile, @NonNull Path trustedCA) + throws CommandLineOperationException { + return verifyCertificate(fullChainFile, List.of(trustedCA)); + } + + public boolean verifyCertificate(@NonNull Path fullChainFile, @NonNull List trustedCAs) + throws CommandLineOperationException { + if (CollectionUtils.isEmpty(trustedCAs)) { + throw new IllegalArgumentException( + "At least one trusted CA certificate must be provided to run the verification command." + ); + } + + Path tmpDir = null; try { + Path tempTrustedBundle; + if (trustedCAs.size() == 1) { + tempTrustedBundle = trustedCAs.getFirst(); + } else { + tmpDir = Files.createTempDirectory(CERTASSIST_TMP_PREFIX); + tempTrustedBundle = tmpDir.resolve("trusted_bundle.pem"); + for (Path ca : trustedCAs) { + Files.writeString( + tempTrustedBundle, + Files.readString(ca), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } + } + StartedProcess verifyCommand = new ProcessExecutor() - .command(resolveOpenSSL(), "x509", "-in", certFile.toString(), "-text", "-noout") - .redirectOutput(Slf4jStream.ofCaller().asDebug()) + .command(resolveOpenSSL(), "verify", "-CAfile", tempTrustedBundle.toString(), fullChainFile.toString()) + .redirectOutput(Slf4jStream.ofCaller().asError()) .redirectError(Slf4jStream.ofCaller().asError()) .start(); var verifyResult = verifyCommand.getFuture().get(); return verifyResult.getExitValue() == 0; } catch (IOException | InterruptedException | ExecutionException e) { throw new RuntimeException(e); + } finally { + if (tmpDir != null) { + try { + Files.walkFileTree(tmpDir, new DeleteRecursiveFileVisitor()); + Files.deleteIfExists(tmpDir); + } catch (IOException e) { + log.error("Failed to clean up temporary verification directory: {}", tmpDir, e); + } + } } } diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index 224be51..eb076ad 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -5,6 +5,9 @@ import static org.mockito.Mockito.*; import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,7 +40,8 @@ class TestOpenSSLCertificateCreator { .build(); try (var cert = certificateCreator.createCertificate(certRequest)) { - assertThat(certificateCreator.verifyCertificate(cert.certificatePath())).isEqualTo(true); + assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), cert.certificatePath())) + .isEqualTo(true); System.out.println("Certificate created: " + cert); CertificateRequest childRequest = CertificateRequest @@ -55,7 +59,11 @@ class TestOpenSSLCertificateCreator { 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); + Path fullchain = childCert.certificatePath().getParent().resolve("fullchain.pem"); + Files.write(fullchain, Files.readAllBytes(childCert.certificatePath()), StandardOpenOption.CREATE); + Files.write(fullchain, Files.readAllBytes(cert.certificatePath()), StandardOpenOption.APPEND); + + assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), fullchain)).isEqualTo(true); } } }