Add EC-SRP5 authentication (RouterOS >= 6.43)
All checks were successful
CI / test (push) Successful in 1m18s
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:
@@ -50,12 +50,20 @@ sequenceDiagram
|
||||
|
||||
alt No auth configured
|
||||
SRV->>TCP: AUTH_OK [01 00 00 00]
|
||||
else MD5 auth
|
||||
else MD5 auth (RouterOS < 6.43)
|
||||
SRV->>TCP: AUTH_REQUIRED [02 00 00 00]
|
||||
SRV->>TCP: Challenge [16 random bytes]
|
||||
MK->>TCP: Response [16 hash + 32 username]
|
||||
Note over SRV: Verify MD5(pass + MD5(pass + challenge))
|
||||
SRV->>TCP: AUTH_OK or AUTH_FAILED
|
||||
else EC-SRP5 auth (RouterOS >= 6.43, --ecsrp5 flag)
|
||||
SRV->>TCP: EC-SRP5 [03 00 00 00]
|
||||
MK->>TCP: [len][username\0][client_pubkey:32][parity:1]
|
||||
SRV->>TCP: [len][server_pubkey:32][parity:1][salt:16]
|
||||
MK->>TCP: [len][client_proof:32]
|
||||
SRV->>TCP: [len][server_proof:32]
|
||||
Note over SRV: Curve25519 Weierstrass EC-SRP5<br/>See docs/ecsrp5-research.md
|
||||
SRV->>TCP: AUTH_OK [01 00 00 00]
|
||||
end
|
||||
|
||||
alt TCP mode
|
||||
@@ -179,6 +187,7 @@ btest-rs/
|
||||
│ ├── lib.rs # Public API (used by integration tests)
|
||||
│ ├── protocol.rs # Wire format: Command, StatusMessage, constants
|
||||
│ ├── auth.rs # MD5 challenge-response authentication
|
||||
│ ├── ecsrp5.rs # EC-SRP5 authentication (Curve25519 Weierstrass)
|
||||
│ ├── server.rs # Server mode: listener, TCP/UDP handlers
|
||||
│ ├── client.rs # Client mode: connector, TCP/UDP handlers
|
||||
│ └── bandwidth.rs # Rate limiting, formatting, shared state
|
||||
|
||||
238
docs/ecsrp5-research.md
Normal file
238
docs/ecsrp5-research.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 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](https://github.com/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`)
|
||||
|
||||
```mermaid
|
||||
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)
|
||||
|
||||
```python
|
||||
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](https://github.com/MarginResearch/mikrotik_authentication)** — Reverse-engineered MikroTik's EC-SRP5 for Winbox/MAC Telnet
|
||||
- **[Margin Research blog](https://margin.re/2022/02/mikrotik-authentication-revealed/)** — Detailed write-up of MikroTik authentication
|
||||
- **btest framing discovery** — MITM analysis showing btest uses `[len][payload]` (no `0x06` handler byte)
|
||||
Reference in New Issue
Block a user