60

I have started writing a wrapper for an API which requires all requests to be over HTTPS. Instead of making requests to the actual API while I am developing and testing it I would like to run my own server locally which mocks the responses.

I am confused about how to generate the certificates I need to create a HTTPS server and send requests to it.

My server looks something like this:

var options = {
  key: fs.readFileSync('./key.pem'),
  cert: fs.readFileSync('./cert.pem')
};

https.createServer(options, function(req, res) {
  res.writeHead(200);
  res.end('OK\n');
}).listen(8000);

The pem files were generated with:

openssl genrsa 1024 > key.pem
openssl req -x509 -new -key key.pem > cert.pem

And a request looks something like this:

var options = {
  host: 'localhost',
  port: 8000,
  path: '/api/v1/test'
};

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();

With this setup I get Error: DEPTH_ZERO_SELF_SIGNED_CERT, so I think I need to add a ca option for the request.

So my question is how should I generate the following:

  1. The server key?
  2. The server cert?
  3. The ca for the request?

I have read a few things about generating self signed certificates with openssl, but can't seem to wrap my head around it and figure out which keys and certificates to use where in my node code.

Update

The API provides a CA certificate to use instead of the defaults. The following code works using their certificate and this is what I want to reproduce locally.

var ca = fs.readFileSync('./certificate.pem');

var options = {
  host: 'example.com',
  path: '/api/v1/test',
  ca: ca
};
options.agent = new https.Agent(options);

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();
Brett
  • 3,825
  • 3
  • 24
  • 27

6 Answers6

69

Update (Nov 2018): Do you need self-signed certs?

Or would real certificates get the job done better? Have you considered any of these?

(Note: Let's Encrypt can also issue certificates to private networks)

ScreenCast

https://coolaj86.com/articles/how-to-create-a-csr-for-https-tls-ssl-rsa-pems/

Full, Working example

  • creates certificates
  • runs node.js server
  • no warnings or errors in node.js client
  • no warnings or errors in cURL

https://github.com/coolaj86/nodejs-self-signed-certificate-example

Using localhost.greenlock.domains as an example (it points to 127.0.0.1):

server.js

'use strict';

var https = require('https')
  , port = process.argv[2] || 8043
  , fs = require('fs')
  , path = require('path')
  , server
  , options
  ;

require('ssl-root-cas')
  .inject()
  .addFile(path.join(__dirname, 'server', 'my-private-root-ca.cert.pem'))
  ;

options = {
  // this is ONLY the PRIVATE KEY
  key: fs.readFileSync(path.join(__dirname, 'server', 'privkey.pem'))
  // You DO NOT specify `ca`, that's only for peer authentication
//, ca: [ fs.readFileSync(path.join(__dirname, 'server', 'my-private-root-ca.cert.pem'))]
  // This should contain both cert.pem AND chain.pem (in that order) 
, cert: fs.readFileSync(path.join(__dirname, 'server', 'fullchain.pem'))
};


function app(req, res) {
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, encrypted world!');
}

server = https.createServer(options, app).listen(port, function () {
  port = server.address().port;
  console.log('Listening on https://127.0.0.1:' + port);
  console.log('Listening on https://' + server.address().address + ':' + port);
  console.log('Listening on https://localhost.greenlock.domains:' + port);
});

client.js

'use strict';

var https = require('https')
  , fs = require('fs')
  , path = require('path')
  , ca = fs.readFileSync(path.join(__dirname, 'client', 'my-private-root-ca.cert.pem'))
  , port = process.argv[2] || 8043
  , hostname = process.argv[3] || 'localhost.greenlock.domains'
  ;

var options = {
  host: hostname
, port: port
, path: '/'
, ca: ca
};
options.agent = new https.Agent(options);

https.request(options, function(res) {
  res.pipe(process.stdout);
}).end();

And the script that makes the certificate files:

make-certs.sh

#!/bin/bash
FQDN=$1

# make directories to work from
mkdir -p server/ client/ all/

# Create your very own Root Certificate Authority
openssl genrsa \
  -out all/my-private-root-ca.privkey.pem \
  2048

# Self-sign your Root Certificate Authority
# Since this is private, the details can be as bogus as you like
openssl req \
  -x509 \
  -new \
  -nodes \
  -key all/my-private-root-ca.privkey.pem \
  -days 1024 \
  -out all/my-private-root-ca.cert.pem \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.com"

# Create a Device Certificate for each domain,
# such as example.com, *.example.com, awesome.example.com
# NOTE: You MUST match CN to the domain name or ip address you want to use
openssl genrsa \
  -out all/privkey.pem \
  2048

# Create a request from your Device, which your Root CA will sign
openssl req -new \
  -key all/privkey.pem \
  -out all/csr.pem \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Tech Inc/CN=${FQDN}"

# Sign the request from Device with your Root CA
openssl x509 \
  -req -in all/csr.pem \
  -CA all/my-private-root-ca.cert.pem \
  -CAkey all/my-private-root-ca.privkey.pem \
  -CAcreateserial \
  -out all/cert.pem \
  -days 500

# Put things in their proper place
rsync -a all/{privkey,cert}.pem server/
cat all/cert.pem > server/fullchain.pem         # we have no intermediates in this case
rsync -a all/my-private-root-ca.cert.pem server/
rsync -a all/my-private-root-ca.cert.pem client/

# create DER format crt for iOS Mobile Safari, etc
openssl x509 -outform der -in all/my-private-root-ca.cert.pem -out client/my-private-root-ca.crt

For example:

bash make-certs.sh 'localhost.greenlock.domains'

Hopefully this puts the nail in the coffin on this one.

And some more explanation: https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless-Self-Signed-Certificates-in-node.js

Install private cert on iOS Mobile Safari

You need to create a copy of the root ca certificate a DER format with a .crt extension:

# create DER format crt for iOS Mobile Safari, etc
openssl x509 -outform der -in all/my-private-root-ca.cert.pem -out client/my-private-root-ca.crt

Then you can simply serve that file with your webserver. When you click the link you should be asked if you want to install the certificate.

For an example of how this works you can try installing MIT's Certificate Authority: https://ca.mit.edu/mitca.crt

Related Examples

coolaj86
  • 74,004
  • 20
  • 105
  • 125
  • Sadly this doesn't seem to be enough. Add in websockets then try to access from Safari. Even if I serve the cert I'll get a message asking if I want to install the cert but after installing it secure websockets fail. Have you had any luck with self-signed certs + safari iOS? – gman Apr 24 '16 at 09:49
  • @gman: If you want to use it in safari you need to add your root ca. I updated the tutorial with an iOS section. – coolaj86 Apr 25 '16 at 19:08
  • If you need to get the CA pem for the server you are calling use this command (its the last certificate in the output) ```openssl s_client -showcerts -connect www.example.com:443 – John Culviner Jul 25 '16 at 16:15
  • 1
    Your answer is worth an article, extremely useful! – anni Oct 29 '19 at 14:13
  • @coolaj86 you mentioned "Let's Encrypt can also issue certificates to private networks" - do you have a link to any relevant documentation here? – Gershom Maes Oct 27 '22 at 22:45
  • @GershomMaes You just need to use DNS validation. As long as the DNS can be publicly validated it's fine for the IP address to be private. So you could have mylaptop.internal.company.com. – coolaj86 Oct 27 '22 at 23:27
12

Try adding this to your request options

var options = {
  host: 'localhost',
  port: 8000,
  path: '/api/v1/test',
  // These next three lines
  rejectUnauthorized: false,
  requestCert: true,
  agent: false
};
Loourr
  • 4,995
  • 10
  • 44
  • 68
3

Your key generation looks okay. You shouldn't need a ca because you aren't rejecting unsigned requests.

Add .toString() to the end of your readFileSync methods so that you are actually passing a string, not a file object.

binderbound
  • 791
  • 1
  • 7
  • 27
  • In recent version of Node the rejectUnauthorized option is set to true by default so the requests are being rejected. The toString is unnecessary because readFileSync returns a Buffer when no encoding is specified, and the ca, cert and key options accept a Buffer or String. – Brett Oct 30 '13 at 06:32
  • So does this mean you made reject unsigned requests false and it was working? – binderbound Oct 31 '13 at 00:13
  • Yes, I can do that but it's not what I want. The API I am using has provided me with a CA certificate that I can use for authorization, and I want to mirror this setup locally. – Brett Oct 31 '13 at 06:20
3

This procedure allows you to create both a certificate authority & a certificate :

  1. grab this ca.cnf file to use as a configuration shortcut :

    wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/ca.cnf


  1. create a new certificate authority using this configuration :

    openssl req -new -x509 -days 9999 -config ca.cnf -keyout ca-key.pem -out ca-cert.pem


  1. now that we have our certificate authority in ca-key.pem and ca-cert.pem, let's generate a private key for the server :

    openssl genrsa -out key.pem 4096


  1. grab this server.cnf file to use as a configuration shortcut :

    wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/server.cnf


  1. generate the certificate signing request using this configuration :

    openssl req -new -config server.cnf -key key.pem -out csr.pem


  1. sign the request :

    openssl x509 -req -extfile server.cnf -days 999 -passin "pass:password" -in csr.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem

I found this procedure here, along with more information on how to use these certificates.

John Slegers
  • 45,213
  • 22
  • 199
  • 169
2

Try adding

  agent: false,
  rejectUnauthorized: false
hkutluay
  • 6,794
  • 2
  • 33
  • 53
user1570577
  • 134
  • 1
  • 6
0

When you have the self-signed cert[s], you tell Node.js to use it with the Environment variable: NODE_EXTRA_CA_CERTS

Copy [cat] all the generated *.cert.pem files to a single file. I put it the directory with all the keys & certs:

> (cd $keys; cat *.cert.pem > node_extra_ca_certs)

Tell node where to find them:

> export NODE_EXTRA_CA_CERTS=$keys/node_extra_ca_certs

Now, when you run node, it will accept your private certs as valid.

Jack Punt
  • 342
  • 1
  • 14