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 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]
I'm aware of two npm modules that handle this problem when you control the app:
- https://github.com/capriza/syswide-cas (I'm the author of this one)
- 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
Edit: Please note that it is not safe to bypass the ca
option! It leaves you open to a person-in-the-middle attack.
Previous answer:
Since you don’t control the app and cannot pass the ca
option (the usual way to do it), you can try telling Node not to verify certificates at all, by setting the environment variable NODE_TLS_REJECT_UNAUTHORIZED
. For example:
$ export NODE_TLS_REJECT_UNAUTHORIZED=0
Then run the app.
As @keinabel says, when you do this the app will trust any (spoofed, fake, legitimate) certificate, opening you up to person-in-the-middle attacks. The correct and safe solution would be for the app developers to let you specify a trusted CA.
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));
}
There is an undocumented seemingly stable API for appending a certificate to 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});