All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.test.security.certificate.Certificate Maven / Gradle / Ivy

package io.quarkus.test.security.certificate;

import static io.quarkus.test.services.Certificate.Format.PEM;
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 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;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;

import org.junit.jupiter.api.condition.OS;

import io.quarkus.test.utils.FileUtils;
import io.quarkus.test.utils.TestExecutionProperties;
import io.smallrye.certs.CertificateGenerator;
import io.smallrye.certs.CertificateRequest;
import io.smallrye.certs.Format;
import io.smallrye.certs.JksCertificateFiles;
import io.smallrye.certs.PemCertificateFiles;
import io.smallrye.certs.Pkcs12CertificateFiles;

public interface Certificate {

    String prefix();

    String format();

    String password();

    String keystorePath();

    String truststorePath();

    Map configProperties();

    ClientCertificate getClientCertificateByCn(String cn);

    Collection clientCertificates();

    interface PemCertificate extends Certificate {

        String keyPath();

        String certPath();

    }

    static Certificate.PemCertificate ofRegeneratedCert(CertificateOptions o) {
        return of(o);
    }

    static Certificate.PemCertificate ofInterchangeable(CertificateOptions options) {
        // this allows to swap cert when it is regenerated without making all the classes above mutable
        // all but regenerated certificates should be interchangeable
        // for regenerated certs, it would just add unnecessary layer (but cause no issues)
        return InterchangeableCertificate.wrapCert(of(options), options);
    }

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

