From dc836861fd9aa9dba0a023a11dc97d69582c6afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=B6ller?= <nisse@lysator.liu.se> Date: Fri, 6 Feb 2015 20:22:56 +0100 Subject: [PATCH] Added reference implementation --- .gitignore | 1 + Makefile | 3 + src/.gitignore | 2 + src/Makefile | 5 ++ src/ed25519-test.py | 57 ++++++++++++++++ src/ed25519.py | 159 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 .gitignore create mode 100644 src/.gitignore create mode 100644 src/Makefile create mode 100644 src/ed25519-test.py create mode 100644 src/ed25519.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/Makefile b/Makefile index d8bd028..a4a0c47 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,6 @@ $(D).txt: $(D).xml $(D).html: $(D).xml xml2rfc $< $@ + +check: + cd src && $(MAKE) $@ diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..2a6ead3 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +/__pycache__ +/sign.input diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..74a3136 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,5 @@ +all: +check: + wget -N http://ed25519.cr.yp.to/python/sign.input + python3 ed25519-test.py < sign.input +.PHONY: all check diff --git a/src/ed25519-test.py b/src/ed25519-test.py new file mode 100644 index 0000000..78a2952 --- /dev/null +++ b/src/ed25519-test.py @@ -0,0 +1,57 @@ +import sys +import binascii + +from ed25519 import * + +def point_valid(P): + zinv = modp_inv(P[2]) + x = P[0] * zinv % p + y = P[1] * zinv % p + assert (x*y - P[3]*zinv) % p == 0 + return (-x*x + y*y - 1 - d*x*x*y*y) % p == 0 + +assert point_valid(G) +Z = (0, 1, 1, 0) +assert point_valid(Z) + +assert point_equal(Z, point_add(Z, Z)) +assert point_equal(G, point_add(Z, G)) +assert point_equal(Z, point_mul(0, G)) +assert point_equal(G, point_mul(1, G)) +assert point_equal(point_add(G, G), point_mul(2, G)) +for i in range(0, 100): + assert point_valid(point_mul(i, G)) +assert point_equal(Z, point_mul(q, G)) + +def munge_string(s, pos, change): + return (s[:pos] + + int.to_bytes(s[pos] ^ change, 1, "little") + + s[pos+1:]) + +# Read a file in the format of +# http://ed25519.cr.yp.to/python/sign.input +lineno = 0 +while True: + line = sys.stdin.readline() + if not line: + break + lineno = lineno + 1 + print(lineno) + fields = line.split(":") + secret = (binascii.unhexlify(fields[0]))[:32] + public = binascii.unhexlify(fields[1]) + msg = binascii.unhexlify(fields[2]) + signature = binascii.unhexlify(fields[3])[:64] + + assert public == secret_to_public(secret) + assert signature == sign(secret, msg) + assert verify(public, msg, signature) + if len(msg) == 0: + bad_msg = b"x" + else: + bad_msg = munge_string(msg, len(msg) // 3, 4) + assert not verify(public, bad_msg, signature) + bad_signature = munge_string(signature, 20, 8) + assert not verify(public, msg, bad_signature) + bad_signature = munge_string(signature, 40, 16) + assert not verify(public, msg, bad_signature) diff --git a/src/ed25519.py b/src/ed25519.py new file mode 100644 index 0000000..2a1553e --- /dev/null +++ b/src/ed25519.py @@ -0,0 +1,159 @@ +# Loosely based on the public domain code at +# http://ed25519.cr.yp.to/software.html +# +# Needs python-3.2 + +import hashlib + + +def sha512(s): + return hashlib.sha512(s).digest() + +# Base field Z_p +p = 2**255 - 19 + + +def modp_inv(x): + return pow(x, p-2, p) + +# Curve constant +d = -121665 * modp_inv(121666) % p + +# Group order +q = 2**252 + 27742317777372353535851937790883648493 + + +def sha512_modq(s): + return int.from_bytes(sha512(s), "little") % q + +# Points are represented as tuples (X, Y, Z, T) of extended coordinates, +# with x = X/Z, y = Y/Z, x*y = T/Z + + +def point_add(P, Q): + A = (P[1]-P[0])*(Q[1]-Q[0]) % p + B = (P[1]+P[0])*(Q[1]+Q[0]) % p + C = 2 * P[3] * Q[3] * d % p + D = 2 * P[2] * Q[2] % p + E = B-A + F = D-C + G = D+C + H = B+A + return (E*F, G*H, F*G, E*H) + + +# Computes Q = s * Q +def point_mul(s, P): + Q = (0, 1, 1, 0) # Neutral element + while s > 0: + # Is there any bit-set predicate? + if s & 1: + Q = point_add(Q, P) + P = point_add(P, P) + s >>= 1 + return Q + + +def point_equal(P, Q): + # x1 / z1 == x2 / z2 <==> x1 * z2 == x2 * z1 + if (P[0] * Q[2] - Q[0] * P[2]) % p != 0: + return False + if (P[1] * Q[2] - Q[1] * P[2]) % p != 0: + return False + return True + +# Square root of -1 +modp_sqrt_m1 = pow(2, (p-1) // 4, p) + + +# Compute corresponding x coordinate, with low bit corresponding to sign, +# or return None on failure +def recover_x(y, sign): + x2 = (y*y-1) * modp_inv(d*y*y+1) + if x2 == 0: + if sign: + return None + else: + return 0 + + # Compute square root of x2 + x = pow(x2, (p+3) // 8, p) + if (x*x - x2) % p != 0: + x = x * modp_sqrt_m1 % p + if (x*x - x2) % p != 0: + return None + + if (x & 1) != sign: + x = p - x + return x + +# Base point +g_y = 4 * modp_inv(5) % p +g_x = recover_x(g_y, 0) +G = (g_x, g_y, 1, g_x * g_y % p) + + +def point_compress(P): + zinv = modp_inv(P[2]) + x = P[0] * zinv % p + y = P[1] * zinv % p + return int.to_bytes(y | ((x & 1) << 255), 32, "little") + + +def point_decompress(s): + if len(s) != 32: + raise Exception("Invalid input length for decompression") + y = int.from_bytes(s, "little") + sign = y >> 255 + y &= (1 << 255) - 1 + + x = recover_x(y, sign) + if x is None: + return None + else: + return (x, y, 1, x*y % p) + + +def secret_expand(secret): + if len(secret) != 32: + raise Exception("Bad size of private key") + h = sha512(secret) + a = int.from_bytes(h[:32], "little") + a &= (1 << 254) - 8 + a |= (1 << 254) + return (a, h[32:]) + + +def secret_to_public(secret): + (a, dummy) = secret_expand(secret) + return point_compress(point_mul(a, G)) + + +def sign(secret, msg): + a, prefix = secret_expand(secret) + A = point_compress(point_mul(a, G)) + r = sha512_modq(prefix + msg) + R = point_mul(r, G) + Rs = point_compress(R) + h = sha512_modq(Rs + A + msg) + s = (r + h * a) % q + return Rs + int.to_bytes(s, 32, "little") + + +def verify(public, msg, signature): + if len(public) != 32: + raise Exception("Bad public-key length") + if len(signature) != 64: + Exception("Bad signature length") + A = point_decompress(public) + if not A: + return False + Rs = signature[:32] + R = point_decompress(Rs) + if not R: + return False + s = int.from_bytes(signature[32:], "little") + h = sha512_modq(Rs + public + msg) + sB = point_mul(s, G) + hA = point_mul(h, A) + return point_equal(sB, point_add(R, hA)) -- GitLab