Test conflicting replicated ops are rejected

This commit is contained in:
Eric Wendland 2026-05-22 14:22:07 +02:00
commit 6f2fb53132
2 changed files with 291 additions and 2 deletions

View file

@ -54,6 +54,28 @@ fn generate_ssh_key(path: &std::path::Path) {
assert!(status.success());
}
fn sign_payload_with_ssh_key(
dir: &std::path::Path,
stem: &str,
private_key_path: &std::path::Path,
namespace: &str,
payload: &[u8],
) -> Vec<u8> {
let payload_path = dir.join(format!("{stem}.payload"));
std::fs::write(&payload_path, payload).expect("write signing payload");
let output = geth_ssh_identity::sign_command(private_key_path, namespace, &payload_path)
.output()
.expect("run ssh-keygen sign");
assert!(
output.status.success(),
"ssh-keygen sign stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let signature_path =
std::path::Path::new(&format!("{}.sig", payload_path.display())).to_path_buf();
std::fs::read(signature_path).expect("read signature")
}
fn wait_for_socket(path: &std::path::Path) {
let started = Instant::now();
while started.elapsed() < Duration::from_secs(5) {
@ -993,6 +1015,273 @@ fn invalidly_signed_keychain_and_auth_ops_are_rejected_during_peer_sync() {
);
}
#[test]
fn conflicting_keychain_and_auth_ops_are_rejected_during_peer_sync() {
if !ssh_keygen_available() {
return;
}
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 admin_key_path = left_home.path().join("admin_ed25519");
generate_ssh_key(&admin_key_path);
let admin_public_key_path = admin_key_path.with_extension("pub");
let admin_public_key =
std::fs::read_to_string(&admin_public_key_path).expect("read admin public key");
let signer = geth_types::KeyId::new(geth_ssh_identity::ssh_public_key_fingerprint(
&admin_public_key,
));
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 trust_admin = run_geth(
left_home.path(),
&[
"keychain",
"init",
"--admin-key",
admin_public_key_path
.to_str()
.expect("admin public key path"),
],
);
assert!(
trust_admin.status.success(),
"trust admin stderr: {}",
String::from_utf8_lossy(&trust_admin.stderr)
);
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 keychain_op_id = geth_types::AuthOpId::new("keychain-op:conflict");
let local_keychain_op = geth_keychain::KeychainOp {
id: keychain_op_id.clone(),
created_at: geth_types::UnixMillis(30),
kind: geth_keychain::KeychainOpKind::UserAdd {
user: geth_types::UserId::new("user:local-conflict"),
name: "Local Conflict".to_owned(),
},
};
let remote_keychain_op = geth_keychain::KeychainOp {
id: keychain_op_id.clone(),
created_at: geth_types::UnixMillis(31),
kind: geth_keychain::KeychainOpKind::UserAdd {
user: geth_types::UserId::new("user:remote-conflict"),
name: "Remote Conflict".to_owned(),
},
};
let auth_op_id = geth_types::AuthOpId::new("auth-op:conflict");
let local_auth_op = geth_auth::AuthOp {
id: auth_op_id.clone(),
resource: geth_types::ResourceId::new("resource:ssh-proxy:local"),
created_at: geth_types::UnixMillis(32),
kind: geth_auth::AuthOpKind::GrantCreate {
grant_id: "grant:local-conflict".to_owned(),
principal: geth_types::PrincipalId::new("node:local-conflict"),
capabilities: vec![geth_types::Capability::new("ssh_proxy.connect")],
},
};
let remote_auth_op = geth_auth::AuthOp {
id: auth_op_id.clone(),
resource: geth_types::ResourceId::new("resource:ssh-proxy:local"),
created_at: geth_types::UnixMillis(33),
kind: geth_auth::AuthOpKind::GrantCreate {
grant_id: "grant:remote-conflict".to_owned(),
principal: geth_types::PrincipalId::new("node:remote-conflict"),
capabilities: vec![geth_types::Capability::new("ssh_proxy.admin_shell")],
},
};
let left_paths = geth_config::GethPaths::from_home(left_home.path());
let right_paths = geth_config::GethPaths::from_home(right_home.path());
let left_store = geth_store::Store::open(&left_paths.metadata_db()).expect("open left store");
let right_store =
geth_store::Store::open(&right_paths.metadata_db()).expect("open right store");
left_store
.insert_keychain_op(&geth_store::StoredKeychainOp {
op_id: local_keychain_op.id.to_string(),
op_json: serde_json::to_string(&local_keychain_op).expect("encode local keychain op"),
created_at_ms: local_keychain_op.created_at.0,
})
.expect("insert local keychain op");
left_store
.insert_auth_op(&geth_store::StoredAuthOp {
op_id: local_auth_op.id.to_string(),
resource_id: local_auth_op.resource.to_string(),
op_json: serde_json::to_string(&local_auth_op).expect("encode local auth op"),
created_at_ms: local_auth_op.created_at.0,
})
.expect("insert local auth op");
right_store
.insert_keychain_op(&geth_store::StoredKeychainOp {
op_id: remote_keychain_op.id.to_string(),
op_json: serde_json::to_string(&remote_keychain_op).expect("encode remote keychain op"),
created_at_ms: remote_keychain_op.created_at.0,
})
.expect("insert remote keychain op");
let keychain_signature = sign_payload_with_ssh_key(
right_home.path(),
"conflicting-keychain-op",
&admin_key_path,
geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE,
&geth_keychain::keychain_signing_payload(&remote_keychain_op)
.expect("keychain signing payload"),
);
right_store
.insert_keychain_signature(&geth_store::StoredKeychainSignature {
op_id: remote_keychain_op.id.to_string(),
signer: signer.to_string(),
signer_public_key: admin_public_key.clone(),
namespace: geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE.to_owned(),
signature: keychain_signature,
created_at_ms: 34,
})
.expect("insert remote keychain signature");
right_store
.insert_auth_op(&geth_store::StoredAuthOp {
op_id: remote_auth_op.id.to_string(),
resource_id: remote_auth_op.resource.to_string(),
op_json: serde_json::to_string(&remote_auth_op).expect("encode remote auth op"),
created_at_ms: remote_auth_op.created_at.0,
})
.expect("insert remote auth op");
let auth_signature = sign_payload_with_ssh_key(
right_home.path(),
"conflicting-auth-op",
&admin_key_path,
geth_auth::AUTH_SIGNATURE_NAMESPACE,
&geth_auth::auth_signing_payload(&remote_auth_op).expect("auth signing payload"),
);
right_store
.insert_auth_signature(&geth_store::StoredAuthSignature {
op_id: remote_auth_op.id.to_string(),
signer: signer.to_string(),
signer_public_key: admin_public_key.clone(),
namespace: geth_auth::AUTH_SIGNATURE_NAMESPACE.to_owned(),
signature: auth_signature,
created_at_ms: 35,
})
.expect("insert remote auth signature");
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_store = geth_store::Store::open(&left_paths.metadata_db()).expect("open left store");
let stored_keychain_op = left_store
.list_keychain_ops()
.expect("list left keychain ops")
.into_iter()
.find(|op| op.op_id == keychain_op_id.as_str())
.expect("local keychain op remains");
assert_eq!(
stored_keychain_op.op_json,
serde_json::to_string(&local_keychain_op).expect("encode expected local keychain op")
);
let stored_auth_op = left_store
.list_auth_ops()
.expect("list left auth ops")
.into_iter()
.find(|op| op.op_id == auth_op_id.as_str())
.expect("local auth op remains");
assert_eq!(
stored_auth_op.op_json,
serde_json::to_string(&local_auth_op).expect("encode expected local auth op")
);
}
#[test]
fn peer_card_export_import_and_list_are_candidate_only() {
let source_home = tempfile::tempdir().expect("source tempdir");