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

com.seeq.link.agent.DefaultCertificateService Maven / Gradle / Ivy

There is a newer version: 66.0.0-v202410141803
Show newest version
package com.seeq.link.agent;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.io.FileUtils;

import com.seeq.link.sdk.interfaces.CertificateService;
import com.seeq.utilities.process.OperatingSystem;
import com.seeq.utilities.process.ProcessSpawn;

import lombok.extern.slf4j.Slf4j;


@Slf4j
public class DefaultCertificateService implements CertificateService {
    private final List trustedCertificatesPaths;
    private final Path untrustedCertificatesPath;
    private volatile KeyStore keyStore;
    private volatile X509TrustManager trustManager;

    public DefaultCertificateService() {
        this(new ArrayList<>(), null);
    }

    public DefaultCertificateService(Path trustedCertificatesPath) {
        this(Collections.singletonList(trustedCertificatesPath), null);
    }

    public DefaultCertificateService(List trustedCertificatesPaths, Path untrustedCertificatesPath) {
        this.trustedCertificatesPaths = trustedCertificatesPaths;
        this.untrustedCertificatesPath = untrustedCertificatesPath;
        this.keyStore = null;
        this.trustManager = null;
    }

    @Override
    public String addUntrustedCertificatesToDisk(String url) {
        List certificates = downloadCertificatesFromSite(url);
        List entries = new ArrayList<>();
        String messagePreamble = String.format("Encountered SSL error connecting to '%s', probably because of an " +
                "untrusted SSL certificate.", url);

        for (X509Certificate certificate : certificates) {
            String certName = getCertName(certificate);
            try {
                entries.add(String.format("%s\n%s", certName, convertCertificateToPEM(certificate)));
            } catch (CertificateEncodingException e) {
                return String.format("%s Seeq attempted to download the untrusted certificate on the " +
                                "Agent machine, but encountered an error converting certificate '%s' to PEM " +
                                "format:\n%s",
                        messagePreamble, certName, e);
            }
        }

        String content = String.join("\n\n", entries);
        if (!this.untrustedCertificatesPath.toFile().isDirectory()) {
            return String.format("%s Seeq attempted to write the untrusted certificate to '%s' on the Agent " +
                            "machine, but the path does not exist or is not a directory.",
                    messagePreamble, this.untrustedCertificatesPath);
        }

        String fileName = String.format("%s.pem", getCleansedUrlMoniker(url));
        Path filePath = this.untrustedCertificatesPath.resolve(fileName);

        try {
            Files.write(filePath, content.getBytes());
        } catch (IOException e) {
            return String.format("%s Seeq attempted to write the untrusted certificate to '%s' on the Agent " +
                            "machine, but encountered an error:\n%s",
                    messagePreamble, filePath, e);
        }

        // We initialize here in case the user moved the certificate from untrusted to trusted, so that the next time
        // this is called, we'll pick it up.
        this.initialize();

        return String.format("%s The untrusted certificate has been written to '%s' on the " +
                        "Agent machine, you can move it to the trusted folder if appropriate. See https://telemetry" +
                        ".seeq.com/support-link/kb/latest/cloud/troubleshooting-secure-configuration-ssl-tls for " +
                        "more information.",
                messagePreamble, filePath);
    }

    private static String getCleansedUrlMoniker(String url) {
        String cleansedUrlMoniker = url;
        try {
            URL urlObj = new URL(url);
            cleansedUrlMoniker = urlObj.getHost() + (urlObj.getPort() != -1 ? ":" + urlObj.getPort() : "");
        } catch (Exception e) {
            LOG.error("Error parsing URL \"{}\"", url, e);
        }

        cleansedUrlMoniker = cleansedUrlMoniker.replaceAll("[^a-zA-Z0-9]", "_");
        return cleansedUrlMoniker;
    }

    public void initialize() {
        synchronized (this) {
            this.keyStore = this.createKeyStore();
            this.trustManager = this.createTrustManager(this.keyStore);
        }
    }

    @Override
    public KeyStore getKeyStore() {
        return this.keyStore;
    }

    @Override
    public X509TrustManager getTrustManager() {
        return this.trustManager;
    }

    private static Path getCacertsPath() {
        return Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
    }

