test: cover adversarial auth diagnostics

This commit is contained in:
Eric Wendland 2026-07-05 23:12:01 +02:00
commit f06db17066
3 changed files with 120 additions and 19 deletions

View file

@ -4328,32 +4328,49 @@ fn explain_auth_for_operator(
}
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 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) {
(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");
if !explanation
.reason
.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!(
"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
);
}
}
(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");
if !explanation.allowed {
explanation.reason = format!(
"subject is not present in the keychain and has no discovered peer card; {}",
explanation.reason
);
let subject_state = if revoked_node {
"subject was revoked from the active keychain"
} else {
"subject is not present in the keychain and has no discovered peer card"
};
explanation.reason = format!("{subject_state}; {}", explanation.reason);
}
}
(Some(node), None) => {
@ -4406,6 +4423,18 @@ fn explain_auth_for_operator(
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(
bearer_secret: Option<String>,
resource: &str,

View file

@ -2616,9 +2616,45 @@ fn auth_explain_distinguishes_endpoint_binding_state() {
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 {
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 {
node: "node:trusted".into(),
endpoint: "endpoint:card-only".to_owned(),
@ -2651,6 +2687,42 @@ fn auth_explain_distinguishes_endpoint_binding_state() {
}
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]