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 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue