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