Repository navigation
Documented way to add certificates to existing SecureContext #27079
Description
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 #20432 (comment)
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:
- No underscores (
_) - It is named in a change log
- Tests use this function.
- Internal code uses this API
Would it make sense to document this feature?
Alternatives
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.
Related Issues
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsAwaiting Triage
Activity
addaleax commentedon Apr 3, 2019
I’m not sure it’s safe to consider
secureContext.contextpublic API, but I think it should be okay to add a publicsecureContext.addCACert()wrapper around it?@nodejs/crypto may have more insight (and more helpful opinions).
cinderblock commentedon Apr 4, 2019
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
SecurityContextwith arbitrary rules.sam-github commentedon Apr 4, 2019
If this is your use case, I believe it is met by #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.
sam-github commentedon Apr 4, 2019
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.cinderblock commentedon Apr 4, 2019
This would solve my use case with documented API, albeit a little more cumbersome than my current approach.
Just to confirm, should we expect the
SecureContext.contextAPI to go away?Yeah, that is what my mind wandered to. Figured it was something like that.
sam-github commentedon Apr 4, 2019
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.
ebickle commentedon Dec 17, 2019
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.rootCertificatesbe 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.sam-github commentedon Dec 17, 2019
@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 othertls.DEFAULT_properties), and would prefer that to a method (or set of methods) that mutatestls.rootCertificates.ebickle commentedon Dec 17, 2019
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_CAis 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 thecaoption oftls.createSecureContext().tls.DEFAULT_OVERRIDE_CA- the default value of theoverrideCAoption oftls.createSecureContext()overrideCAoption totls.createSecureContext()with a default value oftrue. When set tofalse, thecaoption 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
extraCAoption ontls.createSecureContext()and atls.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.sam-github commentedon Dec 17, 2019
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/wwwis any more or less of an operational problem than modifying the project's source code so that it doestls./*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.
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:
Assuming that your suggestion supported your use-case (which I did assume!), your example API is identical in behaviour to my suggestion:
This "override" API proposal doesn't add anything that doesn't exist already, as far as I can see:
It just allows writing:
instead of what is possible right now:
ebickle commentedon Dec 17, 2019
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.rootCertificatesworking with it?rootCertificateswould be the immutable set of the default node.js root certificates plus the ones loaded from NODE_EXTRA_CA_CERTS andDEFAULT_CAwould be initialized at startup (or first use) to a copy ofrootCertificates?sam-github commentedon Dec 17, 2019
Yes, that's what I'd expect.
21 remaining items
github-actions commentedon Mar 25, 2023
There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.
For more information on how the project manages feature requests, please consult the feature request management document.
cinderblock commentedon Mar 27, 2023
This is not the right thing to do. Can we get a
never-stale?cinderblock commentedon Jul 23, 2024
:(
pimterry commentedon Jul 23, 2024
Good nudge @cinderblock!
Since all that ancient discussion above, I've now joined the Node core team, so I can now help a bit more with actually making this happen 😄.
I agree this would be extremely useful for some use cases, so I've reopened this & added never-stale.
I think:
extraCAoption to contexts that would add a CA and preserve the existing defaults, includingNODE_EXTRA_CA_CERTSetc that aren't inrootCertificates)I think if we can get that PR merged (fixing #32010), then this approach should work with very simple changes (AFAICT) and then this issue would be sorted. I don't have a lot of time to dig into this in depth, but I'm happy to help shepherd from Node maintainer side.
Is that all correct? If so, step 1 is fixing the conflicts in #44529 and then finishing up that PR. @ebickle I know this has been a bit of a saga, but any chance you're interested in working on that to wrap this up once & for all?
ebickle commentedon Jul 23, 2024
@pimterry I'm happy to dust off my PR and resolve the conflicts or otherwise get it back up and ready.
I left off with some exploration of other open source tools to see how they integrated OpenSSL directly with the Windows certificate store with the hope that we could do something similar in Node - e.g. using an OS's built in certificates helps resolve the need for adding certificates manually at runtime. I got about halfway done before I realized node.js doesn't have much platform-specific logic outside of libuv, and libuv doesn't have TLS as a feature. I had wanted to think of a good solution for that and then got busy. I'd also played with some ideas for fancier higher level constructs like a "CertificateStore" object in Node.js that could be used at the global/environment level or as a parameter for a TLS agent. I got that about 70% done before I figured I was over-engineering... but if any of that sounds interesting, let me know!
In any event, the plan outlined above sounds good and gets the job done in the shorter/medium term!
pimterry commentedon Jul 23, 2024
This all sounds very cool! Definitely interesting to explore, but yes that's much more complicated & it'd definitely require some wider debate since it's quite a major change. Let's start with solving the practical problem and see where we can go from there.
If you can ping me on that PR once the conflicts are resolved and you think it's ready, I'll review and help move it forwards 👍
dbob16 commentedon Mar 14, 2026
Has something been worked out on this front? It would be very helpful for my TAM3/TAM4 projects.