Skip to content

Commit 1430731

Browse files
authored
fix(node): route TeeAttestationAnnounce on ns/ topic into admission (#2441)
1 parent c7dc679 commit 1430731

3 files changed

Lines changed: 471 additions & 22 deletions

File tree

crates/node/src/handlers/network_event/specialized.rs

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ use crate::handlers::{specialized_node_invite, tee_attestation_admission};
88
use crate::run::NodeMode;
99
use crate::NodeManager;
1010

11+
/// Why a gossipsub topic was rejected as a `TeeAttestationAnnounce`
12+
/// namespace-governance topic.
13+
#[derive(Debug, PartialEq, Eq)]
14+
enum NamespaceTopicError {
15+
/// Topic did not carry the `ns/` namespace-governance prefix. Fleet
16+
/// TEE nodes publish on `ns/<hex(namespace_id)>` via
17+
/// `NodeClient::publish_on_namespace`, so anything else is not an
18+
/// admission announce.
19+
NotNamespaceTopic,
20+
/// Topic had the `ns/` prefix but the suffix was not a 32-byte hex id.
21+
MalformedHex,
22+
}
23+
24+
/// Parse a `TeeAttestationAnnounce` gossipsub topic into its namespace id.
25+
///
26+
/// Fleet TEE nodes announce on `ns/<hex(namespace_id)>` (the namespace
27+
/// governance topic — see `NodeClient::publish_on_namespace`,
28+
/// `governance_broadcast::ns_topic`, and the `ns/` handling in
29+
/// `subscriptions.rs`). The namespace IS its root group, so the returned
30+
/// 32-byte id is used directly as the admission group id.
31+
fn parse_namespace_announce_topic(topic_str: &str) -> Result<[u8; 32], NamespaceTopicError> {
32+
let hex = topic_str
33+
.strip_prefix("ns/")
34+
.ok_or(NamespaceTopicError::NotNamespaceTopic)?;
35+
let mut bytes = [0u8; 32];
36+
hex::decode_to_slice(hex, &mut bytes).map_err(|_| NamespaceTopicError::MalformedHex)?;
37+
Ok(bytes)
38+
}
39+
1140
pub(super) fn handle_specialized_broadcast(
1241
this: &mut NodeManager,
1342
ctx: &mut actix::Context<NodeManager>,
@@ -81,24 +110,26 @@ pub(super) fn handle_specialized_broadcast(
81110
node_type: _,
82111
} => {
83112
let topic_str = topic.as_str();
84-
let group_id_bytes = match topic_str.strip_prefix("group/") {
85-
Some(hex) => {
86-
let mut bytes = [0u8; 32];
87-
if hex::decode_to_slice(hex, &mut bytes).is_err() {
88-
warn!(
89-
%source,
90-
topic = %topic_str,
91-
"Invalid group topic hex in TeeAttestationAnnounce"
92-
);
93-
return true;
94-
}
95-
bytes
113+
// Fleet TEE nodes announce on the namespace governance topic
114+
// `ns/<hex(namespace_id)>` (see `NodeClient::publish_on_namespace`
115+
// and the `ns/` convention in `subscriptions.rs` /
116+
// `governance_broadcast::ns_topic`). The namespace IS its root
117+
// group, so the parsed namespace id is the admission group id.
118+
let namespace_id_bytes = match parse_namespace_announce_topic(topic_str) {
119+
Ok(bytes) => bytes,
120+
Err(NamespaceTopicError::MalformedHex) => {
121+
warn!(
122+
%source,
123+
topic = %topic_str,
124+
"Invalid namespace topic hex in TeeAttestationAnnounce"
125+
);
126+
return true;
96127
}
97-
None => {
128+
Err(NamespaceTopicError::NotNamespaceTopic) => {
98129
warn!(
99130
%source,
100131
topic = %topic_str,
101-
"TeeAttestationAnnounce received on non-group topic"
132+
"TeeAttestationAnnounce received on non-namespace topic"
102133
);
103134
return true;
104135
}
@@ -108,8 +139,8 @@ pub(super) fn handle_specialized_broadcast(
108139
%source,
109140
%public_key,
110141
nonce = %hex::encode(*nonce),
111-
group_id = %hex::encode(group_id_bytes),
112-
"Received TEE attestation announce on group topic"
142+
namespace_id = %hex::encode(namespace_id_bytes),
143+
"Received TEE attestation announce on namespace topic"
113144
);
114145

115146
let context_client = this.clients.context.clone();
@@ -124,7 +155,7 @@ pub(super) fn handle_specialized_broadcast(
124155
quote_bytes,
125156
public_key,
126157
nonce,
127-
group_id_bytes,
158+
namespace_id_bytes,
128159
)
129160
.await
130161
{
@@ -276,3 +307,69 @@ pub(super) fn handle_specialized_network_event(
276307
_ => false,
277308
}
278309
}
310+
311+
#[cfg(test)]
312+
mod tests {
313+
use super::{parse_namespace_announce_topic, NamespaceTopicError};
314+
315+
/// Regression test for the `ns/` vs `group/` topic mismatch (PR #2096):
316+
/// fleet TEE nodes announce `TeeAttestationAnnounce` on
317+
/// `ns/<hex(namespace_id)>`, but the dispatcher used to strip
318+
/// `group/`, so the announce fell into the "non-namespace topic" arm
319+
/// and was dropped — `handle_tee_attestation_announce` / `admit_tee_node`
320+
/// never ran, and fleet TEE nodes were never admitted to the namespace
321+
/// group. The dispatcher must resolve an `ns/` topic to its namespace id
322+
/// and route it into the admission path.
323+
#[test]
324+
fn ns_announce_topic_resolves_to_namespace_id_for_admission() {
325+
let namespace_id = [0x42u8; 32];
326+
let topic = format!("ns/{}", hex::encode(namespace_id));
327+
328+
let parsed = parse_namespace_announce_topic(&topic)
329+
.expect("ns/<hex> announce topic must route into the admission path, not be dropped");
330+
331+
// The resolved id is what gets handed to
332+
// `handle_tee_attestation_announce` → `admit_tee_node` as the
333+
// admission group id (the namespace is its own root group).
334+
assert_eq!(parsed, namespace_id);
335+
}
336+
337+
/// The old (buggy) `group/<hex>` topic must NOT match this path anymore.
338+
/// `group/` is not how TEE announces are published (publish uses
339+
/// `publish_on_namespace` → `ns/`), so a `group/` topic here is a
340+
/// non-namespace topic and is correctly rejected rather than admitted.
341+
#[test]
342+
fn legacy_group_topic_is_not_a_namespace_announce_topic() {
343+
let topic = format!("group/{}", hex::encode([0x42u8; 32]));
344+
assert_eq!(
345+
parse_namespace_announce_topic(&topic),
346+
Err(NamespaceTopicError::NotNamespaceTopic),
347+
);
348+
}
349+
350+
/// A non-prefixed topic (e.g. a raw context id) is not a namespace
351+
/// announce topic.
352+
#[test]
353+
fn unprefixed_topic_is_not_a_namespace_announce_topic() {
354+
assert_eq!(
355+
parse_namespace_announce_topic("some-context-id"),
356+
Err(NamespaceTopicError::NotNamespaceTopic),
357+
);
358+
}
359+
360+
/// An `ns/` topic with a malformed (non-hex / wrong-length) suffix is
361+
/// reported distinctly so the dispatcher can warn precisely instead of
362+
/// silently treating it as the wrong kind of topic.
363+
#[test]
364+
fn ns_topic_with_malformed_hex_is_rejected_as_malformed() {
365+
assert_eq!(
366+
parse_namespace_announce_topic("ns/not-hex"),
367+
Err(NamespaceTopicError::MalformedHex),
368+
);
369+
// Right prefix, valid hex, wrong length (16 bytes, not 32).
370+
assert_eq!(
371+
parse_namespace_announce_topic(&format!("ns/{}", hex::encode([0u8; 16]))),
372+
Err(NamespaceTopicError::MalformedHex),
373+
);
374+
}
375+
}

crates/node/src/handlers/tee_attestation_admission.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! TEE attestation-based admission handler.
22
//!
3-
//! When a fleet TEE node broadcasts `TeeAttestationAnnounce` on a group topic,
3+
//! When a fleet TEE node broadcasts `TeeAttestationAnnounce` on the namespace
4+
//! governance topic (`ns/<hex>`; the namespace is its own root group),
45
//! existing peers verify the TDX quote against the group's `TeeAdmissionPolicy`
56
//! and, if valid, admit the node via a `MemberJoinedViaTeeAttestation` governance op.
67
//!
@@ -13,7 +14,7 @@ use calimero_tee_attestation::{is_mock_quote, verify_attestation, verify_mock_at
1314
use sha2::{Digest, Sha256};
1415
use tracing::{info, warn};
1516

16-
/// Handle a `TeeAttestationAnnounce` broadcast on a group gossip topic.
17+
/// Handle a `TeeAttestationAnnounce` broadcast on a namespace gossip topic.
1718
///
1819
/// Verifies the TDX quote, checks measurements against the group's TEE admission
1920
/// policy, and publishes a `MemberJoinedViaTeeAttestation` governance op if valid.

0 commit comments

Comments
 (0)