feat: Add basic support for running OpenSSL CLI #4

Merged
MarkL4YG merged 11 commits from feat/create-openssl-certs into main 2024-11-17 19:52:16 +00:00
4 changed files with 98 additions and 66 deletions
Showing only changes of commit 1238027487 - Show all commits

View file

@ -20,6 +20,7 @@ public class CertificateRequest {
@Builder.Default @Builder.Default
private int requestedValidityDays = 365; private int requestedValidityDays = 365;
private CertificateSubject subject; private CertificateSubject subject;
private CertificateRequestExtension extension;
public enum RequestType { public enum RequestType {
ROOT_AUTHORITY, ROOT_AUTHORITY,
@ -32,5 +33,10 @@ public class CertificateRequest {
this.subject = builder.build(); this.subject = builder.build();
return this; return this;
} }
public CertificateRequestBuilder extension(CertificateRequestExtension.CertificateRequestExtensionBuilder builder) {
this.extension = builder.build();
return this;
}
} }
} }

View file

@ -0,0 +1,20 @@
package de.mlessmann.certassist.openssl;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class CertificateRequestExtension {
private List<String> alternativeNames;
public static class CertificateRequestExtensionBuilder {
public CertificateRequestExtensionBuilder alternativeNames(String... altNames) {
this.alternativeNames = List.of(altNames);
return this;
}
}
}

View file

@ -13,8 +13,12 @@ import org.zeroturnaround.exec.StartedProcess;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
import java.io.IOException; import java.io.IOException;
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.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.slf4j.LoggerFactory.getLogger; import static org.slf4j.LoggerFactory.getLogger;
@ -24,6 +28,15 @@ public class OpenSSLCertificateCreator {
public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME"; public static final String OPENSSL_CERT_SUBJECT_TEMPLATE = "/C=ISO-COUNTRY/ST=STATE/L=LOCALITY/O=ORGANIZATION/CN=COMMON-NAME";
private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class); private static final Logger LOGGER = getLogger(OpenSSLCertificateCreator.class);
private static final String CSR_EXT_TEMPLATE = """
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
""";
private final ExecutableResolver executableResolver; private final ExecutableResolver executableResolver;
@Autowired @Autowired
@ -32,26 +45,14 @@ public class OpenSSLCertificateCreator {
} }
private static String buildSubjectArg(CertificateRequest request) { private static String buildSubjectArg(CertificateRequest request) {
String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject() String certSubject = OPENSSL_CERT_SUBJECT_TEMPLATE.replace("ISO-COUNTRY", request.getSubject().getCountry()).replace("STATE", request.getSubject().getState()).replace("LOCALITY", request.getSubject().getLocality()).replace("ORGANIZATION", request.getSubject().getOrganization()).replace("COMMON-NAME", request.getCommonName());
.getCountry())
.replace("STATE", request.getSubject()
.getState())
.replace("LOCALITY", request.getSubject()
.getLocality())
.replace("ORGANIZATION", request.getSubject()
.getOrganization())
.replace("COMMON-NAME", request.getCommonName());
if (StringUtils.isNotBlank(request.getSubject() if (StringUtils.isNotBlank(request.getSubject().getOrganizationalUnit())) {
.getOrganizationalUnit())) { certSubject += "/OU=" + request.getSubject().getOrganizationalUnit();
certSubject += "/OU=" + request.getSubject()
.getOrganizationalUnit();
} }
if (StringUtils.isNotBlank(request.getSubject() if (StringUtils.isNotBlank(request.getSubject().getEmailAddress())) {
.getEmailAddress())) { certSubject += "/emailAddress=" + request.getSubject().getEmailAddress();
certSubject += "/emailAddress=" + request.getSubject()
.getEmailAddress();
} }
return certSubject; return certSubject;
} }
@ -65,30 +66,22 @@ public class OpenSSLCertificateCreator {
throw new CommandLineOperationException("Could not create temporary directory for certificate creation", e); throw new CommandLineOperationException("Could not create temporary directory for certificate creation", e);
} }
createKeyfile(request, tmpDir); Path keyFile = createKeyfile(request, tmpDir.resolve("root.key"));
createCertificate(request, tmpDir); Path rootCert = createCertificate(request, keyFile, tmpDir.resolve("root.crt"));
Path childKey = createKeyfile(request, tmpDir.resolve("child.key"));
Path unsignedCert = createCertificate(request, childKey, tmpDir.resolve("child.csr"));
Path signedCert = signCertificate(request, rootCert, keyFile, unsignedCert);
return new OpenSSLCertificateResult(tmpDir); return new OpenSSLCertificateResult(tmpDir);
} }
private Path createKeyfile(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { private Path createKeyfile(CertificateRequest request, Path outFile) throws CommandLineOperationException, InterruptedException {
Path keyFile = tmpDir.resolve("root.key") Path keyFile = outFile.toAbsolutePath();
.toAbsolutePath(); LOGGER.atDebug().log("Writing new certificate key to {}", keyFile);
LOGGER.atDebug()
.log("Writing new certificate key to {}", keyFile);
try { try {
StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "genrsa", "-out", keyFile.toString(), "-passout", "env:KEY_PASS", Integer.toString(request.getRequestedKeyLength())).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start();
keyFile.toString(), keygenProc.getFuture().get();
"-passout", "env:KEY_PASS",
Integer.toString(request.getRequestedKeyLength()))
.environment("KEY_PASS", request.getOid())
.redirectOutput(Slf4jStream.ofCaller()
.asDebug())
.redirectError(Slf4jStream.ofCaller()
.asError())
.start();
keygenProc.getFuture()
.get();
} catch (IOException e) { } catch (IOException e) {
throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e); throw new CommandLineOperationException("Failure running OpenSSL keygen command.", e);
} catch (ExecutionException e) { } catch (ExecutionException e) {
@ -97,38 +90,54 @@ public class OpenSSLCertificateCreator {
return keyFile; return keyFile;
} }
private Path createCertificate(CertificateRequest request, Path tmpDir) throws CommandLineOperationException, InterruptedException { private Path createCertificate(CertificateRequest request, Path keyFile, Path outFile) throws CommandLineOperationException, InterruptedException {
Path keyFile = tmpDir.resolve("root.key") LOGGER.atDebug().log("Writing new certificate file {}", outFile);
.toAbsolutePath();
Path certFile = tmpDir.resolve("root.crt")
.toAbsolutePath();
LOGGER.atDebug()
.log("Writing new certificate file {}", certFile);
String certSubject = buildSubjectArg(request); String certSubject = buildSubjectArg(request);
try { try {
StartedProcess keygenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-nodes", StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "req", "-new", "-passin", "env:KEY_PASS", "-key", keyFile.toString(), "-sha256", "-days", Integer.toString(request.getRequestedValidityDays()), "-out", outFile.toString(), "-passout", "env:KEY_PASS", "-utf8", "-subj", certSubject).environment("KEY_PASS", request.getOid()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start();
"-key", keyFile.toString(), "-sha256", "-days", certGenProc.getFuture().get();
Integer.toString(
request.getRequestedValidityDays()),
"-out",
certFile.toString(),
"-passout", "env:KEY_PASS", "-utf8", "-subj",
certSubject)
.environment("KEY_PASS", request.getOid())
.redirectOutput(Slf4jStream.ofCaller()
.asDebug())
.redirectError(Slf4jStream.ofCaller()
.asError())
.start();
keygenProc.getFuture()
.get();
} catch (IOException e) { } catch (IOException e) {
throw new CommandLineOperationException("Failure running OpenSSL req command.", e); throw new CommandLineOperationException("Failure running OpenSSL req command.", e);
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return certFile; return outFile;
}
private Path signCertificate(CertificateRequest request, Path caCert, Path caKey, Path csrFile) throws CommandLineOperationException, InterruptedException {
Path outFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".crt"));
LOGGER.atDebug().log("Writing new signed certificate file {}", outFile);
Path extFile = csrFile.resolveSibling(csrFile.getFileName().toString().replace(".csr", ".ext"));
try {
String extContent = CSR_EXT_TEMPLATE;
List<String> altNames = Optional.ofNullable(request.getExtension())
.map(CertificateRequestExtension::getAlternativeNames)
.orElse(List.of());
if (!altNames.isEmpty()) {
String altNamesContent = String.join("\n", altNames);
extContent = extContent.replaceAll("\\[alt_names]\n?, ", "[alt_names]\n" + altNamesContent);
} else {
extContent = extContent.replaceAll("\\s*subjectAltName\\s+=\\s+@alt_names\n?", "");
extContent = extContent.replaceAll("\\[alt_names]\n?, ", "");
}
LOGGER.debug("Writing extension file content: \n {}", extContent);
Files.writeString(extFile, extContent, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
StartedProcess certGenProc = new ProcessExecutor().command(resolveOpenSSL(), "x509", "-req", "-days", Integer.toString(request.getRequestedValidityDays()), "-in", csrFile.toString(), "-CA", caCert.toString(), "-CAkey", caKey.toString(), "-CAcreateserial", "-out", outFile.toString(), "-extfile", extFile.toString()).redirectOutput(Slf4jStream.ofCaller().asDebug()).redirectError(Slf4jStream.ofCaller().asError()).start();
certGenProc.getFuture().get();
} catch (IOException e) {
throw new CommandLineOperationException("Failure running OpenSSL x509 command.", e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
return outFile;
} }
private String resolveOpenSSL() throws CommandLineOperationException { private String resolveOpenSSL() throws CommandLineOperationException {

View file

@ -2,6 +2,7 @@ package de.mlessmann.certassist;
import de.mlessmann.certassist.openssl.CertificateRequest; import de.mlessmann.certassist.openssl.CertificateRequest;
import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType;
import de.mlessmann.certassist.openssl.CertificateRequestExtension;
import de.mlessmann.certassist.openssl.CertificateSubject; import de.mlessmann.certassist.openssl.CertificateSubject;
import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator; import de.mlessmann.certassist.openssl.OpenSSLCertificateCreator;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -19,15 +20,11 @@ public class TestOpenSSLCertificateCreator {
@Test @Test
void testCertificateCreation() throws Exception { void testCertificateCreation() throws Exception {
CertificateRequest certRequest = CertificateRequest.builder() CertificateRequest certRequest = CertificateRequest.builder().commonName("test.home").type(RequestType.STANDALONE_CERTIFICATE).subject(CertificateSubject.builder().country("DE").state("SH").locality("HH").organization("Crazy-Cats")).extension(CertificateRequestExtension.builder().alternativeNames("test2.home", "test3.home")).build();
.commonName("test.home")
.type(RequestType.STANDALONE_CERTIFICATE)
.subject(CertificateSubject.builder().country("DE").state("SH")
.locality("").organization("Crazy-Cats"))
.build();
try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) { try (var cert = openSSLCertificateCreator.createCertificate(certRequest)) {
System.out.println("Certificate created: " + cert); System.out.println("Certificate created: " + cert);
} }
throw new RuntimeException("Test not implemented");
} }
} }