    public static List getJvmRootCertificates() {
        // We always load from the cacerts file so that we can pick up new certificates that were added via "keytool"
        // without requiring a restart.
        //
        // In testing Ignition 8.1, it was found that the cacerts file is modified automatically based on what the
        // user may have put in the /certificates/supplemental folder. It was confirmed that this code
        // will therefore pick up any of those supplemental certificates.

        try {
            Path path = getCacertsPath();
            FileInputStream is = new FileInputStream(path.toFile());
            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            String password = "changeit";
            keystore.load(is, password.toCharArray());
            return Collections.list(keystore.aliases()).stream()
                    .map(alias -> {
                        try {
                            return keystore.getCertificate(alias);
                        } catch (KeyStoreException e) {
                            LOG.error("Error retrieving JVM certificate '{}' from '{}'",
                                    alias, path, e);
                            return null;
                        }
                    })
                    .filter(certificate -> certificate instanceof X509Certificate)
                    .map(certificate -> (X509Certificate) certificate)
                    .collect(Collectors.toList());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static List getWindowsCertificates() {
        if (!OperatingSystem.isWindows()) {
            return Collections.emptyList();
        }

        List certificates = new ArrayList<>();
        String[] windowsKeyStoreNames = new String[] {
                "Windows-ROOT",
                "Windows-MY",
                "Windows-ROOT-LOCALMACHINE",
                "Windows-MY-LOCALMACHINE"
        };

        boolean usePowershell = false;
        for (String windowsKeyStoreName : windowsKeyStoreNames) {
            try {
                KeyStore windowsKeyStore = KeyStore.getInstance(windowsKeyStoreName);
                windowsKeyStore.load(null, null);

                Collections.list(windowsKeyStore.aliases()).stream()
                        .map(alias -> {
                            try {
                                return windowsKeyStore.getCertificate(alias);
                            } catch (KeyStoreException e) {
                                LOG.error("Error retrieving Windows certificate '{}' from '{}' key store",
                                        alias, windowsKeyStoreName, e);
                                return null;
                            }
                        })
                        .filter(certificate -> certificate instanceof X509Certificate)
                        .forEach(certificate -> certificates.add((X509Certificate) certificate));
            } catch (KeyStoreException e) {
                if (windowsKeyStoreName.contains("-LOCALMACHINE") && e.getMessage().contains("not found")) {
                    // This happens because https://bugs.openjdk.org/browse/JDK-6782021 may not have been merged into
                    // the version of the JVM we're using. (This happens with Ignition, since Ignition is the thing
                    // dictating what JVM is used.)
                    usePowershell = true;
                } else {
                    LOG.info("Error retrieving Windows certificates from '{}' key store", windowsKeyStoreName, e);
                }
            } catch (IOException e) {
                if (e.getCause() instanceof KeyStoreException &&
                        e.getCause().getMessage().contains("Access is denied")) {
                    // This happens because of https://bugs.openjdk.org/browse/JDK-8313367.
                    usePowershell = true;
                } else {
                    LOG.info("Error retrieving Windows certificates from '{}' key store", windowsKeyStoreName, e);
                }
            } catch (Throwable e) {
                LOG.info("Error retrieving Windows certificates from '{}' key store", windowsKeyStoreName, e);
            }
        }

        if (usePowershell) {
            certificates.addAll(getWindowsLocalMachineCertificatesViaPowershell("root"));
            certificates.addAll(getWindowsLocalMachineCertificatesViaPowershell("my"));
        }

        return certificates;
    }

    private static List getWindowsLocalMachineCertificatesViaPowershell(String certType) {
        // Theoretically we could use KeyStore.getInstance("Windows-ROOT-LOCALMACHINE") and
        // KeyStore.getInstance("Windows-MY-LOCALMACHINE") to accomplish this, which were added as part of
        // https://bugs.openjdk.org/browse/JDK-6782021. Unfortunately, we'll get an Access Denied error due to
        // https://bugs.openjdk.org/browse/JDK-8313367. Plus, none of it was backported to Java 8 anyway, and we
        // would ideally have identical behavior for Ignition v7 which runs on Java 8.
        // So instead of "native" Java functionality we use a Powershell script to dump out all the certificates and
        // read them from disk.

        Path tempFolder;
        List certificates = new ArrayList<>();
        try {
            tempFolder = Files.createTempDirectory("seeq-link-getWindowsLocalMachineCertificates");
        } catch (Throwable e) {
            LOG.error("Error creating temporary folder for Windows certificates", e);
            return Collections.emptyList();
        }

        try {
            String powershellScript = String.format("dir cert:\\localmachine\\%s | Foreach-Object { [system.IO" +
                    ".file]::WriteAllBytes(\".\\$($_.Thumbprint).cer\", ($_.Export('CERT', 'secret')) ) }", certType);
            Path scriptLocation = tempFolder.resolve("getWindowsLocalMachineCertificates.ps1");
            Files.write(scriptLocation, powershellScript.getBytes());
            ProcessSpawn processSpawn = new ProcessSpawn(
                    "powershell.exe", "-ExecutionPolicy", "Bypass", "-File", scriptLocation.toString());
            processSpawn.directory(tempFolder.toFile());
            processSpawn.setCaptureTextEnabled(true);
            processSpawn.start();
            processSpawn.waitFor();
            if (processSpawn.getExitValue() != 0) {
                LOG.warn("Error retrieving local machine Windows certificates for type '{}': Powershell script " +
                                "returned {} exit code.\nOutput:\n{}\nScript contents:\n{}",
                        certType, processSpawn.getExitValue(),
                        processSpawn.getOutputString() + "\n" + processSpawn.getErrorString(), powershellScript);
                return Collections.emptyList();
            }

            Collection cerFiles = FileUtils.listFiles(tempFolder.toFile(), new String[] { "cer" }, false);
            for (File cerFile : cerFiles) {
                try (FileInputStream fis = new FileInputStream(cerFile)) {
                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                    Certificate cert = cf.generateCertificate(fis);
                    if (cert instanceof X509Certificate) {
                        certificates.add((X509Certificate) cert);
                    }
                } catch (Exception e) {
                    LOG.warn("Error reading Windows certificate from '{}'", cerFile, e);
                }
            }
            return certificates;
        } catch (Throwable e) {
            LOG.error("Error retrieving local machine Windows certificates", e);
            return Collections.emptyList();
        } finally {
            if (tempFolder != null) {
                try {
                    FileUtils.deleteDirectory(tempFolder.toFile());
                } catch (Exception e) {
                    LOG.error("Error deleting temporary folder for Windows certificates", e);
                }
            }
        }
    }

    private static void addToKeyStore(KeyStore keyStore, int n, String origin, X509Certificate certificate)
            throws KeyStoreException {
        keyStore.setCertificateEntry(String.format("%03d %s %s", n, origin, getCertName(certificate)), certificate);
    }

    private KeyStore createKeyStore() {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);

            int n = 0;

            // JVM root certificates
            List jvmRootCertificates = getJvmRootCertificates();
            LOG.info("Found {} certificates in '{}'", jvmRootCertificates.size(), getCacertsPath());
            for (X509Certificate certificate : jvmRootCertificates) {
                try {
                    addToKeyStore(keyStore, ++n, "cacerts", certificate);
                } catch (KeyStoreException e) {
                    throw new RuntimeException(String.format("Error adding JVM root certificate '%s' to key store",
                            getCertName(certificate)), e);
                }
            }

            // Windows root certificates
            List windowsRootCertificates = getWindowsCertificates();
            LOG.info("Found {} certificates in Windows trust stores (ROOT, MY, ROOT-LOCALMACHINE, MY-LOCALMACHINE)",
                    windowsRootCertificates.size());
            for (X509Certificate certificate : windowsRootCertificates) {
                try {
                    addToKeyStore(keyStore, ++n, "windows", certificate);
                } catch (KeyStoreException e) {
                    throw new RuntimeException(String.format("Error adding Windows certificate '%s' to key store",
                            getCertName(certificate)), e);
                }
            }

            // User-trusted certificates from the Agent's "global_folder/keys/trusted" folder
            for (String pemCertificate : this.loadTrustedCertificatesFromPaths()) {
                X509Certificate certificate = parsePEM(pemCertificate);
                LOG.info("Using trusted certificate from disk {}", getCertName(certificate));
                addToKeyStore(keyStore, ++n, "seeq-trusted-folder", certificate);
            }

            return keyStore;
        } catch (Exception e) {
            throw new RuntimeException("Error creating key store from certificate configs", e);
        }
    }

