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>
This commit is contained in:
138
proto-test/elliptic_curves.py
Normal file
138
proto-test/elliptic_curves.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user