161 lines
4.5 KiB
Python
161 lines
4.5 KiB
Python
|
|
# secp256k1_pure_python.py
|
|
# Pure Python scalar->public (affine x,y) for secp256k1
|
|
# No external libs required.
|
|
|
|
# Curve parameters for secp256k1
|
|
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
|
A = 0 # a = 0 for secp256k1
|
|
B = 7
|
|
# Base point G
|
|
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
|
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
|
|
# Order of base point
|
|
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
|
|
# ---------- Field helpers ----------
|
|
def inv_mod(x, p=P):
|
|
"""Multiplicative inverse modulo p (Python built-in pow with -1 not supported)"""
|
|
return pow(x, p - 2, p)
|
|
|
|
# ---------- Jacobian coordinates helpers ----------
|
|
# Represent point as tuple (X, Y, Z) meaning affine (X/Z^2, Y/Z^3).
|
|
INFINITY = (0, 1, 0) # Z == 0 means point at infinity in these helpers
|
|
|
|
def is_infinity(Pj):
|
|
return Pj[2] == 0
|
|
|
|
def to_jacobian(x, y):
|
|
return (x % P, y % P, 1)
|
|
|
|
def from_jacobian(Pj):
|
|
X, Y, Z = Pj
|
|
if Z == 0:
|
|
return None # infinity
|
|
Z_inv = inv_mod(Z)
|
|
Z_inv2 = (Z_inv * Z_inv) % P
|
|
Z_inv3 = (Z_inv2 * Z_inv) % P
|
|
x = (X * Z_inv2) % P
|
|
y = (Y * Z_inv3) % P
|
|
return (x, y)
|
|
|
|
# Point doubling in Jacobian coords
|
|
def jacobian_double(Pj):
|
|
X1, Y1, Z1 = Pj
|
|
if Z1 == 0 or Y1 == 0:
|
|
return INFINITY
|
|
# Formula for a = 0
|
|
S = (4 * X1 * pow(Y1, 2, P)) % P
|
|
M = (3 * pow(X1, 2, P)) % P # since a=0
|
|
X3 = (pow(M, 2, P) - 2 * S) % P
|
|
Y3 = (M * (S - X3) - 8 * pow(Y1, 4, P)) % P
|
|
Z3 = (2 * Y1 * Z1) % P
|
|
return (X3, Y3, Z3)
|
|
|
|
# Mixed addition: add Jacobian Pj and affine Q=(x2,y2)
|
|
def jacobian_add_affine(Pj, Qx, Qy):
|
|
X1, Y1, Z1 = Pj
|
|
if Z1 == 0:
|
|
return to_jacobian(Qx, Qy)
|
|
if Y1 == 0 and X1 == 0 and Z1 == 0:
|
|
return to_jacobian(Qx, Qy)
|
|
|
|
Z1z = (Z1 * Z1) % P
|
|
U2 = (Qx * Z1z) % P
|
|
S2 = (Qy * Z1z * Z1) % P # Qy * Z1^3
|
|
|
|
H = (U2 - X1) % P
|
|
r = (S2 - Y1) % P
|
|
|
|
if H == 0:
|
|
if r == 0:
|
|
return jacobian_double(Pj)
|
|
else:
|
|
return INFINITY
|
|
|
|
HH = (H * H) % P
|
|
HHH = (H * HH) % P
|
|
V = (X1 * HH) % P
|
|
|
|
X3 = (r * r - HHH - 2 * V) % P
|
|
Y3 = (r * (V - X3) - Y1 * HHH) % P
|
|
Z3 = (Z1 * HH) % P
|
|
return (X3, Y3, Z3)
|
|
|
|
# ---------- Scalar multiplication ----------
|
|
def scalar_mult(k, Px=Gx, Py=Gy):
|
|
"""
|
|
Multiply base point (Px,Py) by scalar k.
|
|
Returns affine (x,y) or None for infinity.
|
|
k should be integer >=0.
|
|
"""
|
|
if k % N == 0:
|
|
return None
|
|
if k < 0:
|
|
# use -P
|
|
return scalar_mult(-k, Px, (-Py) % P)
|
|
|
|
# Ensure scalar reduced mod N and >0
|
|
k = k % N
|
|
if k == 0:
|
|
return None
|
|
|
|
# Initialize result as infinity (Jacobian)
|
|
R = INFINITY
|
|
# Take base point as affine (we'll use mixed add)
|
|
Px = Px % P
|
|
Py = Py % P
|
|
|
|
# Left-to-right binary method
|
|
bits = k.bit_length()
|
|
for i in range(bits - 1, -1, -1):
|
|
# R = 2*R
|
|
R = jacobian_double(R)
|
|
if (k >> i) & 1:
|
|
# R = R + P
|
|
R = jacobian_add_affine(R, Px, Py)
|
|
# Convert R to affine
|
|
return from_jacobian(R)
|
|
|
|
# ---------- Utility outputs ----------
|
|
def pubkey_bytes_compressed(x, y):
|
|
prefix = b'\x02' if (y % 2 == 0) else b'\x03'
|
|
xb = x.to_bytes(32, 'big')
|
|
return prefix + xb
|
|
|
|
def pubkey_bytes_uncompressed(x, y):
|
|
return b'\x04' + x.to_bytes(32, 'big') + y.to_bytes(32, 'big')
|
|
|
|
# ---------- Public function as user asked ----------
|
|
def priv_to_pub(priv_int):
|
|
"""
|
|
Input: priv_int (Python int) — private key (assumed 0 < priv < N but function reduces)
|
|
Output: (x, y) tuple of Python ints (affine coordinates). Returns None if point at infinity (shouldn't for valid keys).
|
|
"""
|
|
if not isinstance(priv_int, int):
|
|
raise TypeError("priv must be int")
|
|
k = priv_int % N
|
|
if k == 0:
|
|
raise ValueError("private key is zero modulo curve order")
|
|
pt = scalar_mult(k, Gx, Gy)
|
|
if pt is None:
|
|
raise RuntimeError("Result is point at infinity (shouldn't happen for valid priv)")
|
|
return pt
|
|
|
|
# ---------- Example / quick test ----------
|
|
if __name__ == "__main__":
|
|
# Test vector: priv = 1 -> pub = G
|
|
priv = 1
|
|
x, y = priv_to_pub(priv)
|
|
assert x == Gx and y == Gy
|
|
print("priv=1 -> pub == G : OK")
|
|
|
|
# Test with a random private (example)
|
|
import secrets
|
|
priv = secrets.randbelow(N - 1) + 1
|
|
x, y = priv_to_pub(priv)
|
|
print("priv (hex):", hex(priv))
|
|
print("pub x:", hex(x))
|
|
print("pub y:", hex(y))
|
|
print("compressed pubkey:", pubkey_bytes_compressed(x, y).hex())
|