Imagine that your web application wants to accept standard SSL certificate-based authentication, but for one reason or another, you don't want to deal with having to sign client certificates with a CA and telling your web server about it. That is, you want to accept client certificates without them being validated against a list of known CAs. (If you want to support WebID login for your web site, for example, you don't have a choice—you must accept self-signed certificates.) In principle, this is an easy problem: You simply store your users' public keys in your database and check the client's certificates against those for authentication. (WebID works differently, of course, but on the frontend side, the difference doesn't matter.)

Unfortunately, most web server software, like Apache or nginx, assume the presence of a CA that client certificates will be checked against. If you don't supply a CA, all certificates will be rejected and you will have no opportunity to process them.

On the other hand, node.js does permit the use of self-signed certificates, so we can easily build a reverse proxy that passes such certificates on to our web application as HTTP headers. Here's how.

For the skeleton reverse proxy server, refer to my blog post about reverse-proxying HTTP with node.js. Extend the https_opts map in the example code there with the relevant options:

var https_opts = {
  key: fs.readFileSync('/etc/ssl/private/https.key', 'utf8'),
  cert: fs.readFileSync('/etc/ssl/private/https.chain.crt', 'utf8')
  requestCert: { value: true },
  rejectUnauthorized: { value: false }
};

This will cause the reverse proxy to request a client certificate from the user agent as soon as a connection is made. Now extend the request handler to encode the client's public key information into a JSON object and to pass that object on to the backend:

var handleRequest = function (data, next) {
  var peer_cert = req.connection.getPeerCertificate();
  if (peer_cert.valid_to) {
    peer_cert.valid_to = Date.parse(peer_cert.valid_to);
    peer_cert.valid_from = Date.parse(peer_cert.valid_from);
  }
  req.headers['x-mulk-peer-certificate'] = JSON.stringify(peer_cert);
  return next();
};

The object passed in the X-Mulk-Peer-Certificate header will be a map that contains a number of keys, such as (in the case of an RSA key) modulus, exponent, fingerprint, valid_to, valid_from, subject, and subjectaltname. Your backend can use this information to authenticate the user.

Caution: Make absolutely certain that the user will not be able to forge the X-Mulk-Peer-Certificate header! Any connection not established through our specially crafted reverse proxy must unset any such header passed by the user agent, or your security will be completely broken. (Unless you have the frontend authenticate with your backend in some way. Passing a shared secret from the frontend to the backend through an HTTP header is a good way of achieving that.)