28

I'm using node 0.10.26 and trying to establish https connection with client validation.

Server's code:

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

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

var options = {
    key: fs.readFileSync('ssl/server1.key'),
    cert: fs.readFileSync('ssl/server1.pem'),
    requestCert: true,
    rejectUnauthorized: false,
};

var server = https.createServer(options, function (req, res) {
    if (req.client.authorized) {
        res.writeHead(200, {"Content-Type":"application/json"});
        res.end('{"status":"approved"}');
        console.log("Approved Client ", req.client.socket.remoteAddress);
    } else {
        console.log("res.connection.authroizationError:  " + res.connection.authorizationError);
        res.writeHead(403, {"Content-Type":"application/json"});
        res.end('{"status":"denied"}');
        console.log("Denied Client " , req.client.socket.remoteAddress);
    }
});

server.on('error', function(err) {
    console.log("server.error: "  + err);
});

server.on("listening", function () {
    console.log("Server listeining");
});

server.listen(5678);

Client's code:

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

var options = {
    host: 'localhost',
    port: 5678,
    method: 'GET',
    path: '/',
    headers: {},
    agent: false,
    key: fs.readFileSync('ssl/client2.key'),
    cert: fs.readFileSync('ssl/client2.pem'),
    ca: fs.readFileSync('ssl/ca.pem')
};

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

var req = https.request(options, function(res) {
    console.log(res.req.connection.authorizationError);
});

req.on("error", function (err) {
    console.log('error: ' + err);
});

req.end();

I've created certificates with following commands, each time providing result of "uname -n" as "Common Name":

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -days 999 -out ca.pem

openssl genrsa -out server1.key  1024
openssl req -new -key server1.key -out server1.csr
openssl x509 -req -days 999 -in server1.csr -CA ca.pem  -CAkey ca.key -set_serial 01 -out server1.pem

openssl genrsa  -out client1.key 1024
openssl req -new -key client1.key  -out client1.csr
openssl  x509  -req -days 999 -in client1.csr  -CA ca.pem  -CAkey ca.key  -set_serial 01     -out client1.pem

openssl genrsa  -out server2.key 1024
openssl req -new -key server2.key  -out server2.csr
openssl  x509  -req -days 999 -in server2.csr -CA server1.pem -CAkey server1.key -     set_serial 02 -out server2.pem

openssl  genrsa -out client2.key 1024
openssl req -new -key client2.key -out client2.csr
openssl x509 -req -days 999 -in client2.csr -CA client1.pem -CAkey client1.key  -set_serial 02 -out client2.pem

I've run client and server with all compbinations of client's and server's certificates (that is: [(server1, client1), (server1, client2), (server2, client1), (server2, client2)] and for each combination of those server was tested both with default value of "agent" field and with "agent" set to "false".

Each time I ran client.js, res.req.connection.authorizationError was set to DEPTH_ZERO_SELF_SIGNED_CERT.

How can I establish secure connection in node with client's certificate authentication?

Marek
  • 439
  • 2
  • 5
  • 12

8 Answers8

32

I believe you have two problems, one with your code and one with your certificates.

The code issue is in your server. You are not specifying the CA to check client certificates with an options property like you have in your client code:

ca: fs.readFileSync('ssl/ca.pem'),

The second problem is the one that really causes that DEPTH_ZERO_SELF_SIGNED_CERT error. You are giving all your certificates - CA, server, and client - the same Distinguished Name. When the server extracts the issuer information from the client certificate, it sees that the issuer DN is the same as the client certificate DN and concludes that the client certificate is self-signed.

Try regenerating your certificates, giving each one a unique Common Name (to make the DN also unique). For example, name your CA certificate "Foo CA", your server certificate the name of your host ("localhost" in this case), and your client something else (e.g. "Foo Client 1").

