diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7841a3475..c05d62d8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -409,6 +409,12 @@ jobs: - run: pnpm exec vp fmt --check - run: cargo autoinherit && git diff --exit-code - run: cargo shear --deny-warnings + # Bans webpki-roots and webpki-root-certs for the platforms CI builds. + # The security workflow's cargo-deny uses that action's own config. + - uses: taiki-e/install-action@9e1e5806d4a4822de933115878265be9aaa786d9 # v2.82.2 + with: + tool: cargo-deny@0.20.2 + - run: cargo deny check bans - run: cargo fmt --check # --exclude fspy_benchmark: documenting it would build its musl artifact # dependency, which needs a rust-std this job otherwise has no use for. diff --git a/.typos.toml b/.typos.toml index c227ba5df..267d74465 100644 --- a/.typos.toml +++ b/.typos.toml @@ -8,6 +8,7 @@ EXCED = "EXCED" [files] extend-exclude = [ "crates/fspy_detours_sys/detours", + "third_party/rustls-platform-verifier", "crates/fspy_detours_sys/src/generated_bindings.rs", # Intentional typos for testing fuzzy matching and "did you mean" suggestions "crates/vt_select/src/fuzzy.rs", diff --git a/Cargo.lock b/Cargo.lock index 40dad5d5e..cdfb6dc25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3739,8 +3739,6 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -3753,7 +3751,6 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs", "windows-sys 0.61.2", ] @@ -5620,15 +5617,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "wezterm-bidi" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 74ef3590f..d4a57d36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -232,3 +232,7 @@ panic = "abort" # Let it crash and force ourselves to write safe Rust. # would compile them with the build-override profile (opt-level 0) even in # release builds, in this workspace and in every downstream workspace. See # https://github.com/rust-lang/cargo/issues/16719. + +# Drops the wasm32 dependency on webpki-root-certs. See the vendored Cargo.toml. +[patch.crates-io] +rustls-platform-verifier = { path = "third_party/rustls-platform-verifier" } diff --git a/crates/vt_remote_cache/Cargo.toml b/crates/vt_remote_cache/Cargo.toml index 13d7e8b26..9ca2beff5 100644 --- a/crates/vt_remote_cache/Cargo.toml +++ b/crates/vt_remote_cache/Cargo.toml @@ -11,6 +11,10 @@ rust-version.workspace = true base64 = { workspace = true } bytes = { workspace = true } ciborium = { workspace = true } +# `rustls-no-provider` selects rustls with rustls-platform-verifier, which +# verifies certificates using the operating system's trust store. The crypto +# provider is installed in `Client::new`. The vendored verifier does not +# depend on `webpki-roots` or `webpki-root-certs`; deny.toml bans both. reqwest = { workspace = true, features = [ "http2", "multipart", diff --git a/crates/vt_remote_cache/README.md b/crates/vt_remote_cache/README.md index 86ddb1816..ee1fdd9f8 100644 --- a/crates/vt_remote_cache/README.md +++ b/crates/vt_remote_cache/README.md @@ -16,6 +16,6 @@ Client for the [remote cache server API](https://github.com/voidzero-dev/vite-ta Only HTTP 200 counts as success for every operation, except that a fetch also accepts 404 as no match. Redirects aren't followed, so a redirect fails like any other status. -reqwest configures TLS. It uses the process's default rustls crypto provider, which the client installs as ring unless one is already installed, and verifies certificates with the operating system's verifier. HTTPS requests use HTTP/2 if the server accepts it during the TLS handshake and HTTP/1.1 otherwise. HTTP endpoints always use HTTP/1.1. Requests go through the proxy set in `HTTPS_PROXY`, `HTTP_PROXY`, or `ALL_PROXY`, except for hosts in `NO_PROXY`. Without those variables, the system proxy settings are used on macOS and Windows. Connections time out after 10 seconds. Reads time out after 60 seconds, and until the response headers arrive, that limit also covers sending the request. +reqwest configures TLS. It uses the process's default rustls crypto provider, which the client installs as ring unless one is already installed, and verifies certificates with the operating system's trust store. The vendored `rustls-platform-verifier` does not depend on `webpki-roots` or `webpki-root-certs`, so those crates are absent from `Cargo.lock`. `deny.toml` bans them, and CI runs `cargo deny check bans`. HTTPS requests use HTTP/2 if the server accepts it during the TLS handshake and HTTP/1.1 otherwise. HTTP endpoints always use HTTP/1.1. Requests go through the proxy set in `HTTPS_PROXY`, `HTTP_PROXY`, or `ALL_PROXY`, except for hosts in `NO_PROXY`. Without those variables, the system proxy settings are used on macOS and Windows. Connections time out after 10 seconds. Reads time out after 60 seconds, and until the response headers arrive, that limit also covers sending the request. `Error` names the kind of failure: an invalid endpoint, a client that couldn't be created, a blob file that couldn't be read, credentials that couldn't be obtained, a network error (including timeouts and responses that end early), a status other than 200 (or 404, for a fetch), or a malformed fetch response. Its messages contain no OS-specific details, so they can be shown to users as is. The details are in the source: the underlying error, the parse error for an endpoint that isn't a URL, or the message in an error response's body. Network errors leave out the request URL, since the endpoint may contain credentials, such as a token in its query. diff --git a/crates/vt_remote_cache/src/lib.rs b/crates/vt_remote_cache/src/lib.rs index 759792bb0..944935377 100644 --- a/crates/vt_remote_cache/src/lib.rs +++ b/crates/vt_remote_cache/src/lib.rs @@ -155,9 +155,10 @@ impl Client { let fetch_url = route_url(&endpoint, "fetch")?; let store_url = route_url(&endpoint, "store")?; let blob_url = route_url(&endpoint, "blob")?; - // reqwest configures TLS with the process's default crypto provider. - // Installing fails if one is already installed; vite-plus installs - // ring too. + // reqwest configures TLS with the process's default crypto provider + // and verifies certificates with the operating system's trust store. + // Installing the provider fails if one is already installed; vite-plus + // installs ring too. let _ = rustls::crypto::ring::default_provider().install_default(); // A redirect fails like any other status. Following one could turn a // store into a GET of a login page that responds with 200. HTTPS diff --git a/deny.toml b/deny.toml index bf028f611..87a0015e4 100644 --- a/deny.toml +++ b/deny.toml @@ -70,8 +70,6 @@ exceptions = [ # Each entry is the crate and version constraint, and its specific allow # list # { allow = ["Zlib"], name = "adler32", version = "*" }, - # Mozilla's root certificates, used by rustls-platform-verifier on wasm32 only. - { allow = ["CDLA-Permissive-2.0"], name = "webpki-root-certs" }, ] # Some crates don't have (easily) machine readable licensing information, @@ -145,6 +143,13 @@ deny = [ # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate # { name = "ansi_term", version = "=0.11.0", wrappers = [] }, + # + # Compiled-in Mozilla root certificates. HTTPS verification uses the + # operating system's trust store. The vendored rustls-platform-verifier + # does not depend on these crates, so they stay out of Cargo.lock. This + # ban fails if any target starts depending on an embedded trust store. + { name = "webpki-roots" }, + { name = "webpki-root-certs" }, ] # List of features to allow/deny diff --git a/third_party/rustls-platform-verifier/Cargo.toml b/third_party/rustls-platform-verifier/Cargo.toml new file mode 100644 index 000000000..d7eb2f8b4 --- /dev/null +++ b/third_party/rustls-platform-verifier/Cargo.toml @@ -0,0 +1,64 @@ +# Vendored from crates.io rustls-platform-verifier 0.7.0 so the wasm32 +# dependency on webpki-root-certs can be removed. Cargo.lock records every +# target-specific dependency, and that crate is Mozilla's compiled-in trust +# store. Certificate verification on the platforms vite-task ships still uses +# the operating system's trust store. Upstream tests and the update-certs +# example are omitted because they require webpki-root-certs. +# https://github.com/rustls/rustls-platform-verifier +[package] +name = "rustls-platform-verifier" +version = "0.7.0" +description = "rustls-platform-verifier supports verifying TLS certificates in rustls with the operating system verifier" +keywords = ["tls", "certificate", "verification", "os", "native"] +repository = "https://github.com/rustls/rustls-platform-verifier" +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.85" +publish = false + +[lib] +name = "rustls_platform_verifier" + +[features] +# Enables a C interface to use for testing where `cargo` can't be used. +# This feature is not stable, nor is the interface exported when it is enabled. +# Do not rely on this or use it in production. +ffi-testing = ["android_logger", "rustls/ring"] +# Enables APIs that expose lower-level verifier types for debugging purposes. +dbg = [] +# Enables `log::debug` base64-encoded logging of all end-entity certificates processed +# by the platform's verifier. +cert-logging = ["base64"] +# Used for nicely documenting the Android-specific APIs. This feature is not stable. +docsrs = ["jni", "once_cell"] + +[dependencies] +rustls = { version = "0.23.27", default-features = false, features = ["std"] } +log = { version = "0.4" } +base64 = { version = "0.22", optional = true } # Only used when the `cert-logging` feature is enabled. +jni = { version = "0.22.4", default-features = false, optional = true } # Only used during doc generation +once_cell = { version = "1.9", optional = true } # Only used during doc generation + +[target.'cfg(all(unix, not(target_os = "android"), not(target_vendor = "apple"), not(target_arch = "wasm32")))'.dependencies] +rustls-native-certs = "0.8" +webpki = { package = "rustls-webpki", version = "0.103", default-features = false } + +[target.'cfg(target_os = "android")'.dependencies] +once_cell = "1.9" +rustls-platform-verifier-android = "0.1.0" +jni = { version = "0.22", default-features = false } +webpki = { package = "rustls-webpki", version = "0.103", default-features = false } +android_logger = { version = "0.15", optional = true } # Only used during testing. + +[target.'cfg(any(target_vendor = "apple"))'.dependencies] +core-foundation = "0.10" +core-foundation-sys = "0.8" +security-framework = "3.5.0" +security-framework-sys = "2.15" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = ">=0.52.0, <0.62.0", default-features = false, features = ["Win32_Foundation", "Win32_Security_Cryptography"] } + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +features = ["dbg", "docsrs"] diff --git a/third_party/rustls-platform-verifier/LICENSE-APACHE b/third_party/rustls-platform-verifier/LICENSE-APACHE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/third_party/rustls-platform-verifier/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/rustls-platform-verifier/LICENSE-MIT b/third_party/rustls-platform-verifier/LICENSE-MIT new file mode 100644 index 000000000..996412264 --- /dev/null +++ b/third_party/rustls-platform-verifier/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 1Password + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/rustls-platform-verifier/README.md b/third_party/rustls-platform-verifier/README.md new file mode 100644 index 000000000..5bafae8c1 --- /dev/null +++ b/third_party/rustls-platform-verifier/README.md @@ -0,0 +1,371 @@ +# rustls-platform-verifier + +This copy is vendored from crates.io `rustls-platform-verifier` 0.7.0. The wasm32 dependency on `webpki-root-certs` is removed, so Mozilla's compiled-in trust store is not in the lockfile. On wasm, verification has no default roots. + +[![crates.io version](https://img.shields.io/crates/v/rustls-platform-verifier.svg)](https://crates.io/crates/rustls-platform-verifier) +[![crate documentation](https://docs.rs/rustls-platform-verifier/badge.svg)](https://docs.rs/rustls-platform-verifier) +![MSRV](https://img.shields.io/badge/rustc-1.85+-blue.svg) +[![crates.io downloads](https://img.shields.io/crates/d/rustls-platform-verifier.svg)](https://crates.io/crates/rustls-platform-verifier) +![CI](https://github.com/1Password/rustls-platform-verifier/workflows/CI/badge.svg) + +A Rust library to verify the validity of TLS certificates based on the operating system's certificate facilities. +On operating systems that don't have these, `webpki` and/or `rustls-native-certs` is used instead. + +This crate is advantageous over `rustls-native-certs` on its own for a few reasons: +- Improved correctness and security, as the OSes [CA constraints](https://support.apple.com/en-us/HT212865) will be taken into account. +- Better integration with OS certificate stores and enterprise CA deployments. +- Revocation support via verifying validity via OCSP and CRLs. +- Less I/O and memory overhead because all the platform CAs don't need to be loaded and parsed. + +This library supports the following platforms and flows: + +| OS | Certificate Store | Verification Method | Revocation Support | +|----------------|-----------------------------------------------|--------------------------------------|--------------------| +| Windows | Windows platform certificate store | Windows API certificate verification | Yes | +| macOS (10.14+) | macOS platform roots and keychain certificate | macOS `Security.framework` | Yes | +| iOS | iOS platform roots and keychain certificates | iOS `Security.framework` | Yes | +| Android | Android System Trust Store | Android Trust Manager | Sometimes[^1] | +| Linux | System CA bundle, or user-provided certs[^3] | webpki | No[^2] | +| WASM | no default roots | webpki, only with extra roots | No[^2] | + +[^1]: On Android, revocation checking requires API version >= 24 (e.g. at least Android 7.0, August 2016). +When available, revocation checking is only performed for the end-entity certificate. If a stapled OCSP +response for the end-entity cert isn't provided, and the certificate omits both a OCSP responder URL and +CRL distribution point to fetch revocation information from, revocation checking may fail. + +[^2]: The fall-back webpki verifier configured for Linux/WASM does not support providing CRLs for revocation +checking. If you require revocation checking on these platforms, prefer constructing your own +`WebPkiServerVerifier`, providing necessary CRLs. See the Rustls [`ServerCertVerifierBuilder`] docs for more +information. + +[^3]: On Linux the [rustls-native-certs] and [openssl-probe] crates are used to try and discover the system CA bundle. +Users may wish to augment these certificates with [webpki-roots] using [`Verifier::new_with_extra_roots`] in case +a system CA bundle is unavailable. + +[`ServerCertVerifierBuilder`]: https://docs.rs/rustls/latest/rustls/client/struct.ServerCertVerifierBuilder.html +[`Verifier::new_with_extra_roots`]: https://docs.rs/rustls-platform-verifier/latest/rustls_platform_verifier/struct.Verifier.html#method.new_with_extra_roots +[rustls-native-certs]: https://github.com/rustls/rustls-native-certs +[openssl-probe]: https://github.com/alexcrichton/openssl-probe +[webpki-roots]: https://github.com/rustls/webpki-roots + +## Deployment Considerations + +When choosing to use `rustls-platform-verifier` or another trust store option, these differences are important to consider. They +are primarily about root certificate availability: + +| Backend | Updates | Roots used | Supports system-local roots | +|-------------------------------------------------|---------------------------------|-------------------------------------------------------------------------------------------------------|------------------------------| +| `rustls-platform-verifier` (non-Linux/BSD) | Updated by OS | System store, with full (dis)trust decisions from every source available. | Yes | +| `rustls-native-certs` + `webpki` | Updated by OS | System store, with no (dis)trust decisions. All roots are treated equally regardless of their status. | Yes, with exceptions | +| `webpki-roots` + `webpki` | Static, manual updates required | Hardcoded Mozilla CA roots, limited support for constrained roots. | No | + +**In general**: It is the opinion of the `rustls` and `rustls-platform-verifier` teams that this is the best default available for client-side libraries and applications +making connections to TLS servers when running on common operating systems. This is because it gets both live trust information (new roots, explicit markers, and auto-managed CRLs) +and better matches the common expectation of apps running on that platform (to use proxies, for example). Otherwise, it becomes your maintenance burden to +ship updates right away in order to handle increasing numbers of positive and negative trust events in the WebPKI/certificate ecosystem, or risk availability and security concerns. + +#### Linux/BSD +As of the time of writing, `rustls-platform-verifier` on these OSes only loads the trust stores from the OS once upon startup. This is the same behavior as `rustls-native-certs`, but the +abstraction allows better behavior on the other platforms without extra work for downstreams. + +#### Other + +Alternatively, there is a clear answer to use static `webpki-roots` in your application instead if you are deploying containerized applications frequently, where root store changes +will make it to production faster and any possibly used trust root is static by definition. + +Even though platform verifiers are sometimes implemented in memory-unsafe languages, it is very unlikely that Rust apps using this library will become a point of weakness. +This is due to either using a smaller set of servers or just being less exposed then other critical functions of the operating system, default web browser, etc. +But if your activity is identical or close to one of the following examples that process large amounts of untrusted input, a 100% Rust option like `webpki` is a more secure option: +- Seeing how many TLS servers `rustls` with a specific configuration can connect to. +- Harvesting data from various untrusted TLS endpoints exposed on the internet. +- Extracting info from a known-evil endpoint. +- Scanning all TLS certificates on the open internet. + +`rustls-platform-verifier` is widely deployed by several applications that use the `rustls` stack, such as 1Password, Bitwarden, Signal, and `rustup`, on a wide set of OSes. +This means that it has received lots of exposure to edge cases and has real-world experience/expertise invested into it to ensure optimal compatibility and security. + +## Installation and setup +On most platforms, no setup should be required beyond adding the dependency via `cargo`: +```toml +rustls-platform-verifier = "0.5" +``` + +To get a rustls `ClientConfig` configured to use the platform verifier use: + +```rust +use rustls::ClientConfig; +use rustls_platform_verifier::ConfigVerifierExt; +let config = ClientConfig::with_platform_verifier(); +``` + +This crate will use the [rustls process-default crypto provider](https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#using-the-per-process-default-cryptoprovider). To construct a `ClientConfig` with a different `CryptoProvider`, use: + +```rust +use rustls::ClientConfig; +use rustls_platform_verifier::BuilderVerifierExt; +let arc_crypto_provider = std::sync::Arc::new(rustls::crypto::ring::default_provider()); +let config = ClientConfig::builder_with_provider(arc_crypto_provider) + .with_safe_default_protocol_versions() + .unwrap() + .with_platform_verifier() + .unwrap() + .with_no_client_auth(); +``` + +### Android +Some manual setup is required, outside of `cargo`, to use this crate on Android. In order to +use Android's certificate verifier, the crate needs to call into the JVM. A small Kotlin +component must be included in your app's build to support `rustls-platform-verifier`. If distributing a library, that component will need to be bundled into your release jar +[as it is not yet available on Maven](https://github.com/rustls/rustls-platform-verifier/issues/115). + +#### Gradle Setup + +`rustls-platform-verifier` bundles the required native components in the crate, but the project must be setup to locate them +automatically and correctly. These steps assume you are using `.gradle` Groovy files because they're the most common, but if you are using +Kotlin scripts (`.gradle.kts`) for configuration instead, an example snippet is included towards the end of this section. + +Inside of your project's `build.gradle` file, add the following code and Maven repository definition. If applicable, this should only be the one "app" sub-project that +will actually be using this crate at runtime. With multiple projects running this, your Gradle configuration performance may degrade. + +
+ +App Snippets + +`$PATH_TO_DEPENDENT_CRATE` is the relative path to the Cargo manifest (`Cargo.toml`) of any crate in your workspace that depends on `rustls-platform-verifier` from +the location of your `build.gradle` file: + +```groovy +import groovy.json.JsonSlurper + +// ...Your own script code could be here... + +repositories { + // ... Your other repositories could be here... + maven { + url = findRustlsPlatformVerifierProject() + metadataSources.artifact() + } +} + +String findRustlsPlatformVerifierProject() { + def dependencyText = providers.exec { + it.workingDir = new File("../") + commandLine("cargo", "metadata", "--format-version", "1", "--filter-platform", "aarch64-linux-android", "--manifest-path", "$PATH_TO_DEPENDENT_CRATE/Cargo.toml") + }.standardOutput.asText.get() + + def dependencyJson = new JsonSlurper().parseText(dependencyText) + def manifestPath = file(dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }.manifest_path) + return new File(manifestPath.parentFile, "maven").path +} +``` + +Then, wherever you declare your dependencies, add the following: +```groovy +implementation "rustls:rustls-platform-verifier:latest.release" +``` + +
+ +
+Library Snippets + +```groovy +import groovy.json.JsonSlurper + +// ...Your own script code could be here... + +File findRustlsPlatformVerifierClasses() { + def dependencyText = providers.exec { + it.workingDir = new File("../") + commandLine("cargo", "metadata", "--format-version", "1") + }.standardOutput.asText.get() + + def dependencyJson = new JsonSlurper().parseText(dependencyText) + def manifestFile = file(dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }.manifest_path) + return new File(manifestFile.parentFile, "classes.jar") +} +``` + +Then, wherever you declare your dependencies, add the following: +```groovy +implementation files(findRustlsPlatformVerifierClasses()) +``` + +
+ +Cargo automatically handles finding the downloaded crate in the correct location for your project. It also handles updating the version when +new releases of `rustls-platform-verifier` are published. If you only use published releases, no extra maintenance should be required. + +These script snippets can be tweaked as best suits your project, but the `cargo metadata` invocation must be included so that the Android +implementation part can be located on-disk. + +##### Kotlin and Gradle + +
+Kotlin script App example + +`build.gradle.kts`: +```kotlin +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +buildscript { + dependencies { + classpath(libs.kotlinx.serialization.json) + } +} + +repositories { + rustlsPlatformVerifier() +} + +fun RepositoryHandler.rustlsPlatformVerifier(): MavenArtifactRepository { + @Suppress("UnstableApiUsage") + val manifestPath = let { + val dependencyJson = providers.exec { + workingDir = File(project.rootDir, "../") + commandLine("cargo", "metadata", "--format-version", "1", "--filter-platform", "aarch64-linux-android", "--manifest-path", "$PATH_TO_DEPENDENT_CRATE/Cargo.toml") + }.standardOutput.asText + + val path = Json.decodeFromString(dependencyJson.get()) + .getValue("packages") + .jsonArray + .first { element -> + element.jsonObject.getValue("name").jsonPrimitive.content == "rustls-platform-verifier-android" + }.jsonObject.getValue("manifest_path").jsonPrimitive.content + + File(path) + } + + return maven { + url = uri(File(manifestPath.parentFile, "maven").path) + metadataSources.artifact() + } +} + +dependencies { + // `rustls-platform-verifier` is a Rust crate, but it also has a Kotlin component. + implementation(libs.rustls.platform.verifier) +} +``` + +`libs.version.toml`: +```toml +# We always use the latest release because `cargo` keeps it in sync with the associated Rust crate's version. +rustls-platform-verifier = { group = "rustls", name = "rustls-platform-verifier", version = "latest.release" } +``` +
+ +
+Kotlin script Library example + +`build.gradle.kts`: +```kotlin +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +buildscript { + dependencies { + classpath(libs.kotlinx.serialization.json) + } +} + +fun findRustlsPlatformVerifierClasses(): File { + val dependencyJson = providers.exec { + workingDir = File(project.rootDir, "../") + commandLine("cargo", "metadata", "--format-version", "1") + }.standardOutput.asText + + val path = Json.decodeFromString(dependencyJson.get()) + .getValue("packages") + .jsonArray + .first { element -> + element.jsonObject.getValue("name").jsonPrimitive.content == "rustls-platform-verifier-android" + }.jsonObject.getValue("manifest_path").jsonPrimitive.content + + val manifestFile = File(path) + return File(manifestFile.parentFile, "classes.jar") +} + +dependencies { + implementation(files(findRustlsPlatformVerifierClasses())) +} +``` +
+ +#### Proguard + +If your Android application makes use of Proguard for optimizations, its important to make sure that the Android verifier component isn't optimized +out because it looks like dead code. Proguard is unable to see any JNI usage, so your rules must manually opt into keeping it. The following rule +can do this for you: +```text +-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; } +``` + +#### Crate initialization + +In order for the crate to call into the JVM, it needs handles from Android. These +are provided either the `init_external` or `init_hosted` function. These give `rustls-platform-verifier` +the resources it needs to make calls into the Android certificate verifier. + +As an example, if your Rust Android component which the "native" Android +part of your app calls at startup has an initialization, like this: +```rust ,ignore +#[export_name = "Java_com_orgname_android_rust_init"] +extern "C" fn java_init( + env: JNIEnv, + _class: JClass, + context: JObject, +) -> jboolean { + // ... initialize your app's other parts here. +} +``` + +In the simplest case, you should to insert a call to `rustls_platform_verifier::android::init_hosted()` here, +before any networking has a chance to run. This only needs to be called once and +the verifier will be valid for the lifetime of your app's process. + +```rust ,ignore +extern "C" fn java_init( + env: JNIEnv, + _class: JClass, + context: JObject, +) -> jboolean { + // ... initialize your app's other parts here. + + // Then, initialize the certificate verifier for future use. + rustls_platform_verifier::android::init_hosted(&env, context); +} +``` + +In more advanced cases, such as where your code already stores long-lived handles into +the Android environment, you can alternatively use `init_external`. This function takes +a `&'static` reference to something that implements the `android::Runtime` trait, which the +crate then uses to obtain the access when required to the JVM. + +## Credits +Made with ❤️ by the [1Password](https://1password.com/) and `rustls` teams. Portions of the Android and Windows verifier +implementations were adapted and referenced from Chromium's previous verifier implementations as well. + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/third_party/rustls-platform-verifier/src/android.rs b/third_party/rustls-platform-verifier/src/android.rs new file mode 100644 index 000000000..1e36bedf7 --- /dev/null +++ b/third_party/rustls-platform-verifier/src/android.rs @@ -0,0 +1,250 @@ +//! On Android, initialization must be done before any verification is attempted. +//! +//!
+//! Some manual setup is required outside of cargo to use this crate on Android. In order to use +//! Android’s certificate verifier, the crate needs to call into the JVM. A small Kotlin component +//! must be included in your app’s build to support rustls-platform-verifier. +//! +//! See the [crate's Android section][crate#android] for more details. +//!
+//! +//! # Examples +//! +//! ``` +//! // A typical entrypoint signature for obtaining the necessary pointers +//! pub fn android_init(raw_env: *mut c_void, raw_context: *mut c_void) -> Result<(), jni::errors::Error> { +//! let mut env = unsafe { JNIEnv::from_raw(raw_env as *mut jni::sys::JNIEnv).unwrap() }; +//! let context = unsafe { JObject::from_raw(raw_context as jni::sys::jobject) }; +//! rustls_platform_verifier::android::init_with_env(&mut env, context)?; +//! } +//! ``` + +use jni::errors::Error as JNIError; +use jni::objects::{Global, JClass, JClassLoader, JObject}; +use jni::strings::JNIStr; +use jni::{jni_sig, jni_str, Env, JavaVM}; +use once_cell::sync::OnceCell; + +static GLOBAL: OnceCell = OnceCell::new(); + +/// A layer to access the Android runtime which is hosting the current +/// application process. +/// +/// Generally this trait should be implemented in your Rust app component's FFI +/// initialization layer. +pub trait Runtime: Send + Sync { + /// Returns a handle to the current process' JVM. + fn java_vm(&self) -> &JavaVM; + /// Returns a reference to the current app's [Context]. + /// + /// [Context]: + fn context(&self) -> &Global>; + /// Returns a reference to the class returned by the current JVM's `getClassLoader` call. + fn class_loader(&self) -> &Global>; +} + +enum GlobalStorage { + Internal { + java_vm: JavaVM, + context: Global>, + loader: Global>, + }, + External(&'static dyn Runtime), +} + +impl GlobalStorage { + fn vm(&self) -> &JavaVM { + match self { + GlobalStorage::Internal { java_vm, .. } => java_vm, + GlobalStorage::External(runtime) => runtime.java_vm(), + } + } + + fn context(&self, env: &mut Env) -> Result { + let context = match self { + Self::Internal { context, .. } => context, + Self::External(global) => global.context(), + }; + + let loader = match self { + Self::Internal { loader, .. } => loader, + Self::External(global) => global.class_loader(), + }; + + Ok(GlobalContext { + context: env.new_global_ref(context)?, + loader: env.new_global_ref(loader)?, + }) + } +} + +pub(super) struct GlobalContext { + /// The Android application [Context](https://developer.android.com/reference/android/app/Application). + pub(super) context: Global>, + loader: Global>, +} + +fn global() -> &'static GlobalStorage { + GLOBAL + .get() + .expect("Expect rustls-platform-verifier to be initialized") +} + +/// Initialize given a typical Android NDK [`Env`] and [`JObject`] context. +/// +/// This method will setup and store an environment locally. This is useful if nothing else in your +/// application needs to access the Android runtime. +pub fn init_with_env(env: &mut Env, context: JObject) -> Result<(), JNIError> { + GLOBAL.get_or_try_init(|| -> Result<_, JNIError> { + let loader = env + .call_method( + &context, + jni_str!("getClassLoader"), + jni_sig!(() -> JClassLoader), + &[], + )? + .l()?; + let loader = env.cast_local::(loader)?; + + Ok(GlobalStorage::Internal { + java_vm: env.get_java_vm()?, + context: env.new_global_ref(context)?, + loader: env.new_global_ref(loader)?, + }) + })?; + Ok(()) +} + +/// Initialize with a runtime that can dynamically serve references to +/// the JVM, context, and class loader. +/// +/// This is the most flexible option, and is useful for advanced use cases. +/// +/// This function will never panic. +pub fn init_with_runtime(runtime: &'static dyn Runtime) { + GLOBAL.get_or_init(|| GlobalStorage::External(runtime)); +} + +/// Initialize with references to the JVM, context, and class loader. +/// +/// This is useful when you're already interacting with `jni-rs` wrapped objects and want to use +/// global references to objects for efficiency. +/// +/// This function will never panic. +/// +/// # Examples +/// +/// ``` +/// pub fn android_init(raw_env: *mut c_void, raw_context: *mut c_void) -> Result<(), jni::errors::Error> { +/// let mut env = unsafe { jni::EnvUnowned::from_raw(raw_env as *mut jni::sys::JNIEnv).unwrap() }; +/// let context = unsafe { JObject::from_raw(raw_context as jni::sys::jobject) }; +/// let loader = env.get_object_class(&context)?.get_class_loader(env)?; +/// +/// env.with_env(|env| { +/// rustls_platform_verifier::android::init_with_refs( +/// env.get_java_vm(), +/// env.new_global_ref(context)?, +/// env.new_global_ref(loader)?, +/// ); +/// }); +/// } +/// ``` +pub fn init_with_refs( + java_vm: JavaVM, + context: Global>, + loader: Global>, +) { + GLOBAL.get_or_init(|| GlobalStorage::Internal { + java_vm, + context, + loader, + }); +} + +/// Wrapper for JNI errors that will log and clear exceptions +/// It should generally be preferred to `jni::errors::Error` +#[derive(Debug)] +pub(super) struct Error; + +impl From for Error { + #[track_caller] + fn from(cause: JNIError) -> Self { + if let JNIError::JavaException = cause { + let _ = global() + .vm() + .with_top_local_frame(|env| -> Result<(), JNIError> { + env.exception_describe(); + env.exception_clear(); + Ok(()) + }); + } + + Self + } +} + +pub(super) struct LocalContext<'a, 'env> { + pub(super) env: &'a mut Env<'env>, + pub(super) global: GlobalContext, +} + +impl<'env> LocalContext<'_, 'env> { + /// Load a class from the application class loader + /// + /// This should be used instead of `JNIEnv::find_class` to ensure all classes + /// in the application can be found. + fn load_class(&mut self, name: &'static JNIStr) -> Result, Error> { + let name = self.env.new_string(name.to_str())?; + self.global + .loader + .load_class(self.env, name) + .map_err(Error::from) + } +} + +/// Borrow the Android application context and execute the closure +/// `with_context, ensuring locals are properly freed and exceptions +/// are cleared. +pub(super) fn with_context(f: F) -> Result +where + F: FnOnce(&mut LocalContext) -> Result, +{ + let global = global(); + // Use `attach_current_thread_for_scope()` to avoid permanently attaching any threads. + // See https://github.com/rustls/rustls-platform-verifier/pull/185. + global.vm().attach_current_thread_for_scope(|env| { + let global_context = global.context(env)?; + let mut context = LocalContext { + env, + global: global_context, + }; + f(&mut context) + }) +} + +/// Loads and caches a class on first use +pub(super) struct CachedClass { + name: &'static JNIStr, + class: OnceCell>>, +} + +impl CachedClass { + /// Creates a lazily initialized class reference to the class with `name`. + pub(super) const fn new(name: &'static JNIStr) -> Self { + Self { + name, + class: OnceCell::new(), + } + } + + /// Gets the cached class reference, loaded on first use + pub(super) fn get(&self, cx: &mut LocalContext) -> Result<&JClass<'static>, Error> { + let class = self.class.get_or_try_init(|| -> Result<_, Error> { + let class = cx.load_class(self.name)?; + + Ok(cx.env.new_global_ref(class)?) + })?; + + Ok(class) + } +} diff --git a/third_party/rustls-platform-verifier/src/lib.rs b/third_party/rustls-platform-verifier/src/lib.rs new file mode 100644 index 000000000..8de20d75a --- /dev/null +++ b/third_party/rustls-platform-verifier/src/lib.rs @@ -0,0 +1,81 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + +use std::sync::Arc; + +#[cfg(feature = "dbg")] +use rustls::crypto::CryptoProvider; +#[cfg(feature = "dbg")] +use rustls::pki_types::CertificateDer; +use rustls::{client::WantsClientCert, ClientConfig, ConfigBuilder, WantsVerifier}; + +mod verification; +pub use verification::Verifier; + +// Build the Android module when generating docs so that +// the Android-specific functions are included regardless of +// the host. +#[cfg(any(all(doc, docsrs), target_os = "android"))] +#[cfg_attr(docsrs, doc(cfg(target_os = "android")))] +pub mod android; + +/// Exposed for debugging certificate issues with standalone tools. +/// +/// This is not intended for production use, you should use [`BuilderVerifierExt`] or +/// [`ConfigVerifierExt`] instead. +#[cfg(feature = "dbg")] +pub fn verifier_for_dbg( + root: CertificateDer<'static>, + crypto_provider: Arc, +) -> Arc { + Arc::new(Verifier::new_with_fake_root(root, crypto_provider)) +} + +/// Extension trait to help configure [`ClientConfig`]s with the platform verifier. +pub trait BuilderVerifierExt { + /// Configures the `ClientConfig` with the platform verifier. + /// + /// ```rust + /// use rustls::ClientConfig; + /// use rustls_platform_verifier::BuilderVerifierExt; + /// let config = ClientConfig::builder() + /// .with_platform_verifier() + /// .unwrap() + /// .with_no_client_auth(); + /// ``` + fn with_platform_verifier( + self, + ) -> Result, rustls::Error>; +} + +impl BuilderVerifierExt for ConfigBuilder { + fn with_platform_verifier( + self, + ) -> Result, rustls::Error> { + let verifier = Verifier::new(self.crypto_provider().clone())?; + Ok(self + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier))) + } +} + +/// Extension trait to help build a [`ClientConfig`] with the platform verifier. +pub trait ConfigVerifierExt { + /// Build a [`ClientConfig`] with the platform verifier and the default `CryptoProvider`. + /// + /// ```rust + /// use rustls::ClientConfig; + /// use rustls_platform_verifier::ConfigVerifierExt; + /// let config = ClientConfig::with_platform_verifier(); + /// ``` + fn with_platform_verifier() -> Result; +} + +impl ConfigVerifierExt for ClientConfig { + fn with_platform_verifier() -> Result { + Ok(ClientConfig::builder() + .with_platform_verifier()? + .with_no_client_auth()) + } +} diff --git a/third_party/rustls-platform-verifier/src/verification/android.rs b/third_party/rustls-platform-verifier/src/verification/android.rs new file mode 100644 index 000000000..744795f46 --- /dev/null +++ b/third_party/rustls-platform-verifier/src/verification/android.rs @@ -0,0 +1,336 @@ +use jni::{ + jni_sig, jni_str, + objects::{JByteArray, JObject, JObjectArray, JString, JValue}, + signature::MethodSignature, + Env, +}; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier}; +use rustls::crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}; +use rustls::pki_types; +use rustls::Error::InvalidCertificate; +use rustls::{ + CertificateError, DigitallySignedStruct, Error as TlsError, OtherError, SignatureScheme, +}; +use std::sync::Arc; + +use super::{log_server_cert, ALLOWED_EKUS}; +use crate::android::{with_context, CachedClass}; + +static CERT_VERIFIER_CLASS: CachedClass = + CachedClass::new(jni_str!("org.rustls.platformverifier.CertificateVerifier")); + +// Note: Keep these in sync with the Kotlin enum. +#[derive(Debug)] +enum VerifierStatus { + Ok, + Unavailable, + Expired, + UnknownCert, + Revoked, + InvalidEncoding, + InvalidExtension, +} + +// Android's certificate verifier ignores this outright and this is considered the +// official recommendation. See https://bugs.chromium.org/p/chromium/issues/detail?id=627154. +const AUTH_TYPE: &str = "RSA"; + +/// A TLS certificate verifier that utilizes the Android platform verifier. +#[derive(Debug)] +pub struct Verifier { + /// Testing only: The root CA certificate to trust. + #[cfg(any(test, feature = "ffi-testing"))] + test_only_root_ca_override: Option>, + crypto_provider: Arc, +} + +#[cfg(any(test, feature = "ffi-testing"))] +impl Drop for Verifier { + fn drop(&mut self) { + with_context::<_, ()>(|cx| { + let cert_verifier_class = CERT_VERIFIER_CLASS.get(cx)?; + cx.env + .call_static_method( + cert_verifier_class, + jni_str!("clearMockRoots"), + jni_sig!(() -> void), + &[], + )? + .v()?; + Ok(()) + }) + .expect("failed to clear test roots") + } +} + +impl Verifier { + /// Creates a new instance of a TLS certificate verifier that utilizes the + /// Android certificate facilities. + #[cfg_attr(docsrs, doc(cfg(all())))] + pub fn new(crypto_provider: Arc) -> Result { + Ok(Self { + #[cfg(any(test, feature = "ffi-testing"))] + test_only_root_ca_override: None, + crypto_provider, + }) + } + + /// Creates a test-only TLS certificate verifier which trusts our fake root CA cert. + #[cfg(any(test, feature = "ffi-testing"))] + pub(crate) fn new_with_fake_root( + root: pki_types::CertificateDer<'static>, + crypto_provider: Arc, + ) -> Self { + Self { + test_only_root_ca_override: Some(root), + crypto_provider, + } + } + + fn verify_certificate( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName<'_>, + ocsp_response: Option<&[u8]>, + now: pki_types::UnixTime, + ) -> Result<(), TlsError> { + let certificate_chain = std::iter::once(end_entity) + .chain(intermediates) + .map(|cert| cert.as_ref()) + .enumerate(); + + // Convert the unix timestamp into milliseconds, expressed as + // an i64 to later be converted into a Java Long used for a Date + // constructor. + let now: i64 = (now.as_secs() * 1000) + .try_into() + .map_err(|_| TlsError::FailedToGetCurrentTime)?; + + let verification_result = with_context(|cx| { + let cert_verifier_class = CERT_VERIFIER_CLASS.get(cx)?; + + let cert_list = { + let array = JObjectArray::::new( + cx.env, + intermediates.len() + 1, + &JByteArray::null(), + )?; + + for (idx, cert) in certificate_chain { + let cert_buffer = cx.env.byte_array_from_slice(cert)?; + array.set_element(cx.env, idx, cert_buffer)?; + } + + array + }; + + let allowed_ekus = { + let array = + JObjectArray::::new(cx.env, ALLOWED_EKUS.len(), &JString::null())?; + + for (idx, eku) in ALLOWED_EKUS.iter().enumerate() { + let eku = cx.env.new_string(eku.to_str().expect( + "ALLOWED_EKUS entries are ASCII constants -- always valid UTF-8", + ))?; + array.set_element(cx.env, idx, eku)? + } + + array + }; + + let ocsp_response = match ocsp_response { + Some(b) => cx.env.byte_array_from_slice(b)?, + None => JByteArray::null(), + }; + + #[cfg(any(test, feature = "ffi-testing"))] + { + if let Some(mock_root) = &self.test_only_root_ca_override { + let mock_root = cx.env.byte_array_from_slice(mock_root)?; + cx.env + .call_static_method( + cert_verifier_class, + jni_str!("addMockRoot"), + jni_sig!((byte[]) -> void), + &[JValue::from(&mock_root)], + )? + .v() + .expect("failed to add test root") + } + } + + const VERIFIER_CALL: MethodSignature<'static, 'static> = jni_sig!( + ( + android.content.Context, + JString, + JString, + JString[], + byte[], + jlong, + byte[][] + ) -> org.rustls.platformverifier.VerificationResult + ); + + let server_name = cx.env.new_string(server_name.to_str())?; + let auth_type = cx.env.new_string(AUTH_TYPE)?; + + let result = cx + .env + .call_static_method( + cert_verifier_class, + jni_str!("verifyCertificateChain"), + VERIFIER_CALL, + &[ + JValue::from(cx.global.context.as_ref()), + JValue::from(&server_name), + JValue::from(&auth_type), + JValue::from(&JObject::from(allowed_ekus)), + JValue::from(&ocsp_response), + JValue::Long(now), + JValue::from(&JObject::from(cert_list)), + ], + )? + .l()?; + + Ok(extract_result_info(cx.env, result)) + }); + + match verification_result { + Ok((status, maybe_msg)) => { + // `maybe_msg` is safe to log as its exactly what the system told us. + // + // The branches which unwrap it will never fail since the Kotlin side always sets it + // for the variants. + match status { + VerifierStatus::Ok => { + // If everything else was OK, check the hostname. + rustls::client::verify_server_name( + &rustls::server::ParsedCertificate::try_from(end_entity)?, + server_name, + ) + } + VerifierStatus::Unavailable => Err(TlsError::General(String::from( + "No system trust stores available", + ))), + VerifierStatus::Expired => Err(InvalidCertificate(CertificateError::Expired)), + VerifierStatus::UnknownCert => { + log::warn!("certificate was not trusted: {}", maybe_msg.unwrap()); + Err(InvalidCertificate(CertificateError::UnknownIssuer)) + } + VerifierStatus::Revoked => { + log::warn!("certificate was revoked: {}", maybe_msg.unwrap()); + Err(InvalidCertificate(CertificateError::Revoked)) + } + VerifierStatus::InvalidEncoding => { + Err(InvalidCertificate(CertificateError::BadEncoding)) + } + VerifierStatus::InvalidExtension => Err(InvalidCertificate( + CertificateError::Other(OtherError(std::sync::Arc::new(super::EkuError))), + )), + } + } + Err(e) => Err(TlsError::General(format!( + "failed to call native verifier: {e:?}", + ))), + } + } +} + +fn extract_result_info(env: &mut Env<'_>, result: JObject<'_>) -> (VerifierStatus, Option) { + let status_code = env + .get_field(&result, jni_str!("code"), jni_sig!(jint)) + .and_then(|code| code.i()) + .unwrap(); + + let status = match status_code { + 0 => VerifierStatus::Ok, + 1 => VerifierStatus::Unavailable, + 2 => VerifierStatus::Expired, + 3 => VerifierStatus::UnknownCert, + 4 => VerifierStatus::Revoked, + 5 => VerifierStatus::InvalidEncoding, + 6 => VerifierStatus::InvalidExtension, + i => unreachable!("unknown status code: {i}"), + }; + + // Extract the `String?`. + let msg = env + .get_field(result, jni_str!("message"), jni_sig!(java.lang.String)) + .and_then(|m| m.l()) + .map(|s| { + if s.is_null() { + None + } else { + env.cast_local::(s) + .and_then(|s| s.try_to_string(env)) + .ok() + } + }) + .unwrap(); + (status, msg) +} + +#[cfg_attr(docsrs, doc(cfg(all())))] +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + let ocsp_data = if !ocsp_response.is_empty() { + Some(ocsp_response) + } else { + None + }; + + match self.verify_certificate(end_entity, intermediates, server_name, ocsp_data, now) { + Ok(()) => Ok(rustls::client::danger::ServerCertVerified::assertion()), + Err(e) => { + // This error only tells us what the system errored with, so it doesn't leak anything + // sensitive. + log::error!("failed to verify TLS certificate: {}", e); + Err(e) + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.crypto_provider + .signature_verification_algorithms + .supported_schemes() + } +} diff --git a/third_party/rustls-platform-verifier/src/verification/apple.rs b/third_party/rustls-platform-verifier/src/verification/apple.rs new file mode 100644 index 000000000..8cf0eef4d --- /dev/null +++ b/third_party/rustls-platform-verifier/src/verification/apple.rs @@ -0,0 +1,311 @@ +use std::sync::Arc; + +use core_foundation::date::CFDate; +use core_foundation_sys::date::kCFAbsoluteTimeIntervalSince1970; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier}; +use rustls::crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}; +use rustls::pki_types; +use rustls::{ + CertificateError, DigitallySignedStruct, Error as TlsError, OtherError, SignatureScheme, +}; +use security_framework::{ + certificate::SecCertificate, policy::SecPolicy, secure_transport::SslProtocolSide, + trust::SecTrust, +}; + +use super::log_server_cert; +use crate::verification::invalid_certificate; + +mod errors { + pub(super) use security_framework_sys::base::{ + errSecCertificateRevoked, errSecCreateChainFailed, errSecHostNameMismatch, + errSecInvalidExtendedKeyUsage, + }; +} + +#[allow(clippy::as_conversions)] +fn system_time_to_cfdate(time: pki_types::UnixTime) -> Result { + // SAFETY: The interval is defined by macOS externally, but is always present and never modified at runtime + // since its a global variable. + // + // See https://developer.apple.com/documentation/corefoundation/kcfabsolutetimeintervalsince1970. + let unix_adjustment = unsafe { kCFAbsoluteTimeIntervalSince1970 as u64 }; + + // Convert a system timestamp based off the UNIX epoch into the + // Apple epoch used by all `CFAbsoluteTime` values. + // Subtracting Durations with sub() will panic on overflow + time.as_secs() + .checked_sub(unix_adjustment) + .ok_or(TlsError::FailedToGetCurrentTime) + .map(|epoch| CFDate::new(epoch as f64)) +} + +/// A TLS certificate verifier that utilizes the Apple platform certificate facilities. +#[derive(Debug)] +pub struct Verifier { + /// Extra trust anchors to add to the verifier above and beyond those provided by + /// the system-provided trust stores. + extra_roots: Vec, + /// Testing only: The root CA certificate to trust. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: Option, + crypto_provider: Arc, +} + +impl Verifier { + /// Creates a new instance of a TLS certificate verifier that utilizes the Apple certificate + /// facilities. + #[cfg_attr(docsrs, doc(cfg(all())))] + pub fn new(crypto_provider: Arc) -> Result { + Ok(Self { + extra_roots: Vec::new(), + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: None, + crypto_provider, + }) + } + + /// Creates a new instance of a TLS certificate verifier that utilizes the Apple certificate + /// facilities with the addition of extra root certificates to trust. + /// + /// See [Verifier::new] for the external requirements the verifier needs. + #[cfg_attr(docsrs, doc(cfg(not(target_os = "android"))))] + pub fn new_with_extra_roots( + roots: impl IntoIterator>, + crypto_provider: Arc, + ) -> Result { + let extra_roots = roots + .into_iter() + .map(|root| { + SecCertificate::from_der(&root) + .map_err(|_| TlsError::InvalidCertificate(CertificateError::BadEncoding)) + }) + .collect::, _>>()?; + Ok(Self { + extra_roots, + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: None, + crypto_provider, + }) + } + + /// Creates a test-only TLS certificate verifier which trusts our fake root CA cert. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + pub(crate) fn new_with_fake_root( + root: pki_types::CertificateDer<'static>, + crypto_provider: Arc, + ) -> Self { + Self { + extra_roots: Vec::new(), + test_only_root_ca_override: Some(SecCertificate::from_der(root.as_ref()).unwrap()), + crypto_provider, + } + } + + fn verify_certificate( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &str, + ocsp_response: Option<&[u8]>, + now: pki_types::UnixTime, + ) -> Result<(), TlsError> { + let certificates: Vec = std::iter::once(end_entity.as_ref()) + .chain(intermediates.iter().map(|cert| cert.as_ref())) + .map(|cert| { + SecCertificate::from_der(cert) + .map_err(|_| TlsError::InvalidCertificate(CertificateError::BadEncoding)) + }) + .collect::, _>>()?; + + // Create our verification policy suitable for verifying TLS chains. + // This uses the "default" verification engine and parameters, the same as Windows. + // + // The protocol side should be set to `server` for a client to verify server TLS + // certificates. + // + // The server name will be required to match what the end-entity certificate reports + // + // Ref: https://developer.apple.com/documentation/security/1392592-secpolicycreatessl + let policy = SecPolicy::create_ssl(SslProtocolSide::SERVER, Some(server_name)); + + // Create our trust evaluation context/chain. + // + // Apple requires that the certificate to be verified is always first in the array, and we + // always place the end-entity certificate at the start. + // + // Ref: https://developer.apple.com/documentation/security/1401555-sectrustcreatewithcertificates + let mut trust_evaluation = SecTrust::create_with_certificates(&certificates, &[policy]) + .map_err(|e| TlsError::General(e.to_string()))?; + + // Tell the system that we want to consider the certificates evaluation at the point + // in time that `rustls` provided. + let now = system_time_to_cfdate(now)?; + trust_evaluation + .set_trust_verify_date(&now) + .map_err(|e| invalid_certificate(e.to_string()))?; + + // If we have OCSP response data, make sure the system makes use of it. + if let Some(ocsp_response) = ocsp_response { + trust_evaluation + .set_trust_ocsp_response(std::iter::once(ocsp_response)) + .map_err(|e| invalid_certificate(e.to_string()))?; + } + + #[cfg(not(any(test, feature = "ffi-testing", feature = "dbg")))] + let extra_roots = self.extra_roots.as_slice(); + + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + let extra_roots: Vec<_> = self + .extra_roots + .iter() + .chain(self.test_only_root_ca_override.as_ref()) + .cloned() + .collect(); + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + let extra_roots = extra_roots.as_slice(); + + // If any extra roots were provided by the user (or tests), provide them to the trust + // evaluation regardless of their system trust settings or status. + if !extra_roots.is_empty() { + trust_evaluation + .set_anchor_certificates(extra_roots) + .map_err(|e| TlsError::Other(OtherError(Arc::new(e))))?; + + // We want to trust both the system-installed and the extra roots. This must be set + // since calling `SecTrustSetAnchorCertificates` "disables the trusting of any + // anchors other than the ones specified by this function call" by default. + trust_evaluation + .set_trust_anchor_certificates_only(false) + .map_err(|e| TlsError::Other(OtherError(Arc::new(e))))?; + } + + // When testing, support using fake roots and ignoring default roots present on the system for + // consistency/reproducibility reasons. + // + // XXX: This does not currently limit revocation from fetching information online, or prevent + // the downloading of root CAs. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + { + if self.test_only_root_ca_override.is_some() { + // XXX: The test root was already provided to the trust evaluation as an extra root. + // We only need to stop use of the default system-installed roots. + + // As per [Apple's docs], building and verifying a certificate chain will + // search through the system and keychain to find certificates that it + // needs to try and construct a trust chain back to the root. + // + // `SecTrustSetAnchorCertificatesOnly` must be called after setting custom + // anchor certificates, which "disables trusting any other anchors than the ones passed in + // with the `SecTrustSetAnchorCertificates` function". + // + // [Apple's docs]: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/trust/creating_a_trust_object + trust_evaluation + .set_trust_anchor_certificates_only(true) + .expect("failed to tell trust to only use provided anchors"); + } + } + + let trust_error = match trust_evaluation.evaluate_with_error() { + Ok(()) => return Ok(()), + Err(e) => e, + }; + + let err_code = trust_error.code(); + + let err = err_code + .try_into() + .map_err(|_| ()) + .and_then(|code| { + // Only map the errors we need for tests. + match code { + errors::errSecHostNameMismatch => Ok(TlsError::InvalidCertificate( + CertificateError::NotValidForName, + )), + errors::errSecCreateChainFailed => Ok(TlsError::InvalidCertificate( + CertificateError::UnknownIssuer, + )), + errors::errSecInvalidExtendedKeyUsage => Ok(TlsError::InvalidCertificate( + CertificateError::Other(OtherError(Arc::new(super::EkuError))), + )), + errors::errSecCertificateRevoked => { + Ok(TlsError::InvalidCertificate(CertificateError::Revoked)) + } + _ => Err(()), + } + }) + // Fallback to an error containing the description and specific error code so that + // the exact error cause can be looked up easily. + .unwrap_or_else(|_| invalid_certificate(format!("{trust_error}: {err_code}"))); + + Err(err) + } +} + +#[cfg_attr(docsrs, doc(cfg(all())))] +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + // Convert IP addresses to name strings to ensure match check on leaf certificate. + // Ref: https://developer.apple.com/documentation/security/1392592-secpolicycreatessl + let server = server_name.to_str(); + + let ocsp_data = if !ocsp_response.is_empty() { + Some(ocsp_response) + } else { + None + }; + + match self.verify_certificate(end_entity, intermediates, &server, ocsp_data, now) { + Ok(()) => Ok(rustls::client::danger::ServerCertVerified::assertion()), + Err(e) => { + // This error only tells us what the system errored with, so it doesn't leak anything + // sensitive. + log::error!("failed to verify TLS certificate: {}", e); + Err(e) + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.crypto_provider + .signature_verification_algorithms + .supported_schemes() + } +} diff --git a/third_party/rustls-platform-verifier/src/verification/mod.rs b/third_party/rustls-platform-verifier/src/verification/mod.rs new file mode 100644 index 000000000..9d947ff28 --- /dev/null +++ b/third_party/rustls-platform-verifier/src/verification/mod.rs @@ -0,0 +1,86 @@ +#[cfg(any(windows, target_vendor = "apple"))] +use std::sync::Arc; + +#[cfg(all( + any(unix, target_arch = "wasm32"), + not(target_os = "android"), + not(target_vendor = "apple"), +))] +mod others; + +#[cfg(all( + any(unix, target_arch = "wasm32"), + not(target_os = "android"), + not(target_vendor = "apple"), +))] +pub use others::Verifier; + +#[cfg(target_vendor = "apple")] +mod apple; + +#[cfg(target_vendor = "apple")] +pub use apple::Verifier; + +#[cfg(target_os = "android")] +pub(crate) mod android; + +#[cfg(target_os = "android")] +pub use android::Verifier; + +#[cfg(windows)] +mod windows; + +#[cfg(windows)] +pub use windows::Verifier; + +/// An EKU was invalid for the use case of verifying a server certificate. +/// +/// This error is used primarily for tests. +#[cfg_attr(windows, allow(dead_code))] // not used by windows verifier +#[derive(Debug, PartialEq)] +pub(crate) struct EkuError; + +impl std::fmt::Display for EkuError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("certificate had invalid extensions") + } +} + +impl std::error::Error for EkuError {} + +// Log the certificate we are verifying so that we can try and find what may be wrong with it +// if we need to debug a user's situation. +fn log_server_cert(_end_entity: &rustls::pki_types::CertificateDer<'_>) { + #[cfg(feature = "cert-logging")] + { + use base64::Engine; + log::debug!( + "verifying certificate: {}", + base64::engine::general_purpose::STANDARD.encode(_end_entity.as_ref()) + ); + } +} + +// Unknown certificate error shorthand. Used when we need to construct an "Other" certificate +// error with a platform specific error message. +#[cfg(any(windows, target_vendor = "apple"))] +fn invalid_certificate(reason: impl Into) -> rustls::Error { + rustls::Error::InvalidCertificate(rustls::CertificateError::Other(rustls::OtherError( + Arc::from(Box::from(reason.into())), + ))) +} + +/// List of EKUs that one or more of that *must* be in the end-entity certificate. +/// +/// Legacy server-gated crypto OIDs are assumed to no longer be in use. +/// +/// Currently supported: +/// - id-kp-serverAuth +// TODO: Chromium also allows for `OID_ANY_EKU` on Android. +#[cfg(target_os = "windows")] +// XXX: Windows requires that we NUL terminate EKU strings. +// See https://github.com/rustls/rustls-platform-verifier/issues/126#issuecomment-2306232794. +const ALLOWED_EKUS: &[windows_sys::core::PCSTR] = + &[windows_sys::Win32::Security::Cryptography::szOID_PKIX_KP_SERVER_AUTH]; +#[cfg(target_os = "android")] +pub const ALLOWED_EKUS: &[&std::ffi::CStr] = &[c"1.3.6.1.5.5.7.3.1"]; diff --git a/third_party/rustls-platform-verifier/src/verification/others.rs b/third_party/rustls-platform-verifier/src/verification/others.rs new file mode 100644 index 000000000..440e32abd --- /dev/null +++ b/third_party/rustls-platform-verifier/src/verification/others.rs @@ -0,0 +1,189 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::client::WebPkiServerVerifier; +use rustls::pki_types; +use rustls::{ + crypto::CryptoProvider, CertificateError, DigitallySignedStruct, Error as TlsError, OtherError, + SignatureScheme, +}; + +use super::log_server_cert; + +/// A TLS certificate verifier that uses the system's root store and WebPKI. +#[derive(Debug)] +pub struct Verifier { + // We currently keep one set of certificates per-verifier so that + // recreating the verifier will pull fresh root certificates from disk, + // picking up on any changes that might have been made since. + inner: Arc, +} + +impl Verifier { + /// Creates a new verifier whose certificate validation is provided by + /// WebPKI, using root certificates provided by the platform. + #[cfg_attr(docsrs, doc(cfg(all())))] + pub fn new(crypto_provider: Arc) -> Result { + Self::new_inner([], None, crypto_provider) + } + + /// Creates a new verifier whose certificate validation is provided by + /// WebPKI, using root certificates provided by the platform and augmented by + /// the provided extra root certificates. + #[cfg_attr(docsrs, doc(cfg(not(target_os = "android"))))] + pub fn new_with_extra_roots( + extra_roots: impl IntoIterator>, + crypto_provider: Arc, + ) -> Result { + Self::new_inner(extra_roots, None, crypto_provider) + } + + /// Creates a test-only TLS certificate verifier which trusts our fake root CA cert. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + pub(crate) fn new_with_fake_root( + root: pki_types::CertificateDer<'static>, + crypto_provider: Arc, + ) -> Self { + Self::new_inner([], Some(root), crypto_provider) + .expect("failed to create verifier with fake root") + } + + /// Creates a new verifier whose certificate validation is provided by + /// WebPKI, using root certificates provided by the platform and augmented by + /// the provided extra root certificates. + fn new_inner( + extra_roots: impl IntoIterator>, + #[allow(unused)] // test_root is only used in tests + test_root: Option>, + crypto_provider: Arc, + ) -> Result { + let mut root_store = rustls::RootCertStore::empty(); + + // For testing only: load fake root cert, instead of native/WebPKI roots + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + { + if let Some(test_root) = test_root { + root_store.add(test_root)?; + return Ok(Self { + inner: WebPkiServerVerifier::builder_with_provider( + root_store.into(), + crypto_provider.clone(), + ) + .build() + .map_err(|e| TlsError::Other(OtherError(Arc::new(e))))?, + }); + } + } + + // While we ignore invalid certificates from the system, we forward errors from + // parsing the extra roots to the caller. + for cert in extra_roots { + root_store.add(cert)?; + } + + #[cfg(all( + unix, + not(target_os = "android"), + not(target_vendor = "apple"), + not(target_arch = "wasm32"), + ))] + { + let result = rustls_native_certs::load_native_certs(); + let (added, ignored) = root_store.add_parsable_certificates(result.certs); + if ignored > 0 { + log::warn!("{ignored} platform CA root certificates were ignored due to errors"); + } + + for error in result.errors { + log::warn!("Error loading CA root certificate: {error}"); + } + + // Don't return an error if this fails when other roots have already been loaded via + // `new_with_extra_roots`. It leads to extra failure cases where connections would otherwise still work. + if root_store.is_empty() { + return Err(rustls::Error::General( + "No CA certificates were loaded from the system".to_owned(), + )); + } else { + log::debug!("Loaded {added} CA root certificates from the system"); + } + } + + // No compiled-in trust store. wasm has no OS roots here; caller-supplied + // extra roots are the only anchors. + #[cfg(target_arch = "wasm32")] + if root_store.is_empty() { + return Err(rustls::Error::General( + "No CA certificates were loaded from the system".to_owned(), + )); + } + + Ok(Self { + inner: WebPkiServerVerifier::builder_with_provider( + root_store.into(), + crypto_provider.clone(), + ) + .build() + .map_err(|e| TlsError::Other(OtherError(Arc::new(e))))?, + }) + } +} + +#[cfg_attr(docsrs, doc(cfg(all())))] +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + self.inner + .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) + .map_err(map_webpki_errors) + // This only contains information from the system or other public + // bits of the TLS handshake, so it can't leak anything. + .map_err(|e| { + log::error!("failed to verify TLS certificate: {}", e); + e + }) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + +fn map_webpki_errors(err: TlsError) -> TlsError { + match &err { + TlsError::InvalidCertificate(CertificateError::InvalidPurpose) + | TlsError::InvalidCertificate(CertificateError::InvalidPurposeContext { .. }) => { + TlsError::InvalidCertificate(CertificateError::Other(OtherError(Arc::new( + super::EkuError, + )))) + } + _ => err, + } +} diff --git a/third_party/rustls-platform-verifier/src/verification/windows.rs b/third_party/rustls-platform-verifier/src/verification/windows.rs new file mode 100644 index 000000000..6b71c6f8f --- /dev/null +++ b/third_party/rustls-platform-verifier/src/verification/windows.rs @@ -0,0 +1,792 @@ +//! `Verifier` implementation for Windows targets. +//! +//! The design of the rustls-native-certs crate for Windows doesn't work +//! completely enough. In general it is hard to emulate enough of what +//! Windows does to be compatible with all users' configurations, especially +//! when corporate MitM proxies or custom CAs or complex trust policies are +//! used. Instead, delegate to Windows's own certificate validation engine +//! directly. +//! +//! This implementation was modeled on: +//! * Chromium's [cert_verify_proc_win.cc] and [x509_util_win.cc] +//! * Golang's [root_windows.go] +//! * [Microsoft's Documentation] and [Microsoft's Example] +//! +//! [cert_verify_proc_win.cc]: +//! [x509_util_win.cc]: +//! [root_windows.go]: +//! [Microsoft's Documentation]: +//! [Microsoft's Example]: + +use std::{ + convert::TryInto, + mem::{self, MaybeUninit}, + os::raw::c_void, + ptr::{self, NonNull}, + sync::Arc, +}; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier}; +use rustls::crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}; +use rustls::pki_types; +use rustls::{ + CertificateError, DigitallySignedStruct, Error as TlsError, Error::InvalidCertificate, + SignatureScheme, +}; +use windows_sys::Win32::{ + Foundation::{ + CERT_E_CN_NO_MATCH, CERT_E_EXPIRED, CERT_E_INVALID_NAME, CERT_E_UNTRUSTEDROOT, + CERT_E_WRONG_USAGE, CRYPT_E_REVOKED, FILETIME, TRUE, + }, + Security::Cryptography::{ + CertAddEncodedCertificateToStore, CertCloseStore, CertCreateCertificateChainEngine, + CertFreeCertificateChain, CertFreeCertificateChainEngine, CertFreeCertificateContext, + CertGetCertificateChain, CertOpenStore, CertSetCertificateContextProperty, + CertVerifyCertificateChainPolicy, HTTPSPolicyCallbackData, AUTHTYPE_SERVER, + CERT_CHAIN_CACHE_END_CERT, CERT_CHAIN_CONTEXT, + CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS, CERT_CHAIN_POLICY_PARA, + CERT_CHAIN_POLICY_SSL, CERT_CHAIN_POLICY_STATUS, + CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, CERT_CHAIN_REVOCATION_CHECK_END_CERT, + CERT_CONTEXT, CERT_OCSP_RESPONSE_PROP_ID, CERT_SET_PROPERTY_IGNORE_PERSIST_ERROR_FLAG, + CERT_STORE_ADD_ALWAYS, CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG, CERT_STORE_PROV_MEMORY, + CERT_STRONG_SIGN_PARA, CERT_TRUST_IS_PARTIAL_CHAIN, CERT_TRUST_IS_UNTRUSTED_ROOT, + CERT_USAGE_MATCH, CRYPT_INTEGER_BLOB, CTL_USAGE, HCERTSTORE, USAGE_MATCH_TYPE_AND, + X509_ASN_ENCODING, + }, +}; + +use super::{log_server_cert, ALLOWED_EKUS}; + +// The `windows-sys` definition for `CERT_CHAIN_PARA` does not take old OS versions +// into account so we define it ourselves for better OS backwards compat. +// In the future a compile-time size assertion can be added against the upstream type to help stay in sync. +#[allow(non_camel_case_types, non_snake_case)] +#[repr(C)] +struct CERT_CHAIN_PARA { + pub cbSize: u32, + pub RequestedUsage: CERT_USAGE_MATCH, + pub RequestedIssuancePolicy: CERT_USAGE_MATCH, + pub dwUrlRetrievalTimeout: u32, + pub fCheckRevocationFreshnessTime: i32, // BOOL + pub dwRevocationFreshnessTime: u32, + pub pftCacheResync: *mut FILETIME, + // XXX: `pStrongSignPara` and `dwStrongSignFlags` might or might not be defined on the current system. It started + // being available in Windows 8. See https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-cert_chain_para + #[cfg(not(target_vendor = "win7"))] + pub pStrongSignPara: *const CERT_STRONG_SIGN_PARA, + #[cfg(not(target_vendor = "win7"))] + pub dwStrongSignFlags: u32, +} + +// Same workaround with CERT_CHAIN_PARA +#[allow(non_camel_case_types, non_snake_case)] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CERT_CHAIN_ENGINE_CONFIG { + pub cbSize: u32, + pub hRestrictedRoot: HCERTSTORE, + pub hRestrictedTrust: HCERTSTORE, + pub hRestrictedOther: HCERTSTORE, + pub cAdditionalStore: u32, + pub rghAdditionalStore: *mut HCERTSTORE, + pub dwFlags: u32, + pub dwUrlRetrievalTimeout: u32, + pub MaximumCachedCertificates: u32, + pub CycleDetectionModulus: u32, + pub hExclusiveRoot: HCERTSTORE, + pub hExclusiveTrustedPeople: HCERTSTORE, + // XXX: `dwExclusiveFlags` started being available in Windows 8 and Windows Server 2012 + // See https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-cert_chain_engine_config + #[cfg(not(target_vendor = "win7"))] + pub dwExclusiveFlags: u32, +} + +use crate::verification::invalid_certificate; + +// SAFETY: see method implementation +unsafe impl ZeroedWithSize for CERT_CHAIN_PARA { + fn zeroed_with_size() -> Self { + // SAFETY: `CERT_CHAIN_PARA` only contains pointers and integers, which are safe to zero. + // Additionally, MSDN states you *MUST* zero all unused fields. + let mut new: Self = unsafe { mem::zeroed() }; + new.cbSize = Self::SIZE; + new + } +} + +// SAFETY: see method implementation +unsafe impl ZeroedWithSize for HTTPSPolicyCallbackData { + fn zeroed_with_size() -> Self { + // SAFETY: zeroed is needed here since it contains a union. + let mut new: Self = unsafe { mem::zeroed() }; + new.Anonymous.cbSize = Self::SIZE; + new + } +} + +// SAFETY: see method implementation +unsafe impl ZeroedWithSize for CERT_CHAIN_POLICY_PARA { + fn zeroed_with_size() -> Self { + // SAFETY: This structure only contains integers and pointers. + let mut new: Self = unsafe { mem::zeroed() }; + new.cbSize = Self::SIZE; + new + } +} + +// SAFETY: see method implementation +unsafe impl ZeroedWithSize for CERT_CHAIN_ENGINE_CONFIG { + fn zeroed_with_size() -> Self { + // SAFETY: This structure only contains integers and pointers. + let mut new: Self = unsafe { mem::zeroed() }; + new.cbSize = Self::SIZE; + new + } +} + +struct CertChain { + inner: NonNull, +} + +impl CertChain { + fn verify_chain_policy( + &self, + mut server_null_terminated: Vec, + ) -> Result { + let mut extra_params = HTTPSPolicyCallbackData::zeroed_with_size(); + extra_params.dwAuthType = AUTHTYPE_SERVER; + // `server_null_terminated` outlives `extra_params`. + extra_params.pwszServerName = server_null_terminated.as_mut_ptr(); + + let mut params = CERT_CHAIN_POLICY_PARA::zeroed_with_size(); + // Ignore any errors when trying to obtain OCSP revocation information. + // This is also done in OpenSSL, Secure Transport from Apple, etc. + params.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS; + // `extra_params` outlives `params`. + params.pvExtraPolicyPara = NonNull::from(&mut extra_params).cast::().as_ptr(); + + let mut status: MaybeUninit = MaybeUninit::uninit(); + + // SAFETY: The certificate chain is non-null, `params` is valid for reads, and its valid to write to `status`. + let res = unsafe { + CertVerifyCertificateChainPolicy( + CERT_CHAIN_POLICY_SSL, + self.inner.as_ptr(), + ¶ms, + status.as_mut_ptr(), + ) + }; + + // This should rarely, if ever, be false since it would imply no TLS verification + // is currently possible on the system: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certverifycertificatechainpolicy#return-value + if res != TRUE { + return Err(TlsError::General(String::from( + "TLS certificate verification was unavailable on the system!", + ))); + } + + // SAFETY: The verification call was checked to have succeeded, so the status + // is written correctly and initialized. + let status = unsafe { status.assume_init() }; + Ok(status) + } +} + +impl Drop for CertChain { + fn drop(&mut self) { + // SAFETY: The pointer is guaranteed to be non-null. + unsafe { CertFreeCertificateChain(self.inner.as_ptr()) } + } +} + +/// A representation of a certificate. +/// +/// The `CertificateStore` must be opened with the correct flags to ensure the +/// certificate may outlive it; see the `CertificateStore` documentation. +struct Certificate { + inner: NonNull, +} + +impl Certificate { + /// Sets the specified property of this certificate context. + /// + /// ### Safety + /// `prop_data` must be a valid pointer for the property type. + unsafe fn set_property( + &mut self, + prop_id: u32, + prop_data: *const c_void, + ) -> Result<(), TlsError> { + // SAFETY: `cert` points to a valid certificate context and the OCSP data is valid to read. + call_with_last_error(|| { + (CertSetCertificateContextProperty( + self.inner.as_ptr(), + prop_id, + CERT_SET_PROPERTY_IGNORE_PERSIST_ERROR_FLAG, + prop_data, + ) == TRUE) + .then_some(()) + }) + } +} + +impl Drop for Certificate { + fn drop(&mut self) { + // SAFETY: The certificate context is non-null and points to a valid location. + unsafe { CertFreeCertificateContext(self.inner.as_ptr()) }; + } +} + +#[derive(Debug)] +struct CertEngine { + inner: NonNull, // HCERTENGINECONTEXT +} + +impl CertEngine { + fn new_with_extra_roots( + roots: impl IntoIterator>, + ) -> Result { + let mut exclusive_store = CertificateStore::new()?; + for root in roots { + exclusive_store.add_cert(&root)?; + } + + let mut config = CERT_CHAIN_ENGINE_CONFIG::zeroed_with_size(); + config.hExclusiveRoot = exclusive_store.inner.as_ptr(); + + let mut engine = EnginePtr::NULL; + + // XXX: Due to the redefinition of `CERT_CHAIN_ENGINE_CONFIG`, we need to do pointer casts + // in order to pass our expanded structure into `CertCreateCertificateChainEngine`. + // See also `CERT_CHAIN_PARA` casting below. + let config = NonNull::from(&config).cast().as_ptr(); + // SAFETY: `engine` is valid to be written to and the config is valid to be read. + let res = unsafe { CertCreateCertificateChainEngine(config, &mut engine) }; + + #[allow(clippy::as_conversions)] + let engine = call_with_last_error(|| match NonNull::new(engine as *mut c_void) { + Some(c) if res == TRUE => Some(c), + _ => None, + })?; + Ok(Self { inner: engine }) + } + + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + fn new_with_fake_root(root: &[u8]) -> Result { + use windows_sys::Win32::Security::Cryptography::{ + CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL, CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE, + }; + + let mut root_store = CertificateStore::new()?; + root_store.add_cert(root)?; + + let mut config = CERT_CHAIN_ENGINE_CONFIG::zeroed_with_size(); + // We use these flags for the following reasons: + // + // - CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL is used in an attempt to stop Windows from using the internet to + // fetch anything during the tests, regardless of what test data is used. + // + // - CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE is used as a minor performance optimization to allow Windows to reuse + // data inside of a test and avoid any extra parsing, etc, it might need to do pulling directly from the store each time. + // + // Ref: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-cert_chain_engine_config + config.dwFlags = CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL | CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE; + config.hExclusiveRoot = root_store.inner.as_ptr(); + + let mut engine = EnginePtr::NULL; + // Same workaround with as above when creating the engine. + let config = NonNull::from(&config).cast().as_ptr(); + // SAFETY: `engine` is valid to be written to and the config is valid to be read. + let res = unsafe { CertCreateCertificateChainEngine(config, &mut engine) }; + + #[allow(clippy::as_conversions)] + let engine = call_with_last_error(|| match NonNull::new(engine as *mut c_void) { + Some(c) if res == TRUE => Some(c), + _ => None, + })?; + + Ok(Self { inner: engine }) + } +} + +impl Drop for CertEngine { + fn drop(&mut self) { + // SAFETY: The engine pointer is guaranteed to be non-null. + unsafe { CertFreeCertificateChainEngine(EnginePtr::from_raw(self.inner)) }; + } +} + +// SAFETY: We know no other threads is mutating the `CertEngine`, because it would require `unsafe`. +// Across the FFI, `CertGetCertificateChain` don't mutate it either. +unsafe impl Sync for CertEngine {} +// SAFETY: All methods of `CertEngine`, including `Drop`, are safe to be called from other +// threads, because all contained resources are owned by Windows and we only maintain reference counted handles to them. +unsafe impl Send for CertEngine {} + +/// An in-memory Windows certificate store. +/// +/// # Safety +/// +/// `CertificateStore` creates `Certificate` objects that may outlive the +/// `CertificateStore`. This is only safe to do if the certificate store is +/// constructed with `CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG`. +struct CertificateStore { + inner: NonNull, // HCERTSTORE + // In production code, this is always `None`. + // + // During tests, we set this to `Some` as the tests use a + // custom verification engine that only uses specific roots. + engine: Option, // HCERTENGINECONTEXT +} + +impl Drop for CertificateStore { + fn drop(&mut self) { + // SAFETY: See the `CertificateStore` documentation. + unsafe { CertCloseStore(self.inner.as_ptr(), 0) }; + } +} + +impl CertificateStore { + /// Creates a new, in-memory certificate store. + fn new() -> Result { + let store = call_with_last_error(|| { + // SAFETY: Called with valid constants and result is checked to be non-null. + // The `CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG` flag is critical; + // see the `CertificateStore` documentation for more info. + NonNull::new(unsafe { + CertOpenStore( + CERT_STORE_PROV_MEMORY, + 0, // Set to zero since this uses `PROV_MEMORY`. + 0, // This field shouldn't be used. + CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG, + ptr::null(), + ) + }) + })?; + + // Use the system's default root store and rules. + Ok(Self { + inner: store, + engine: None, + }) + } + + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + fn new_with_fake_root(root: &[u8]) -> Result { + let mut inner = Self::new()?; + + let mut root_store = CertificateStore::new()?; + root_store.add_cert(root)?; + + let engine = CertEngine::new_with_fake_root(root)?; + inner.engine = Some(engine); + + Ok(inner) + } + + /// Adds the provided certificate to the store. + /// + /// The certificate must be encoded as ASN.1 DER. + /// + /// Errors if the certificate was malformed and couldn't be added. + fn add_cert(&mut self, cert: &[u8]) -> Result { + let mut cert_context: *mut CERT_CONTEXT = ptr::null_mut(); + + // SAFETY: `inner` is a valid certificate store, and `cert` is a valid a byte array valid + // for reads, the correct length is being provided, and `cert_context` is valid to write to. + let res = unsafe { + CertAddEncodedCertificateToStore( + self.inner.as_ptr(), + X509_ASN_ENCODING, + cert.as_ptr(), + cert.len() + .try_into() + .map_err(|_| InvalidCertificate(CertificateError::BadEncoding))?, + CERT_STORE_ADD_ALWAYS, + &mut cert_context, + ) + }; + + // SAFETY: Constructing a `Certificate` is only safe if the store was + // created with the right flags; see the `CertificateStore` docs. + match (res, NonNull::new(cert_context)) { + (TRUE, Some(cert)) => Ok(Certificate { inner: cert }), + _ => Err(InvalidCertificate(CertificateError::BadEncoding)), + } + } + + fn new_chain_in( + &self, + certificate: &Certificate, + now: pki_types::UnixTime, + engine: Option<&CertEngine>, + ) -> Result { + let mut cert_chain = ptr::null_mut(); + + let mut parameters = CERT_CHAIN_PARA::zeroed_with_size(); + + #[allow(clippy::as_conversions)] + // https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-cert_usage_match + let usage = CERT_USAGE_MATCH { + dwType: USAGE_MATCH_TYPE_AND, + Usage: CTL_USAGE { + cUsageIdentifier: ALLOWED_EKUS.len() as u32, + rgpszUsageIdentifier: ALLOWED_EKUS.as_ptr() as *mut windows_sys::core::PSTR, + }, + }; + parameters.RequestedUsage = usage; + + #[allow(clippy::as_conversions)] + let time = { + /// Seconds between Jan 1st, 1601 and Jan 1, 1970. + const UNIX_ADJUSTMENT: std::time::Duration = + std::time::Duration::from_secs(11_644_473_600); + + let since_unix_epoch = now.as_secs(); + + // Convert the duration from the UNIX epoch to the Window one, and then convert + // the result into a `FILETIME` structure. + + let since_windows_epoch = since_unix_epoch + UNIX_ADJUSTMENT.as_secs(); + let intervals = (since_windows_epoch * 1_000_000_000) / 100; + + FILETIME { + dwLowDateTime: (intervals & u32::MAX as u64) as u32, + dwHighDateTime: (intervals >> 32) as u32, + } + }; + + // `CERT_CHAIN_REVOCATION_CHECK_END_CERT` only checks revocation for end cert. See the crate's revocation documentation + // for more details. + // `CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT` accumulates network retrievals timeouts + // to limit network time and improve performance. + // `CERT_CHAIN_CACHE_END_CERT` speeds up the common case of multiple connections to same server. + const FLAGS: u32 = CERT_CHAIN_REVOCATION_CHECK_END_CERT + | CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT + | CERT_CHAIN_CACHE_END_CERT; + + // Lowering URL retrieval timeout from default 15s to 10s to account for higher internet speeds + parameters.dwUrlRetrievalTimeout = 10 * 1000; // milliseconds + + // SAFETY: `cert` points to a valid certificate context, parameters is valid for reads, `cert_chain` is valid + // for writes, and the certificate store is valid and initialized. + let res = unsafe { + // XXX: Due to the redefinition of `CERT_CHAIN_PARA`, we need to do pointer casts + // in order to pass our expanded structure into `CertGetCertificateChain`. + // This is safe because the OS uses `cbSize` to know if the extra parameters + // are present or not. As we set `cbSize` correctly, the fields can be read from correctly. + let parameters = NonNull::from(¶meters).cast().as_ptr(); + + CertGetCertificateChain( + match engine { + Some(eng) => EnginePtr::from_raw(eng.inner), + None => EnginePtr::NULL, + }, + certificate.inner.as_ptr(), + &time, + self.inner.as_ptr(), + parameters, + FLAGS, + ptr::null_mut(), + &mut cert_chain, + ) + }; + + // XXX: Windows will internally map the chain's `TrustStatus.dwErrorStatus` to a `dwError` when + // a chain policy is verified, so we only check for errors there. + call_with_last_error(|| match NonNull::new(cert_chain) { + Some(c) if res == TRUE => Some(CertChain { inner: c }), + _ => None, + }) + } +} + +// `windows-sys` >= 0.60 +impl EnginePtr for *mut c_void { + fn from_raw(val: NonNull) -> Self { + val.as_ptr() + } + + const NULL: Self = ptr::null_mut(); +} + +// `windows-sys` 0.52-0.59 +impl EnginePtr for isize { + #[allow(clippy::as_conversions)] + fn from_raw(val: NonNull) -> Self { + val.as_ptr() as isize + } + + const NULL: Self = 0; +} + +/// An abstraction trait over the different ways various `windows-sys` versions represent +/// the type of `HCERTCHAINENGINE`. +trait EnginePtr: Sized { + fn from_raw(val: NonNull) -> Self; + + const NULL: Self; +} + +fn call_with_last_error Option>(mut call: F) -> Result { + if let Some(res) = call() { + Ok(res) + } else { + Err(TlsError::General( + std::io::Error::last_os_error().to_string(), + )) + } +} + +/// A TLS certificate verifier that utilizes the Windows certificate facilities. +#[derive(Debug)] +pub struct Verifier { + /// Testing only: The root CA certificate to trust. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: Option>, + crypto_provider: Arc, + /// Extra trust anchors to add to the verifier above and beyond those provided by + /// the system-provided trust stores. + extra_roots: Option, +} + +impl Verifier { + /// Creates a new instance of a TLS certificate verifier that utilizes the + /// Windows certificate facilities. + #[cfg_attr(docsrs, doc(cfg(all())))] + pub fn new(crypto_provider: Arc) -> Result { + Ok(Self { + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: None, + crypto_provider, + extra_roots: None, + }) + } + + /// Creates a new instance of a TLS certificate verifier that utilizes the + /// Windows certificate facilities and augmented by the provided extra root certificates. + #[cfg_attr(docsrs, doc(cfg(not(target_os = "android"))))] + pub fn new_with_extra_roots( + roots: impl IntoIterator>, + crypto_provider: Arc, + ) -> Result { + let cert_engine = CertEngine::new_with_extra_roots(roots)?; + Ok(Self { + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + test_only_root_ca_override: None, + crypto_provider, + extra_roots: Some(cert_engine), + }) + } + + /// Creates a test-only TLS certificate verifier which trusts our fake root CA cert. + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + pub(crate) fn new_with_fake_root( + root: pki_types::CertificateDer<'static>, + crypto_provider: Arc, + ) -> Self { + Self { + test_only_root_ca_override: Some(root), + crypto_provider, + extra_roots: None, + } + } + + /// Verifies a certificate and its chain for the specified `server`. + /// + /// Return `Ok(())` if the certificate was valid. + fn verify_certificate( + &self, + primary_cert: &[u8], + intermediate_certs: &[&[u8]], + server: &[u8], + ocsp_data: Option<&[u8]>, + now: pki_types::UnixTime, + ) -> Result<(), TlsError> { + #[cfg(any(test, feature = "ffi-testing", feature = "dbg"))] + let mut store = match self.test_only_root_ca_override.as_ref() { + Some(test_only_root_ca_override) => { + CertificateStore::new_with_fake_root(test_only_root_ca_override)? + } + None => CertificateStore::new()?, + }; + + #[cfg(not(any(test, feature = "ffi-testing", feature = "dbg")))] + let mut store = CertificateStore::new()?; + + let mut primary_cert = store.add_cert(primary_cert)?; + + for cert in intermediate_certs.iter().copied() { + store.add_cert(cert)?; + } + + if let Some(ocsp_data) = ocsp_data { + #[allow(clippy::as_conversions)] + let data = CRYPT_INTEGER_BLOB { + cbData: ocsp_data.len().try_into().map_err(|_| { + invalid_certificate("Malformed OCSP response stapled to server certificate") + })?, + pbData: ocsp_data.as_ptr() as *mut u8, + }; + + // SAFETY: `data` is a valid pointer and matches the property ID. + unsafe { + primary_cert.set_property( + CERT_OCSP_RESPONSE_PROP_ID, + NonNull::from(&data).cast::().as_ptr(), + )?; + } + } + + // Encode UTF-16, null-terminated + let server: Vec = server + .iter() + .map(|c| u16::from(*c)) + .chain(Some(0)) + .collect(); + + let mut cert_chain = store.new_chain_in(&primary_cert, now, store.engine.as_ref())?; + + // We only use `TrustStatus` here because it hasn't had verification performed on it. + // SAFETY: The pointer is guaranteed to be non-null. + let cert_error_status = unsafe { *cert_chain.inner.as_ptr() } + .TrustStatus + .dwErrorStatus; + + let extra_roots_may_needed = + (cert_error_status & (CERT_TRUST_IS_PARTIAL_CHAIN | CERT_TRUST_IS_UNTRUSTED_ROOT)) != 0; + + // If we have extra roots and building the chain gave us an error, we try to build a + // new one with the extra roots. + if extra_roots_may_needed && self.extra_roots.is_some() { + let mut store = CertificateStore::new()?; + + for cert in intermediate_certs.iter().copied() { + store.add_cert(cert)?; + } + + cert_chain = store.new_chain_in(&primary_cert, now, self.extra_roots.as_ref())?; + } + + let status = cert_chain.verify_chain_policy(server)?; + + if status.dwError == 0 { + return Ok(()); + } + + // Only map the errors we have tests for. + #[allow(clippy::as_conversions)] + let win_error = status.dwError as i32; + Err(match win_error { + CERT_E_CN_NO_MATCH | CERT_E_INVALID_NAME => { + InvalidCertificate(CertificateError::NotValidForName) + } + CRYPT_E_REVOKED => InvalidCertificate(CertificateError::Revoked), + CERT_E_EXPIRED => InvalidCertificate(CertificateError::Expired), + CERT_E_UNTRUSTEDROOT => InvalidCertificate(CertificateError::UnknownIssuer), + CERT_E_WRONG_USAGE => InvalidCertificate(CertificateError::InvalidPurpose), + error_num => { + let err = std::io::Error::from_raw_os_error(error_num); + // The included error message has both the description and raw OS error code. + invalid_certificate(err.to_string()) + } + }) + } +} + +#[cfg_attr(docsrs, doc(cfg(all())))] +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + let name = server_name.to_str(); + + let intermediate_certs: Vec<&[u8]> = intermediates.iter().map(|c| c.as_ref()).collect(); + + let ocsp_data = if !ocsp_response.is_empty() { + Some(ocsp_response) + } else { + None + }; + + match self.verify_certificate( + end_entity.as_ref(), + &intermediate_certs, + name.as_bytes(), + ocsp_data, + now, + ) { + Ok(()) => Ok(rustls::client::danger::ServerCertVerified::assertion()), + Err(e) => { + // SAFETY: + // Errors are our own custom errors, WinAPI errors, or static strings. + log::error!("failed to verify TLS certificate: {}", e); + Err(e) + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.crypto_provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.crypto_provider + .signature_verification_algorithms + .supported_schemes() + } +} + +/// A trait to represent an object that can be safely created with all zero values +/// and have a size assigned to it. +/// +/// # Safety +/// +/// This has the same safety requirements as [std::mem::zeroed]. +unsafe trait ZeroedWithSize: Sized { + const SIZE: u32 = { + let size = core::mem::size_of::(); + + // NB: `TryInto` isn't stable in const yet. + #[allow(clippy::as_conversions)] + if size <= u32::MAX as usize { + size as u32 + } else { + panic!("structure was larger then DWORD") + } + }; + + /// Returns a zeroed structure with its structure size (`cbSize`) field set to the correct value. + fn zeroed_with_size() -> Self; +} diff --git a/vite.config.ts b/vite.config.ts index 7007109b2..c915cfc9e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ singleQuote: true, ignorePatterns: [ 'crates/fspy_detours_sys/detours', + 'third_party/rustls-platform-verifier', 'crates/vt_graph/run-config.ts', '**/fixtures/*/snapshots', 'packages/vite-task-client/src/index.d.ts',