273 lines
9.0 KiB
Rust
273 lines
9.0 KiB
Rust
// spec, раздел "Сетевой уровень → Обфускация транспорта → Identity-Bound Tunnel (IBT)"
|
||
//
|
||
// Online IBT proof:
|
||
// proof = ML-DSA-65_sign(client_sk, "mt-tunnel" || server_node_id || floor(W / 2))
|
||
//
|
||
// Mesh IBT proof (Mesh transport IBT extension):
|
||
// proof = ML-DSA-65_sign(client_sk,
|
||
// "mt-tunnel-mesh" || peer_node_id ||
|
||
// floor(cached_W / 2) || mesh_session_nonce)
|
||
|
||
use alloc::vec::Vec;
|
||
|
||
use mt_codec::domain::{TUNNEL_MESH, TUNNEL_ONLINE};
|
||
use mt_codec::{write_bytes, write_u64};
|
||
use mt_crypto::{sign, verify, CryptoError, PublicKey, SecretKey, Signature};
|
||
|
||
use crate::error::NetError;
|
||
|
||
// Re-export для backwards compatibility public API mt-net до Phase B.0 callsites.
|
||
pub use mt_codec::domain::TUNNEL_MESH as DOMAIN_TUNNEL_MESH;
|
||
pub use mt_codec::domain::TUNNEL_ONLINE as DOMAIN_TUNNEL_ONLINE;
|
||
|
||
pub const MESH_NONCE_SIZE: usize = 32;
|
||
pub const MESH_STALENESS_BOUND_TAU1: u64 = 7;
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum IbtError {
|
||
InvalidSignature,
|
||
StalenessBoundExceeded,
|
||
ReplayedNonce,
|
||
CryptoFailure,
|
||
}
|
||
|
||
impl From<CryptoError> for IbtError {
|
||
fn from(_: CryptoError) -> Self {
|
||
IbtError::CryptoFailure
|
||
}
|
||
}
|
||
|
||
impl From<IbtError> for NetError {
|
||
fn from(_: IbtError) -> Self {
|
||
NetError::InvalidPayloadField
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn window_slot(window_index: u64) -> u64 {
|
||
window_index / 2
|
||
}
|
||
|
||
pub fn ibt_online_message(server_node_id: &[u8; 32], window_index: u64) -> Vec<u8> {
|
||
let mut buf = Vec::with_capacity(TUNNEL_ONLINE.len() + 32 + 8);
|
||
write_bytes(&mut buf, TUNNEL_ONLINE);
|
||
write_bytes(&mut buf, server_node_id);
|
||
write_u64(&mut buf, window_slot(window_index));
|
||
buf
|
||
}
|
||
|
||
pub fn ibt_online_proof(
|
||
client_sk: &SecretKey,
|
||
server_node_id: &[u8; 32],
|
||
window_index: u64,
|
||
) -> Result<Signature, IbtError> {
|
||
let msg = ibt_online_message(server_node_id, window_index);
|
||
sign(client_sk, &msg).map_err(IbtError::from)
|
||
}
|
||
|
||
pub fn ibt_online_verify(
|
||
client_pk: &PublicKey,
|
||
server_node_id: &[u8; 32],
|
||
current_window: u64,
|
||
proof: &Signature,
|
||
) -> Result<u64, IbtError> {
|
||
// spec: window slot = current ИЛИ previous (acceptable bound)
|
||
let candidates = [current_window, current_window.saturating_sub(2)];
|
||
for &w in &candidates {
|
||
let msg = ibt_online_message(server_node_id, w);
|
||
if verify(client_pk, &msg, proof) {
|
||
return Ok(w);
|
||
}
|
||
}
|
||
Err(IbtError::InvalidSignature)
|
||
}
|
||
|
||
pub fn ibt_mesh_message(
|
||
peer_node_id: &[u8; 32],
|
||
cached_window_index: u64,
|
||
mesh_session_nonce: &[u8; MESH_NONCE_SIZE],
|
||
) -> Vec<u8> {
|
||
let mut buf = Vec::with_capacity(TUNNEL_MESH.len() + 32 + 8 + MESH_NONCE_SIZE);
|
||
write_bytes(&mut buf, TUNNEL_MESH);
|
||
write_bytes(&mut buf, peer_node_id);
|
||
write_u64(&mut buf, window_slot(cached_window_index));
|
||
write_bytes(&mut buf, mesh_session_nonce);
|
||
buf
|
||
}
|
||
|
||
pub fn ibt_mesh_proof(
|
||
client_sk: &SecretKey,
|
||
peer_node_id: &[u8; 32],
|
||
cached_window_index: u64,
|
||
mesh_session_nonce: &[u8; MESH_NONCE_SIZE],
|
||
) -> Result<Signature, IbtError> {
|
||
let msg = ibt_mesh_message(peer_node_id, cached_window_index, mesh_session_nonce);
|
||
sign(client_sk, &msg).map_err(IbtError::from)
|
||
}
|
||
|
||
pub fn ibt_mesh_verify_with_window(
|
||
client_pk: &PublicKey,
|
||
peer_node_id: &[u8; 32],
|
||
known_window: u64,
|
||
mesh_session_nonce: &[u8; MESH_NONCE_SIZE],
|
||
proof: &Signature,
|
||
tau1_windows: u64,
|
||
) -> Result<u64, IbtError> {
|
||
// Backwards-compat fallback API: searches full bound. Prefer
|
||
// ibt_mesh_verify_explicit с явным cached_W из mesh advertisement
|
||
// (per Pass 21 algorithmic complexity audit — O(1) verify path).
|
||
let span = MESH_STALENESS_BOUND_TAU1 * tau1_windows;
|
||
let lo = known_window.saturating_sub(span);
|
||
let hi = known_window;
|
||
let mut w = hi;
|
||
loop {
|
||
let msg = ibt_mesh_message(peer_node_id, w, mesh_session_nonce);
|
||
if verify(client_pk, &msg, proof) {
|
||
return Ok(w);
|
||
}
|
||
if w <= lo {
|
||
break;
|
||
}
|
||
w -= 1;
|
||
}
|
||
Err(IbtError::InvalidSignature)
|
||
}
|
||
|
||
pub fn ibt_mesh_verify_explicit(
|
||
client_pk: &PublicKey,
|
||
peer_node_id: &[u8; 32],
|
||
cached_window_index: u64,
|
||
known_window: u64,
|
||
mesh_session_nonce: &[u8; MESH_NONCE_SIZE],
|
||
proof: &Signature,
|
||
tau1_windows: u64,
|
||
) -> Result<u64, IbtError> {
|
||
// O(1) path: cached_W пришёл от sender в plain mesh advertisement;
|
||
// verifier проверяет staleness bound и одну ML-DSA-65 verify.
|
||
let span = MESH_STALENESS_BOUND_TAU1 * tau1_windows;
|
||
if cached_window_index > known_window || cached_window_index < known_window.saturating_sub(span)
|
||
{
|
||
return Err(IbtError::StalenessBoundExceeded);
|
||
}
|
||
let msg = ibt_mesh_message(peer_node_id, cached_window_index, mesh_session_nonce);
|
||
if verify(client_pk, &msg, proof) {
|
||
Ok(cached_window_index)
|
||
} else {
|
||
Err(IbtError::InvalidSignature)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use mt_crypto::keypair;
|
||
|
||
#[test]
|
||
fn online_proof_roundtrip() {
|
||
let (pk, sk) = keypair();
|
||
let server_node_id = [0x42u8; 32];
|
||
let proof = ibt_online_proof(&sk, &server_node_id, 1000).unwrap();
|
||
let w = ibt_online_verify(&pk, &server_node_id, 1000, &proof).unwrap();
|
||
assert_eq!(w, 1000);
|
||
}
|
||
|
||
#[test]
|
||
fn online_proof_previous_window_accepted() {
|
||
let (pk, sk) = keypair();
|
||
let server_node_id = [0x42u8; 32];
|
||
// Sign at slot 500 (window 1000 либо 1001 → slot 500)
|
||
let proof = ibt_online_proof(&sk, &server_node_id, 1000).unwrap();
|
||
// Verify при current=1002 (slot 501) — ожидание fail так как replay
|
||
// window = current ИЛИ previous (sub 2 windows = 1000, slot 500)
|
||
let w = ibt_online_verify(&pk, &server_node_id, 1002, &proof).unwrap();
|
||
assert_eq!(w, 1000);
|
||
}
|
||
|
||
#[test]
|
||
fn online_proof_too_old_rejected() {
|
||
let (pk, sk) = keypair();
|
||
let server_node_id = [0x42u8; 32];
|
||
let proof = ibt_online_proof(&sk, &server_node_id, 100).unwrap();
|
||
// current = 1000 — slot 500; old slot 50 → reject (вне 2-window window)
|
||
assert_eq!(
|
||
ibt_online_verify(&pk, &server_node_id, 1000, &proof),
|
||
Err(IbtError::InvalidSignature)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn online_proof_wrong_server_id_rejected() {
|
||
let (pk, sk) = keypair();
|
||
let server_a = [0x42u8; 32];
|
||
let server_b = [0x33u8; 32];
|
||
let proof = ibt_online_proof(&sk, &server_a, 1000).unwrap();
|
||
assert_eq!(
|
||
ibt_online_verify(&pk, &server_b, 1000, &proof),
|
||
Err(IbtError::InvalidSignature)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_proof_roundtrip_within_staleness_window() {
|
||
let (pk, sk) = keypair();
|
||
let peer = [0x33u8; 32];
|
||
let nonce = [0x77u8; 32];
|
||
let cached_w = 5000;
|
||
let known_w = 5000;
|
||
let tau1 = 60;
|
||
let proof = ibt_mesh_proof(&sk, &peer, cached_w, &nonce).unwrap();
|
||
let w = ibt_mesh_verify_with_window(&pk, &peer, known_w, &nonce, &proof, tau1).unwrap();
|
||
// window_slot(5000) == window_slot(5001), so verifier may accept even
|
||
// window
|
||
assert!(w == 5000 || w == 5001);
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_proof_outside_staleness_window_rejected() {
|
||
let (pk, sk) = keypair();
|
||
let peer = [0x33u8; 32];
|
||
let nonce = [0x77u8; 32];
|
||
let tau1 = 60;
|
||
let proof = ibt_mesh_proof(&sk, &peer, 1000, &nonce).unwrap();
|
||
// known_window = 1000 + 8·τ₁ — outside 7·τ₁ acceptable bound
|
||
let known_w = 1000 + 8 * tau1;
|
||
assert_eq!(
|
||
ibt_mesh_verify_with_window(&pk, &peer, known_w, &nonce, &proof, tau1),
|
||
Err(IbtError::InvalidSignature)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn cross_context_domain_separation() {
|
||
let server = [0x42u8; 32];
|
||
let nonce = [0x77u8; 32];
|
||
let online = ibt_online_message(&server, 1000);
|
||
let mesh = ibt_mesh_message(&server, 1000, &nonce);
|
||
assert_ne!(online, mesh, "online vs mesh domain separator must differ");
|
||
}
|
||
|
||
#[test]
|
||
fn online_message_byte_layout() {
|
||
let server = [0x42u8; 32];
|
||
let msg = ibt_online_message(&server, 1000);
|
||
assert_eq!(msg.len(), TUNNEL_ONLINE.len() + 32 + 8);
|
||
assert_eq!(&msg[..TUNNEL_ONLINE.len()], TUNNEL_ONLINE);
|
||
assert_eq!(&msg[TUNNEL_ONLINE.len()..TUNNEL_ONLINE.len() + 32], &server);
|
||
assert_eq!(&msg[TUNNEL_ONLINE.len() + 32..], &500u64.to_le_bytes());
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_message_byte_layout() {
|
||
let peer = [0x33u8; 32];
|
||
let nonce = [0x77u8; 32];
|
||
let msg = ibt_mesh_message(&peer, 5000, &nonce);
|
||
let exp_len = DOMAIN_TUNNEL_MESH.len() + 32 + 8 + 32;
|
||
assert_eq!(msg.len(), exp_len);
|
||
assert_eq!(&msg[..TUNNEL_MESH.len()], TUNNEL_MESH);
|
||
let off = TUNNEL_MESH.len();
|
||
assert_eq!(&msg[off..off + 32], &peer);
|
||
assert_eq!(&msg[off + 32..off + 40], &2500u64.to_le_bytes());
|
||
assert_eq!(&msg[off + 40..], &nonce);
|
||
}
|
||
}
|