#![cfg(feature = "net")]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use net::adapter::net::identity::EntityKeypair;
use net::adapter::net::subnet::{
admission::unix_now_secs, SubnetAuthError, SubnetAuthPresentation, SubnetAuthorityConfig,
SubnetCredentialSet, SubnetGrant, SubnetRef, SubnetRevocationFloor, SubnetRights,
TopologySubnetId,
};
use net::adapter::net::{MeshNode, MeshNodeConfig, SocketBufferConfig};
const PSK: [u8; 32] = [0x73u8; 32];
const DAY: u64 = 24 * 60 * 60;
const SCOPE: &[u8] = &[3, 7];
fn authority_root() -> EntityKeypair {
EntityKeypair::from_bytes([0xA1; 32])
}
fn base_config() -> MeshNodeConfig {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let mut cfg = MeshNodeConfig::new(addr, PSK)
.with_heartbeat_interval(Duration::from_millis(200))
.with_session_timeout(Duration::from_secs(5))
.with_handshake(3, Duration::from_secs(2));
cfg.socket_buffers = SocketBufferConfig {
send_buffer_size: 256 * 1024,
recv_buffer_size: 256 * 1024,
};
cfg
}
struct Fixture {
verifier: Arc<MeshNode>,
subject: Arc<MeshNode>,
subject_kp: EntityKeypair,
root: EntityKeypair,
}
async fn fixture() -> Fixture {
let root = authority_root();
let cfg = base_config().with_subnet_authority(SubnetAuthorityConfig {
authority: root.entity_id().clone(),
roots: vec![root.entity_id().clone()],
maximum_grant_lifetime_secs: 7 * DAY,
});
let verifier = Arc::new(
MeshNode::new(EntityKeypair::generate(), cfg)
.await
.expect("verifier"),
);
let subject_kp = EntityKeypair::generate();
let subject = Arc::new(
MeshNode::new(subject_kp.clone(), base_config())
.await
.expect("subject"),
);
handshake(&subject, &verifier).await;
Fixture {
verifier,
subject,
subject_kp,
root,
}
}
async fn handshake(a: &Arc<MeshNode>, b: &Arc<MeshNode>) {
let a_id = a.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b.node_id())
.await
.expect("connect");
accept.await.expect("accept task").expect("accept");
a.start();
b.start();
}
fn grant_for(
root: &EntityKeypair,
subject: &EntityKeypair,
scope: &[u8],
rights: SubnetRights,
duration: u64,
) -> SubnetCredentialSet {
SubnetCredentialSet::Direct(
SubnetGrant::try_issue(
root,
root.entity_id().clone(),
TopologySubnetId::new(scope),
0,
subject.entity_id().clone(),
rights,
1,
unix_now_secs() - 60,
duration,
)
.expect("issue grant"),
)
}
#[allow(clippy::too_many_arguments)]
fn present(
signer: &EntityKeypair,
set: &SubnetCredentialSet,
session_id: u64,
verifier: &Arc<MeshNode>,
nonce: [u8; 32],
root: &EntityKeypair,
target: &[u8],
rights: SubnetRights,
) -> SubnetAuthPresentation {
SubnetAuthPresentation::try_issue(
signer,
set.credential_set_hash(),
session_id,
verifier.entity_id().clone(),
nonce,
SubnetRef {
authority: root.entity_id().clone(),
path: TopologySubnetId::new(target),
},
rights,
)
.expect("issue presentation")
}
fn session_id_of(verifier: &Arc<MeshNode>, node: u64) -> u64 {
verifier
.peer_session_id(node)
.expect("peer session must exist")
}
#[tokio::test]
async fn valid_presentation_compiles_a_context() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("issue challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
&[3, 7, 2],
SubnetRights::ATTACH,
);
let ctx = f
.verifier
.admit_subnet_session(sub_id, &p, &set)
.expect("admission must succeed");
assert_eq!(ctx.scope, TopologySubnetId::new(SCOPE));
assert_eq!(ctx.rights, SubnetRights::ATTACH);
assert_eq!(&ctx.subject, f.subject_kp.entity_id());
assert_eq!(ctx.subject_node, sub_id);
assert_eq!(ctx.session_id, sid);
let live = f
.verifier
.subnet_context_for(sub_id)
.expect("context must be live");
assert!(live.allows(
0,
0,
unix_now_secs(),
TopologySubnetId::new(&[3, 7, 2]),
SubnetRights::ATTACH
));
assert!(!live.allows(
0,
0,
unix_now_secs(),
TopologySubnetId::new(&[4]),
SubnetRights::ATTACH
));
assert!(!live.allows(
0,
0,
unix_now_secs(),
TopologySubnetId::new(&[3, 7]),
SubnetRights::ROUTE
));
}
#[tokio::test]
async fn parent_grant_attached_at_a_child_records_both_points() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, &[3], SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
&[3, 7, 2],
SubnetRights::ATTACH,
);
let ctx = f
.verifier
.admit_subnet_session(sub_id, &p, &set)
.expect("a parent grant admits a child attachment");
assert_eq!(
ctx.attachment,
TopologySubnetId::new(&[3, 7, 2]),
"attachment is the exact presented target",
);
assert_eq!(
ctx.scope,
TopologySubnetId::new(&[3]),
"scope remains the credential's broader ceiling",
);
assert_ne!(
ctx.attachment, ctx.scope,
"the two must stay distinguishable — forwarding reads attachment",
);
}
#[tokio::test]
async fn session_without_admission_has_no_context() {
let f = fixture().await;
assert!(
f.verifier.subnet_context_for(f.subject.node_id()).is_none(),
"a handshaken peer holds no subnet authority until it proves a grant",
);
}
#[tokio::test]
async fn presentation_signed_by_another_entity_is_refused() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let impostor = EntityKeypair::generate();
let p = present(
&impostor,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongSubject
);
assert!(f.verifier.subnet_context_for(sub_id).is_none());
}
#[tokio::test]
async fn wrong_or_replayed_challenge_is_refused() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
[0x11; 32],
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongChallenge
);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.expect("first use succeeds");
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongChallenge,
"a consumed challenge cannot be replayed",
);
}
#[tokio::test]
async fn a_rejected_attempt_still_consumes_the_challenge() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let over = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::EXPORT,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &over, &set)
.unwrap_err(),
SubnetAuthError::RightNotGranted
);
let ok = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &ok, &set)
.unwrap_err(),
SubnetAuthError::WrongChallenge
);
}
#[tokio::test]
async fn wrong_session_verifier_credentials_or_target_are_refused() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let sid = session_id_of(&f.verifier, sub_id);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let p = present(
&f.subject_kp,
&set,
sid ^ 0xFFFF,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongSession
);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let elsewhere = EntityKeypair::generate();
let p = SubnetAuthPresentation::try_issue(
&f.subject_kp,
set.credential_set_hash(),
sid,
elsewhere.entity_id().clone(),
nonce,
SubnetRef {
authority: f.root.entity_id().clone(),
path: TopologySubnetId::new(SCOPE),
},
SubnetRights::ATTACH,
)
.expect("issue");
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongVerifier
);
let other_set = grant_for(&f.root, &f.subject_kp, &[3, 8], SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let p = present(
&f.subject_kp,
&other_set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::WrongChallenge
);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
&[4],
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::ScopeNotAncestor
);
}
#[tokio::test]
async fn unanchored_authority_fails_closed() {
let root = authority_root();
let verifier = Arc::new(
MeshNode::new(EntityKeypair::generate(), base_config())
.await
.expect("verifier"),
);
let subject_kp = EntityKeypair::generate();
let subject = Arc::new(
MeshNode::new(subject_kp.clone(), base_config())
.await
.expect("subject"),
);
handshake(&subject, &verifier).await;
let sub_id = subject.node_id();
let set = grant_for(&root, &subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let nonce = verifier.issue_subnet_challenge(sub_id).expect("challenge");
let sid = session_id_of(&verifier, sub_id);
let p = present(
&subject_kp,
&set,
sid,
&verifier,
nonce,
&root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
verifier.admit_subnet_session(sub_id, &p, &set).unwrap_err(),
SubnetAuthError::UnknownAuthority
);
}
#[tokio::test]
async fn expiry_epoch_moves_and_withdrawal_invalidate_the_context() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let admit = |set: &SubnetCredentialSet| {
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
f.verifier.admit_subnet_session(sub_id, &p, set)
};
admit(&set).expect("admit");
assert!(f.verifier.subnet_context_for(sub_id).is_some());
f.verifier.withdraw_subnet_admission(sub_id);
assert!(f.verifier.subnet_context_for(sub_id).is_none());
admit(&set).expect("re-admit");
assert!(f.verifier.subnet_context_for(sub_id).is_some());
f.verifier.advance_subnet_topology_epoch();
assert!(
f.verifier.subnet_context_for(sub_id).is_none(),
"reparenting invalidates old ancestry authority",
);
assert_eq!(
admit(&set).unwrap_err(),
SubnetAuthError::WrongTopologyEpoch
);
}
#[tokio::test]
async fn accepted_floor_invalidates_stale_contexts() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let set = grant_for(&f.root, &f.subject_kp, SCOPE, SubnetRights::ATTACH, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
let ctx = f
.verifier
.admit_subnet_session(sub_id, &p, &set)
.expect("admit");
assert_eq!(ctx.subnet_auth_epoch, 0);
assert!(f.verifier.subnet_context_for(sub_id).is_some());
let floor = SubnetRevocationFloor::try_issue(
&f.root,
SubnetRef {
authority: f.root.entity_id().clone(),
path: TopologySubnetId::new(SCOPE),
},
0,
5,
1,
unix_now_secs(),
)
.expect("issue floor");
assert!(f.verifier.apply_subnet_floor(&floor).expect("apply floor"));
assert!(
f.verifier.subnet_context_for(sub_id).is_none(),
"the epoch move must invalidate the compiled context",
);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &set)
.unwrap_err(),
SubnetAuthError::Revoked
);
}
#[tokio::test]
async fn expired_grant_is_refused() {
let f = fixture().await;
let sub_id = f.subject.node_id();
let expired = SubnetCredentialSet::Direct(
SubnetGrant::try_issue(
&f.root,
f.root.entity_id().clone(),
TopologySubnetId::new(SCOPE),
0,
f.subject_kp.entity_id().clone(),
SubnetRights::ATTACH,
1,
unix_now_secs() - 600,
30,
)
.expect("issue"),
);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&expired,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
SubnetRights::ATTACH,
);
assert_eq!(
f.verifier
.admit_subnet_session(sub_id, &p, &expired)
.unwrap_err(),
SubnetAuthError::Expired
);
}
#[tokio::test]
async fn admitting_a_peer_confers_no_gateway_authority() {
let f = fixture().await;
let sub_id = f.subject.node_id();
assert!(
f.verifier.subnet_gateway_contexts().is_none(),
"precondition: the verifier holds no gateway authority",
);
let rights = SubnetRights::ATTACH.union(SubnetRights::ROUTE);
let set = grant_for(&f.root, &f.subject_kp, SCOPE, rights, DAY);
let nonce = f
.verifier
.issue_subnet_challenge(sub_id)
.expect("challenge");
let sid = session_id_of(&f.verifier, sub_id);
let p = present(
&f.subject_kp,
&set,
sid,
&f.verifier,
nonce,
&f.root,
SCOPE,
rights,
);
let ctx = f
.verifier
.admit_subnet_session(sub_id, &p, &set)
.expect("admit");
assert!(ctx.rights.contains(SubnetRights::ROUTE));
assert!(
f.verifier.subnet_gateway_contexts().is_none(),
"a peer's proven ROUTE must never become the verifier's forwarding authority",
);
assert_eq!(
f.verifier
.install_subnet_gateway_credentials(&[set])
.unwrap_err(),
SubnetAuthError::WrongSubject,
);
}