Search code examples
node.jssslssl-certificateself-signed-certificate

How to properly configure node.js to use Self Signed root certificates?


So, on the road of desperation, I would want to know if someone, somewhere, can help me to configure nodejs to accept a root CA self signed. I need it in order to access a custom API in development via node-fetch with TLS.

Envrionment

  • OS : Ubuntu 20.04 as a guest in a VM. Windows 10 host.
  • Nodejs v15.12.0
  • Apache2.4 server

The API I'm working on is a PHP script that allow my nodejs backend to query some data.

The self signed root cert and API cert have been generated with openssl and are eprfectly fine, since I can query the API from the browser with HTTPS without any problem.

The error

When trying to query the API from the nodejs backend, I get this error :

FetchError: request to https://myapi.dev.local failed, reason: self signed certificate
    at ClientRequest.<anonymous> (./node_modules/node-fetch/lib/index.js:1461:11)
    at ClientRequest.emit (node:events:369:20)
    at TLSSocket.socketErrorListener (node:_http_client:462:9)
    at TLSSocket.emit (node:events:369:20)
    at emitErrorNT (node:internal/streams/destroy:188:8)
    at emitErrorCloseNT (node:internal/streams/destroy:153:3)
    at processTicksAndRejections (node:internal/process/task_queues:81:21)"

Tries & fails

First, I tried to install the cert on ubuntu with dpkg-reconfigure ca-certificates, but then I figured that nodejs use a hard coded list.

So, since I do not want to use the env variable NODE_TLS_REJECT_UNAUTHORIZED=0 for security sakes, I tried to use the NODE_EXTRA_CA_CERTS=pathToMycert.pem en variable, but it doesn't change anything and I can't find any info to know what's going on.

In my nodejs backend, if I do a console.log(process.env.NODE_EXTRA_CA_CERTS), it prints the good path.

I tried to match my CA against tls.rootCertificates whith this check :


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

const ca = await fs.readFileSync(process.env.NODE_EXTRA_CA_CERTS, 'utf8');
console.log(ca); //successfully print the CA, so it exists.
const inList = tls.rootCertificates.some( cert =>{
    console.log('testing ca : \n',cert);
    return cert == ca;
});
console.log(`CA is ${ !inList ? 'not' : '' } in rootCertificates list...`);

It prints 'CA is not in rootCertificates list'. Not a surprise.

So, I tried to monkeypatch the tls secureContext to include my certificate :


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

const origCreateSecureContext = tls.createSecureContext;

tls.createSecureContext = options => {
    const context = origCreateSecureContext(options);

    const list = (process.env.NODE_EXTRA_CA_CERTS || '').split(',');
    list.forEach(extraCert => {
        const pem = fs.readFileSync(extraCert, { encoding : 'utf8' }).replace(/\r\n/g, "\n");
        const certs = pem.match(/-----BEGIN CERTIFICATE-----\n[\s\S]+?\n-----END CERTIFICATE-----/g);
        if(!certs) throw new Error(
            `SelfSignedCertSupport : Invalid extra certificate ${extraCert}`
        );
        certs.forEach(cert => context.context.addCACert(cert.trim()));
    });

    return context;
};

Doesn't work.

And I tried (following this issue : https://github.com/nodejs/node/issues/27079) to do this :


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

const additionalCerts = [];
const list = (process.env.NODE_EXTRA_CA_CERTS || '').split(',');
list.forEach(extraCert => {
    const pem = fs.readFileSync(extraCert, { encoding : 'utf8' }).replace(/\r\n/g, "\n");
    const certs = pem.match(/-----BEGIN CERTIFICATE-----\n[\s\S]+?\n-----END CERTIFICATE-----/g);
    if(!certs) throw new Error(
        `SelfSignedCertSupport : Invalid extra certificate ${extraCert}`
    );
    additionalCerts.push(...certs);
});

tls.rootCertificates = [
    ...tls.rootCertificates,
    ...additionalCerts
];

Without any luck.

What am I doing wrong ?


Solution

  • I figured out what's going on. This was a conjunction of two problems.

    First, I generated my CA certificate and my other self signed certificates with the same CN. It's ok for all browsers and webservers, but not for node. For node, ensure that all your CN have different names (as described in this answer).

    The second problem is that the env var NODE_EXTRA_CA_CERTS is not working for somewhat reason in my environment. Trying to monkey patch as I tried works but is ugly, since addCACert is not a part of the public nodejs API. It should not be used. ​

    Since I use the fetch API that depends on the https package, I created a little module that I require at the top of my backend nodejs app :

    
    if(!process.env.NODE_EXTRA_CA_CERTS) return;
    
    const https = require('https');
    const tls = require('tls');
    const fs = require('fs');
    
    const list = (process.env.NODE_EXTRA_CA_CERTS || '').split(',');
    const additionalCerts = list.map(extraCert => fs.readFileSync(extraCert, 'utf8'));
    
    https.globalAgent.options.ca = [
        ...tls.rootCertificates,
        ...additionalCerts
    ];
    
    

    This way, all requests that use https and does not redefine the ca options will read the ca list from the globalAgent and you don't have to pollute your codebase with ca specific code. In my case, I didn't want my dev environment to produce code that I'll have to remove in production.

    So, now it works for me, even if I don't know what goiging on with the NODE_EXTRA_CA_CERTS env var that doesn't do its job.