Skip to content

Commit

Permalink
feat: SSL support or Websockets (#835)
Browse files Browse the repository at this point in the history
* feat: SSL support or Websockets

* feat: used native java implementation to convert let encrypt PEM to PKCS2

* chore: updated docs, and minor cleanups
  • Loading branch information
ohager authored Oct 12, 2024
1 parent 74d5e18 commit ad38e43
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 72 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ target
/build/
/bin/
launch.json

*.pem
*.p12
140 changes: 140 additions & 0 deletions SSL_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# SSL Configuration

## Signum Node - Local SSL Configuration

This guide explains how to generate SSL certificates to run a Signum Node locally with HTTPS enabled.

### Prerequisites

Ensure you have `openssl` installed on your system. You can verify this by running the following command:

```bash
openssl version
```

If not installed, you can install it using your package manager (e.g., `brew install openssl` on macOS,
`sudo apt install openssl` on Ubuntu).

### Steps to Generate SSL Certificates

1. **Generate a private key**

Use the following command to generate a private RSA key:

```bash
openssl genpkey -algorithm RSA -out localhost.pem
```

2. **Generate a self-signed certificate**

With the private key, create a self-signed certificate valid for 365 days:

```bash
openssl req -x509 -new -key localhost.pem -out localhost_chain.pem -days 365
```

You will be prompted to fill in some details like Country, State, and Common Name. For local development, you can use
`localhost` as the Common Name (CN).

3. **Generate a keystore**

Finally, create a PKCS#12 keystore that bundles the private key and certificate together:

```bash
openssl pkcs12 -export -inkey localhost.pem -in localhost_chain.pem -out localhost_keystore.p12 -name "localhost" -password pass:development
```

This creates a keystore named `localhost_keystore.p12` protected with the password `development`.

### Update `node.properties`

In your `node.properties` file, enable SSL for the API and point to the newly created keystore. Add or update the
following lines:

```properties
API.SSL=on
API.SSL_keyStorePath=./localhost_keystore.p12
API.SSL_keyStorePassword=development
```

### Final Steps

1. Restart the Signum Node to apply the changes.
2. Your Signum Node should now be running locally with SSL enabled.

You can access it using `https://localhost:<your_port>` and/or `wss://localhost:<your_port>/events` with the port number
configured for your node.

## Using Certbot to Generate SSL Certificates for a Signum Node for your custom domain

Certbot is a tool used to automate the process of obtaining and renewing SSL certificates from Let's Encrypt or other
Certificate Authorities. This guide explains how to use Certbot to generate SSL certificates for running a Signum Node
locally.
### Prerequisites
1. **Certbot installation**: Ensure Certbot is installed. You can check by running:
```bash
certbot --version
```
If it's not installed, follow the [official installation guide](https://certbot.eff.org/instructions) for your
system.

2. **Domain name**: To use Certbot, you need a publicly accessible domain (Certbot won't work for pure localhost
setups). If you are running a local node accessible from the internet (e.g., via a reverse proxy like Nginx), you'll
need a registered domain name pointing to your local machine.

3. **Port forwarding (optional)**: If your node is not publicly accessible, you may need to set up port forwarding to
allow Certbot to perform HTTP-01 or DNS-01 validation.

### Request a certificate

Run Certbot to obtain a certificate for your domain. Replace `yourdomain.com` with your actual domain name.

```bash
sudo certbot certonly --standalone -d yourdomain.com
```

Certbot will generate the necessary files, including the certificate (`.crt`) and private key (`.key`).

By default, these will be stored in `/etc/letsencrypt/live/yourdomain.com/`.

> The Signum Node looks into the "letsencryptpath" and converts it to the necesary keystore file. No further action necessary here.

### Update `node.properties`

In your `node.properties` file, enable SSL for the API and configure the path to the Certbot-generated keystore:

```properties
API.SSL=on
# the file name of your keystore file. Let's Encrypt Cert will be automatically converted and stored under this path.
API.SSL_keyStorePath=./keystore.p12
API.SSL_keyStorePassword=<your_password>
# your path of letsencrypt certs. The Node looks for "privkey.pem" and "fullchain.pem" files
API.SSL_letsencryptPath=/etc/letsencrypt/live/<yourdomain>.com
```

### Automating Certificate Renewal

Certbot certificates expire every 90 days. You can automate the renewal process using Certbot's cron job feature.
> Signum Nodes reloads the certificate on startup and/or every 7 days while running
1. Set up a cron job to automatically renew certificates:
```bash
sudo crontab -e
```
2. Add the following line to renew certificates automatically:
```bash
0 0 * * * certbot renew --quiet
```
### Final Steps
1. Restart your Signum Node after the certificate is created and the `node.properties` file is updated.
2. Access the Signum Node using `https://yourdomain.com:<your_port>`.
13 changes: 7 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ dependencies {
implementation 'com.github.signum-network:signumj:v1.3.1'

implementation 'io.reactivex.rxjava2:rxjava:2.2.15'
implementation 'org.bouncycastle:bcprov-jdk18on:1.75'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
implementation 'org.ehcache:ehcache:3.9.9'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'commons-cli:commons-cli:1.4'
Expand Down Expand Up @@ -74,11 +75,11 @@ dependencies {
implementation 'org.slf4j:slf4j-api:1.7.35'
implementation 'org.slf4j:slf4j-jdk14:1.7.35'

implementation 'org.eclipse.jetty:jetty-server:10.0.19'
implementation 'org.eclipse.jetty:jetty-servlet:10.0.19'
implementation 'org.eclipse.jetty:jetty-servlets:10.0.19'
implementation 'org.eclipse.jetty:jetty-rewrite:10.0.19'
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:10.0.19'
implementation 'org.eclipse.jetty:jetty-server:10.0.24'
implementation 'org.eclipse.jetty:jetty-servlet:10.0.24'
implementation 'org.eclipse.jetty:jetty-servlets:10.0.24'
implementation 'org.eclipse.jetty:jetty-rewrite:10.0.24'
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:10.0.24'

implementation 'javax.annotation:javax.annotation-api:1.3.2'

Expand Down
8 changes: 8 additions & 0 deletions conf/mock/node.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node.network = signum.net.MockNetwork
DB.SqliteJournalMode = WAL

# Database connection JDBC url
DB.Url=jdbc:sqlite:file:./db/signum-test.sqlite.db

SoloMiningPassphrases="some phrase"
AllowOtherSoloMiners=true
4 changes: 4 additions & 0 deletions conf/mock/node.sqlite-mock.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DB.SqliteJournalMode = WAL

# Database connection JDBC url
DB.Url=jdbc:sqlite:file:./db/signum-mock.sqlite.db
3 changes: 0 additions & 3 deletions conf/node-default.properties
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,12 @@
# API.ServerEnforcePOST = yes

## Your keystore file and password, required if uiSSL or apiSSL are enabled.

# API.SSL_keyStorePath = keystore
# API.SSL_keyStorePassword = password

## If you use https://certbot.eff.org/ to issue your certificate, provide below the path for your keys.
## BRS will automatically create the keystore file using the password above and will reload it weekly.
## Make sure you configure certbot to renew your certificate automatically so you don't need to worry about it.
## Note, you need 'openssl' on your path for this to work, most Linux distributions have it already.

# API.SSL_letsencryptPath = /etc/letsencrypt/live/yourdomain.com

#### DATABASE ####
Expand Down
113 changes: 113 additions & 0 deletions src/brs/web/server/AbstractServerConnectorBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package brs.web.server;

import brs.props.Props;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public abstract class AbstractServerConnectorBuilder {

private static final Logger logger = LoggerFactory.getLogger(AbstractServerConnectorBuilder.class);
protected final WebServerContext context;

public AbstractServerConnectorBuilder(WebServerContext context) {
this.context = context;
}

abstract ServerConnector build(Server server);

protected ServerConnector createSSLConnector(Server server) {
logger.info("Creating SSL Connector");
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecureScheme("https");
httpsConfig.setSecurePort(context.getPropertyService().getInt(Props.API_PORT));
httpsConfig.addCustomizer(new SecureRequestCustomizer());
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();

sslContextFactory.setKeyStorePath(context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PATH));
sslContextFactory.setKeyStorePassword(context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PASSWORD));

// Handle optional Let's Encrypt Certificates...
String letsencryptPath = context.getPropertyService().getString(Props.API_SSL_LETSENCRYPT_PATH);
if (letsencryptPath != null && !letsencryptPath.isEmpty()) {
try {
loadLetsEncryptCertsAsPkcs12(letsencryptPath, context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PATH), context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PASSWORD));
} catch (Exception e) {
logger.error(e.getMessage());
}

// Reload the certificate every week, in case it was renewed
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
Runnable reloadCert = () -> {
try {
loadLetsEncryptCertsAsPkcs12(letsencryptPath, context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PATH), context.getPropertyService().getString(Props.API_SSL_KEY_STORE_PASSWORD));
sslContextFactory.reload(consumer -> logger.info("SSL keystore from letsencrypt reloaded."));
} catch (Exception e) {
logger.error(e.getMessage());
}
};
scheduler.scheduleWithFixedDelay(reloadCert, 7, 7, TimeUnit.DAYS);
}

