Files
btest-rs/docs/ecsrp5-research.md
Siavash Sameni 58274da859
All checks were successful
CI / test (push) Successful in 1m18s
Add EC-SRP5 authentication (RouterOS >= 6.43)
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>
2026-03-31 16:56:38 +04:00

7.9 KiB

EC-SRP5 Authentication Research

Summary

MikroTik RouterOS >= 6.43 uses EC-SRP5 (Elliptic Curve Secure Remote Password) for authentication. When the btest server has auth enabled, it responds with 03 00 00 00 instead of 02 00 00 00 (legacy MD5).

Status: Fully reverse-engineered and verified. Python prototype authenticates successfully against MikroTik RouterOS 7.x btest server.

Discovery Process

Step 1: Initial Capture

Connected our client to MikroTik btest server with auth enabled. Server responded with 03 00 00 00 and waited for the client to initiate.

Step 2: Winbox EC-SRP5 Verification

Tested the EC-SRP5 crypto implementation (from MarginResearch/mikrotik_authentication) against MikroTik's Winbox port (8291). Authentication succeeded, confirming the elliptic curve math is correct.

Step 3: Framing Discovery via MITM

The Winbox [len][0x06][payload] framing was rejected by the btest port. To discover the correct framing, we built a MITM proxy (proto-test/btest_mitm.py) and routed a MikroTik client through it to the MikroTik server.

Finding: btest uses [len][payload] — identical to Winbox but without the 0x06 handler byte.

Step 4: Successful Authentication

Updated the Python prototype to use [len][payload] framing. EC-SRP5 authentication against MikroTik's btest server succeeded and data transfer began.

Protocol Specification

Auth Trigger

After the standard btest handshake (HELLO + Command), the server responds:

01 00 00 00  →  No auth required
02 00 00 00  →  MD5 challenge-response (RouterOS < 6.43)
03 00 00 00  →  EC-SRP5 (RouterOS >= 6.43)

EC-SRP5 Handshake (4 messages after 03 00 00 00)

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over S: Server sent 03 00 00 00

    C->>S: MSG1: [len][username\0][client_pubkey:32][parity:1]
    Note over C: len = 1 byte, total = len + 1 bytes

    S->>C: MSG2: [len][server_pubkey:32][parity:1][salt:16]
    Note over S: len = 49 (0x31)

    C->>S: MSG3: [len][client_confirmation:32]
    Note over C: len = 32 (0x20)

    S->>C: MSG4: [len][server_confirmation:32]
    Note over S: len = 32 (0x20)

    Note over S: Then continues with normal btest flow:
    S->>C: AUTH_OK [01 00 00 00]
    S->>C: UDP port [2 bytes BE] (if UDP mode)

Framing Comparison

Protocol Message framing
Winbox (port 8291) [len:1][0x06][payload]
btest (port 2000) [len:1][payload]
MAC Telnet (UDP 20561) Control packets with magic bytes

The 0x06 handler byte in Winbox identifies the message as an auth message. Btest omits it since the auth context is implicit after 03 00 00 00.

Captured Exchange (from MITM)

CLIENT → SERVER (40 bytes):
  27 61 6e 74 61 72 00 38 8a 37 36 52 6a 32 e9 87   'antar.8.76Rj2..
  4e 92 f8 c3 aa a1 18 da cd 71 b6 ab 76 fd 72 aa   N........q..v.r.
  c3 f6 6a 43 9b c8 a1 01                             ..jC....

  Decoded:
    len=0x27 (39 bytes payload)
    username="antar\0"
    pubkey=388a373652...c8a1 (32 bytes)
    parity=0x01

