diff --git a/.gitignore b/.gitignore
index 911faa0..40c191c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
btest_original
.claude/
.env
+proto-test/venv/
+proto-test/__pycache__/
diff --git a/docs/architecture.md b/docs/architecture.md
index 56c1ebd..f13766c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -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)
+ 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
See docs/ecsrp5-research.md
+ SRV->>TCP: AUTH_OK [01 00 00 00]
end
alt TCP mode
@@ -178,7 +186,7 @@ btest-rs/
│ ├── main.rs # CLI entry point, argument parsing
│ ├── lib.rs # Public API (used by integration tests)
│ ├── protocol.rs # Wire format: Command, StatusMessage, constants
-│ ├── auth.rs # MD5 challenge-response authentication
+│ ├── auth.rs # Authentication (MD5 + EC-SRP5)
│ ├── server.rs # Server mode: listener, TCP/UDP handlers
│ ├── client.rs # Client mode: connector, TCP/UDP handlers
│ └── bandwidth.rs # Rate limiting, formatting, shared state
@@ -193,6 +201,7 @@ btest-rs/
├── docs/
│ ├── architecture.md # This file
│ ├── protocol.md # Protocol specification
+│ ├── ecsrp5-research.md # EC-SRP5 reverse-engineering findings
│ ├── user-guide.md # Usage documentation
│ └── docker.md # Docker & deployment guide
├── Dockerfile # Production Docker image
diff --git a/docs/ecsrp5-research.md b/docs/ecsrp5-research.md
new file mode 100644
index 0000000..c9a6708
--- /dev/null
+++ b/docs/ecsrp5-research.md
@@ -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)
diff --git a/proto-test/btest_ecsrp5_client.py b/proto-test/btest_ecsrp5_client.py
new file mode 100644
index 0000000..d93ce4f
--- /dev/null
+++ b/proto-test/btest_ecsrp5_client.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+"""
+btest EC-SRP5 authentication test client.
+Connects to a MikroTik btest server (RouterOS >= 6.43) and performs
+the EC-SRP5 handshake to authenticate.
+
+Usage:
+ python3 btest_ecsrp5_client.py -a 172.16.81.1 -u admin -p password
+ python3 btest_ecsrp5_client.py -a 172.16.81.1 -u admin -p password --receive
+"""
+import socket
+import secrets
+import hashlib
+import argparse
+import struct
+import sys
+import time
+
+import elliptic_curves
+
+BTEST_PORT = 2000
+
+
+def sha256(data: bytes) -> bytes:
+ return hashlib.sha256(data).digest()
+
+
+def hexdump(label: str, data: bytes):
+ print(f" {label} ({len(data)} bytes): {data.hex()}")
+
+
+class BtestECSRP5Client:
+ def __init__(self, host: str, port: int = BTEST_PORT):
+ self.host = host
+ self.port = port
+ self.w = elliptic_curves.WCurve()
+ self.sock = None
+
+ def connect(self):
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.sock.settimeout(10)
+ self.sock.connect((self.host, self.port))
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ print(f"Connected to {self.host}:{self.port}")
+
+ def recv_exact(self, n: int) -> bytes:
+ buf = b""
+ while len(buf) < n:
+ chunk = self.sock.recv(n - len(buf))
+ if not chunk:
+ raise ConnectionError("Connection closed")
+ buf += chunk
+ return buf
+
+ def do_hello_and_command(self, proto=1, direction=2, conn_count=0, tx_size=0x8000):
+ """Send command, return auth response type."""
+ # Receive HELLO
+ hello = self.recv_exact(4)
+ hexdump("HELLO", hello)
+ assert hello == b"\x01\x00\x00\x00", f"Bad HELLO: {hello.hex()}"
+
+ # Send command
+ cmd = struct.pack("= 6.43)")
+ return self.do_ecsrp5_auth(username, password)
+ else:
+ print(f"Unknown auth response: {resp.hex()}")
+ return False
+
+
+def main():
+ parser = argparse.ArgumentParser(description="btest EC-SRP5 auth test client")
+ parser.add_argument("-a", "--address", required=True, help="MikroTik IP address")
+ parser.add_argument("-u", "--username", default="admin", help="Username")
+ parser.add_argument("-p", "--password", default="", help="Password")
+ parser.add_argument("-P", "--port", type=int, default=BTEST_PORT, help="Port")
+ parser.add_argument("--receive", action="store_true", help="Test receive direction")
+ args = parser.parse_args()
+
+ direction = "receive" if args.receive else "receive"
+
+ client = BtestECSRP5Client(args.address, args.port)
+ try:
+ success = client.run_test(args.username, args.password, direction)
+ if success:
+ print("\nAuth passed! Reading some data...")
+ try:
+ data = client.sock.recv(4096)
+ hexdump("First data received", data[:64])
+ print(f" (total {len(data)} bytes)")
+
+ # Read for a few seconds to confirm data flows
+ client.sock.settimeout(2)
+ total = len(data)
+ for _ in range(5):
+ try:
+ data = client.sock.recv(65536)
+ total += len(data)
+ except socket.timeout:
+ break
+ print(f"\nTotal received: {total} bytes ({total * 8 / 1_000_000:.2f} Mbps over ~2s)")
+ except Exception as e:
+ print(f"Data read error: {e}")
+ sys.exit(0 if success else 1)
+ except Exception as e:
+ print(f"Error: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/proto-test/btest_mitm.py b/proto-test/btest_mitm.py
new file mode 100644
index 0000000..44ac107
--- /dev/null
+++ b/proto-test/btest_mitm.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+"""
+MITM proxy for btest protocol.
+Sits between MikroTik client and MikroTik server, logs all bytes exchanged.
+
+Usage:
+ python3 btest_mitm.py --listen 2000 --target 172.16.81.1:2000
+
+Then point MikroTik client at THIS machine's IP on port 2000.
+"""
+import socket
+import select
+import sys
+import argparse
+import time
+import threading
+
+
+def hexdump_line(data, offset=0):
+ """Format a single line of hex dump."""
+ hex_part = " ".join(f"{b:02x}" for b in data)
+ ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in data)
+ return f" {offset:04x} {hex_part:<48s} {ascii_part}"
+
+
+def hexdump(label, data):
+ """Pretty hex dump with ASCII."""
+ ts = time.strftime("%H:%M:%S", time.localtime())
+ print(f"\n[{ts}] {label} ({len(data)} bytes)")
+ for i in range(0, len(data), 16):
+ print(hexdump_line(data[i : i + 16], i))
+ sys.stdout.flush()
+
+
+def annotate_btest(direction, data, state):
+ """Try to annotate known btest protocol messages."""
+ if len(data) == 4:
+ val = data.hex()
+ if val == "01000000":
+ return "HELLO / AUTH_OK"
+ elif val == "02000000":
+ return "AUTH_REQUIRED (MD5)"
+ elif val == "03000000":
+ state["auth_type"] = "ecsrp5"
+ return "AUTH_REQUIRED (EC-SRP5)"
+ elif val == "00000000":
+ return "AUTH_FAILED"
+
+ if len(data) == 16 and state.get("stage") == "command":
+ proto = "UDP" if data[0] == 0 else "TCP"
+ dirs = {1: "RX", 2: "TX", 3: "BOTH"}
+ d = dirs.get(data[1], f"0x{data[1]:02x}")
+ conn = data[3]
+ return f"COMMAND: proto={proto} dir={d} conn_count={conn}"
+
+ if len(data) == 2 and state.get("auth_type") == "ecsrp5":
+ return f"UDP port assignment: {int.from_bytes(data, 'big')}"
+
+ if state.get("auth_type") == "ecsrp5" and state.get("stage") == "ecsrp5":
+ if len(data) >= 33:
+ return f"EC-SRP5 message (likely pubkey + parity/salt)"
+
+ return None
+
+
+def proxy_connection(client_sock, client_addr, target_host, target_port):
+ """Handle one proxied connection."""
+ print(f"\n{'='*60}")
+ print(f"New connection from {client_addr[0]}:{client_addr[1]}")
+ print(f"Proxying to {target_host}:{target_port}")
+ print(f"{'='*60}")
+
+ try:
+ server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server_sock.settimeout(30)
+ server_sock.connect((target_host, target_port))
+ server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ except Exception as e:
+ print(f"Failed to connect to target: {e}")
+ client_sock.close()
+ return
+
+ state = {"stage": "hello", "msg_count": 0}
+
+ try:
+ while True:
+ readable, _, _ = select.select(
+ [client_sock, server_sock], [], [], 30
+ )
+
+ if not readable:
+ print("\n[TIMEOUT] No data for 30 seconds")
+ break
+
+ for sock in readable:
+ if sock is server_sock:
+ data = server_sock.recv(65536)
+ if not data:
+ print("\n[SERVER CLOSED]")
+ return
+ direction = "SERVER → CLIENT"
+ annotation = annotate_btest("s2c", data, state)
+ hexdump(direction, data)
+ if annotation:
+ print(f" >>> {annotation}")
+
+ # Track state
+ if state["stage"] == "hello" and len(data) == 4:
+ state["stage"] = "command"
+ elif state["stage"] == "auth_resp":
+ state["stage"] = "ecsrp5"
+
+ # Forward to client
+ client_sock.sendall(data)
+
+ elif sock is client_sock:
+ data = client_sock.recv(65536)
+ if not data:
+ print("\n[CLIENT CLOSED]")
+ return
+ direction = "CLIENT → SERVER"
+ annotation = annotate_btest("c2s", data, state)
+ hexdump(direction, data)
+ if annotation:
+ print(f" >>> {annotation}")
+
+ # Track state
+ if state["stage"] == "command" and len(data) == 16:
+ state["stage"] = "auth_resp"
+ state["msg_count"] += 1
+
+ # Forward to server
+ server_sock.sendall(data)
+
+ except (ConnectionResetError, BrokenPipeError) as e:
+ print(f"\n[CONNECTION ERROR] {e}")
+ except Exception as e:
+ print(f"\n[ERROR] {e}")
+ import traceback
+ traceback.print_exc()
+ finally:
+ client_sock.close()
+ server_sock.close()
+ print(f"\n{'='*60}")
+ print(f"Connection closed ({state['msg_count']} client messages)")
+ print(f"{'='*60}")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="btest MITM proxy")
+ parser.add_argument(
+ "-l", "--listen", type=int, default=2000, help="Listen port (default: 2000)"
+ )
+ parser.add_argument(
+ "-t",
+ "--target",
+ required=True,
+ help="Target MikroTik address:port (e.g., 172.16.81.1:2000)",
+ )
+ args = parser.parse_args()
+
+ target_parts = args.target.split(":")
+ target_host = target_parts[0]
+ target_port = int(target_parts[1]) if len(target_parts) > 1 else 2000
+
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listener.bind(("0.0.0.0", args.listen))
+ listener.listen(5)
+
+ print(f"MITM proxy listening on 0.0.0.0:{args.listen}")
+ print(f"Forwarding to {target_host}:{target_port}")
+ print(f"Point MikroTik client at this machine's IP, port {args.listen}")
+ print()
+
+ while True:
+ client_sock, client_addr = listener.accept()
+ client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ t = threading.Thread(
+ target=proxy_connection,
+ args=(client_sock, client_addr, target_host, target_port),
+ daemon=True,
+ )
+ t.start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/proto-test/elliptic_curves.py b/proto-test/elliptic_curves.py
new file mode 100644
index 0000000..05e7ff1
--- /dev/null
+++ b/proto-test/elliptic_curves.py
@@ -0,0 +1,138 @@
+"""
+Elliptic curve implementation for MikroTik EC-SRP5 authentication.
+Based on MarginResearch/mikrotik_authentication (MIT License).
+Curve25519 in Weierstrass form.
+"""
+import hashlib
+import ecdsa
+
+
+def _egcd(a, b):
+ if a == 0:
+ return (b, 0, 1)
+ else:
+ g, y, x = _egcd(b % a, a)
+ return (g, x - (b // a) * y, y)
+
+
+def _modinv(a: int, p: int):
+ if a < 0:
+ a = a % p
+ g, x, y = _egcd(a, p)
+ if g != 1:
+ raise Exception("modular inverse does not exist")
+ return x % p
+
+
+def _legendre_symbol(a: int, p: int):
+ l = pow(a, (p - 1) // 2, p)
+ if l == p - 1:
+ return -1
+ return l
+
+
+def _prime_mod_sqrt(a: int, p: int):
+ a %= p
+ if a == 0:
+ return [0]
+ if p == 2:
+ return [a]
+ if _legendre_symbol(a, p) != 1:
+ return []
+ if p % 4 == 3:
+ x = pow(a, (p + 1) // 4, p)
+ return [x, p - x]
+
+ q, s = p - 1, 0
+ while q % 2 == 0:
+ s += 1
+ q //= 2
+
+ z = 1
+ while _legendre_symbol(z, p) != -1:
+ z += 1
+ c = pow(z, q, p)
+
+ x = pow(a, (q + 1) // 2, p)
+ t = pow(a, q, p)
+ m = s
+ while t != 1:
+ i, e = 0, 2
+ for i in range(1, m):
+ if pow(t, e, p) == 1:
+ break
+ e *= 2
+ b = pow(c, 2 ** (m - i - 1), p)
+ x = (x * b) % p
+ t = (t * b * b) % p
+ c = (b * b) % p
+ m = i
+
+ return [x, p - x]
+
+
+class WCurve:
+ def __init__(self):
+ self.__p = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED
+ self.__r = 0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED
+ self.__mont_a = 486662
+ self.__conversion_from_m = self.__mont_a * _modinv(3, self.__p) % self.__p
+ self.__conversion = (self.__p - self.__mont_a * _modinv(3, self.__p)) % self.__p
+ self.__a = 0x2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144
+ self.__b = 0x7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864
+ self.__h = 8
+ self.__curve = ecdsa.ellipticcurve.CurveFp(self.__p, self.__a, self.__b, self.__h)
+ self.__g = self.lift_x(9, 0)
+
+ def gen_public_key(self, priv: bytes):
+ assert len(priv) == 32
+ priv = int.from_bytes(priv, "big")
+ pt = priv * self.__g
+ return self.to_montgomery(pt)
+
+ def to_montgomery(self, pt):
+ x = (pt.x() + self.__conversion) % self.__p
+ return int(x).to_bytes(32, "big"), pt.y() & 1
+
+ def lift_x(self, x: int, parity: bool):
+ x = x % self.__p
+ y_squared = (x**3 + self.__mont_a * x**2 + x) % self.__p
+ x += self.__conversion_from_m
+ x %= self.__p
+ ys = _prime_mod_sqrt(y_squared, self.__p)
+ if ys != []:
+ pt1 = ecdsa.ellipticcurve.PointJacobi(self.__curve, x, ys[0], 1, self.__r)
+ pt2 = ecdsa.ellipticcurve.PointJacobi(self.__curve, x, ys[1], 1, self.__r)
+ if pt1.y() & 1 == 1 and parity != 0:
+ return pt1
+ elif pt2.y() & 1 == 1 and parity != 0:
+ return pt2
+ elif pt1.y() & 1 == 0 and parity == 0:
+ return pt1
+ else:
+ return pt2
+ else:
+ return -1
+
+ def redp1(self, x: bytes, parity: bool):
+ x = hashlib.sha256(x).digest()
+ while True:
+ x2 = hashlib.sha256(x).digest()
+ pt = self.lift_x(int.from_bytes(x2, "big"), parity)
+ if pt == -1:
+ x = (int.from_bytes(x, "big") + 1).to_bytes(32, "big")
+ else:
+ break
+ return pt
+
+ def gen_password_validator_priv(self, username: str, password: str, salt: bytes):
+ assert len(salt) == 0x10
+ return hashlib.sha256(
+ salt + hashlib.sha256((username + ":" + password).encode("utf-8")).digest()
+ ).digest()
+
+ def multiply_by_g(self, a: int):
+ return a * self.__g
+
+ def finite_field_value(self, a: int):
+ return a % self.__r