Test conflicting replicated ops are rejected
This commit is contained in:
parent
2a8a348b97
commit
6f2fb53132
2 changed files with 291 additions and 2 deletions
|
|
@ -54,6 +54,28 @@ fn generate_ssh_key(path: &std::path::Path) {
|
||||||
assert!(status.success());
|
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) {
|
fn wait_for_socket(path: &std::path::Path) {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
while started.elapsed() < Duration::from_secs(5) {
|
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]
|
#[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");
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Implementation order:
|
||||||
keychain/auth operations are rejected and not imported.
|
keychain/auth operations are rejected and not imported.
|
||||||
- `[x]` Add initial two-daemon tests proving invalidly signed replicated
|
- `[x]` Add initial two-daemon tests proving invalidly signed replicated
|
||||||
keychain/auth operations are rejected and not imported.
|
keychain/auth operations are rejected and not imported.
|
||||||
- `[ ]` Add tests proving conflicting replicated keychain/auth records do
|
- `[x]` Add tests proving conflicting replicated keychain/auth records do
|
||||||
not mutate trust/resource state.
|
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
|
||||||
|
|
@ -75,7 +75,7 @@ Implementation order:
|
||||||
local trust or resource state.
|
local trust or resource state.
|
||||||
- `[x]` Tests assert invalidly signed replicated keychain/auth records do not
|
- `[x]` Tests assert invalidly signed replicated keychain/auth records do not
|
||||||
mutate local trust or resource state.
|
mutate local trust or resource state.
|
||||||
- `[ ]` Tests assert conflicting replicated keychain/auth records do not
|
- `[x]` Tests assert conflicting replicated keychain/auth records do not
|
||||||
mutate local trust or resource state.
|
mutate local trust or resource state.
|
||||||
|
|
||||||
- `[~]` Remote authorization enforcement audit.
|
- `[~]` Remote authorization enforcement audit.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue