Test unsigned replicated ops are rejected

This commit is contained in:
Eric Wendland 2026-05-22 14:17:17 +02:00
commit 0ffc950dc6
4 changed files with 171 additions and 4 deletions

2
Cargo.lock generated
View file

@ -1041,12 +1041,14 @@ name = "geth"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"geth-auth",
"geth-cas", "geth-cas",
"geth-cli", "geth-cli",
"geth-config", "geth-config",
"geth-control", "geth-control",
"geth-discovery", "geth-discovery",
"geth-iroh", "geth-iroh",
"geth-keychain",
"geth-node", "geth-node",
"geth-ssh-identity", "geth-ssh-identity",
"geth-store", "geth-store",

View file

@ -16,11 +16,13 @@ tracing-subscriber.workspace = true
geth-cli = { path = "../geth-cli" } geth-cli = { path = "../geth-cli" }
[dev-dependencies] [dev-dependencies]
geth-auth = { path = "../geth-auth" }
geth-cas = { path = "../geth-cas" } geth-cas = { path = "../geth-cas" }
geth-config = { path = "../geth-config" } geth-config = { path = "../geth-config" }
geth-control = { path = "../geth-control" } geth-control = { path = "../geth-control" }
geth-discovery = { path = "../geth-discovery" } geth-discovery = { path = "../geth-discovery" }
geth-iroh = { path = "../geth-iroh" } geth-iroh = { path = "../geth-iroh" }
geth-keychain = { path = "../geth-keychain" }
geth-node = { path = "../geth-node" } geth-node = { path = "../geth-node" }
geth-ssh-identity = { path = "../geth-ssh-identity" } geth-ssh-identity = { path = "../geth-ssh-identity" }
geth-store = { path = "../geth-store" } geth-store = { path = "../geth-store" }

View file

