feat: Actually encrypt private keys on disk and in DB #16

Merged
MarkL4YG merged 10 commits from feat/pkeyEncryption into main 2024-11-23 16:58:55 +00:00
2 changed files with 57 additions and 50 deletions
Showing only changes of commit 3f7e41b245 - Show all commits

View file

@ -1,6 +1,7 @@
package de.mlessmann.certassist.openssl; 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.concurrent.TimeUnit.*;
import static org.slf4j.LoggerFactory.getLogger; import static org.slf4j.LoggerFactory.getLogger;
import de.mlessmann.certassist.DeleteRecursiveFileVisitor; import de.mlessmann.certassist.DeleteRecursiveFileVisitor;
@ -18,7 +19,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -95,7 +95,7 @@ public class OpenSSLCertificateCreator {
try { try {
log.debug("Process is still alive. Asking politely for it to destroy itself and waiting on exit for 2s"); log.debug("Process is still alive. Asking politely for it to destroy itself and waiting on exit for 2s");
sysProc.destroy(); sysProc.destroy();
sysProc.waitFor(2_000, TimeUnit.MILLISECONDS); sysProc.waitFor(2_000, MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.debug("Interrupted while waiting for process to terminate. Registering forceful termination onExit", e); log.debug("Interrupted while waiting for process to terminate. Registering forceful termination onExit", e);
Runtime.getRuntime().addShutdownHook(new Thread(sysProc::destroyForcibly)); Runtime.getRuntime().addShutdownHook(new Thread(sysProc::destroyForcibly));
@ -103,8 +103,7 @@ public class OpenSSLCertificateCreator {
} }
@NonNull @NonNull
public OpenSSLCertificateResult createCertificate(CertificateRequest request) public OpenSSLCertificateResult createCertificate(CertificateRequest request) throws CommandLineOperationException {
throws CommandLineOperationException, InterruptedException {
Path tmpDir; Path tmpDir;
try { try {
tmpDir = Files.createTempDirectory(CERTASSIST_TMP_PREFIX); tmpDir = Files.createTempDirectory(CERTASSIST_TMP_PREFIX);
@ -151,12 +150,14 @@ public class OpenSSLCertificateCreator {
} }
private Path createKeyfile(CertificateRequest request, Path outFile, String filePassword) private Path createKeyfile(CertificateRequest request, Path outFile, String filePassword)
throws CommandLineOperationException, InterruptedException { throws CommandLineOperationException {
Path keyFile = outFile.toAbsolutePath(); Path keyFile = outFile.toAbsolutePath();
log.debug("Writing new certificate key to {}", keyFile); log.debug("Writing new certificate key to {}", keyFile);
StartedProcess keygenProc = null;
try { try {
StartedProcess keygenProc = new ProcessExecutor() keygenProc =
new ProcessExecutor()
.command( .command(
resolveOpenSSL(), resolveOpenSSL(),
"genrsa", "genrsa",
@ -171,17 +172,17 @@ public class OpenSSLCertificateCreator {
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug()) .redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
keygenProc.getFuture().get(); keygenProc.getFuture().get(3, MINUTES);
} catch (IOException e) { } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e);
} catch (ExecutionException e) { } finally {
throw new RuntimeException(e); killIfActive(keygenProc);
} }
return keyFile; return keyFile;
} }
private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile, String keyPassphrase) private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile, String keyPassphrase)
throws CommandLineOperationException, InterruptedException { throws CommandLineOperationException {
log.debug("Writing new certificate file {}", outFile); log.debug("Writing new certificate file {}", outFile);
String certSubject = buildSubjectArg(request); String certSubject = buildSubjectArg(request);
@ -211,11 +212,9 @@ public class OpenSSLCertificateCreator {
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug()) .redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
certGenProc.getFuture().get(); certGenProc.getFuture().get(30, SECONDS);
} catch (IOException e) { } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
throw new CommandLineOperationException("Failure running OpenSSL req command.", e); throw new CommandLineOperationException("Failure running OpenSSL req command.", e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally { } finally {
killIfActive(certGenProc); killIfActive(certGenProc);
} }
@ -223,12 +222,14 @@ public class OpenSSLCertificateCreator {
} }
private Path createSigningRequest(CertificateRequest request, Path keyFile, Path outFile, String certPassword) private Path createSigningRequest(CertificateRequest request, Path keyFile, Path outFile, String certPassword)
throws CommandLineOperationException, InterruptedException { throws CommandLineOperationException {
log.atDebug().log("Writing new certificate signing request file {}", outFile); log.atDebug().log("Writing new certificate signing request file {}", outFile);
String certSubject = buildSubjectArg(request); String certSubject = buildSubjectArg(request);
StartedProcess certGenProc = null;
try { try {
StartedProcess certGenProc = new ProcessExecutor() certGenProc =
new ProcessExecutor()
.command( .command(
resolveOpenSSL(), resolveOpenSSL(),
"req", "req",
@ -248,11 +249,11 @@ public class OpenSSLCertificateCreator {
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug()) .redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
certGenProc.getFuture().get(); certGenProc.getFuture().get(30, SECONDS);
} catch (IOException e) { } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
throw new CommandLineOperationException("Failure running OpenSSL req command.", e); throw new CommandLineOperationException("Failure running OpenSSL req command.", e);
} catch (ExecutionException e) { } finally {
throw new RuntimeException(e); killIfActive(certGenProc);
} }
return outFile; return outFile;
} }
@ -271,6 +272,7 @@ public class OpenSSLCertificateCreator {
} }
Path tmpDir = null; Path tmpDir = null;
StartedProcess verifyCommand = null;
try { try {
Path tempTrustedBundle; Path tempTrustedBundle;
if (trustedCAs.size() == 1) { if (trustedCAs.size() == 1) {
@ -289,16 +291,18 @@ public class OpenSSLCertificateCreator {
} }
} }
StartedProcess verifyCommand = new ProcessExecutor() verifyCommand =
new ProcessExecutor()
.command(resolveOpenSSL(), "verify", "-CAfile", tempTrustedBundle.toString(), fullChainFile.toString()) .command(resolveOpenSSL(), "verify", "-CAfile", tempTrustedBundle.toString(), fullChainFile.toString())
.redirectOutput(Slf4jStream.of(openSSLLogger).asError()) .redirectOutput(Slf4jStream.of(openSSLLogger).asError())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
var verifyResult = verifyCommand.getFuture().get(); var verifyResult = verifyCommand.getFuture().get(30, SECONDS);
return verifyResult.getExitValue() == 0; return verifyResult.getExitValue() == 0;
} catch (IOException | InterruptedException | ExecutionException e) { } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally { } finally {
killIfActive(verifyCommand);
if (tmpDir != null) { if (tmpDir != null) {
try { try {
Files.walkFileTree(tmpDir, new DeleteRecursiveFileVisitor()); Files.walkFileTree(tmpDir, new DeleteRecursiveFileVisitor());
@ -313,7 +317,7 @@ public class OpenSSLCertificateCreator {
/** /**
* Checks whether the provided key file is encrypted using a passphrase * Checks whether the provided key file is encrypted using a passphrase
*/ */
public boolean isKeyEncrypted(@NonNull Path keyFile) throws InterruptedException, CommandLineOperationException { public boolean isKeyEncrypted(@NonNull Path keyFile) throws CommandLineOperationException {
// If the key is not encrypted, any passphrase will work -> so generate a random one to check. // If the key is not encrypted, any passphrase will work -> so generate a random one to check.
String passphrase = UUID.randomUUID().toString(); String passphrase = UUID.randomUUID().toString();
boolean firstPass = verifyKeyPassphrase(keyFile, passphrase); boolean firstPass = verifyKeyPassphrase(keyFile, passphrase);
@ -330,9 +334,11 @@ public class OpenSSLCertificateCreator {
* @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)
throws CommandLineOperationException, InterruptedException { throws CommandLineOperationException {
StartedProcess verifyCommand = null;
try { try {
StartedProcess verifyCommand = new ProcessExecutor() verifyCommand =
new ProcessExecutor()
.command( .command(
resolveOpenSSL(), resolveOpenSSL(),
"rsa", "rsa",
@ -346,10 +352,12 @@ public class OpenSSLCertificateCreator {
.redirectOutput(Slf4jStream.of(openSSLLogger).asError()) .redirectOutput(Slf4jStream.of(openSSLLogger).asError())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
var verifyResult = verifyCommand.getFuture().get(); var verifyResult = verifyCommand.getFuture().get(30, SECONDS);
return verifyResult.getExitValue() == 0; return verifyResult.getExitValue() == 0;
} catch (IOException | InterruptedException | ExecutionException e) { } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
throw new CommandLineOperationException("Failed to verify key encryption", e); throw new CommandLineOperationException("Failed to verify key encryption", e);
} finally {
killIfActive(verifyCommand);
} }
} }
@ -359,7 +367,7 @@ public class OpenSSLCertificateCreator {
Path caKey, Path caKey,
String caKeyPassphrase, String caKeyPassphrase,
Path csrFile Path csrFile
) throws CommandLineOperationException, InterruptedException { ) throws CommandLineOperationException {
Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt")); Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt"));
log.debug("Writing new signed certificate file {}", outFile); log.debug("Writing new signed certificate file {}", outFile);
Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext")); Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext"));
@ -422,31 +430,31 @@ public class OpenSSLCertificateCreator {
.redirectOutput(Slf4jStream.of(openSSLLogger).asDebug()) .redirectOutput(Slf4jStream.of(openSSLLogger).asDebug())
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
ProcessResult result = certGenProc.getFuture().get(30, TimeUnit.SECONDS); ProcessResult result = certGenProc.getFuture().get(30, SECONDS);
// Check exit code // Check exit code
if (result.getExitValue() != 0) { if (result.getExitValue() != 0) {
throw new CommandLineOperationException("Failed to sign certificate. Exit code: " + result.getExitValue()); throw new CommandLineOperationException(
"Failed to sign certificate. Exit code: " + result.getExitValue()
);
} }
} catch (IOException | TimeoutException | ExecutionException | InterruptedException e) {
} catch (IOException | TimeoutException e) {
throw new CommandLineOperationException("Failure running OpenSSL x509 command.", e); throw new CommandLineOperationException("Failure running OpenSSL x509 command.", e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally { } finally {
killIfActive(certGenProc); killIfActive(certGenProc);
} }
return outFile; return outFile;
} }
public String getCertificateFingerprint(Path certificate) public String getCertificateFingerprint(Path certificate) throws CommandLineOperationException {
throws CommandLineOperationException, InterruptedException { StartedProcess fingerprintProc = null;
try { try {
StartedProcess fingerprintProc = new ProcessExecutor() fingerprintProc =
new ProcessExecutor()
.command(resolveOpenSSL(), "x509", "-in", certificate.toString(), "-noout", "-fingerprint") .command(resolveOpenSSL(), "x509", "-in", certificate.toString(), "-noout", "-fingerprint")
.readOutput(true) .readOutput(true)
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
var fingerprintResult = fingerprintProc.getFuture().get(); var fingerprintResult = fingerprintProc.getFuture().get(30, SECONDS);
String output = fingerprintResult.getOutput().getUTF8(); String output = fingerprintResult.getOutput().getUTF8();
if (fingerprintResult.getExitValue() != 0) { if (fingerprintResult.getExitValue() != 0) {
@ -473,14 +481,18 @@ public class OpenSSLCertificateCreator {
); );
} }
return "%s;%s".formatted(algorithm, fingerprint); return "%s;%s".formatted(algorithm, fingerprint);
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException | TimeoutException | InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally {
killIfActive(fingerprintProc);
} }
} }
public CertificateRequest getCertificateInfo(Path path) throws CommandLineOperationException, InterruptedException { public CertificateRequest getCertificateInfo(Path path) throws CommandLineOperationException {
StartedProcess infoProc = null;
try { try {
StartedProcess infoProc = new ProcessExecutor() infoProc =
new ProcessExecutor()
.command( .command(
resolveOpenSSL(), resolveOpenSSL(),
"x509", "x509",
@ -507,7 +519,7 @@ public class OpenSSLCertificateCreator {
.readOutput(true) .readOutput(true)
.redirectError(Slf4jStream.of(openSSLLogger).asError()) .redirectError(Slf4jStream.of(openSSLLogger).asError())
.start(); .start();
var infoResult = infoProc.getFuture().get(); 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) {
log.debug("Certificate info command output:\n{}", output); log.debug("Certificate info command output:\n{}", output);
@ -516,7 +528,7 @@ public class OpenSSLCertificateCreator {
); );
} }
return getCertificateInfo(output.lines().toArray(String[]::new)); return getCertificateInfo(output.lines().toArray(String[]::new));
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View file

@ -32,9 +32,6 @@ public class CertificateCreationService {
) { ) {
certificate.setPrivateKey(Files.readAllBytes(certificateCreatorResult.certificateKeyPath())); certificate.setPrivateKey(Files.readAllBytes(certificateCreatorResult.certificateKeyPath()));
certificate.setCert(Files.readAllBytes(certificateCreatorResult.certificatePath())); certificate.setCert(Files.readAllBytes(certificateCreatorResult.certificatePath()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted exception", e);
} catch (CommandLineOperationException | IOException e) { } catch (CommandLineOperationException | IOException e) {
throw new IllegalStateException("Failed to create certificate!", e); throw new IllegalStateException("Failed to create certificate!", e);
} }
@ -81,8 +78,6 @@ public class CertificateCreationService {
return certificateRepository.save(entity); return certificateRepository.save(entity);
} catch (CommandLineOperationException | IOException e) { } catch (CommandLineOperationException | IOException e) {
throw new RuntimeException("Unable to import certificate", e); throw new RuntimeException("Unable to import certificate", e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} }
} }