diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f44d5ec5..e6c467bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,6 @@ jobs: export CARGO_INCREMENTAL=0 cargo clippy --workspace --all-targets --exclude v8 -- -D warnings - - name: Check for banned TLS crates - run: ./scripts/check-no-native-tls.sh - openapi-schema-check: name: Check OpenAPI Schema is Up-to-Date runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 27a8e5c9..b6da45e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -175,7 +175,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -823,10 +823,13 @@ dependencies = [ "http-body 1.0.1", "hyper 1.8.1", "hyper-util", + "openssl", + "openssl-sys", "pin-project-lite", "rustls", "rustls-pki-types", "tokio", + "tokio-openssl", "tokio-rustls", "tower-service", ] @@ -852,6 +855,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base32" version = "0.5.1" @@ -2330,6 +2339,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -2371,6 +2392,33 @@ dependencies = [ "libloading 0.9.0", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.20.11" @@ -3075,7 +3123,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3215,6 +3263,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ecow" version = "0.2.6" @@ -3224,6 +3286,30 @@ dependencies = [ "serde", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -3233,6 +3319,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email_address" version = "0.2.9" @@ -3325,7 +3432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3472,6 +3579,22 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.27" @@ -4073,6 +4196,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -4226,6 +4350,9 @@ dependencies = [ "opentelemetry_sdk", "pastey", "pctx_code_mode", + "pem", + "pkcs1", + "pkcs8", "pulldown-cmark", "rand 0.8.5", "rayon", @@ -4235,6 +4362,7 @@ dependencies = [ "rubato", "sacp", "schemars 1.2.1", + "sec1", "serde", "serde_json", "serde_urlencoded", @@ -4428,6 +4556,7 @@ dependencies = [ "goose-mcp", "hex", "http 1.4.0", + "openssl", "rand 0.8.5", "rcgen", "reqwest 0.13.2", @@ -4475,6 +4604,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gzip-header" version = "1.0.0" @@ -4811,6 +4951,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -5282,7 +5438,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5479,11 +5635,18 @@ checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "aws-lc-rs", "base64 0.22.1", + "ed25519-dalek", "getrandom 0.2.17", + "hmac", "js-sys", + "p256", + "p384", "pem", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", "signature", "simple_asn1", ] @@ -5958,6 +6121,23 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -6070,7 +6250,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6518,6 +6698,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "par-core" version = "2.0.0" @@ -7051,6 +7255,15 @@ dependencies = [ "num-integer", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -7630,9 +7843,11 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -7643,6 +7858,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tower", "tower-http", @@ -7674,11 +7890,13 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -7690,6 +7908,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -7712,6 +7931,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.53" @@ -7909,7 +8138,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7968,7 +8197,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8180,6 +8409,20 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -8759,7 +9002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8852,6 +9095,7 @@ dependencies = [ "indexmap 2.13.0", "log", "memchr", + "native-tls", "once_cell", "percent-encoding", "rustls", @@ -9897,7 +10141,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10304,6 +10548,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -10333,10 +10598,12 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", + "native-tls", "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", + "tokio-native-tls", "tokio-rustls", "tungstenite", ] @@ -10785,6 +11052,7 @@ dependencies = [ "http 1.4.0", "httparse", "log", + "native-tls", "rand 0.9.2", "rustls", "rustls-pki-types", @@ -11462,7 +11730,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Justfile b/Justfile index d1d18ace..2ba028c1 100644 --- a/Justfile +++ b/Justfile @@ -11,8 +11,6 @@ check-everything: cargo fmt --all @echo " → Running clippy linting..." cargo clippy --all-targets -- -D warnings - @echo " → Checking for banned TLS crates..." - ./scripts/check-no-native-tls.sh @echo " → Checking UI code formatting..." cd ui/desktop && pnpm run lint:check @echo " → Validating OpenAPI schema..." diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index bb5e0983..32f2148c 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -16,15 +16,17 @@ name = "generate-acp-schema" path = "src/bin/generate_acp_schema.rs" [features] -default = ["code-mode"] +default = ["code-mode", "rustls-tls"] code-mode = ["goose/code-mode"] +rustls-tls = ["goose/rustls-tls", "goose-mcp/rustls-tls"] +native-tls = ["goose/native-tls", "goose-mcp/native-tls"] [lints] workspace = true [dependencies] goose = { path = "../goose", default-features = false } -goose-mcp = { path = "../goose-mcp" } +goose-mcp = { path = "../goose-mcp", default-features = false } rmcp = { workspace = true } sacp = { workspace = true, features = ["unstable"] } agent-client-protocol-schema = { workspace = true } diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index edad607a..78bdcf9f 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -49,7 +49,7 @@ async-trait = { workspace = true } base64 = { workspace = true } regex = { workspace = true } tar = "0.4.45" -reqwest = { workspace = true, features = ["blocking", "rustls"], default-features = false } +reqwest = { workspace = true, features = ["blocking"], default-features = false } zip = { workspace = true } bzip2 = "0.5" webbrowser = { workspace = true } @@ -62,19 +62,31 @@ urlencoding = { workspace = true } clap_complete = "4.5.62" comfy-table = "7.2.2" sha2 = { workspace = true } -sigstore-verify = { version = "0.6", default-features = false, features = ["rustls"] } +sigstore-verify = { version = "0.6", default-features = false } [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } [features] -default = ["code-mode", "local-inference", "aws-providers"] +default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] code-mode = ["goose/code-mode", "goose-acp/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] # disables the update command disable-update = [] +rustls-tls = [ + "reqwest/rustls", + "sigstore-verify/rustls", + "goose/rustls-tls", + "goose-mcp/rustls-tls", +] +native-tls = [ + "reqwest/native-tls", + "sigstore-verify/native-tls", + "goose/native-tls", + "goose-mcp/native-tls", +] [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/goose-cli/src/lib.rs b/crates/goose-cli/src/lib.rs index b5006389..be821a2a 100644 --- a/crates/goose-cli/src/lib.rs +++ b/crates/goose-cli/src/lib.rs @@ -1,3 +1,9 @@ +#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] +compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled"); + +#[cfg(all(feature = "rustls-tls", feature = "native-tls"))] +compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive"); + pub mod cli; pub mod commands; pub mod logging; diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 600d9f36..f1e5dfc9 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -10,6 +10,10 @@ description.workspace = true [lints] workspace = true +[features] +rustls-tls = ["reqwest/rustls"] +native-tls = ["reqwest/native-tls"] + [dependencies] rmcp = { workspace = true, features = ["server", "client", "transport-io", "macros"] } anyhow = { workspace = true } @@ -23,7 +27,7 @@ serde = { workspace = true } serde_json = { workspace = true } schemars = { workspace = true } indoc = { workspace = true } -reqwest = { workspace = true, features = ["json", "rustls", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "system-proxy"], default-features = false } chrono = { workspace = true } etcetera = { workspace = true } tempfile = { workspace = true } diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 20bb9b15..2accd001 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -11,11 +11,28 @@ description.workspace = true workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers"] +default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] code-mode = ["goose/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] +rustls-tls = [ + "reqwest/rustls", + "tokio-tungstenite/rustls-tls-native-roots", + "axum-server/tls-rustls", + "dep:rustls", + "dep:aws-lc-rs", + "goose/rustls-tls", + "goose-mcp/rustls-tls", +] +native-tls = [ + "reqwest/native-tls", + "tokio-tungstenite/native-tls", + "axum-server/tls-openssl", + "dep:openssl", + "goose/native-tls", + "goose-mcp/native-tls", +] [dependencies] goose = { path = "../goose", default-features = false } @@ -41,21 +58,22 @@ thiserror = { workspace = true } clap = { workspace = true } serde_yaml = { workspace = true } utoipa = { workspace = true, features = ["axum_extras", "chrono"] } -reqwest = { workspace = true, features = ["json", "rustls", "blocking", "multipart", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "blocking", "multipart", "system-proxy"], default-features = false } tokio-util = { workspace = true } serde_path_to_error = "0.1.20" -tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] } +tokio-tungstenite = { version = "0.28.0" } url = { workspace = true } rand = { workspace = true } hex = "0.4.3" subtle = "2.6" socket2 = "0.6.1" fs2 = { workspace = true } -rustls = { version = "0.23", features = ["aws_lc_rs"] } +rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true } uuid = { workspace = true } rcgen = "0.13" -axum-server = { version = "0.8.0", features = ["tls-rustls"] } -aws-lc-rs = "1.16.0" +axum-server = { version = "0.8.0" } +aws-lc-rs = { version = "1.16.0", optional = true } +openssl = { version = "0.10", optional = true } [target.'cfg(windows)'.dependencies] winreg = { version = "0.55.0" } diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index c5799e24..e1e642fa 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -4,6 +4,7 @@ use anyhow::Result; use axum::middleware; use axum_server::Handle; use goose_server::auth::check_token; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] use goose_server::tls::self_signed_config; use tower_http::cors::{Any, CorsLayer}; use tracing::info; @@ -31,6 +32,7 @@ pub async fn run() -> Result<()> { // gateways, etc.) try to open TLS connections. Both `ring` and `aws-lc-rs` // features are enabled on rustls (via different transitive deps), so rustls // cannot auto-detect a provider — we must pick one explicitly. + #[cfg(feature = "rustls-tls")] let _ = rustls::crypto::ring::default_provider().install_default(); crate::logging::setup_logging(Some("goosed"))?; @@ -74,21 +76,39 @@ pub async fn run() -> Result<()> { }); if settings.tls { - let tls_setup = self_signed_config().await?; + #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] + { + let tls_setup = self_signed_config().await?; - let handle = Handle::new(); - let shutdown_handle = handle.clone(); - tokio::spawn(async move { - shutdown_signal().await; - shutdown_handle.graceful_shutdown(None); - }); + let handle = Handle::new(); + let shutdown_handle = handle.clone(); + tokio::spawn(async move { + shutdown_signal().await; + shutdown_handle.graceful_shutdown(None); + }); - info!("listening on https://{}", addr); + info!("listening on https://{}", addr); - axum_server::bind_rustls(addr, tls_setup.config) - .handle(handle) - .serve(app.into_make_service()) - .await?; + #[cfg(feature = "rustls-tls")] + axum_server::bind_rustls(addr, tls_setup.config) + .handle(handle) + .serve(app.into_make_service()) + .await?; + + #[cfg(feature = "native-tls")] + axum_server::bind_openssl(addr, tls_setup.config) + .handle(handle) + .serve(app.into_make_service()) + .await?; + } + + #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] + { + anyhow::bail!( + "TLS was requested but no TLS backend is enabled. \ + Enable the `rustls-tls` or `native-tls` feature." + ); + } } else { let listener = tokio::net::TcpListener::bind(addr).await?; diff --git a/crates/goose-server/src/lib.rs b/crates/goose-server/src/lib.rs index ab35c400..2323f287 100644 --- a/crates/goose-server/src/lib.rs +++ b/crates/goose-server/src/lib.rs @@ -1,3 +1,9 @@ +#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] +compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled"); + +#[cfg(all(feature = "rustls-tls", feature = "native-tls"))] +compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive"); + pub mod auth; pub mod configuration; pub mod error; @@ -5,6 +11,7 @@ pub mod openapi; pub mod routes; pub mod session_event_bus; pub mod state; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] pub mod tls; pub mod tunnel; diff --git a/crates/goose-server/src/tls.rs b/crates/goose-server/src/tls.rs index 8a5cf86d..5eb7e3f4 100644 --- a/crates/goose-server/src/tls.rs +++ b/crates/goose-server/src/tls.rs @@ -1,22 +1,31 @@ +//! TLS configuration for the goose server. +//! +//! Two TLS backends are supported for the HTTPS listener via `axum-server`: +//! +//! - **`rustls-tls`** (enabled by default) – uses `axum-server/tls-rustls` with +//! the `aws-lc-rs` crypto provider. +//! - **`native-tls`** – uses `axum-server/tls-openssl`, which links against the +//! platform's OpenSSL (or a compatible fork such as LibreSSL / BoringSSL). +//! On Linux this *is* the platform-native TLS stack; on macOS/Windows the +//! `native-tls` crate used by the HTTP *client* delegates to Security.framework +//! / SChannel respectively, but `axum-server` does not offer those backends so +//! the server listener always uses OpenSSL when this feature is active. + use anyhow::Result; -use aws_lc_rs::digest; -use axum_server::tls_rustls::RustlsConfig; use rcgen::{CertificateParams, DnType, KeyPair, SanType}; +#[cfg(feature = "rustls-tls")] +pub type TlsConfig = axum_server::tls_rustls::RustlsConfig; + +#[cfg(feature = "native-tls")] +pub type TlsConfig = axum_server::tls_openssl::OpenSSLConfig; + pub struct TlsSetup { - pub config: RustlsConfig, + pub config: TlsConfig, pub fingerprint: String, } -/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and -/// return a [`TlsSetup`] containing the rustls config and the SHA-256 -/// fingerprint of the generated certificate (colon-separated hex). -/// -/// The fingerprint is printed to stdout so the parent process (e.g. Electron) -/// can pin it and reject connections from any other certificate. -pub async fn self_signed_config() -> Result { - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - +fn generate_self_signed_cert() -> Result<(rcgen::Certificate, KeyPair)> { let mut params = CertificateParams::default(); params .distinguished_name @@ -28,22 +37,62 @@ pub async fn self_signed_config() -> Result { let key_pair = KeyPair::generate()?; let cert = params.self_signed(&key_pair)?; + Ok((cert, key_pair)) +} - let cert_der = cert.der(); - let sha256 = digest::digest(&digest::SHA256, cert_der); - let fingerprint = sha256 - .as_ref() - .iter() - .map(|b| format!("{b:02X}")) - .collect::>() - .join(":"); +fn sha256_fingerprint(der: &[u8]) -> String { + #[cfg(feature = "rustls-tls")] + { + let sha256 = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, der); + sha256 + .as_ref() + .iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join(":") + } + #[cfg(feature = "native-tls")] + { + use openssl::hash::MessageDigest; + let digest = + openssl::hash::hash(MessageDigest::sha256(), der).expect("SHA-256 hash failed"); + digest + .iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join(":") + } +} + +/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and +/// return a [`TlsSetup`] containing the server config and the SHA-256 +/// fingerprint of the generated certificate (colon-separated hex). +/// +/// The fingerprint is printed to stdout so the parent process (e.g. Electron) +/// can pin it and reject connections from any other certificate. +pub async fn self_signed_config() -> Result { + #[cfg(feature = "rustls-tls")] + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let (cert, key_pair) = generate_self_signed_cert()?; + + let fingerprint = sha256_fingerprint(cert.der()); println!("GOOSED_CERT_FINGERPRINT={fingerprint}"); let cert_pem = cert.pem(); let key_pem = key_pair.serialize_pem(); - let config = RustlsConfig::from_pem(cert_pem.into_bytes(), key_pem.into_bytes()).await?; + #[cfg(feature = "rustls-tls")] + let config = axum_server::tls_rustls::RustlsConfig::from_pem( + cert_pem.into_bytes(), + key_pem.into_bytes(), + ) + .await?; + + #[cfg(feature = "native-tls")] + let config = + axum_server::tls_openssl::OpenSSLConfig::from_pem(cert_pem.as_bytes(), key_pem.as_bytes())?; Ok(TlsSetup { config, diff --git a/crates/goose-server/src/tunnel/lapstone.rs b/crates/goose-server/src/tunnel/lapstone.rs index 94c6d5d8..9b55afbf 100644 --- a/crates/goose-server/src/tunnel/lapstone.rs +++ b/crates/goose-server/src/tunnel/lapstone.rs @@ -485,6 +485,7 @@ async fn run_single_connection( scheme: String, restart_tx: mpsc::Sender<()>, ) { + #[cfg(feature = "rustls-tls")] let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let worker_url = get_worker_url(); diff --git a/crates/goose-server/tests/tls_test.rs b/crates/goose-server/tests/tls_test.rs new file mode 100644 index 00000000..a955ebd0 --- /dev/null +++ b/crates/goose-server/tests/tls_test.rs @@ -0,0 +1,109 @@ +use goose_server::tls::{self_signed_config, TlsConfig}; + +#[cfg(not(feature = "native-tls"))] +#[test] +fn default_tls_config_is_rustls() { + fn assert_type(_: &T) {} + let rt = tokio::runtime::Runtime::new().unwrap(); + let setup = rt.block_on(self_signed_config()).unwrap(); + // Proves TlsConfig resolves to RustlsConfig when native-tls is disabled. + let _: &axum_server::tls_rustls::RustlsConfig = &setup.config; + assert_type::(&setup.config); +} + +#[cfg(feature = "native-tls")] +#[test] +fn native_tls_config_is_openssl() { + fn assert_type(_: &T) {} + let rt = tokio::runtime::Runtime::new().unwrap(); + let setup = rt.block_on(self_signed_config()).unwrap(); + // Proves TlsConfig resolves to OpenSSLConfig when native-tls is enabled. + let _: &axum_server::tls_openssl::OpenSSLConfig = &setup.config; + assert_type::(&setup.config); +} + +#[tokio::test] +async fn self_signed_config_produces_valid_fingerprint() { + let setup = self_signed_config().await.unwrap(); + + assert!( + !setup.fingerprint.is_empty(), + "fingerprint must not be empty" + ); + + let parts: Vec<&str> = setup.fingerprint.split(':').collect(); + assert_eq!( + parts.len(), + 32, + "SHA-256 fingerprint must have 32 hex pairs" + ); + + for part in &parts { + assert_eq!( + part.len(), + 2, + "each fingerprint segment must be 2 hex chars" + ); + assert!( + part.chars().all(|c| c.is_ascii_hexdigit()), + "fingerprint segment '{}' must be valid hex", + part + ); + } +} + +#[tokio::test] +async fn self_signed_config_returns_usable_tls_config() { + use axum::routing::get; + use std::net::SocketAddr; + + let setup = self_signed_config().await.unwrap(); + + let app = axum::Router::new().route("/health", get(|| async { "ok" })); + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + + #[cfg(not(feature = "native-tls"))] + let server = axum_server::bind_rustls(addr, setup.config); + + #[cfg(feature = "native-tls")] + let server = axum_server::bind_openssl(addr, setup.config); + + let handle = axum_server::Handle::new(); + let shutdown_handle = handle.clone(); + + let server_handle = tokio::spawn({ + let handle = handle.clone(); + async move { + server + .handle(handle) + .serve(app.into_make_service()) + .await + .unwrap(); + } + }); + + // Wait for the server to start listening. + let listening_addr = loop { + if let Some(addr) = handle.listening().await { + break addr; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + }; + + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + + let resp = client + .get(format!("https://{}/health", listening_addr)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + assert_eq!(resp.text().await.unwrap(), "ok"); + + shutdown_handle.graceful_shutdown(None); + let _ = server_handle.await; +} diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 225aeb1b..9f5014b0 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true description.workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers"] +default = ["code-mode", "local-inference", "aws-providers", "rustls-tls"] code-mode = ["dep:pctx_code_mode"] local-inference = [ "dep:candle-core", @@ -27,6 +27,26 @@ aws-providers = [ "dep:aws-sdk-sagemakerruntime", ] cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"] +rustls-tls = [ + "reqwest/rustls", + "rmcp/reqwest", + "sqlx/runtime-tokio-rustls", + "jsonwebtoken/aws_lc_rs", + "oauth2/reqwest", + "oauth2/rustls-tls", +] +native-tls = [ + "dep:pem", + "dep:pkcs1", + "dep:pkcs8", + "dep:sec1", + "reqwest/native-tls", + "rmcp/reqwest-native-tls", + "sqlx/runtime-tokio-native-tls", + "jsonwebtoken/rust_crypto", + "oauth2/reqwest", + "oauth2/native-tls", +] [lints] workspace = true @@ -35,17 +55,16 @@ workspace = true lru = { workspace = true } rmcp = { workspace = true, features = [ "client", - "reqwest", "transport-child-process", "transport-streamable-http-client", "transport-streamable-http-client-reqwest", ] } -oauth2 = "5.0" +oauth2 = { version = "5.0", default-features = false } anyhow = { workspace = true } thiserror = { workspace = true } futures = { workspace = true } dirs = { workspace = true } -reqwest = { workspace = true, features = ["rustls", "json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"], default-features = false } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -94,7 +113,6 @@ tokio-cron-scheduler = "0.14.0" urlencoding = { workspace = true } v_htmlescape = "0.15" sqlx = { version = "0.8", default-features = false, features = [ - "runtime-tokio-rustls", "sqlite", "chrono", "json", @@ -111,7 +129,7 @@ aws-sdk-bedrockruntime = { version = "=1.120.0", default-features = false, featu aws-sdk-sagemakerruntime = { version = "1.62.0", default-features = false, features = ["default-https-client", "rt-tokio"], optional = true } # For GCP Vertex AI provider auth -jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] } +jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem"] } blake3 = "1.5" fs2 = { workspace = true } @@ -160,6 +178,10 @@ llama-cpp-2 = { version = "0.1.137", features = ["sampler"], optional = true } encoding_rs = "0.8.35" pastey = "0.2.1" shell-words = { workspace = true } +pem = { version = "3", optional = true } +pkcs1 = { version = "0.7", default-features = false, features = ["pkcs8"], optional = true } +pkcs8 = { version = "0.10", default-features = false, features = ["alloc"], optional = true } +sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index f73313e3..1e7d7a92 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -1,3 +1,9 @@ +#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] +compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled"); + +#[cfg(all(feature = "rustls-tls", feature = "native-tls"))] +compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive"); + pub mod acp; pub mod action_required_manager; pub mod agents; diff --git a/crates/goose/src/providers/api_client.rs b/crates/goose/src/providers/api_client.rs index 9f4feefe..c0e4bbd1 100644 --- a/crates/goose/src/providers/api_client.rs +++ b/crates/goose/src/providers/api_client.rs @@ -119,12 +119,21 @@ impl TlsConfig { let key_pem = read_to_string(&cert_key_pair.key_path) .map_err(|e| anyhow::anyhow!("Failed to read client private key: {}", e))?; - // Create a combined PEM file with certificate and private key - let combined_pem = format!("{}\n{}", cert_pem, key_pem); + #[cfg(not(feature = "native-tls"))] + let identity = { + let combined_pem = format!("{}\n{}", cert_pem, key_pem); + Identity::from_pem(combined_pem.as_bytes()).map_err(|e| { + anyhow::anyhow!("Failed to create identity from cert and key: {}", e) + })? + }; - let identity = Identity::from_pem(combined_pem.as_bytes()).map_err(|e| { - anyhow::anyhow!("Failed to create identity from cert and key: {}", e) - })?; + #[cfg(feature = "native-tls")] + let identity = { + let pkcs8_key_pem = convert_key_to_pkcs8_pem(&key_pem)?; + Identity::from_pkcs8_pem(cert_pem.as_bytes(), pkcs8_key_pem.as_bytes()).map_err( + |e| anyhow::anyhow!("Failed to create identity from cert and key: {}", e), + )? + }; Ok(Some(identity)) } else { @@ -154,6 +163,71 @@ impl Default for TlsConfig { } } +/// Convert a PEM private key from any format (PKCS#1, SEC1, PKCS#8) to PKCS#8 PEM. +/// +/// `reqwest::Identity::from_pkcs8_pem` (native-tls) only accepts PKCS#8 +/// (`-----BEGIN PRIVATE KEY-----`), but private keys in the wild come in three formats: +/// +/// - **PKCS#1** (`-----BEGIN RSA PRIVATE KEY-----`): Legacy RSA-specific format. +/// Generated by default with `openssl genrsa`. Very common in older setups, tutorials, +/// and CA-issued key files. +/// - **SEC1** (`-----BEGIN EC PRIVATE KEY-----`): Legacy EC-specific format. +/// Generated by default with `openssl ecparam -genkey`. Common with EC certificates. +/// - **PKCS#8** (`-----BEGIN PRIVATE KEY-----`): Generic, algorithm-agnostic wrapper. +/// This is the only format native-tls accepts. +/// +/// Without this conversion, users with legacy-format keys (Kubernetes secrets, corporate +/// PKI, etc.) would get a cryptic "Failed to create identity" error and need to manually +/// run `openssl pkey -in key.pem -out key-pkcs8.pem`. +/// +/// Note: the rustls code path (`Identity::from_pem`) accepts all formats natively, +/// so this conversion is only needed for native-tls. +#[cfg(feature = "native-tls")] +fn convert_key_to_pkcs8_pem(key_pem_str: &str) -> Result { + use pkcs8::der::{Decode, Encode}; + + let parsed = + pem::parse(key_pem_str).map_err(|e| anyhow::anyhow!("Failed to parse PEM key: {}", e))?; + + match parsed.tag() { + "PRIVATE KEY" => Ok(key_pem_str.to_string()), + "RSA PRIVATE KEY" => { + let info = pkcs8::PrivateKeyInfo::new(pkcs1::ALGORITHM_ID, parsed.contents()); + let der_bytes = info + .to_der() + .map_err(|e| anyhow::anyhow!("Failed to encode PKCS#8: {}", e))?; + Ok(pem::encode(&pem::Pem::new("PRIVATE KEY", der_bytes))) + } + "EC PRIVATE KEY" => { + let ec_key = sec1::EcPrivateKey::from_der(parsed.contents()) + .map_err(|e| anyhow::anyhow!("Failed to parse EC key: {}", e))?; + let curve_oid = ec_key + .parameters + .and_then(|p| p.named_curve()) + .ok_or_else(|| { + anyhow::anyhow!( + "EC key missing curve parameters. Convert to PKCS#8: \ + openssl pkey -in key.pem -out key-pkcs8.pem" + ) + })?; + let algorithm = pkcs8::AlgorithmIdentifierRef { + oid: sec1::ALGORITHM_OID, + parameters: Some((&curve_oid).into()), + }; + let info = pkcs8::PrivateKeyInfo::new(algorithm, parsed.contents()); + let der_bytes = info + .to_der() + .map_err(|e| anyhow::anyhow!("Failed to encode PKCS#8: {}", e))?; + Ok(pem::encode(&pem::Pem::new("PRIVATE KEY", der_bytes))) + } + tag => Err(anyhow::anyhow!( + "Unsupported key format '{}'. Expected PKCS#8, PKCS#1, or SEC1. \ + Convert with: openssl pkey -in key.pem -out key-pkcs8.pem", + tag + )), + } +} + pub struct OAuthConfig { pub host: String, pub client_id: String, @@ -440,6 +514,127 @@ impl fmt::Debug for ApiClient { } } +#[cfg(all(test, feature = "native-tls"))] +mod native_tls_tests { + use super::convert_key_to_pkcs8_pem; + + const PKCS8_RSA_KEY: &str = "\ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzmc7SPaB07+gK +p/VETtmayzNASEaaGIEKO5eOrlrKfIMdmSyB38Ia4dCyxb8WKAROeBtaev8AXfKh +5sPRPi42SNF77d171QWKU4WKXRWWpO2UWwzrXc/BaCNr0Ey9S4ej3v0DmdPkW+pY +PZ5lzHUT6wTutGwJsceCN+/lgPmazaZvfnTdLlRChLyPFjwGkGFwmu7YZACdbIqd +CW5oIs+weAK2L9+HZaLi0TWcnJrnONs00GHTm2DwLkl8ulKRn6+XygWZLpY4owE6 +/x4v4CrTKStvfJlPiErcR/t13vZ+kZq3XxcgIY5V0HEz+AT57/MvvGomQXyMmQnN +bQrmdbvRAgMBAAECggEABd6knqSmuDeogeOmHYzFqhxSwRqzmwDqw7i5ne9HFpfN +6bNaO+biF0KS0Wrq0Tiu1lkwzKrZJ2wKnF24P+AMPtgvp4EZRF0sjI4o/sIGzPj4 +LklxAUa8Pd4PTUxeG7Aqn1GP+5SFEzEpVieyY/GUEHn1e+8v2jvCEViOabXmwndA +tGHScHwMA0qW1smCOLj12mfFJJjJjlj7QveJFSJ94wo8XjdZvzLDonQoAf4vN99f +rf40zgAx8VQk1UmOPYgc7mqN3JerjWX3rG820UWilLnAA35rrVxjzeV9yfAbgpVC +qHPaOaN4tBVTb3z9w94wK/uDjd4Px26Qq0Yipi4xGQKBgQD7RXE6hNlx1hW7pdWw +hrxdH9ofptBzJfQwYnO0cq6KFF6+QQYzJcMAZAKEytDzfjkH1iHURtBFA1ndKBSI +B1G8iR7Jie4szDghJ+zS9QHHhXLRprSqyamRefnLe/614YlaP7i9wq/sHg8KKm5b +g+S49EfuKctAenMx8sDPDG7Y2QKBgQC2+xOZTE5uPPitOyR8eMUF1ye9zeONzyJ2 +l2rjEZuIOWd12wsrgGVTRXoecpqFNIxcmK2mnBtrWCJ/A5xFXjQjIZ1huJ/oNm+t +bI/GtlM5BknfEP6m8NVtMx5ubzqDIlsgRpXCWco3ytaKZJtdnJhZAOmBogAKUiPr +NL2afWbfuQKBgA722MJybPzBkjPFsY88xvUI05W0+o8RAJTtGBT0C37/rZDJEJR1 +OELKsfe0mHMX3k+gKg2ZVgf2JiDspSRgwzZmOCYqh7u1QdZ/qTP7EWsPgIIJ2pKd +RfL6/6xlqwqr7uEoEFmEwbNfKughFMdweGunaK0/YfJqGHguC9uI0wUpAoGBAKDM +U5TLscC+Y+oXpyCbIMjZIltxqx7bf/Wnao7Q0lUL3Rd8vnwkAOcEjyRiodedLhvR +MAjR9maGtQnZKmLrPfYBfER78komTE2isVZ2svvRwuj8Dky2J1gnK/7wUAMdFedv +H/wC2+nbnl1PvBivnFHas1jv/AkV1erEFjrFKLpBAoGABQ29M3xsVv7++/ivl0JM +lNzUZwWbG4GI9Yx/zBMJpjiPi9Bkbe2Iu7EgLuiaTnktLSzm9hwVEIxd6a6X/wRA +cGr4L/FDExxjHQPzYpeGQ5fA9HlDWZzQ7Ou+nZ/O+OwXuvnmjXTXyB0djVJHOmLU +gOfZqLKYsL5zn6pefvXu0dU= +-----END PRIVATE KEY-----"; + + const PKCS1_RSA_KEY: &str = "\ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAs5nO0j2gdO/oCqf1RE7ZmsszQEhGmhiBCjuXjq5aynyDHZks +gd/CGuHQssW/FigETngbWnr/AF3yoebD0T4uNkjRe+3de9UFilOFil0VlqTtlFsM +613PwWgja9BMvUuHo979A5nT5FvqWD2eZcx1E+sE7rRsCbHHgjfv5YD5ms2mb350 +3S5UQoS8jxY8BpBhcJru2GQAnWyKnQluaCLPsHgCti/fh2Wi4tE1nJya5zjbNNBh +05tg8C5JfLpSkZ+vl8oFmS6WOKMBOv8eL+Aq0ykrb3yZT4hK3Ef7dd72fpGat18X +ICGOVdBxM/gE+e/zL7xqJkF8jJkJzW0K5nW70QIDAQABAoIBAAXepJ6kprg3qIHj +ph2MxaocUsEas5sA6sO4uZ3vRxaXzemzWjvm4hdCktFq6tE4rtZZMMyq2SdsCpxd +uD/gDD7YL6eBGURdLIyOKP7CBsz4+C5JcQFGvD3eD01MXhuwKp9Rj/uUhRMxKVYn +smPxlBB59XvvL9o7whFYjmm15sJ3QLRh0nB8DANKltbJgji49dpnxSSYyY5Y+0L3 +iRUifeMKPF43Wb8yw6J0KAH+LzffX63+NM4AMfFUJNVJjj2IHO5qjdyXq41l96xv +NtFFopS5wAN+a61cY83lfcnwG4KVQqhz2jmjeLQVU298/cPeMCv7g43eD8dukKtG +IqYuMRkCgYEA+0VxOoTZcdYVu6XVsIa8XR/aH6bQcyX0MGJztHKuihRevkEGMyXD +AGQChMrQ8345B9Yh1EbQRQNZ3SgUiAdRvIkeyYnuLMw4ISfs0vUBx4Vy0aa0qsmp +kXn5y3v+teGJWj+4vcKv7B4PCipuW4PkuPRH7inLQHpzMfLAzwxu2NkCgYEAtvsT +mUxObjz4rTskfHjFBdcnvc3jjc8idpdq4xGbiDlnddsLK4BlU0V6HnKahTSMXJit +ppwba1gifwOcRV40IyGdYbif6DZvrWyPxrZTOQZJ3xD+pvDVbTMebm86gyJbIEaV +wlnKN8rWimSbXZyYWQDpgaIAClIj6zS9mn1m37kCgYAO9tjCcmz8wZIzxbGPPMb1 +CNOVtPqPEQCU7RgU9At+/62QyRCUdThCyrH3tJhzF95PoCoNmVYH9iYg7KUkYMM2 +ZjgmKoe7tUHWf6kz+xFrD4CCCdqSnUXy+v+sZasKq+7hKBBZhMGzXyroIRTHcHhr +p2itP2Hyahh4LgvbiNMFKQKBgQCgzFOUy7HAvmPqF6cgmyDI2SJbcase23/1p2qO +0NJVC90XfL58JADnBI8kYqHXnS4b0TAI0fZmhrUJ2Spi6z32AXxEe/JKJkxNorFW +drL70cLo/A5MtidYJyv+8FADHRXnbx/8Atvp255dT7wYr5xR2rNY7/wJFdXqxBY6 +xSi6QQKBgAUNvTN8bFb+/vv4r5dCTJTc1GcFmxuBiPWMf8wTCaY4j4vQZG3tiLux +IC7omk55LS0s5vYcFRCMXemul/8EQHBq+C/xQxMcYx0D82KXhkOXwPR5Q1mc0Ozr +vp2fzvjsF7r55o1018gdHY1SRzpi1IDn2aiymLC+c5+qXn717tHV +-----END RSA PRIVATE KEY-----"; + + const SEC1_EC_KEY: &str = "\ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIQyP9q8dmPTyzxgCAF22fS5w6GJP7lVrkq3gdG216TtoAoGCCqGSM49 +AwEHoUQDQgAEAxC2ZMkrbwEImb3S2BD8qA9rquFbozW6d7VrhVk1tOqnIuM+vXkz +ShGoCNbfNS+COlPMRAujyDlATZcLs9p4tA== +-----END EC PRIVATE KEY-----"; + + #[test] + fn test_pkcs8_key_passthrough() { + let result = convert_key_to_pkcs8_pem(PKCS8_RSA_KEY).unwrap(); + assert_eq!(result, PKCS8_RSA_KEY.to_string()); + } + + #[test] + fn test_pkcs1_rsa_key_conversion() { + let result = convert_key_to_pkcs8_pem(PKCS1_RSA_KEY).unwrap(); + assert!(result.contains("-----BEGIN PRIVATE KEY-----")); + assert!(result.contains("-----END PRIVATE KEY-----")); + assert!(!result.contains("RSA PRIVATE KEY")); + // Verify the result is valid PKCS#8 by re-parsing + let re_parsed = pem::parse(&result).unwrap(); + assert_eq!(re_parsed.tag(), "PRIVATE KEY"); + use pkcs8::der::Decode; + pkcs8::PrivateKeyInfo::from_der(re_parsed.contents()).unwrap(); + } + + #[test] + fn test_sec1_ec_key_conversion() { + let result = convert_key_to_pkcs8_pem(SEC1_EC_KEY).unwrap(); + assert!(result.contains("-----BEGIN PRIVATE KEY-----")); + assert!(result.contains("-----END PRIVATE KEY-----")); + assert!(!result.contains("EC PRIVATE KEY")); + // Verify the result is valid PKCS#8 by re-parsing + let re_parsed = pem::parse(&result).unwrap(); + assert_eq!(re_parsed.tag(), "PRIVATE KEY"); + use pkcs8::der::Decode; + pkcs8::PrivateKeyInfo::from_der(re_parsed.contents()).unwrap(); + } + + #[test] + fn test_unsupported_key_format() { + let bad_pem = + "-----BEGIN DSA PRIVATE KEY-----\nMIIBuwIBAAKB\n-----END DSA PRIVATE KEY-----"; + let result = convert_key_to_pkcs8_pem(bad_pem); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Unsupported key format"),); + } + + #[test] + fn test_invalid_pem() { + let result = convert_key_to_pkcs8_pem("not a pem"); + assert!(result.is_err()); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/scripts/check-no-native-tls.sh b/scripts/check-no-native-tls.sh deleted file mode 100755 index c0a337fb..00000000 --- a/scripts/check-no-native-tls.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# Prevent native-tls/OpenSSL from being added to the dependency tree. -# These cause Linux compatibility issues with OpenSSL version mismatches. -# See: https://github.com/block/goose/issues/6034 - -set -e - -BANNED_CRATES=("native-tls" "openssl-sys" "openssl") -FOUND_BANNED=0 - -for crate in "${BANNED_CRATES[@]}"; do - if cargo tree -i "$crate" 2>/dev/null | grep -q "$crate"; then - echo "ERROR: Found banned crate '$crate' in dependency tree" - echo "This causes Linux compatibility issues with OpenSSL versions." - echo "Use rustls-based alternatives instead (e.g., rustls-tls-native-roots)." - echo "" - echo "Dependency chain:" - cargo tree -i "$crate" - echo "" - FOUND_BANNED=1 - fi -done - -if [ $FOUND_BANNED -eq 1 ]; then - exit 1 -fi - -echo "✓ No banned TLS crates found (native-tls, openssl, openssl-sys)"