@ -630,6 +630,165 @@ fn denied_remote_operations_do_not_mutate_serving_node_state() {
assert!(denied_admin_stdout.contains("ssh admin shell denied")); assert!(denied_admin_stdout.contains("ssh admin shell denied"));
} }
#[test]
fn unsigned_keychain_and_auth_ops_are_rejected_during_peer_sync() {
let left_home = tempfile::tempdir().expect("left tempdir");
let right_home = tempfile::tempdir().expect("right tempdir");
if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) {
return;
}
assert!(run_geth(left_home.path(), &["init"]).status.success());
assert!(run_geth(right_home.path(), &["init"]).status.success());
for home in [left_home.path(), right_home.path()] {
std::fs::write(
home.join("config.toml"),
"[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n",
)
.expect("write config");
}
let mut left_daemon = spawn_daemon(left_home.path());
let mut right_daemon = spawn_daemon(right_home.path());
wait_for_socket(&left_home.path().join("run/geth.sock"));
wait_for_socket(&right_home.path().join("run/geth.sock"));
let left_card_path = left_home.path().join("left-peer-card.json");
let right_card_path = right_home.path().join("right-peer-card.json");
assert!(
run_geth(
left_home.path(),
&[
"peer",
"export",
"--out",
left_card_path.to_str().expect("left card path"),
],
)
.status
.success()
);
assert!(
run_geth(
right_home.path(),
&[
"peer",
"export",
"--out",
right_card_path.to_str().expect("right card path"),
],
)
.status
.success()
);
let right_card_json = std::fs::read_to_string(&right_card_path).expect("read right card");
let right_card: geth_discovery::PeerCard =
serde_json::from_str(&right_card_json).expect("decode right card");
assert!(
run_geth(
left_home.path(),
&[
"peer",
"import",
right_card_path.to_str().expect("right card path"),
],
)
.status
.success()
);
assert!(
run_geth(
right_home.path(),
&[
"peer",
"import",
left_card_path.to_str().expect("left card path"),
],
)
.status
.success()
);
let unsigned_keychain_op = geth_keychain::KeychainOp {
id: geth_types::AuthOpId::new("keychain-op:unsigned-user-add"),
created_at: geth_types::UnixMillis(10),
kind: geth_keychain::KeychainOpKind::UserAdd {
user: geth_types::UserId::new("user:unsigned"),
name: "Unsigned".to_owned(),
},
};
let unsigned_auth_op = geth_auth::AuthOp {
id: geth_types::AuthOpId::new("auth-op:unsigned-grant"),
resource: geth_types::ResourceId::new("resource:ssh-proxy:local"),
created_at: geth_types::UnixMillis(11),
kind: geth_auth::AuthOpKind::GrantCreate {
grant_id: "grant:unsigned".to_owned(),
principal: geth_types::PrincipalId::new("node:unsigned"),
capabilities: vec![geth_types::Capability::new("ssh_proxy.connect")],
},
};
let right_paths = geth_config::GethPaths::from_home(right_home.path());
let right_store =
geth_store::Store::open(&right_paths.metadata_db()).expect("open right store");
right_store
.insert_keychain_op(&geth_store::StoredKeychainOp {
op_id: unsigned_keychain_op.id.to_string(),
op_json: serde_json::to_string(&unsigned_keychain_op).expect("encode keychain op"),
created_at_ms: unsigned_keychain_op.created_at.0,
})
.expect("insert unsigned keychain op");
right_store
.insert_auth_op(&geth_store::StoredAuthOp {
op_id: unsigned_auth_op.id.to_string(),
resource_id: unsigned_auth_op.resource.to_string(),
op_json: serde_json::to_string(&unsigned_auth_op).expect("encode auth op"),
created_at_ms: unsigned_auth_op.created_at.0,
})
.expect("insert unsigned auth op");
let keychain_sync = run_geth(
left_home.path(),
&["keychain", "sync", right_card.node_id.as_str()],
);
let auth_sync = run_geth(
left_home.path(),
&["auth", "sync", right_card.node_id.as_str()],
);
let _ = left_daemon.kill();
let _ = right_daemon.kill();
let _ = left_daemon.wait();
let _ = right_daemon.wait();
assert!(
keychain_sync.status.success(),
"keychain sync stderr: {}",
String::from_utf8_lossy(&keychain_sync.stderr)
);
assert!(
auth_sync.status.success(),
"auth sync stderr: {}",
String::from_utf8_lossy(&auth_sync.stderr)
);
assert!(String::from_utf8_lossy(&keychain_sync.stdout).contains("invalid_ops_rejected: 1"));
assert!(String::from_utf8_lossy(&auth_sync.stdout).contains("invalid_ops_rejected: 1"));
let left_paths = geth_config::GethPaths::from_home(left_home.path());
let left_store = geth_store::Store::open(&left_paths.metadata_db()).expect("open left store");
assert!(
!left_store
.list_keychain_ops()
.expect("list left keychain ops")
.iter()
.any(|op| op.op_id == unsigned_keychain_op.id.as_str())
);
assert!(
!left_store
.list_auth_ops()
.expect("list left auth ops")
.iter()
.any(|op| op.op_id == unsigned_auth_op.id.as_str())
);
}
#[test] #[test]
fn peer_card_export_import_and_list_are_candidate_only() { fn peer_card_export_import_and_list_are_candidate_only() {
let source_home = tempfile::tempdir().expect("source tempdir"); let source_home = tempfile::tempdir().expect("source tempdir");

View file

@ -21,8 +21,10 @@ Implementation order:
- `[x]` Add initial two-daemon tests proving denied remote pubsub publish, - `[x]` Add initial two-daemon tests proving denied remote pubsub publish,
remote pipe listen, and SSH admin shell requests do not mutate serving remote pipe listen, and SSH admin shell requests do not mutate serving
node state. node state.
- `[ ]` Add tests proving unsigned, invalidly signed, and conflicting - `[x]` Add initial two-daemon tests proving unsigned replicated
replicated records do not mutate trust/resource state. keychain/auth operations are rejected and not imported.
- `[ ]` Add tests proving invalidly signed and conflicting replicated
keychain/auth records do not mutate trust/resource state.
- `[ ]` Improve `auth explain` diagnostics enough for operators to - `[ ]` Improve `auth explain` diagnostics enough for operators to
distinguish discovered-only peers, missing endpoint bindings, missing distinguish discovered-only peers, missing endpoint bindings, missing
grants, matching grants, revocations, and bearer access. grants, matching grants, revocations, and bearer access.
@ -67,8 +69,10 @@ Implementation order:
`geth sync status`. `geth sync status`.
- `[x]` A two-daemon test proves approved keychain/auth state reaches the - `[x]` A two-daemon test proves approved keychain/auth state reaches the
enrolled node without using the old one-off `node enroll sync` shortcut. enrolled node without using the old one-off `node enroll sync` shortcut.
- `[ ]` Tests assert rejected/unsigned/conflicting replicated records do not - `[x]` Tests assert unsigned replicated keychain/auth records do not mutate
mutate local trust or resource state. local trust or resource state.
- `[ ]` Tests assert invalidly signed/conflicting replicated keychain/auth
records do not mutate local trust or resource state.
- `[~]` Remote authorization enforcement audit. - `[~]` Remote authorization enforcement audit.
Acceptance criteria: Acceptance criteria: