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:
parent
6b1c969ce6
commit
0cac57dd15
1 changed files with 101 additions and 99 deletions
|
@ -3,6 +3,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 static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.concurrent.TimeUnit.*;
|
import static java.util.concurrent.TimeUnit.*;
|
||||||
|
import static lombok.AccessLevel.PRIVATE;
|
||||||
import static org.slf4j.LoggerFactory.getLogger;
|
import static org.slf4j.LoggerFactory.getLogger;
|
||||||
|
|
||||||
import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
|
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;
|
||||||
import de.mlessmann.certassist.models.CertificateInfoSubject.CertificateInfoSubjectBuilder;
|
import de.mlessmann.certassist.models.CertificateInfoSubject.CertificateInfoSubjectBuilder;
|
||||||
import de.mlessmann.certassist.service.ExecutableResolver;
|
import de.mlessmann.certassist.service.ExecutableResolver;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -49,9 +50,9 @@ import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OpenSSLService {
|
public class OpenSSLService {
|
||||||
|
|
||||||
private static final Logger openSSLLogger = getLogger("OpenSSL-Logger");
|
|
||||||
public static final String OPENSSL_CERT_SUBJECT_TEMPLATE =
|
public static final String OPENSSL_CERT_SUBJECT_TEMPLATE =
|
||||||
"/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME";
|
"/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 =
|
private static final String CSR_EXT_TEMPLATE =
|
||||||
"""
|
"""
|
||||||
authorityKeyIdentifier=keyid,issuer
|
authorityKeyIdentifier=keyid,issuer
|
||||||
|
@ -369,6 +370,7 @@ public class OpenSSLService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies a passphrase against a provided key.
|
* 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).)
|
* @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)
|
public boolean verifyKeyPassphrase(@NonNull Path keyFile, @NonNull String passphrase)
|
||||||
|
@ -495,48 +497,40 @@ public class OpenSSLService {
|
||||||
public String getCertificateFingerprint(@NonNull Path certificate) throws CommandLineOperationException {
|
public String getCertificateFingerprint(@NonNull Path certificate) throws CommandLineOperationException {
|
||||||
requireNonNull(certificate, "Certificate must be provided to generate fingerprint.");
|
requireNonNull(certificate, "Certificate must be provided to generate fingerprint.");
|
||||||
|
|
||||||
StartedProcess fingerprintProc = null;
|
|
||||||
try {
|
try {
|
||||||
fingerprintProc =
|
return getCertificateFingerprint(Files.readString(certificate));
|
||||||
new ProcessExecutor()
|
} catch (IOException e) {
|
||||||
.command(resolveOpenSSL(), "x509", "-in", certificate.toString(), "-noout", "-fingerprint")
|
throw new CommandLineOperationException("Certificate content could not be read.", e);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@NonNull
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public String getCertificateFingerPrint(X509Certificate jdkCert) {
|
public String getCertificateFingerPrint(X509Certificate jdkCert) {
|
||||||
|
@ -551,71 +545,25 @@ public class OpenSSLService {
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public String getCertificateFingerprint(@NonNull String pemContent) throws CommandLineOperationException {
|
public String getCertificateFingerprint(@NonNull String pemContent) throws CommandLineOperationException {
|
||||||
requireNonNull(pemContent, "Certificate PEM content must be provided to generate fingerprint from string.");
|
requireNonNull(pemContent, "Certificate PEM content must be provided to generate fingerprint from string.");
|
||||||
Path tmpFile = Files.createTempFile(CERTASSIST_TMP_PREFIX, ".pem");
|
|
||||||
try {
|
StartedProcess fingerprintProc = null;
|
||||||
Files.writeString(tmpFile, pemContent);
|
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||||
return getCertificateFingerprint(tmpFile);
|
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 {
|
} finally {
|
||||||
try {
|
killIfActive(fingerprintProc);
|
||||||
Files.deleteIfExists(tmpFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Unable to delete temporary file, adding to shutdown hook. {}", tmpFile);
|
|
||||||
tmpFile.toFile().deleteOnExit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public CertificateInfo getCertificateInfo(String pemContent) {
|
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;
|
StartedProcess infoProc = null;
|
||||||
try {
|
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||||
infoProc =
|
infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start();
|
||||||
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();
|
|
||||||
var infoResult = infoProc.getFuture().get(30, SECONDS);
|
var infoResult = infoProc.getFuture().get(30, SECONDS);
|
||||||
String output = infoResult.getOutput().getUTF8();
|
String output = infoResult.getOutput().getUTF8();
|
||||||
if (infoResult.getExitValue() != 0) {
|
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
|
@NonNull
|
||||||
private String resolveOpenSSL() throws CommandLineOperationException {
|
private String resolveOpenSSL() throws CommandLineOperationException {
|
||||||
try {
|
try {
|
||||||
|
@ -747,4 +705,48 @@ public class OpenSSLService {
|
||||||
killIfActive(keyReadProc);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue