diff --git a/pom.xml b/pom.xml
index 1155bc6..eeaceb0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,6 +100,7 @@
3.3.1
2.17.2
0.9.0.M3
+ 1.78.1
true
@@ -221,6 +222,16 @@
org.eclipse.sisu.inject
${org.eclipse.sisu.version}
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${bouncycastle.version}
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${bouncycastle.version}
+
diff --git a/test-frame-common/pom.xml b/test-frame-common/pom.xml
index 86b855d..c87fe6f 100644
--- a/test-frame-common/pom.xml
+++ b/test-frame-common/pom.xml
@@ -130,6 +130,14 @@
log4j-slf4j2-impl
test
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/TestFrameConstants.java b/test-frame-common/src/main/java/io/skodjob/testframe/TestFrameConstants.java
index 3590f49..f45acf9 100644
--- a/test-frame-common/src/main/java/io/skodjob/testframe/TestFrameConstants.java
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/TestFrameConstants.java
@@ -47,6 +47,16 @@ public interface TestFrameConstants {
*/
long GLOBAL_STABILITY_TIME = Duration.ofMinutes(1).toMillis();
+ /**
+ * CA validity delay
+ */
+ long CA_CERT_VALIDITY_DELAY = 10;
+
+ /**
+ * Poll interval for resource readiness
+ */
+ long POLL_INTERVAL_FOR_RESOURCE_READINESS = Duration.ofSeconds(1).toMillis();
+
/**
* OpenShift client type.
*/
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKey.java b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKey.java
new file mode 100644
index 0000000..af4ba5e
--- /dev/null
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKey.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+
+/**
+ * Record for certificate and key in memory
+ *
+ * @param certificate certificate
+ * @param privateKey key
+ */
+public record CertAndKey(X509Certificate certificate, PrivateKey privateKey) {
+
+ /**
+ * Returns certificate
+ *
+ * @return certificate
+ */
+ public X509Certificate getCertificate() {
+ return certificate;
+ }
+
+ /**
+ * Returns public key
+ *
+ * @return public key
+ */
+ public PublicKey getPublicKey() {
+ return certificate.getPublicKey();
+ }
+
+ /**
+ * Returns private key
+ *
+ * @return private key
+ */
+ public PrivateKey getPrivateKey() {
+ return privateKey;
+ }
+}
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyBuilder.java b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyBuilder.java
new file mode 100644
index 0000000..11149f6
--- /dev/null
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyBuilder.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Object;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static java.util.Arrays.asList;
+import static org.bouncycastle.asn1.x509.Extension.authorityKeyIdentifier;
+import static org.bouncycastle.asn1.x509.Extension.basicConstraints;
+import static org.bouncycastle.asn1.x509.Extension.extendedKeyUsage;
+import static org.bouncycastle.asn1.x509.Extension.keyUsage;
+import static org.bouncycastle.asn1.x509.Extension.subjectAlternativeName;
+import static org.bouncycastle.asn1.x509.Extension.subjectKeyIdentifier;
+import static org.bouncycastle.asn1.x509.GeneralName.dNSName;
+import static org.bouncycastle.asn1.x509.KeyPurposeId.id_kp_clientAuth;
+import static org.bouncycastle.asn1.x509.KeyPurposeId.id_kp_serverAuth;
+import static org.bouncycastle.asn1.x509.KeyUsage.cRLSign;
+import static org.bouncycastle.asn1.x509.KeyUsage.digitalSignature;
+import static org.bouncycastle.asn1.x509.KeyUsage.keyCertSign;
+import static org.bouncycastle.asn1.x509.KeyUsage.keyEncipherment;
+import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
+
+/**
+ * Builder of certificates using java
+ */
+public class CertAndKeyBuilder {
+
+ /**
+ * Key size
+ */
+ public static final int KEY_SIZE = 2048;
+
+ /**
+ * Key pair algorithm
+ */
+ public static final String KEY_PAIR_ALGORITHM = "RSA";
+
+ /**
+ * Sign algorithm
+ */
+ public static final String SIGNATURE_ALGORITHM = "SHA256WithRSA";
+
+ /**
+ * Default cert validity period
+ */
+ public static final Duration CERTIFICATE_VALIDITY_PERIOD = Duration.ofDays(30);
+
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private final KeyPair keyPair;
+ private final CertAndKey caCert;
+ private final List extensions;
+ private X500Name issuer;
+ private X500Name subject;
+
+ private CertAndKeyBuilder(KeyPair keyPair, CertAndKey caCert, List extensions) {
+ this.keyPair = keyPair;
+ this.caCert = caCert;
+ if (caCert != null) {
+ try {
+ this.issuer = new JcaX509CertificateHolder(caCert.getCertificate()).getSubject();
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ this.extensions = new ArrayList<>(extensions);
+ }
+
+ /**
+ * Returns builder for root CA
+ *
+ * @return Returns builder for root CA
+ */
+ public static CertAndKeyBuilder rootCaCertBuilder() {
+ KeyPair keyPair = generateKeyPair();
+ return new CertAndKeyBuilder(
+ keyPair,
+ null,
+ asList(
+ new Extension(keyUsage, true, keyUsage(keyCertSign | cRLSign)),
+ new Extension(basicConstraints, true, ca()),
+ new Extension(subjectKeyIdentifier, false, createSubjectKeyIdentifier(keyPair.getPublic()))
+ )
+ );
+ }
+
+ /**
+ * Returns builder for intermediate CA
+ *
+ * @param caCert ca certificate
+ * @return Returns builder for intermediate CA
+ */
+ public static CertAndKeyBuilder intermediateCaCertBuilder(CertAndKey caCert) {
+ KeyPair keyPair = generateKeyPair();
+ return new CertAndKeyBuilder(
+ keyPair,
+ caCert,
+ asList(
+ new Extension(keyUsage, true, keyUsage(keyCertSign)),
+ new Extension(basicConstraints, true, ca()),
+ new Extension(subjectKeyIdentifier, false, createSubjectKeyIdentifier(keyPair.getPublic())),
+ new Extension(authorityKeyIdentifier, false, createAuthorityKeyIdentifier(caCert.getPublicKey()))
+ )
+ );
+ }
+
+ /**
+ * Returns builder for application cert
+ *
+ * @param caCert ca certificate
+ * @return Returns builder for application cert
+ */
+ public static CertAndKeyBuilder appCaCertBuilder(CertAndKey caCert) {
+ KeyPair keyPair = generateKeyPair();
+ return new CertAndKeyBuilder(
+ keyPair,
+ caCert,
+ asList(
+ new Extension(basicConstraints, true, ca()),
+ new Extension(subjectKeyIdentifier, false, createSubjectKeyIdentifier(keyPair.getPublic())),
+ new Extension(authorityKeyIdentifier, false, createAuthorityKeyIdentifier(caCert.getPublicKey()))
+ )
+ );
+ }
+
+ /**
+ * Returns builder for end entity cert
+ *
+ * @param caCert ca certificate
+ * @return Returns builder for end entity cert
+ */
+ public static CertAndKeyBuilder endEntityCertBuilder(CertAndKey caCert) {
+ KeyPair keyPair = generateKeyPair();
+ return new CertAndKeyBuilder(
+ keyPair,
+ caCert,
+ asList(
+ new Extension(keyUsage, true, keyUsage(digitalSignature | keyEncipherment)),
+ new Extension(extendedKeyUsage, false, extendedKeyUsage(id_kp_serverAuth, id_kp_clientAuth)),
+ new Extension(basicConstraints, true, notCa()),
+ new Extension(subjectKeyIdentifier, false, createSubjectKeyIdentifier(keyPair.getPublic())),
+ new Extension(authorityKeyIdentifier, false, createAuthorityKeyIdentifier(caCert.getPublicKey()))
+ )
+ );
+ }
+
+ /**
+ * Sets issues DN
+ *
+ * @param issuerDn issues DN
+ * @return builder
+ */
+ public CertAndKeyBuilder withIssuerDn(String issuerDn) {
+ this.issuer = new X500Name(issuerDn);
+ return this;
+ }
+
+ /**
+ * Sets subject DN
+ *
+ * @param subjectDn subject DN
+ * @return builder
+ */
+ public CertAndKeyBuilder withSubjectDn(String subjectDn) {
+ this.subject = new X500Name(subjectDn);
+ return this;
+ }
+
+ /**
+ * Sets san dns
+ *
+ * @param hostName hostname
+ * @return builder
+ */
+ public CertAndKeyBuilder withSanDnsName(final String hostName) {
+ final GeneralName dnsName = new GeneralName(dNSName, hostName);
+ final byte[] subjectAltName = encode(GeneralNames.getInstance(new DERSequence(dnsName)));
+ extensions.add(new Extension(subjectAlternativeName, false, subjectAltName));
+ return this;
+ }
+
+ /**
+ * Sets multiple san dns names
+ *
+ * @param sanDnsNames list of san dns names
+ * @return builder
+ */
+ public CertAndKeyBuilder withSanDnsNames(final ASN1Encodable[] sanDnsNames) {
+ final DERSequence subjectAlternativeNames = new DERSequence(sanDnsNames);
+ final byte[] subjectAltName = encode(GeneralNames.getInstance(subjectAlternativeNames));
+ extensions.add(new Extension(subjectAlternativeName, false, subjectAltName));
+
+ return this;
+ }
+
+ /**
+ * Returns cert and key in memory from builder
+ *
+ * @return Returns cert and key in memory
+ */
+ public CertAndKey build() {
+ try {
+ BigInteger certSerialNumber = BigInteger.valueOf(System.currentTimeMillis());
+ ContentSigner contentSigner = createContentSigner();
+ Instant startDate = Instant.now().minus(1, DAYS);
+ Instant endDate = startDate.plus(CERTIFICATE_VALIDITY_PERIOD);
+ JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuer,
+ certSerialNumber,
+ Date.from(startDate),
+ Date.from(endDate),
+ subject,
+ keyPair.getPublic()
+ );
+ for (Extension extension : extensions) {
+ certBuilder.addExtension(extension);
+ }
+ X509Certificate certificate = new JcaX509CertificateConverter()
+ .setProvider(PROVIDER_NAME)
+ .getCertificate(certBuilder.build(contentSigner));
+ return new CertAndKey(certificate, keyPair.getPrivate());
+ } catch (CertIOException | CertificateException | OperatorCreationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private ContentSigner createContentSigner() throws OperatorCreationException {
+ JcaContentSignerBuilder builder = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM);
+ if (caCert == null) {
+ return builder.build(keyPair.getPrivate());
+ } else {
+ return builder.build(caCert.getPrivateKey());
+ }
+ }
+
+ private static KeyPair generateKeyPair() {
+ try {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM, PROVIDER_NAME);
+ keyPairGenerator.initialize(KEY_SIZE);
+ return keyPairGenerator.generateKeyPair();
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static byte[] keyUsage(int usage) {
+ return encode(new KeyUsage(usage));
+ }
+
+ private static byte[] extendedKeyUsage(KeyPurposeId... usages) {
+ return encode(new ExtendedKeyUsage(usages));
+ }
+
+ private static byte[] notCa() {
+ return encode(new BasicConstraints(false));
+ }
+
+ private static byte[] ca() {
+ return encode(new BasicConstraints(true));
+ }
+
+ private static byte[] createSubjectKeyIdentifier(PublicKey publicKey) {
+ JcaX509ExtensionUtils extensionUtils = createExtensionUtils();
+ return encode(extensionUtils.createSubjectKeyIdentifier(publicKey));
+ }
+
+ private static byte[] createAuthorityKeyIdentifier(PublicKey publicKey) {
+ JcaX509ExtensionUtils extensionUtils = createExtensionUtils();
+ return encode(extensionUtils.createAuthorityKeyIdentifier(publicKey));
+ }
+
+ private static byte[] encode(ASN1Object object) {
+ try {
+ return object.getEncoded();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static JcaX509ExtensionUtils createExtensionUtils() {
+ try {
+ return new JcaX509ExtensionUtils();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyFiles.java b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyFiles.java
new file mode 100644
index 0000000..2dbeb12
--- /dev/null
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/security/CertAndKeyFiles.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import java.io.File;
+
+/**
+ * Record for cert and key files on disk
+ *
+ * @param certFile cert file
+ * @param keyFile key file
+ */
+public record CertAndKeyFiles(File certFile, File keyFile) {
+
+ /**
+ * Returns path of cert file
+ *
+ * @return string path
+ */
+ public String getCertPath() {
+ return certFile.getPath();
+ }
+
+ /**
+ * Returns path of key file
+ *
+ * @return string path
+ */
+ public String getKeyPath() {
+ return keyFile.getPath();
+ }
+}
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/security/OpenSsl.java b/test-frame-common/src/main/java/io/skodjob/testframe/security/OpenSsl.java
new file mode 100644
index 0000000..ee5da2d
--- /dev/null
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/security/OpenSsl.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import io.skodjob.testframe.TestFrameConstants;
+import io.skodjob.testframe.wait.Wait;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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;
+
+/**
+ * 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 = LoggerFactory.getLogger(OpenSsl.class);
+
+ private OpenSsl() {
+ // empty constructor
+ }
+
+ private static class OpenSslCommand {
+ ProcessBuilder pb = new ProcessBuilder();
+
+ OpenSslCommand(String command) {
+ this("openssl", command);
+ }
+
+ 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);
+ }
+ }
+
+ }
+ }
+
+ /**
+ * Generates private key
+ *
+ * @return file with private key
+ */
+ public static File generatePrivateKey() {
+ return generatePrivateKey(2048);
+ }
+
+ /**
+ * Generates private key
+ *
+ * @param keyLengthBits key length
+ * @return file with private key
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Generates cert signing request
+ *
+ * @param privateKey file with private key
+ * @param subject subject
+ * @return csr file
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Generates signed cert
+ *
+ * @param csr csr
+ * @param caCrt ca file
+ * @param caKey ca key
+ * @return file with signed cert
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Waits for cert is valid in current range
+ *
+ * @param certificate signed cert file
+ */
+ 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 d[d] HH:mm:ss yyyy z");
+ ZonedDateTime notBefore = ZonedDateTime.of(LocalDateTime.parse(startDate, formatter), gmtZone);
+ ZonedDateTime notAfter = ZonedDateTime.of(LocalDateTime.parse(endDate, formatter), gmtZone);
+
+ Wait.until("certificate to be in valid date range",
+ TestFrameConstants.POLL_INTERVAL_FOR_RESOURCE_READINESS, TestFrameConstants.GLOBAL_POLL_INTERVAL_LONG,
+ () -> {
+ ZonedDateTime now = ZonedDateTime.now(gmtZone);
+ return (now.isAfter(notBefore.plusSeconds(TestFrameConstants.CA_CERT_VALIDITY_DELAY))
+ && now.isBefore(notAfter.minusSeconds(TestFrameConstants.CA_CERT_VALIDITY_DELAY)));
+ });
+ }
+}
diff --git a/test-frame-common/src/main/java/io/skodjob/testframe/utils/SecurityUtils.java b/test-frame-common/src/main/java/io/skodjob/testframe/utils/SecurityUtils.java
new file mode 100644
index 0000000..69b55e3
--- /dev/null
+++ b/test-frame-common/src/main/java/io/skodjob/testframe/utils/SecurityUtils.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.utils;
+
+import io.skodjob.testframe.security.CertAndKey;
+import io.skodjob.testframe.security.CertAndKeyBuilder;
+import io.skodjob.testframe.security.CertAndKeyFiles;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.HashSet;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Utils for manipulating with certs
+ */
+public class SecurityUtils {
+
+ private SecurityUtils() {
+ // empty constructor
+ }
+
+ /**
+ * Export in-memory cert and key into pem files
+ *
+ * @param certs im memory certs
+ * @return exported files
+ */
+ public static CertAndKeyFiles exportToPemFiles(CertAndKey... certs) {
+ if (certs.length == 0) {
+ throw new IllegalArgumentException("List of certificates should has at least one element");
+ }
+ try {
+ File keyFile = exportPrivateKeyToPemFile(certs[0].getPrivateKey());
+ File certFile = exportCertsToPemFile(certs);
+ return new CertAndKeyFiles(certFile, keyFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Converts private key into PKCS8File
+ *
+ * @param privatekey private key
+ * @return exported file on disk
+ * @throws NoSuchAlgorithmException bad algorithm
+ * @throws InvalidKeySpecException bad key
+ * @throws IOException io exception
+ */
+ public static File convertPrivateKeyToPKCS8File(PrivateKey privatekey)
+ throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ byte[] encoded = privatekey.getEncoded();
+ final PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(encoded);
+
+ final ASN1Encodable asn1Encodable = privateKeyInfo.parsePrivateKey();
+ final byte[] privateKeyPKCS8Formatted = asn1Encodable.toASN1Primitive().getEncoded(ASN1Encoding.DER);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyPKCS8Formatted);
+
+ KeyFactory kf = KeyFactory.getInstance(CertAndKeyBuilder.KEY_PAIR_ALGORITHM);
+ PrivateKey privateKey = kf.generatePrivate(keySpec);
+ return exportPrivateKeyToPemFile(privateKey);
+ }
+
+ /**
+ * Exports private key into pem file
+ *
+ * @param privateKey private key
+ * @return exported em file on disk
+ * @throws IOException io exception
+ */
+ private static File exportPrivateKeyToPemFile(PrivateKey privateKey) throws IOException {
+ File keyFile = Files.createTempFile("key-", ".key").toFile();
+ try (JcaPEMWriter pemWriter = new JcaPEMWriter(new FileWriter(keyFile, UTF_8))) {
+ pemWriter.writeObject(privateKey);
+ pemWriter.flush();
+ }
+ return keyFile;
+ }
+
+ /**
+ * Exports in memory certificates into single pem file
+ *
+ * @param certs certificates
+ * @return file with all certificates
+ * @throws IOException io exception
+ */
+ private static File exportCertsToPemFile(CertAndKey... certs) throws IOException {
+ File certFile = Files.createTempFile("crt-", ".crt").toFile();
+ try (JcaPEMWriter pemWriter = new JcaPEMWriter(new FileWriter(certFile, UTF_8))) {
+ for (CertAndKey certAndKey : certs) {
+ pemWriter.writeObject(certAndKey.getCertificate());
+ }
+ pemWriter.flush();
+ }
+ 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);
+ }
+ }
+
+ /**
+ * Check if principal1 contains all dns of principal2 in dn
+ *
+ * @param principal1 principal
+ * @param principal2 principal
+ * @return true of principal 1 contains all dn of principal 2
+ */
+ public static boolean containsAllDN(String principal1, String principal2) {
+ try {
+ return new HashSet<>(new LdapName(principal1).getRdns()).containsAll(new LdapName(principal2).getRdns());
+ } catch (InvalidNameException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+}
diff --git a/test-frame-common/src/test/java/io/skodjob/testframe/clients/TestEnvironmentVariablesTest.java b/test-frame-common/src/test/java/io/skodjob/testframe/environment/TestEnvironmentVariablesTest.java
similarity index 94%
rename from test-frame-common/src/test/java/io/skodjob/testframe/clients/TestEnvironmentVariablesTest.java
rename to test-frame-common/src/test/java/io/skodjob/testframe/environment/TestEnvironmentVariablesTest.java
index 781906d..3891ac4 100644
--- a/test-frame-common/src/test/java/io/skodjob/testframe/clients/TestEnvironmentVariablesTest.java
+++ b/test-frame-common/src/test/java/io/skodjob/testframe/environment/TestEnvironmentVariablesTest.java
@@ -2,9 +2,8 @@
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
-package io.skodjob.testframe.clients;
+package io.skodjob.testframe.environment;
-import io.skodjob.testframe.environment.TestEnvironmentVariables;
import org.junit.jupiter.api.Test;
import java.io.IOException;
diff --git a/test-frame-common/src/test/java/io/skodjob/testframe/security/CertAndKeyBuilderTest.java b/test-frame-common/src/test/java/io/skodjob/testframe/security/CertAndKeyBuilderTest.java
new file mode 100644
index 0000000..76cbfdb
--- /dev/null
+++ b/test-frame-common/src/test/java/io/skodjob/testframe/security/CertAndKeyBuilderTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CertAndKeyBuilderTest {
+ static final String ROOT_CA = "C=COM, L=Boston, O=Example, CN=ExampleRootCA";
+ static final String INTERMEDIATE_CA = "C=COM, L=Boston, O=Example, CN=ExampleIntermediateCA";
+ static final String END_SUBJECT = "C=COM, L=Boston, O=Example, CN=end-app.example.io";
+ static final String APP_SUBJECT = "C=COM, L=Boston, O=Example, CN=app.example.io";
+
+ static final String COMPARE_ROOT_DN = "CN=ExampleRootCA,O=Example,L=Boston,C=COM";
+ static final String COMPARE_INTERMEDIATE_DN = "CN=ExampleIntermediateCA,O=Example,L=Boston,C=COM";
+
+ @Test
+ void testGenerateCerts() {
+ CertAndKey ca = CertAndKeyBuilder.rootCaCertBuilder()
+ .withIssuerDn(ROOT_CA)
+ .withSubjectDn(ROOT_CA)
+ .build();
+
+ assertEquals(COMPARE_ROOT_DN, ca.certificate().getIssuerX500Principal().getName());
+ assertDoesNotThrow(() -> ca.certificate().checkValidity());
+
+ CertAndKey intermediateCa = CertAndKeyBuilder.intermediateCaCertBuilder(ca)
+ .withIssuerDn(INTERMEDIATE_CA)
+ .withSubjectDn(INTERMEDIATE_CA)
+ .build();
+
+ assertEquals(COMPARE_INTERMEDIATE_DN, intermediateCa.certificate().getIssuerX500Principal().getName());
+ assertDoesNotThrow(() -> intermediateCa.certificate().checkValidity());
+
+ CertAndKey appCert = CertAndKeyBuilder.appCaCertBuilder(ca)
+ .withSubjectDn(APP_SUBJECT)
+ .build();
+
+ assertEquals(COMPARE_ROOT_DN, appCert.certificate().getIssuerX500Principal().getName());
+ assertDoesNotThrow(() -> appCert.certificate().checkValidity());
+
+ CertAndKey endAppCert = CertAndKeyBuilder.endEntityCertBuilder(intermediateCa)
+ .withSubjectDn(END_SUBJECT)
+ .withSanDnsName("*.example.io")
+ .build();
+
+ assertEquals(COMPARE_INTERMEDIATE_DN, endAppCert.certificate().getIssuerX500Principal().getName());
+ assertDoesNotThrow(() -> endAppCert.certificate().checkValidity());
+
+ // check cert signing
+ assertDoesNotThrow(() -> appCert.certificate().verify(ca.getPublicKey()));
+ assertDoesNotThrow(() -> endAppCert.certificate().verify(intermediateCa.getPublicKey()));
+ assertDoesNotThrow(() -> intermediateCa.certificate().verify(ca.getPublicKey()));
+ }
+}
diff --git a/test-frame-common/src/test/java/io/skodjob/testframe/security/SecurityUtilsTest.java b/test-frame-common/src/test/java/io/skodjob/testframe/security/SecurityUtilsTest.java
new file mode 100644
index 0000000..41ae40f
--- /dev/null
+++ b/test-frame-common/src/test/java/io/skodjob/testframe/security/SecurityUtilsTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Skodjob authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.skodjob.testframe.security;
+
+import io.skodjob.testframe.utils.SecurityUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.CertificateEncodingException;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class SecurityUtilsTest {
+ static final String ROOT_CA = "C=COM, L=Boston, O=Example, CN=ExampleRootCA";
+ static final String INTERMEDIATE_CA = "C=COM, L=Boston, O=Example, CN=ExampleIntermediateCA";
+ static final String END_SUBJECT = "C=COM, L=Boston, O=Example, CN=end-app.example.io";
+ static final String APP_SUBJECT = "C=COM, L=Boston, O=Example, CN=app.example.io";
+
+ CertAndKey ca;
+ CertAndKey intermediateCa;
+ CertAndKey appCert;
+ CertAndKey endAppCert;
+
+ @BeforeAll
+ void setup() {
+ ca = CertAndKeyBuilder.rootCaCertBuilder()
+ .withIssuerDn(ROOT_CA)
+ .withSubjectDn(ROOT_CA)
+ .build();
+
+ intermediateCa = CertAndKeyBuilder.intermediateCaCertBuilder(ca)
+ .withIssuerDn(INTERMEDIATE_CA)
+ .withSubjectDn(INTERMEDIATE_CA)
+ .build();
+
+ appCert = CertAndKeyBuilder.appCaCertBuilder(ca)
+ .withSubjectDn(APP_SUBJECT)
+ .build();
+
+ endAppCert = CertAndKeyBuilder.endEntityCertBuilder(intermediateCa)
+ .withSubjectDn(END_SUBJECT)
+ .withSanDnsName("*.example.io")
+ .build();
+ }
+
+ @Test
+ void testExportCertsToPem() throws IOException, CertificateEncodingException {
+ CertAndKeyFiles all = SecurityUtils.exportToPemFiles(ca, intermediateCa, appCert);
+
+ String content = Files.readString(Paths.get(all.getCertPath()));
+ assertNotEquals("", content);
+ }
+
+ @Test
+ void testExportDataToCa() throws IOException {
+ File caCert = SecurityUtils.exportCaDataToFile(ca.getPublicKey().toString(), "ca", ".crt");
+
+ String content = Files.readString(caCert.toPath());
+ assertNotEquals("", content);
+ }
+}