Question

I am trying to create a TLS server / client setup using Node.js 0.8.8 with a self-signed certificate.

The essential server code looks like

var tlsServer = tls.createServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
}, function (connection) {
  // [...]
});
tlsServer.listen(3000);

Now when I try to connect to this server I use the following code:

var connection = tls.connect({
  host: '192.168.178.31',
  port: 3000,

  rejectUnauthorized: true,
  ca: [ fs.readFileSync('server-cert.pem') ]
}, function () {
  console.log(connection.authorized);
  console.log(connection.authorizationError);
  console.log(connection.getPeerCertificate());
});

If I remove the line

ca: [ fs.readFileSync('server-cert.pem') ]

from the client-side code, Node.js throws an error telling me DEPTH_ZERO_SELF_SIGNED_CERT. As far as I understand it this is due to the fact that it is a self-signed cert and there is no other party who trusts this certificate.

If I remove

rejectUnauthorized: true,

as well, the error is gone - but connection.authorized is equal to false which effectively means that my connection is not encrypted. Anyway, using getPeerCertificate() I can access the certificate sent by the server. As I want to enforce an encrypted connection, I understand that I may not remove this line.

Now I read that I can use the ca property to specify any CA that I want Node.js to trust. The documentation of the TLS module implies that it's enough to add the server certificate to the ca array, and then everything should be fine.

If I do that, this error is gone, but I get a new one:

Hostname/IP doesn't match certificate's altnames

To me this means that the CA is now basically trusted, hence that's okay now, but the certificate was made for another host than the one I use.

I created the certificate using

$ openssl genrsa -out server-key.pem 2048
$ openssl req -new -key server-key.pem -out server-csr.pem
$ openssl x509 -req -in server-csr.pem -signkey server-key.pem -out server-cert.pem

as the documentation implies. When creating the CSR I am asked the usual questions, such as for country, state, ... and common name (CN). As you are told "on the web" for an SSL certificate you do not provide your name as CN, but the host name you would like to use.

And this is probably where I fail.

I tried

  • localhost
  • 192.168.178.31
  • eisbaer
  • eisbaer.fritz.box

where the last two are the local name and the fully qualified local name of my machine.

Any idea what I am doing wrong here?

Was it helpful?

Solution

Recently there was an addition to node.js which allows overriding hostname check with a custom function. It was added to v0.11.14 and will be available in the next stable release (0.12). Now you can do something like:

var options = {
  host: '192.168.178.31',
  port: 3000,
  ca: [ fs.readFileSync('server-cert.pem') ],
  checkServerIdentity: function (host, cert) {
    return undefined;
  }
};
options.agent = new https.Agent(options);
var req = https.request(options, function (res) {
  //...
});

This will now accept any server identity, but still encrypt the connection and verify keys.

Note that in previous versions (e.g. v0.11.14), the checkServerIdentity was to return a boolean indicating the validity of the server. That has been changed (before v4.3.1) to the function returning (not throwing) an error if there is a problem and undefined if there is it's valid.

OTHER TIPS

In tls.js, lines 112-141, you can see that if the host name used when calling connect is an IP address, the certificate's CN is ignored and only the SANs are being used.

As my certificate doesn't use SANs, verification fails.

If you're using a host name to connect, the host name will be checked against the Subject Alternative Names of DNS type, if any, and fall back on the CN in the Subject Distinguished Name otherwise.

If you're using an IP address to connect, the IP address will be be checked against the SANs of IP address type, without falling back on the CN.

This is at least what implementations compliant with the HTTP over TLS specification (i.e. HTTPS) do. Some browser are a bit more tolerant.

This is exactly the same problem as in this answer in Java, which also gives a method to put custom SANs via OpenSSL (see this document too).

Generally speaking, unless it's for a test CA, it's quite hard to manage certificates that rely on IP addresses. Connecting with a host name is better.

Mitar had a wrong assumption that checkServerIdentity should return 'true' at success, but actually it should return 'undefined' at success. Any other values are treated as error descriptions.

So such a code is correct:

var options = {
  host: '192.168.178.31',
  port: 3000,
  ca: [ fs.readFileSync('server-cert.pem') ],
  checkServerIdentity: function (host, cert) {
    // It can be useful to resolve both parts to IP or to Hostname (with some synchronous resolver (I wander why they did not add done() callback as the third parameter)).
    // Be carefull with SNI (when many names are bound to the same IP).
    if (host != cert.subject.CN)
      return 'Incorrect server identity';// Return error in case of failed checking.
      // Return undefined value in case of successful checking.
      // I.e. you could use empty function body to accept all CN's.
  }
};
options.agent = new https.Agent(options);
var req = https.request(options, function (res) {
  //...
});

I tried just to make edit in the Mitar's answer but the edit was rejected, so I created a separated answer.

What you're doing wrong is using an IP address instead of a domain name. Create a domain name and stick it in a DNS server (or just in a hosts file), create a self-signed certificate with the domain name as the Common Name, and connect to the domain name rather than the IP address.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top