    private X509TrustManager createTrustManager(KeyStore keyStore) {
        try {
            // Create a TrustManager that trusts the certificate in the KeyStore
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);

            TrustManager[] trustManagers = tmf.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
            }

            return (X509TrustManager) trustManagers[0];
        } catch (Exception e) {
            throw new RuntimeException("Error creating trust manager from certificate configs", e);
        }
    }

    static List downloadCertificatesFromSite(String url) {
        List certificates = new ArrayList<>();
        try {
            URL urlObj = new URL(url);

            // Create a trust manager that accepts all certificates
            TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            } };

            HostnameVerifier allHostsValid = (hostname, session) -> true;

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, null);

            // Open a connection to the HTTPS site using the private SSL context
            HttpsURLConnection connection = (HttpsURLConnection) urlObj.openConnection();
            connection.setSSLSocketFactory(sslContext.getSocketFactory());
            connection.setHostnameVerifier(allHostsValid);

            // This call causes the connection to be made
            connection.getResponseCode();

            Certificate[] serverCertificates = connection.getServerCertificates();

            for (Certificate certificate : serverCertificates) {
                if (certificate instanceof X509Certificate) {
                    certificates.add((X509Certificate) certificate);
                } else {
                    LOG.warn("Certificate from {} is not an X509Certificate: {}", url,
                            certificate.getClass().getName());
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(
                    String.format("Error downloading untrusted certificates from '%s'", url), e);
        }

        return certificates;
    }

    private List loadTrustedCertificatesFromPaths() {
        List certificates = new ArrayList<>();

        for (Path trustedCertificatesPath : this.trustedCertificatesPaths) {
            if (!Files.exists(trustedCertificatesPath)) {
                LOG.warn("Trusted certificates path \"{}\" does not exist", trustedCertificatesPath);
                continue;
            }

            if (trustedCertificatesPath.toFile().isDirectory()) {
                try (Stream trustedFiles = Files.list(trustedCertificatesPath)) {
                    trustedFiles.forEach(
                            trustedFile -> certificates.addAll(loadAndVerifyTrustedCertificates(trustedFile)));
                } catch (Exception e) {
                    LOG.error("Could not load trusted certificates from \"{}\":", trustedCertificatesPath, e);
                }
            } else {
                certificates.addAll(loadAndVerifyTrustedCertificates(trustedCertificatesPath));
            }
        }

        return certificates;
    }

    /**
     * Verifies the certificate chain present in a file.
     * 

* The certificate PEM file should contain all the certificates necessary to validate the entire chain in the * standard certificate order (most specific/server certificate to least specific), so the certificates can be * validated in order: *

* If the server certificate is issued by an official Certificate Authority (CA): * Server Certificate * CA Intermediate Certificate 1 * CA Intermediate Certificate 2 *

* If the server certificate is self-issued (meaning an internal/non-official Certificate Authority was used): * Server Certificate * CA Intermediate Certificate 1 * CA Certificate *

* If the server certificate is self-signed (meaning no Certificate Authority was used): * Server Certificate * * @param certificates * The certificates to verify * @return true if the certificate is signed by a trusted CA, false if it is self-signed or self-issued */ static boolean verifyCertificateChain(List certificates) { CertificateFactory certificateFactory; TrustManagerFactory trustManagerFactory; try { certificateFactory = CertificateFactory.getInstance("X.509"); trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); } catch (Exception e) { throw new RuntimeException("Error retrieving X.509 CertificateFactory and TrustManagerFactory", e); } LOG.info("Number of certificates found in file: {}", certificates.size()); if (certificates.isEmpty()) { return false; } if (certificates.size() > 1) { for (int i = 0; i < certificates.size() - 1; i++) { String currentPemCert = certificates.get(i); String nextPemCert = certificates.get(i + 1); X509Certificate currentCert; try { currentCert = (X509Certificate) certificateFactory .generateCertificate(new ByteArrayInputStream(currentPemCert.getBytes())); } catch (Exception e) { throw new RuntimeException( String.format("Error generating X.509 certificate from PEM certificate:\n%s", currentPemCert), e); } X509Certificate nextCert; try { nextCert = (X509Certificate) certificateFactory .generateCertificate(new ByteArrayInputStream(nextPemCert.getBytes())); } catch (Exception e) { throw new RuntimeException( String.format("Error generating X.509 certificate from PEM certificate:\n%s", nextPemCert), e); } LOG.info("Verifying #{}: '{}' (valid until {}), issued by '{}' against '{}' (valid until {})...", i, getCertName(currentCert), currentCert.getNotAfter(), currentCert.getIssuerX500Principal(), getCertName(nextCert), nextCert.getNotAfter()); try { currentCert.verify(nextCert.getPublicKey()); } catch (Exception e) { throw new RuntimeException( String.format("Error verifying certificate chain:\n%s\n%s", currentPemCert, nextPemCert), e); } } } // Verify the last cert in the PEM file against the CAs in the default trust store String lastPemCert = certificates.get(certificates.size() - 1); // Last cert in the PEM file X509Certificate lastCert = parsePEM(lastPemCert); LOG.info("Verifying #{}: '{}' (valid until {}, issued by '{}') against CA trust store...", certificates.size() - 1, getCertName(lastCert), lastCert.getNotAfter(), lastCert.getIssuerX500Principal()); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); boolean certIsSignedByOfficialCertAuthority = false; for (TrustManager trustManager : trustManagers) { X509TrustManager x509TrustManager = (X509TrustManager) trustManager; for (X509Certificate caCert : x509TrustManager.getAcceptedIssuers()) { try { lastCert.verify(caCert.getPublicKey()); LOG.info("Successfully verified '{}' against CA '{}'/'{}'", getCertName(lastCert), getCertName(caCert), caCert.getIssuerX500Principal().getName()); certIsSignedByOfficialCertAuthority = true; } catch (Exception e) { // Ignore any verification failure - it was most likely the wrong CA certificate } } } if (certIsSignedByOfficialCertAuthority) { LOG.info("Certificate chain verification complete. Certificate is signed by an official CA."); } else { LOG.info("Certificate chain verification complete. Certificate is NOT signed by an official CA " + "(it's self-issued or self-signed)."); } return certIsSignedByOfficialCertAuthority; } static String getCertName(X509Certificate cert) { return cert.getSubjectX500Principal().getName("RFC2253"); } private static X509Certificate parsePEM(String pem) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); return (X509Certificate) certificateFactory.generateCertificate( new ByteArrayInputStream(pem.getBytes())); } catch (Exception e) { throw new RuntimeException(String.format("Error parsing PEM certificate\n%s", pem), e); } } private static List loadAndVerifyTrustedCertificates(Path trustedFile) { LOG.info("Loading and verifying trusted certificates from \"{}\"...", trustedFile); List certificates = loadCertificatesFromPath(trustedFile); verifyCertificateChain(certificates); return certificates; } static List loadCertificatesFromPath(Path trustedFile) { List certificates = new ArrayList<>(); try { String contents = new String(Files.readAllBytes(trustedFile)); Pattern pattern = Pattern.compile("-+BEGIN\\s+CERTIFICATE-+(.*?)-+END\\s+CERTIFICATE-+", Pattern.DOTALL); Matcher matcher = pattern.matcher(contents); while (matcher.find()) { certificates.add(matcher.group(0)); } } catch (Exception e) { LOG.error("Could not load trusted certificates from \"{}\":", trustedFile, e); } return certificates; } static String convertCertificateToPEM(X509Certificate certificate) throws CertificateEncodingException { byte[] certBytes = certificate.getEncoded(); String encoded = Base64.getEncoder().encodeToString(certBytes); StringBuilder pemCertificate = new StringBuilder(); pemCertificate.append("-----BEGIN CERTIFICATE-----\n"); // Break the Base64 encoded string into lines of 64 characters for (int i = 0; i < encoded.length(); i += 64) { pemCertificate.append(encoded, i, Math.min(i + 64, encoded.length())).append('\n'); } pemCertificate.append("-----END CERTIFICATE-----\n"); return pemCertificate.toString(); } /** * Outputs the default trusted CA certificates to the log. This is only meant for debugging CA certificate issues. */ @SuppressWarnings("unused") public static void logDefaultTruststore() { TrustManagerFactory tmf; try { tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init((KeyStore) null); } catch (Exception e) { LOG.error("Error getting default TrustManagerFactory", e); return; } for (TrustManager m : tmf.getTrustManagers()) { X509TrustManager mgr = (X509TrustManager) m; for (X509Certificate c : mgr.getAcceptedIssuers()) { LOG.debug("Trusted CA Certificate: {}", c.getSubjectX500Principal()); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy