2

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 ?

Jordan Breton
  • 1,167
  • 10
  • 17
  • (0) You're right the system (ca-certificates) store is irrelevant (1) I don't do odd majors and my Ubuntu 20.04 (with standard repos) can only find 10.19, but testing with 14.15.5 on Windows `NODE_EXTRA_CA_CERTS` works for me, assuming the file is good, which it should be if created by OpenSSL and not modified. Can you check `openssl x509 -in file` displays the cert with no error? (2) `tls.rootCertificates` gives you a copy of the builtin list only, not the actual X509_STORE that contains the adds, and patching it has no useful effect at all. ... – dave_thompson_085 Aug 23 '21 at 23:07
  • ... (3) Reading source I think `addCACert` also _should_ work, although you don't need the match+forEach (despite the singular name it actually accepts a sequence, but silently ignores invalid input) and you don't need to remove CRs (or 'comment' info) – dave_thompson_085 Aug 23 '21 at 23:08
  • Thanks for your answers. `openssl x509 -in file` printed the cert without error. I regenerated all my certificates and tried again. Still not working. 2 -> that's what I suspected... 3 : I tried to add the cert without mathing/changing the file content : not working. I also tried to add the CA at fetch level with the `ca` param : always the same error. I'll try the last version of node to see if it's a bug of the v15.12.0. – Jordan Breton Aug 24 '21 at 07:09

1 Answers1

2

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.

Jordan Breton
  • 1,167
  • 10
  • 17
  • do you change NODE_EXTRA_CA_CERTS before startup? Docs say: > The NODE_EXTRA_CA_CERTS environment variable is only read when the Node.js process is first launched. Changing the value at runtime using process.env.NODE_EXTRA_CA_CERTS has no effect on the current process. https://nodejs.org/docs/latest-v14.x/api/cli.html#cli_node_extra_ca_certs_file – mmmm Nov 23 '22 at 21:51
  • @mmmm I was altering NODE_EXTRA_CA_CERTS in my bash environment through the commandline. But I suspect something altered the environment variable in my bash environment before node call, since I'm still unable to reproduce the issue with the same version of node in a similar environment. – Jordan Breton Nov 24 '22 at 06:45
  • OK, good luck to you. Sorry I cannot be of help with this – mmmm Nov 25 '22 at 14:38