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

com.palantir.conjure.java.config.ssl.KeyStores Maven / Gradle / Ivy

/*
 * (c) Copyright 2017 Palantir Technologies Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.palantir.conjure.java.config.ssl;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.io.BaseEncoding;
import com.palantir.conjure.java.config.ssl.pkcs1.Pkcs1PrivateKeyReader;
import com.palantir.logsafe.exceptions.SafeRuntimeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

final class KeyStores {

    private static final Cache certCache =
            Caffeine.newBuilder().maximumSize(1024).softValues().build();

    /**
     * Pattern that matches a single RSA key in a PEM file. Has a capture group that captures the content of the key
     * (everything that occurs between the header and footer).
     */
    private static final Pattern KEY_PATTERN = Pattern.compile(
            "-----BEGIN (RSA)? ?PRIVATE KEY-----\n?(.+?)\n?-----END (RSA)? ?PRIVATE KEY-----", Pattern.DOTALL);

    /**
     * Pattern that matches a single certificate in a PEM file. Has a capture group that captures the content of the
     * certificate (everything that occurs between the header and footer).
     */
    private static final Pattern CERT_PATTERN =
            Pattern.compile("-----BEGIN CERTIFICATE-----\n?(.+?)\n?-----END CERTIFICATE-----", Pattern.DOTALL);

    private static final FileFilter VISIBLE_FILE_FILTER = new FileFilter() {
        @Override
        public boolean accept(File pathname) {
            return !pathname.isHidden();
        }
    };

    private KeyStores() {}

    /**
     * Returns a {@link KeyStore} created by loading the certificate or certificates specified by the provided path.
     *
     * @param path a path to an X.509 certificate in PEM or DER format, or to a directory containing such files. If the
     *     path specifies a directory, every non-hidden file in the directory must be a file of the specified format or
     *     an exception is thrown.
     * @return a new KeyStore of type {@link KeyStore#getDefaultType()} that contains the certificates specified by the
     *     provided path. The name of the file used to create a certificate entry is used as the alias for the entry.
     *     The returned store will not have any password.
     */
    static KeyStore createTrustStoreFromCertificates(Path path) {
        KeyStore keyStore;
        keyStore = createKeyStore();

        for (File currFile : getFilesForPath(path)) {
            try (InputStream in = new BufferedInputStream(Files.newInputStream(currFile.toPath()))) {
                addCertificatesToKeystore(keyStore, currFile.getName(), readX509Certificates(in));
            } catch (IOException e) {
                throw new RuntimeException(
                        String.format("IOException encountered when opening '%s'", currFile.toPath()), e);
            } catch (CertificateException | KeyStoreException e) {
                throw new RuntimeException(
                        String.format("Could not read file at \"%s\" as an X.509 certificate", currFile.toPath()), e);
            }
        }

        return keyStore;
    }

    /**
     * Returns a {@link KeyStore} created by loading the given certificates.
     *
     * @param certificatesByAlias a map of X.509 certificate in PEM or DER format by the alias to load the certificate
     *     as.
     */
    static KeyStore createTrustStoreFromCertificates(Map certificatesByAlias) {
        KeyStore keyStore;
        keyStore = createKeyStore();

        for (Map.Entry entry : certificatesByAlias.entrySet()) {
            try (InputStream certIn =
                    new ByteArrayInputStream(entry.getValue().pemCertificate().getBytes(StandardCharsets.UTF_8))) {
                addCertificatesToKeystore(keyStore, entry.getKey(), readX509Certificates(certIn));
            } catch (IOException e) {
                throw new RuntimeException(e);
            } catch (GeneralSecurityException e) {
                throw new RuntimeException(
                        String.format(
                                "Could not read certificate alias \"%s\" as an X.509 certificate", entry.getKey()),
                        e);
            }
        }

        return keyStore;
    }

    private static void addCertificatesToKeystore(
            KeyStore keyStore, String certificateEntryNamePrefix, List certificates)
            throws KeyStoreException {
        int certIndex = 0;
        for (Certificate cert : certificates) {
            keyStore.setCertificateEntry(certificateEntryNamePrefix + "-" + certIndex, cert);
            certIndex++;
        }
    }

    /**
     * Returns a {@link KeyStore} created by loading the combined PEM (a single PEM with both the key and certificate
     * chain) file or files specified by the provided path.
     *
     * @param filePathOrDirectory a path to a PEM file containing a PKCS#1 RSA private key and the certificate(s) for
     *     that key in PEM format, or to a directory containing such files. If the path specifies a directory, every
     *     non-hidden file in the directory must be a file of the specified format or an exception is thrown.
     * @return a new KeyStore of type {@link KeyStore#getDefaultType()} that contains the key entries specified by the
     *     provided path. The name of the file used to create a key entry is used as the alias for the entry. The
     *     provided password is used to secure the key store and all of the key entries.
     */
    static KeyStore createKeyStoreFromCombinedPems(Path filePathOrDirectory) {
        try {
            KeyStore keyStore = KeyStore.getInstance("pkcs12");
            keyStore.load(null, null);

            for (File currFile : getFilesForPath(filePathOrDirectory)) {
                KeyStore.PrivateKeyEntry privateKeyEntry = readKeyEntryFromPems(currFile.toPath(), currFile.toPath());
                keyStore.setKeyEntry(
                        currFile.getName(),
                        privateKeyEntry.getPrivateKey(),
                        null,
                        privateKeyEntry.getCertificateChain());
            }

            return keyStore;
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns a {@link KeyStore} created by loading the PEM files for keys and certificates from the directories at the
     * specified paths. Every visible file in the keyDirPath directory that ends with keyExtension will be read in as a
     * private key with a certificate chain that comes from a file in the certDirPath directory that consists of the
     * base name of the file concatenated with certExtension. The key files must contain an RSA private key in PKCS#1
     * format and the certificate files must contain X.509 certificates. Throws an exception if either path that is
     * provided is not a directory or if there is any key which does not have a corresponding certificate.
     *
     * @param keyDirPath path to the directory that contains the key files (PKCS#1 in PEM format)
     * @param keyExtension file extension for the files in the keyDirPath directory that should be considered keys. Is
     *     used to perform a suffix match. Case-sensitive and should include a period character if it is desired (for
     *     example, ".key", ".pem").
     * @param certDirPath path to the directory that contains the certificate files for the keys
     * @param certExtension file extension for the files in certDirPath directory that should be considered certificate
     *     files. For every key file, the file "fileName - keyExtension + certExtension" must exist in the certDirPath.
     *     Case-sensitive and should include a period character if it is desired (for example, ".cer", ".pem").
     * @return a new KeyStore of type {@link KeyStore#getDefaultType()} that contains the key entries specified by the
     *     arguments. The base name of the file used to create a key entry is used as the alias for the entry. The
     *     provided password is used to secure the key store and all of the key entries.
     */
    static KeyStore createKeyStoreFromPemDirectories(
            Path keyDirPath, String keyExtension, Path certDirPath, String certExtension) {
        if (!keyDirPath.toFile().isDirectory()) {
            throw new IllegalStateException(String.format("keyDirPath is not a directory: \"%s\"", keyDirPath));
        } else if (!certDirPath.toFile().isDirectory()) {
            throw new IllegalStateException(String.format("certDirPath is not a directory: \"%s\"", certDirPath));
        }

        try {
            KeyStore keyStore = KeyStore.getInstance("pkcs12");
            keyStore.load(null, null);

            File[] keyFiles = getFilesForPath(keyDirPath);
            for (File currKeyFile : keyFiles) {
                // find key files based on extension
                String currKeyFileName = currKeyFile.getName();
                if (currKeyFileName.endsWith(keyExtension)) {
                    // derive cert file based on key file
                    String baseName = currKeyFileName.substring(0, currKeyFileName.length() - keyExtension.length());
                    Path currCertPath = certDirPath.resolve(baseName + certExtension);

                    KeyStore.PrivateKeyEntry privateKeyEntry = readKeyEntryFromPems(currKeyFile.toPath(), currCertPath);
                    keyStore.setKeyEntry(
                            baseName, privateKeyEntry.getPrivateKey(), null, privateKeyEntry.getCertificateChain());
                }
            }

            return keyStore;
        } catch (GeneralSecurityException | IOException e) {
            throw new SafeRuntimeException("Failed to create key store from PEM directories", e);
        }
    }

    /**
     * Returns an array of files for the specified path. If the specified path is a directory, returns an array of the
     * visible (not hidden) files in the directory. Otherwise, returns an array with a single element that consists of
     * the file referred to by the provided path.
     */
    private static File[] getFilesForPath(Path path) {
        File[] files;
        File pathFile = path.toFile();

        if (pathFile.isDirectory()) {
            files = pathFile.listFiles(VISIBLE_FILE_FILTER);
            if (files == null) {
                throw new IllegalStateException(String.format("failed to list visible files in directory %s", path));
            }
        } else {
            files = new File[] {pathFile};
        }

        return files;
    }

    /**
     * Returns a {@link KeyStore} created by by loading all of the entries in the store specified by the provided
     * parameters.
     *
     * @param storeType the type of the store specified by the provided path. Must be a type accepted by
     *     {@link KeyStore#getInstance(String)}. The returned store will also be of this type.
     * @param path the path to the existing store to load.
     * @param password an optional password used to read the provided store. If absent, no password will be used to read
     *     the store.
     * @return a new KeyStore of type storeType that contains the entries read from the store at the provided path using
     *     the provided password.
     */
    static KeyStore loadKeyStore(String storeType, Path path, Optional password) {
        try {
            KeyStore keyStore = KeyStore.getInstance(storeType);
            try (InputStream stream = Files.newInputStream(path)) {
                char[] passwordChars = password.map(String::toCharArray).orElse(null);
                keyStore.load(stream, passwordChars);
            }
            return keyStore;
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Return a new {@link KeyStore} that contains the key and certificate chain with the provided alias in the given
     * key store.
     *
     * @param original key store that contains the key and certificate chain
     * @param password the password for the provided key store. Will also be used as the password of the returned key
     *     store.
     * @param alias the alias of the key and certificate chain in the provided key store
     * @return a newly constructed key store that contains a single entry that consists of the key and certificate chain
     *     with the provided alias in the given key store. The trust store will be of the same type as the provided one,
     *     will use the same password, and will store the key and certificate chain using the same alias.
     * @throws IllegalStateException if the provided key store does not contain a key and certificate chain with the
     *     given alias
     */
    static KeyStore newKeyStoreWithEntry(KeyStore original, Optional password, String alias) {
        try {
            KeyStore newKeyStore = KeyStore.getInstance(original.getType());
            char[] passwordChar = password.map(String::toCharArray).orElse(null);
            newKeyStore.load(null, passwordChar);

            Key aliasKey = original.getKey(alias, passwordChar);
            if (aliasKey == null) {
                throw new IllegalStateException(
                        String.format("Could not find key with alias \"%s\" in key store", alias));
            }
            Certificate[] certificateChain = original.getCertificateChain(alias);
            if (certificateChain == null) {
                throw new IllegalStateException(
                        String.format("Could not find certificate chain with alias \"%s\" in key store", alias));
            }

            newKeyStore.setKeyEntry(alias, aliasKey, passwordChar, certificateChain);
            return newKeyStore;
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static KeyStore createKeyStore() {
        KeyStore keyStore;
        try {
            keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException(e);
        }
        return keyStore;
    }

    static List readX509Certificates(InputStream certificateIn) throws CertificateException {
        return CertificateFactory.getInstance("X.509").generateCertificates(certificateIn).stream()
                .map(cert -> getCertFromCache((X509Certificate) cert))
                .collect(Collectors.toList());
    }

    /**
     * Returns a KeyStore.PrivateKeyEntry consisting of the private key and certificate chain in the files at the
     * provided paths. The key file must contain an RSA private key in PEM format (PKCS#1) and the certificate file must
     * contain the certificate chain for the key in PEM format. The key and cert file paths may be the same if there is
     * a single file that contains both the key and the certificates.
     *
     * @param privateKeyFilePath path to a file that contains the RSA private key in PEM format (PKCS#1)
     * @param certFilePath path to a file that contains the certificates for the key in PEM format. If there are
     *     multiple certificates in the chain, the file should contain them in chaining order.
     * @return a KeyStore.PrivateKeyEntry that consists of the private key and the certificate chain for the key read
     *     from the provided paths.
     */
    private static KeyStore.PrivateKeyEntry readKeyEntryFromPems(Path privateKeyFilePath, Path certFilePath) {
        // read private key
        String keyPemFileString;
        PrivateKey privateKey;
        try {
            keyPemFileString = readFileAsString(privateKeyFilePath);
            privateKey = getPrivateKeyFromString(keyPemFileString);
        } catch (IOException | GeneralSecurityException e) {
            throw new RuntimeException(
                    String.format("Failed to read private key from file at \"%s\"", privateKeyFilePath), e);
        }

        // read certificates
        Certificate[] certificates;
        try {
            // if key and cert file are the same, use string that was already created
            String certPemFileString =
                    privateKeyFilePath.equals(certFilePath) ? keyPemFileString : readFileAsString(certFilePath);
            certificates = getCertificatesFromString(certPemFileString).toArray(new Certificate[0]);
        } catch (IOException | GeneralSecurityException e) {
            throw new RuntimeException(
                    String.format("Failed to read certificates from file at \"%s\"", certFilePath), e);
        }

        return new KeyStore.PrivateKeyEntry(privateKey, certificates);
    }

    private static String readFileAsString(Path path) throws IOException {
        byte[] fileBytes = Files.readAllBytes(path);
        return new String(fileBytes, StandardCharsets.UTF_8);
    }

    /**
     * Returns a PrivateKey representing the first PEM formatted RSA key in the provided string. Throws an exception if
     * the provided string does not contain a valid RSA key.
     *
     * @param pemFileString string that contains PEM formatted RSA key (PKCS#1). The string can contain other content as
     *     well (for example, certificates or other information) as long as properly formatted RSA key content exists in
     *     the string.
     * @return PrivateKey representing the first RSA key in the provided string
     */
    static PrivateKey getPrivateKeyFromString(String pemFileString) throws GeneralSecurityException {
        Matcher matcher = KEY_PATTERN.matcher(pemFileString);
        if (!matcher.find() || !Objects.equals(matcher.group(1), matcher.group(3))) {
            throw new GeneralSecurityException(
                    String.format("unable to find valid RSA key in the provided string: %s", pemFileString));
        }

        // get content between headers and strip newlines to get Base64 encoded ASN1 DER only
        String privateKeyString = matcher.group(2).replace("\n", "");

        // read private key
        byte[] privateKeyDerBytes = BaseEncoding.base64().decode(privateKeyString);

        KeySpec rsaPrivKeySpec = "RSA".equals(matcher.group(1))
                ? parsePkcs1PrivateKey(privateKeyDerBytes)
                : parsePkcs8PrivateKey(privateKeyDerBytes);
        return KeyFactory.getInstance("RSA").generatePrivate(rsaPrivKeySpec);
    }

    static RSAPrivateKeySpec parsePkcs1PrivateKey(byte[] pkcs1DerBytes) {
        return new Pkcs1PrivateKeyReader(pkcs1DerBytes).readRsaKey();
    }

    static PKCS8EncodedKeySpec parsePkcs8PrivateKey(byte[] pkcs8DerBytes) {
        return new PKCS8EncodedKeySpec(pkcs8DerBytes);
    }

    /**
     * Returns a List of certificates representing the PEM formatted X.509 certificates in the provided string. The
     * order of the certificates in the list will match the order that the certificates occur in the string.
     *
     * @param pemFileString string that contains PEM formatted X.509 certificates. The string can contain other content
     *     as well (for example, RSA keys or other information) as long as properly formatted certificate content exists
     *     in the string.
     * @return list of Certificates that represent the X.509 certificates that were found in the input string in the
     *     order that they appeared in the string. Will be empty if no certificates were found in the input string.
     */
    private static List getCertificatesFromString(String pemFileString)
            throws IOException, CertificateException {
        Matcher matcher = CERT_PATTERN.matcher(pemFileString);

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        List certList = new ArrayList<>();
        while (matcher.find()) {
            try (InputStream stream = new ByteArrayInputStream(matcher.group().getBytes(StandardCharsets.UTF_8))) {
                certList.add(getCertFromCache((X509Certificate) certFactory.generateCertificate(stream)));
            }
        }

        return certList;
    }

    private static X509Certificate getCertFromCache(X509Certificate certificate) {
        try {
            return certCache.get(new EqualByteArray(certificate.getEncoded()), _input -> certificate);
        } catch (CertificateEncodingException e) {
            throw new SafeRuntimeException("Unable to get certificate bytes", e);
        }
    }

    private static class EqualByteArray {

        private final byte[] bytes;
        private int hash;

        EqualByteArray(byte[] bytes) {
            this.bytes = bytes;
        }

        @Override
        public int hashCode() {
            if (hash == 0 && bytes.length > 0) {
                hash = Arrays.hashCode(bytes);
            }
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof EqualByteArray)) {
                return false;
            }
            EqualByteArray other = (EqualByteArray) obj;
            return Arrays.equals(this.bytes, other.bytes);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy