diff --git a/.spelling b/.spelling index ea386895d..a5b75af48 100644 --- a/.spelling +++ b/.spelling @@ -556,3 +556,8 @@ addressability bumpable dereferenceable NonNull +crypto +rustls +TLS +verifier +Verifier diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c3b358f..08c54da40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Please see each crate's change log below: - [`data_privacy_macros`](./crates/data_privacy_macros/CHANGELOG.md) - [`data_privacy_macros_impl`](./crates/data_privacy_macros_impl/CHANGELOG.md) - [`fetch_hyper`](./crates/fetch_hyper/CHANGELOG.md) +- [`fetch_tls`](./crates/fetch_tls/CHANGELOG.md) - [`fundle`](./crates/fundle/CHANGELOG.md) - [`fundle_macros`](./crates/fundle_macros/CHANGELOG.md) - [`fundle_macros_impl`](./crates/fundle_macros_impl/CHANGELOG.md) diff --git a/Cargo.lock b/Cargo.lock index 82c9a6994..dce92cd5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,9 +234,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "automation" @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.3" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -832,9 +832,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.2.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", @@ -932,9 +932,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", @@ -987,9 +987,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embedded-io" @@ -1058,11 +1058,12 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fetch_hyper" -version = "0.1.4" +version = "0.2.0" dependencies = [ "anyspawn", "bytes", "bytesbuf", + "fetch_tls", "futures", "http", "http-body-util", @@ -1093,6 +1094,22 @@ dependencies = [ "wiremock", ] +[[package]] +name = "fetch_tls" +version = "0.2.0" +dependencies = [ + "base64", + "http", + "insta", + "mutants", + "native-tls", + "ohno 0.3.4", + "rstest", + "rustls", + "rustls-pki-types", + "static_assertions", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1773,9 +1790,9 @@ dependencies = [ [[package]] name = "infinity_pool" -version = "0.8.18" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484aa4e251c49527ea35ce8c82f2a5e40d00cc0536c612301d92020eaeb2f370" +checksum = "5e77de9e116ed8eb5d865f630a3931a9c27a51f69a50838a2f1b4c4d20043e10" dependencies = [ "new_zealand", "num-integer", @@ -1871,9 +1888,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -1939,9 +1956,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -1987,9 +2004,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" @@ -2127,9 +2144,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -2303,9 +2320,9 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.32.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" +checksum = "368afaed344110f40b179bb8fbe54bc52d98f9bd2b281799ef32487c2650c956" dependencies = [ "futures-channel", "futures-executor", @@ -2369,9 +2386,9 @@ dependencies = [ [[package]] name = "pastey" -version = "0.2.3" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pct-str" @@ -3003,9 +3020,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -3786,9 +3803,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -3799,9 +3816,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3809,9 +3826,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -3822,9 +3839,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 7e6d70bf9..54760e332 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,8 @@ cachet_tier = { path = "crates/cachet_tier", default-features = false, version = data_privacy = { path = "crates/data_privacy", default-features = false, version = "0.11.2" } data_privacy_macros = { path = "crates/data_privacy_macros", default-features = false, version = "0.9.2" } data_privacy_macros_impl = { path = "crates/data_privacy_macros_impl", default-features = false, version = "0.9.2" } -fetch_hyper = { path = "crates/fetch_hyper", default-features = false, version = "0.1.4" } +fetch_hyper = { path = "crates/fetch_hyper", default-features = false, version = "0.2.0" } +fetch_tls = { path = "crates/fetch_tls", default-features = false, version = "0.2.0" } fundle = { path = "crates/fundle", default-features = false, version = "0.3.2" } fundle_macros = { path = "crates/fundle_macros", default-features = false, version = "0.3.2" } fundle_macros_impl = { path = "crates/fundle_macros_impl", default-features = false, version = "0.3.2" } @@ -61,6 +62,7 @@ alloc_tracker = { version = "0.5.9", default-features = false } allocator-api2 = { version = "0.4.0", default-features = false } anyhow = { version = "1.0.100", default-features = false } async-once-cell = { version = "0.5.0", default-features = false } +base64 = { version = "0.22.0", default-features = false, features = ["alloc"] } bolero = { version = "0.13.4", default-features = false } bumpalo = { version = "3.20.2", default-features = false } bytemuck = { version = "1.25.0", default-features = false } @@ -85,10 +87,10 @@ hashbrown = { version = "0.17.0", default-features = false } http = { version = "1.4.1", default-features = false, features = ["std"] } http-body = { version = "1.0.1", default-features = false } http-body-util = { version = "0.1.3", default-features = false } -hyper = { version = "1.7.0", default-features = false } +hyper = { version = "1.10.1", default-features = false } hyper-rustls = { version = "0.27.9", default-features = false } hyper-tls = { version = "0.6.0", default-features = false } -hyper-util = { version = "0.1.16", default-features = false } +hyper-util = { version = "0.1.20", default-features = false } infinity_pool = { version = "0.8.1", default-features = false } insta = { version = "1.44.1", default-features = false } jiff = { version = "0.2.21", default-features = false } @@ -122,6 +124,7 @@ regex = { version = "1.12.2", default-features = false } rstest = { version = "0.26.0", default-features = false } rustc-hash = { version = "2.1.0", default-features = false } rustls = { version = "0.23.40", default-features = false } +rustls-pki-types = { version = "1.14.1", default-features = false } serde = { version = "1.0.228", default-features = false } serde_core = { version = "1.0.228", default-features = false } serde_json = { version = "1.0.145", default-features = false } diff --git a/crates/anyspawn/README.md b/crates/anyspawn/README.md index 1fa13d9d6..042ea2d54 100644 --- a/crates/anyspawn/README.md +++ b/crates/anyspawn/README.md @@ -54,7 +54,7 @@ contention-free, NUMA-friendly task dispatch. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQblHE7Bl8YSN4bb97k0EOW-rkbZQa-GdodS-cbCkeYjGZgZ-BhZIKCaGFueXNwYXduZTAuNS4ygmx0aHJlYWRfYXdhcmVlMC43LjI + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG5RxOwZfGEjeG2_e5NBDlvq5G2UGvhnaHUvnGwpHmIxmYGfgYWSCgmhhbnlzcGF3bmUwLjUuMoJsdGhyZWFkX2F3YXJlZTAuNy4y [__link0]: https://docs.rs/anyspawn/0.5.2/anyspawn/?search=Spawner [__link1]: https://docs.rs/anyspawn/0.5.2/anyspawn/?search=SpawnCustom [__link2]: https://docs.rs/anyspawn/0.5.2/anyspawn/?search=CustomSpawnerBuilder diff --git a/crates/bytesbuf/README.md b/crates/bytesbuf/README.md index cd654bbab..f3c492597 100644 --- a/crates/bytesbuf/README.md +++ b/crates/bytesbuf/README.md @@ -471,7 +471,7 @@ See the `mem::testing` module for details (requires `test-util` Cargo feature). This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQb3Um38gMny5obPDtS9we40N0baM-dMLra_2cbwUZ6yJrd_CNhZIGCaGJ5dGVzYnVmZTAuNS4y + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG91Jt_IDJ8uaGzw7UvcHuNDdG2jPnTC62v9nG8FGesia3fwjYWSBgmhieXRlc2J1ZmUwLjUuMg [__link0]: https://docs.rs/bytesbuf/0.5.2/bytesbuf/?search=BytesBuf [__link1]: https://docs.rs/bytesbuf/0.5.2/bytesbuf/?search=BytesView [__link10]: https://docs.rs/bytesbuf/0.5.2/bytesbuf/?search=BytesView diff --git a/crates/bytesbuf_io/README.md b/crates/bytesbuf_io/README.md index bac716d4e..9cae1b48f 100644 --- a/crates/bytesbuf_io/README.md +++ b/crates/bytesbuf_io/README.md @@ -35,7 +35,7 @@ types that produce or consume streams of bytes. These are in the `testing` modul This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbzALaooHm88wblzy9ny6Wy9IbBKVdX0-eOvkbIABdjy2GM0phZIGCa2J5dGVzYnVmX2lvZTAuNS4y + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG8wC2qKB5vPMG5c8vZ8ulsvSGwSlXV9Pnjr5GyAAXY8thjNKYWSBgmtieXRlc2J1Zl9pb2UwLjUuMg [__link0]: https://docs.io/bytesbuf [__link1]: https://docs.rs/bytesbuf_io/0.5.2/bytesbuf_io/?search=Read [__link2]: https://docs.rs/bytesbuf_io/0.5.2/bytesbuf_io/?search=Write diff --git a/crates/cachet/README.md b/crates/cachet/README.md index e2d202edd..88377b536 100644 --- a/crates/cachet/README.md +++ b/crates/cachet/README.md @@ -265,7 +265,7 @@ See the `telemetry_subscriber` example for a complete demonstration. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbg_hDqE88LP4bMh0J5Y4y4Osb0zDJ1kwqOsoblCGrm49Rx2thZIiCaGJ5dGVzYnVmZTAuNS4ygmZjYWNoZXRlMC42LjKCbWNhY2hldF9tZW1vcnllMC4zLjGCbmNhY2hldF9zZXJ2aWNlZTAuMi4ygmtjYWNoZXRfdGllcmUwLjIuMYJkdGlja2UwLjMuMoJndHJhY2luZ2YwLjEuNDSCaXVuaWZsaWdodGUwLjIuMg + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG4P4Q6hPPCz-GzIdCeWOMuDrG9MwydZMKjrKG5Qhq5uPUcdrYWSIgmhieXRlc2J1ZmUwLjUuMoJmY2FjaGV0ZTAuNi4ygm1jYWNoZXRfbWVtb3J5ZTAuMy4xgm5jYWNoZXRfc2VydmljZWUwLjIuMoJrY2FjaGV0X3RpZXJlMC4yLjGCZHRpY2tlMC4zLjKCZ3RyYWNpbmdmMC4xLjQ0gml1bmlmbGlnaHRlMC4yLjI [__link0]: https://docs.rs/cachet/0.6.2/cachet/?search=TimeToRefresh [__link1]: https://crates.io/crates/uniflight/0.2.2 [__link10]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=CacheTier diff --git a/crates/cachet_memory/README.md b/crates/cachet_memory/README.md index 9671c3ee0..8ce09318f 100644 --- a/crates/cachet_memory/README.md +++ b/crates/cachet_memory/README.md @@ -91,7 +91,7 @@ TTL/TTI unset or set them to a sufficiently high ceiling. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbN0kpRlU_G9QbWC713oa4KjsbRG6BIsW3BU8bzI21NivEBVphZIKCbWNhY2hldF9tZW1vcnllMC4zLjGCa2NhY2hldF90aWVyZTAuMi4x + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEGzdJKUZVPxvUG1gu9d6GuCo7G0RugSLFtwVPG8yNtTYrxAVaYWSCgm1jYWNoZXRfbWVtb3J5ZTAuMy4xgmtjYWNoZXRfdGllcmUwLjIuMQ [__link0]: https://docs.rs/cachet_memory/0.3.1/cachet_memory/?search=InMemoryCache [__link1]: https://docs.rs/cachet_memory/0.3.1/cachet_memory/?search=InMemoryCacheBuilder [__link10]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=CacheEntry::expires_after diff --git a/crates/cachet_service/README.md b/crates/cachet_service/README.md index 9670c19bb..df054f52b 100644 --- a/crates/cachet_service/README.md +++ b/crates/cachet_service/README.md @@ -45,7 +45,7 @@ let tier = ServiceAdapter::new(my_service); This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbcrlL8sHnAG4b1ofYj6gT3UEbqnvnufpKEjIbZAmyA7kxTiRhZIKCbmNhY2hldF9zZXJ2aWNlZTAuMi4ygmtjYWNoZXRfdGllcmUwLjIuMQ + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG3K5S_LB5wBuG9aH2I-oE91BG6p757n6ShIyG2QJsgO5MU4kYWSCgm5jYWNoZXRfc2VydmljZWUwLjIuMoJrY2FjaGV0X3RpZXJlMC4yLjE [__link0]: https://docs.rs/cachet_service/0.2.2/cachet_service/?search=ServiceAdapter [__link1]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=CacheTier [__link2]: https://docs.rs/cachet_service/0.2.2/cachet_service/?search=ServiceAdapter diff --git a/crates/cachet_tier/README.md b/crates/cachet_tier/README.md index 6a947eb45..d4de7de2a 100644 --- a/crates/cachet_tier/README.md +++ b/crates/cachet_tier/README.md @@ -74,7 +74,7 @@ for multi-tier caches with heterogeneous storage backends. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbSFGoN9aDWgMbkFPVkj7eiZMblYTgYHQyDnsb4bh5vMZ5KTlhZIGCa2NhY2hldF90aWVyZTAuMi4x + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG0hRqDfWg1oDG5BT1ZI-3omTG5WE4GB0Mg57G-G4ebzGeSk5YWSBgmtjYWNoZXRfdGllcmUwLjIuMQ [__link0]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=CacheTier [__link1]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=CacheEntry [__link2]: https://docs.rs/cachet_tier/0.2.1/cachet_tier/?search=Error diff --git a/crates/data_privacy/README.md b/crates/data_privacy/README.md index a1c48929a..ab76268bd 100644 --- a/crates/data_privacy/README.md +++ b/crates/data_privacy/README.md @@ -191,7 +191,7 @@ assert_eq!(output_buffer, "********"); This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbcz--FS0HmAUb4PVl4_uBbJcbNpizJAPcY7sbA9e9gxKHdnZhZIGCbGRhdGFfcHJpdmFjeWYwLjExLjI + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG3M_vhUtB5gFG-D1ZeP7gWyXGzaYsyQD3GO7GwPXvYMSh3Z2YWSBgmxkYXRhX3ByaXZhY3lmMC4xMS4y [__link0]: https://docs.rs/data_privacy/0.11.2/data_privacy/?search=Classified [__link1]: https://docs.rs/data_privacy/0.11.2/data_privacy/?search=Redactor [__link10]: https://docs.rs/data_privacy/0.11.2/data_privacy/?search=classified diff --git a/crates/fetch_hyper/CHANGELOG.md b/crates/fetch_hyper/CHANGELOG.md index c5f785ef7..56f77edc5 100644 --- a/crates/fetch_hyper/CHANGELOG.md +++ b/crates/fetch_hyper/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## [0.1.4] - 2026-06-02 +## [0.2.0] - 2026-06-02 + + - depend on `fetch_tls` crate - 🔧 Maintenance @@ -58,6 +60,7 @@ - 🧩 Miscellaneous - Update tool versions ([#462](https://github.com/microsoft/oxidizer/pull/462)) +>>>>>>> origin/main ## [0.1.2] - 2026-06-01 diff --git a/crates/fetch_hyper/Cargo.toml b/crates/fetch_hyper/Cargo.toml index d4176f0fb..2f21b575d 100644 --- a/crates/fetch_hyper/Cargo.toml +++ b/crates/fetch_hyper/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "fetch_hyper" description = "Hyper-based HTTP transport utilities for fetch." -version = "0.1.4" +version = "0.2.0" readme = "README.md" keywords = ["oxidizer", "hyper", "fetch", "http", "tls"] categories = ["network-programming"] @@ -20,6 +20,7 @@ repository = "https://github.com/microsoft/oxidizer/tree/main/crates/fetch_hyper allowed_external_types = [ # Workspace sibling crates "anyspawn::spawner::Spawner", + "fetch_tls::*", "http_extensions::*", "layered::*", "templated_uri::*", @@ -38,13 +39,19 @@ allowed_external_types = [ all-features = true [features] -rustls = ["dep:rustls", "dep:hyper-rustls"] -native-tls = ["dep:native-tls", "dep:hyper-tls", "dep:tokio-native-tls"] +rustls = ["dep:rustls", "dep:hyper-rustls", "fetch_tls/rustls"] +native-tls = [ + "dep:native-tls", + "dep:hyper-tls", + "dep:tokio-native-tls", + "fetch_tls/native-tls", +] [dependencies] # internal anyspawn = { workspace = true } bytesbuf = { workspace = true } +fetch_tls = { workspace = true } http_extensions = { workspace = true } layered = { workspace = true, features = ["dynamic-service"] } ohno = { workspace = true } @@ -76,6 +83,7 @@ tokio-native-tls = { workspace = true, optional = true } [dev-dependencies] anyspawn = { path = "../anyspawn", features = ["tokio"] } bytes = { workspace = true } +fetch_tls = { path = "../fetch_tls", features = ["rustls", "native-tls"] } futures = { workspace = true, features = ["std", "executor"] } http_extensions = { path = "../http_extensions", features = ["test-util"] } hyper-rustls = { workspace = true, features = ["http1", "http2"] } diff --git a/crates/fetch_hyper/README.md b/crates/fetch_hyper/README.md index 1b9aeeb6e..416dad749 100644 --- a/crates/fetch_hyper/README.md +++ b/crates/fetch_hyper/README.md @@ -50,12 +50,12 @@ The runtime is supplied entirely by the caller via an This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbLiTyV0MU86EbZU15e0PmecoboQ9jo59bnAEbyDXw04U13GlhYvRhcoQbbsmHAnNeS6Yb0KCzC29u2TcbXVFDR4jSRbUbztoQvIRhtHlhZIOCaGFueXNwYXduZTAuNS4ygmtmZXRjaF9oeXBlcmUwLjEuNIJvaHR0cF9leHRlbnNpb25zZTAuNC40 - [__link0]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=HyperTransportBuilder - [__link1]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=Connect - [__link2]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=HyperTransportBuilder::configure_hyper - [__link3]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=HyperTransport + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG27JhwJzXkumG9Cgswtvbtk3G11RQ0eI0kW1G87aELyEYbR5YWSDgmhhbnlzcGF3bmUwLjUuMoJrZmV0Y2hfaHlwZXJlMC4yLjCCb2h0dHBfZXh0ZW5zaW9uc2UwLjQuNA + [__link0]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=HyperTransportBuilder + [__link1]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=Connect + [__link2]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=HyperTransportBuilder::configure_hyper + [__link3]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=HyperTransport [__link4]: https://docs.rs/http_extensions/0.4.4/http_extensions/?search=RequestHandler - [__link5]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=HyperTransportBuilder::build + [__link5]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=HyperTransportBuilder::build [__link6]: https://docs.rs/anyspawn/0.5.2/anyspawn/?search=Spawner - [__link7]: https://docs.rs/fetch_hyper/0.1.4/fetch_hyper/?search=Connect + [__link7]: https://docs.rs/fetch_hyper/0.2.0/fetch_hyper/?search=Connect diff --git a/crates/fetch_hyper/src/builder.rs b/crates/fetch_hyper/src/builder.rs index 85a8b471f..d1445ae08 100644 --- a/crates/fetch_hyper/src/builder.rs +++ b/crates/fetch_hyper/src/builder.rs @@ -9,6 +9,7 @@ use std::marker::PhantomData; use std::time::Duration; use anyspawn::Spawner; +use fetch_tls::TlsBackend; use http::Version; use http_extensions::{HttpBodyBuilder, HttpRequest, HttpResponse, Result}; use hyper_util::client::legacy; @@ -20,7 +21,6 @@ use crate::HyperIo; use crate::connection::Connect; use crate::connection::hyper_handler::build_hyper_handler; use crate::options::{ConnectionLifetime, RequestFilter}; -use crate::tls::TlsBackend; /// A type-erased Hyper request handler. #[derive(Clone, Debug)] @@ -84,7 +84,7 @@ where /// /// ``` /// use anyspawn::Spawner; -/// use fetch_hyper::{HyperTransport, HyperTransportBuilder, TlsBackend}; +/// use fetch_hyper::{HyperTransport, HyperTransportBuilder}; /// use http_extensions::HttpBodyBuilder; /// use hyper_util::rt::TokioIo; /// use layered::Execute; @@ -104,7 +104,7 @@ where /// // performs expensive certificate/store initialization, so we skip it /// // here with `unreachable!()` — the async function below is never /// // actually called. -/// let tls: TlsBackend = unreachable!("doc example; never invoked"); +/// let tls: fetch_tls::TlsBackend = unreachable!("doc example; never invoked"); /// /// let transport: HyperTransport = HyperTransportBuilder::new( /// Execute::new(connect), @@ -200,8 +200,16 @@ where } /// Sets the negotiable HTTP versions for outgoing requests. + /// + /// # Panics + /// + /// Panics if `versions` is empty. #[must_use] pub fn supported_http_versions(mut self, versions: &[Version]) -> Self { + assert!( + !versions.is_empty(), + "supported_http_versions cannot be empty; configure at least one HTTP version (for example HTTP/1.1 or HTTP/2)" + ); self.supported_http_versions = versions.to_vec(); self } diff --git a/crates/fetch_hyper/src/connection/hyper_handler.rs b/crates/fetch_hyper/src/connection/hyper_handler.rs index 262d26751..329a5855c 100644 --- a/crates/fetch_hyper/src/connection/hyper_handler.rs +++ b/crates/fetch_hyper/src/connection/hyper_handler.rs @@ -191,6 +191,7 @@ mod tests { use anyspawn::Spawner; use bytes::Bytes; + use fetch_tls::TlsBackend; use http_body_util::BodyExt as _; use http_extensions::{HttpBodyBuilder, HttpRequestBuilder}; use layered::Service as _; @@ -200,7 +201,6 @@ mod tests { use crate::HyperTransport; use crate::options::{ConnectionLifetime, RequestFilter}; use crate::testing::{FakeConnector, create_hyper_error, fake_body_builder}; - use crate::tls::TlsBackend; fn tls() -> TlsBackend { native_tls::TlsConnector::new().unwrap().into() diff --git a/crates/fetch_hyper/src/lib.rs b/crates/fetch_hyper/src/lib.rs index becd43e1d..ab7643675 100644 --- a/crates/fetch_hyper/src/lib.rs +++ b/crates/fetch_hyper/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/fetch_hyper/logo.png")] #![doc(html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/fetch_hyper/favicon.ico")] @@ -56,4 +56,3 @@ pub use builder::{HyperTransport, HyperTransportBuilder}; pub use connection::{Connect, HyperIo}; pub use options::{ConnectionLifetime, RequestFilter}; pub use telemetry::ConnectionInfo; -pub use tls::TlsBackend; diff --git a/crates/fetch_hyper/src/testing.rs b/crates/fetch_hyper/src/testing.rs index 22c7ef052..a8e80f302 100644 --- a/crates/fetch_hyper/src/testing.rs +++ b/crates/fetch_hyper/src/testing.rs @@ -363,16 +363,17 @@ mod tests { use anyspawn::Spawner; use bytes::Bytes; + use fetch_tls::TlsBackend; use http_body_util::BodyExt; use layered::Service as _; use native_tls::TlsConnector; use seatbelt::RecoveryInfo; use crate::testing::{FakeConnector, TestError, create_test_request, fake_body_builder}; - use crate::{HyperTransportBuilder, RequestFilter, TlsBackend}; + use crate::{HyperTransportBuilder, RequestFilter}; fn build_tls() -> TlsBackend { - TlsBackend::NativeTls(TlsConnector::new().unwrap()) + TlsConnector::new().unwrap().into() } fn http_1_response() -> Bytes { diff --git a/crates/fetch_hyper/src/tls/connector.rs b/crates/fetch_hyper/src/tls/connector.rs index 7f96aeac8..33bcd56b1 100644 --- a/crates/fetch_hyper/src/tls/connector.rs +++ b/crates/fetch_hyper/src/tls/connector.rs @@ -4,7 +4,10 @@ //! An enum that wraps the `TLS` connector, dispatching to the configured backend. use std::marker::PhantomData; +#[cfg(any(feature = "rustls", test))] +use std::sync::Arc; +use fetch_tls::TlsBackend; use http::Version; use templated_uri::BaseUri; #[cfg(any(feature = "rustls", feature = "native-tls", test))] @@ -13,7 +16,6 @@ use tower::Service as _; #[cfg(any(feature = "rustls", feature = "native-tls", test))] use crate::connection::hyper_connector_adapter::HyperConnectorAdapter; use crate::options::RequestFilter; -use crate::tls::TlsBackend; use crate::{Connect, HyperIo}; /// An enum that wraps the `TLS` connector, dispatching to the correct backend at runtime. @@ -57,22 +59,38 @@ where #[expect(clippy::allow_attributes, reason = "expect would be unfulfilled when a TLS feature is enabled")] #[allow( unused_variables, + unreachable_patterns, clippy::needless_pass_by_value, - reason = "parameters are consumed only in feature-gated match arms" + reason = "parameters are consumed only in feature-gated match arms; the fallback `_` arm is unreachable when fetch_tls only carries variants whose features are enabled here" )] pub(crate) fn new(backend: TlsBackend, connector: C, request_filter: RequestFilter, supported_versions: &[Version]) -> Self { match backend { #[cfg(any(feature = "rustls", test))] TlsBackend::Rustls(config) => Self::Rustls( - build_rustls_connector(config, connector, request_filter, supported_versions), + build_rustls_connector(Arc::unwrap_or_clone(config), connector, request_filter, supported_versions), PhantomData, ), #[cfg(any(feature = "native-tls", test))] TlsBackend::NativeTls(native) => Self::NativeTls(build_native_tls_connector(native, connector, request_filter), PhantomData), + + // When `fetch_hyper` is built without any TLS feature but feature + // unification (e.g. during `cargo test --doc` across the workspace) + // still enables variants on `fetch_tls::TlsBackend`, the match + // arms above are cfg'd out. This unreachable arm keeps the match + // exhaustive in that configuration without affecting normal builds. + #[cfg(not(any(feature = "rustls", feature = "native-tls", test)))] + _ => no_tls_backend_unreachable(), } } } +#[cfg(not(any(feature = "rustls", feature = "native-tls", test)))] +#[cfg_attr(coverage_nightly, coverage(off))] +#[cold] +fn no_tls_backend_unreachable() -> ! { + unreachable!("`TlsBackend` variants cannot be constructed when no TLS feature is enabled in `fetch_hyper`") +} + // The internal ALPN selection only manifests through TLS handshakes against // a real HTTPS server, which is out of scope for these tests; the surviving // boolean mutations on `http1`/`http2` produce observably identical results @@ -80,7 +98,7 @@ where #[cfg(any(feature = "rustls", test))] #[cfg_attr(test, mutants::skip)] fn build_rustls_connector( - config: rustls::ClientConfig, + mut config: rustls::ClientConfig, connector: C, request_filter: RequestFilter, supported_versions: &[Version], @@ -89,6 +107,8 @@ where C: Connect, S: HyperIo, { + // hyper-rustls expects ALPN to be configured via enable_http1/enable_http2. + config.alpn_protocols.clear(); let builder = hyper_rustls::HttpsConnectorBuilder::new().with_tls_config(config); let builder = match request_filter { @@ -196,7 +216,8 @@ mod tests { .unwrap() .with_root_certificates(rustls::RootCertStore::empty()) .with_no_client_auth(); - TlsBackend::Rustls(config) + + config.into() } fn fake_connector() -> FakeConnector { diff --git a/crates/fetch_hyper/src/tls/mod.rs b/crates/fetch_hyper/src/tls/mod.rs index 76f59ae2d..ccfa21018 100644 --- a/crates/fetch_hyper/src/tls/mod.rs +++ b/crates/fetch_hyper/src/tls/mod.rs @@ -7,116 +7,3 @@ mod connector; pub(crate) use connector::TlsConnector; - -/// Selects and supplies the `TLS` backend used by the transport. -/// -/// When neither the `rustls` nor `native-tls` feature is enabled this enum -/// has no variants and is therefore uninhabited: the crate still compiles, -/// but a [`TlsBackend`] value cannot be constructed and the transport -/// cannot be used. Enable at least one `TLS` feature to make outbound -/// connections. -#[derive(Clone, Debug)] -#[allow( - clippy::allow_attributes, - clippy::large_enum_variant, - reason = "backend is not on hot path, we want to keep API clean so no Box<..>" -)] -pub enum TlsBackend { - /// Use the `rustls` backend with the given pre-built configuration. - #[cfg(any(feature = "rustls", test))] - Rustls(rustls::ClientConfig), - - /// Use the platform `native-tls` backend with the given connector. - #[cfg(any(feature = "native-tls", test))] - NativeTls(native_tls::TlsConnector), -} - -#[cfg(any(feature = "rustls", test))] -impl From for TlsBackend { - fn from(config: rustls::ClientConfig) -> Self { - Self::Rustls(config) - } -} - -#[cfg(any(feature = "rustls", test))] -impl From> for TlsBackend { - fn from(config: std::sync::Arc) -> Self { - Self::Rustls(std::sync::Arc::unwrap_or_clone(config)) - } -} - -#[cfg(any(feature = "native-tls", test))] -impl From for TlsBackend { - fn from(connector: native_tls::TlsConnector) -> Self { - Self::NativeTls(connector) - } -} - -#[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] -mod tests { - use super::*; - - #[test] - #[cfg_attr(miri, ignore)] - fn from_client_config_makes_rustls_variant() { - let provider = rustls::crypto::CryptoProvider::get_default() - .cloned() - .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider())); - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .unwrap() - .with_root_certificates(rustls::RootCertStore::empty()) - .with_no_client_auth(); - let backend: TlsBackend = config.into(); - assert!(matches!(backend, TlsBackend::Rustls(_))); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn from_arc_client_config_makes_rustls_variant() { - let provider = rustls::crypto::CryptoProvider::get_default() - .cloned() - .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider())); - let config = std::sync::Arc::new( - rustls::ClientConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .unwrap() - .with_root_certificates(rustls::RootCertStore::empty()) - .with_no_client_auth(), - ); - let backend: TlsBackend = config.into(); - assert!(matches!(backend, TlsBackend::Rustls(_))); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn from_native_tls_connector_makes_native_variant() { - let nc = native_tls::TlsConnector::new().unwrap(); - let backend: TlsBackend = nc.into(); - assert!(matches!(backend, TlsBackend::NativeTls(_))); - } - - #[test] - #[cfg_attr(miri, ignore)] - fn clone_preserves_variant() { - let nc = native_tls::TlsConnector::new().unwrap(); - let backend = TlsBackend::NativeTls(nc); - let cloned = backend.clone(); - assert!(matches!(backend, TlsBackend::NativeTls(_))); - assert!(matches!(cloned, TlsBackend::NativeTls(_))); - - let provider = rustls::crypto::CryptoProvider::get_default() - .cloned() - .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider())); - let config = rustls::ClientConfig::builder_with_provider(provider) - .with_safe_default_protocol_versions() - .unwrap() - .with_root_certificates(rustls::RootCertStore::empty()) - .with_no_client_auth(); - let rustls_backend: TlsBackend = config.into(); - let rustls_cloned = rustls_backend.clone(); - assert!(matches!(rustls_backend, TlsBackend::Rustls(_))); - assert!(matches!(rustls_cloned, TlsBackend::Rustls(_))); - } -} diff --git a/crates/fetch_hyper/tests/smoke.rs b/crates/fetch_hyper/tests/smoke.rs index d9ca687d2..6144587f4 100644 --- a/crates/fetch_hyper/tests/smoke.rs +++ b/crates/fetch_hyper/tests/smoke.rs @@ -11,7 +11,8 @@ use std::time::Duration; use anyspawn::Spawner; use bytes::Bytes; -use fetch_hyper::{HyperTransportBuilder, RequestFilter, TlsBackend}; +use fetch_hyper::{HyperTransportBuilder, RequestFilter}; +use fetch_tls::TlsBackend; use http::{Method, StatusCode, Version}; use http_extensions::{HttpBodyBuilder, HttpRequestBuilder, Result}; use hyper_util::rt::TokioIo; diff --git a/crates/fetch_tls/CHANGELOG.md b/crates/fetch_tls/CHANGELOG.md new file mode 100644 index 000000000..681c94d06 --- /dev/null +++ b/crates/fetch_tls/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +## [0.2.0] - 2026-06-02 + +- ✨ Features + + - release new `fetch_tls` crate diff --git a/crates/fetch_tls/Cargo.toml b/crates/fetch_tls/Cargo.toml new file mode 100644 index 000000000..7282cb31a --- /dev/null +++ b/crates/fetch_tls/Cargo.toml @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "fetch_tls" +description = "TLS configurations and APIs used by 'fetch' crate." +version = "0.2.0" +readme = "README.md" +keywords = ["oxidizer", "tls", "fetch", "http", "client"] +categories = ["network-programming"] + +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository = "https://github.com/microsoft/oxidizer/tree/main/crates/fetch_tls" + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + # ohno macro-generated trait impls + "ohno::enrichable::Enrichable", + "ohno::error_ext::ErrorExt", + # External dependencies surfaced in the public API + "http::version::Version", + # TLS backends (feature-gated) + "rustls::client::client_conn::ClientConfig", + "rustls::client::client_conn::ResolvesClientCert", + "rustls::crypto::CryptoProvider", + "rustls::verify::ServerCertVerifier", + "native_tls::TlsConnector", +] + +[package.metadata.docs.rs] +all-features = true + +[features] +default = [] +rustls = ["dep:rustls"] +native-tls = ["dep:native-tls"] + +[dependencies] +# internal +ohno = { workspace = true } + +# external +base64 = { workspace = true } +http = { workspace = true } +native-tls = { workspace = true, optional = true, features = ["alpn"] } +# No crypto provider feature is enabled here; callers supply one via +# `TlsBackendBuilder::configure_rustls`. See the crate docs for details. +rustls = { workspace = true, features = [ + "tls12", + "std", +], default-features = false, optional = true } +rustls-pki-types = { workspace = true, features = ["alloc", "std"] } + +[dev-dependencies] +insta = { workspace = true } +mutants = { workspace = true } +native-tls = { workspace = true, features = ["alpn"] } +rstest = { workspace = true } +# Test code unconditionally references both backends; pull them in as +# dev-dependencies so `cargo test` works with no features enabled. +rustls = { workspace = true, features = ["tls12", "std", "aws-lc-rs"], default-features = false } +static_assertions = { workspace = true } + +[lints] +workspace = true diff --git a/crates/fetch_tls/README.md b/crates/fetch_tls/README.md new file mode 100644 index 000000000..a28647bd1 --- /dev/null +++ b/crates/fetch_tls/README.md @@ -0,0 +1,63 @@ +
+ Fetch Tls Logo + +# Fetch Tls + +[![crate.io](https://img.shields.io/crates/v/fetch_tls.svg)](https://crates.io/crates/fetch_tls) +[![docs.rs](https://docs.rs/fetch_tls/badge.svg)](https://docs.rs/fetch_tls) +[![MSRV](https://img.shields.io/crates/msrv/fetch_tls)](https://crates.io/crates/fetch_tls) +[![CI](https://github.com/microsoft/oxidizer/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/microsoft/oxidizer/actions/workflows/main.yml) +[![Coverage](https://codecov.io/gh/microsoft/oxidizer/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/microsoft/oxidizer) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE) +This crate was developed as part of the Oxidizer project + +
+ +Backend-agnostic TLS configuration for HTTP clients. + +`fetch_tls` separates *what* TLS behavior an application wants from +*which* TLS implementation actually provides it. Applications describe +their TLS requirements once, and the HTTP client (or other consuming +library) decides which backend to materialize at runtime. + +## Two perspectives + +Applications work with [`TlsOptions`][__link0] (and its builder, +[`TlsOptionsBuilder`][__link1]) to describe what they want: leave the backend +choice entirely to the consuming library via +[`TlsOptions::builder`][__link2], pick a specific backend, wrap an already-built +backend, or use [`TlsOptions::default`][__link3] for backend-agnostic defaults. + +Libraries that adopt `fetch_tls` use [`TlsBackendBuilder`][__link4] to turn a +[`TlsOptions`][__link5] into a ready-to-use [`TlsBackend`][__link6]. The library +contributes the environment-specific pieces (such as the rustls crypto +provider and default certificate verifier) and decides which backend to +use when the application did not pin one. + +## Cargo features + +* `rustls` — enables the rustls backend. `fetch_tls` does not bundle a + crypto provider; the consuming library supplies one along with a + default server certificate verifier. +* `native-tls` — enables the platform native TLS backend (`SChannel` on + Windows, Security Framework on `macOS`, `OpenSSL` on Linux). + +With neither feature enabled, the API surface is limited to wrapping a +pre-built backend; attempting to build any other configuration returns +a [`BackendError`][__link7]. + + +
+ +This crate was developed as part of The Oxidizer Project. Browse this crate's source code. + + + [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG0APetLRG81kG2jqZKP1V1oAGzCNx4vdBBsCG91rLMGz6fsVYWSBgmlmZXRjaF90bHNlMC4yLjA + [__link0]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsOptions + [__link1]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsOptionsBuilder + [__link2]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsOptions::builder + [__link3]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsOptions::default + [__link4]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsBackendBuilder + [__link5]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsOptions + [__link6]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=TlsBackend + [__link7]: https://docs.rs/fetch_tls/0.2.0/fetch_tls/?search=BackendError diff --git a/crates/fetch_tls/favicon.ico b/crates/fetch_tls/favicon.ico new file mode 100644 index 000000000..b394ce24d --- /dev/null +++ b/crates/fetch_tls/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:118aa67d3e567980eae98e411c94eb6703b152121ff16fd286ad10c7406c98a8 +size 201783 diff --git a/crates/fetch_tls/logo.png b/crates/fetch_tls/logo.png new file mode 100644 index 000000000..2196837fc --- /dev/null +++ b/crates/fetch_tls/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd2869fb1f894691ce53146253fc98b5b00ff1bef9b1af443d4c4943cac7a6ff +size 88709 diff --git a/crates/fetch_tls/src/alpn.rs b/crates/fetch_tls/src/alpn.rs new file mode 100644 index 000000000..737020289 --- /dev/null +++ b/crates/fetch_tls/src/alpn.rs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! `ALPN` protocol mapping from supported HTTP versions. + +use http::Version; + +const HTTP_11_ALPN: &str = "http/1.1"; +const HTTP_2_ALPN: &str = "h2"; + +/// Maps configured HTTP versions to the advertised `ALPN` identifiers. +pub(crate) fn map_to_alpn(versions: &[Version]) -> &[&str] { + let http1 = supports_http1(versions); + let http2 = supports_http2(versions); + if http2 && http1 { + &[HTTP_2_ALPN, HTTP_11_ALPN] + } else if http2 { + &[HTTP_2_ALPN] + } else if http1 { + &[HTTP_11_ALPN] + } else { + &[] + } +} + +fn supports_http1(versions: &[Version]) -> bool { + versions.contains(&Version::HTTP_11) || versions.contains(&Version::HTTP_10) +} + +fn supports_http2(versions: &[Version]) -> bool { + versions.contains(&Version::HTTP_2) +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(&[Version::HTTP_11, Version::HTTP_2], &["h2", "http/1.1"])] + #[case(&[Version::HTTP_2], &["h2"])] + #[case(&[Version::HTTP_11], &["http/1.1"])] + #[case(&[Version::HTTP_10], &["http/1.1"])] + #[case(&[], &[])] + #[case(&[Version::HTTP_3], &[])] + #[case(&[Version::HTTP_10, Version::HTTP_2], &["h2", "http/1.1"])] + fn map_to_alpn(#[case] versions: &[Version], #[case] expected_str: &[&str]) { + assert_eq!(super::map_to_alpn(versions), expected_str); + } +} diff --git a/crates/fetch_tls/src/backend.rs b/crates/fetch_tls/src/backend.rs new file mode 100644 index 000000000..a67224aa9 --- /dev/null +++ b/crates/fetch_tls/src/backend.rs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Fully constructed TLS backends ready for use by an HTTP client. + +/// Error returned when materializing a [`TlsBackend`] from +/// [`TlsOptions`](super::TlsOptions) fails. +#[ohno::error] +pub struct BackendError; + +/// A fully constructed TLS backend ready for use by an HTTP client. +/// +/// Where [`TlsOptions`](super::TlsOptions) describes *how* to build a TLS +/// configuration, `TlsBackend` holds the result. Which variants are +/// available depends on the enabled Cargo features. +/// +/// Typically produced by [`TlsBackendBuilder`](super::TlsBackendBuilder); +/// construct directly only when wrapping a backend you have already built. +#[derive(Debug, Clone)] +#[allow( + clippy::allow_attributes, + clippy::large_enum_variant, + reason = "configuration object; boxing would clutter the public API without performance benefit" +)] +pub enum TlsBackend { + /// rustls backend, carrying a shared `rustls::ClientConfig`. + #[cfg(any(feature = "rustls", test))] + Rustls(std::sync::Arc<::rustls::ClientConfig>), + + /// Platform native TLS backend (`SChannel` on Windows, Security Framework + /// on `macOS`, `OpenSSL` on Linux). + #[cfg(any(feature = "native-tls", test))] + NativeTls(::native_tls::TlsConnector), +} + +#[cfg(any(feature = "rustls", test))] +impl From<::rustls::ClientConfig> for TlsBackend { + fn from(config: ::rustls::ClientConfig) -> Self { + Self::Rustls(std::sync::Arc::new(config)) + } +} + +#[cfg(any(feature = "rustls", test))] +impl From> for TlsBackend { + fn from(config: std::sync::Arc<::rustls::ClientConfig>) -> Self { + Self::Rustls(config) + } +} + +#[cfg(any(feature = "native-tls", test))] +impl From<::native_tls::TlsConnector> for TlsBackend { + fn from(connector: ::native_tls::TlsConnector) -> Self { + Self::NativeTls(connector) + } +} diff --git a/crates/fetch_tls/src/backend_builder.rs b/crates/fetch_tls/src/backend_builder.rs new file mode 100644 index 000000000..8490af841 --- /dev/null +++ b/crates/fetch_tls/src/backend_builder.rs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! [`TlsBackendBuilder`] — materializes [`TlsOptions`] into a [`TlsBackend`]. + +use http::Version; + +use crate::backend::{BackendError, TlsBackend}; +use crate::options::{SharedOptions, TlsOptions, TlsOptionsKind}; + +/// Builds a [`TlsBackend`] from a [`TlsOptions`] using environment-supplied +/// defaults. +/// +/// Lets HTTP client crates own platform and policy choices (such as which +/// crypto provider, root store, or default backend to use) without baking +/// them into `fetch_tls`. Each backend that needs environment state has its +/// own setter; the native-tls and pre-configured backends do not consult +/// these defaults. +/// +/// In addition to backend-specific defaults, a `TlsBackendBuilder` carries: +/// +/// - the default backend used when a [`TlsOptions`] does not pin one, and +/// - the default list of supported HTTP versions used when the options +/// builder did not set them. +/// +/// Use [`new`](Self::new) when no backend-specific state is required. +#[derive(Clone, Debug)] +pub struct TlsBackendBuilder { + #[cfg(any(feature = "rustls", test))] + pub(crate) rustls: Option, + + pub(crate) default: DefaultBackend, + pub(crate) supported_http_versions: Vec, +} + +/// Environment-supplied defaults specific to the rustls backend. +#[cfg(any(feature = "rustls", test))] +#[derive(Clone, Debug)] +pub(crate) struct RustlsDefaults { + pub(crate) crypto_provider: std::sync::Arc<::rustls::crypto::CryptoProvider>, + pub(crate) verifier: std::sync::Arc, +} + +impl TlsBackendBuilder { + /// Creates an empty builder. + /// + /// Sufficient for the native-tls and pre-configured backends. + /// Materializing a rustls backend from an empty builder returns a + /// [`BackendError`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the default HTTP versions used when the options builder did not + /// set them. + /// + /// # Panics + /// + /// Panics if `versions` is empty. + #[must_use] + pub fn supported_http_versions(mut self, versions: &[Version]) -> Self { + assert!( + !versions.is_empty(), + "supported_http_versions cannot be empty; configure at least one HTTP version (for example HTTP/1.1 or HTTP/2)" + ); + self.supported_http_versions = versions.to_vec(); + self + } + + /// Configures the rustls crypto provider and a fallback server + /// certificate verifier. + /// + /// The verifier is used only when the application did not supply one of + /// its own on the options builder. + /// + /// If no default backend has been selected yet, this call also promotes + /// rustls to be the default backend. Call `defaults_to_native_tls` + /// afterwards to override that promotion. + #[cfg(any(feature = "rustls", test))] + #[must_use] + pub fn configure_rustls( + mut self, + crypto_provider: std::sync::Arc<::rustls::crypto::CryptoProvider>, + verifier: std::sync::Arc, + ) -> Self { + self.rustls = Some(RustlsDefaults { crypto_provider, verifier }); + + if matches!(self.default, DefaultBackend::Unselected) { + self.default = DefaultBackend::Rustls; + } + + self + } + + /// Sets the default backend to native-tls. + /// + /// This default applies to any [`TlsOptions`] that did not pin a + /// backend itself. + #[cfg(any(feature = "native-tls", test))] + #[must_use] + pub fn defaults_to_native_tls(mut self) -> Self { + self.default = DefaultBackend::NativeTls; + self + } + + /// Sets the default backend to rustls. + /// + /// This default applies to any [`TlsOptions`] that did not pin a + /// backend itself. rustls still requires `configure_rustls` to be + /// called; selecting rustls without configuring it makes + /// [`build_backend`](Self::build_backend) fail with a [`BackendError`]. + #[cfg(any(feature = "rustls", test))] + #[must_use] + pub fn defaults_to_rustls(mut self) -> Self { + self.default = DefaultBackend::Rustls; + self + } + + /// Materializes `options` into a [`TlsBackend`] using this builder. + /// + /// Behavior depends on how `options` was constructed: + /// + /// - default (no backend pinned) — uses this builder's configured + /// default backend. + /// - rustls — requires `configure_rustls` to have been called; + /// values set on the options builder take precedence over the + /// defaults on this builder. + /// - native-tls — this builder is ignored. + /// - pre-configured — the wrapped backend is returned unchanged. + /// + /// # Errors + /// + /// Returns [`BackendError`] if no backend is selected, if required + /// rustls defaults are missing, or if backend construction fails (for + /// example, invalid client identity material). + pub fn build_backend(&self, options: TlsOptions) -> Result { + match options.inner { + TlsOptionsKind::Auto => self.build_auto_backend(options.shared), + TlsOptionsKind::PreConfigured(backend) => Ok(backend), + #[cfg(any(feature = "rustls", test))] + TlsOptionsKind::Rustls(rustls_backend) => { + let config = rustls_backend.build(self, &options.shared)?; + Ok(TlsBackend::Rustls(std::sync::Arc::new(config))) + } + #[cfg(any(feature = "native-tls", test))] + TlsOptionsKind::NativeTls(native_backend) => { + let connector = native_backend.build(self, &options.shared)?; + Ok(TlsBackend::NativeTls(connector)) + } + } + } + + #[allow( + clippy::allow_attributes, + clippy::no_effect_underscore_binding, + reason = "`shared` is used by feature-gated arms; with neither rustls nor native-tls enabled only Unselected is reachable" + )] + fn build_auto_backend(&self, shared: SharedOptions) -> Result { + match self.default { + #[cfg(any(feature = "rustls", test))] + DefaultBackend::Rustls => { + let config = crate::rustls::RustlsOptions::new().build(self, &shared)?; + Ok(TlsBackend::Rustls(std::sync::Arc::new(config))) + } + #[cfg(any(feature = "native-tls", test))] + DefaultBackend::NativeTls => { + let connector = crate::native_tls::NativeTlsOptions::new().build(self, &shared)?; + Ok(TlsBackend::NativeTls(connector)) + } + DefaultBackend::Unselected => { + // use the shared options + let _shared = shared; + + Err(BackendError::caused_by( + "no default TLS backend is configured on TlsBackendBuilder; call defaults_to_rustls() / defaults_to_native_tls() (or configure_rustls(), which implies rustls), or construct TlsOptions via one of its builders", + )) + } + } + } +} + +impl Default for TlsBackendBuilder { + fn default() -> Self { + Self { + #[cfg(any(feature = "rustls", test))] + rustls: None, + default: DefaultBackend::Unselected, + supported_http_versions: vec![Version::HTTP_11, Version::HTTP_2], + } + } +} + +/// Default TLS backend used when a [`TlsOptions`] does not pin one. +#[derive(Debug, Clone, Default)] +pub(crate) enum DefaultBackend { + /// No default backend chosen. Building an unpinned [`TlsOptions`] + /// against such a builder returns a [`BackendError`]. + #[default] + Unselected, + + #[cfg(any(feature = "rustls", test))] + Rustls, + + #[cfg(any(feature = "native-tls", test))] + NativeTls, +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::*; + + #[test] + fn default_supported_http_versions_is_http1_and_http2() { + let builder = TlsBackendBuilder::new(); + assert_eq!(builder.supported_http_versions, vec![Version::HTTP_11, Version::HTTP_2]); + } + + #[test] + fn supported_http_versions_overrides_defaults() { + let builder = TlsBackendBuilder::new().supported_http_versions(&[Version::HTTP_11]); + assert_eq!(builder.supported_http_versions, vec![Version::HTTP_11]); + } + + #[test] + fn tls_backend_builder_is_cloneable() { + static_assertions::assert_impl_all!(TlsBackendBuilder: Clone); + } + + mod build_backend { + use std::sync::Arc; + + use ::rustls::crypto::aws_lc_rs; + + use super::*; + use crate::testing::AcceptAllServerCertVerifier as AcceptAll; + + fn rustls_defaults() -> TlsBackendBuilder { + TlsBackendBuilder::new().configure_rustls(Arc::new(aws_lc_rs::default_provider()), Arc::new(AcceptAll)) + } + + #[test] + #[cfg_attr(miri, ignore)] + fn auto_without_default_backend_returns_error() { + let defaults = TlsBackendBuilder::new(); + + let err = defaults.build_backend(TlsOptions::default()).unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("no default TLS backend"), "unexpected error: {msg}"); + } + + mod rustls { + use super::*; + + #[test] + #[cfg_attr(miri, ignore)] + fn rustls_falls_back_to_default_verifier() { + let tls = TlsOptions::builder_rustls().build(); + let backend = rustls_defaults().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn rustls_uses_caller_verifier_when_set() { + let tls = TlsOptions::builder_rustls() + .server_certificate_verifier(|_| Arc::new(AcceptAll)) + .build(); + let backend = rustls_defaults().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn rustls_without_defaults_returns_error() { + let tls = TlsOptions::builder_rustls().build(); + let err = TlsBackendBuilder::new().build_backend(tls).unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("crypto provider"), "unexpected error: {msg}"); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn preconfigured_passes_backend_through_unchanged() { + let config = ::rustls::ClientConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider())) + .with_safe_default_protocol_versions() + .unwrap() + .dangerous() + .with_custom_certificate_verifier(Arc::new(AcceptAll)) + .with_no_client_auth(); + let tls = TlsOptions::from(config); + let backend = rustls_defaults().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + } + + mod native_tls { + use super::*; + + #[test] + #[cfg_attr(miri, ignore)] + fn native_tls_ignores_rustls_defaults() { + let tls = TlsOptions::builder_native_tls().build(); + let backend = TlsBackendBuilder::new().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::NativeTls(_))); + } + } + + mod auto { + use super::*; + + #[test] + #[cfg_attr(miri, ignore)] + fn builder_routes_through_default_backend() { + let tls = TlsOptions::builder().build(); + let backend = rustls_defaults().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn builder_propagates_shared_options_to_chosen_backend() { + let tls = TlsOptions::builder().supported_http_versions(&[Version::HTTP_2]).build(); + let backend = TlsBackendBuilder::new().defaults_to_native_tls().build_backend(tls).unwrap(); + assert!(matches!(backend, TlsBackend::NativeTls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn configure_rustls_auto_promotes_unselected_to_rustls() { + let backend = rustls_defaults().build_backend(TlsOptions::default()).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn defaults_to_rustls_selects_rustls() { + let defaults = rustls_defaults().defaults_to_rustls(); + let backend = defaults.build_backend(TlsOptions::default()).unwrap(); + assert!(matches!(backend, TlsBackend::Rustls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn defaults_to_rustls_without_rustls_defaults_returns_crypto_provider_error() { + let defaults = TlsBackendBuilder::new().defaults_to_rustls(); + let err = defaults.build_backend(TlsOptions::default()).unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("crypto provider"), "unexpected error: {msg}"); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn defaults_to_native_tls_selects_native_tls() { + let defaults = TlsBackendBuilder::new().defaults_to_native_tls(); + let backend = defaults.build_backend(TlsOptions::default()).unwrap(); + assert!(matches!(backend, TlsBackend::NativeTls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn defaults_to_native_tls_after_configure_rustls_overrides_promotion() { + let defaults = rustls_defaults().defaults_to_native_tls(); + let backend = defaults.build_backend(TlsOptions::default()).unwrap(); + assert!(matches!(backend, TlsBackend::NativeTls(_))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn configure_rustls_after_defaults_to_native_tls_keeps_native_tls() { + let defaults = TlsBackendBuilder::new() + .defaults_to_native_tls() + .configure_rustls(Arc::new(aws_lc_rs::default_provider()), Arc::new(AcceptAll)); + let backend = defaults.build_backend(TlsOptions::default()).unwrap(); + assert!(matches!(backend, TlsBackend::NativeTls(_))); + } + } + } +} diff --git a/crates/fetch_tls/src/client_identity.rs b/crates/fetch_tls/src/client_identity.rs new file mode 100644 index 000000000..d98f13e8e --- /dev/null +++ b/crates/fetch_tls/src/client_identity.rs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Backend-agnostic `mTLS` client identity. +//! +//! [`ClientIdentity`] holds a `DER`-encoded certificate chain and private key +//! and is consumed by both backends, so `mTLS` is configured the same way +//! regardless of which backend is selected. + +use std::fmt; + +use rustls_pki_types::pem::PemObject; + +/// Error returned when constructing a [`ClientIdentity`] from key material fails. +#[ohno::error] +pub struct ClientIdentityError; + +/// Client identity for mutual TLS (`mTLS`) authentication. +/// +/// Holds the client certificate chain and private key presented during the +/// TLS handshake. The same value works with either backend; each one +/// converts the contained `DER` bytes into its own internal form. +/// +/// # Example +/// +/// ```rust,no_run +/// use fetch_tls::ClientIdentity; +/// +/// # fn example() -> Result<(), Box> { +/// let cert_pem = std::fs::read("client.pem")?; +/// let key_pem = std::fs::read("client-key.pem")?; +/// let identity = ClientIdentity::from_pem(&cert_pem, &key_pem)?; +/// # let _ = identity; +/// # Ok(()) +/// # } +/// ``` +pub struct ClientIdentity { + cert_chain: Vec>, + private_key: rustls_pki_types::PrivateKeyDer<'static>, +} + +impl Clone for ClientIdentity { + fn clone(&self) -> Self { + Self { + cert_chain: self.cert_chain.clone(), + // `PrivateKeyDer` does not implement `Clone` because the inner key + // material is sensitive; `clone_key` makes the copy explicit. + private_key: self.private_key.clone_key(), + } + } +} + +impl ClientIdentity { + /// Creates a client identity from `PEM`-encoded certificate and private key. + /// + /// `cert_pem` may contain one or more certificates (leaf first, then + /// intermediates). `key_pem` must contain exactly one private key + /// (`PKCS#1`, `PKCS#8`, or `SEC1`). + /// + /// The native-tls backend only accepts `PKCS#8` keys; `PKCS#1` and + /// `SEC1` work with rustls but cause native-tls to fail at build time. + /// + /// # Errors + /// + /// Returns an error if either input is not valid `PEM`. + pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result { + let cert_chain: Vec> = rustls_pki_types::CertificateDer::pem_slice_iter(cert_pem) + .collect::>() + .map_err(ClientIdentityError::caused_by)?; + let private_key = rustls_pki_types::PrivateKeyDer::from_pem_slice(key_pem).map_err(ClientIdentityError::caused_by)?; + Ok(Self { cert_chain, private_key }) + } + + /// Creates a client identity from `DER`-encoded certificate and private key. + /// + /// `cert_chain` is leaf-first; `key_der` must be a `PKCS#8`-encoded + /// private key. + #[must_use] + pub fn from_der(cert_chain: I, key_der: K) -> Self + where + I: IntoIterator, + C: AsRef<[u8]>, + K: AsRef<[u8]>, + { + let cert_chain = cert_chain + .into_iter() + .map(|c| rustls_pki_types::CertificateDer::from(c.as_ref().to_vec())) + .collect(); + let private_key = rustls_pki_types::PrivateKeyDer::from(rustls_pki_types::PrivatePkcs8KeyDer::from(key_der.as_ref().to_vec())); + Self { cert_chain, private_key } + } + + /// Returns the certificate chain as rustls types. + #[cfg(any(feature = "rustls", test))] + #[cfg_attr(test, mutants::skip)] // trivial accessor, tested via `mTLS` integration + pub(crate) fn cert_chain(&self) -> &[rustls_pki_types::CertificateDer<'static>] { + &self.cert_chain + } + + /// Returns the private key as rustls types. + #[cfg(any(feature = "rustls", test))] + pub(crate) fn private_key(&self) -> &rustls_pki_types::PrivateKeyDer<'static> { + &self.private_key + } + + /// Builds a [`native_tls::Identity`] from this client identity. + /// + /// Re-encodes the `DER` components as `PEM` and feeds them to + /// [`native_tls::Identity::from_pkcs8`], the format supported across all + /// platform backends. Fails if the private key is not `PKCS#8` or if the + /// platform native TLS implementation rejects the material. + #[cfg(any(feature = "native-tls", test))] + pub(crate) fn build_native_identity(&self) -> Result { + let key_pkcs8_der = match &self.private_key { + rustls_pki_types::PrivateKeyDer::Pkcs8(key) => key.secret_pkcs8_der(), + _ => { + return Err(ClientIdentityError::caused_by("native-tls backend requires a `PKCS#8` private key")); + } + }; + + let mut cert_pem = Vec::new(); + for cert in &self.cert_chain { + write_pem_block(&mut cert_pem, "CERTIFICATE", cert.as_ref()); + } + let mut key_pem = Vec::new(); + write_pem_block(&mut key_pem, "PRIVATE KEY", key_pkcs8_der); + + native_tls::Identity::from_pkcs8(&cert_pem, &key_pem).map_err(ClientIdentityError::caused_by) + } +} + +/// Writes a `PEM`-encoded object to `out`. +/// +/// Format per `RFC 7468`: a textual `-----BEGIN