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.IOException; 008import java.io.InputStream; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.nio.file.Paths; 012import java.security.GeneralSecurityException; 013import java.security.InvalidAlgorithmParameterException; 014import java.security.KeyStore; 015import java.security.KeyStoreException; 016import java.security.MessageDigest; 017import java.security.NoSuchAlgorithmException; 018import java.security.cert.CertificateEncodingException; 019import java.security.cert.CertificateException; 020import java.security.cert.CertificateFactory; 021import java.security.cert.PKIXParameters; 022import java.security.cert.TrustAnchor; 023import java.security.cert.X509Certificate; 024import java.util.Objects; 025 026import javax.net.ssl.SSLContext; 027import javax.net.ssl.TrustManagerFactory; 028 029import org.openstreetmap.josm.Main; 030import org.openstreetmap.josm.spi.preferences.Config; 031import org.openstreetmap.josm.tools.Logging; 032import org.openstreetmap.josm.tools.Utils; 033 034/** 035 * Class to add missing root certificates to the list of trusted certificates 036 * for TLS connections. 037 * 038 * The added certificates are deemed trustworthy by the main web browsers and 039 * operating systems, but not included in some distributions of Java. 040 * 041 * The certificates are added in-memory at each start, nothing is written to disk. 042 * @since 9995 043 */ 044public final class CertificateAmendment { 045 046 /** 047 * A certificate amendment. 048 * @since 11943 049 */ 050 public static class CertAmend { 051 private final String filename; 052 private final String sha256; 053 054 protected CertAmend(String filename, String sha256) { 055 this.filename = Objects.requireNonNull(filename); 056 this.sha256 = Objects.requireNonNull(sha256); 057 } 058 059 /** 060 * Returns the certificate filename. 061 * @return filename for both JOSM embedded certificate and Unix platform certificate 062 * @since 12241 063 */ 064 public final String getFilename() { 065 return filename; 066 } 067 068 /** 069 * Returns the SHA-256 hash. 070 * @return the SHA-256 hash, in hexadecimal 071 */ 072 public final String getSha256() { 073 return sha256; 074 } 075 } 076 077 /** 078 * An embedded certificate amendment. 079 * @since 13450 080 */ 081 public static class EmbeddedCertAmend extends CertAmend { 082 private final String url; 083 084 EmbeddedCertAmend(String url, String filename, String sha256) { 085 super(filename, sha256); 086 this.url = Objects.requireNonNull(url); 087 } 088 089 /** 090 * Returns the embedded URL in JOSM jar. 091 * @return path for JOSM embedded certificate 092 */ 093 public final String getUrl() { 094 return url; 095 } 096 097 @Override 098 public String toString() { 099 return url; 100 } 101 } 102 103 /** 104 * A certificate amendment relying on native platform certificate store. 105 * @since 13450 106 */ 107 public static class NativeCertAmend extends CertAmend { 108 private final String winAlias; 109 private final String macAlias; 110 private final String httpsWebSite; 111 112 NativeCertAmend(String winAlias, String macAlias, String filename, String sha256, String httpsWebSite) { 113 super(filename, sha256); 114 this.winAlias = Objects.requireNonNull(winAlias); 115 this.macAlias = Objects.requireNonNull(macAlias); 116 this.httpsWebSite = Objects.requireNonNull(httpsWebSite); 117 } 118 119 /** 120 * Returns the Windows alias in System Root Certificates keystore. 121 * @return the Windows alias in System Root Certificates keystore 122 */ 123 public final String getWinAlias() { 124 return winAlias; 125 } 126 127 /** 128 * Returns the macOS alias in System Root Certificates keychain. 129 * @return the macOS alias in System Root Certificates keychain 130 */ 131 public final String getMacAlias() { 132 return macAlias; 133 } 134 135 /** 136 * Returns the https website we need to call to notify Windows we need its root certificate. 137 * @return the https website signed with this root CA 138 * @since 13451 139 */ 140 public String getWebSite() { 141 return httpsWebSite; 142 } 143 144 @Override 145 public String toString() { 146 String result = winAlias; 147 if (!winAlias.equals(macAlias)) { 148 result += " / " + macAlias; 149 } 150 return result; 151 } 152 } 153 154 /** 155 * Certificates embedded in JOSM 156 */ 157 private static final EmbeddedCertAmend[] CERT_AMEND = { 158 }; 159 160 /** 161 * Certificates looked into platform native keystore and not embedded in JOSM. 162 * Identifiers must match Windows/macOS keystore aliases and Unix filenames for efficient search. 163 */ 164 private static final NativeCertAmend[] PLATFORM_CERT_AMEND = { 165 // Let's Encrypt - should be included in JDK, but problems with Ubuntu 18.04, see #15851 166 new NativeCertAmend("DST Root CA X3", "DST Root CA X3", 167 "DST_Root_CA_X3.pem", 168 "0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739", 169 "https://acme-v02.api.letsencrypt.org"), 170 // Government of Netherlands 171 new NativeCertAmend("Staat der Nederlanden Root CA - G2", "Staat der Nederlanden Root CA - G2", 172 "Staat_der_Nederlanden_Root_CA_-_G2.crt", 173 "668c83947da63b724bece1743c31a0e6aed0db8ec5b31be377bb784f91b6716f", 174 "https://roottest-g2.pkioverheid.nl"), 175 // Government of Netherlands 176 new NativeCertAmend("Government of Netherlands G3", "Staat der Nederlanden Root CA - G3", 177 "Staat_der_Nederlanden_Root_CA_-_G3.crt", 178 "3c4fb0b95ab8b30032f432b86f535fe172c185d0fd39865837cf36187fa6f428", 179 "https://roottest-g3.pkioverheid.nl"), 180 // Trusted and used by French Government - https://www.certigna.fr/autorites/index.xhtml?ac=Racine#lracine 181 new NativeCertAmend("Certigna", "Certigna", "Certigna.crt", 182 "e3b6a2db2ed7ce48842f7ac53241c7b71d54144bfb40c11f3f1d0b42f5eea12d", 183 "https://www.certigna.fr"), 184 }; 185 186 private CertificateAmendment() { 187 // Hide default constructor for utility classes 188 } 189 190 /** 191 * Add missing root certificates to the list of trusted certificates for TLS connections. 192 * @throws IOException if an I/O error occurs 193 * @throws GeneralSecurityException if a security error occurs 194 */ 195 public static void addMissingCertificates() throws IOException, GeneralSecurityException { 196 if (!Config.getPref().getBoolean("tls.add-missing-certificates", true)) 197 return; 198 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 199 Path cacertsPath = Paths.get(Utils.getSystemProperty("java.home"), "lib", "security", "cacerts"); 200 try (InputStream is = Files.newInputStream(cacertsPath)) { 201 keyStore.load(is, "changeit".toCharArray()); 202 } catch (SecurityException e) { 203 Logging.log(Logging.LEVEL_ERROR, "Unable to load keystore", e); 204 return; 205 } 206 207 MessageDigest md = MessageDigest.getInstance("SHA-256"); 208 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 209 boolean certificateAdded = false; 210 // Add embedded certificates. Exit in case of error 211 for (EmbeddedCertAmend certAmend : CERT_AMEND) { 212 try (CachedFile certCF = new CachedFile(certAmend.url)) { 213 X509Certificate cert = (X509Certificate) cf.generateCertificate( 214 new ByteArrayInputStream(certCF.getByteContent())); 215 if (checkAndAddCertificate(md, cert, certAmend, keyStore)) { 216 certificateAdded = true; 217 } 218 } 219 } 220 221 try { 222 // Try to add platform certificates. Do not exit in case of error (embedded certificates may be OK) 223 for (NativeCertAmend certAmend : PLATFORM_CERT_AMEND) { 224 X509Certificate cert = Main.platform.getX509Certificate(certAmend); 225 if (checkAndAddCertificate(md, cert, certAmend, keyStore)) { 226 certificateAdded = true; 227 } 228 } 229 } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | IllegalStateException e) { 230 Logging.error(e); 231 } 232 233 if (certificateAdded) { 234 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 235 tmf.init(keyStore); 236 SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); 237 sslContext.init(null, tmf.getTrustManagers(), null); 238 SSLContext.setDefault(sslContext); 239 } 240 } 241 242 private static boolean checkAndAddCertificate(MessageDigest md, X509Certificate cert, CertAmend certAmend, KeyStore keyStore) 243 throws CertificateEncodingException, KeyStoreException, InvalidAlgorithmParameterException { 244 if (cert != null) { 245 String sha256 = Utils.toHexString(md.digest(cert.getEncoded())); 246 if (!certAmend.sha256.equals(sha256)) { 247 throw new IllegalStateException( 248 tr("Error adding certificate {0} - certificate fingerprint mismatch. Expected {1}, was {2}", 249 certAmend, certAmend.sha256, sha256)); 250 } 251 if (certificateIsMissing(keyStore, cert)) { 252 if (Logging.isDebugEnabled()) { 253 Logging.debug(tr("Adding certificate for TLS connections: {0}", cert.getSubjectX500Principal().getName())); 254 } 255 String alias = "josm:" + certAmend.filename; 256 keyStore.setCertificateEntry(alias, cert); 257 return true; 258 } 259 } 260 return false; 261 } 262 263 /** 264 * Check if the certificate is missing and needs to be added to the keystore. 265 * @param keyStore the keystore 266 * @param crt the certificate 267 * @return true, if the certificate is not contained in the keystore 268 * @throws InvalidAlgorithmParameterException if the keystore does not contain at least one trusted certificate entry 269 * @throws KeyStoreException if the keystore has not been initialized 270 */ 271 private static boolean certificateIsMissing(KeyStore keyStore, X509Certificate crt) 272 throws KeyStoreException, InvalidAlgorithmParameterException { 273 PKIXParameters params = new PKIXParameters(keyStore); 274 String id = crt.getSubjectX500Principal().getName(); 275 for (TrustAnchor ta : params.getTrustAnchors()) { 276 X509Certificate cert = ta.getTrustedCert(); 277 if (Objects.equals(id, cert.getSubjectX500Principal().getName())) 278 return false; 279 } 280 return true; 281 } 282}