Ignore:
Timestamp:
2014-07-26T03:50:31+02:00 (10 years ago)
Author:
Don-vip
Message:

see #10230, see #10033 - big rework of HTTPS support for Remote Control:

  • HTTPS disabled by default, must be enabled in remote control preferences
  • Old certificate and private key removed from jar and Windows keystore if found, even if remote control disabled
  • New certificate generated at runtime with critical X509 extensions BasicConstraints (non-CA certificate), ExtendedKeyUsage (usage restriction for TLS server sessions)
  • New passwords generated at runtime (but stored in clear in user preferences)
  • Private key is no longer stored in Windows keystore (only certificate)
Location:
trunk/src/org/openstreetmap/josm/io/remotecontrol
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControl.java

    r6941 r7335  
    22package org.openstreetmap.josm.io.remotecontrol;
    33
     4import org.openstreetmap.josm.Main;
    45import org.openstreetmap.josm.data.preferences.BooleanProperty;
    56import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
     
    1112 * and increment the major version and set minor to 0 on incompatible changes.
    1213 */
    13 public class RemoteControl
    14 {
     14public class RemoteControl {
     15
    1516    /**
    1617     * If the remote control feature is enabled or disabled. If disabled,
     
    1819     */
    1920    public static final BooleanProperty PROP_REMOTECONTROL_ENABLED = new BooleanProperty("remotecontrol.enabled", false);
     21
     22    /**
     23     * If the remote control feature is enabled or disabled for HTTPS. If disabled,
     24     * only HTTP access will be available.
     25     * @since 7335
     26     */
     27    public static final BooleanProperty PROP_REMOTECONTROL_HTTPS_ENABLED = new BooleanProperty("remotecontrol.https.enabled", false);
    2028
    2129    /**
     
    5462        RequestProcessor.addRequestHandlerClass(command, handlerClass);
    5563    }
     64
     65    /**
     66     * Returns the remote control directory.
     67     * @return The remote control directory
     68     * @since 7335
     69     */
     70    public static String getRemoteControlDir() {
     71        return Main.pref.getPreferencesDir() + "remotecontrol/";
     72    }
    5673}
  • trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpsServer.java

    r7206 r7335  
    77import java.io.IOException;
    88import java.io.InputStream;
     9import java.math.BigInteger;
    910import java.net.BindException;
    1011import java.net.InetAddress;
     
    1213import java.net.Socket;
    1314import java.net.SocketException;
    14 import java.security.Key;
    15 import java.security.KeyManagementException;
     15import java.nio.file.Files;
     16import java.nio.file.Path;
     17import java.nio.file.Paths;
     18import java.nio.file.StandardOpenOption;
     19import java.security.GeneralSecurityException;
     20import java.security.KeyPair;
     21import java.security.KeyPairGenerator;
    1622import java.security.KeyStore;
    17 import java.security.KeyStoreException;
    1823import java.security.NoSuchAlgorithmException;
    1924import java.security.PrivateKey;
    20 import java.security.UnrecoverableEntryException;
     25import java.security.SecureRandom;
    2126import java.security.cert.Certificate;
    22 import java.security.cert.CertificateException;
     27import java.security.cert.X509Certificate;
    2328import java.util.Arrays;
     29import java.util.Date;
    2430import java.util.Enumeration;
     31import java.util.Vector;
    2532
    2633import javax.net.ssl.KeyManagerFactory;
     
    3239
    3340import org.openstreetmap.josm.Main;
     41import org.openstreetmap.josm.data.preferences.StringProperty;
     42
     43import sun.security.util.ObjectIdentifier;
     44import sun.security.x509.AlgorithmId;
     45import sun.security.x509.BasicConstraintsExtension;
     46import sun.security.x509.CertificateAlgorithmId;
     47import sun.security.x509.CertificateExtensions;
     48import sun.security.x509.CertificateIssuerName;
     49import sun.security.x509.CertificateSerialNumber;
     50import sun.security.x509.CertificateSubjectName;
     51import sun.security.x509.CertificateValidity;
     52import sun.security.x509.CertificateVersion;
     53import sun.security.x509.CertificateX509Key;
     54import sun.security.x509.DNSName;
     55import sun.security.x509.ExtendedKeyUsageExtension;
     56import sun.security.x509.GeneralName;
     57import sun.security.x509.GeneralNameInterface;
     58import sun.security.x509.GeneralNames;
     59import sun.security.x509.IPAddressName;
     60import sun.security.x509.OIDName;
     61import sun.security.x509.SubjectAlternativeNameExtension;
     62import sun.security.x509.URIName;
     63import sun.security.x509.X500Name;
     64import sun.security.x509.X509CertImpl;
     65import sun.security.x509.X509CertInfo;
    3466
    3567/**
     
    4779    private SSLContext sslContext;
    4880
    49     private static final String KEYSTORE_PATH = "/data/josm.keystore";
    50     private static final String KEYSTORE_PASSWORD = "josm_ssl";
     81    private static final String KEYSTORE_FILENAME = "josm.keystore";
     82
     83    /**
     84     * Preference for keystore password (automatically generated by JOSM).
     85     * @since 7335
     86     */
     87    public StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
     88
     89    /**
     90     * Preference for certificate password (automatically generated by JOSM).
     91     * @since 7335
     92     */
     93    public StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
     94
     95    /**
     96     * Creates a GeneralName object from known types.
     97     * @param t one of 4 known types
     98     * @param v value
     99     * @return which one
     100     * @throws IOException
     101     */
     102    private static GeneralName createGeneralName(String t, String v) throws IOException {
     103        GeneralNameInterface gn;
     104        switch (t.toLowerCase()) {
     105            case "uri": gn = new URIName(v); break;
     106            case "dns": gn = new DNSName(v); break;
     107            case "ip": gn = new IPAddressName(v); break;
     108            default: gn = new OIDName(v);
     109        }
     110        return new GeneralName(gn);
     111    }
     112
     113    /**
     114     * Create a self-signed X.509 Certificate.
     115     * @param dn the X.509 Distinguished Name, eg "CN=localhost, OU=JOSM, O=OpenStreetMap"
     116     * @param pair the KeyPair
     117     * @param days how many days from now the Certificate is valid for
     118     * @param algorithm the signing algorithm, eg "SHA256withRSA"
     119     * @param san SubjectAlternativeName extension (optional)
     120     */
     121    private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) throws GeneralSecurityException, IOException {
     122        PrivateKey privkey = pair.getPrivate();
     123        X509CertInfo info = new X509CertInfo();
     124        Date from = new Date();
     125        Date to = new Date(from.getTime() + days * 86400000l);
     126        CertificateValidity interval = new CertificateValidity(from, to);
     127        BigInteger sn = new BigInteger(64, new SecureRandom());
     128        X500Name owner = new X500Name(dn);
     129
     130        info.set(X509CertInfo.VALIDITY, interval);
     131        info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
     132        info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
     133        info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
     134        info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
     135        info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
     136        AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
     137        info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
     138
     139        CertificateExtensions ext = new CertificateExtensions();
     140        // Critical: Not CA, max path len 0
     141        ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, false, 0));
     142        // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
     143        ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(true,
     144                new Vector<ObjectIdentifier>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
     145
     146        if (san != null) {
     147            int colonpos;
     148            String[] ps = san.split(",");
     149            GeneralNames gnames = new GeneralNames();
     150            for(String item: ps) {
     151                colonpos = item.indexOf(':');
     152                if (colonpos < 0) {
     153                    throw new IllegalArgumentException("Illegal item " + item + " in " + san);
     154                }
     155                String t = item.substring(0, colonpos);
     156                String v = item.substring(colonpos+1);
     157                gnames.add(createGeneralName(t, v));
     158            }
     159            // Non critical
     160            ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(false, gnames));
     161        }
     162
     163        info.set(X509CertInfo.EXTENSIONS, ext);
     164
     165        // Sign the cert to identify the algorithm that's used.
     166        X509CertImpl cert = new X509CertImpl(info);
     167        cert.sign(privkey, algorithm);
     168
     169        // Update the algorithm, and resign.
     170        algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG);
     171        info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
     172        cert = new X509CertImpl(info);
     173        cert.sign(privkey, algorithm);
     174        return cert;
     175    }
    51176
    52177    private void initialize() {
    53178        if (!initOK) {
    54179            try {
    55                 // Create new keystore
    56                 KeyStore ks = KeyStore.getInstance("JKS");
    57                 char[] password = KEYSTORE_PASSWORD.toCharArray();
    58 
    59                 // Load keystore generated with Java 7 keytool as follows:
    60                 // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
    61                 // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
    62                 try (InputStream in = RemoteControlHttpsServer.class.getResourceAsStream(KEYSTORE_PATH)) {
    63                     if (in == null) {
    64                         Main.error(tr("Unable to find JOSM keystore at {0}. Remote control will not be available on HTTPS.", KEYSTORE_PATH));
    65                     } else {
    66                         ks.load(in, password);
    67 
    68                         if (Main.isDebugEnabled()) {
    69                             for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
    70                                 Main.debug("Alias in keystore: "+aliases.nextElement());
    71                             }
     180                char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
     181                char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
     182
     183                Path dir = Paths.get(RemoteControl.getRemoteControlDir());
     184                Path path = dir.resolve(KEYSTORE_FILENAME);
     185                Files.createDirectories(dir);
     186
     187                if (!Files.exists(path)) {
     188                    Main.debug("No keystore found, creating a new one");
     189
     190                    // Create new keystore like previous one generated with JDK keytool as follows:
     191                    // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
     192                    // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
     193
     194                    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
     195                    generator.initialize(2048);
     196                    KeyPair pair = generator.generateKeyPair();
     197
     198                    X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA", "ip:127.0.0.1");
     199
     200                    KeyStore ks = KeyStore.getInstance("JKS");
     201                    ks.load(null, null);
     202
     203                    // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
     204                    SecureRandom random = new SecureRandom();
     205                    KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
     206                    KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
     207
     208                    storePassword = KEYSTORE_PASSWORD.get().toCharArray();
     209                    entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
     210
     211                    ks.setKeyEntry("josm_localhost", pair.getPrivate(), entryPassword, new Certificate[]{cert});
     212                    ks.store(Files.newOutputStream(path, StandardOpenOption.CREATE), storePassword);
     213                }
     214
     215                try (InputStream in = Files.newInputStream(path)) {
     216                    // Load keystore
     217                    KeyStore ks = KeyStore.getInstance("JKS");
     218                    ks.load(in, storePassword);
     219
     220                    if (Main.isDebugEnabled()) {
     221                        for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
     222                            Main.debug("Alias in keystore: "+aliases.nextElement());
    72223                        }
    73 
    74                         KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    75                         kmf.init(ks, password);
    76 
    77                         TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    78                         tmf.init(ks);
    79 
    80                         sslContext = SSLContext.getInstance("TLS");
    81                         sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
    82 
    83                         if (Main.isDebugEnabled()) {
    84                             Main.debug("SSL Context protocol: " + sslContext.getProtocol());
    85                             Main.debug("SSL Context provider: " + sslContext.getProvider());
    86                         }
    87 
    88                         Enumeration<String> aliases = ks.aliases();
    89                         if (aliases.hasMoreElements()) {
    90                             String aliasKey = aliases.nextElement();
    91                             Key key = ks.getKey(aliasKey, password);
    92                             Certificate[] chain = ks.getCertificateChain(aliasKey);
    93                             Main.platform.setupHttpsCertificate(new KeyStore.PrivateKeyEntry((PrivateKey) key, chain));
    94                         }
    95 
    96                         initOK = true;
    97224                    }
    98                 }
    99             } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException |
    100                     IOException | KeyManagementException | UnrecoverableEntryException e) {
     225
     226                    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
     227                    kmf.init(ks, entryPassword);
     228
     229                    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
     230                    tmf.init(ks);
     231
     232                    sslContext = SSLContext.getInstance("TLS");
     233                    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
     234
     235                    if (Main.isTraceEnabled()) {
     236                        Main.trace("SSL Context protocol: " + sslContext.getProtocol());
     237                        Main.trace("SSL Context provider: " + sslContext.getProvider());
     238                    }
     239
     240                    Enumeration<String> aliases = ks.aliases();
     241                    if (aliases.hasMoreElements()) {
     242                        Main.platform.setupHttpsCertificate(new KeyStore.TrustedCertificateEntry(ks.getCertificate(aliases.nextElement())));
     243                    }
     244
     245                    initOK = true;
     246                }
     247            } catch (IOException | GeneralSecurityException e) {
    101248                Main.error(e);
    102249            }
     
    112259            stopRemoteControlHttpsServer();
    113260
    114             instance = new RemoteControlHttpsServer(port);
    115             if (instance.initOK) {
    116                 instance.start();
     261            if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
     262                instance = new RemoteControlHttpsServer(port);
     263                if (instance.initOK) {
     264                    instance.start();
     265                }
    117266            }
    118267        } catch (BindException ex) {
     
    152301        initialize();
    153302
     303        if (!initOK) {
     304            Main.error(tr("Unable to initialize Remote Control HTTPS Server"));
     305            return;
     306        }
     307
    154308        // Create SSL Server factory
    155309        SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
    156         if (Main.isDebugEnabled()) {
    157             Main.debug("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
     310        if (Main.isTraceEnabled()) {
     311            Main.trace("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
    158312        }
    159313
     
    165319            InetAddress.getByName(Main.pref.get("remote.control.host", "localhost")));
    166320
    167         if (Main.isDebugEnabled() && server instanceof SSLServerSocket) {
     321        if (Main.isTraceEnabled() && server instanceof SSLServerSocket) {
    168322            SSLServerSocket sslServer = (SSLServerSocket) server;
    169             Main.debug("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
    170             Main.debug("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
    171             Main.debug("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
    172             Main.debug("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
    173             Main.debug("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
    174             Main.debug("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
     323            Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
     324            Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
     325            Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
     326            Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
     327            Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
     328            Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
    175329        }
    176330    }
     
    187341                @SuppressWarnings("resource")
    188342                Socket request = server.accept();
    189                 if (Main.isDebugEnabled() && request instanceof SSLSocket) {
     343                if (Main.isTraceEnabled() && request instanceof SSLSocket) {
    190344                    SSLSocket sslSocket = (SSLSocket) request;
    191                     Main.debug("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
    192                     Main.debug("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
    193                     Main.debug("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
    194                     Main.debug("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
    195                     Main.debug("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
    196                     Main.debug("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
    197                     Main.debug("SSL socket - Session: "+sslSocket.getSession());
     345                    Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
     346                    Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
     347                    Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
     348                    Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
     349                    Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
     350                    Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
     351                    Main.trace("SSL socket - Session: "+sslSocket.getSession());
    198352                }
    199353                RequestProcessor.processRequest(request);
     
    214368     */
    215369    public void stopServer() throws IOException {
    216         server.close();
    217         Main.info(marktr("RemoteControl::Server (https) stopped."));
     370        if (server != null) {
     371            server.close();
     372            Main.info(marktr("RemoteControl::Server (https) stopped."));
     373        }
    218374    }
    219375}
Note: See TracChangeset for help on using the changeset viewer.