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

io.kroxylicious.testing.kafka.common.KeytoolCertificateGenerator Maven / Gradle / Ivy

/*
 * Copyright Kroxylicious Authors.
 *
 * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package io.kroxylicious.testing.kafka.common;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;

/**
 * Used to configure and manage test certificates using the JDK's keytool.
 */
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Requires ability to write test key material to file-system.")
public class KeytoolCertificateGenerator {
    private static final String PKCS12_KEYSTORE_TYPE = "PKCS12";
    private String password;
    private final Path certFilePath;
    private final Path keyStoreFilePath;
    private final Path trustStoreFilePath;
    private final System.Logger log = System.getLogger(KeytoolCertificateGenerator.class.getName());

    /**
     * Instantiates a new Keytool certificate generator.
     *
     * @throws IOException the io exception
     */
    public KeytoolCertificateGenerator() throws IOException {
        this(null, null);
    }

    /**
     * Instantiates a new Keytool certificate generator.
     *
     * @param certFilePath the cert file path
     * @param trustStorePath the trust store path
     * @throws IOException the io exception
     */
    public KeytoolCertificateGenerator(String certFilePath, String trustStorePath) throws IOException {
        Path certsDirectory = Files.createTempDirectory("kproxy");
        this.certFilePath = Path.of(certsDirectory.toAbsolutePath() + "/cert-file");
        this.keyStoreFilePath = (certFilePath != null) ? Path.of(certFilePath) : Paths.get(certsDirectory.toAbsolutePath().toString(), "kafka.keystore.jks");
        this.trustStoreFilePath = (trustStorePath != null) ? Path.of(trustStorePath) : Paths.get(certsDirectory.toAbsolutePath().toString(), "kafka.truststore.jks");

        certsDirectory.toFile().deleteOnExit();
        if (certFilePath == null) {
            this.keyStoreFilePath.toFile().deleteOnExit();
        }
        if (trustStorePath == null) {
            this.trustStoreFilePath.toFile().deleteOnExit();
        }
        this.certFilePath.toFile().deleteOnExit();
    }

    /**
     * Gets cert file path.
     *
     * @return the absolute path to the certificate file
     */
    public String getCertFilePath() {
        return certFilePath.toAbsolutePath().toString();
    }

    /**
     * Gets key store location.
     *
     * @return the absolute path to the key store
     */
    public String getKeyStoreLocation() {
        return keyStoreFilePath.toAbsolutePath().toString();
    }

    /**
     * Gets trust store location.
     *
     * @return the absolute path to the trust store
     */
    public String getTrustStoreLocation() {
        return trustStoreFilePath.toAbsolutePath().toString();
    }

    /**
     * Gets password.
     *
     * @return the password
     */
    public String getPassword() {
        if (password == null) {
            password = UUID.randomUUID().toString().replace("-", "");
        }
        return password;
    }

    /**
     * Can generate wildcard san.
     *
     * @return true if java version is greater or equal to 17, false otherwise
     */
    public boolean canGenerateWildcardSAN() {
        return Runtime.version().feature() >= 17;
    }

    /**
     * Generate trust store.
     *
     * @param certFilePath the cert file path
     * @param alias the alias
     * @throws GeneralSecurityException the general security exception
     * @throws IOException the io exception
     */
    public void generateTrustStore(String certFilePath, String alias) throws GeneralSecurityException, IOException {
        this.generateTrustStore(certFilePath, alias, getTrustStoreLocation());
    }

    /**
     * Generate trust store.
     *
     * @param certFilePath the cert file path
     * @param alias the alias
     * @param trustStoreFilePath the trust store file path
     * @throws GeneralSecurityException the general security exception
     * @throws IOException the io exception
     */
    public void generateTrustStore(String certFilePath, String alias, String trustStoreFilePath)
            throws GeneralSecurityException, IOException {
        // keytool -import -trustcacerts -keystore truststore.jks -storepass password -noprompt -alias localhost -file cert.crt
        if (Path.of(trustStoreFilePath).toFile().exists()) {
            var keyStore = KeyStore.getInstance(new File(trustStoreFilePath), getPassword().toCharArray());
            if (keyStore.containsAlias(alias)) {
                keyStore.deleteEntry(alias);
                keyStore.store(new FileOutputStream(trustStoreFilePath), getPassword().toCharArray());
            }
        }

        final List commandParameters = new ArrayList<>(List.of("keytool", "-import", "-trustcacerts"));
        commandParameters.addAll(List.of("-keystore", trustStoreFilePath));
        commandParameters.addAll(List.of("-storepass", getPassword()));
        commandParameters.add("-noprompt");
        commandParameters.addAll(List.of("-alias", alias));
        commandParameters.addAll(List.of("-file", certFilePath));
        runCommand(commandParameters);
    }

