source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/RemoteControlHttpsServer.java@ 7337

Last change on this file since 7337 was 7337, checked in by Don-vip, 10 years ago

see #10230, see #10033 - SAN tweaks + fix unit test (for real?)

File size: 16.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.IOException;
8import java.io.InputStream;
9import java.math.BigInteger;
10import java.net.BindException;
11import java.net.InetAddress;
12import java.net.ServerSocket;
13import java.net.Socket;
14import java.net.SocketException;
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;
22import java.security.KeyStore;
23import java.security.NoSuchAlgorithmException;
24import java.security.PrivateKey;
25import java.security.SecureRandom;
26import java.security.cert.Certificate;
27import java.security.cert.X509Certificate;
28import java.util.Arrays;
29import java.util.Date;
30import java.util.Enumeration;
31import java.util.Vector;
32
33import javax.net.ssl.KeyManagerFactory;
34import javax.net.ssl.SSLContext;
35import javax.net.ssl.SSLServerSocket;
36import javax.net.ssl.SSLServerSocketFactory;
37import javax.net.ssl.SSLSocket;
38import javax.net.ssl.TrustManagerFactory;
39
40import 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;
66
67/**
68 * Simple HTTPS server that spawns a {@link RequestProcessor} for every secure connection.
69 *
70 * @since 6941
71 */
72public class RemoteControlHttpsServer extends Thread {
73
74 /** The server socket */
75 private ServerSocket server;
76
77 private static RemoteControlHttpsServer instance;
78 private boolean initOK = false;
79 private SSLContext sslContext;
80
81 private static final int HTTPS_PORT = 8112;
82
83 /**
84 * JOSM keystore file name.
85 * @since 7337
86 */
87 public static final String KEYSTORE_FILENAME = "josm.keystore";
88
89 /**
90 * Preference for keystore password (automatically generated by JOSM).
91 * @since 7335
92 */
93 public StringProperty KEYSTORE_PASSWORD = new StringProperty("remotecontrol.https.keystore.password", "");
94
95 /**
96 * Preference for certificate password (automatically generated by JOSM).
97 * @since 7335
98 */
99 public StringProperty KEYENTRY_PASSWORD = new StringProperty("remotecontrol.https.keyentry.password", "");
100
101 /**
102 * Creates a GeneralName object from known types.
103 * @param t one of 4 known types
104 * @param v value
105 * @return which one
106 * @throws IOException
107 */
108 private static GeneralName createGeneralName(String t, String v) throws IOException {
109 GeneralNameInterface gn;
110 switch (t.toLowerCase()) {
111 case "uri": gn = new URIName(v); break;
112 case "dns": gn = new DNSName(v); break;
113 case "ip": gn = new IPAddressName(v); break;
114 default: gn = new OIDName(v);
115 }
116 return new GeneralName(gn);
117 }
118
119 /**
120 * Create a self-signed X.509 Certificate.
121 * @param dn the X.509 Distinguished Name, eg "CN=localhost, OU=JOSM, O=OpenStreetMap"
122 * @param pair the KeyPair
123 * @param days how many days from now the Certificate is valid for
124 * @param algorithm the signing algorithm, eg "SHA256withRSA"
125 * @param san SubjectAlternativeName extension (optional)
126 */
127 private static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm, String san) throws GeneralSecurityException, IOException {
128 PrivateKey privkey = pair.getPrivate();
129 X509CertInfo info = new X509CertInfo();
130 Date from = new Date();
131 Date to = new Date(from.getTime() + days * 86400000l);
132 CertificateValidity interval = new CertificateValidity(from, to);
133 BigInteger sn = new BigInteger(64, new SecureRandom());
134 X500Name owner = new X500Name(dn);
135
136 info.set(X509CertInfo.VALIDITY, interval);
137 info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn));
138 info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
139 info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
140 info.set(X509CertInfo.KEY, new CertificateX509Key(pair.getPublic()));
141 info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
142 AlgorithmId algo = new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid);
143 info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algo));
144
145 CertificateExtensions ext = new CertificateExtensions();
146 // Critical: Not CA, max path len 0
147 ext.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, false, 0));
148 // Critical: only allow TLS ("serverAuth" = 1.3.6.1.5.5.7.3.1)
149 ext.set(ExtendedKeyUsageExtension.NAME, new ExtendedKeyUsageExtension(true,
150 new Vector<ObjectIdentifier>(Arrays.asList(new ObjectIdentifier("1.3.6.1.5.5.7.3.1")))));
151
152 if (san != null) {
153 int colonpos;
154 String[] ps = san.split(",");
155 GeneralNames gnames = new GeneralNames();
156 for(String item: ps) {
157 colonpos = item.indexOf(':');
158 if (colonpos < 0) {
159 throw new IllegalArgumentException("Illegal item " + item + " in " + san);
160 }
161 String t = item.substring(0, colonpos);
162 String v = item.substring(colonpos+1);
163 gnames.add(createGeneralName(t, v));
164 }
165 // Non critical
166 ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(false, gnames));
167 }
168
169 info.set(X509CertInfo.EXTENSIONS, ext);
170
171 // Sign the cert to identify the algorithm that's used.
172 X509CertImpl cert = new X509CertImpl(info);
173 cert.sign(privkey, algorithm);
174
175 // Update the algorithm, and resign.
176 algo = (AlgorithmId)cert.get(X509CertImpl.SIG_ALG);
177 info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, algo);
178 cert = new X509CertImpl(info);
179 cert.sign(privkey, algorithm);
180 return cert;
181 }
182
183 private void initialize() {
184 if (!initOK) {
185 try {
186 char[] storePassword = KEYSTORE_PASSWORD.get().toCharArray();
187 char[] entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
188
189 Path dir = Paths.get(RemoteControl.getRemoteControlDir());
190 Path path = dir.resolve(KEYSTORE_FILENAME);
191 Files.createDirectories(dir);
192
193 if (!Files.exists(path)) {
194 Main.debug("No keystore found, creating a new one");
195
196 // Create new keystore like previous one generated with JDK keytool as follows:
197 // keytool -genkeypair -storepass josm_ssl -keypass josm_ssl -alias josm_localhost -dname "CN=localhost, OU=JOSM, O=OpenStreetMap"
198 // -ext san=ip:127.0.0.1 -keyalg RSA -validity 1825
199
200 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
201 generator.initialize(2048);
202 KeyPair pair = generator.generateKeyPair();
203
204 X509Certificate cert = generateCertificate("CN=localhost, OU=JOSM, O=OpenStreetMap", pair, 1825, "SHA256withRSA",
205 "ip:127.0.0.1,dns:localhost,uri:https://127.0.0.1:"+HTTPS_PORT);
206
207 KeyStore ks = KeyStore.getInstance("JKS");
208 ks.load(null, null);
209
210 // Generate new passwords. See https://stackoverflow.com/a/41156/2257172
211 SecureRandom random = new SecureRandom();
212 KEYSTORE_PASSWORD.put(new BigInteger(130, random).toString(32));
213 KEYENTRY_PASSWORD.put(new BigInteger(130, random).toString(32));
214
215 storePassword = KEYSTORE_PASSWORD.get().toCharArray();
216 entryPassword = KEYENTRY_PASSWORD.get().toCharArray();
217
218 ks.setKeyEntry("josm_localhost", pair.getPrivate(), entryPassword, new Certificate[]{cert});
219 ks.store(Files.newOutputStream(path, StandardOpenOption.CREATE), storePassword);
220 }
221
222 try (InputStream in = Files.newInputStream(path)) {
223 // Load keystore
224 KeyStore ks = KeyStore.getInstance("JKS");
225 ks.load(in, storePassword);
226
227 if (Main.isDebugEnabled()) {
228 for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
229 Main.debug("Alias in keystore: "+aliases.nextElement());
230 }
231 }
232
233 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
234 kmf.init(ks, entryPassword);
235
236 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
237 tmf.init(ks);
238
239 sslContext = SSLContext.getInstance("TLS");
240 sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
241
242 if (Main.isTraceEnabled()) {
243 Main.trace("SSL Context protocol: " + sslContext.getProtocol());
244 Main.trace("SSL Context provider: " + sslContext.getProvider());
245 }
246
247 Enumeration<String> aliases = ks.aliases();
248 if (aliases.hasMoreElements()) {
249 Main.platform.setupHttpsCertificate(new KeyStore.TrustedCertificateEntry(ks.getCertificate(aliases.nextElement())));
250 }
251
252 initOK = true;
253 }
254 } catch (IOException | GeneralSecurityException e) {
255 Main.error(e);
256 }
257 }
258 }
259
260 /**
261 * Starts or restarts the HTTPS server
262 */
263 public static void restartRemoteControlHttpsServer() {
264 int port = Main.pref.getInteger("remote.control.https.port", HTTPS_PORT);
265 try {
266 stopRemoteControlHttpsServer();
267
268 if (RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get()) {
269 instance = new RemoteControlHttpsServer(port);
270 if (instance.initOK) {
271 instance.start();
272 }
273 }
274 } catch (BindException ex) {
275 Main.warn(marktr("Cannot start remotecontrol https server on port {0}: {1}"),
276 Integer.toString(port), ex.getLocalizedMessage());
277 } catch (IOException ioe) {
278 Main.error(ioe);
279 } catch (NoSuchAlgorithmException e) {
280 Main.error(e);
281 }
282 }
283
284 /**
285 * Stops the HTTPS server
286 */
287 public static void stopRemoteControlHttpsServer() {
288 if (instance != null) {
289 try {
290 instance.stopServer();
291 instance = null;
292 } catch (IOException ioe) {
293 Main.error(ioe);
294 }
295 }
296 }
297
298 /**
299 * Constructs a new {@code RemoteControlHttpsServer}.
300 * @param port The port this server will listen on
301 * @throws IOException when connection errors
302 * @throws NoSuchAlgorithmException if the JVM does not support TLS (can not happen)
303 */
304 public RemoteControlHttpsServer(int port) throws IOException, NoSuchAlgorithmException {
305 super("RemoteControl HTTPS Server");
306 this.setDaemon(true);
307
308 initialize();
309
310 if (!initOK) {
311 Main.error(tr("Unable to initialize Remote Control HTTPS Server"));
312 return;
313 }
314
315 // Create SSL Server factory
316 SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
317 if (Main.isTraceEnabled()) {
318 Main.trace("SSL factory - Supported Cipher suites: "+Arrays.toString(factory.getSupportedCipherSuites()));
319 }
320
321 // Start the server socket with only 1 connection.
322 // Also make sure we only listen
323 // on the local interface so nobody from the outside can connect!
324 // NOTE: On a dual stack machine with old Windows OS this may not listen on both interfaces!
325 this.server = factory.createServerSocket(port, 1,
326 InetAddress.getByName(Main.pref.get("remote.control.host", "localhost")));
327
328 if (Main.isTraceEnabled() && server instanceof SSLServerSocket) {
329 SSLServerSocket sslServer = (SSLServerSocket) server;
330 Main.trace("SSL server - Enabled Cipher suites: "+Arrays.toString(sslServer.getEnabledCipherSuites()));
331 Main.trace("SSL server - Enabled Protocols: "+Arrays.toString(sslServer.getEnabledProtocols()));
332 Main.trace("SSL server - Enable Session Creation: "+sslServer.getEnableSessionCreation());
333 Main.trace("SSL server - Need Client Auth: "+sslServer.getNeedClientAuth());
334 Main.trace("SSL server - Want Client Auth: "+sslServer.getWantClientAuth());
335 Main.trace("SSL server - Use Client Mode: "+sslServer.getUseClientMode());
336 }
337 }
338
339 /**
340 * The main loop, spawns a {@link RequestProcessor} for each connection.
341 */
342 @Override
343 public void run() {
344 Main.info(marktr("RemoteControl::Accepting secure connections on port {0}"),
345 Integer.toString(server.getLocalPort()));
346 while (true) {
347 try {
348 @SuppressWarnings("resource")
349 Socket request = server.accept();
350 if (Main.isTraceEnabled() && request instanceof SSLSocket) {
351 SSLSocket sslSocket = (SSLSocket) request;
352 Main.trace("SSL socket - Enabled Cipher suites: "+Arrays.toString(sslSocket.getEnabledCipherSuites()));
353 Main.trace("SSL socket - Enabled Protocols: "+Arrays.toString(sslSocket.getEnabledProtocols()));
354 Main.trace("SSL socket - Enable Session Creation: "+sslSocket.getEnableSessionCreation());
355 Main.trace("SSL socket - Need Client Auth: "+sslSocket.getNeedClientAuth());
356 Main.trace("SSL socket - Want Client Auth: "+sslSocket.getWantClientAuth());
357 Main.trace("SSL socket - Use Client Mode: "+sslSocket.getUseClientMode());
358 Main.trace("SSL socket - Session: "+sslSocket.getSession());
359 }
360 RequestProcessor.processRequest(request);
361 } catch (SocketException se) {
362 if (!server.isClosed()) {
363 Main.error(se);
364 }
365 } catch (IOException ioe) {
366 Main.error(ioe);
367 }
368 }
369 }
370
371 /**
372 * Stops the HTTPS server.
373 *
374 * @throws IOException if any I/O error occurs
375 */
376 public void stopServer() throws IOException {
377 if (server != null) {
378 server.close();
379 Main.info(marktr("RemoteControl::Server (https) stopped."));
380 }
381 }
382}
Note: See TracBrowser for help on using the repository browser.