//! Extended KEYED managed-config scenarios. Harness + seam/serial constraints: //! `signed_managed_config/common.rs`. //! //! Placement rule: new keyed scenarios land HERE; `signed_managed_config.rs` //! stays fixed to the review-cited security claims (verify-persists / //! reject-persists-nothing / sidecar-deletion-refuses). #[path = "signed_managed_config/common.rs"] mod common; #[cfg(unix)] use common::skip_as_root; use common::{ MANAGED, REQUIREMENTS_FAIL_CLOSED, TEST_EXPIRES_AT, TEST_KEY_ID, dk_identity, forged_dk_body, install_test_key, reset, sign_envelope, signed_dk_body, spawn_mock, test_home, write_dk_config, }; use kigi_config::signed_policy::{self, SignedPayload}; use serial_test::serial; /// The healthy fail-closed starting state the tamper/heal scenarios mutate; /// the mock keeps serving the same body, so a healing sync can refetch it. async fn sync_fail_closed_policy(home: &std::path::Path, kp: &ring::signature::Ed25519KeyPair) { let url = spawn_mock(signed_dk_body( kp, "dep-42", Some(MANAGED), Some(REQUIREMENTS_FAIL_CLOSED), )); write_dk_config(home, &url, "dep-key-1"); kigi_shell::managed_config::sync() .await .expect("initial sync should succeed"); assert!(kigi_shell::managed_config::managed_policy_gate().is_ok()); } /// The signed-empty deployment response: a `{}` body (no legacy fields) whose /// envelope binds ABSENCE to `deployment_id` — what the server serves for a /// provisioned key with no config row. fn signed_dk_empty_body(kp: &ring::signature::Ed25519KeyPair, deployment_id: &str) -> String { let payload = SignedPayload { version: kigi_config::signed_policy::SIGNED_PAYLOAD_VERSION, deployment_id: Some(deployment_id.to_owned()), team_id: None, managed_config: None, requirements: None, fail_closed: false, expires_at: TEST_EXPIRES_AT, key_id: TEST_KEY_ID.into(), }; serde_json::json!({ "signatures": [sign_envelope(kp, &payload)] }).to_string() } /// The marker principal for an applied signed-EMPTY dk response comes from the /// VERIFIED payload's deployment_id (the `{}` body carries none), so the gate's /// cross-tenant binding holds even on an unprovisioned dk machine. #[tokio::test] #[serial] async fn empty_dk_response_marker_binds_the_verified_deployment_id() { let home = test_home().clone(); reset(&home); let (kp, _pubkey) = install_test_key(); let url = spawn_mock(signed_dk_empty_body(&kp, "dep-42")); write_dk_config(&home, &url, "dep-key-1"); // No team auth: the empty dk body is applied (converges), not fallen through. let wrote = kigi_shell::managed_config::sync() .await .expect("signed-empty dk sync should succeed"); assert!(!wrote, "nothing to write for an empty row"); let marker = std::fs::read_to_string(home.join("managed_config_cache.json")).unwrap(); let v: serde_json::Value = serde_json::from_str(&marker).unwrap(); assert_eq!( v["principal"].as_str(), Some("dep-42"), "the marker must bind the VERIFIED deployment id: {marker}" ); assert!( home.join("managed_config.sig.json").exists(), "the absence envelope is persisted" ); assert!(kigi_shell::managed_config::managed_policy_gate().is_ok()); } /// A signature-rejected sync surfaces as failure in BOTH `grok setup` and the /// post-login sync — never as Installed/NoChange while nothing was persisted. #[tokio::test] #[serial] async fn rejected_signature_surfaces_as_setup_and_login_failure() { let home = test_home().clone(); reset(&home); let (kp, _pubkey) = install_test_key(); let url = spawn_mock(forged_dk_body(&kp, "dep-42")); write_dk_config(&home, &url, "dep-key-1"); let outcome = kigi_shell::managed_config::run_setup().await; assert!( matches!( outcome, kigi_shell::managed_config::SetupOutcome::Failed( kigi_shell::managed_config::ManagedConfigError::SignatureRejected ) ), "setup must surface the signature rejection, got {outcome:?}" ); let login = kigi_shell::managed_config::post_login_sync(None).await; assert_eq!( login, kigi_shell::managed_config::ManagedConfigSync::Failed, "post-login sync must report Failed, not NoChange" ); } /// A response that stops serving requirements deletes the on-disk file, and the /// NEW sidecar (written after the deletion) covers the absence — the converged cache /// reads fresh and the gate allows. #[tokio::test] #[serial] async fn withdrawn_requirements_is_deleted_and_covered_by_the_new_sidecar() { let home = test_home().clone(); reset(&home); let (kp, pubkey) = install_test_key(); sync_fail_closed_policy(&home, &kp).await; assert!(home.join("requirements.toml").exists()); let url_partial = spawn_mock(signed_dk_body(&kp, "dep-42", Some(MANAGED), None)); write_dk_config(&home, &url_partial, "dep-key-1"); let wrote = kigi_shell::managed_config::sync() .await .expect("withdrawing sync should succeed"); assert!(wrote, "the deletion is a change"); assert!( !home.join("requirements.toml").exists(), "the withdrawn artifact is removed" ); let sidecar: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(home.join("managed_config.sig.json")).unwrap(), ) .unwrap(); let payload = signed_policy::verify_signed_payload( sidecar["signed_payload"].as_str().unwrap(), sidecar["signature"].as_str().unwrap(), &[(TEST_KEY_ID, &pubkey)], ) .expect("the refreshed sidecar must verify"); assert!( payload.requirements.is_none(), "the new sidecar covers the absence" ); assert!( !kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()), "the converged, covered cache is not hard-stale" ); assert!(kigi_shell::managed_config::managed_policy_gate().is_ok()); } /// A directory squatting at a signed artifact path reads COMPROMISED at the gate /// (not lenient-unreadable), and an online sync converges over it — clearing the /// directory, rewriting the file, and restoring enforcement. #[tokio::test] #[serial] async fn directory_squat_reads_compromised_and_online_sync_heals() { let home = test_home().clone(); reset(&home); let (kp, _pubkey) = install_test_key(); sync_fail_closed_policy(&home, &kp).await; // Dir-squat the enforced artifact (with a child, like a real squat). std::fs::remove_file(home.join("requirements.toml")).unwrap(); std::fs::create_dir(home.join("requirements.toml")).unwrap(); std::fs::write(home.join("requirements.toml").join("junk"), "x").unwrap(); let gate = kigi_shell::managed_config::managed_policy_gate(); assert!( gate.is_err(), "a directory squat on a fail-closed policy must refuse offline" ); // The gate verdict, not an incidental error; classification is unit-pinned // in signed_policy::directory_squat_is_tamper_not_unreadable. assert!( gate.unwrap_err() .contains("Managed policy is required for this account"), "the refusal is the managed-policy gate message" ); assert!( kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()), "the squat must trigger the refetch" ); let wrote = kigi_shell::managed_config::sync() .await .expect("healing sync should succeed"); assert!(wrote, "the healing sync must rewrite the squatted artifact"); assert_eq!( std::fs::read_to_string(home.join("requirements.toml")).unwrap(), REQUIREMENTS_FAIL_CLOSED, "the served file replaces the squatting directory" ); assert!( kigi_shell::managed_config::managed_policy_gate().is_ok(), "enforcement is restored after the heal" ); } /// A sidecar read blip (chmod 000) is not tamper: the gate allows while the /// refetch trigger fires — mirroring the artifact-slot blip semantics. #[cfg(unix)] #[tokio::test] #[serial] async fn sidecar_read_blip_allows_session_and_triggers_refetch() { use std::os::unix::fs::PermissionsExt; let home = test_home().clone(); reset(&home); let (kp, _pubkey) = install_test_key(); sync_fail_closed_policy(&home, &kp).await; let sidecar_path = home.join("managed_config.sig.json"); std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o000)).unwrap(); if skip_as_root( &sidecar_path, "sidecar_read_blip_allows_session_and_triggers_refetch", ) { let _ = std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o600)); return; } assert!( kigi_shell::managed_config::managed_policy_gate().is_ok(), "a transient sidecar read blip must not refuse the session" ); assert!( kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()), "the blip must trigger the refetch so the self-heal runs" ); // Restore so the tempdir (and later tests) stay clean. std::fs::set_permissions(&sidecar_path, std::fs::Permissions::from_mode(0o600)).unwrap(); } /// A directory squatting at the SIDECAR path refuses at the gate, and the online /// sync clears it — a bare rename would error forever. #[tokio::test] #[serial] async fn sidecar_directory_squat_refuses_then_online_sync_heals() { let home = test_home().clone(); reset(&home); let (kp, _pubkey) = install_test_key(); sync_fail_closed_policy(&home, &kp).await; // Dir-squat the sidecar (with a child, like a real squat). let sidecar_path = home.join("managed_config.sig.json"); std::fs::remove_file(&sidecar_path).unwrap(); std::fs::create_dir(&sidecar_path).unwrap(); std::fs::write(sidecar_path.join("junk"), "x").unwrap(); let gate = kigi_shell::managed_config::managed_policy_gate(); assert!( gate.is_err(), "an unreadable (squatted) sidecar under a fail-closed marker must refuse offline" ); // The gate verdict, not an incidental error; classification is unit-pinned // in signed_policy::sidecar_directory_squat_is_absence_not_a_blip. assert!( gate.unwrap_err() .contains("Managed policy is required for this account"), "the refusal is the managed-policy gate message" ); assert!( kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()), "the squat must trigger the refetch" ); kigi_shell::managed_config::sync() .await .expect("healing sync should succeed"); assert!( sidecar_path.is_file(), "the rewrite must replace the squatting directory with a sidecar FILE" ); // Under a fail-closed marker the gate requires an authentic sidecar, so // allowing here also pins that the healed sidecar verifies. assert!( kigi_shell::managed_config::managed_policy_gate().is_ok(), "enforcement is restored after the heal" ); }