Skip to content

Commit

Permalink
[ST] Add a way to simulate tls-external auth client producer and cons…
Browse files Browse the repository at this point in the history
…umer (strimzi#9210)

Signed-off-by: jkalinic <[email protected]>
  • Loading branch information
jankalinic authored Oct 12, 2023
1 parent 1f010b0 commit cee167d
Show file tree
Hide file tree
Showing 3 changed files with 310 additions and 102 deletions.
197 changes: 197 additions & 0 deletions systemtest/src/main/java/io/strimzi/systemtest/security/OpenSsl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright Strimzi authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.strimzi.systemtest.security;

import io.strimzi.systemtest.Constants;
import io.strimzi.test.TestUtils;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* The `OpenSsl` class encapsulates OpenSSL command execution using the OpenSSLCommand object,
* which interfaces with the command-line version of OpenSSL. It serves as a versatile tool
* for various OpenSSL operations, primarily focusing on the creation of private keys, the
* generation of certificate signing requests (CSRs), and the signing of these CSRs using
* a certificate authority (CA). The primary use case for this class is to facilitate the
* simulation of externally provided client certificates, offering a seamless solution for
* integrating secure authentication mechanisms into your application.
*/
public class OpenSsl {
private static final Logger LOGGER = LogManager.getLogger(OpenSsl.class);

private static class OpenSslCommand {
ProcessBuilder pb = new ProcessBuilder();

public OpenSslCommand(String command) {
this("openssl", command);
}

public OpenSslCommand(String binary, String command) {
pb.command().add(binary);
pb.command().add(command);
}

public OpenSslCommand withOption(String option) {
pb.command().add(option);
return this;
}

public OpenSslCommand withOptionAndArgument(String option, File argument) {
pb.command().add(option);
pb.command().add(argument.getAbsolutePath());
return this;
}

public OpenSslCommand withOptionAndArgument(String option, String argument) {
pb.command().add(option);
pb.command().add(argument);
return this;
}

public void execute() {
executeAndReturnOnSuccess(true);
}

public String executeAndReturn() {
return executeAndReturnOnSuccess(true);
}

public String executeAndReturnOnSuccess(boolean failOnNonZeroOutput) {

Path commandOutput = null;
try {
commandOutput = Files.createTempFile("openssl-command-output-", ".txt");

pb.redirectErrorStream(true)
.redirectOutput(commandOutput.toFile());

LOGGER.debug("Running command: {}", pb.command());

Process process = pb.start();

OutputStream outputStream = process.getOutputStream();
outputStream.close();

int exitCode = process.waitFor();
String outputText = Files.readString(commandOutput, StandardCharsets.UTF_8);

if (exitCode != 0 && failOnNonZeroOutput) {
throw new RuntimeException("Openssl command failed. " + outputText);
}

return outputText;
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
} finally {
removeFile(commandOutput);
}
}

static void removeFile(Path fileToRemove) {
if (fileToRemove != null && Files.exists(fileToRemove)) {
try {
Files.delete(fileToRemove);
} catch (IOException e) {
LOGGER.debug("File could not be removed: {}", fileToRemove);
}
}

}
}

public static File generatePrivateKey() {
return generatePrivateKey(2048);
}

public static File generatePrivateKey(int keyLengthBits) {
try {
LOGGER.info("Creating client RSA private key with size of {} bits", keyLengthBits);
File privateKey = Files.createTempFile("private-key-", ".pem").toFile();

new OpenSslCommand("genpkey")
.withOptionAndArgument("-algorithm", "RSA")
.withOptionAndArgument("-pkeyopt", "rsa_keygen_bits:" + keyLengthBits)
.withOptionAndArgument("-out", privateKey)
.execute();

return privateKey;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static File generateCertSigningRequest(File privateKey, String subject) {
try {
LOGGER.info("Creating Certificate Signing Request file");
File csr = Files.createTempFile("csr-", ".pem").toFile();

new OpenSslCommand("req")
.withOption("-new")
.withOptionAndArgument("-key", privateKey)
.withOptionAndArgument("-out", csr)
.withOptionAndArgument("-subj", subject)
.execute();

return csr;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static File generateSignedCert(File csr, File caCrt, File caKey) {
try {
LOGGER.info("Creating signed certificate file");
File cert = Files.createTempFile("signed-cert-", ".pem").toFile();

new OpenSslCommand("x509")
.withOption("-req")
.withOptionAndArgument("-in", csr)
.withOptionAndArgument("-CA", caCrt)
.withOptionAndArgument("-CAkey", caKey)
.withOptionAndArgument("-out", cert)
.withOption("-CAcreateserial")
.execute();

waitForCertIsInValidDateRange(cert);

return cert;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void waitForCertIsInValidDateRange(File certificate) {
String dates = new OpenSslCommand("x509")
.withOption("-noout")
.withOption("-dates")
.withOptionAndArgument("-in", certificate)
.executeAndReturn()
.trim().replace("\s\s", "\s");

String startDate = dates.split("\n")[0].replace("notBefore=", "");
String endDate = dates.split("\n")[1].replace("notAfter=", "");

ZoneId gmtZone = ZoneId.of("GMT");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd HH:mm:ss yyyy z");
ZonedDateTime notBefore = ZonedDateTime.of(LocalDateTime.parse(startDate, formatter), gmtZone);
ZonedDateTime notAfter = ZonedDateTime.of(LocalDateTime.parse(endDate, formatter), gmtZone);

TestUtils.waitFor("certificate to be in valid date range", Constants.POLL_INTERVAL_FOR_RESOURCE_READINESS, Constants.CO_OPERATION_TIMEOUT_SHORT,
() -> {
ZonedDateTime now = ZonedDateTime.now(gmtZone);
return (now.isAfter(notBefore.plusSeconds(4)) && now.isBefore(notAfter.minusSeconds(3)));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package io.strimzi.systemtest.security;

import java.nio.charset.StandardCharsets;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
Expand Down Expand Up @@ -169,6 +170,35 @@ private static File exportCertsToPemFile(SystemTestCertAndKey... certs) throws I
return certFile;
}

/**
* This method exports Certificate Authority (CA) data to a temporary file for cases in which mentioned data is
* necessary in form of file - for use in applications like OpenSSL. The primary purpose is to save CA files,
* such as certificates and private keys (e.g., ca.key and ca.cert), into temporary files.
* These files are essential when you need to provide CA data to other applications, such as OpenSSL,
* for signing user Certificate Signing Requests (CSRs).
*
* @param caData The Certificate Authority data to be saved to the temporary file.
* @param prefix The prefix for the temporary file's name.
* @param suffix The suffix for the temporary file's name.
* @return A File object representing the temporary file containing the CA data.
* @throws RuntimeException If an IOException occurs while creating a file or writing into the temporary file
* given the critical role these operations play in ensuring proper functionality.
*/
public static File exportCaDataToFile(String caData, String prefix, String suffix) {
try {
File tempFile = Files.createTempFile(prefix + "-", suffix).toFile();

try (FileWriter fileWriter = new FileWriter(tempFile, StandardCharsets.UTF_8)) {
fileWriter.write(caData);
fileWriter.flush();
}

return tempFile;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static boolean containsAllDN(String principal1, String principal2) {
try {
return new LdapName(principal1).getRdns().containsAll(new LdapName(principal2).getRdns());
Expand Down
Loading

0 comments on commit cee167d

Please sign in to comment.