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