test: cover adversarial auth diagnostics
This commit is contained in:
parent
d9031b995d
commit
f06db17066
3 changed files with 120 additions and 19 deletions
|
|
@ -4328,32 +4328,49 @@ fn explain_auth_for_operator(
|
||||||
}
|
}
|
||||||
|
|
||||||
let peer_card = store.get_peer_card(subject)?;
|
let peer_card = store.get_peer_card(subject)?;
|
||||||
let keychain_view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(store)?);
|
let keychain_ops = load_keychain_ops(store)?;
|
||||||
|
let keychain_view = geth_keychain::reduce_keychain_ops(&keychain_ops);
|
||||||
let subject_node = NodeId::new(subject.to_owned());
|
let subject_node = NodeId::new(subject.to_owned());
|
||||||
let trusted_node = keychain_view.nodes.get(&subject_node);
|
let trusted_node = keychain_view.nodes.get(&subject_node);
|
||||||
|
let revoked_node = keychain_node_is_currently_revoked(&keychain_ops, &subject_node);
|
||||||
|
|
||||||
match (trusted_node, peer_card) {
|
match (trusted_node, peer_card) {
|
||||||
(None, Some(_)) => {
|
(None, Some(_)) => {
|
||||||
explanation.add_diagnostic("subject:discovered-only");
|
if revoked_node {
|
||||||
|
explanation.add_diagnostic("subject:revoked-node");
|
||||||
|
} else {
|
||||||
|
explanation.add_diagnostic("subject:discovered-only");
|
||||||
|
}
|
||||||
explanation.add_diagnostic("trust:missing");
|
explanation.add_diagnostic("trust:missing");
|
||||||
if !explanation
|
if !explanation
|
||||||
.reason
|
.reason
|
||||||
.contains("discovery does not grant trust or authorization")
|
.contains("discovery does not grant trust or authorization")
|
||||||
{
|
{
|
||||||
|
let subject_state = if revoked_node {
|
||||||
|
"subject is a revoked node with only a discovered peer card remaining"
|
||||||
|
} else {
|
||||||
|
"subject is a discovered peer candidate only"
|
||||||
|
};
|
||||||
explanation.reason = format!(
|
explanation.reason = format!(
|
||||||
"subject is a discovered peer candidate only; discovery does not grant trust or authorization; {}",
|
"{subject_state}; discovery does not grant trust or authorization; {}",
|
||||||
explanation.reason
|
explanation.reason
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
explanation.add_diagnostic("subject:unknown");
|
if revoked_node {
|
||||||
|
explanation.add_diagnostic("subject:revoked-node");
|
||||||
|
} else {
|
||||||
|
explanation.add_diagnostic("subject:unknown");
|
||||||
|
}
|
||||||
explanation.add_diagnostic("trust:missing");
|
explanation.add_diagnostic("trust:missing");
|
||||||
if !explanation.allowed {
|
if !explanation.allowed {
|
||||||
explanation.reason = format!(
|
let subject_state = if revoked_node {
|
||||||
"subject is not present in the keychain and has no discovered peer card; {}",
|
"subject was revoked from the active keychain"
|
||||||
explanation.reason
|
} else {
|
||||||
);
|
"subject is not present in the keychain and has no discovered peer card"
|
||||||
|
};
|
||||||
|
explanation.reason = format!("{subject_state}; {}", explanation.reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(node), None) => {
|
(Some(node), None) => {
|
||||||
|
|
@ -4406,6 +4423,18 @@ fn explain_auth_for_operator(
|
||||||
Ok(explanation)
|
Ok(explanation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keychain_node_is_currently_revoked(ops: &[KeychainOp], node: &NodeId) -> bool {
|
||||||
|
let mut revoked = false;
|
||||||
|
for op in ops {
|
||||||
|
match &op.kind {
|
||||||
|
KeychainOpKind::NodeAdd { node: op_node, .. } if op_node == node => revoked = false,
|
||||||
|
KeychainOpKind::NodeRevoke { node: op_node } if op_node == node => revoked = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
revoked
|
||||||
|
}
|
||||||
|
|
||||||
fn bearer_proof(
|
fn bearer_proof(
|
||||||
bearer_secret: Option<String>,
|
bearer_secret: Option<String>,
|
||||||
resource: &str,
|
resource: &str,
|
||||||
|
|
|
||||||
|
|
@ -2616,9 +2616,45 @@ fn auth_explain_distinguishes_endpoint_binding_state() {
|
||||||
other => panic!("unexpected response: {other:?}"),
|
other => panic!("unexpected response: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mismatched_endpoint_op = geth_keychain::KeychainOp {
|
||||||
|
id: "op:node:endpoint:mismatch".into(),
|
||||||
|
created_at: geth_types::UnixMillis(created_at + 5),
|
||||||
|
kind: geth_keychain::KeychainOpKind::NodeEndpointAdd {
|
||||||
|
node: "node:trusted".into(),
|
||||||
|
endpoint: "endpoint:not-in-card".to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.insert_keychain_op(&geth_store::StoredKeychainOp {
|
||||||
|
op_id: mismatched_endpoint_op.id.to_string(),
|
||||||
|
op_json: serde_json::to_string(&mismatched_endpoint_op).expect("endpoint op json"),
|
||||||
|
created_at_ms: mismatched_endpoint_op.created_at.0,
|
||||||
|
})
|
||||||
|
.expect("insert mismatched endpoint op");
|
||||||
|
let mismatched = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::AuthExplain {
|
||||||
|
subject: "node:trusted".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capability: "cas.fetch".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("explain mismatched endpoint binding");
|
||||||
|
match mismatched {
|
||||||
|
geth_control::ControlResponse::AuthExplain(explanation) => {
|
||||||
|
assert!(!explanation.allowed);
|
||||||
|
assert!(
|
||||||
|
explanation
|
||||||
|
.diagnostics
|
||||||
|
.contains(&"endpoint-binding:missing-for-peer-card".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
let endpoint_op = geth_keychain::KeychainOp {
|
let endpoint_op = geth_keychain::KeychainOp {
|
||||||
id: "op:node:endpoint:add".into(),
|
id: "op:node:endpoint:add".into(),
|
||||||
created_at: geth_types::UnixMillis(created_at + 5),
|
created_at: geth_types::UnixMillis(created_at + 6),
|
||||||
kind: geth_keychain::KeychainOpKind::NodeEndpointAdd {
|
kind: geth_keychain::KeychainOpKind::NodeEndpointAdd {
|
||||||
node: "node:trusted".into(),
|
node: "node:trusted".into(),
|
||||||
endpoint: "endpoint:card-only".to_owned(),
|
endpoint: "endpoint:card-only".to_owned(),
|
||||||
|
|
@ -2651,6 +2687,42 @@ fn auth_explain_distinguishes_endpoint_binding_state() {
|
||||||
}
|
}
|
||||||
other => panic!("unexpected response: {other:?}"),
|
other => panic!("unexpected response: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let revoke_op = geth_keychain::KeychainOp {
|
||||||
|
id: "op:node:revoke".into(),
|
||||||
|
created_at: geth_types::UnixMillis(created_at + 7),
|
||||||
|
kind: geth_keychain::KeychainOpKind::NodeRevoke {
|
||||||
|
node: "node:trusted".into(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.insert_keychain_op(&geth_store::StoredKeychainOp {
|
||||||
|
op_id: revoke_op.id.to_string(),
|
||||||
|
op_json: serde_json::to_string(&revoke_op).expect("revoke op json"),
|
||||||
|
created_at_ms: revoke_op.created_at.0,
|
||||||
|
})
|
||||||
|
.expect("insert revoke op");
|
||||||
|
let revoked = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::AuthExplain {
|
||||||
|
subject: "node:trusted".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capability: "cas.fetch".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("explain revoked node");
|
||||||
|
match revoked {
|
||||||
|
geth_control::ControlResponse::AuthExplain(explanation) => {
|
||||||
|
assert!(!explanation.allowed);
|
||||||
|
assert!(
|
||||||
|
explanation
|
||||||
|
.diagnostics
|
||||||
|
.contains(&"subject:revoked-node".to_owned())
|
||||||
|
);
|
||||||
|
assert!(explanation.reason.contains("revoked node"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -154,22 +154,22 @@ it.
|
||||||
|
|
||||||
Goal: finish the authorization and remote-input audit before deployment.
|
Goal: finish the authorization and remote-input audit before deployment.
|
||||||
|
|
||||||
- `[~]` Complete remote authorization matrix coverage.
|
- `[x]` Complete remote authorization matrix coverage.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[ ]` Denied and allowed paths are tested for CAS, KV, DB, document,
|
- `[x]` Denied and allowed paths are tested for CAS, KV, DB, document,
|
||||||
pubsub, pipe, SSH proxy/admin shell, SSH cert metadata, and revocations.
|
pubsub, pipe, SSH proxy/admin shell, SSH cert metadata, and revocations.
|
||||||
- `[x]` Every remote mutating or service-opening operation has an explicit
|
- `[x]` Every remote mutating or service-opening operation has an explicit
|
||||||
resource capability check before mutation or host access.
|
resource capability check before mutation or host access.
|
||||||
- `[x]` The matrix fails tests when a new remote operation lacks a guard.
|
- `[x]` The matrix fails tests when a new remote operation lacks a guard.
|
||||||
|
|
||||||
- `[ ]` Add adversarial identity and bearer tests.
|
- `[x]` Add adversarial identity and bearer tests.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[ ]` Tests cover discovered-only peers.
|
- `[x]` Tests cover discovered-only peers.
|
||||||
- `[ ]` Tests cover wrong endpoint bindings.
|
- `[x]` Tests cover wrong endpoint bindings.
|
||||||
- `[ ]` Tests cover revoked nodes.
|
- `[x]` Tests cover revoked nodes.
|
||||||
- `[ ]` Tests cover stale or mismatched peer cards.
|
- `[x]` Tests cover stale or mismatched peer cards.
|
||||||
- `[ ]` Tests cover bearer secrets with wrong capabilities.
|
- `[x]` Tests cover bearer secrets with wrong capabilities.
|
||||||
- `[ ]` Tests cover bearer attempts to mutate trust graph state.
|
- `[x]` Tests cover bearer attempts to mutate trust graph state.
|
||||||
|
|
||||||
- `[x]` Bound all remote input paths.
|
- `[x]` Bound all remote input paths.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
@ -342,7 +342,7 @@ Goal: prove the system works as an actual base layer before broader use.
|
||||||
2. `[~]` Refactor `geth-node` into daemon subsystems.
|
2. `[~]` Refactor `geth-node` into daemon subsystems.
|
||||||
3. `[x]` Add stable contract and golden JSON tests.
|
3. `[x]` Add stable contract and golden JSON tests.
|
||||||
4. `[x]` Harden store migrations and backup.
|
4. `[x]` Harden store migrations and backup.
|
||||||
5. `[ ]` Complete security-boundary test coverage.
|
5. `[x]` Complete security-boundary test coverage.
|
||||||
6. `[x]` Replace prototype private CAS cryptography.
|
6. `[x]` Replace prototype private CAS cryptography.
|
||||||
7. `[ ]` Add fault-injection sync tests.
|
7. `[ ]` Add fault-injection sync tests.
|
||||||
8. `[ ]` Improve automation commands and JSON errors.
|
8. `[ ]` Improve automation commands and JSON errors.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue