How to add custom certificate authority (CA) to nodejs

node.jsNpm

node.js Problem Overview


I'm using a CLI tool to build hybrid mobile apps which has a cool upload feature so I can test the app on a device without going through the app store (it's ionic-cli). However, in my company like so many other companies TLS requests are re-signed with the company's own custom CA certificate which I have on my machine in the keychain (OS X). However, nodejs does not use the keychain to get its list of CA's to trust. I don't control the ionic-cli app so I can't simply pass in a { ca: } property to the https module. I could also see this being a problem for any node app which I do not control. Is it possible to tell nodejs to trust a CA?

I wasn't sure if this belonged in Information Security or any of the other exchanges...

node.js Solutions


Solution 1 - node.js

Node.js 7.3.0 (and the LTS versions 6.10.0 and 4.8.0) added NODE_EXTRA_CA_CERTS environment variable for you to pass the CA certificate file. It will be safer than disabling certificate verification using NODE_TLS_REJECT_UNAUTHORIZED.

$ export NODE_EXTRA_CA_CERTS=[your CA certificate file path]

Solution 2 - node.js

I'm aware of two npm modules that handle this problem when you control the app:

  1. https://github.com/fujifish/syswide-cas (I'm the author of this one)
  2. https://github.com/coolaj86/node-ssl-root-cas

node-ssl-root-cas bundles it's own copies of nodes root CAs and also enables adding your own CAs to trust. It places the certs on the https global agent, so it will only be used for https module, not pure tls connections. Also, you will need extra steps if you use a custom Agent instead of the global agent.

syswide-cas loads certificates from pre-defined directories (such as /etc/ssl/certs) and uses node internal API to add them to the trusted list of CAs in conjunction to the bundled root CAs. There is no need to use the ca option since it makes the change globally which affects all later TLS calls automatically. It's also possible to add CAs from other directories/files if needed. It was verified to work with node 0.10, node 5 and node 6.

Since you do not control the app you can create a wrapper script to enable syswide-cas (or node-ssl-root-cas) and then require the ionic-cli script:

require('syswide-cas'); // this adds your custom CAs in addition to bundled CAs
require('./path/to/real/script'); // this runs the actual script

Solution 3 - node.js

There is an undocumented, seemingly stable, API for appending a certificate to the default list:

const tls = require('tls');

const secureContext = tls.createSecureContext();

// https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt
secureContext.context.addCACert(`-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----`);

const sock = tls.connect(443, 'host', { secureContext });

For more information, checkout the open issue on the subject: https://github.com/nodejs/node/issues/27079

Solution 4 - node.js

You can specify a command line option to tell node to use the system CA store:

node --use-openssl-ca

Alternatively this can be specified as an environment variable, if you are not running the node CLI directly yourself:

NODE_OPTIONS=--use-openssl-ca

Solution 5 - node.js

This is not currently possible unless you compile a custom version of nodejs with custom CA certs. Hard-baked CA certs is a current limitation of nodejs until someone submits a PR and it's merged. It's a problem for others as well.

Below I have some copies of workarounds which might help some people but probably not the OP.

As far as I know OP can:

  • Custom compile nodejs
  • submit a PR for nodejs to fix the issue
  • file an issue or PR with ionic-cli to support custom CA certs: <https://github.com/driftyco/ionic-cli> (as suggested by @Nate)
  • Force less security (no TLS or silence verification also suggested by @Nate)

Others, if you control the nodejs app in question you have more options. You can of course specify the ca cert in each request. Some clever people have shared some workarounds in the github issue <https://github.com/nodejs/node/issues/4175>;. I haven't tried any of these myself yet so no promises, I'm just sharing what I've read.

DuBistKomisch explains how to get nodejs to use the operating system's CA certs: > My workaround is to load and parse the system CA certs manually. Then, > as recommended by the request docs, pass them in with the ca option > everywhere we make a request. I presume you could also just set ca on > the global agent if that works for your use case. > > fs.readFileSync('/etc/ssl/certs/ca-certificates.crt') > .toString() > .split(/-----END CERTIFICATE-----\n?/) > // may include an extra empty string at the end > .filter(function (cert) { return cert !== ''; }) > // effectively split after delimiter by adding it back > .map(function (cert) { return cert + '-----END CERTIFICATE-----\n'; })

mwain explains how to set the CA certs globally and not on each https request: > Had similar issues with this, have internal apps using an internally > signed cert. Opted to use https.globalAgent and set an array of CA's > which are defined in a config and updated on an env basis. > > > const trustedCa = [ > '/etc/pki/tls/certs/ca-bundle.crt', > '/path/to/custom/cert.crt' > ]; >
> https.globalAgent.options.ca = []; > for (const ca of trustedCa) { > https.globalAgent.options.ca.push(fs.readFileSync(ca)); > }

Solution 6 - node.js

This answer is more focused towards package maintainers/builders.

One can use this method if you do not want end users to rely on additional environment variables.

When nodejs is built from source, it (by default, can be overridden) embeds the Mozilla CA certificate database into the binary itself. One can add more certificates to this database using the following commands:

# Convert your PEM certificate to DER
openssl x509 -in /path/to/your/CA.pem -outform der -out CA.der

# Add converted certificate to certdata
nss-addbuiltin -n "MyCompany-CA" -t "CT,C,C" < CA.der >> tools/certdata.txt

# Regenerate src/node_root_certs.h header file
perl tools/mk-ca-bundle.pl

# Finally, compile
make install

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMike HaasView Question on Stackoverflow
Solution 1 - node.jsCheng-Lin TsaoView Answer on Stackoverflow
Solution 2 - node.jsfujifishView Answer on Stackoverflow
Solution 3 - node.jsCameron TacklindView Answer on Stackoverflow
Solution 4 - node.jsJimbaliView Answer on Stackoverflow
Solution 5 - node.jschrishiestandView Answer on Stackoverflow
Solution 6 - node.jsNehal J WaniView Answer on Stackoverflow