montana/Montana-Protocol/Code/crates/mt-net/src/ibt.rs

273 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}
}