From 0cac57dd151aedbecb4b55db7563ade61cc0b594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40MarkL4YG=29?= Date: Sun, 24 Nov 2024 13:44:34 +0100 Subject: [PATCH] feat: Allow some OpenSSL commands to happen inline - This reduces the number of disk reads/writes and allows for less potential IOExceptions --- .../certassist/openssl/OpenSSLService.java | 200 +++++++++--------- 1 file changed, 101 insertions(+), 99 deletions(-) diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLService.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLService.java index 1c09421..294a623 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLService.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLService.java @@ -3,6 +3,7 @@ package de.mlessmann.certassist.openssl; import static de.mlessmann.certassist.Constants.CERTASSIST_TMP_PREFIX; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.*; +import static lombok.AccessLevel.PRIVATE; import static org.slf4j.LoggerFactory.getLogger; import de.mlessmann.certassist.DeleteRecursiveFileVisitor; @@ -14,15 +15,14 @@ import de.mlessmann.certassist.models.CertificateInfoExtension; import de.mlessmann.certassist.models.CertificateInfoSubject; import de.mlessmann.certassist.models.CertificateInfoSubject.CertificateInfoSubjectBuilder; import de.mlessmann.certassist.service.ExecutableResolver; +import java.io.ByteArrayInputStream; 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.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -49,9 +50,9 @@ import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; @Slf4j public class OpenSSLService { - private static final Logger openSSLLogger = getLogger("OpenSSL-Logger"); public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; + private static final Logger openSSLLogger = getLogger("OpenSSL-Logger"); private static final String CSR_EXT_TEMPLATE = """ authorityKeyIdentifier=keyid,issuer @@ -369,6 +370,7 @@ public class OpenSSLService { /** * Verifies a passphrase against a provided key. + * * @implNote Due to the implementation of the OpenSSL cli, any password will be valid for unencrypted keys. (Check with {@link #isKeyEncrypted(Path).) */ public boolean verifyKeyPassphrase(@NonNull Path keyFile, @NonNull String passphrase) @@ -495,48 +497,40 @@ public class OpenSSLService { public String getCertificateFingerprint(@NonNull Path certificate) throws CommandLineOperationException { requireNonNull(certificate, "Certificate must be provided to generate fingerprint."); - StartedProcess fingerprintProc = null; try { - fingerprintProc = - new ProcessExecutor() - .command(resolveOpenSSL(), "x509", "-in", certificate.toString(), "-noout", "-fingerprint") - .readOutput(true) - .redirectError(Slf4jStream.of(openSSLLogger).asError()) - .start(); - var fingerprintResult = fingerprintProc.getFuture().get(30, SECONDS); - String output = fingerprintResult.getOutput().getUTF8(); - - if (fingerprintResult.getExitValue() != 0) { - log.debug("Fingerprint command output:\n{}", output); - throw new CommandLineOperationException( - "Failed to get fingerprint of certificate. Exit code: %d".formatted( - fingerprintResult.getExitValue() - ) - ); - } - - Matcher matcher = FINGERPRINT_EXTRACTOR.matcher(output); - if (!matcher.find()) { - log.debug(output); - throw new CommandLineOperationException( - "Unexpected output of fingerprint command. (See log for more details)" - ); - } - String algorithm = matcher.group("algo"); - String fingerprint = matcher.group("finger"); - if (StringUtils.isBlank(algorithm) || StringUtils.isBlank(fingerprint)) { - throw new CommandLineOperationException( - "Unexpected output of fingerprint command: %s %s".formatted(algorithm, fingerprint) - ); - } - return "%s;%s".formatted(algorithm, fingerprint); - } catch (IOException | ExecutionException | TimeoutException | InterruptedException e) { - throw new RuntimeException(e); - } finally { - killIfActive(fingerprintProc); + return getCertificateFingerprint(Files.readString(certificate)); + } catch (IOException e) { + throw new CommandLineOperationException("Certificate content could not be read.", e); } } + private String extractFingerprintFromOutput(ProcessResult fingerprintResult) throws CommandLineOperationException { + String output = fingerprintResult.getOutput().getUTF8(); + + if (fingerprintResult.getExitValue() != 0) { + log.debug("Fingerprint command output:\n{}", output); + throw new CommandLineOperationException( + "Failed to get fingerprint of certificate. Exit code: %d".formatted(fingerprintResult.getExitValue()) + ); + } + + Matcher matcher = FINGERPRINT_EXTRACTOR.matcher(output); + if (!matcher.find()) { + log.debug(output); + throw new CommandLineOperationException( + "Unexpected output of fingerprint command. (See log for more details)" + ); + } + String algorithm = matcher.group("algo"); + String fingerprint = matcher.group("finger"); + if (StringUtils.isBlank(algorithm) || StringUtils.isBlank(fingerprint)) { + throw new CommandLineOperationException( + "Unexpected output of fingerprint command: %s %s".formatted(algorithm, fingerprint) + ); + } + return "%s;%s".formatted(algorithm, fingerprint); + } + @NonNull @SneakyThrows public String getCertificateFingerPrint(X509Certificate jdkCert) { @@ -551,71 +545,25 @@ public class OpenSSLService { @SneakyThrows public String getCertificateFingerprint(@NonNull String pemContent) throws CommandLineOperationException { requireNonNull(pemContent, "Certificate PEM content must be provided to generate fingerprint from string."); - Path tmpFile = Files.createTempFile(CERTASSIST_TMP_PREFIX, ".pem"); - try { - Files.writeString(tmpFile, pemContent); - return getCertificateFingerprint(tmpFile); + + StartedProcess fingerprintProc = null; + try (var input = new ByteArrayInputStream(pemContent.getBytes())) { + fingerprintProc = Commands.fingerprintCommand(resolveOpenSSL()).redirectInput(input).start(); + var fingerprintResult = fingerprintProc.getFuture().get(30, SECONDS); + return extractFingerprintFromOutput(fingerprintResult); + } catch (IOException | ExecutionException | TimeoutException | InterruptedException e) { + throw new RuntimeException(e); } finally { - try { - Files.deleteIfExists(tmpFile); - } catch (IOException e) { - log.warn("Unable to delete temporary file, adding to shutdown hook. {}", tmpFile); - tmpFile.toFile().deleteOnExit(); - } + killIfActive(fingerprintProc); } } @NonNull @SneakyThrows public CertificateInfo getCertificateInfo(String pemContent) { - Path tmpFile = Files.createTempFile(CERTASSIST_TMP_PREFIX, ".pem"); - try { - Files.writeString(tmpFile, pemContent); - return getCertificateInfo(tmpFile); - } finally { - try { - Files.deleteIfExists(tmpFile); - } catch (IOException e) { - log.warn("Unable to delete temporary file, adding to shutdown hook. {}", tmpFile); - tmpFile.toFile().deleteOnExit(); - } - } - } - - @NonNull - public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException { - requireNonNull(path, "Certificate file must be provided to read the info."); - StartedProcess infoProc = null; - try { - infoProc = - new ProcessExecutor() - .command( - resolveOpenSSL(), - "x509", - "-in", - path.toString(), - "-noout", - "-dateopt", - "iso_8601", - "-fingerprint", - "-subject", - "-issuer", - "-serial", - "-dates", - "-alias", - "-email", - "-purpose", - "-ext", - "subjectAltName", - "-nameopt", - "sep_multiline", - "-nameopt", - "lname" - ) - .readOutput(true) - .redirectError(Slf4jStream.of(openSSLLogger).asError()) - .start(); + try (var input = new ByteArrayInputStream(pemContent.getBytes())) { + infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start(); var infoResult = infoProc.getFuture().get(30, SECONDS); String output = infoResult.getOutput().getUTF8(); if (infoResult.getExitValue() != 0) { @@ -632,6 +580,16 @@ public class OpenSSLService { } } + @NonNull + public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException { + requireNonNull(path, "Certificate file must be provided to read the info."); + try { + return getCertificateInfo(Files.readString(path)); + } catch (IOException e) { + throw new CommandLineOperationException("Failed to read certificate file.", e); + } + } + @NonNull private String resolveOpenSSL() throws CommandLineOperationException { try { @@ -747,4 +705,48 @@ public class OpenSSLService { killIfActive(keyReadProc); } } + + @NoArgsConstructor(access = PRIVATE) + private static class Commands { + + private static ProcessExecutor infoCommand(String openSSL, String... additArgs) { + List command = new ArrayList<>( + List.of( + openSSL, + "x509", + "-noout", + "-dateopt", + "iso_8601", + "-fingerprint", + "-subject", + "-issuer", + "-serial", + "-dates", + "-alias", + "-email", + "-purpose", + "-ext", + "subjectAltName", + "-nameopt", + "sep_multiline", + "-nameopt", + "lname" + ) + ); + command.addAll(Arrays.asList(additArgs)); + return new ProcessExecutor() + .command(command) + .readOutput(true) + .redirectError(Slf4jStream.of(openSSLLogger).asError()); + } + + private static ProcessExecutor fingerprintCommand(String openSSL, String... additArgs) { + List command = new ArrayList<>(List.of(openSSL, "x509", "-noout", "-fingerprint")); + command.addAll(Arrays.asList(additArgs)); + return new ProcessExecutor() + .command(command) + .readOutput(true) + .redirectError(Slf4jStream.of(openSSLLogger).asError()); + } + } }