Search code examples
node.jssslpop3snistarttls

NodeJS STARTTLS Use SNI


I'm building a simple, STARTTLS capable POP3 Proxy in Node.JS and I'm having quite a hard time.

The proxy serves as a front-end for many back-end servers, so it must load their certificates dynamically, depending on the Client's connection.

I'm trying to use the SNICallback, which brings me the servername the client uses, but I can't set the right certificate after this, because I need one certificate before I have this call, when I create the secure context.

The code is as bellow:

// Load libraries
var net = require('net');
var tls = require('tls');
var fs = require('fs');

// Load certificates (created with openssl)
var certs = [];
for (var i = 1; i <= 8; i++) {
   var hostName = 'localhost' + i;
   certs[hostName] = {
      key : fs.readFileSync('./private-key.pem'),
      cert : fs.readFileSync('./public-cert' + i + '.pem'),
   }
}

var server = net.createServer(function(socket) {
   socket.write('+OK localhost POP3 Proxy Ready\r\n');

   socket.on('data', function(data) {
      if (data == "STLS\r\n") {
         socket.write("+OK begin TLS negotiation\r\n");
         upgradeSocket(socket);
      } else if (data == "QUIT\r\n") {
         socket.write("+OK Logging out.\r\n");
         socket.end();
      } else {
         socket.write("-ERR unknown command.\r\n");
      }
   });

}).listen(10110);

and upgradeSocket() is as follows:

function upgradeSocket(socket) {
   // I need this 'details' or handshake will fail with a message: 
   // SSL routines:ssl3_get_client_hello:no shared cipher
   var details = {
       key : fs.readFileSync('./private-key.pem'),
       cert : fs.readFileSync('./public-cert1.pem'),
   }

   var options = {
      isServer : true,
      server : server,
      SNICallback : function(serverName) {
         return tls.createSecureContext(certs[serverName]);
      },
   }

   sslcontext = tls.createSecureContext(details);
   pair = tls.createSecurePair(sslcontext, true, false, false, options);

   pair.encrypted.pipe(socket);
   socket.pipe(pair.encrypted);

   pair.fd = socket.fd;
   pair.on("secure", function() {
      console.log("TLS connection secured");
   });
}

It handshakes correctly but the certificate I use is the static one in 'details', not the one I get in the SNICallback.

To test it I'm running the server and using gnutls-cli as a Client:

~$ gnutls-cli -V -s -p 10110 --crlf --insecure -d 5 localhost3
STLS
^D (Control+D)

The above command is supposed to get me the 'localhost3' certificate but it's getting the 'localhost1' because it's defined in 'details' var;

There are just too many examples throughout the internet with HTTPS or for TLS Clients, which it's a lot different from what I have here, and even for Servers as well but they're not using SNI. Any help will be appreciated.

Thanks in advance.


Solution

  • The answer is quite simple using tls.TLSSocket, though there is a gotcha with the listeners.

    You have to remove all the listeners from the regular net.Socket you have, instantiate a new tls.TLSSocket using your net.Socket and put the listeners back on the tls.TLSSocket.

    To achieve this easily, use a wrapper like Haraka's tls_socket pluggableStream over the regular net.Socket and replace the "upgrade" function to something like:

    pluggableStream.prototype.upgrade = function(options) {
        var self = this;
        var socket = self;
        var netSocket = self.targetsocket;
    
        socket.clean();
    
        var secureContext = tls.createSecureContext(options)
        var tlsSocket = new tls.TLSSocket(netSocket, {
            // ...
            secureContext : secureContext,
            SNICallback : options.SNICallback
            // ...
        });
    
        self.attach(tlsSocket);
    }
    

    and your options object would have the SNICallback defined as:

    var options {
        // ...
        SNICallback : function(serverName, callback){
            callback(null, tls.createSecureContext(getCertificateFor(serverName));
        // ...
        }
    }