Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tls): Optionally support loading native certs #11491

Merged

Conversation

justinmchase
Copy link
Contributor

@justinmchase justinmchase commented Jul 22, 2021

Overview

This changes Deno to support optionally loading certificates from the users local certificate store. This will allow them to successfully connect via tls with corporate and self signed certs provided they have them installed in their keystore. It also allows them to deal with revoked certs by simply updating their keystore without having to upgrade Deno.

Potentially in the future Deno could consider modifying its default behavior to load from the certificate store but for now it is only optionally enabled by specifying it in the new flag described below.

This change also adds a new flag DENO_TLS_CA_STORE which can have the values system,mozilla and will load them in the order they are specified and defaults to mozilla. system controls loading certs from the users certificate store and mozilla controls loading the bundled mozilla certs.

Issues

Fixes #11482
Related To #10312

Testing

I have access through VPN to an internal website which is using a certificate issued by my companies internal CA. With latest Deno I am unable to access that site because the only certificate I have is installed in my system keychain by my IT department.

The app code:

const { ok, status } = await fetch('http:https://example-app.acme.com')
console.log(`${ok ? 'OK' : 'FAIL'} ${status}`)

To see this error I am able to run:

$ deno run --unstable -A main.ts
Check file:https:///Users/jchase24/code/example/main.ts
Sending fatal alert BadCertificate
error: Uncaught (in promise) TypeError: error sending request for url (https://example-app.acme.com/#undefined): error trying to connect: invalid certificate: UnknownIssuer
    at deno:core/core.js:86:46
    at unwrapOpResult (deno:core/core.js:106:13)
    at async mainFetch (deno:extensions/fetch/26_fetch.js:229:14)

Now with these changes I've run

$ cd ~/code/deno
$ cargo build
$ cd ~/code/example
$ DENO_TLS_CA_STORE=system ../deno/target/debug/deno run -A main.ts
Check file:https:///Users/jchase24/code/example/main.ts
OK 200

Using the flag to remove the certificate store, results in the error again (as expected)

DENO_TLS_CA_STORE=mozilla ../deno/target/debug/deno run -A main.ts
Sending fatal alert BadCertificate
error: Uncaught (in promise) TypeError: error sending request for url (https://example-app.acme.com/#undefined): error trying to connect: invalid certificate: UnknownIssuer
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async mainFetch (deno:extensions/fetch/26_fetch.js:265:14)

@CLAassistant
Copy link

CLAassistant commented Jul 22, 2021

CLA assistant check
All committers have signed the CLA.

@justinmchase justinmchase marked this pull request as draft July 22, 2021 15:29
extensions/fetch/lib.rs Outdated Show resolved Hide resolved
extensions/fetch/lib.rs Outdated Show resolved Hide resolved
@justinmchase
Copy link
Contributor Author

justinmchase commented Jul 23, 2021

Outstanding Issues

  • There should probably be one function in all of Deno for initializing the ClientBuilder and tls config
  • There should be one cache for tls persistence
  • Unit tests.

@justinmchase justinmchase marked this pull request as ready for review July 23, 2021 03:51
Copy link
Member

@lucacasonato lucacasonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is now a lot of duplicated logic. I would suggest the following:

On ProgramState creation, create a single rustls::RootCertStore should be created containing certificates based on the user requested CA stores, and the certificate provided by --cert. This rustls::RootCertStore should then be injected into create_http_client (and deno_fetch), and all other places where we use TLS (deno_net etc).

This would also allow you to remove all the places we inject ca_data, because that would already be part of the rustls::RootCertStore.

cli/flags.rs Outdated Show resolved Hide resolved
cli/http_util.rs Outdated Show resolved Hide resolved
@justinmchase justinmchase changed the title fix(tls): Switch to native certs fix(tls): Optionally support loading native certs Jul 26, 2021
@justinmchase justinmchase force-pushed the feature/use-native-certs-for-tls branch from 7d0744a to a60f607 Compare July 26, 2021 14:53
@justinmchase justinmchase marked this pull request as draft July 26, 2021 15:01
cli/standalone.rs Outdated Show resolved Hide resolved
@justinmchase justinmchase force-pushed the feature/use-native-certs-for-tls branch from a60f607 to 567eae6 Compare July 26, 2021 15:56
@justinmchase
Copy link
Contributor Author

justinmchase commented Jul 26, 2021

Ok this was refactored and updated to:

  1. Consolidate duplicate code up into deno_core deno_tls
  2. Replace passing ca_data,ca_file with root_cert_store.
  3. Default behavior is to only load the mozilla certs as it does today, the system certs are only loaded if specified via the flag.
  4. Some places in code such as op_* functions can have arguments which pass certs directly in, that behavior should not have been changed and they get added to a clone of the base store per function call.
  5. Some modules such as rustls were bumped up to deno_core deno_tls and are referenced directly there rather than having each other module have a duplicate dependency.

Copy link
Contributor

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by review.

cli/program_state.rs Outdated Show resolved Hide resolved
cli/program_state.rs Outdated Show resolved Hide resolved
core/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmchase nice work! I pointed out some nitpicks and how to solve problem with adding multiple TLS related crates to deno_core crate.

cli/file_fetcher.rs Outdated Show resolved Hide resolved
cli/Cargo.toml Outdated Show resolved Hide resolved
cli/Cargo.toml Outdated Show resolved Hide resolved
cli/tests/integration/mod.rs Outdated Show resolved Hide resolved
core/Cargo.toml Outdated Show resolved Hide resolved
@bartlomieju bartlomieju added this to the 1.13.0 milestone Aug 3, 2021
@justinmchase
Copy link
Contributor Author

Ok thanks @bartlomieju I know how to proceed.

@justinmchase
Copy link
Contributor Author

justinmchase commented Aug 3, 2021

Ok merged with main and all PR comments addressed and two unit tests were added, which need to be reviewed.

The new tests call https://deno.land since its signed with a valid root CA covered by the mozilla certs. Arguably this is a forbidden action but I'm not sure how else to test it specifically.

These tests positively test that the mozilla certs connect to deno.land and positively test that not loading any ca certs (an empty root_cert_store) fails to connect to deno.land.

The only other test vector that isn't covered by existing tests would be to then put the right cert into the system keystore and test loading that... I don't know how to do that consistently across systems without some really complicated automation and frankly... it feels a bit tautological to test as that entire functionality exists in a different dependency we are adding which has its own testing.

I've been testing manually and can confirm that adding the new flag allows me to connect to my internal company sites successfully.

I'll leave it up to the maintainers to decide which tests are needed.

@justinmchase
Copy link
Contributor Author

Curious... on Windows it seems to successfully make a request even without certs. Does it not have tls support by default? Or maybe its loading certs from the certificate store regardless? I'm not sure.

Screen Shot 2021-08-03 at 3 59 58 PM

Copy link
Contributor

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments but your PR mostly looks (very!) good.

Node supports all these use-cases via esoteric OpenSSL env vars. We can centralize and clean this up.

We'll be doing even better. Node can only use the system's openssl certificate bundle, whereas we will be able to use the native macos and windows stores.

It's an often-requested feature that never happened because openssl can't do it. Take nodejs/node#39657 - a feature request from < 24 hours ago.

cli/program_state.rs Outdated Show resolved Hide resolved
cli/program_state.rs Outdated Show resolved Hide resolved
cli/standalone.rs Outdated Show resolved Hide resolved
Comment on lines +698 to +702
let root_cert_store = state
.borrow()
.borrow::<DefaultTlsOptions>()
.root_cert_store
.clone();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloning the store every time is somewhat inefficient. On my system it copies ~200 kB of data.

It's not really a regression though because we currently copy webpki_roots::TLS_SERVER_ROOTS and that's about the same size.

Ideally, we create and cache the Arc<rustls::ClientConfig> so it can be reused instead of created anew for every connection...

...but that's something that can wait for a follow-up PR, it doesn't have to block this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is why I did that, the roots were cloned before and this ended up following that pattern for better or worse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how this already feels to be max size for this PR I'm happy to leave this for a future refactor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but please add a TODO here so we don't forget about it :)

extensions/tls/lib.rs Outdated Show resolved Hide resolved
@ry
Copy link
Member

ry commented Aug 5, 2021

If Ben and Luca are ok with this, then I have no objections. We just need to make sure this is properly documented after this patch lands.

@justinmchase justinmchase force-pushed the feature/use-native-certs-for-tls branch from ddb5082 to e7dcd32 Compare August 6, 2021 04:25
@justinmchase justinmchase force-pushed the feature/use-native-certs-for-tls branch from e7dcd32 to 19653bf Compare August 6, 2021 04:36
@justinmchase
Copy link
Contributor Author

@ry If Ben and Luca are ok with this, then I have no objections

Just to be clear by "this" I assume you mean keeping the functionality as it is in this pr right now? To have the DENO_TLS_CA_STORE with string[] arguments, not the DENO_SYSTEM_CA_STORE=1 boolean flag?

If so then this PR is ready I believe. I linted, formatted and squashed the changes in the branch as well as manually tested one more time.

@bnoordhuis
Copy link
Contributor

For posterity: I don't have opinions on how this feature should be activated. Bartek or Luca are more qualified to make that call.

extensions/tls/lib.rs Outdated Show resolved Hide resolved
@bartlomieju
Copy link
Member

@ry If Ben and Luca are ok with this, then I have no objections

Just to be clear by "this" I assume you mean keeping the functionality as it is in this pr right now? To have the DENO_TLS_CA_STORE with string[] arguments, not the DENO_SYSTEM_CA_STORE=1 boolean flag?

If so then this PR is ready I believe. I linted, formatted and squashed the changes in the branch as well as manually tested one more time.

That's right, I'll give this PR another review tonight.

Could you please open a PR in https://github.com/denoland/manual that describes this functionality? Turning Luca's comment into some kind of doc would be awesome.

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmchase this PR is really well implemented, very nice work! I have only a couple minor nitpicks left and we're good to merge.

Comment on lines +2360 to +2365
[[package]]
name = "openssl-probe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes 😬 it comes from rustls-native-certs and it doesn't seem it could be in any way skipped (no feature flags in that crate). I looked up that crate's code and it doesn't do any interaction with OpenSSL so I guess we can live with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I think it does say something about it in the docs now that you mention it:

On Linux and other UNIX-like operating systems, the openssl-probe crate is used to discover the filename of the system CA bundle.

cli/program_state.rs Outdated Show resolved Hide resolved
cli/http_util.rs Outdated Show resolved Hide resolved
cli/standalone.rs Show resolved Hide resolved
Comment on lines +698 to +702
let root_cert_store = state
.borrow()
.borrow::<DefaultTlsOptions>()
.root_cert_store
.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but please add a TODO here so we don't forget about it :)

extensions/tls/lib.rs Outdated Show resolved Hide resolved
extensions/tls/lib.rs Outdated Show resolved Hide resolved
@justinmchase
Copy link
Contributor Author

One of the tests returned this result, I'm not sure how to run it locally to debug it. It passed in a previous iteration, so it's an interesting result. How can I tell which test is failing here exactly?

Screen Shot 2021-08-06 at 3 50 05 PM

@bartlomieju
Copy link
Member

One of the tests returned this result, I'm not sure how to run it locally to debug it. It passed in a previous iteration, so it's an interesting result. How can I tell which test is failing here exactly?

Screen Shot 2021-08-06 at 3 50 05 PM

@justinmchase it looks like this error was returned when pulling code for deno_std - before any test was run - ie. when process was starting up and downloading dependencies.

@justinmchase
Copy link
Contributor Author

Succeeded this time, I didn't change anything. Weird. 🤷

All comments have been addressed I believe. Thanks all.

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you @justinmchase!

@bartlomieju bartlomieju merged commit 02c74fb into denoland:main Aug 7, 2021
@justinmchase justinmchase deleted the feature/use-native-certs-for-tls branch August 8, 2021 05:07
andreubotella pushed a commit to andreubotella/deno that referenced this pull request Oct 25, 2021
`fetch()` and client-side websocket used to support HTTP/2, but this
regressed in denoland#11491. This patch reenables it by explicitly adding `h2`
and `http/1.1` to the list of ALPN protocols on the HTTP and websocket
clients.

Fixes denoland#12517.
lucacasonato pushed a commit that referenced this pull request Oct 25, 2021
`fetch()` and client-side websocket used to support HTTP/2, but this
regressed in #11491. This patch reenables it by explicitly adding `h2`
and `http/1.1` to the list of ALPN protocols on the HTTP and websocket
clients.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Sending fatal alert BadCertificate: invalid certificate: UnknownIssuer
7 participants