String[] strongCiphers = {
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
// Add more strong ciphers as needed
};
sslContextFactory.setIncludeCipherSuites(strongCiphers);
sslContextFactory.setIncludeProtocols("TLSv1.2", "TLSv1.3");
sslContextFactory.setExcludeProtocols("SSLv3");
return new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"),
new HttpConnectionFactory(httpsConfig));
}

public void loadLetsEncryptCertsAsPkcs12(String letsencryptPath, String p12Filename, String password) throws Exception {

logger.info("Converting Let's Encrypt Certificate to PKCS12...");
Security.addProvider(new BouncyCastleProvider());

try (InputStream keyIn = new FileInputStream(letsencryptPath + "/privkey.pem");
InputStream certIn = new FileInputStream(letsencryptPath + "/fullchain.pem")) {

// Load PEM files
PEMParser keyParser = new PEMParser(new InputStreamReader(keyIn));
PEMParser certParser = new PEMParser(new InputStreamReader(certIn));

// make keys compatible with java.security.keystore
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) keyParser.readObject();
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded()));

X509CertificateHolder certObj = (X509CertificateHolder) certParser.readObject();
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
certConverter.setProvider("BC");
X509Certificate certificate = certConverter.getCertificate(certObj);
X509Certificate[] certificates = new X509Certificate[]{certificate};

// Add the private key and certificates to the keystore
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
keyStore.setKeyEntry("SIGNUM_NODE_CERT", privateKey, password.toCharArray(), certificates);

// Finally, save as PKCS12 file...
try (OutputStream out = new FileOutputStream(p12Filename)) {
keyStore.store(out, password.toCharArray());
logger.info("Let's Encrypt Certificate successfully converted to PKCS12 and saved under: {}", p12Filename);
}
}
}
}
Loading

0 comments on commit ad38e43

Please sign in to comment.