diff --git a/Cargo.lock b/Cargo.lock index 88c7dc0b7c..acf5fff2c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -138,7 +138,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1356,15 +1356,16 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-rc.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.2", "fiat-crypto", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -1615,9 +1616,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.17.0-rc.18" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der 0.8.0", "digest 0.11.2", @@ -1640,9 +1641,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-rc.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", @@ -1665,9 +1666,9 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.14.0-rc.33" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102d3643d30dd8b559613c5cced68317199597fffb278cdc88daa2ef7fafc935" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", @@ -1677,7 +1678,6 @@ dependencies = [ "group", "hkdf 0.13.0", "hybrid-array", - "once_cell", "pem-rfc7468 1.0.0", "pkcs8 0.11.0", "rand_core 0.10.1", @@ -1737,7 +1737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2869,33 +2869,21 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "jsonwebtoken" version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", "getrandom 0.2.17", "js-sys", + "pem", "serde", "serde_json", "signature 2.2.0", + "simple_asn1", ] [[package]] @@ -3371,7 +3359,7 @@ dependencies = [ "module-lattice", "pkcs8 0.11.0", "rand_core 0.10.1", - "sha3", + "sha3 0.11.0", ] [[package]] @@ -3467,7 +3455,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3588,7 +3576,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-auth", - "jsonwebtoken 10.3.0", + "jsonwebtoken", "lazy_static", "oci-spec", "olpc-cjson", @@ -4108,7 +4096,7 @@ dependencies = [ "hyper-rustls 0.27.9", "hyper-util", "ipnet", - "jsonwebtoken 9.3.1", + "jsonwebtoken", "k8s-openapi", "kube", "metrics", @@ -4144,6 +4132,7 @@ dependencies = [ "rcgen", "reqwest 0.12.28", "ring", + "rsa 0.9.10", "russh", "rustix 1.1.4", "rustls 0.23.38", @@ -4427,9 +4416,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "p256" -version = "0.14.0-rc.10" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41adc63effe99d48837a8cc0e6d7a77e32ae6a07f6000df466178dbc2193093e" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", @@ -4440,9 +4429,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.14.0-rc.10" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd5333afa5ae0347f39e6a0f2c9c155da431583fd71fe5555bd0521b4ccaf02" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ "ecdsa", "elliptic-curve", @@ -4454,9 +4443,9 @@ dependencies = [ [[package]] name = "p521" -version = "0.14.0-rc.10" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a5297f53dc16d35909060ba3032cff7867e8809f01e273ff325579d5f0ceae" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" dependencies = [ "base16ct", "ecdsa", @@ -4847,11 +4836,15 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.14.0-rc.10" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2793f22b9b6fd11ef3ac1d59bf003c2573593e4968702341605c2748fd90bf" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -5354,12 +5347,12 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ + "crypto-bigint", "hmac 0.13.0", - "subtle", ] [[package]] @@ -5429,9 +5422,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.61.2" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf893f64684e58da8a68d56a5e84d1cf0440226274c515770fe267707a7d0b0" +checksum = "da7c230e0ed9cbeb92fbad6c8848985d6df2a1464c0dc247a021abd666e9005e" dependencies = [ "aes", "aws-lc-rs", @@ -5486,7 +5479,7 @@ dependencies = [ "sec1", "sha1 0.11.0", "sha2 0.11.0", - "sha3", + "sha3 0.12.0", "signature 3.0.0", "spki 0.8.0", "ssh-encoding", @@ -5501,9 +5494,9 @@ dependencies = [ [[package]] name = "russh-cryptovec" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" dependencies = [ "log", "nix 0.31.3", @@ -5582,7 +5575,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5662,7 +5655,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6046,6 +6039,17 @@ dependencies = [ "keccak", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.2", + "keccak", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -6181,7 +6185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6241,6 +6245,12 @@ dependencies = [ "der 0.8.0", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlx" version = "0.8.6" @@ -6431,17 +6441,15 @@ dependencies = [ [[package]] name = "ssh-cipher" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" dependencies = [ "aead", "aes", "aes-gcm", - "cbc", "chacha20", "cipher", - "ctr", "ctutils", "des", "poly1305", @@ -6451,9 +6459,9 @@ dependencies = [ [[package]] name = "ssh-encoding" -version = "0.3.0-rc.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "bytes", @@ -6466,9 +6474,9 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.7.0-rc.10" +version = "0.7.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" dependencies = [ "argon2", "bcrypt-pbkdf", @@ -6646,9 +6654,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -6674,7 +6682,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6710,7 +6718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7707,7 +7715,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -8249,6 +8257,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 9d801570ee..26c1f72f11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ base64 = "0.22" # Crypto / Auth sha2 = "0.10" rand = "0.9" -jsonwebtoken = "9" +jsonwebtoken = { version = "10", features = ["aws_lc_rs"] } getrandom = "0.3" ring = "0.17" spiffe = { version = "0.15", default-features = false, features = ["workload-api-jwt", "tracing"] } diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 7182d0f702..e158edda48 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -106,7 +106,7 @@ async-trait = "0.1" url = { workspace = true } glob = { workspace = true } hex = "0.4" -russh = "0.61" +russh = "0.62" rand = { workspace = true } petname = "2" ipnet = "2" @@ -128,6 +128,8 @@ test-support = [] [dev-dependencies] hyper-rustls = { version = "0.27", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "ring"] } rcgen = { version = "0.13", features = ["crypto", "pem"] } +rsa = { version = "0.9", features = ["pem"] } +base64 = { workspace = true } tokio-tungstenite = { workspace = true } futures-util = "0.3" wiremock = "0.6" diff --git a/crates/openshell-server/src/auth/oidc.rs b/crates/openshell-server/src/auth/oidc.rs index cbe83ff060..fd599b501a 100644 --- a/crates/openshell-server/src/auth/oidc.rs +++ b/crates/openshell-server/src/auth/oidc.rs @@ -510,4 +510,256 @@ mod tests { let scopes = claims.extract_scopes("scope"); assert!(scopes.is_empty()); } + + // ----------------------------------------------------------------------- + // RS256 verification through the real JWKS path + // + // The tests above only cover claim extraction from an already-trusted + // payload. These sign real RS256 tokens and push them through + // `JwksCache::new` + `validate_token`, so the JWKS `n`/`e` decoding and the + // RSA signature check are exercised against whichever crypto backend + // `jsonwebtoken` is built with — a backend swap is otherwise invisible to + // the test suite. + // ----------------------------------------------------------------------- + + const TEST_KID: &str = "test-signing-key"; + const TEST_AUDIENCE: &str = "openshell-cli"; + + /// One RSA key per test binary. Key generation dominates the runtime of + /// these tests and the key carries no meaning beyond being valid. + static TEST_RSA_KEY: std::sync::LazyLock = + std::sync::LazyLock::new(TestRsaKey::generate); + + struct TestRsaKey { + private_pem: String, + modulus_b64: String, + exponent_b64: String, + } + + impl TestRsaKey { + fn generate() -> Self { + use base64::Engine as _; + use rsa::pkcs1::EncodeRsaPrivateKey as _; + use rsa::traits::PublicKeyParts as _; + + let private = rsa::RsaPrivateKey::new(&mut rsa::rand_core::OsRng, 2048) + .expect("generate RSA test key"); + let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; + Self { + private_pem: private + .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF) + .expect("encode RSA private key as PEM") + .to_string(), + modulus_b64: b64.encode(private.n().to_bytes_be()), + exponent_b64: b64.encode(private.e().to_bytes_be()), + } + } + } + + fn now_secs() -> i64 { + i64::try_from( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is after the unix epoch") + .as_secs(), + ) + .expect("current time fits in i64") + } + + /// Sign `claims` with the test key, tagging the header with `kid`. + fn mint_rs256(claims: &serde_json::Value, kid: &str) -> String { + let mut header = jsonwebtoken::Header::new(Algorithm::RS256); + header.kid = Some(kid.to_owned()); + let key = jsonwebtoken::EncodingKey::from_rsa_pem(TEST_RSA_KEY.private_pem.as_bytes()) + .expect("load RSA signing key"); + jsonwebtoken::encode(&header, claims, &key).expect("sign RS256 token") + } + + fn claims_for(issuer: &str, audience: &str, exp: i64) -> serde_json::Value { + serde_json::json!({ + "sub": "user-42", + "preferred_username": "ada", + "iss": issuer, + "aud": audience, + "exp": exp, + "scope": "openid profile sandbox:write", + "realm_access": { "roles": ["openshell-user"] }, + }) + } + + /// Serve an OIDC discovery document and a JWKS carrying the test key, then + /// build a cache against them the same way production does. + async fn cache_with_mock_issuer(server: &wiremock::MockServer) -> JwksCache { + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let issuer = server.uri(); + Mock::given(method("GET")) + .and(path("/.well-known/openid-configuration")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "issuer": issuer, + "jwks_uri": format!("{issuer}/jwks"), + }))) + .mount(server) + .await; + Mock::given(method("GET")) + .and(path("/jwks")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "keys": [{ + "kid": TEST_KID, + "kty": "RSA", + "n": TEST_RSA_KEY.modulus_b64, + "e": TEST_RSA_KEY.exponent_b64, + }], + }))) + .mount(server) + .await; + + JwksCache::new(&OidcConfig { + issuer, + audience: TEST_AUDIENCE.to_owned(), + jwks_ttl_secs: 3600, + roles_claim: "realm_access.roles".to_owned(), + admin_role: "openshell-admin".to_owned(), + user_role: "openshell-user".to_owned(), + scopes_claim: "scope".to_owned(), + }) + .await + .expect("cache should build from the mock issuer") + } + + #[tokio::test] + async fn rs256_token_signed_by_jwks_key_is_accepted() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let token = mint_rs256( + &claims_for(&server.uri(), TEST_AUDIENCE, now_secs() + 3600), + TEST_KID, + ); + let identity = cache + .validate_token(&token) + .await + .expect("a correctly signed token must be accepted"); + + assert_eq!(identity.subject, "user-42"); + assert_eq!(identity.display_name.as_deref(), Some("ada")); + assert_eq!(identity.roles, vec!["openshell-user".to_owned()]); + assert_eq!(identity.scopes, vec!["sandbox:write".to_owned()]); + assert_eq!(identity.provider, IdentityProvider::Oidc); + } + + #[tokio::test] + async fn rs256_token_with_tampered_payload_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let exp = now_secs() + 3600; + let token = mint_rs256(&claims_for(&server.uri(), TEST_AUDIENCE, exp), TEST_KID); + + // Keep the header and signature but swap in a payload that escalates + // the subject: only the RSA check stands between this and an identity. + let segments: Vec<&str> = token.split('.').collect(); + assert_eq!(segments.len(), 3, "a JWT has three segments"); + let mut forged_claims = claims_for(&server.uri(), TEST_AUDIENCE, exp); + forged_claims["sub"] = serde_json::json!("root"); + let forged_payload = { + use base64::Engine as _; + base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&forged_claims).expect("serialize forged claims")) + }; + let forged = format!("{}.{forged_payload}.{}", segments[0], segments[2]); + + cache + .validate_token(&forged) + .await + .expect_err("a swapped payload must fail the signature check"); + } + + #[tokio::test] + async fn rs256_token_signed_by_unrelated_key_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let other = TestRsaKey::generate(); + let mut header = jsonwebtoken::Header::new(Algorithm::RS256); + header.kid = Some(TEST_KID.to_owned()); + let token = jsonwebtoken::encode( + &header, + &claims_for(&server.uri(), TEST_AUDIENCE, now_secs() + 3600), + &jsonwebtoken::EncodingKey::from_rsa_pem(other.private_pem.as_bytes()) + .expect("load unrelated signing key"), + ) + .expect("sign with unrelated key"); + + cache + .validate_token(&token) + .await + .expect_err("a token signed by a key outside the JWKS must be rejected"); + } + + #[tokio::test] + async fn rs256_expired_token_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + // Beyond the 60s default leeway. + let token = mint_rs256( + &claims_for(&server.uri(), TEST_AUDIENCE, now_secs() - 3600), + TEST_KID, + ); + + cache + .validate_token(&token) + .await + .expect_err("an expired token must be rejected"); + } + + #[tokio::test] + async fn rs256_token_from_other_issuer_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let token = mint_rs256( + &claims_for("https://evil.example.com", TEST_AUDIENCE, now_secs() + 3600), + TEST_KID, + ); + + cache + .validate_token(&token) + .await + .expect_err("a token from another issuer must be rejected"); + } + + #[tokio::test] + async fn rs256_token_for_other_audience_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let token = mint_rs256( + &claims_for(&server.uri(), "some-other-client", now_secs() + 3600), + TEST_KID, + ); + + cache + .validate_token(&token) + .await + .expect_err("a token minted for another audience must be rejected"); + } + + #[tokio::test] + async fn rs256_token_with_unknown_kid_is_rejected() { + let server = wiremock::MockServer::start().await; + let cache = cache_with_mock_issuer(&server).await; + + let token = mint_rs256( + &claims_for(&server.uri(), TEST_AUDIENCE, now_secs() + 3600), + "rotated-away-key", + ); + + cache + .validate_token(&token) + .await + .expect_err("a token naming a kid absent from the JWKS must be rejected"); + } } diff --git a/crates/openshell-supervisor-process/Cargo.toml b/crates/openshell-supervisor-process/Cargo.toml index 575923d4c4..7b91887588 100644 --- a/crates/openshell-supervisor-process/Cargo.toml +++ b/crates/openshell-supervisor-process/Cargo.toml @@ -21,7 +21,7 @@ hex = "0.4" miette = { workspace = true } nix = { workspace = true } rand = "0.10" -russh = "0.61" +russh = "0.62" serde_json = { workspace = true } sha2 = { workspace = true } tokio = { workspace = true } diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index be1b679530..07302da953 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -20,9 +20,9 @@ use openshell_core::provider_credentials::ProviderCredentialState; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, SeverityId, SshActivityBuilder, StatusId, ocsf_emit, }; -use russh::ChannelId; use russh::keys::{Algorithm, PrivateKey}; -use russh::server::{Auth, Handle, Session}; +use russh::server::{Auth, ChannelOpenHandle, Handle, Session}; +use russh::{ChannelId, ChannelOpenFailure}; use std::collections::HashMap; use std::io::{Read, Write}; use std::os::fd::{AsRawFd, RawFd}; @@ -297,10 +297,12 @@ impl russh::server::Handler for SshHandler { async fn channel_open_session( &mut self, channel: russh::Channel, + reply: ChannelOpenHandle, _session: &mut Session, - ) -> Result { + ) -> Result<(), Self::Error> { self.channels.insert(channel.id(), ChannelState::default()); - Ok(true) + reply.accept().await; + Ok(()) } /// Clean up per-channel state when the channel is closed. @@ -324,8 +326,9 @@ impl russh::server::Handler for SshHandler { port_to_connect: u32, _originator_address: &str, _originator_port: u32, + reply: ChannelOpenHandle, _session: &mut Session, - ) -> Result { + ) -> Result<(), Self::Error> { // Validate port range before truncating u32 -> u16. The SSH protocol // uses u32 for ports, but valid TCP ports are 0-65535. Without this // check, port 65537 truncates to port 1 (privileged). @@ -339,7 +342,10 @@ impl russh::server::Handler for SshHandler { "direct-tcpip rejected: port {port_to_connect} exceeds valid TCP range for host {host_to_connect}" )) .build()); - return Ok(false); + reply + .reject(ChannelOpenFailure::AdministrativelyProhibited) + .await; + return Ok(()); } // Only allow forwarding to loopback destinations to prevent the @@ -354,7 +360,10 @@ impl russh::server::Handler for SshHandler { "direct-tcpip rejected: non-loopback destination {host_to_connect}:{port_to_connect}" )) .build()); - return Ok(false); + reply + .reject(ChannelOpenFailure::AdministrativelyProhibited) + .await; + return Ok(()); } let host = host_to_connect.to_string(); @@ -363,6 +372,10 @@ impl russh::server::Handler for SshHandler { let port = u16::try_from(port_to_connect).unwrap_or(u16::MAX); let netns_fd = self.netns_fd; + // Confirm the channel before spawning: the task below writes to it, and + // the peer must see the open-confirmation first. + reply.accept().await; + tokio::spawn(async move { let addr = format!("{host}:{port}"); let tcp = match connect_in_netns(&addr, netns_fd).await { @@ -387,7 +400,7 @@ impl russh::server::Handler for SshHandler { let _ = tokio::io::copy_bidirectional(&mut channel_stream, &mut tcp_stream).await; }); - Ok(true) + Ok(()) } async fn pty_request( @@ -1951,4 +1964,178 @@ mod tests { "resolved-identity-ok" ); } + + // ----------------------------------------------------------------------- + // direct-tcpip authorization wiring (SEC-007) + // + // The `loopback_host_*` tests above cover the predicate in isolation. + // These drive the real `russh::server::Handler` over an in-memory duplex + // so the deny path itself is covered: channel-open authorization travels + // through a reply handle rather than the handler's return value, so a + // handler that never rejects anything still type-checks and still passes + // every predicate test. + // ----------------------------------------------------------------------- + + struct AcceptAnyServerKey; + + impl russh::client::Handler for AcceptAnyServerKey { + type Error = russh::Error; + + async fn check_server_key( + &mut self, + _server_public_key: &russh::keys::PublicKey, + ) -> Result { + Ok(true) + } + } + + fn forwarding_test_policy() -> SandboxPolicy { + use openshell_core::policy::{ + FilesystemPolicy, LandlockPolicy, NetworkPolicy, ProcessPolicy, + }; + + SandboxPolicy { + version: 0, + filesystem: FilesystemPolicy::default(), + network: NetworkPolicy::default(), + landlock: LandlockPolicy::default(), + process: ProcessPolicy { + run_as_user: None, + run_as_group: None, + }, + } + } + + /// Serve `SshHandler` on one end of an in-memory duplex and return an + /// authenticated client handle for the other end. + /// + /// The handler gets `netns_fd: None` so `connect_in_netns` performs a plain + /// TCP connect, making the forwarding path reachable without a network + /// namespace. + async fn authenticated_test_client() -> russh::client::Handle { + // Scoped so the `!Send` ThreadRng is dropped before the first await. + let host_key = { + let mut rng = rand::rng(); + PrivateKey::random(&mut rng, Algorithm::Ed25519).expect("host key") + }; + let mut server_config = russh::server::Config { + auth_rejection_time: Duration::from_millis(1), + ..Default::default() + }; + server_config.keys.push(host_key); + + let handler = SshHandler::new( + forwarding_test_policy(), + ResolvedWorkspace::default(), + None, + None, + None, + ProviderCredentialState::from_child_env_snapshot(0, HashMap::new()), + HashMap::new(), + ResolvedProcessIdentity::default(), + ProcessEnforcementMode::NetworkOnly, + ); + + let (server_stream, client_stream) = tokio::io::duplex(64 * 1024); + tokio::spawn(async move { + if let Ok(session) = + russh::server::run_stream(Arc::new(server_config), server_stream, handler).await + { + let _ = session.await; + } + }); + + let mut client = russh::client::connect_stream( + Arc::new(russh::client::Config::default()), + client_stream, + AcceptAnyServerKey, + ) + .await + .expect("SSH handshake should complete over the duplex"); + + let auth = client + .authenticate_none("sandbox") + .await + .expect("auth_none should not error"); + assert!( + matches!(auth, russh::client::AuthResult::Success), + "sandbox SSH server accepts the none auth method" + ); + + client + } + + #[tokio::test] + async fn direct_tcpip_rejects_non_loopback_destination() { + let client = authenticated_test_client().await; + + let err = client + .channel_open_direct_tcpip("10.0.0.1", 80, "127.0.0.1", 0) + .await + .expect_err("forwarding to a non-loopback host must be refused"); + + assert!( + matches!( + err, + russh::Error::ChannelOpenFailure(ChannelOpenFailure::AdministrativelyProhibited) + ), + "expected AdministrativelyProhibited, got {err:?}" + ); + } + + #[tokio::test] + async fn direct_tcpip_rejects_port_above_tcp_range() { + let client = authenticated_test_client().await; + + // 65_537 truncates to port 1 when cast to u16, so the guard has to + // reject it before the cast rather than forward to a privileged port. + let err = client + .channel_open_direct_tcpip("127.0.0.1", 65_537, "127.0.0.1", 0) + .await + .expect_err("a port outside the TCP range must be refused"); + + assert!( + matches!( + err, + russh::Error::ChannelOpenFailure(ChannelOpenFailure::AdministrativelyProhibited) + ), + "expected AdministrativelyProhibited, got {err:?}" + ); + } + + #[tokio::test] + async fn direct_tcpip_forwards_to_loopback_listener() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind loopback echo listener"); + let port = listener.local_addr().expect("listener address").port(); + tokio::spawn(async move { + if let Ok((mut socket, _)) = listener.accept().await { + let mut buf = [0u8; 64]; + if let Ok(n) = socket.read(&mut buf).await + && n > 0 + { + let _ = socket.write_all(&buf[..n]).await; + } + } + }); + + let client = authenticated_test_client().await; + let channel = client + .channel_open_direct_tcpip("127.0.0.1", u32::from(port), "127.0.0.1", 0) + .await + .expect("forwarding to a loopback listener must be allowed"); + + let mut stream = channel.into_stream(); + stream.write_all(b"ping").await.expect("write to channel"); + + let mut echoed = [0u8; 4]; + tokio::time::timeout(Duration::from_secs(10), stream.read_exact(&mut echoed)) + .await + .expect("relayed response should arrive before the timeout") + .expect("read from channel"); + assert_eq!(&echoed, b"ping", "bytes round-trip through the tunnel"); + } } diff --git a/scripts/lint-mermaid/package-lock.json b/scripts/lint-mermaid/package-lock.json index a28a4f295a..1fa73b21e6 100644 --- a/scripts/lint-mermaid/package-lock.json +++ b/scripts/lint-mermaid/package-lock.json @@ -44,42 +44,10 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", - "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "12.0.0", - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/gast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", - "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", - "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", - "license": "Apache-2.0" - }, "node_modules/@chevrotain/types": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", - "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", - "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", - "license": "Apache-2.0" + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==" }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", @@ -209,12 +177,11 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", - "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", "dependencies": { - "langium": "^4.0.0" + "@chevrotain/types": "~11.1.2" } }, "node_modules/@types/d3": { @@ -533,34 +500,6 @@ "node": ">= 0.4" } }, - "node_modules/chevrotain": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", - "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "12.0.0", - "@chevrotain/gast": "12.0.0", - "@chevrotain/regexp-to-ast": "12.0.0", - "@chevrotain/types": "12.0.0", - "@chevrotain/utils": "12.0.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", - "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^12.0.0" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -617,10 +556,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", - "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", - "license": "MIT", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "engines": { "node": ">=0.10" } @@ -1255,17 +1193,21 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==" + }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -1363,10 +1305,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dependencies": { "function-bind": "^1.1.2" }, @@ -1509,24 +1450,6 @@ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/langium": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", - "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", - "license": "MIT", - "dependencies": { - "@chevrotain/regexp-to-ast": "~12.0.0", - "chevrotain": "~12.0.0", - "chevrotain-allstar": "~0.4.1", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.1.0" - }, - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -1567,32 +1490,31 @@ } }, "node_modules/mermaid": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", - "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", - "license": "MIT", + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", + "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.0", + "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", + "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", - "katex": "^0.16.25", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", "khroma": "^2.1.0", - "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "node_modules/mime-db": { @@ -1833,67 +1755,17 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "uuid": "dist-node/bin/uuid" } }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -1951,10 +1823,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", + "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", "engines": { "node": ">=10.0.0" },