SERVER → CLIENT (50 bytes):
  31 6c c9 e3 1a 79 43 4a 40 51 de fd 55 cc 8d 6d   1l...yCJ@Q..U..m
  3c ec cd 73 19 1f a6 83 15 94 62 52 97 fe 5d 89   <..s......bR..].
  1a 00 3c ec 65 b8 34 28 0a 16 c5 48 0d 7b 50 00   ..<.e.4(...H.{P.
  e3 80                                               ..

  Decoded:
    len=0x31 (49 bytes payload)
    server_pubkey=6cc9e31a...5d891a (32 bytes)
    parity=0x00
    salt=3cec65b834280a16c5480d7b5000e380 (16 bytes)

CLIENT → SERVER (33 bytes):
  20 9b 1f 74 ec 40 31 2c ...

  Decoded:
    len=0x20 (32 bytes payload)
    client_cc=9b1f74ec... (32 bytes, SHA256 proof)

SERVER → CLIENT (33 bytes):
  20 7d 59 b3 2e 28 6e 52 ...

  Decoded:
    len=0x20 (32 bytes payload)
    server_cc=7d59b32e... (32 bytes, SHA256 proof)

POST-AUTH:
  01 00 00 00 07 fa

  Decoded:
    AUTH_OK=01000000
    UDP_port=0x07fa (2042)

Cryptographic Details

Elliptic Curve: Curve25519 in Weierstrass Form

p  = 2^255 - 19
r  = curve order (same as Ed25519)
Montgomery A = 486662

Weierstrass conversion:
  a = 0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144
  b = 0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864

Generator: lift_x(9) in Montgomery, converted to Weierstrass
Cofactor: 8

All EC math is in Weierstrass form. Public keys are transmitted as Montgomery x-coordinates (32 bytes big-endian) plus a 1-byte y-parity flag.

Key Derivation

inner = SHA256(username + ":" + password)
validator_priv (i) = SHA256(salt || inner)
validator_pub (x_gamma) = i * G

Shared Secret Computation

Client side (ECPESVDP-SRP-A):

v = redp1(x_gamma, parity=1)         # hash-to-curve of validator pubkey
w_b = lift_x(server_pubkey) + v      # undo verifier blinding
j = SHA256(client_pubkey || server_pubkey)
scalar = (i * j + s_a) mod r         # combined scalar
Z = scalar * w_b                     # shared secret point
z = to_montgomery(Z).x              # Montgomery x-coordinate

Server side (ECPESVDP-SRP-B):

gamma = redp1(x_gamma, parity=0)
w_a = lift_x(client_pubkey)
Z = s_b * (w_a + j * gamma)         # where j = SHA256(x_w_a || x_w_b)
z = to_montgomery(Z).x

Confirmation Codes

client_cc = SHA256(j || z)
server_cc = SHA256(j || client_cc || z)

Both sides verify the peer's confirmation code to ensure the shared secret matches.

redp1 (Hash-to-Curve)

def redp1(x_bytes, parity):
    x = SHA256(x_bytes)
    while True:
        x2 = SHA256(x)
        point = lift_x(int(x2), parity)
        if point is valid:
            return point
        x = (int(x) + 1).to_bytes(32)

Implementation Plan for Rust

Required Crates

Crate Purpose
num-bigint + num-traits Big integer arithmetic for field operations
sha2 SHA-256
ecdsa or custom Curve25519 Weierstrass point operations

Note: curve25519-dalek operates in Montgomery/Edwards form, not Weierstrass. We need Weierstrass arithmetic for compatibility with MikroTik's implementation. Options:

  1. Use num-bigint for direct field arithmetic (like the Python ecdsa library)
  2. Use the p256 crate's infrastructure with custom curve parameters
  3. Port the Python WCurve class directly using big integers

Implementation Steps

  1. Port WCurve — Weierstrass curve with Curve25519 parameters, point multiplication, lift_x, redp1, Montgomery conversion
  2. Port EC-SRP5 client — generate keypair, compute shared secret, confirmation codes
  3. Port EC-SRP5 server — verify client proof, generate server proof (for our server mode)
  4. Integrate into auth.rs — handle 03 00 00 00 response with btest-specific [len][payload] framing
  5. Server registration — derive salt + validator from username/password for server-side verification

Server-Side Specifics

When our server receives a client with EC-SRP5 support, we need to:

  1. Store salt and x_gamma (validator public key) per user — derived from username + password at startup
  2. Generate ephemeral server keypair
  3. Compute password-entangled public key: W_b = s_b * G + redp1(x_gamma, 0)
  4. Verify client's confirmation code
  5. Send server confirmation code

Files

File Purpose
proto-test/elliptic_curves.py Curve25519 Weierstrass implementation
proto-test/btest_ecsrp5_client.py Working Python btest EC-SRP5 client
proto-test/btest_mitm.py MITM proxy for protocol analysis

Credits

  • MarginResearch — Reverse-engineered MikroTik's EC-SRP5 for Winbox/MAC Telnet
  • Margin Research blog — Detailed write-up of MikroTik authentication
  • btest framing discovery — MITM analysis showing btest uses [len][payload] (no 0x06 handler byte)