use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::pin::pin;
use std::time::Duration;
use slither::config::Config;
use slither::error::ConnectError;
use slither::identity::{Identity, PublicKeyOf};
use slither::testutil::{CountingIdentity, DhCounter, Network, Tap};
const GIVEUP: Duration = Duration::from_secs(90);
const RETRANSMIT_BASE: Duration = Duration::from_secs(5);
const SHELL_LATENESS_BOUND: Duration = Duration::from_millis(250);
const INIT_PACKET_LEN: usize = 196;
const PKT_HANDSHAKE_INIT: u8 = 0x01;
type Suite = slither::packet::ReferenceSuite;
type Id = CountingIdentity<Suite>;
type Pk = PublicKeyOf<Id>;
type Endpoint = slither::shell::Endpoint<Id>;
type Connection = slither::shell::Connection<Suite>;
fn addr(port: u16) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port)
}
struct Node {
ep: Endpoint,
dhs: DhCounter,
pk: Pk,
addr: SocketAddr,
}
impl Node {
fn spawn(net: &Network, key_seed: u8, port: u16) -> Node {
let a = addr(port);
let id: Id = CountingIdentity::seeded([key_seed; 32]);
let dhs = id.counter();
let pk = *id.public_static();
let ep = Endpoint::builder()
.identity(id)
.wire(net.wire(a))
.config(Config::new())
.build();
Node {
ep,
dhs,
pk,
addr: a,
}
}
fn ep(&self) -> &Endpoint {
&self.ep
}
}
fn other_static(key_seed: u8) -> Pk {
let id: Id = CountingIdentity::seeded([key_seed; 32]);
*id.public_static()
}
fn msg1_to(tap: &Tap, to: SocketAddr) -> usize {
tap.datagrams()
.iter()
.filter(|(_from, dst, bytes)| {
*dst == to && bytes.len() == INIT_PACKET_LEN && bytes[0] == PKT_HANDSHAKE_INIT
})
.count()
}
fn msg1_from(tap: &Tap, from: SocketAddr) -> usize {
tap.datagrams()
.iter()
.filter(|(src, _dst, bytes)| {
*src == from && bytes.len() == INIT_PACKET_LEN && bytes[0] == PKT_HANDSHAKE_INIT
})
.count()
}
fn sent_count(tap: &Tap, from: SocketAddr) -> usize {
tap.datagrams()
.iter()
.filter(|(src, _dst, _bytes)| *src == from)
.count()
}
async fn establish(dialler: &Node, listener: &Node) -> (Connection, Connection) {
let dial = dialler
.ep()
.connect(listener.addr, listener.pk)
.expect("connect() on a NONE static must be Ok");
let accept = async {
let intro = listener
.ep()
.accept()
.await
.expect("§16.2: accept() yields None only when the endpoint is closed");
let claimed = intro
.read_identity()
.await
.expect("§6.1: read_identity() on a genuine msg1 must not be Malformed");
let proven = claimed.authenticate().await.expect(
"§6.1/§17.1: authenticate() must succeed. An `IntroError::Replay` HERE is the \
failure this test exists to catch: it would mean the mac1-invalid initiations \
dropped earlier had written the §17.1 timestamp guard, which §6.1 places two \
stages further down the ladder (`authenticate() -> Proven`) and which a packet \
that dies at the mac1 gate can never reach",
);
proven.accept().await.expect("accept")
};
let (initiator, responder) = tokio::join!(dial, accept);
(
initiator.expect("Connecting resolved with an error"),
responder,
)
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn s22_a_wrong_static_against_a_live_responder_installs_nothing() {
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let net = Network::new();
let tap = net.tap();
let a = Node::spawn(&net, 1, 7001);
let b = Node::spawn(&net, 2, 7002);
let wrong = other_static(3);
assert_ne!(
wrong.as_ref(),
b.pk.as_ref(),
"fixture: the dialled static must NOT be the responder's, or this test \
asserts nothing at all"
);
let dial = a.ep().connect(b.addr, wrong).expect(
"connect() on a NONE static must be Ok — the fault is the peer's key, and \
§16.1 knows nothing about it yet",
);
let mut dial = pin!(dial);
let mut accept = pin!(b.ep().accept());
let early = tokio::select! {
r = tokio::time::timeout(GIVEUP - Duration::from_millis(1), dial.as_mut()) => r,
_ = accept.as_mut() => panic!(
"§4.2/§6.1: a mac1-invalid initiation minted an `Intro`. mac1 keys on the \
RECIPIENT's static (§4.1) and a bad mac1 is a silent drop BEFORE the \
stage-0 queue"
),
};
assert!(
early.is_err(),
"§5.5 step 6: `Connecting` resolved BEFORE HANDSHAKE_GIVEUP (90 s)"
);
let late = tokio::select! {
r = tokio::time::timeout(
Duration::from_millis(1) + SHELL_LATENESS_BOUND,
dial.as_mut(),
) => r,
_ = accept.as_mut() => panic!(
"§4.2/§6.1: a mac1-invalid initiation minted an `Intro` late in the ladder \
— the gate held for the first packets and not for the rest"
),
};
let outcome = late
.expect("`Connecting` had not resolved by HANDSHAKE_GIVEUP + SHELL_LATENESS_BOUND");
assert!(
matches!(outcome, Err(ConnectError::TimedOut)),
"§5.5 step 6 / §18.1: the give-up is `ConnectError::TimedOut` — and there is \
no wrong-static variant to report instead (ruling 72 fixes ConnectError at \
three). Got {outcome:?}",
);
let n = msg1_to(&tap, b.addr);
assert!(
(15..=20).contains(&n),
"§5.5 step 2: a fixed {RETRANSMIT_BASE:?} + jitter train puts 15-20 \
initiations on the wire across HANDSHAKE_GIVEUP; the responder must have \
dropped ALL of them, not one. Observed {n}"
);
assert_eq!(
sent_count(&tap, b.addr),
0,
"§4.2/§6.1: a bad mac1 is a SILENT drop. The responder transmitted {} \
datagram(s) in reply to {n} unverifiable initiations — §6.9's amplification \
accounting rests on that being zero",
sent_count(&tap, b.addr)
);
assert_eq!(
b.dhs.get(),
0,
"§4.2: mac1 is verified BEFORE any curve or DH work — a wrong-key packet \
'dies at one keyed hash and never reaches the DH provider'"
);
let probe = b.ep().connect(a.addr, a.pk);
assert!(
probe.is_ok(),
"§5.4/§18.1: the responder installed something for the dialler's static on a \
handshake that never authenticated — got {:?}",
probe.err()
);
drop(probe);
})
.await;
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn s22_both_endpoints_stay_usable_after_a_wrong_static_dial() {
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let net = Network::new();
let tap = net.tap();
let a = Node::spawn(&net, 1, 7011);
let b = Node::spawn(&net, 2, 7012);
let wrong = other_static(3);
let dial = a.ep().connect(b.addr, wrong).expect("first dial");
assert!(
matches!(dial.await, Err(ConnectError::TimedOut)),
"§5.5 step 6: the wrong-static dial must give up with TimedOut"
);
let dropped = msg1_to(&tap, b.addr);
assert!(
dropped >= 15,
"fixture: the responder must have dropped a full ladder before the recovery \
is interesting; observed {dropped}"
);
assert_eq!(
sent_count(&tap, b.addr),
0,
"§4.2: the responder answered an unverifiable initiation"
);
let (ca, cb) = establish(&a, &b).await;
assert_eq!(
ca.remote_static().as_ref(),
b.pk.as_ref(),
"§16.2: remote_static() is the peer we dialled"
);
assert_eq!(
cb.remote_static().as_ref(),
a.pk.as_ref(),
"§16.2: the responder's peer is the dialler"
);
ca.send_message(b"after the wrong static")
.await
.expect("§11: the initiator must be able to send");
assert_eq!(
cb.recv_message()
.await
.expect("§11: the responder must be able to receive"),
b"after the wrong static".to_vec()
);
cb.send_message(b"and back again")
.await
.expect("§11: the responder must be able to send");
assert_eq!(
ca.recv_message()
.await
.expect("§11: the initiator must be able to receive"),
b"and back again".to_vec()
);
})
.await;
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn s22_a_wrong_static_costs_the_responder_no_dh_on_the_hinted_path() {
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let net = Network::new();
let tap = net.tap();
let a = Node::spawn(&net, 1, 7021);
let b = Node::spawn(&net, 2, 7022);
let b_dial = b
.ep()
.connect(a.addr, other_static(4))
.expect("B's own dial");
let a_dial = a
.ep()
.connect(b.addr, other_static(3))
.expect("A's wrong-static dial");
let mut b_accept = pin!(b.ep().accept());
let mut a_accept = pin!(a.ep().accept());
let (a_out, b_out) = tokio::select! {
pair = async { tokio::join!(a_dial, b_dial) } => pair,
_ = b_accept.as_mut() => panic!(
"§4.2/§6.5: a mac1-invalid initiation reached the responder's stage-0 \
queue on the HINTED path"
),
_ = a_accept.as_mut() => panic!(
"§4.2/§6.5: a mac1-invalid initiation reached the dialler's stage-0 \
queue on the HINTED path"
),
};
assert!(
matches!(a_out, Err(ConnectError::TimedOut)),
"§5.5 step 6: A's wrong-static dial must give up with TimedOut; got {a_out:?}"
);
assert!(
matches!(b_out, Err(ConnectError::TimedOut)),
"§5.5 step 6: B's wrong-static dial must give up with TimedOut; got {b_out:?}"
);
let b_sent = msg1_from(&tap, b.addr);
let a_sent = msg1_from(&tap, a.addr);
assert!(
(15..=20).contains(&b_sent) && (15..=20).contains(&a_sent),
"fixture: both ladders must have run (A {a_sent}, B {b_sent}); a DH equality \
over an empty train asserts nothing"
);
assert_eq!(
sent_count(&tap, b.addr),
b_sent,
"§4.2: every datagram B emitted must be one of its OWN initiations — a reply \
to A's unverifiable initiations is an amplification vector (§6.9)"
);
assert_eq!(
b.dhs.get() as usize,
2 * b_sent,
"§4.2/§6.1: B's DH spend must be exactly its own initiator ladder \
(es+ss = 2 DH per initiation, {b_sent} initiations = {}). A larger figure \
means A's {a_sent} mac1-invalid initiations reached the DH provider through \
§6.5 step 3's eager path",
2 * b_sent
);
assert_eq!(
a.dhs.get() as usize,
2 * a_sent,
"§4.2/§6.1: and symmetrically for A, whose address is likewise hinted at B"
);
})
.await;
}