Skip to content

Instantly share code, notes, and snippets.

@pcan
Last active April 17, 2024 01:45
Show Gist options
  • Star 56 You must be signed in to star a gist
  • Fork 26 You must be signed in to fork a gist
  • Save pcan/e384fcad2a83e3ce20f9a4c33f4a13ae to your computer and use it in GitHub Desktop.
Save pcan/e384fcad2a83e3ce20f9a4c33f4a13ae to your computer and use it in GitHub Desktop.
Node.js plain TLS Client & Server, 2-way Cert Auth

Node.js TLS plain TLS sockets

This guide shows how to set up a bidirectional client/server authentication for plain TLS sockets.

Newer versions of openssl are stricter about certificate purposes. Use extensions accordingly.

Prepare certificates

Generate a Certificate Authority:

openssl req -new -x509 -days 9999 -keyout ca-key.pem -out ca-crt.pem
  • Insert a CA Password
  • Specify a CA Common Name, like 'root.localhost' or 'ca.localhost'. This MUST be different from both server and client CN.

Server certificate

Generate Server Key:

openssl genrsa -out server-key.pem 4096

Generate Server certificate signing request:

openssl req -new -key server-key.pem -out server-csr.pem
  • Specify server Common Name, like 'localhost' or 'server.localhost'. The client will verify this, so make sure you have a vaild DNS name for this.
  • For this example, do not insert the challenge password.

Sign certificate using the CA:

openssl x509 -req -days 9999 -in server-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out server-crt.pem
  • insert CA Password

Verify server certificate:

openssl verify -CAfile ca-crt.pem server-crt.pem

Client certificate

Generate Client Key:

openssl genrsa -out client1-key.pem 4096

Generate Client certificate signing request:

openssl req -new -key client1-key.pem -out client1-csr.pem
  • Specify client Common Name, like 'client.localhost'. Server should not verify this, since it should not do reverse-dns lookup.
  • For this example, do not insert the challenge password.

Sign certificate using the CA:

openssl x509 -req -days 9999 -in client1-csr.pem -CA ca-crt.pem -CAkey ca-key.pem -CAcreateserial -out client1-crt.pem
  • insert CA Password

Verify client certificate:

openssl verify -CAfile ca-crt.pem client1-crt.pem

Server code

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

const options = { 
    key: fs.readFileSync('server-key.pem'), 
    cert: fs.readFileSync('server-crt.pem'), 
    ca: fs.readFileSync('ca-crt.pem'), 
    requestCert: true, 
    rejectUnauthorized: true
}; 

const server = tls.createServer(options, (socket) => {
    console.log('server connected', 
        socket.authorized ? 'authorized' : 'unauthorized');
    
    socket.on('error', (error) => {
        console.log(error);
    });
    
    socket.write('welcome!\n');
    socket.setEncoding('utf8');
    socket.pipe(process.stdout);
    socket.pipe(socket);
});

server.listen(8000, () => {
    console.log('server bound');
});

Client code

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

const options = {
    ca: fs.readFileSync('ca-crt.pem'),
    key: fs.readFileSync('client1-key.pem'),
    cert: fs.readFileSync('client1-crt.pem'),
    host: 'server.localhost',
    port: 8000,
    rejectUnauthorized:true,
    requestCert:true
};

const socket = tls.connect(options, () => {
    console.log('client connected', 
        socket.authorized ? 'authorized' : 'unauthorized');
    process.stdin.pipe(socket);
    process.stdin.resume();
});

socket.setEncoding('utf8');

socket.on('data', (data) => {
    console.log(data);
});

socket.on('error', (error) => {
    console.log(error);
});

socket.on('end', (data) => {
    console.log('Socket end event');
});

Credits

See the original post by Anders Brownworth.

Thanks to this StackOverflow answer, too (I was using same CN for CA, Server and Client and I got the DEPTH_ZERO_SELF_SIGNED_CERT error).

@mihir83in
Copy link

Hey there, when doing a secure mutual auth, I think its fine to assert client's identity through a reverse-dns lookup at server end. I came across your post and I wonder is there a specific reason you've voted against reverse-dns ?

