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 * To find correct values, see https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport 164 */ 165 private static final NativeCertAmend[] PLATFORM_CERT_AMEND = { 166 // Let's Encrypt - should be included in JDK, but problems with Ubuntu 18.04, see #15851 167 new NativeCertAmend("DST Root CA X3", "DST Root CA X3", 168 "DST_Root_CA_X3.pem", 169 "0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739", 170 "https://acme-v02.api.letsencrypt.org"), 171 // Government of Netherlands 172 new NativeCertAmend("Staat der Nederlanden Root CA - G2", "Staat der Nederlanden Root CA - G2", 173 "Staat_der_Nederlanden_Root_CA_-_G2.crt", 174 "668c83947da63b724bece1743c31a0e6aed0db8ec5b31be377bb784f91b6716f", 175 "https://roottest-g2.pkioverheid.nl"), 176 // Government of Netherlands 177 new NativeCertAmend("Government of Netherlands G3", "Staat der Nederlanden Root CA - G3", 178 "Staat_der_Nederlanden_Root_CA_-_G3.crt", 179 "3c4fb0b95ab8b30032f432b86f535fe172c185d0fd39865837cf36187fa6f428", 180 "https://roottest-g3.pkioverheid.nl"), 181 // Trusted and used by French Government - https://www.certigna.fr/autorites/index.xhtml?ac=Racine#lracine 182 new NativeCertAmend("Certigna", "Certigna", "Certigna.crt", 183 "e3b6a2db2ed7ce48842f7ac53241c7b71d54144bfb40c11f3f1d0b42f5eea12d", 184 "https://www.certigna.fr"), 185 // Trusted and used by Slovakian Government - https://eidas.disig.sk/en/cacert/ 186 new NativeCertAmend("CA Disig Root R2", "CA Disig Root R2", "CA_Disig_Root_R2.pem", 187 "e23d4a036d7b70e9f595b1422079d2b91edfbb1fb651a0633eaa8a9dc5f80703", 188 "https://eidas.disig.sk"), 189 }; 190 191 private CertificateAmendment() { 192 // Hide default constructor for utility classes 193 } 194 195 /** 196 * Add missing root certificates to the list of trusted certificates for TLS connections. 197 * @throws IOException if an I/O error occurs 198 * @throws GeneralSecurityException if a security error occurs 199 */ 200 public static void addMissingCertificates() throws IOException, GeneralSecurityException { 201 if (!Config.getPref().getBoolean("tls.add-missing-certificates", true)) 202 return; 203 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 204 Path cacertsPath = Paths.get(Utils.getSystemProperty("java.home"), "lib", "security", "cacerts"); 205 try (InputStream is = Files.newInputStream(cacertsPath)) { 206 keyStore.load(is, "changeit".toCharArray()); 207 } catch (SecurityException e) { 208 Logging.log(Logging.LEVEL_ERROR, "Unable to load keystore", e); 209 return; 210 } 211 212 MessageDigest md = MessageDigest.getInstance("SHA-256"); 213 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 214 boolean certificateAdded = false; 215 // Add embedded certificates. Exit in case of error 216 for (EmbeddedCertAmend certAmend : CERT_AMEND) { 217 try (CachedFile certCF = new CachedFile(certAmend.url)) { 218 X509Certificate cert = (X509Certificate) cf.generateCertificate( 219 new ByteArrayInputStream(certCF.getByteContent())); 220 if (checkAndAddCertificate(md, cert, certAmend, keyStore)) { 221 certificateAdded = true; 222 } 223 } 224 } 225 226 try { 227 // Try to add platform certificates. Do not exit in case of error (embedded certificates may be OK) 228 for (NativeCertAmend certAmend : PLATFORM_CERT_AMEND) { 229 X509Certificate cert = Main.platform.getX509Certificate(certAmend); 230 if (checkAndAddCertificate(md, cert, certAmend, keyStore)) { 231 certificateAdded = true; 232 } 233 } 234 } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | IllegalStateException e) { 235 Logging.error(e); 236 } 237 238 if (certificateAdded) { 239 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 240 tmf.init(keyStore); 241 SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); 242 sslContext.init(null, tmf.getTrustManagers(), null); 243 SSLContext.setDefault(sslContext); 244 } 245 } 246 247 private static boolean checkAndAddCertificate(MessageDigest md, X509Certificate cert, CertAmend certAmend, KeyStore keyStore) 248 throws CertificateEncodingException, KeyStoreException, InvalidAlgorithmParameterException { 249 if (cert != null) { 250 String sha256 = Utils.toHexString(md.digest(cert.getEncoded())); 251 if (!certAmend.sha256.equals(sha256)) { 252 throw new IllegalStateException( 253 tr("Error adding certificate {0} - certificate fingerprint mismatch. Expected {1}, was {2}", 254 certAmend, certAmend.sha256, sha256)); 255 } 256 if (certificateIsMissing(keyStore, cert)) { 257 if (Logging.isDebugEnabled()) { 258 Logging.debug(tr("Adding certificate for TLS connections: {0}", cert.getSubjectX500Principal().getName())); 259 } 260 String alias = "josm:" + certAmend.filename; 261 keyStore.setCertificateEntry(alias, cert); 262 return true; 263 } 264 } 265 return false; 266 } 267 268 /** 269 * Check if the certificate is missing and needs to be added to the keystore. 270 * @param keyStore the keystore 271 * @param crt the certificate 272 * @return true, if the certificate is not contained in the keystore 273 * @throws InvalidAlgorithmParameterException if the keystore does not contain at least one trusted certificate entry 274 * @throws KeyStoreException if the keystore has not been initialized 275 */ 276 private static boolean certificateIsMissing(KeyStore keyStore, X509Certificate crt) 277 throws KeyStoreException, InvalidAlgorithmParameterException { 278 PKIXParameters params = new PKIXParameters(keyStore); 279 String id = crt.getSubjectX500Principal().getName(); 280 for (TrustAnchor ta : params.getTrustAnchors()) { 281 X509Certificate cert = ta.getTrustedCert(); 282 if (Objects.equals(id, cert.getSubjectX500Principal().getName())) 283 return false; 284 } 285 return true; 286 } 287}