feat: Allow some OpenSSL commands to happen inline

- This reduces the number of disk reads/writes and allows for less potential IOExceptions
This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-24 13:44:34 +01:00
parent 6b1c969ce6
commit 0cac57dd15

View file

@ -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, <em>any password</em> 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<String> 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<String> 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());
}
}
}