001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.ByteArrayInputStream;
007import java.io.File;
008import java.io.IOException;
009import java.io.InputStream;
010import java.nio.file.Files;
011import java.nio.file.Path;
012import java.nio.file.Paths;
013import java.security.GeneralSecurityException;
014import java.security.InvalidAlgorithmParameterException;
015import java.security.KeyStore;
016import java.security.KeyStoreException;
017import java.security.MessageDigest;
018import java.security.cert.CertificateFactory;
019import java.security.cert.PKIXParameters;
020import java.security.cert.TrustAnchor;
021import java.security.cert.X509Certificate;
022import java.util.Objects;
023
024import javax.net.ssl.SSLContext;
025import javax.net.ssl.TrustManagerFactory;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * Class to add missing root certificates to the list of trusted certificates
032 * for TLS connections.
033 *
034 * The added certificates are deemed trustworthy by the main web browsers and
035 * operating systems, but not included in some distributions of Java.
036 *
037 * The certificates are added in-memory at each start, nothing is written to disk.
038 * @since 9995
039 */
040public final class CertificateAmendment {
041
042    private static final String[] CERT_AMEND = {
043        "resource://data/security/DST_Root_CA_X3.pem",
044        "resource://data/security/StartCom_Certification_Authority.pem"
045    };
046
047    private static final String[] SHA_HASHES = {
048        "0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739",
049        "c766a9bef2d4071c863a31aa4920e813b2d198608cb7b7cfe21143b836df09ea"
050    };
051
052    private CertificateAmendment() {
053        // Hide default constructor for utility classes
054    }
055
056    /**
057     * Add missing root certificates to the list of trusted certificates for TLS connections.
058     * @throws IOException if an I/O error occurs
059     * @throws GeneralSecurityException if a security error occurs
060     */
061    public static void addMissingCertificates() throws IOException, GeneralSecurityException {
062        if (!Main.pref.getBoolean("tls.add-missing-certificates", true))
063            return;
064        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
065        Path cacertsPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
066        try (InputStream is = Files.newInputStream(cacertsPath)) {
067            keyStore.load(is, "changeit".toCharArray());
068        }
069
070        CertificateFactory cf = CertificateFactory.getInstance("X.509");
071        boolean certificateAdded = false;
072        for (int i = 0; i < CERT_AMEND.length; i++) {
073            try (CachedFile certCF = new CachedFile(CERT_AMEND[i])) {
074                byte[] certBytes = certCF.getByteContent();
075                ByteArrayInputStream certIS = new ByteArrayInputStream(certBytes);
076                X509Certificate cert = (X509Certificate) cf.generateCertificate(certIS);
077                MessageDigest md = MessageDigest.getInstance("SHA-256");
078                String sha1 = Utils.toHexString(md.digest(cert.getEncoded()));
079                if (!SHA_HASHES[i].equals(sha1)) {
080                    throw new IllegalStateException(
081                            tr("Error adding certificate {0} - certificate fingerprint mismatch. Expected {1}, was {2}",
082                            CERT_AMEND[i],
083                            SHA_HASHES[i],
084                            sha1
085                            ));
086                }
087                if (certificateIsMissing(keyStore, cert)) {
088                    if (Main.isDebugEnabled()) {
089                        Main.debug(tr("Adding certificate for TLS connections: {0}", cert.getSubjectX500Principal().getName()));
090                    }
091                    String alias = "josm:" + new File(CERT_AMEND[i]).getName();
092                    keyStore.setCertificateEntry(alias, cert);
093                    certificateAdded = true;
094                }
095            }
096        }
097
098        if (certificateAdded) {
099            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
100            tmf.init(keyStore);
101            SSLContext sslContext = SSLContext.getInstance("TLS");
102            sslContext.init(null, tmf.getTrustManagers(), null);
103            SSLContext.setDefault(sslContext);
104        }
105    }
106
107    /**
108     * Check if the certificate is missing and needs to be added to the keystore.
109     * @param keyStore the keystore
110     * @param crt the certificate
111     * @return true, if the certificate is not contained in the keystore
112     * @throws InvalidAlgorithmParameterException if the keystore does not contain at least one trusted certificate entry
113     * @throws KeyStoreException if the keystore has not been initialized
114     */
115    private static boolean certificateIsMissing(KeyStore keyStore, X509Certificate crt)
116            throws KeyStoreException, InvalidAlgorithmParameterException {
117        PKIXParameters params = new PKIXParameters(keyStore);
118        String id = crt.getSubjectX500Principal().getName();
119        for (TrustAnchor ta : params.getTrustAnchors()) {
120            X509Certificate cert = ta.getTrustedCert();
121            if (Objects.equals(id, cert.getSubjectX500Principal().getName()))
122                return false;
123        }
124        return true;
125    }
126}