diff --git a/src/freenet/clients/http/ToadletContextImpl.java b/src/freenet/clients/http/ToadletContextImpl.java index 55e33b0720..9a6ba9fb82 100644 --- a/src/freenet/clients/http/ToadletContextImpl.java +++ b/src/freenet/clients/http/ToadletContextImpl.java @@ -27,6 +27,7 @@ import freenet.clients.http.FProxyFetchInProgress.REFILTER_POLICY; import freenet.clients.http.annotation.AllowData; import freenet.clients.http.bookmark.BookmarkManager; +import freenet.crypt.SSL; import freenet.l10n.NodeL10n; import freenet.node.useralerts.UserAlertManager; import freenet.support.HTMLEncoder; @@ -203,9 +204,8 @@ public void sendReplyHeadersStatic(int replyCode, String replyDescription, Multi @Override public void sendReplyHeadersFProxy(int replyCode, String replyDescription, MultiValueTable mvt, String mimeType, long contentLength) throws ToadletContextClosedException, IOException { - boolean enableJavascript = false; - if(container.isFProxyWebPushingEnabled() && container.isFProxyJavascriptEnabled()) - enableJavascript = true; + boolean enableJavascript; + enableJavascript = container.isFProxyWebPushingEnabled() && container.isFProxyJavascriptEnabled(); sendReplyHeaders(replyCode, replyDescription, mvt, mimeType, contentLength, null, false, true, enableJavascript); } @@ -215,12 +215,11 @@ private void sendReplyHeaders(int replyCode, String replyDescription, MultiValue throw new IllegalStateException("Already sent headers!", firstReplySendingException); } firstReplySendingException = new Exception(); - - if(replyCookies != null) { - if (mvt == null) { - mvt = new MultiValueTable(); - } - + + if (mvt == null) { + mvt = new MultiValueTable(); + } + if (replyCookies != null) { // We do NOT use "set-cookie2" even though we should according though RFC2965 - Firefox 3.0.14 ignores it for me! for(Cookie cookie : replyCookies) { @@ -230,6 +229,14 @@ private void sendReplyHeaders(int replyCode, String replyDescription, MultiValue Logger.minor(this, "set-cookie: " + cookieHeader); } } + + if (container.isSSL()) { + String HSTS = SSL.getHSTSHeader(); + if (!HSTS.isEmpty() && !mvt.containsKey("strict-transport-security")) { + // SSL enabled, set strict-transport-security so that the user agent upgrade future requests to SSL. + mvt.put("strict-transport-security", HSTS); + } + } sendReplyHeaders(sockOutputStream, replyCode, replyDescription, mvt, mimeType, contentLength, mTime, shouldDisconnect, enableJavascript, allowFrames); } @@ -433,7 +440,11 @@ static void sendReplyHeaders( mvt.put("x-content-security-policy", contentSecurityPolicy); mvt.put("x-webkit-csp", contentSecurityPolicy); mvt.put("x-frame-options", allowFrames ? "SAMEORIGIN" : "DENY"); - + String HSTS = SSL.getHSTSHeader(); + if(!HSTS.isEmpty() && !mvt.containsKey("strict-transport-security")) { + // SSL enabled, set strict-transport-security so that the user agent upgrade future requests to SSL. + mvt.put("strict-transport-security", HSTS); + } StringBuilder buf = new StringBuilder(1024); buf.append("HTTP/1.1 "); buf.append(replyCode); @@ -517,13 +528,11 @@ private static String fixKey(String key) { * Handle an incoming connection. Blocking, obviously. */ public static void handle(Socket sock, ToadletContainer container, PageMaker pageMaker, UserAlertManager userAlertManager, BookmarkManager bookmarkManager) { - try { + try ( InputStream is = new BufferedInputStream(sock.getInputStream(), 4096); - - LineReadingInputStream lis = new LineReadingInputStream(is); - + LineReadingInputStream lis = new LineReadingInputStream(is) + ) { while(true) { - String firstLine = lis.readLine(32768, 128, false); // ISO-8859-1 or US-ASCII, _not_ UTF-8 if (firstLine == null) { sock.close(); diff --git a/src/freenet/crypt/SSL.java b/src/freenet/crypt/SSL.java index b9786fe8fb..36fbfae9c1 100644 --- a/src/freenet/crypt/SSL.java +++ b/src/freenet/crypt/SSL.java @@ -19,30 +19,41 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyManagementException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.PrivateKey; +import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; -import java.util.Arrays; - +import java.security.cert.X509Certificate; +import java.util.Date; import javax.net.ServerSocketFactory; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.X509Extensions; +import org.bouncycastle.asn1.x509.X509Name; +import org.bouncycastle.x509.X509V3CertificateGenerator; import freenet.config.InvalidConfigValueException; import freenet.config.SubConfig; +import freenet.node.NodeStarter; import freenet.support.Logger; import freenet.support.api.BooleanCallback; +import freenet.support.api.IntCallback; import freenet.support.api.StringCallback; -import freenet.support.io.Closer; import java.net.ServerSocket; public class SSL { @@ -64,6 +75,7 @@ public class SSL { private static String keyStore; private static String keyStorePass; private static String keyPass; + private static int HSTSMaxAge; /** * Call this function before ask ServerSocket @@ -73,6 +85,13 @@ public static boolean available() { return (ssf != null); } + public static String getHSTSHeader() { + if(available() && HSTSMaxAge > 0) + return "max-age=" + HSTSMaxAge; + else + return ""; + } + /** * Configure SSL support * @param sslConfig @@ -95,7 +114,7 @@ public void set(Boolean newValue) throws InvalidConfigValueException { enable = newValue; if(enable) try { - loadKeyStore(); + loadKeyStoreAndCreateCertificate(); createSSLContext(); } catch(Exception e) { enable = false; @@ -110,7 +129,7 @@ public void set(Boolean newValue) throws InvalidConfigValueException { } }); - sslConfig.register("sslKeyStore", "datastore/certs", configItemOrder++, true, true, "SSL.keyStore", "SSL.keyStore", + sslConfig.register("sslKeyStore", "datastore/certs", configItemOrder++, true, true, "SSL.keyStore", "SSL.keyStoreLong", new StringCallback() { @Override @@ -185,10 +204,28 @@ public void set(String newKeyPass) throws InvalidConfigValueException { } }); + sslConfig.register("sslHSTS", 0, configItemOrder++, true, true, "SSL.HSTS", "SSL.HSTSLong", + new IntCallback() { + + @Override + public Integer get() { + return HSTSMaxAge; + } + + @Override + public void set(Integer newHSTSMaxAge) throws InvalidConfigValueException { + if(newHSTSMaxAge < 0) + throwConfigError("HSTS Max age must be not less than 0", new IllegalArgumentException()); + else + HSTSMaxAge = newHSTSMaxAge; + } + }); + enable = sslConfig.getBoolean("sslEnable"); keyStore = sslConfig.getString("sslKeyStore"); keyStorePass = sslConfig.getString("sslKeyStorePass"); keyPass = sslConfig.getString("sslKeyPass"); + HSTSMaxAge = sslConfig.getInt("sslHSTS"); try { keystore = KeyStore.getInstance("PKCS12"); @@ -196,9 +233,14 @@ public void set(String newKeyPass) throws InvalidConfigValueException { createSSLContext(); } catch(Exception e) { Logger.error(SSL.class, "Keystore cannot be loaded, SSL will be disabled", e); + } finally { + if(enable && !available()) { + Logger.error(SSL.class, "SSL cannot be enabled!"); + } else if(enable) { + Logger.normal(SSL.class, "SSL is enabled."); + } + sslConfig.finishedInitialization(); } - sslConfig.finishedInitialization(); - } /** @@ -212,70 +254,90 @@ public static ServerSocket createServerSocket() throws IOException { return ssf.createServerSocket(); } - private static void loadKeyStore() throws NoSuchAlgorithmException, CertificateException, IOException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { + /** + * Load key store from file but do not try to create a self-signed certificate when the file does not exist. + * Used when the node is starting up, but not enough entropy is collected to generate a certificate. + * @throws NoSuchAlgorithmException, CertificateException, IOException + */ + private static void loadKeyStore() throws NoSuchAlgorithmException, CertificateException, IOException { if(enable) { // A keystore is where keys and certificates are kept // Both the keystore and individual private keys should be password protected - FileInputStream fis = null; - try { - fis = new FileInputStream(keyStore); + try (FileInputStream fis = new FileInputStream(keyStore)) { keystore.load(fis, keyStorePass.toCharArray()); } catch(FileNotFoundException fnfe) { // If keystore not exist, create keystore and server certificate keystore.load(null, keyStorePass.toCharArray()); - try { - Class certAndKeyGenClazz = anyClass( - "sun.security.x509.CertAndKeyGen", // Java 7 and earlier - "sun.security.tools.keytool.CertAndKeyGen" // Java 8 and later - ); - Constructor certAndKeyGenCtor = certAndKeyGenClazz.getConstructor(String.class, String.class, String.class); - Object keypair = certAndKeyGenCtor.newInstance(KEY_ALGORITHM, SIG_ALGORITHM, "BC"); - - Class x500NameClazz = Class.forName("sun.security.x509.X500Name"); - Constructor x500NameCtor = x500NameClazz.getConstructor(String.class, String.class, - String.class, String.class, String.class, String.class); - Object x500Name = x500NameCtor.newInstance(CERTIFICATE_CN, CERTIFICATE_OU, CERTIFICATE_ON, "", "", ""); - - Method certAndKeyGenGenerate = certAndKeyGenClazz.getMethod("generate", int.class); - certAndKeyGenGenerate.invoke(keypair, KEY_SIZE); - - Method certAndKeyGetPrivateKey = certAndKeyGenClazz.getMethod("getPrivateKey"); - PrivateKey privKey = (PrivateKey) certAndKeyGetPrivateKey.invoke(keypair); - - Certificate[] chain = new Certificate[1]; - Method certAndKeyGenGetSelfCertificate = certAndKeyGenClazz.getMethod("getSelfCertificate", - x500NameClazz, long.class); - chain[0] = (Certificate) certAndKeyGenGetSelfCertificate.invoke(keypair, x500Name, - CERTIFICATE_LIFETIME); - - keystore.setKeyEntry("freenet", privKey, keyPass.toCharArray(), chain); - storeKeyStore(); - createSSLContext(); - } catch (ClassNotFoundException cnfe) { - throw new UnsupportedOperationException("The JVM you are using does not support generating strong SSL certificates", cnfe); - } catch (NoSuchMethodException nsme) { - throw new UnsupportedOperationException("The JVM you are using does not support generating strong SSL certificates", nsme); - } - } finally { - Closer.close(fis); } } } + /** + * Load key store from file and create a self-signed certificate when the file does not exist. + */ + private static void loadKeyStoreAndCreateCertificate() throws NoSuchAlgorithmException, CertificateException, IOException, IllegalArgumentException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, InvalidKeyException, NoSuchProviderException, SignatureException { + if(enable) { + // A keystore is where keys and certificates are kept + // Both the keystore and individual private keys should be password protected + try (FileInputStream fis = new FileInputStream(keyStore)) { + keystore.load(fis, keyStorePass.toCharArray()); + } catch(FileNotFoundException fnfe) { + createSelfSignedCertificate(); + } + } + } + + private static void createSelfSignedCertificate() throws NoSuchAlgorithmException, CertificateException, IOException, IllegalArgumentException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, InvalidKeyException, NoSuchProviderException, SignatureException { + // If keystore not exist, create keystore and server certificate + keystore.load(null, keyStorePass.toCharArray()); + // Based on https://stackoverflow.com/questions/29852290/self-signed-x509-certificate-with-bouncy-castle-in-java + + // generate a key pair + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM, "BC"); + keyPairGenerator.initialize(KEY_SIZE, NodeStarter.getGlobalSecureRandom()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + // build a certificate generator + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + X500Principal dnName = new X500Principal("CN=" + CERTIFICATE_CN + ", OU=" + CERTIFICATE_OU); + + // add some options + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(new X509Name("dc=" + CERTIFICATE_CN)); + certGen.setIssuerDN(dnName); // use the same + // now + certGen.setNotBefore(new Date(System.currentTimeMillis())); + // CERTIFICATE_LIFETIME + certGen.setNotAfter(new Date(System.currentTimeMillis() + CERTIFICATE_LIFETIME * 1000)); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm(SIG_ALGORITHM); + certGen.addExtension(X509Extensions.ExtendedKeyUsage, true, + new ExtendedKeyUsage(KeyPurposeId.id_kp_timeStamping)); + + // finally, sign the certificate with the private key of the same KeyPair + X509Certificate cert = certGen.generate(keyPair.getPrivate(), "BC"); + PrivateKey privKey = keyPair.getPrivate(); + Certificate[] chain = new Certificate[1]; + chain[0] = cert; + keystore.setKeyEntry("freenet", privKey, keyPass.toCharArray(), chain); + storeKeyStore(); + createSSLContext(); + } + private static void storeKeyStore() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { if(enable) { - FileOutputStream fos = null; - try { - fos = new FileOutputStream(keyStore); + try (FileOutputStream fos = new FileOutputStream(keyStore);) { keystore.store(fos, keyStorePass.toCharArray()); - } finally { - Closer.close(fos); } } } private static void createSSLContext() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, KeyManagementException { if(enable) { + if(keystore.size() == 0) { + // No certificates here, can't create SSL context + return; + } // A KeyManagerFactory is used to create key managers KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); // Initialize the KeyManagerFactory to work with our keystore @@ -290,17 +352,6 @@ private static void createSSLContext() throws NoSuchAlgorithmException, Unrecove } } - private static Class anyClass(String... names) throws ClassNotFoundException { - for (String clazz : names) { - try { - return Class.forName(clazz); - } catch (ClassNotFoundException e) { - Logger.minor(SSL.class, "Class " + clazz + " not found", e); - } - } - throw new ClassNotFoundException("Any of " + Arrays.toString(names)); - } - private static void throwConfigError(String message, Throwable cause) throws InvalidConfigValueException { String causeMsg = cause.getMessage(); diff --git a/src/freenet/io/SSLNetworkInterface.java b/src/freenet/io/SSLNetworkInterface.java index 8b343352aa..65d03b3a34 100644 --- a/src/freenet/io/SSLNetworkInterface.java +++ b/src/freenet/io/SSLNetworkInterface.java @@ -72,10 +72,10 @@ protected ServerSocket createServerSocket() throws IOException { return serverSocket; } - private static final Set ALLOWED_CIPHERS = new HashSet(Arrays.asList( + private static final Set ALLOWED_CIPHERS = new HashSet(Arrays.asList( + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_EMPTY_RENEGOTIATION_INFO_SCSV")); } diff --git a/src/freenet/l10n/freenet.l10n.en.properties b/src/freenet/l10n/freenet.l10n.en.properties index 970a5625da..b0c74dd53c 100644 --- a/src/freenet/l10n/freenet.l10n.en.properties +++ b/src/freenet/l10n/freenet.l10n.en.properties @@ -1985,7 +1985,10 @@ SimpleToadletServer.sendAllThemesLong=If set true, all available themes will be SimpleToadletServer.ssl=Enable SSL? SimpleToadletServer.sslLong=Enable SSL on the web interface. You will need the 'Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files' for it to work. SSL.enable=Activate SSL support? +SSL.HSTS=HTTP Strict-Transport-Security max-age +SSL.HSTSLong=Set the time, in seconds, that the browser can access the web interface using HTTPS only. Common values: 3600 - 1 hour, 86400 - 1 day, 604800 - 7 days, 2592000 - 30 days. A very large value can make the web interface inaccessible, if the certificate expires and the browser keeps showing SSL errors. If set to 0 or if SSL is disabled, HSTS header will be disabled. SSL.keyStore=Path to the key store +SSL.keyStoreLong=Path to the key store file. You can create a valid key file in PKCS12 format with OpenSSL and use it with Freenet. If the file doesn't exist, a self-signed certificate will be created here. SSL.keyStorePass=Key store password SSL.keyPass=Private key password SSL.version=Version of SSL diff --git a/src/freenet/node/NodeStarter.java b/src/freenet/node/NodeStarter.java index 7fc0e1c3e6..de7f064f94 100644 --- a/src/freenet/node/NodeStarter.java +++ b/src/freenet/node/NodeStarter.java @@ -108,10 +108,6 @@ public Integer start(String[] args) { System.out.println("Usage: $ java freenet.node.Node "); return -1; } - String builtWithMessage = "freenet.jar built with freenet-ext.jar Build #" + ExtVersion.buildNumber + " r" + ExtVersion.cvsRevision+" running with ext build "+extBuildNumber+" r" + extRevisionNumber; - Logger.normal(this, builtWithMessage); - System.out.println(builtWithMessage); - File configFilename; if(args.length == 0) { System.out.println("Using default config filename freenet.ini"); @@ -147,6 +143,11 @@ public Integer start(String[] args) { return -2; } + String builtWithMessage = "freenet.jar built with freenet-ext.jar Build #" + ExtVersion.buildNumber + " r" + ExtVersion.cvsRevision+" running with ext build "+extBuildNumber+" r" + extRevisionNumber; + Logger.normal(this, builtWithMessage); + System.out.println(builtWithMessage); + + System.out.println("Starting executor..."); executor.start();