    /**
     * Generate self-signed certificate entry.
     *
     * @param email the email
     * @param domain the domain
     * @param organizationUnit the organization unit
     * @param organization the organization
     * @param city the city
     * @param state the state
     * @param country the country
     * @throws GeneralSecurityException the general security exception
     * @throws IOException the io exception
     */
    public void generateSelfSignedCertificateEntry(String email, String domain, String organizationUnit,
                                                   String organization, String city, String state,
                                                   String country)
            throws GeneralSecurityException, IOException {

        if (keyStoreFilePath.toFile().exists()) {
            var keyStore = KeyStore.getInstance(keyStoreFilePath.toFile(), getPassword().toCharArray());
            keyStore.load(new FileInputStream(keyStoreFilePath.toFile()), getPassword().toCharArray());

            if (keyStore.containsAlias(domain)) {
                keyStore.deleteEntry(domain);
                keyStore.store(new FileOutputStream(keyStoreFilePath.toFile()), getPassword().toCharArray());
            }
        }

        final List commandParameters = new ArrayList<>(List.of("keytool", "-genkey"));
        commandParameters.addAll(List.of("-alias", domain));
        commandParameters.addAll(List.of("-keyalg", "RSA"));
        commandParameters.addAll(List.of("-keysize", "2048"));
        commandParameters.addAll(List.of("-sigalg", "SHA256withRSA"));
        commandParameters.addAll(List.of("-storetype", PKCS12_KEYSTORE_TYPE));
        commandParameters.addAll(List.of("-keystore", getKeyStoreLocation()));
        commandParameters.addAll(List.of("-storepass", getPassword()));
        commandParameters.addAll(List.of("-keypass", getPassword()));
        commandParameters.addAll(
                List.of("-dname", getDomainName(email, domain, organizationUnit, organization, city, state, country)));
        commandParameters.addAll(List.of("-validity", "365"));
        if (canGenerateWildcardSAN() && !isWildcardDomain(domain)) {
            commandParameters.addAll(getSAN(domain));
        }
        runCommand(commandParameters);

        createCrtFileToImport(domain);
    }

    private void createCrtFileToImport(String alias) throws IOException {
        // keytool -export -keystore examplestore -alias signFiles -file Example.cer
        final List commandParameters = new ArrayList<>(List.of("keytool", "-export", "-rfc"));
        commandParameters.addAll(List.of("-keystore", getKeyStoreLocation()));
        commandParameters.addAll(List.of("-storepass", getPassword()));
        commandParameters.addAll(List.of("-storetype", getKeyStoreType()));
        commandParameters.addAll(List.of("-alias", alias));
        commandParameters.addAll(List.of("-file", certFilePath.toAbsolutePath().toString()));
        runCommand(commandParameters);
    }

    private void runCommand(List commandParameters) throws IOException {
        ProcessBuilder keytool = new ProcessBuilder().command(commandParameters);

        final Process process = keytool.start();
        try {
            process.waitFor();
        }
        catch (InterruptedException e) {
            throw new IOException("Keytool execution error");
        }

        log.log(DEBUG, "Generating certificate using `keytool` using command: {0}, parameters: {1}",
                process.info(), commandParameters);

        if (process.exitValue() > 0) {
            final String processError = (new BufferedReader(new InputStreamReader(process.getErrorStream()))).lines()
                    .collect(Collectors.joining(" \\ "));
            final String processOutput = (new BufferedReader(new InputStreamReader(process.getInputStream()))).lines()
                    .collect(Collectors.joining(" \\ "));
            log.log(WARNING, "Error generating certificate, error output: {0}, normal output: {1}, " +
                    "commandline parameters: {2}",
                    processError, processOutput, commandParameters);
            throw new IOException(
                    "Keytool execution error: '" + processError + "', output: '" + processOutput + "'" + ", commandline parameters: " + commandParameters);
        }
    }

    private boolean isWildcardDomain(String domain) {
        return domain.startsWith("*.");
    }

    private String getDomainName(String email, String domain, String organizationUnit, String organization, String city,
                                 String state, String country) {
        // keytool doesn't allow for a domain with wildcard in SAN extension, so it has to go directly to CN
        return "CN=" + domain + ", OU=" + organizationUnit + ", O=" + organization + ", L=" + city + ", ST=" + state +
                ", C=" + country + ", EMAILADDRESS=" + email;
    }

    private List getSAN(String domain) {
        return List.of("-ext", "SAN=dns:" + domain);
    }

    public String getTrustStoreType() {
        return PKCS12_KEYSTORE_TYPE;
    }

    public String getKeyStoreType() {
        return PKCS12_KEYSTORE_TYPE;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy