Add EC-SRP5 authentication (RouterOS >= 6.43)
All checks were successful
CI / test (push) Successful in 1m18s

Client: auto-detects 03 response and performs EC-SRP5 handshake
Server: --ecsrp5 flag enables Curve25519 Weierstrass EC-SRP5 auth
  btest -s -a admin -p password --ecsrp5

Protocol: [len][payload] framing (no 0x06 handler, unlike Winbox)
Crypto: Curve25519 in Weierstrass form, SHA256, SRP key exchange

Based on MarginResearch/mikrotik_authentication (Apache 2.0).
Verified against MikroTik RouterOS 7.x via MITM protocol analysis.

34 tests (10 unit, 6 EC-SRP5 integration, 8 base integration, 10 doc-tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-31 16:56:38 +04:00
parent 8fe4e72bb3
commit 58274da859
15 changed files with 1303 additions and 38 deletions

View File

@@ -26,9 +26,27 @@ pub async fn run_server(
port: u16,
auth_user: Option<String>,
auth_pass: Option<String>,
use_ecsrp5: bool,
) -> Result<()> {
let addr = format!("0.0.0.0:{}", port);
let listener = TcpListener::bind(&addr).await?;
// Pre-derive EC-SRP5 credentials if enabled
let ecsrp5_creds = if use_ecsrp5 {
match (auth_user.as_deref(), auth_pass.as_deref()) {
(Some(user), Some(pass)) => {
tracing::info!("EC-SRP5 authentication enabled for user '{}'", user);
Some(Arc::new(crate::ecsrp5::EcSrp5Credentials::derive(user, pass)))
}
_ => {
tracing::warn!("--ecsrp5 requires -a and -p to be set");
None
}
}
} else {
None
};
tracing::info!("btest server listening on {}", addr);
let udp_port_offset = Arc::new(std::sync::atomic::AtomicU16::new(0));
@@ -42,10 +60,11 @@ pub async fn run_server(
let auth_pass = auth_pass.clone();
let udp_offset = udp_port_offset.clone();
let sessions = sessions.clone();
let ecsrp5 = ecsrp5_creds.clone();
tokio::spawn(async move {
if let Err(e) =
handle_client(stream, peer, auth_user, auth_pass, udp_offset, sessions).await
handle_client(stream, peer, auth_user, auth_pass, udp_offset, sessions, ecsrp5).await
{
tracing::error!("Client {} error: {}", peer, e);
}
@@ -60,6 +79,7 @@ async fn handle_client(
auth_pass: Option<String>,
udp_port_offset: Arc<std::sync::atomic::AtomicU16>,
sessions: SessionMap,
ecsrp5_creds: Option<Arc<crate::ecsrp5::EcSrp5Credentials>>,
) -> Result<()> {
stream.set_nodelay(true)?;
@@ -182,13 +202,33 @@ async fn handle_client(
}
// Primary connection auth
auth::server_authenticate(
&mut stream,
auth_user.as_deref(),
auth_pass.as_deref(),
&ok_response,
)
.await?;
if let Some(ref creds) = ecsrp5_creds {
// EC-SRP5 authentication
let auth_resp: [u8; 4] = [0x03, 0x00, 0x00, 0x00];
stream.write_all(&auth_resp).await?;
stream.flush().await?;
crate::ecsrp5::server_authenticate(
&mut stream,
auth_user.as_deref().unwrap_or("admin"),
auth_pass.as_deref().unwrap_or(""),
creds,
)
.await?;
// Send auth OK (with session token if multi-conn)
stream.write_all(&ok_response).await?;
stream.flush().await?;
} else {
// MD5 or no auth
auth::server_authenticate(
&mut stream,
auth_user.as_deref(),
auth_pass.as_deref(),
&ok_response,
)
.await?;
}
if cmd.is_udp() {
run_udp_test_server(&mut stream, peer, &cmd, udp_port_offset).await