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>
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:
- Use
num-bigintfor direct field arithmetic (like the Pythonecdsalibrary) - Use the
p256crate's infrastructure with custom curve parameters - Port the Python
WCurveclass directly using big integers
Implementation Steps
- Port
WCurve— Weierstrass curve with Curve25519 parameters, point multiplication,lift_x,redp1, Montgomery conversion - Port EC-SRP5 client — generate keypair, compute shared secret, confirmation codes
- Port EC-SRP5 server — verify client proof, generate server proof (for our server mode)
- Integrate into
auth.rs— handle03 00 00 00response with btest-specific[len][payload]framing - 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:
- Store
saltandx_gamma(validator public key) per user — derived from username + password at startup - Generate ephemeral server keypair
- Compute password-entangled public key:
W_b = s_b * G + redp1(x_gamma, 0) - Verify client's confirmation code
- 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](no0x06handler byte)