In some cases, I've found that I've wanted to add a single CA to the list of trusted CAs that Node.js uses by default. There seems to be no documented way to do this. As it stands, officially, if you want to use non standard CAs, you can, but must also specify all CAs that might have otherwise been loaded automatically.
From the createSecureContext documentation:
ca <string> | <string[]> | <Buffer> | <Buffer[]>Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla. Mozilla's CAs are completely replaced when CAs are explicitly specified using this option. [...]
In trying to accomplish this, I've come across a seemingly stable but undocumented API https://github.com/nodejs/node/issues/20432#issuecomment-441514919
const tls = require('tls'); // Create context with default CAs from Mozilla const secureContext = tls.createSecureContext(); // Add a CA certificate from Let's Encrypt // 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-----`); // Use it const sock = tls.connect(443, 'host', {secureContext});
It looks to be a real API for a number of reasons:
_)Would it make sense to document this feature?
If the default list of CAs were accessible in node, we could do this ourselves without adding extra APIs. I have not actually looked at if this is possible or unintentionally exposed by the SecureContext API.
https://github.com/nodejs/node/issues/4464
https://github.com/nodejs/node/issues/20432
https://github.com/nodejs/node/pull/26908#issuecomment-479147423
Would it make sense to document this feature?
I鈥檓 not sure it鈥檚 safe to consider secureContext.context public API, but I think it should be okay to add a public secureContext.addCACert() wrapper around it?
@nodejs/crypto may have more insight (and more helpful opinions).
I'm not sure a wrapper is the right solution. That feels like the start of extra functions to maintain.
Besides appending to or enumerating the current list, removing is equally useful (as opposed to revoking).
I wonder if adding more documentation would enable a home rolled SecurityContext with arbitrary rules.
If the default list of CAs were accessible in node, we could do this ourselves without adding extra APIs.
If this is your use case, I believe it is met by https://github.com/nodejs/node/pull/26415
Adding direct access to SecureContext.context is a no-go, we're more likely to deprecate that and then make it inaccessible from user space, re-exporting well defined functions. This allows us to do less error checking in C++, among other things, and just assert on C++ API misuse.
a home rolled SecurityContext
Not sure if that is feasible, only the .context (a wrapper (EDIT: "container" is a better term) around an OpenSSL SSL_CTX object) is passed into C++, so its not replaceable in javascript if that is what you hoped to do.
If the default list of CAs were accessible in node, we could do this ourselves without adding extra APIs.
If this is your use case, I believe it is met by #26415
This would solve my use case with documented API, albeit a little more cumbersome than my current approach.
Adding direct access to SecureContext.context is a no-go, we're more likely to deprecate that and then make it inaccessible from user space, re-exporting well defined functions. This allows us to do less error checking in C++, among other things, and just assert on C++ API misuse.
Just to confirm, should we expect the SecureContext.context API to go away?
a home rolled SecurityContext
Not sure if that is feasible, only the
.context(a wrapper (EDIT: "container" is a better term) around an OpenSSLSSL_CTXobject) is passed into C++, so its not replaceable in javascript if that is what you hoped to do.
Yeah, that is what my mind wandered to. Figured it was something like that.
should we expect the SecureContext.context API
Let me be abolutely clear: SecureContext.context is an undocumented implementation detail of SecureContext. It is not an API.
Because node.js attempts not to break user's code, even code that depends on undocumented implementation details or internal properties, we are unlikely to make breaking changes to SecureContext outside a semver-major, but you use it at your own risk.
The ability to add additional certificate authorities at runtime is an important need for the company I work for. The short version is that the lack of support for Windows' built-in certificate store results in many developers slamming NODE_TLS_REJECT_UNAUTHORIZED=0 into their code as a quick workaround so they can debug and test locally.
@sam-github any thought on what a well designed Node.js API for adding additional CAs would look like?
The most obvious idea would be to let tls.rootCertificates be mutable or assignable, although its unfortunate design with a field rather than a function makes it a significant challenge.
Alternately, what do you think about tls.addRootCertificate(cert)? Assuming the API is agreeable, I could look at putting together a PR.
@ebickle Your devs have demonstrated that they are comfortable setting an env variable, so why not set NODE_EXTRA_CA_CERTS instead of NODE_TLS_REJECT_UNAUTHORIZED?
ca: tls.rootCertificates.concat([myCA1, myCA2]) would also work fine.
That said, I personally don't have any problem with a mutable tls.DEFAULT_CA (similar to the other tls.DEFAULT_ properties), and would prefer that to a method (or set of methods) that mutates tls.rootCertificates.
The crux of it is that NODE_EXTRA_CA_CERTS cannot be set at runtime, unfortunately.
The longer answer is is that developers run into the TLS verification failure and they're in the mental mode of "get the project working" not "configure my development workstation". They Google around for documentation, give NODE_EXTRA_CA_CERTS a try at runtime and fail, then give up and toss NODE_TLS_REJECT_UNAUTHORIZED into the codebase regardless of how much security training we toss at them. They can't find the solution (at a project-level) within a reasonable amount of time and then move on to get their work done.
In an enterprise-scenario with a very large number of nonhomogeneous developer workstations around the world, getting NODE_EXTRA_CA_CERTS on the dev machines consistently is also an operational challenge.
Appreciate the advice on using tls.rootCertificates, I'll give that a go and write it up as a pattern to share with devs internally.
tls.DEFAULT_CA is a good idea but wouldn't directly cover the need to avoid overriding CAs for systems that need to access both public and internal services. Expanding on that idea, what do you think about this?
tls.DEFAULT_CA - The default value of the ca option of tls.createSecureContext().tls.DEFAULT_OVERRIDE_CA - the default value of the overrideCA option of tls.createSecureContext()overrideCA option to tls.createSecureContext() with a default value of true. When set to false, the ca option adds additional trusted CA certificates to the default well-known ones instead of overriding them.Bigger change, but gets directly to the use case of wanting to add private enterprise CAs to Node.js without affecting anything else. By adding it to createSecureContext, the functionality would be consistent across all of Node.js instead of just being patched in here or there.
An alternate solution would be to have an extraCA option on tls.createSecureContext() and a tls.DEFAULT_EXTRA_CA. It's interesting in that it aligns with the environment variable - you could even see internal functionality of the environment variable reading the CA files off disk and setting DEFAULT_EXTRA_CA at startup.
In an enterprise-scenario with a very large number of nonhomogeneous developer workstations around the world, getting NODE_EXTRA_CA_CERTS on the dev machines consistently is also an operational challenge.
But you asked for a js API to add a cert... which means that a cert (probably in PEM) has to exist on the machine (probably checked into the code base) to be passed to that js API....
I'm not seeing how modifying a project's package.json to do start: NODE_EXTRA_CA_CERTS=ca.pem node bin/www is any more or less of an operational problem than modifying the project's source code so that it does tls./*some js*/(fs.readFileSync('ca.pem')). In both cases, the dev needs the enterprise's CA cert.
In a sufficiently enterprise env, I'd expect the start script to be baked into the base Dockerfile.
tls.DEFAULT_CA is a good idea but wouldn't directly cover the need to avoid overriding CAs for systems that need to access both public and internal services.
Please describe why not. It seems completely equivalent to your proposals, but much simpler and consistent with how tls is configured now.
Your initial suggestion was:
what do you think about tls.addRootCertificate(cert)? Assuming the API is agreeable, I could look at putting together a PR.
Assuming that your suggestion supported your use-case (which I did assume!), your example API is identical in behaviour to my suggestion:
tls.DEFAULT_CA.push(cert)
This "override" API proposal doesn't add anything that doesn't exist already, as far as I can see:
Add tls.DEFAULT_OVERRIDE_CA - the default value of the overrideCA option of tls.createSecureContext()
Add overrideCA option to tls.createSecureContext() with a default value of true. When set to false, the ca option adds additional trusted CA certificates to the default well-known ones instead of overriding them.
It just allows writing:
tls.createSecureContext({
ca: CERTS,
overrideCA: false // EDIT: sorry, got the boolean wrong just now, false was intended
})
instead of what is possible _right now_:
tls.createSecureContext({
ca: tls.rootCertificates.concat(CERTS),
})
The tls.DEFAULT_CA.push(cert) example makes a lot of sense. For some reason I had visualized that field being null by default and only used if set.
How would you see tls.rootCertificates working with it? rootCertificates would be the immutable set of the default node.js root certificates plus the ones loaded from NODE_EXTRA_CA_CERTS and DEFAULT_CA would be initialized at startup (or first use) to a copy of rootCertificates?
Yes, that's what I'd expect.
I've been doing some initial work on this over the past few weeks.
After digging through the code, I'd recommend against a tls.DEFAULT_CA option. The biggest issue is that it would introduce a significant performance degradation for all TLS connections; every time a TLS channel is created it would have to initialize a new X509_STORE rather than reuse the existing root_cert_store. There's already some GitHub issues around the performance of the CA option with a small number of certs; increasing the number of certs (e.g. including the default ones) and making it the default would only make the performance significantly worse.
What about adding a setter to tls.rootCertificates instead?
After validating the input and ensuring the certs can be read as valid X509 objects they can be placed in the root_cert_store static variable of node_crypto.cc, similar to how the NODE_EXTRA_CA_CERTS are handled. The X509_STORE is reference counted, so all existing references to the old root_cert_store continue working in their SecureContexts while any new ones get the 'set' CAs.
The frozen nature of the tls.rootCertificates property is advantageous; it would encourage consumers to make a single "update" to the root certificate store instead of multiple small ones.
Separately, while doing a code review of node_crypto.cc I noticed a few software defects - what's the best way to discuss or handle those? I'd like a sanity check before I toss them in as GitHub issues. Looks as though a few edge cases were missed when NODE_EXTRA_CA_CERTS and tls.rootCertificates were added.
every time a TLS channel is created it would have to initialize a new X509_STORE rather than reuse the existing root_cert_store.
The performance issues are a good point. What about cacheing?
If the textual value of DEFAULT_CA is the same as that used for the default X509_STORE, use the default store?
It seems like a general fix to the ca: peformance problem would be to cache X509_STORE objects using the PEM input as a key. Keep the builtin always cached, keep a cache size of 2 or something small for others (I suspect that usually ca is set to a single string used for all connections).
Separately, while doing a code review of node_crypto.cc I noticed a few software defects
PR fixes for them, or open an issue to ask if they are a problem, or ask in the slack/irc if you'd like a quick conversation. If you think they are security problems, report on hacker one (see our security page).
Most helpful comment
Let me be abolutely clear:
SecureContext.contextis an undocumented implementation detail ofSecureContext. It is not an API.Because node.js attempts not to break user's code, even code that depends on undocumented implementation details or internal properties, we are unlikely to make breaking changes to SecureContext outside a semver-major, but you use it at your own risk.