Skip to content

Commit

Permalink
Support certs generation when SSL enabled to fix FIPS scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik authored and mjurc committed Apr 29, 2024
1 parent d4ebef6 commit 7cf11c2
Show file tree
Hide file tree
Showing 19 changed files with 289 additions and 54 deletions.
Binary file not shown.
Binary file not shown.
3 changes: 0 additions & 3 deletions examples/https/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
# HTTPS
quarkus.http.ssl.certificate.key-store-file=META-INF/resources/server.keystore
quarkus.http.ssl.certificate.key-store-file-type=JKS
quarkus.http.ssl.certificate.key-store-password=password
quarkus.ssl.native=true
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import io.quarkus.test.bootstrap.DevModeQuarkusService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.DevModeQuarkusApplication;

@QuarkusScenario
public class DevModeGreetingResourceIT {
@DevModeQuarkusApplication(ssl = true)
@DevModeQuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true))
static DevModeQuarkusService app = new DevModeQuarkusService();

@Test
Expand Down
7 changes: 7 additions & 0 deletions examples/https/src/test/java/io/quarkus/qe/HttpIT.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package io.quarkus.qe;

import static io.quarkus.test.services.Certificate.Format.JKS;
import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;

import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;
import io.restassured.specification.RequestSpecification;

@QuarkusScenario
public class HttpIT {

private final RequestSpecification spec = given();

@QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS))
static final RestService app = new RestService();

@Test
public void shouldSayHelloWorld() {
untilAsserted(() -> spec.get("/greeting")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qe;

import static io.quarkus.test.services.Certificate.Format.JKS;
import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted;
import static org.hamcrest.Matchers.is;

Expand All @@ -10,12 +11,13 @@

import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
@DisabledOnOs(OS.WINDOWS)
public class UnixGreetingResourceIT {
@QuarkusApplication(ssl = true)
@QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS))
static final RestService app = new RestService();

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qe;

import static io.quarkus.test.services.Certificate.Format.JKS;
import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted;
import static org.hamcrest.Matchers.is;

Expand All @@ -10,14 +11,14 @@

import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Certificate;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
@EnabledOnOs(OS.WINDOWS)
public class WindowsGreetingResourceIT {
@QuarkusApplication(ssl = true)
static final RestService app = new RestService()
.withProperties("windows.application.properties");
@QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS))
static final RestService app = new RestService();

