Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support encrypted communication between Quarkus app with MSSQL JDBC extension and SQL Server #1138

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ protected ServiceContext getContext() {
@Override
public void init(Annotation annotation) {
Container metadata = (Container) annotation;
this.image = PropertiesUtils.resolveProperty(metadata.image());
this.command = metadata.command();
this.expectedLog = PropertiesUtils.resolveProperty(metadata.expectedLog());
this.port = metadata.port();
this.portDockerHostToLocalhost = metadata.portDockerHostToLocalhost();
init(metadata.image(), metadata.command(), metadata.expectedLog(), metadata.port(),
metadata.portDockerHostToLocalhost());
}

protected void init(String image, String[] command, String expectedLog, int port, boolean portDockerHostToLocalhost) {
this.image = PropertiesUtils.resolveProperty(image);
this.command = command;
this.expectedLog = PropertiesUtils.resolveProperty(expectedLog);
this.port = port;
this.portDockerHostToLocalhost = portDockerHostToLocalhost;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX_MATCHER;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_SPLIT_CHAR;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_WITH_DESTINATION_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.SLASH;

import java.nio.file.Files;
Expand All @@ -28,6 +29,7 @@
import io.quarkus.test.logging.TestContainersLoggingHandler;
import io.quarkus.test.services.URILike;
import io.quarkus.test.utils.DockerUtils;
import io.quarkus.test.utils.FileUtils;

public abstract class DockerContainerManagedResource implements ManagedResource {

Expand Down Expand Up @@ -131,6 +133,11 @@ private Map<String, String> resolveProperties() {
if (isResource(entry.getValue())) {
value = entry.getValue().replace(RESOURCE_PREFIX, StringUtils.EMPTY);
addFileToContainer(value);
} else if (isSecretWithDestinationPath(entry.getValue())) {
value = entry.getValue().replace(SECRET_WITH_DESTINATION_PREFIX, StringUtils.EMPTY);
String destinationPath = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[0];
String fileName = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[1];
addFileToContainer(destinationPath, fileName);
} else if (isResourceWithDestinationPath(entry.getValue())) {
value = entry.getValue().replace(RESOURCE_WITH_DESTINATION_PREFIX, StringUtils.EMPTY);
if (!value.matches(RESOURCE_WITH_DESTINATION_PREFIX_MATCHER)) {
Expand Down Expand Up @@ -163,8 +170,17 @@ private void addFileToContainer(String filePath) {
}

private void addFileToContainer(String destinationPath, String hostFilePath) {
var filePath = FileUtils.findTargetFile(Path.of("target"), hostFilePath);
String containerFullPath = destinationPath + SLASH + hostFilePath;
innerContainer.withClasspathResourceMapping(hostFilePath, containerFullPath, BindMode.READ_ONLY);
if (filePath.isEmpty()) {
innerContainer.withClasspathResourceMapping(hostFilePath, containerFullPath, BindMode.READ_ONLY);
} else {
innerContainer.withCopyFileToContainer(MountableFile.forHostPath(filePath.get()), containerFullPath);
}
}

private static boolean isSecretWithDestinationPath(String key) {
return key.startsWith(SECRET_WITH_DESTINATION_PREFIX);
}

private boolean isResourceWithDestinationPath(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public ServiceContext withTestScopeConfigProperty(String key, String value) {
return this;
}

Map<String, String> getConfigPropertiesWithTestScope() {
public Map<String, String> getConfigPropertiesWithTestScope() {
return configPropertiesWithTestScope;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.test.scenarios.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.extension.ExtendWith;

@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DisabledOnFipsAndJava17Condition.class)
public @interface DisabledOnFipsAndJava17 {
/**
* Why is the annotated test class or test method disabled.
*/
String reason() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.test.scenarios.annotations;

import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;

public class DisabledOnFipsAndJava17Condition implements ExecutionCondition {

/**
* We set environment variable "FIPS" to "fips" in our Jenkins jobs when FIPS are enabled.
*/
private static final String FIPS_ENABLED = "fips";
private static final int JAVA_17 = 17;

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (isFipsEnabledEnvironment() && isJava17()) {
return ConditionEvaluationResult.disabled("The test is running in FIPS enabled environment with Java 17");
}

return ConditionEvaluationResult.enabled("The test is not running in FIPS enabled environment with Java 17");
}

private static boolean isFipsEnabledEnvironment() {
return FIPS_ENABLED.equals(System.getenv().get("FIPS"));
}

private static boolean isJava17() {
return JAVA_17 == Runtime.version().feature();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
import static io.quarkus.test.services.Certificate.Format.PKCS12;
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 static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -27,7 +29,6 @@
import java.util.Random;
import java.util.stream.Collectors;

import io.quarkus.test.utils.FileUtils;
import me.escoffier.certs.CertificateGenerator;
import me.escoffier.certs.CertificateRequest;
import me.escoffier.certs.JksCertificateFiles;
Expand Down Expand Up @@ -60,27 +61,40 @@ interface PemCertificate extends Certificate {

}

static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password, Path targetDir,
ContainerMountStrategy containerMountStrategy, boolean createPkcs12TsForPem) {
return of(new CertificateOptions(prefix, format, password, false, false, false,
new io.quarkus.test.services.Certificate.ClientCertificate[0],
targetDir, containerMountStrategy, createPkcs12TsForPem));
}

static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password) {
return of(prefix, format, password, false, false, false, new io.quarkus.test.services.Certificate.ClientCertificate[0]);
return of(prefix, format, password, createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false);
}

static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password,
boolean keystoreProps, boolean truststoreProps, boolean keystoreManagementInterfaceProps,
io.quarkus.test.services.Certificate.ClientCertificate[] clientCertificates) {
return of(new CertificateOptions(prefix, format, password, keystoreProps, truststoreProps,
keystoreManagementInterfaceProps,
clientCertificates, createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false));
}

private static Certificate of(CertificateOptions o) {
Map<String, String> props = new HashMap<>();
CertificateGenerator generator = new CertificateGenerator(createCertsTempDir(prefix), false);
CertificateGenerator generator = new CertificateGenerator(o.localTargetDir(), false);
String serverTrustStoreLocation = null;
String serverKeyStoreLocation = null;
String keyLocation = null;
String certLocation = null;
List<ClientCertificate> generatedClientCerts = new ArrayList<>();
String[] cnAttrs = collectCommonNames(clientCertificates);
var unknownClientCn = getUnknownClientCnAttr(clientCertificates, cnAttrs);
String[] cnAttrs = collectCommonNames(o.clientCertificates());
var unknownClientCn = getUnknownClientCnAttr(o.clientCertificates(), cnAttrs);

// 1. GENERATE FIRST CERTIFICATE AND SERVER KEYSTORE AND TRUSTSTORE
boolean withClientCerts = cnAttrs.length > 0;
String cn = withClientCerts ? cnAttrs[0] : "localhost";
final CertificateRequest request = createCertificateRequest(prefix, format, password, withClientCerts, cn);
final CertificateRequest request = createCertificateRequest(o.prefix(), o.format(), o.password(), withClientCerts, cn);
try {
var certFile = generator.generate(request).get(0);
if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) {
Expand All @@ -96,17 +110,32 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format
} else if (certFile instanceof PemCertificateFiles pemCertsFile) {
keyLocation = getPathOrNull(pemCertsFile.keyFile());
certLocation = getPathOrNull(pemCertsFile.certFile());
if (isOpenshiftPlatform() || isKubernetesPlatform()) {
if (o.createPkcs12TsForPem()) {
// PKCS12 truststore
serverTrustStoreLocation = createPkcs12TruststoreForPem(pemCertsFile.trustStore(), o.password(), cn);
} else {
// ca-cert
serverTrustStoreLocation = getPathOrNull(pemCertsFile.trustStore());
}
if (o.containerMountStrategy().mountToContainer()) {
if (certLocation != null) {
certLocation = makeFileMountPathUnique(prefix, certLocation);
// mount certificate to the pod
props.put(getRandomPropKey("crt"), toSecretProperty(certLocation));
var containerMountPath = o.containerMountStrategy().certPath(certLocation);
if (o.containerMountStrategy().containerShareMountPathWithApp()) {
certLocation = containerMountPath;
}

// mount certificate to the container
props.put(getRandomPropKey("crt"), toSecretProperty(containerMountPath));
}

if (keyLocation != null) {
keyLocation = makeFileMountPathUnique(prefix, keyLocation);
// mount private key to the pod
props.put(getRandomPropKey("key"), toSecretProperty(keyLocation));
var containerMountPath = o.containerMountStrategy().keyPath(keyLocation);
if (o.containerMountStrategy().containerShareMountPathWithApp()) {
keyLocation = containerMountPath;
}

// mount private key to the container
props.put(getRandomPropKey("key"), toSecretProperty(containerMountPath));
}
}
} else if (certFile instanceof JksCertificateFiles jksCertFile) {
Expand All @@ -126,18 +155,18 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format

// 2. IF THERE IS MORE THAN ONE CLIENT CERTIFICATE, GENERATE OTHERS
if (withClientCerts && cnAttrs.length > 1) {
if (format != PKCS12) {
if (o.format() != PKCS12) {
throw new IllegalArgumentException(
"Generation of more than one client certificate is only implemented for PKCS12.");
}
var correctClientTruststore = Path.of(generatedClientCerts.get(0).truststorePath()).toFile();
for (int i = 1; i < cnAttrs.length; i++) {
var clientCn = cnAttrs[i];
var clientPrefix = clientCn + "-" + prefix;
var clientRequest = createCertificateRequest(clientPrefix, format, password, true, clientCn);
var clientPrefix = clientCn + "-" + o.prefix();
var clientRequest = createCertificateRequest(clientPrefix, o.format(), o.password(), true, clientCn);
try {
var clientCertFile = (Pkcs12CertificateFiles) generator.generate(clientRequest).get(0);
fixGeneratedClientCerts(clientPrefix, password, clientCertFile, correctClientTruststore,
fixGeneratedClientCerts(clientPrefix, o.password(), clientCertFile, correctClientTruststore,
serverTrustStoreLocation, unknownClientCn, clientCn);
generatedClientCerts
.add(new ClientCertificateImpl(clientCn, getPathOrNull(clientCertFile.clientKeyStoreFile()),
Expand All @@ -150,36 +179,45 @@ static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format

// 3. PREPARE QUARKUS APPLICATION CONFIGURATION PROPERTIES
if (serverTrustStoreLocation != null) {
if (isOpenshiftPlatform() || isKubernetesPlatform()) {
// mount truststore to the pod
props.put(getRandomPropKey("truststore"), toSecretProperty(serverTrustStoreLocation));
if (o.containerMountStrategy().mountToContainer()) {
var containerMountPath = o.containerMountStrategy().truststorePath(serverTrustStoreLocation);
if (o.containerMountStrategy().containerShareMountPathWithApp()) {
serverTrustStoreLocation = containerMountPath;
}

// mount truststore to the container
props.put(getRandomPropKey("truststore"), toSecretProperty(containerMountPath));
}
if (truststoreProps) {
if (o.truststoreProps()) {
props.put("quarkus.http.ssl.certificate.trust-store-file", serverTrustStoreLocation);
props.put("quarkus.http.ssl.certificate.trust-store-file-type", format.toString());
props.put("quarkus.http.ssl.certificate.trust-store-password", password);
props.put("quarkus.http.ssl.certificate.trust-store-file-type", o.format().toString());
props.put("quarkus.http.ssl.certificate.trust-store-password", o.password());
}
}
if (serverKeyStoreLocation != null) {
if (isOpenshiftPlatform() || isKubernetesPlatform()) {
serverKeyStoreLocation = makeFileMountPathUnique(prefix, serverKeyStoreLocation);
// mount keystore to the pod
props.put(getRandomPropKey("keystore"), toSecretProperty(serverKeyStoreLocation));
if (o.containerMountStrategy().mountToContainer()) {
var containerMountPath = o.containerMountStrategy().keystorePath(serverKeyStoreLocation);
if (o.containerMountStrategy().containerShareMountPathWithApp()) {
serverKeyStoreLocation = containerMountPath;
}

// mount keystore to the container
props.put(getRandomPropKey("keystore"), toSecretProperty(containerMountPath));
}
if (keystoreProps) {
if (o.keystoreProps()) {
props.put("quarkus.http.ssl.certificate.key-store-file", serverKeyStoreLocation);
props.put("quarkus.http.ssl.certificate.key-store-file-type", format.toString());
props.put("quarkus.http.ssl.certificate.key-store-password", password);
props.put("quarkus.http.ssl.certificate.key-store-file-type", o.format().toString());
props.put("quarkus.http.ssl.certificate.key-store-password", o.password());
}
if (keystoreManagementInterfaceProps) {
if (o.keystoreManagementInterfaceProps()) {
props.put("quarkus.management.ssl.certificate.key-store-file", serverKeyStoreLocation);
props.put("quarkus.management.ssl.certificate.key-store-file-type", format.toString());
props.put("quarkus.management.ssl.certificate.key-store-password", password);
props.put("quarkus.management.ssl.certificate.key-store-file-type", o.format().toString());
props.put("quarkus.management.ssl.certificate.key-store-password", o.password());
}
}

return new CertificateImpl(serverKeyStoreLocation, serverTrustStoreLocation, Map.copyOf(props),
List.copyOf(generatedClientCerts), password, format.toString(), keyLocation, certLocation, prefix);
List.copyOf(generatedClientCerts), o.password(), o.format().toString(), keyLocation, certLocation, o.prefix());
}

private static String getUnknownClientCnAttr(io.quarkus.test.services.Certificate.ClientCertificate[] clientCertificates,
Expand Down Expand Up @@ -275,14 +313,7 @@ private static String getPathOrNull(Path file) {
return null;
}

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) {
static Path createCertsTempDir(String prefix) {
Path certsDir;
try {
certsDir = Files.createTempDirectory(prefix + "-certs");
Expand All @@ -291,4 +322,26 @@ private static Path createCertsTempDir(String prefix) {
}
return certsDir;
}

private static String createPkcs12TruststoreForPem(Path caCertPath, String password, String alias) {
try {
CertificateFactory fact = CertificateFactory.getInstance("X.509");
try (FileInputStream is = new FileInputStream(caCertPath.toFile())) {
X509Certificate cer = (X509Certificate) fact.generateCertificate(is);

var newTruststorePath = Files.createTempFile("pem-12-truststore", ".p12");

try (OutputStream truststoreOs = Files.newOutputStream(newTruststorePath)) {
KeyStore truststore = KeyStore.getInstance("PKCS12");
truststore.load(null, password.toCharArray());
truststore.setCertificateEntry(alias, cer);
truststore.store(truststoreOs, password.toCharArray());
}

return newTruststorePath.toAbsolutePath().toString();
}
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to create PKCS12 truststore", e);
}
}
}
Loading