feat: Implement verification of trust chains in OpenSSLCertificateCreator

This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-22 10:24:57 +01:00
parent 2b6473929a
commit e888ea57c1
3 changed files with 64 additions and 5 deletions

View file

@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -16,6 +17,8 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
public class ExecutableResolver { public class ExecutableResolver {
private static final AtomicBoolean loggedPath = new AtomicBoolean(false);
@Value("${openssl.path:#{null}}") @Value("${openssl.path:#{null}}")
private String opensslPath; private String opensslPath;
@ -39,6 +42,13 @@ public class ExecutableResolver {
Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!"); Objects.requireNonNull(envPath, "Environment variable 'PATH' is not set?!");
String[] pathEntries = envPath.split(File.pathSeparator); 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 pathEntry : pathEntries) {
for (String fileExtension : getAllowedExtensions()) { for (String fileExtension : getAllowedExtensions()) {
Path executablePath = Path.of(pathEntry, executableName + fileExtension); Path executablePath = Path.of(pathEntry, executableName + fileExtension);

View file

@ -2,6 +2,7 @@ package de.mlessmann.certassist.openssl;
import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX; import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX;
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
import de.mlessmann.certassist.ExecutableResolver; import de.mlessmann.certassist.ExecutableResolver;
import de.mlessmann.certassist.except.CommandLineOperationException; import de.mlessmann.certassist.except.CommandLineOperationException;
import de.mlessmann.certassist.except.UnresolvableCLIDependency; import de.mlessmann.certassist.except.UnresolvableCLIDependency;
@ -23,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.StartedProcess;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
@ -211,17 +213,56 @@ public class OpenSSLCertificateCreator {
return outFile; 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<Path> 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 { 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() StartedProcess verifyCommand = new ProcessExecutor()
.command(resolveOpenSSL(), "x509", "-in", certFile.toString(), "-text", "-noout") .command(resolveOpenSSL(), "verify", "-CAfile", tempTrustedBundle.toString(), fullChainFile.toString())
.redirectOutput(Slf4jStream.ofCaller().asDebug()) .redirectOutput(Slf4jStream.ofCaller().asError())
.redirectError(Slf4jStream.ofCaller().asError()) .redirectError(Slf4jStream.ofCaller().asError())
.start(); .start();
var verifyResult = verifyCommand.getFuture().get(); var verifyResult = verifyCommand.getFuture().get();
return verifyResult.getExitValue() == 0; return verifyResult.getExitValue() == 0;
} catch (IOException | InterruptedException | ExecutionException e) { } catch (IOException | InterruptedException | ExecutionException e) {
throw new RuntimeException(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);
}
}
} }
} }

View file

@ -5,6 +5,9 @@ import static org.mockito.Mockito.*;
import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.*;
import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -37,7 +40,8 @@ class TestOpenSSLCertificateCreator {
.build(); .build();
try (var cert = certificateCreator.createCertificate(certRequest)) { 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); System.out.println("Certificate created: " + cert);
CertificateRequest childRequest = CertificateRequest CertificateRequest childRequest = CertificateRequest
@ -55,7 +59,11 @@ class TestOpenSSLCertificateCreator {
when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert); when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert);
try (var childCert = certificateCreator.createCertificate(childRequest)) { try (var childCert = certificateCreator.createCertificate(childRequest)) {
System.out.println("Child certificate created: " + childCert); 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);
} }
} }
} }