@Test
public void shouldSayHelloWorld() {
Expand Down

This file was deleted.

4 changes: 4 additions & 0 deletions quarkus-test-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>me.escoffier.certs</groupId>
<artifactId>certificate-generator</artifactId>
</dependency>
<!-- used by our own SureFire debug provider -->
<dependency>
<groupId>org.apache.maven.surefire</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package io.quarkus.test.security.certificate;

import static io.quarkus.test.utils.PropertiesUtils.DESTINATION_TO_FILENAME_SEPARATOR;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_WITH_DESTINATION_PREFIX;
import static io.quarkus.test.utils.TestExecutionProperties.isKubernetesPlatform;
import static io.quarkus.test.utils.TestExecutionProperties.isOpenshiftPlatform;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import io.quarkus.test.services.Certificate.Format;
import io.quarkus.test.utils.FileUtils;
import me.escoffier.certs.CertificateGenerator;
import me.escoffier.certs.CertificateRequest;
import me.escoffier.certs.JksCertificateFiles;
import me.escoffier.certs.PemCertificateFiles;
import me.escoffier.certs.Pkcs12CertificateFiles;

public interface CertificateBuilder {

/**
* Test context instance key.
*/
String INSTANCE_KEY = "io.quarkus.test.security.certificate#INSTANCE";

List<Certificate> certificates();

interface Certificate {

String keystorePath();

String truststorePath();

Map<String, String> configProperties();

}

static CertificateBuilder of(io.quarkus.test.services.Certificate[] certificates) {
if (certificates == null || certificates.length == 0) {
return null;
}
return createBuilder(certificates);
}

static Certificate of(String prefix, Format format, String password) {
return of(prefix, format, password, false, false);
}

private static Certificate of(String prefix, Format format, String password, boolean keystoreProps,
boolean truststoreProps) {
Path certsDir = createCertsTempDir(prefix);
CertificateGenerator generator = new CertificateGenerator(certsDir, true);
CertificateRequest request = (new CertificateRequest()).withName(prefix)
.withClientCertificate(false)
.withFormat(me.escoffier.certs.Format.valueOf(format.toString()))
.withCN("localhost")
.withPassword(password)
.withDuration(Duration.ofDays(2));
String trustStoreLocation = null;
String keyStoreLocation = null;
try {
var certFile = generator.generate(request).get(0);
if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) {
if (pkcs12CertFile.trustStoreFile() != null) {
trustStoreLocation = pkcs12CertFile.trustStoreFile().toAbsolutePath().toString();
}
if (pkcs12CertFile.keyStoreFile() != null) {
keyStoreLocation = pkcs12CertFile.keyStoreFile().toAbsolutePath().toString();
}
} else if (certFile instanceof PemCertificateFiles pemCertsFile) {
if (pemCertsFile.serverTrustFile() != null) {
trustStoreLocation = pemCertsFile.serverTrustFile().toAbsolutePath().toString();
}
} else if (certFile instanceof JksCertificateFiles jksCertFile) {
if (jksCertFile.trustStoreFile() != null) {
trustStoreLocation = jksCertFile.trustStoreFile().toAbsolutePath().toString();
}
if (jksCertFile.keyStoreFile() != null) {
keyStoreLocation = jksCertFile.keyStoreFile().toAbsolutePath().toString();
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to generate certificate", e);
}
Map<String, String> props = new HashMap<>();
if (trustStoreLocation != null) {
if (isOpenshiftPlatform() || isKubernetesPlatform()) {
// mount truststore to the pod
props.put(getRandomPropKey("truststore"), toSecretProperty(trustStoreLocation));
}
if (truststoreProps) {
props.put("quarkus.http.ssl.certificate.trust-store-file", trustStoreLocation);
props.put("quarkus.http.ssl.certificate.trust-store-file-type", format.toString());
props.put("quarkus.http.ssl.certificate.trust-store-password", password);
}
}
if (keyStoreLocation != null) {
if (isOpenshiftPlatform() || isKubernetesPlatform()) {
keyStoreLocation = makeFileMountPathUnique(prefix, keyStoreLocation);
// mount keystore to the pod
props.put(getRandomPropKey("keystore"), toSecretProperty(keyStoreLocation));
}
if (keystoreProps) {
props.put("quarkus.http.ssl.certificate.key-store-file", keyStoreLocation);
props.put("quarkus.http.ssl.certificate.key-store-file-type", format.toString());
props.put("quarkus.http.ssl.certificate.key-store-password", password);
}
}
return new CertificateImpl(keyStoreLocation, trustStoreLocation, Map.copyOf(props));
}

private static String makeFileMountPathUnique(String prefix, String storeLocation) {
var newTempCertDir = createCertsTempDir(prefix);
var storeFile = Path.of(storeLocation).toFile();
FileUtils.copyFileTo(storeFile, newTempCertDir);
return newTempCertDir.resolve(storeFile.getName()).toAbsolutePath().toString();
}

private static Path createCertsTempDir(String prefix) {
Path certsDir;
try {
certsDir = Files.createTempDirectory(prefix + "-certs");
} catch (IOException e) {
throw new RuntimeException(e);
}
return certsDir;
}

private static CertificateBuilder createBuilder(io.quarkus.test.services.Certificate[] certificates) {
Certificate[] generatedCerts = new Certificate[certificates.length];
for (int i = 0; i < certificates.length; i++) {
generatedCerts[i] = of(certificates[i].prefix(), certificates[i].format(), certificates[i].password(),
certificates[i].configureKeystore(), certificates[i].configureTruststore());
}
return new CertificateBuilderImp(List.of(generatedCerts));
}

private static String getRandomPropKey(String store) {
return store + "-" + new Random().nextInt();
}

private static String toSecretProperty(String path) {
int fileNameSeparatorIdx = path.lastIndexOf(File.separator);
String fileName = path.substring(fileNameSeparatorIdx + 1);
String pathToFile = path.substring(0, fileNameSeparatorIdx);
return SECRET_WITH_DESTINATION_PREFIX + pathToFile + DESTINATION_TO_FILENAME_SEPARATOR + fileName;
}

record CertificateBuilderImp(List<Certificate> certificates) implements CertificateBuilder {
}

record CertificateImpl(String keystorePath, String truststorePath,
Map<String, String> configProperties) implements Certificate {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.test.services;

/**
* Defines certificate request for which this framework will generate a keystore and truststore.
*/
public @interface Certificate {

enum Format {
PEM,
JKS,
PKCS12
}

/**
* Prefix keystore and truststore name with this attribute.
*/
String prefix() default "quarkus-qe";

/**
* Secure file format.
*/
Format format() default Format.PKCS12;

/**
* Keystore and truststore password.
*/
String password() default "password";

/**
* Whether following configuration properties should be set for you:
*
* - `quarkus.http.ssl.certificate.key-store-file`
* - `quarkus.http.ssl.certificate.key-store-file-type`
* - `quarkus.http.ssl.certificate.key-store-password`
*
* You still can set and/or override these properties
* with {@link io.quarkus.test.bootstrap.BaseService#withProperty(String, String)} service method.
*/
boolean configureKeystore() default false;

/**
* Whether following configuration properties should be set for you:
*
* - `quarkus.http.ssl.certificate.trust-store-file`
* - `quarkus.http.ssl.certificate.trust-store-file-type`
* - `quarkus.http.ssl.certificate.trust-store-password`
*
* You still can set and/or override these properties
* with {@link io.quarkus.test.bootstrap.BaseService#withProperty(String, String)} service method.
*/
boolean configureTruststore() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@
* `quarkus.http.ssl.certificate.key-store-password` to be set.
*/
boolean ssl() default false;

/**
* Certificates that should be generated by this framework.
*/
Certificate[] certificates() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
*/
boolean ssl() default false;

/**
* Certificates that should be generated by this framework.
*/
Certificate[] certificates() default {};

/**
* Enable GRPC configuration. This property will map the gPRC service to a random port.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.quarkus.test.bootstrap.ManagedResource;
import io.quarkus.test.bootstrap.ServiceContext;
import io.quarkus.test.security.certificate.CertificateBuilder;
import io.quarkus.test.services.DevModeQuarkusApplication;
import io.quarkus.test.services.quarkus.model.QuarkusProperties;
import io.quarkus.test.utils.FileUtils;
Expand All @@ -21,6 +22,7 @@ public void init(Annotation annotation) {
setPropertiesFile(metadata.properties());
setGrpcEnabled(metadata.grpc());
setSslEnabled(metadata.ssl());
setCertificateBuilder(CertificateBuilder.of(metadata.certificates()));
}

@Override
Expand All @@ -32,6 +34,7 @@ protected Path getResourcesApplicationFolder() {
public ManagedResource build(ServiceContext context) {
setContext(context);
configureLogging();
configureCertificates();
if (QuarkusProperties.disableBuildAnalytics()) {
getContext()
.getOwner()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.quarkus.test.bootstrap.ManagedResource;
import io.quarkus.test.bootstrap.ServiceContext;
import io.quarkus.test.common.PathTestHelper;
import io.quarkus.test.security.certificate.CertificateBuilder;
import io.quarkus.test.services.QuarkusApplication;
import io.quarkus.test.services.quarkus.model.QuarkusProperties;
import io.quarkus.test.utils.Command;
Expand Down Expand Up @@ -68,12 +69,14 @@ public void init(Annotation annotation) {
setGrpcEnabled(metadata.grpc());
initAppClasses(metadata.classes());
initForcedDependencies(metadata.dependencies());
setCertificateBuilder(CertificateBuilder.of(metadata.certificates()));
}

@Override
public ManagedResource build(ServiceContext context) {
setContext(context);
configureLogging();
configureCertificates();
managedResource = findManagedResource();
build();

Expand Down
Loading

0 comments on commit 7cf11c2

Please sign in to comment.