Specify client Common Name, like 'client.localhost'. Server should not verify this, since it should not do reverse-dns lookup.

@pcan
Copy link
Author

pcan commented Nov 20, 2019

I wonder is there a specific reason you've voted against reverse-dns

Hi @mihir83in, it's just a general assumption, based on a common client-server internet-based scenario. When a server is publicly available over the internet, usually it has a DNS name to make it more accessible. A "client" in this sense, may be just a PC or a smartphone with a dynamic IP address, and this is often resolved to a dynamic host name through a reverse DNS lookup query. In this use case, there is no way to assess the client identity based on its (dynamic) name.
In a server-to-server scenario, a reverse DNS lookup may be reasonable, since both servers may have a fixed dns name.

@mihir83in
Copy link

@pcan, Thanks for your explanation. Totally agree with you on that one. 👍

@jimmyrleung
Copy link

jimmyrleung commented May 18, 2020

Hello there, I'm working in my first project with two-way SSL, so I would like to know if you could help me with the following question (sorry if it's too newbie):

Our customer requires the certificate to be signed by one of the following trusted CA: DigiCert, Entrust or GeoTrust. So, considering the first step that you have described, can I assume that the files ca-key.pem and ca-crt.pem will be provided by the chosen trusted CA?

Thanks in advance!

@pcan
Copy link
Author

pcan commented May 18, 2020

Hi @jimmyrleung, thanks for your question. The only way to achieve mutual TLS authentication with publicly-recognized certificates, is to ask DigiCert, Entrust or GeoTrust to sign your "private" CA. In other words you need a "child" CA that you can use to issue client & server certificates. In this way, your CA would enter into an official "certification chain" that can be publicly verified.

@jimmyrleung
Copy link

Hi @pcan, thanks for replying quickly!

Oh, I see, so I have to follow these steps to generate all the necessary certificate files and then submit my CSR to DigiCert, Entrust or GeoTrust, right?

Thanks!

@pcan
Copy link
Author

pcan commented May 18, 2020

Hi @jimmyrleung, you're welcome. Actually I never had the opportunity to follow this procedure, but maybe the CSR request needs some additional steps that I'm missing in this gist. I don't know if the "standard" certificate signing procedure is the same of a custom CA certificate signing (this is a very specific case), but it should be feasible. I think it's just a matter of cost: if you own a private CA, in theory you should be able to issue an unlimited number of (valid) certificates, thus "skipping" the cost for each one. Maybe the official DigiCert/Entrust/GeoTrust support can help you better on this point :)

@jimmyrleung
Copy link

Hey @pcan, thanks again!

To be honest that's very new for me, we usually have to authenticate with this customer using two-way SSL, but always as a client. Now we need to do the opposite, so I'm really learning a lot and your guide is the one that helped me the most to understand the flow from the server side perspective. I'll contact both the customer's tech team and DigiCert/Entrust/GeoTrust to see if they can help me with these questions.

Many thanks again,

Jimmy.

@zhamidi
Copy link

zhamidi commented Oct 7, 2020

Hey there! Thank you for this solution!
I was wondering however what could be used instead of fs and require on the client side since those two things can't be used in the browser.
If you could help me that would be amazing!

@pcan
Copy link
Author

pcan commented Oct 7, 2020

Actually I don't think you can use TLS socket at all in the browser, while the fs thing is the minor issue (I was just using it to read the PEM files content, that are just plaintext files). Just curious, what is your use case here?

@zhamidi
Copy link

zhamidi commented Oct 12, 2020

I wanted to use it for a secure key exchange, to mostly authenticate the client. Hence why wanted to use that on the browser side.

@orefalo
Copy link

orefalo commented May 16, 2022

Lost, your description says "This MUST be different from both server and client CN." therefore two ca(s), one for each - the client and the service.

Then in your scripts, you only use one

@pcan
Copy link
Author

pcan commented May 16, 2022

@orefalo where exactly in the scripts? I was referring to generation not usage (you never need to reference the CA name anywhere in the code). Please advise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment