- Cache Curve25519 constants (P, CURVE_ORDER, WEIERSTRASS_A) with LazyLock eliminating ~768 BigUint heap allocations per auth handshake - Optimize scalar_mul to use bit() instead of clone+shift - Set TCP socket buffers to 4MB via socket2 (matching UDP path) - Increase TCP RX buffers from 64KB to 256KB - Use 256KB writes at unlimited rate (vs 32KB) reducing syscall overhead - Fix LDAP filter injection with RFC 4515 escaping - Fix unwrap panic on empty LDAP search results Benchmarked on WiFi against MikroTik: TCP Download: +67% (19.7 → 32.9 Mbps avg) TCP Upload: +87% (3.6 → 6.7 Mbps avg) Local CPU: lower across all tests (29-36% vs 32-58%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
661 lines
20 KiB
Rust
661 lines
20 KiB
Rust
//! EC-SRP5 authentication for MikroTik RouterOS >= 6.43.
|
|
//!
|
|
//! Implements the Curve25519-Weierstrass EC-SRP5 protocol used by MikroTik btest.
|
|
//! Based on research by Margin Research (Apache-2.0 License):
|
|
//! https://github.com/MarginResearch/mikrotik_authentication
|
|
//!
|
|
//! btest framing: `[len:1][payload]` (no 0x06 handler byte, unlike Winbox).
|
|
|
|
use std::sync::LazyLock;
|
|
|
|
use num_bigint::BigUint;
|
|
use num_integer::Integer;
|
|
use num_traits::{One, Zero};
|
|
use sha2::{Digest, Sha256};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
use crate::protocol::{BtestError, Result};
|
|
|
|
// --- Curve25519 parameters in Weierstrass form (cached, computed once) ---
|
|
|
|
static P: LazyLock<BigUint> = LazyLock::new(|| {
|
|
BigUint::parse_bytes(
|
|
b"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed",
|
|
16,
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
static CURVE_ORDER: LazyLock<BigUint> = LazyLock::new(|| {
|
|
BigUint::parse_bytes(
|
|
b"1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed",
|
|
16,
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
static WEIERSTRASS_A: LazyLock<BigUint> = LazyLock::new(|| {
|
|
BigUint::parse_bytes(
|
|
b"2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144",
|
|
16,
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
const MONT_A: u64 = 486662;
|
|
|
|
// --- Modular arithmetic ---
|
|
|
|
fn modinv(a: &BigUint, modulus: &BigUint) -> BigUint {
|
|
// Fermat's little theorem: a^(p-2) mod p
|
|
let exp = modulus - BigUint::from(2u32);
|
|
a.modpow(&exp, modulus)
|
|
}
|
|
|
|
fn legendre_symbol(a: &BigUint, p: &BigUint) -> i32 {
|
|
let exp = (p - BigUint::one()) / BigUint::from(2u32);
|
|
let l = a.modpow(&exp, p);
|
|
if l == p - BigUint::one() {
|
|
-1
|
|
} else if l == BigUint::zero() {
|
|
0
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
|
|
fn prime_mod_sqrt(a: &BigUint, p_val: &BigUint) -> Option<(BigUint, BigUint)> {
|
|
let a = a % p_val;
|
|
if a.is_zero() {
|
|
return Some((BigUint::zero(), BigUint::zero()));
|
|
}
|
|
if legendre_symbol(&a, p_val) != 1 {
|
|
return None;
|
|
}
|
|
|
|
// For p ≡ 5 (mod 8) — which is Curve25519's case — use Atkin's algorithm
|
|
// This is more reliable than Tonelli-Shanks for this specific case
|
|
let p_mod_8 = p_val % BigUint::from(8u32);
|
|
if p_mod_8 == BigUint::from(5u32) {
|
|
// v = (2a)^((p-5)/8) mod p
|
|
let exp = (p_val - BigUint::from(5u32)) / BigUint::from(8u32);
|
|
let two_a = (BigUint::from(2u32) * &a) % p_val;
|
|
let v = two_a.modpow(&exp, p_val);
|
|
// i = 2 * a * v^2 mod p
|
|
let i_val = (BigUint::from(2u32) * &a % p_val * &v % p_val * &v) % p_val;
|
|
// x = a * v * (i - 1) mod p
|
|
let i_minus_1 = if i_val >= BigUint::one() {
|
|
(&i_val - BigUint::one()) % p_val
|
|
} else {
|
|
(p_val - BigUint::one() + &i_val) % p_val
|
|
};
|
|
let x = (&a * &v % p_val * &i_minus_1) % p_val;
|
|
// Verify: x^2 ≡ a (mod p)
|
|
let check = (&x * &x) % p_val;
|
|
if check == a {
|
|
let other = p_val - &x;
|
|
return Some((x, other));
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if p_mod_8 == BigUint::from(3u32) || p_mod_8 == BigUint::from(7u32) {
|
|
let exp = (p_val + BigUint::one()) / BigUint::from(4u32);
|
|
let x = a.modpow(&exp, p_val);
|
|
let other = p_val - &x;
|
|
return Some((x, other));
|
|
}
|
|
|
|
// General Tonelli-Shanks for other primes
|
|
let mut q = p_val - BigUint::one();
|
|
let mut s = 0u32;
|
|
while q.is_even() {
|
|
s += 1;
|
|
q >>= 1;
|
|
}
|
|
|
|
let mut z = BigUint::from(2u32);
|
|
while legendre_symbol(&z, p_val) != -1 {
|
|
z += BigUint::one();
|
|
}
|
|
let mut c = z.modpow(&q, p_val);
|
|
let mut x = a.modpow(&((&q + BigUint::one()) / BigUint::from(2u32)), p_val);
|
|
let mut t = a.modpow(&q, p_val);
|
|
let mut m = s;
|
|
|
|
while t != BigUint::one() {
|
|
let mut i = 1u32;
|
|
let mut tmp = (&t * &t) % p_val;
|
|
while tmp != BigUint::one() {
|
|
tmp = (&tmp * &tmp) % p_val;
|
|
i += 1;
|
|
}
|
|
let b = c.modpow(&BigUint::from(1u32 << (m - i - 1)), p_val);
|
|
x = (&x * &b) % p_val;
|
|
t = ((&t * &b % p_val) * &b) % p_val;
|
|
c = (&b * &b) % p_val;
|
|
m = i;
|
|
}
|
|
|
|
let other = p_val - &x;
|
|
Some((x, other))
|
|
}
|
|
|
|
// --- Weierstrass curve point ---
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Point {
|
|
x: BigUint,
|
|
y: BigUint,
|
|
infinity: bool,
|
|
}
|
|
|
|
impl Point {
|
|
fn infinity() -> Self {
|
|
Self {
|
|
x: BigUint::zero(),
|
|
y: BigUint::zero(),
|
|
infinity: true,
|
|
}
|
|
}
|
|
|
|
fn new(x: BigUint, y: BigUint) -> Self {
|
|
Self {
|
|
x,
|
|
y,
|
|
infinity: false,
|
|
}
|
|
}
|
|
|
|
fn add(&self, other: &Point) -> Point {
|
|
let p_val = &*P;
|
|
if self.infinity {
|
|
return other.clone();
|
|
}
|
|
if other.infinity {
|
|
return self.clone();
|
|
}
|
|
if self.x == other.x && self.y != other.y {
|
|
return Point::infinity();
|
|
}
|
|
|
|
let lam = if self.x == other.x && self.y == other.y {
|
|
// Point doubling
|
|
let three_x_sq = (BigUint::from(3u32) * &self.x * &self.x + &*WEIERSTRASS_A) % p_val;
|
|
let two_y = (BigUint::from(2u32) * &self.y) % p_val;
|
|
(three_x_sq * modinv(&two_y, p_val)) % p_val
|
|
} else {
|
|
// Point addition
|
|
let dy = if other.y >= self.y {
|
|
(&other.y - &self.y) % p_val
|
|
} else {
|
|
(p_val - (&self.y - &other.y) % p_val) % p_val
|
|
};
|
|
let dx = if other.x >= self.x {
|
|
(&other.x - &self.x) % p_val
|
|
} else {
|
|
(p_val - (&self.x - &other.x) % p_val) % p_val
|
|
};
|
|
(dy * modinv(&dx, p_val)) % p_val
|
|
};
|
|
|
|
let x3 = {
|
|
let lam_sq = (&lam * &lam) % p_val;
|
|
let sum_x = (&self.x + &other.x) % p_val;
|
|
if lam_sq >= sum_x {
|
|
(lam_sq - sum_x) % p_val
|
|
} else {
|
|
(p_val - (sum_x - lam_sq) % p_val) % p_val
|
|
}
|
|
};
|
|
let y3 = {
|
|
let dx = if self.x >= x3 {
|
|
(&self.x - &x3) % p_val
|
|
} else {
|
|
(p_val - (&x3 - &self.x) % p_val) % p_val
|
|
};
|
|
let prod = (&lam * dx) % p_val;
|
|
if prod >= self.y {
|
|
(prod - &self.y) % p_val
|
|
} else {
|
|
(p_val - (&self.y - prod) % p_val) % p_val
|
|
}
|
|
};
|
|
|
|
Point::new(x3, y3)
|
|
}
|
|
|
|
fn scalar_mul(&self, scalar: &BigUint) -> Point {
|
|
let mut result = Point::infinity();
|
|
let mut base = self.clone();
|
|
let bits = scalar.bits();
|
|
|
|
for i in 0..bits {
|
|
if scalar.bit(i) {
|
|
result = result.add(&base);
|
|
}
|
|
base = base.add(&base);
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
// --- WCurve: Curve25519 in Weierstrass form ---
|
|
|
|
struct WCurve {
|
|
g: Point,
|
|
conversion_from_m: BigUint,
|
|
conversion_to_m: BigUint,
|
|
}
|
|
|
|
impl WCurve {
|
|
fn new() -> Self {
|
|
let p_val = &*P;
|
|
let mont_a = BigUint::from(MONT_A);
|
|
let three_inv = modinv(&BigUint::from(3u32), p_val);
|
|
let conversion_from_m = (&mont_a * &three_inv) % p_val;
|
|
let conversion_to_m = (p_val - &conversion_from_m) % p_val;
|
|
|
|
let mut curve = WCurve {
|
|
g: Point::infinity(),
|
|
conversion_from_m,
|
|
conversion_to_m,
|
|
};
|
|
curve.g = curve.lift_x(&BigUint::from(9u32), false);
|
|
curve
|
|
}
|
|
|
|
fn to_montgomery(&self, pt: &Point) -> ([u8; 32], u8) {
|
|
let p_val = &*P;
|
|
let x = (&pt.x + &self.conversion_to_m) % p_val;
|
|
let parity = if pt.y.bit(0) { 1u8 } else { 0u8 };
|
|
let mut bytes = [0u8; 32];
|
|
let x_bytes = x.to_bytes_be();
|
|
let start = 32 - x_bytes.len().min(32);
|
|
bytes[start..].copy_from_slice(&x_bytes[..x_bytes.len().min(32)]);
|
|
(bytes, parity)
|
|
}
|
|
|
|
fn lift_x(&self, x_mont: &BigUint, parity: bool) -> Point {
|
|
let p_val = &*P;
|
|
let x = x_mont % p_val;
|
|
// y^2 = x^3 + Ax^2 + x (Montgomery)
|
|
let y_squared = (&x * &x * &x + BigUint::from(MONT_A) * &x * &x + &x) % p_val;
|
|
// Convert x to Weierstrass
|
|
let x_w = (&x + &self.conversion_from_m) % p_val;
|
|
|
|
if let Some((y1, y2)) = prime_mod_sqrt(&y_squared, p_val) {
|
|
let pt1 = Point::new(x_w.clone(), y1);
|
|
let pt2 = Point::new(x_w, y2);
|
|
if parity {
|
|
if pt1.y.bit(0) { pt1 } else { pt2 }
|
|
} else {
|
|
if !pt1.y.bit(0) { pt1 } else { pt2 }
|
|
}
|
|
} else {
|
|
Point::infinity()
|
|
}
|
|
}
|
|
|
|
fn gen_public_key(&self, priv_key: &[u8; 32]) -> ([u8; 32], u8) {
|
|
let scalar = BigUint::from_bytes_be(priv_key);
|
|
let pt = self.g.scalar_mul(&scalar);
|
|
self.to_montgomery(&pt)
|
|
}
|
|
|
|
fn redp1(&self, x_bytes: &[u8; 32], parity: bool) -> Point {
|
|
let mut x = sha256_bytes(x_bytes);
|
|
loop {
|
|
let x2 = sha256_bytes(&x);
|
|
let x_int = BigUint::from_bytes_be(&x2);
|
|
let pt = self.lift_x(&x_int, parity);
|
|
if !pt.infinity {
|
|
return pt;
|
|
}
|
|
let mut val = BigUint::from_bytes_be(&x);
|
|
val += BigUint::one();
|
|
x = bigint_to_32bytes(&val);
|
|
}
|
|
}
|
|
|
|
fn gen_password_validator_priv(
|
|
&self,
|
|
username: &str,
|
|
password: &str,
|
|
salt: &[u8; 16],
|
|
) -> [u8; 32] {
|
|
let inner = sha256_bytes(format!("{}:{}", username, password).as_bytes());
|
|
let mut input = Vec::with_capacity(16 + 32);
|
|
input.extend_from_slice(salt);
|
|
input.extend_from_slice(&inner);
|
|
sha256_bytes(&input)
|
|
}
|
|
}
|
|
|
|
fn sha256_bytes(data: &[u8]) -> [u8; 32] {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(data);
|
|
let result = hasher.finalize();
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(&result);
|
|
out
|
|
}
|
|
|
|
fn bigint_to_32bytes(val: &BigUint) -> [u8; 32] {
|
|
let bytes = val.to_bytes_be();
|
|
let mut out = [0u8; 32];
|
|
let start = 32usize.saturating_sub(bytes.len());
|
|
let copy_len = bytes.len().min(32);
|
|
out[start..start + copy_len].copy_from_slice(&bytes[bytes.len() - copy_len..]);
|
|
out
|
|
}
|
|
|
|
// --- EC-SRP5 Client Authentication ---
|
|
|
|
/// Perform EC-SRP5 authentication as a client.
|
|
/// Called after receiving `03 00 00 00` from the server.
|
|
pub async fn client_authenticate<S: AsyncReadExt + AsyncWriteExt + Unpin>(
|
|
stream: &mut S,
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<()> {
|
|
tracing::info!("Starting EC-SRP5 authentication");
|
|
let w = WCurve::new();
|
|
|
|
// Generate client ephemeral keypair
|
|
let s_a: [u8; 32] = rand::random();
|
|
let (x_w_a, x_w_a_parity) = w.gen_public_key(&s_a);
|
|
|
|
// MSG1: [len][username\0][pubkey:32][parity:1]
|
|
let mut payload = Vec::new();
|
|
payload.extend_from_slice(username.as_bytes());
|
|
payload.push(0x00);
|
|
payload.extend_from_slice(&x_w_a);
|
|
payload.push(x_w_a_parity);
|
|
let mut msg1 = vec![payload.len() as u8];
|
|
msg1.extend_from_slice(&payload);
|
|
stream.write_all(&msg1).await?;
|
|
stream.flush().await?;
|
|
tracing::debug!("EC-SRP5: sent client pubkey ({} bytes)", msg1.len());
|
|
|
|
// MSG2: [len][server_pubkey:32][parity:1][salt:16]
|
|
let mut resp_header = [0u8; 1];
|
|
stream.read_exact(&mut resp_header).await?;
|
|
let resp_len = resp_header[0] as usize;
|
|
let mut resp_data = vec![0u8; resp_len];
|
|
stream.read_exact(&mut resp_data).await?;
|
|
|
|
if resp_data.len() < 49 {
|
|
return Err(BtestError::Protocol(format!(
|
|
"EC-SRP5: server challenge too short ({} bytes)",
|
|
resp_data.len()
|
|
)));
|
|
}
|
|
|
|
let mut x_w_b = [0u8; 32];
|
|
x_w_b.copy_from_slice(&resp_data[0..32]);
|
|
let x_w_b_parity = resp_data[32] != 0;
|
|
let mut salt = [0u8; 16];
|
|
salt.copy_from_slice(&resp_data[33..49]);
|
|
|
|
tracing::debug!("EC-SRP5: received server challenge (salt={})", hex::encode(&salt));
|
|
|
|
// Compute shared secret
|
|
let i = w.gen_password_validator_priv(username, password, &salt);
|
|
let (x_gamma, _) = w.gen_public_key(&i);
|
|
let v = w.redp1(&x_gamma, true);
|
|
|
|
let w_b_point = w.lift_x(&BigUint::from_bytes_be(&x_w_b), x_w_b_parity);
|
|
let w_b_unblinded = w_b_point.add(&v);
|
|
|
|
let mut j_input = Vec::with_capacity(64);
|
|
j_input.extend_from_slice(&x_w_a);
|
|
j_input.extend_from_slice(&x_w_b);
|
|
let j = sha256_bytes(&j_input);
|
|
|
|
let i_int = BigUint::from_bytes_be(&i);
|
|
let j_int = BigUint::from_bytes_be(&j);
|
|
let s_a_int = BigUint::from_bytes_be(&s_a);
|
|
let order = &*CURVE_ORDER;
|
|
let scalar = ((&i_int * &j_int) + &s_a_int) % order;
|
|
|
|
let z_point = w_b_unblinded.scalar_mul(&scalar);
|
|
let (z, _) = w.to_montgomery(&z_point);
|
|
|
|
// MSG3: [len][client_cc:32]
|
|
let mut cc_input = Vec::with_capacity(64);
|
|
cc_input.extend_from_slice(&j);
|
|
cc_input.extend_from_slice(&z);
|
|
let client_cc = sha256_bytes(&cc_input);
|
|
|
|
let mut msg3 = vec![client_cc.len() as u8];
|
|
msg3.extend_from_slice(&client_cc);
|
|
stream.write_all(&msg3).await?;
|
|
stream.flush().await?;
|
|
tracing::debug!("EC-SRP5: sent client proof");
|
|
|
|
// MSG4: [len][server_cc:32]
|
|
let mut resp4_header = [0u8; 1];
|
|
stream.read_exact(&mut resp4_header).await?;
|
|
let resp4_len = resp4_header[0] as usize;
|
|
let mut server_cc_received = vec![0u8; resp4_len];
|
|
stream.read_exact(&mut server_cc_received).await?;
|
|
|
|
// Verify server confirmation
|
|
let mut sc_input = Vec::with_capacity(96);
|
|
sc_input.extend_from_slice(&j);
|
|
sc_input.extend_from_slice(&client_cc);
|
|
sc_input.extend_from_slice(&z);
|
|
let server_cc_expected = sha256_bytes(&sc_input);
|
|
|
|
if server_cc_received == server_cc_expected {
|
|
tracing::info!("EC-SRP5 authentication successful");
|
|
Ok(())
|
|
} else {
|
|
// Check if server sent an error message
|
|
if let Ok(msg) = std::str::from_utf8(&server_cc_received) {
|
|
Err(BtestError::Protocol(format!(
|
|
"EC-SRP5 authentication failed: {}",
|
|
msg
|
|
)))
|
|
} else {
|
|
Err(BtestError::AuthFailed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- EC-SRP5 Server Authentication ---
|
|
|
|
/// Server-side EC-SRP5 credential store.
|
|
pub struct EcSrp5Credentials {
|
|
salt: [u8; 16],
|
|
x_gamma: [u8; 32],
|
|
gamma_parity: bool,
|
|
}
|
|
|
|
impl EcSrp5Credentials {
|
|
/// Derive EC-SRP5 credentials from username/password (done once at startup).
|
|
pub fn derive(username: &str, password: &str) -> Self {
|
|
let salt: [u8; 16] = rand::random();
|
|
let w = WCurve::new();
|
|
let i = w.gen_password_validator_priv(username, password, &salt);
|
|
let (x_gamma, parity) = w.gen_public_key(&i);
|
|
Self {
|
|
salt,
|
|
x_gamma,
|
|
gamma_parity: parity != 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Perform EC-SRP5 authentication as a server.
|
|
/// Called after sending `03 00 00 00` to the client.
|
|
pub async fn server_authenticate<S: AsyncReadExt + AsyncWriteExt + Unpin>(
|
|
stream: &mut S,
|
|
username: &str,
|
|
creds: &EcSrp5Credentials,
|
|
) -> Result<()> {
|
|
tracing::info!("Starting EC-SRP5 server authentication");
|
|
let w = WCurve::new();
|
|
|
|
// MSG1: read [len][username\0][pubkey:32][parity:1]
|
|
let mut len_buf = [0u8; 1];
|
|
stream.read_exact(&mut len_buf).await?;
|
|
let msg_len = len_buf[0] as usize;
|
|
let mut msg1_data = vec![0u8; msg_len];
|
|
stream.read_exact(&mut msg1_data).await?;
|
|
|
|
// Parse username
|
|
let null_pos = msg1_data.iter().position(|&b| b == 0)
|
|
.ok_or_else(|| BtestError::Protocol("EC-SRP5: no null terminator in username".into()))?;
|
|
let client_username = std::str::from_utf8(&msg1_data[..null_pos])
|
|
.map_err(|_| BtestError::Protocol("EC-SRP5: invalid username encoding".into()))?;
|
|
|
|
if client_username != username {
|
|
tracing::warn!("EC-SRP5: username mismatch (got '{}')", client_username);
|
|
return Err(BtestError::AuthFailed);
|
|
}
|
|
|
|
let key_start = null_pos + 1;
|
|
if msg1_data.len() < key_start + 33 {
|
|
return Err(BtestError::Protocol("EC-SRP5: client message too short".into()));
|
|
}
|
|
let mut x_w_a = [0u8; 32];
|
|
x_w_a.copy_from_slice(&msg1_data[key_start..key_start + 32]);
|
|
let x_w_a_parity = msg1_data[key_start + 32] != 0;
|
|
|
|
tracing::debug!("EC-SRP5: received client pubkey from '{}'", client_username);
|
|
|
|
// Generate server ephemeral keypair
|
|
let s_b: [u8; 32] = rand::random();
|
|
let s_b_int = BigUint::from_bytes_be(&s_b);
|
|
let pub_b = w.g.scalar_mul(&s_b_int);
|
|
|
|
// Compute password-entangled public key: W_b = s_b*G + redp1(x_gamma, 0)
|
|
let gamma = w.redp1(&creds.x_gamma, false);
|
|
let w_b = pub_b.add(&gamma);
|
|
let (x_w_b, x_w_b_parity) = w.to_montgomery(&w_b);
|
|
|
|
// MSG2: [len][server_pubkey:32][parity:1][salt:16]
|
|
let mut payload2 = Vec::with_capacity(49);
|
|
payload2.extend_from_slice(&x_w_b);
|
|
payload2.push(x_w_b_parity);
|
|
payload2.extend_from_slice(&creds.salt);
|
|
let mut msg2 = vec![payload2.len() as u8];
|
|
msg2.extend_from_slice(&payload2);
|
|
stream.write_all(&msg2).await?;
|
|
stream.flush().await?;
|
|
tracing::debug!("EC-SRP5: sent server challenge");
|
|
|
|
// Compute shared secret (server side: ECPESVDP-SRP-B)
|
|
let mut j_input = Vec::with_capacity(64);
|
|
j_input.extend_from_slice(&x_w_a);
|
|
j_input.extend_from_slice(&x_w_b);
|
|
let j = sha256_bytes(&j_input);
|
|
let j_int = BigUint::from_bytes_be(&j);
|
|
|
|
// Server ECPESVDP-SRP-B: Z = s_b * (W_a + j * gamma)
|
|
// gamma = lift_x(x_gamma, parity=1) — the raw validator public key point
|
|
// (NOT redp1 — that's used for blinding W_b, not for verification)
|
|
let w_a = w.lift_x(&BigUint::from_bytes_be(&x_w_a), x_w_a_parity);
|
|
let gamma = w.lift_x(&BigUint::from_bytes_be(&creds.x_gamma), creds.gamma_parity);
|
|
let j_gamma = gamma.scalar_mul(&j_int);
|
|
let sum = w_a.add(&j_gamma);
|
|
let z_point = sum.scalar_mul(&s_b_int);
|
|
let (z, _) = w.to_montgomery(&z_point);
|
|
|
|
// MSG3: read [len][client_cc:32]
|
|
let mut len3 = [0u8; 1];
|
|
stream.read_exact(&mut len3).await?;
|
|
let mut client_cc = vec![0u8; len3[0] as usize];
|
|
stream.read_exact(&mut client_cc).await?;
|
|
|
|
// Verify client confirmation
|
|
let mut cc_input = Vec::with_capacity(64);
|
|
cc_input.extend_from_slice(&j);
|
|
cc_input.extend_from_slice(&z);
|
|
let expected_cc = sha256_bytes(&cc_input);
|
|
|
|
if client_cc != expected_cc {
|
|
tracing::warn!("EC-SRP5: client proof mismatch");
|
|
return Err(BtestError::AuthFailed);
|
|
}
|
|
|
|
// MSG4: [len][server_cc:32]
|
|
let mut sc_input = Vec::with_capacity(96);
|
|
sc_input.extend_from_slice(&j);
|
|
sc_input.extend_from_slice(&client_cc);
|
|
sc_input.extend_from_slice(&z);
|
|
let server_cc = sha256_bytes(&sc_input);
|
|
|
|
let mut msg4 = vec![server_cc.len() as u8];
|
|
msg4.extend_from_slice(&server_cc);
|
|
stream.write_all(&msg4).await?;
|
|
stream.flush().await?;
|
|
|
|
tracing::info!("EC-SRP5 server authentication successful for '{}'", client_username);
|
|
Ok(())
|
|
}
|
|
|
|
mod hex {
|
|
pub fn encode(data: &[u8]) -> String {
|
|
data.iter().map(|b| format!("{:02x}", b)).collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_curve_generator() {
|
|
let w = WCurve::new();
|
|
assert!(!w.g.infinity);
|
|
// Generator from lift_x(9, false) should produce a valid point
|
|
let (x_mont, _) = w.to_montgomery(&w.g);
|
|
let x_int = BigUint::from_bytes_be(&x_mont);
|
|
assert_eq!(x_int, BigUint::from(9u32));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pubkey_generation() {
|
|
let w = WCurve::new();
|
|
let priv_key = [1u8; 32];
|
|
let (pubkey, parity) = w.gen_public_key(&priv_key);
|
|
assert_ne!(pubkey, [0u8; 32]);
|
|
assert!(parity <= 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_password_validator() {
|
|
let w = WCurve::new();
|
|
let salt = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10];
|
|
let i = w.gen_password_validator_priv("testuser", "testpass", &salt);
|
|
assert_ne!(i, [0u8; 32]);
|
|
// Deterministic: same inputs produce same output
|
|
let i2 = w.gen_password_validator_priv("testuser", "testpass", &salt);
|
|
assert_eq!(i, i2);
|
|
// Different password produces different result
|
|
let i3 = w.gen_password_validator_priv("testuser", "other", &salt);
|
|
assert_ne!(i, i3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_redp1() {
|
|
let w = WCurve::new();
|
|
let input = [42u8; 32];
|
|
let pt = w.redp1(&input, false);
|
|
assert!(!pt.infinity);
|
|
}
|
|
|
|
#[test]
|
|
fn test_scalar_mul_identity() {
|
|
let w = WCurve::new();
|
|
let one = BigUint::one();
|
|
let pt = w.g.scalar_mul(&one);
|
|
assert_eq!(pt.x, w.g.x);
|
|
assert_eq!(pt.y, w.g.y);
|
|
}
|
|
}
|