JavaScript
Article

How to Use SSL/TLS with Node.js

By Florian Rappl

Using HTTPS is becoming more and more prevalent, therefore we should know how to implement SSL/TLS in our Node.js applications – either for accessing HTTPS resources or for providing resources with encryption. What does HTTPS actually mean? What does it imply? Are there any constraints and restrictions? We will try to find an answer for all of these questions.

Additionally, we should not only try to protect our clients by providing HTTPS, but we should also demand encrypted connections from the servers we are talking to. We will see that possibilities exist to activate the SSL/TLS layer even if it wouldn’t be enabled by default. Let’s start with a short review of HTTPS’s current state.

HTTPS Everywhere

On February the 17th 2015 the HTTP/2 protocol was approved by the IESG to be published as a proposed standard. This was a major milestone. Now we can all upgrade our servers to use HTTP/2. One of the most important aspects is the backwards compatibility with HTTP 1.1 and negotiation mechanism to choose a different protocol. Although the standard does not specify mandatory encryption, most browsers will only support HTTP/2 over TLS. This gives HTTPS another boost. Finally HTTPS everywhere!

What does our stack actually look like? From the perspective of a website running in the browser (application level) we have roughly the following layers to reach the IP level:

  1. Client Browser
  2. HTTP
  3. SSL/TLS
  4. TCP
  5. IP

HTTPS is nothing more than the HTTP protocol on top of SSL/TLS. Hence all the rules of HTTP still have to apply. What does this additional layer actually give us? There are multiple advantages. We get authentication by having keys and certificates. Also a certain kind of privacy and confidentiality is guaranteed, as the connection is encrypted in an asymmetric manner. Last but not least data integrity is also preserved, i.e. that transmitted data cannot be changed during transit.

One of the most common myths is that using SSL/TLS requires too many resources and slows down the server. This is certainly not true anymore. We also do not need any specialized hardware with cryptography units. Even for Google, the SSL/TLS layer accounts for less than 1% of the CPU load. Furthermore the network overhead of HTTPS as compared to HTTP is below 2%. All in all it would not make sense to forgo HTTPS for having a little bit of overhead.

istlsfastyet

The most recent version is TLS 1.2 (currently version 1.3 is available as a working draft). TLS is the successor of SSL, which is available in its latest release SSL 3.0. The changes from SSL to TLS preclude interoperability. The basic procedure is, however, unchanged. We have three different encrypted channels. The first is a public key infrastructure for certificate chains. The second provides public key cryptography for key exchanges. Finally, the third one is symmetric. Here we have cryptography for data transfers.

We can use a variety of hashing algorithms, e.g., MD5, SHA1, SHA256. MD5 is already busted and should not be used. SHA1 is still acceptable, but may be busted soon. Much better is SHA256, which is currently gaining more users. One of the issues with SHA256 was the support from (older) operating systems. For instance on Windows XP SHA256 it can only be used with installed Service Pack 3.

HTTPS is also gaining more attention for clients. Privacy and security concerns have always been around, but with the growing amount of online accessible data and services people are getting more and more concerned. A useful browser plugin is “HTTPS Everywhere”, which encrypts our communications with most websites.

HTTPS-everywhere

The creators realized that many websites offer HTTPS only partially. The plugin allows us to rewrite requests for those sites, which offer only partial HTTPS support. Alternatively we can also block HTTP altogether (see screenshot above).

Basic Communication

The certificate’s validation process involves validating the certificate signature and expiration. Also we need to verify that it chains to trusted root. Finally we need to check to see if it was revoked.

The sequence diagram for HTTPS handshake looks as follows. We start with the init from the client, which is followed by a message with the certificate and key exchange. After the server send its completed package the client can start the key exchange and cipher specification transmission. At this point the client is finished. Finally the server confirms the cipher specification selection and closes the handshake.

HTTPS-sequence

The whole sequence is triggered independently of HTTP. If we decide to use HTTPS only the socket handling is changed. The client is still issuing HTTP requests, however, the socket will perform the previously described handshake and encrypt the content (header and body).

So what packages can be used to handle SSL/TLS with Node.js? In the following we will shortly present three standard packages.

ssl-root-cas

The ssl-root-cas module helps us when we face issues with custom or expired certificates. It is also very helpful if the authority is not available.

Most of the time it boils down to using the following command. The inject method modifies https.globalAgent.options.ca to include a set of standard certificates.

require('ssl-root-cas').inject();

However, it might be required to add local custom certificates. This is, of course, possible as well:

require('ssl-root-cas').addFile('my-cert.crt');

The object returned from the ssl-root-cas module is chainable and can therefore be used to add multiple files or inject the standard certificates in a single call.

https

Similar to http we can use https. The major difference is that we need to supply a key if we want to publish a server. This will be discussed in the next section.

For now we look on how we can create a HTTPS request.

var https = require('https');

https.get('https://www.amazon.com/', function(res) {
   console.log('statusCode: ', res.statusCode);

   res.on('data', function(d) {
      process.stdout.write(d);
   });
});

Basically https offers the same functionality as http, but adds the SSL/TLS layer on top. The API is mostly identical.

tls

The tls module requires the openssl toolkit, which will be discussed in the next section. The module provides SSL/TLS for encrypted stream communication. Internally tls is used by https. It shouldn’t be a surprise that using the tls API feels somehow related to the methods of the https module.

