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.
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.
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)"
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 ?
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.