rhashimoto
  • 15,650
  • 2
  • 52
  • 80
  • 2
    You sir, are a genius! I was having the exact problem that my CA had same CN as the server certificate. I didn't realize it would be an issue, but now that I read your answer I feel like a big dummy. – joonas.fi Jul 12 '14 at 20:54
  • 2
    You are a life saver. We ran into this with Kibana. All it gave was the error `Request error, retrying HEAD https://:9200/ => self signed certificate`. We tried *everything* and finally had to bust out the Kibana source code and add printlns. Eventually, one println turned up the `DEPTH_ZERO_SELF_SIGNED_CERT` and your solution helped us figure out the cause: we had generated our CA and used it to sign a cert and both had the same CN (Common Name). We would've never figured that out without this post. Thank you! – Yevgeniy Brikman Jul 05 '18 at 17:37
  • You point on `DEPTH_ZERO_SELF_SIGNED_CERT` makes very good sense. I reran all both server and client openssl with totally diff DN [here](https://serverfault.com/q/1085340/852810), still the same, appreciate it if you can provide some insight. – Jeb50 Dec 03 '21 at 22:49
  • This is crazy! Why assume that two certificates with the same CN are the same issuer? Especially when you can see that the certificate that signed it has a completely different signature... – Aeolun Oct 05 '22 at 05:33
  • @Aeolun It's not totally crazy. The server gets a client certificate to verify. The client certificate has a DN and a signature that it says is by the same DN. The DN is supposed to be unique so it's reasonable for the server to conclude the certificate is self-signed. Granted, the server could attempt to verify the "self-signed" signature and fail but it's already raising an error so doing that extra work would only produce a more correct error. – rhashimoto Oct 05 '22 at 16:18
21

For those of that want to use a self-signed certificate, the answer is to add rejectUnauthorized: false to the https.request options.

Nick Hingston
  • 8,724
  • 3
  • 50
  • 59
B T
  • 57,525
  • 34
  • 189
  • 207
  • 2
    downvote because it is not secured and because the previous answer give a reason to the problem – Hesham Yassin Nov 04 '16 at 15:20
  • 12
    @HeshamYassin And if you're working in a dev environment with the server on your own machine, you literally *can't* get a certificate, so you need to use this solution. So think before you downvote please. – B T Nov 05 '16 at 22:30
  • 1
    good point, you still can always work with self signed certificate: the next three commands will create a certificate and a key. believe me, from bad experience, not working with certificates on dev, you will suffer in prod. here are the commands: openssl genrsa -out my_key.key 2048 openssl req -new -key my_key.key -out my_request.csr openssl x509 -req -days 3650 -in my_request.csr -signkey my_key.key -out my_cert.crt – Hesham Yassin Nov 07 '16 at 07:36
  • 3
    But a self-signed cert will still fail unless you use `rejectUnauthorized: false` – B T Nov 08 '16 at 10:38
13

This one worked for me:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

Note: Posting answer so it can help others in future.

Manwal
  • 23,450
  • 12
  • 63
  • 93
3

Despite of long lines of description in this page, I still got 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' error in the client side with this recipe. Maybe I had something wrong in following rhashimoto's comment.

By referencing book of 'Professional Node.js', I found another way to test HTTPS with Client Certificate Verification successfully.
Here is my story.
By setting requestCert: true in server side, the server tries to validate client certificate. But the default CA doesn't validate the self-signed certificate of the client. I can succeed the test with simple trick -- copy the client certificate and say that is a Certificate Authority.

I reused original code and modified it slightly to make it work. The big difference is in creating certificate files.

Creating certificate files:

# create client private key
openssl genrsa -out client2.key
openssl req -new -key client2.key -out client2.csr
# create client certificate
openssl x509 -req -in client2.csr -signkey client2.key -out client2.pem

# create server private key and certificate
openssl genrsa -out server1.key
openssl req -new -key server1.key -out server1.csr
openssl x509 -req -in server1.csr -signkey server1.key -out server1.pem

# * Important *: create fake CA with client certificate for test purpose
cp client2.pem fake_ca.pem

Server code:

var options = {
    key: fs.readFileSync('ssl/server1.key'),
    cert: fs.readFileSync('ssl/server1.pem'),
    ca: [fs.readFileSync('ssl/fake_ca.pem')], // Line added
    requestCert: true,
    rejectUnauthorized: false,
};

Client code:

    key: fs.readFileSync('ssl/client2.key'),
    cert: fs.readFileSync('ssl/client2.pem'),
    //ca: fs.readFileSync('ssl/ca.pem') // Line commented
};

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var req = https.request(options, function(res) {
    //console.log(res.req.connection.authorizationError);
    console.log("statusCode:", res.statusCode);
    res.on('data', function(d) {
      console.log('data:', d.toString());
    });
});
req.on("error", function (err) {
    console.log('error: ' + err);
});
req.end();
Community
  • 1
  • 1
Hill
  • 3,391
  • 3
  • 24
  • 28
  • If you have a leaf node error, it's because you haven't included the intermediate certificate with the server certificate. You must concatenate the cert with all intermediate certs in the chain. – Bradley Kreider May 03 '17 at 16:53
1

Just add strictSSL: false to your options.

taco
  • 1,367
  • 17
  • 32
0

As mentioned above, there is a sledgehammer to hammer in your nail, using rejectUnauthorized: false.

A more sensible option, from a security point of view, would be to ask the user if (s)he would like to accept and store the self-signed server certificate, just like a browser (or SSH) does.

That would require:

(1) That NodeJS throws an exception that contains the server certificate, and

(2) that the application calls https.request with the stored certificate in the ca: property (see above for description of ca) after the certificate has been manually accepted.

It seems that NodeJS does not do (1), making (2) impossible?

Even better from a security point of view would be to use EFF's SSL observatory to make a crowd-sourced judgement on the validity of a self-signed certificate. Again, that requires NodeJS to do (1).

I think a developer needs to improve NodeJS with respect to (1)...

Jim Shark
  • 1
  • 2
  • In fact, you can't event get to the server certificate if you connect with rejectUnauthorized: false. You can util.inspect() all the way down on the request, response, connection etc. objects and you'll never get as much as a glimpse at the certificate that NodeJS received. For now, you'll just have to either accept all certificates or CA-signed certificates only via your code. – Jim Shark Feb 07 '15 at 13:18
0

If you have only a .pem self-signed certificate (e.g. /tmp/ca-keyAndCert.pem) the following options will work:

var options = {
      url: 'https://<MYHOST>:<MY_PORT>/',
      key: fs.readFileSync('/tmp/ca-keyAndCert.pem'),
      cert: fs.readFileSync('/tmp/ca-keyAndCert.pem'),
      requestCert: false,
      rejectUnauthorized: false
};
AR1
  • 4,507
  • 4
  • 26
  • 42
0

I just ran into the DEPTH_ZERO_SELF_SIGNED_CERT error too. But in my case, the service is deployed by docker and I'm very unfamiliar with the source code. Thus it would be annoying to add an option rejectUnauthorized: false in the code inside the docker.

After googling for a while, I find a more friendly solution to me in this post: set the option as an environment variable

NODE_TLS_REJECT_UNAUTHORIZED=0
Vvvvvv
  • 136
  • 7