The following simple code creates a server that listens for connections at port 8000. Incoming connections will receive an encrypted message.

var tls = require('tls');
var fs = require('fs');

var options = {
   key  : fs.readFileSync('private.key'),
   cert : fs.readFileSync('public.cert')
};

var server = tls.createServer(options, function (res) {
   res.write('Hello World!');
   res.pipe(res);
}).listen(8000);

Similarly we can also connect to such a server.

var tls = require('tls');
var fs = require('fs');

var options = {
   key  : fs.readFileSync('private.key'),
   cert : fs.readFileSync('public.cert')
};

var client = tls.connect(8000, options, function () {
   console.log(client.authorized ? 'Authorized' : 'Not authorized');
});

client.on('data', function (data) {
   console.log(data.toString());
   client.end();
});

At this point we should have a look on how to generate our own key and certificate files.

Generating Certificates

A certificate is not mandatory from a technical point of view. Nevertheless, it was decided that man-in-the-middle attacks would be far too common considering the openness of the web. Therefore a certificate, which is signed by a trusted certificate authority (CA), is demanded by the specification. The CA ensures that the certificate holder is really who he claims to be.

In the following we will use the openssl toolkit. The application will give us the ability to generate a private key using RSA encryption. We can also issue a certificate signing request (CSR). Furthermore the core library of the OpenSSL project can be used to generate self-signed certificates. These certificates can then be used for testing purposes. They should not be used publicly.

The first step is to create a private key. As an example we can use the following command, generating a file called server.enc.key.

openssl genrsa -des3 -out server.enc.key 1024

The generated RSA key is a 1024-bit key encrypted using triple DES. The file is human readable. As a slight variant we can also omit the -des3 option and change the strength of the encryption, e.g. to 2048. Even though we use a 1024-bit key in this example, we should consider 2048-bit the minimum length for real keys.

Once we have our key generated we need to issue a certificate signing request. A request can be generated using using the following command.

openssl req -new -key server.enc.key -out server.csr

The created CSR is stored in the server.csr file. We will be required to self-sign this file.

Until this point it is required to enter the pass-phrase when using the key from the server.enc.key file. Since we will only use the certificate internally (and for testing purposes) we can think about removing the password protection. Removing the triple DES encryption from the key is done with the following command.

openssl rsa -in server.enc.key -out server.key

Here the original (encrypted) file server.enc.key is transformed to the (unencrypted) file server.key.

Finally we have to self-sign the certificate. The self-signed certificate will generate an error in browsers. The reason is that the signing certificate authority is unknown and therefore not trusted. To generate a temporary certificate server.crt we need to issue the following command:

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

The created certificate will expire in 365 days after issuing.

Integration with Express

Using HTTPS instead of HTTP in a Node.js web application is possible and often desired. We’ll start with the code for a simple Node.js web application using Express.

var http = require('http');
var app = require('express')();

app.get('/', function (req, res) {
   res.send('Hello World!');
});

http.createServer(app).listen(3000, function () {
   console.log('Started!');
});

What do we need to change to enable HTTPS? Obviously we need to important a different package. Finally we also need to supply the right key / certificate eventually. The contents of both files need to be present. They are handed over to the createServer method of the https package.

Let’s rewrite our Express “Hello World!” example using SSL/TLS:

var fs = require('fs');
var https = require('https');
var app = require('express')();
var options = {
   key  : fs.readFileSync('server.key'),
   cert : fs.readFileSync('server.crt')
};

app.get('/', function (req, res) {
   res.send('Hello World!');
});

https.createServer(options, app).listen(3000, function () {
   console.log('Started!');
});

This is it! In the previous code we assume that the key and certificate lie in the working directory. There is still a lot of room for improvement though. One possibility is to place the code for creating the server in a module. The module selects the right package for the selected protocol. Additionally the port, the key and the certificate are transported by a settings object.

A sample implementation could look as follows.

var fs = require('fs');

function setup (ssl) {
   if (ssl && ssl.active) {
      return {
         key  : fs.readFileSync(ssl.key),
         cert : fs.readFileSync(ssl.certificate)
      };
   }
}

function start (app, options) {
   if (options)
      return require('https').createServer(options, app);

   return require('http').createServer(app);
}

module.exports = {
   create: function (settings, app, cb) {
      var options = setup(settings.ssl);
      return start(app, options).listen(settings.port, cb);
   }
};

The crucial part is the ssl object within settings. If ssl actually exists and has a property called active that evaluates to true we will enabled SSL/TLS using the provided key / certificate (if any).

Conclusions

In 2015, there is no excuse to dismiss HTTPS. The future direction is clearly visible – HTTPS everywhere! In Node.js we have a lot of options to utilize SSL/TLS. We can publish our websites in HTTPS, we can create requests to encrypted websites and we can authorize otherwise untrusted certificates.

What’s your opinion on HTTPS everywhere? Are there Node.js packages for anything SSL/TLS related would you recommend?

  • Frank Fischer

    Isn’t it way easier to configure e.g. nginx as reverse proxy and letting it do the SSL termination?

  • Florian Rappl

    I think configuring the server is possibly easier, but in general that depends on your setup. The article was not about the simplest solution under certain circumstances, but rather a general introduction to using SSL/TLS with Node.js.

    @kelunik:disqus I will place a remark in the article, but please not that I’ve never recommended using 1024-bit keys. The length was only used in an example (which generated a key for testing purposes only).

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.