diff --git a/crates/iam/src/root_credentials.rs b/crates/iam/src/root_credentials.rs index bf375c8a4..7a69e30b8 100644 --- a/crates/iam/src/root_credentials.rs +++ b/crates/iam/src/root_credentials.rs @@ -22,6 +22,15 @@ pub(crate) fn credentials_or_default() -> Credentials { credentials().unwrap_or_default() } +/// The signing key for STS session tokens. +/// +/// GHSA-m77q-r63m-pj89 (intentionally UNFIXED): this returns the root secret +/// key, so STS JWTs are signed with the shared root secret and anyone holding +/// it can forge session tokens. The behavior is pinned by +/// `test_ghsa_m77q_sts_session_token_signed_with_root_secret` in `sys.rs`; +/// fixing the advisory (a dedicated STS signing key distinct from the root +/// secret) must update that test red -> green. See +/// docs/testing/security-regressions.md. pub(crate) fn token_signing_key() -> Option { credentials().map(|cred| cred.secret_key) } diff --git a/crates/iam/src/sys.rs b/crates/iam/src/sys.rs index 5dfcce7fc..7895abe87 100644 --- a/crates/iam/src/sys.rs +++ b/crates/iam/src/sys.rs @@ -1952,6 +1952,18 @@ mod tests { ); } + /// GHSA-m77q-r63m-pj89 (STS JWTs signed with the shared root secret — + /// intentionally UNFIXED). This test characterizes the STS credential + /// lifecycle: a session token signed with the active token-signing key + /// decodes and authorizes through the STS path. It pins that the SAME key + /// both signs and verifies STS tokens. + /// + /// MUST UPDATE WHEN GHSA-m77q IS FIXED: the fix introduces a dedicated STS + /// signing key distinct from the root secret. The narrower pin that captures + /// the advisory's exact signature (signing key == root secret) is + /// `test_ghsa_m77q_sts_session_token_signed_with_root_secret`; keep both in + /// lockstep and flip them red -> green together. + /// Advisory: #[tokio::test] async fn test_created_sts_credentials_authorize_with_session_token_claims() { ensure_test_global_credentials(); @@ -2053,6 +2065,95 @@ mod tests { ); } + /// GHSA-m77q-r63m-pj89 flow-level pin (STS session tokens are signed with the + /// shared root secret — intentionally UNFIXED). Anyone holding the root secret + /// can forge STS session tokens because RustFS reuses the root secret as the + /// STS token-signing key. This test PINS that vulnerable-by-design behavior end + /// to end: (1) the token-signing key IS the root secret, (2) an AssumeRole-style + /// session token issued with that key decodes with the ROOT secret and NOT with + /// any other secret, and (3) it authorizes through the STS path. + /// + /// MUST UPDATE WHEN GHSA-m77q IS FIXED: the fix introduces a dedicated STS + /// signing key distinct from the root secret. That makes assertion (1) and the + /// "root secret decodes the token" assertion fail (red). Whoever fixes m77q must + /// replace this pin with a regression asserting the NEW behavior — the root + /// secret can no longer decode STS session tokens — i.e. red -> green. + /// Advisory: + #[tokio::test] + async fn test_ghsa_m77q_sts_session_token_signed_with_root_secret() { + ensure_test_global_credentials(); + + let root = crate::root_credentials::credentials().expect("global action (root) credentials should be initialized"); + assert!(!root.secret_key.is_empty(), "root secret must be present for the STS signing-key pin"); + + // (1) m77q signature: the STS token-signing key IS the root secret. A fix + // that introduces a dedicated STS key makes this fail -> update red->green. + assert_eq!( + crate::root_credentials::token_signing_key().as_deref(), + Some(root.secret_key.as_str()), + "GHSA-m77q pin: STS tokens are signed with the root secret. If this fails, \ + m77q may have been fixed (dedicated STS key) — update this test red->green." + ); + + // AssumeRole-style issuance: mint a session token with the active signing key. + // Use the mock store's registered STS parent (group `testgroup` -> readwrite) + // so authorization resolves through the STS path; the m77q pin is about the + // signing key, not the parent identity. + let parent_user = "sts-fallback-test-parent"; + let signing_key = crate::root_credentials::token_signing_key().expect("STS token-signing key should be present"); + let mut claims = HashMap::new(); + claims.insert("parent".to_string(), Value::String(parent_user.to_string())); + claims.insert( + "exp".to_string(), + Value::Number(serde_json::Number::from( + (OffsetDateTime::now_utc() + time::Duration::hours(1)).unix_timestamp(), + )), + ); + let mut cred = get_new_credentials_with_metadata(&claims, &signing_key).expect("STS credentials should be created"); + cred.parent_user = parent_user.to_string(); + + // (2) The session token decodes with the ROOT secret specifically — the crux + // of m77q — and rejects any non-root secret (HMAC verification fails). + let decoded = get_claims_from_token_with_secret(&cred.session_token, &root.secret_key) + .expect("GHSA-m77q pin: STS session token must decode with the ROOT secret"); + assert_eq!(decoded.get("parent").and_then(Value::as_str), Some(parent_user)); + assert!( + get_claims_from_token_with_secret(&cred.session_token, "not-the-root-secret").is_err(), + "GHSA-m77q pin: STS session token must NOT decode with a non-root secret" + ); + + // (3) The root-secret-signed STS credentials authorize end to end. + let store = StsTestMockStore::new(false); + let cache_manager = IamCache::new(store).await.unwrap(); + let iam_sys = IamSys::new(cache_manager); + iam_sys + .set_temp_user(&cred.access_key, &cred, None) + .await + .expect("STS credentials should be persisted in the temp-user cache"); + + let groups: Option> = None; + let args = Args { + account: &cred.access_key, + groups: &groups, + action: Action::S3Action(S3Action::ListBucketAction), + bucket: "mybucket", + conditions: &HashMap::new(), + is_owner: false, + object: "", + claims: &decoded, + deny_only: false, + }; + let prepared = iam_sys.prepare_auth(&args).await; + assert!( + matches!(prepared.mode, PreparedIamMode::Sts { .. }), + "root-secret-signed STS credentials must use the STS authorization path" + ); + assert!( + iam_sys.eval_prepared(&prepared, &args).await, + "root-secret-signed STS credentials should authorize through the parent group policy (m77q)" + ); + } + /// Regression test: temp credentials without groups in args still receive group-attached /// policies via the parent user (groups fallback). Without the fallback, policy_db_get /// would get None for groups and the user would have no group policies, so the action diff --git a/docs/testing/security-regressions.md b/docs/testing/security-regressions.md index f0b5740d5..5a727ea5c 100644 --- a/docs/testing/security-regressions.md +++ b/docs/testing/security-regressions.md @@ -20,7 +20,7 @@ forced to update its pinned test (red -> green). | --- | --- | --- | --- | --- | | [GHSA-3p3x-734c-h5vx](https://github.com/rustfs/rustfs/security/advisories/GHSA-3p3x-734c-h5vx) | Constant-time secret comparison on WebDAV/FTPS password login | rustfs/rustfs#4403 | `assert_ftps_ghsa_3p3x_wrong_credentials_rejected` (`crates/e2e_test/src/protocols/ftps_core.rs`); `GHSA-3p3x` auth-failure block in `test_webdav_core_operations` (`crates/e2e_test/src/protocols/webdav_core.rs`) | e2e (protocols suite) | | [GHSA-r5qv-rc46-hv8q](https://github.com/rustfs/rustfs/security/advisories/GHSA-r5qv-rc46-hv8q) | Internode RPC authentication must fail closed | rustfs/rustfs#4402 | `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback`, `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth` (`crates/ecstore/src/cluster/rpc/http_auth.rs`) | unit | -| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`) — pins current behavior; sec-7 adds GHSA naming | unit | +| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_ghsa_m77q_sts_session_token_signed_with_root_secret` (flow-level pin: signing key == root secret, root-only decode, authorizes) and `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`); `token_signing_key` doc (`crates/iam/src/root_credentials.rs`) — pin current by-design behavior; fixing m77q must update red -> green | unit | ## Where these run (CI-execution map)