huskies: merge 1206 story fleet_identity MCP tool: read sled pins vs live signed identity, and re-pin via TOFU
This commit is contained in:
@@ -83,6 +83,63 @@ pub fn check_identity(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fleet identity read report (story 1206) ────────────────────────────────
|
||||
|
||||
/// Per-sled identity report returned by the `fleet_identity` MCP tool's read
|
||||
/// mode: the recorded pin next to the live, cryptographically-verified
|
||||
/// identity.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
pub struct SledIdentityReport {
|
||||
/// The project/sled name.
|
||||
pub project: String,
|
||||
/// The sled's configured base URL, if any.
|
||||
pub url: Option<String>,
|
||||
/// Whether the `/identity` probe reached the sled at all.
|
||||
pub connected: bool,
|
||||
/// The pinned `expected_node_id` recorded in `projects.toml`, if any.
|
||||
pub expected_pin: Option<String>,
|
||||
/// The node ID from a signature-verified challenge-response, if the
|
||||
/// signature verified. Never populated from the unsigned `node_id`
|
||||
/// display field alone — an invalid or missing signature leaves this
|
||||
/// `None`.
|
||||
pub live_node_id: Option<String>,
|
||||
/// `true` only when the sled is connected, its signature verified, and
|
||||
/// the verified node ID equals `expected_pin`.
|
||||
pub matched: bool,
|
||||
}
|
||||
|
||||
/// Build a [`SledIdentityReport`] from the outcome of probing one sled.
|
||||
///
|
||||
/// `check` is `None` when the sled was unreachable (the probe never got a
|
||||
/// response). Otherwise it's the result of running [`check_identity`] against
|
||||
/// whatever response was received.
|
||||
pub fn build_identity_report(
|
||||
project: &str,
|
||||
url: Option<&str>,
|
||||
expected_node_id: Option<&str>,
|
||||
check: Option<IdentityCheck>,
|
||||
) -> SledIdentityReport {
|
||||
let connected = check.is_some();
|
||||
let (live_node_id, matched) = match &check {
|
||||
Some(IdentityCheck::Match) => (expected_node_id.map(str::to_string), true),
|
||||
Some(IdentityCheck::Mismatch { responder_node_id }) => {
|
||||
(Some(responder_node_id.clone()), false)
|
||||
}
|
||||
Some(IdentityCheck::FirstContact { node_id }) => (Some(node_id.clone()), false),
|
||||
Some(IdentityCheck::InvalidSignature) | Some(IdentityCheck::MissingSignature) | None => {
|
||||
(None, false)
|
||||
}
|
||||
};
|
||||
SledIdentityReport {
|
||||
project: project.to_string(),
|
||||
url: url.map(str::to_string),
|
||||
connected,
|
||||
expected_pin: expected_node_id.map(str::to_string),
|
||||
live_node_id,
|
||||
matched,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -172,4 +229,79 @@ mod tests {
|
||||
let resp: IdentityProbeResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.signature.is_none());
|
||||
}
|
||||
|
||||
// ── build_identity_report (story 1206) ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn report_matching_pin_is_connected_and_matched() {
|
||||
let report = build_identity_report(
|
||||
"myapp",
|
||||
Some("http://sled:3001"),
|
||||
Some("abc123"),
|
||||
Some(IdentityCheck::Match),
|
||||
);
|
||||
assert!(report.connected);
|
||||
assert!(report.matched);
|
||||
assert_eq!(report.expected_pin.as_deref(), Some("abc123"));
|
||||
assert_eq!(report.live_node_id.as_deref(), Some("abc123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_mismatch_names_the_responder_node_id_and_is_not_matched() {
|
||||
let report = build_identity_report(
|
||||
"myapp",
|
||||
Some("http://sled:3001"),
|
||||
Some("expected-id"),
|
||||
Some(IdentityCheck::Mismatch {
|
||||
responder_node_id: "different-id".to_string(),
|
||||
}),
|
||||
);
|
||||
assert!(report.connected);
|
||||
assert!(!report.matched);
|
||||
assert_eq!(report.expected_pin.as_deref(), Some("expected-id"));
|
||||
assert_eq!(report.live_node_id.as_deref(), Some("different-id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_no_pin_recorded_is_never_matched_even_on_first_contact() {
|
||||
let report = build_identity_report(
|
||||
"myapp",
|
||||
Some("http://sled:3001"),
|
||||
None,
|
||||
Some(IdentityCheck::FirstContact {
|
||||
node_id: "some-id".to_string(),
|
||||
}),
|
||||
);
|
||||
assert!(report.connected);
|
||||
assert!(report.expected_pin.is_none());
|
||||
assert_eq!(report.live_node_id.as_deref(), Some("some-id"));
|
||||
assert!(
|
||||
!report.matched,
|
||||
"no pin recorded yet means there is nothing to match against"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_unreachable_sled_is_not_connected() {
|
||||
let report = build_identity_report("myapp", Some("http://sled:3001"), Some("abc123"), None);
|
||||
assert!(!report.connected);
|
||||
assert!(!report.matched);
|
||||
assert!(report.live_node_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_invalid_signature_is_connected_but_no_live_node_id() {
|
||||
let report = build_identity_report(
|
||||
"myapp",
|
||||
Some("http://sled:3001"),
|
||||
Some("abc123"),
|
||||
Some(IdentityCheck::InvalidSignature),
|
||||
);
|
||||
assert!(report.connected, "the probe did reach the sled");
|
||||
assert!(!report.matched);
|
||||
assert!(
|
||||
report.live_node_id.is_none(),
|
||||
"an unverified signature must never populate live_node_id"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user