Skip to content

Commit

Permalink
Merge remote-tracking branch 'torusrxxx/SSL' into next
Browse files Browse the repository at this point in the history
  • Loading branch information
ArneBab committed Nov 30, 2024
2 parents a9b18be + c9f2803 commit 82f2ede
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 87 deletions.
39 changes: 24 additions & 15 deletions src/freenet/clients/http/ToadletContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -203,9 +204,8 @@ public void sendReplyHeadersStatic(int replyCode, String replyDescription, Multi

@Override
public void sendReplyHeadersFProxy(int replyCode, String replyDescription, MultiValueTable<String,String> 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);
}

Expand All @@ -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<String,String>();
}


if (mvt == null) {
mvt = new MultiValueTable<String,String>();
}
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) {
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
181 changes: 116 additions & 65 deletions src/freenet/crypt/SSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -95,7 +114,7 @@ public void set(Boolean newValue) throws InvalidConfigValueException {
enable = newValue;
if(enable)
try {
loadKeyStore();
loadKeyStoreAndCreateCertificate();
createSSLContext();
} catch(Exception e) {
enable = false;
Expand All @@ -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
Expand Down Expand Up @@ -185,20 +204,43 @@ 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");
loadKeyStore();
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();

}

/**
Expand All @@ -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
Expand All @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions src/freenet/io/SSLNetworkInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ protected ServerSocket createServerSocket() throws IOException {

return serverSocket;
}
private static final Set<String> ALLOWED_CIPHERS = new HashSet(Arrays.asList(
private static final Set<String> ALLOWED_CIPHERS = new HashSet<String>(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"));
}
3 changes: 3 additions & 0 deletions src/freenet/l10n/freenet.l10n.en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 82f2ede

Please sign in to comment.