    static Certificate.PemCertificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password,
            boolean tlsRegistryEnabled, String tlsConfigName, ClientCertificateRequest[] clientCertRequests) {
        return ofInterchangeable(new CertificateOptions(prefix, format, password, false, false, false,
                clientCertRequests, createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false,
                null, null, null, null, tlsRegistryEnabled, tlsConfigName, false));
    }

    static Certificate of(String prefix, io.quarkus.test.services.Certificate.Format format, String password,
            boolean tlsRegistryEnabled, String tlsConfigName) {
        return ofInterchangeable(new CertificateOptions(prefix, format, password, false, false, false,
                new ClientCertificateRequest[0], createCertsTempDir(prefix), new DefaultContainerMountStrategy(prefix), false,
                null, null, null, null, tlsRegistryEnabled, tlsConfigName, false));
    }

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

    private static Certificate.PemCertificate of(CertificateOptions o) {
        Map props = new HashMap<>();
        CertificateGenerator generator = new CertificateGenerator(o.localTargetDir(), true);
        String serverTrustStoreLocation = null;
        String serverKeyStoreLocation = null;
        String keyLocation = null;
        String certLocation = null;
        List generatedClientCerts = new ArrayList<>();
        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(o.prefix(), o.format(), o.password(), withClientCerts, cn);
        try {
            var certFile = generator.generate(request).get(0);
            if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) {
                serverKeyStoreLocation = getPathOrNull(pkcs12CertFile.keyStoreFile());
                if (withClientCerts) {
                    serverTrustStoreLocation = getPathOrNull(pkcs12CertFile.serverTrustStoreFile());
                    var clientKeyStoreLocation = getPathOrNull(pkcs12CertFile.clientKeyStoreFile());
                    var clientTrustStoreLocation = getPathOrNull(pkcs12CertFile.trustStoreFile());
                    generatedClientCerts.add(new ClientCertificateImpl(cn, clientKeyStoreLocation, clientTrustStoreLocation));
                } else {
                    serverTrustStoreLocation = getPathOrNull(pkcs12CertFile.trustStoreFile());
                }
            } else if (certFile instanceof PemCertificateFiles pemCertsFile) {
                keyLocation = getPathOrNull(pemCertsFile.keyFile());
                certLocation = getPathOrNull(pemCertsFile.certFile());
                if (o.createPkcs12TsForPem()) {
                    // PKCS12 truststore
                    serverTrustStoreLocation = createPkcs12TruststoreForPem(pemCertsFile.trustStore(), o.password(), cn);
                } else {
                    if (withClientCerts) {
                        serverTrustStoreLocation = getPathOrNull(pemCertsFile.serverTrustFile());
                        var clientCertLocation = getPathOrNull(pemCertsFile.clientCertFile());
                        var clientKeyLocation = getPathOrNull(pemCertsFile.clientKeyFile());
                        var clientTrustStore = getPathOrNull(pemCertsFile.trustFile());
                        generatedClientCerts
                                .add(new ClientCertificateImpl(cn, null, clientTrustStore, clientKeyLocation,
                                        clientCertLocation));
                    } else {
                        // ca-cert
                        serverTrustStoreLocation = getPathOrNull(pemCertsFile.trustStore());
                    }
                }
                if (o.containerMountStrategy().mountToContainer()) {
                    if (certLocation != null) {
                        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) {
                        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) {
                serverKeyStoreLocation = getPathOrNull(jksCertFile.keyStoreFile());
                if (withClientCerts) {
                    serverTrustStoreLocation = getPathOrNull(jksCertFile.serverTrustStoreFile());
                    var clientKeyStoreLocation = getPathOrNull(jksCertFile.clientKeyStoreFile());
                    var clientTrustStoreLocation = getPathOrNull(jksCertFile.trustStoreFile());
                    generatedClientCerts.add(new ClientCertificateImpl(cn, clientKeyStoreLocation, clientTrustStoreLocation));
                } else {
                    serverTrustStoreLocation = getPathOrNull(jksCertFile.trustStoreFile());
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate certificate", e);
        }

        // 2. IF THERE IS MORE THAN ONE CLIENT CERTIFICATE, GENERATE OTHERS
        if (withClientCerts && cnAttrs.length > 1) {
            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 + "-" + o.prefix();
                var clientRequest = createCertificateRequest(clientPrefix, o.format(), o.password(), true, clientCn);
                try {
                    var clientCertFile = (Pkcs12CertificateFiles) generator.generate(clientRequest).get(0);
                    fixGeneratedClientCerts(clientPrefix, o.password(), clientCertFile, correctClientTruststore,
                            serverTrustStoreLocation, unknownClientCn, clientCn);
                    generatedClientCerts
                            .add(new ClientCertificateImpl(clientCn, getPathOrNull(clientCertFile.clientKeyStoreFile()),
                                    getPathOrNull(clientCertFile.trustStoreFile())));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }

        // 3. PREPARE QUARKUS APPLICATION CONFIGURATION PROPERTIES
        if (serverTrustStoreLocation != null) {
            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));
            }
            configureServerTrustStoreProps(o, props, serverTrustStoreLocation);
        }
        if (serverKeyStoreLocation != null) {
            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));
            }
            configureServerKeyStoreProps(o, props, serverKeyStoreLocation);
        }
        configureManagementInterfaceProps(o, props, serverKeyStoreLocation);
        configureHttpServerProps(o, props);
        configurePemConfigurationProperties(o, props, keyLocation, certLocation, serverTrustStoreLocation);
        doubleBackSlashesOnWin(props);

        return createCertificate(serverKeyStoreLocation, serverTrustStoreLocation, Map.copyOf(props),
                List.copyOf(generatedClientCerts), keyLocation, certLocation, o);
    }

    private static void doubleBackSlashesOnWin(Map props) {
        if (OS.WINDOWS.isCurrentOs()) {
            // we need to quote back slashes passed as command lines in Windows as they have special meaning
            // TODO: this must be done for all config properties, but I do not dare to change it now
            //   now is not the good time to break test suite as this PR is going to be backported and we have pressing
            //   matters; let's do it later: https://github.com/quarkus-qe/quarkus-test-framework/issues/1275
            props.replaceAll((key, value) -> value.replace("\\", "\\\\"));
        }
    }

    private static void configurePemConfigurationProperties(CertificateOptions options, Map props,
            String keyLocation, String certLocation, String serverTrustStoreLocation) {
        if (options.format() == PEM && options.tlsRegistryEnabled()) {
            var keyStorePropertyPrefix = tlsConfigPropPrefix(options, "key-store");
            if (keyLocation != null) {
                props.put(keyStorePropertyPrefix + "pem-1.key", keyLocation);
            }
            if (certLocation != null) {
                props.put(keyStorePropertyPrefix + "pem-1.cert", certLocation);
            }
            var trustStorePropertyPrefix = tlsConfigPropPrefix(options, "trust-store");
            if (serverTrustStoreLocation != null) {
                props.put(trustStorePropertyPrefix + "certs", serverTrustStoreLocation);
            }
        }
    }

    private static void configureManagementInterfaceProps(CertificateOptions o, Map props,
            String serverKeyStoreLocation) {
        if (o.configureManagementInterface()) {
            props.put(TestExecutionProperties.MANAGEMENT_INTERFACE_ENABLED, Boolean.TRUE.toString());
            if (o.tlsRegistryEnabled()) {
                if (isNotDefaultTlsConfig(o)) {
                    // default TLS registry is configured automatically
                    props.put("quarkus.management.tls-configuration-name", o.tlsConfigName());
                }
            } else if (serverKeyStoreLocation != null) {
                props.put("quarkus.management.ssl.certificate.key-store-file", serverKeyStoreLocation);
                props.put("quarkus.management.ssl.certificate.key-store-file-type", o.format().toString());
                props.put("quarkus.management.ssl.certificate.key-store-password", o.password());
            }
        }
    }

    private static void configureHttpServerProps(CertificateOptions o, Map props) {
        if (o.configureHttpServer() && o.tlsRegistryEnabled()) {
            if (isNotDefaultTlsConfig(o)) {
                // default TLS registry is configured automatically
                props.put("quarkus.http.tls-configuration-name", o.tlsConfigName());
            }
        }
    }

    private static boolean isNotDefaultTlsConfig(CertificateOptions o) {
        if (o.tlsConfigName() == null) {
            throw new IllegalArgumentException("TLS registry is enabled but TLS config name is null");
        }
        return !io.quarkus.test.services.Certificate.DEFAULT_CONFIG.equals(o.tlsConfigName());
    }

    private static void configureServerKeyStoreProps(CertificateOptions o, Map props,
            String serverKeyStoreLocation) {
        if (o.keystoreProps()) {
            if (o.tlsRegistryEnabled()) {
                if (o.format() != PEM) {
                    var propPrefix = tlsConfigPropPrefix(o, "key-store");
                    props.put(propPrefix + "path", serverKeyStoreLocation);
                    props.put(propPrefix + "password", o.password());
                }
            } else {
                props.put("quarkus.http.ssl.certificate.key-store-file", serverKeyStoreLocation);
                props.put("quarkus.http.ssl.certificate.key-store-file-type", o.format().toString());
                props.put("quarkus.http.ssl.certificate.key-store-password", o.password());
            }
        }
    }

    private static void configureServerTrustStoreProps(CertificateOptions o, Map props,
            String serverTrustStoreLocation) {
        if (o.truststoreProps()) {
            if (o.tlsRegistryEnabled()) {
                if (o.format() != PEM) {
                    var propPrefix = tlsConfigPropPrefix(o, "trust-store");
                    props.put(propPrefix + "path", serverTrustStoreLocation);
                    props.put(propPrefix + "password", o.password());
                }
            } else {
                props.put("quarkus.http.ssl.certificate.trust-store-file", serverTrustStoreLocation);
                props.put("quarkus.http.ssl.certificate.trust-store-file-type", o.format().toString());
                props.put("quarkus.http.ssl.certificate.trust-store-password", o.password());
            }
        }
    }

    private static String tlsConfigPropPrefix(CertificateOptions o, String propInfix) {
        if (o.tlsConfigName() == null) {
            throw new IllegalArgumentException("TLS registry is enabled but TLS config name is null");
        }
        final String baseConfigProp;
        if (io.quarkus.test.services.Certificate.DEFAULT_CONFIG.equals(o.tlsConfigName())) {
            // default config
            baseConfigProp = "quarkus.tls.";
        } else {
            // named config
            baseConfigProp = "quarkus.tls." + o.tlsConfigName() + ".";
        }
        final String storeFormat = switch (o.format()) {
            case PKCS12 -> "p12.";
            case JKS -> "jks.";
            default -> "pem.";
        };
        return baseConfigProp + propInfix + "." + storeFormat;
    }

    private static Certificate.PemCertificate createCertificate(String keystorePath, String truststorePath,
            Map configProperties, Collection clientCertificates, String keyPath,
            String certPath, CertificateOptions o) {
        var format = o.format().toString();

        // if customizers required new location, let's move file there
        certPath = moveFileIfRequired(o.certLocation(), certPath);
        keyPath = moveFileIfRequired(o.keyLocation(), keyPath);
        keystorePath = moveFileIfRequired(o.serverKeyStoreLocation(), keystorePath);
        truststorePath = moveFileIfRequired(o.serverTrustStoreLocation(), truststorePath);

        return new CertificateImpl(keystorePath, truststorePath, Map.copyOf(configProperties),
                clientCertificates, o.password(), format, keyPath, certPath, o.prefix());
    }

    private static String getUnknownClientCnAttr(ClientCertificateRequest[] clientCertificates,
            String[] cnAttrs) {
        var cnAttrsUnknownToServer = Arrays.stream(clientCertificates)
                .filter(ClientCertificateRequest::unknownToServer)
                .map(ClientCertificateRequest::cnAttribute)
                .collect(Collectors.toSet());
        if (cnAttrsUnknownToServer.isEmpty()) {
            return null;
        }

        if (cnAttrsUnknownToServer.size() > 1) {
            throw new IllegalArgumentException("Only one client certificate can be unknown to the server");
        }
        if (cnAttrs.length == 1) {
            throw new IllegalArgumentException("More than one client certificate must be specified to support unknown one");
        }

        var unknownClientCn = cnAttrsUnknownToServer.stream().findFirst().get();

        // make sure the unknown client is not at the first position as it would be harder to implement
        if (unknownClientCn.equals(cnAttrs[0])) {
            var lastCn = cnAttrs[cnAttrs.length - 1];
            cnAttrs[0] = lastCn;
            cnAttrs[cnAttrs.length - 1] = unknownClientCn;
        }

        return unknownClientCn;
    }

    private static void fixGeneratedClientCerts(String prefix, String password, Pkcs12CertificateFiles clientCertFile,
            File correctClientTruststore, String serverTrustStoreLocation, String unknownClientCn, String clientCn)
            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        // ATM the certificate generator we use does not support generation of more than one client certificate
        // as it prefers aliases, however for backwards compatibility I don't want to enforce aliases

        // basically, here we have client certificates for a different server keystore / truststore
        // we need:
        // 1. fix client trust store by reusing the one client truststore we already have
        var targetClientTruststore = clientCertFile.trustStoreFile().toFile();
        org.apache.commons.io.FileUtils.copyFile(correctClientTruststore, targetClientTruststore, REPLACE_EXISTING);

        if (unknownClientCn == null || !unknownClientCn.equals(clientCn)) {
            // 2. add our client certificate to server truststore
            var clientKs = KeyStore.getInstance(clientCertFile.clientKeyStoreFile().toFile(), password.toCharArray());
            var clientCertFromKs = clientKs.getCertificate(prefix);
            Objects.requireNonNull(clientCertFromKs);
            var serverTsFile = Path.of(serverTrustStoreLocation).toFile();
            var serverTs = KeyStore.getInstance(serverTsFile, password.toCharArray());
            serverTs.setCertificateEntry(prefix, clientCertFromKs);
            try (FileOutputStream trustStoreFos = new FileOutputStream(serverTsFile)) {
                serverTs.store(trustStoreFos, password.toCharArray());
            }
        }
    }

    private static String[] collectCommonNames(ClientCertificateRequest[] clientCertificates) {
        return Arrays
                .stream(clientCertificates)
                .map(ClientCertificateRequest::cnAttribute)
                .toArray(String[]::new);
    }

    private static CertificateRequest createCertificateRequest(String prefix,
            io.quarkus.test.services.Certificate.Format format, String password, boolean withClientCerts, String cn) {
        return (new CertificateRequest())
                .withName(prefix)
                .withFormat(Format.valueOf(format.toString()))
                .withClientCertificate(withClientCerts)
                .withCN(cn)
                .withPassword(password)
                .withSubjectAlternativeName("localhost")
                .withSubjectAlternativeName("0.0.0.0")
                .withDuration(Duration.ofDays(2));
    }

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

    private static String toSecretProperty(String path) {
        var file = Path.of(path).toFile();
        String fileName = file.getName();
        String pathToFile = file.getParentFile().getAbsolutePath();
        return SECRET_WITH_DESTINATION_PREFIX + pathToFile + DESTINATION_TO_FILENAME_SEPARATOR + fileName;
    }

    private static String getPathOrNull(Path file) {
        if (file != null) {
            return file.toAbsolutePath().toString();
        }
        return null;
    }

    static Path createCertsTempDir(String prefix) {
        Path certsDir;
        try {
            certsDir = Files.createTempDirectory(prefix + "-certs");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        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);
        }
    }

    private static String moveFileIfRequired(String newPath, String currentPath) {
        if (newPath != null) {
            FileUtils.copyFileTo(currentPath, Path.of(newPath));
            return newPath;
        }
        return currentPath;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy