Files
btest-rs/proto-test/elliptic_curves.py
Siavash Sameni afe389ce7e Research: EC-SRP5 authentication fully reverse-engineered
Key findings:
- btest EC-SRP5 uses [len][payload] framing (NO 0x06 handler byte)
- Winbox uses [len][0x06][payload] — that one byte was the difference
- Crypto is identical: Curve25519 Weierstrass, SHA256, SRP-like key exchange
- Python prototype successfully authenticates against MikroTik RouterOS 7.x

Files:
- docs/ecsrp5-research.md: complete protocol spec, captured exchange, impl plan
- proto-test/btest_ecsrp5_client.py: working Python EC-SRP5 btest client
- proto-test/btest_mitm.py: MITM proxy used to discover the framing
- proto-test/elliptic_curves.py: Curve25519 Weierstrass (from MarginResearch)

Based on MarginResearch/mikrotik_authentication (MIT License).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:33:07 +04:00

139 lines
3.9 KiB
Python

"""
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