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>
139 lines
3.9 KiB
Python
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
|