Files
paperspec/tools/oracle/vrf.py
T
alexm 4fa326dc76
check / check (push) Successful in 42s
site / site (push) Successful in 3m4s
First implementation (#1)
Reviewed-on: #1
2026-10-15 05:47:05 -04:00

180 lines
5.2 KiB
Python

"""ECVRF-EDWARDS25519-SHA512-TAI, the verifiable random function of RFC 9381 over the Ed25519 curve.
Non-normative; see spec/08-fidelity.md §8.8.
A VRF proof has exactly one valid value for a key and an input, where an Ed25519 signature has many, so a
spot-check seed that is a VRF proof cannot be ground by the checker. The arithmetic is the reference form of
RFC 8032 in plain Python, slow and clear; the keys are ordinary Ed25519 keys, so the roster needs nothing new.
The RFC's Example 16 is pinned in vectors/vrf.jsonl.
"""
import hashlib
SUITE = b"\x03"
COFACTOR = 8
P = 2**255 - 19
Q = 2**252 + 27742317777372353535851937790883648493
D = (-121665 * pow(121666, P - 2, P)) % P
_I = pow(2, (P - 1) // 4, P)
def _inv(x):
return pow(x, P - 2, P)
def _recover_x(y, sign):
if y >= P:
return None
x2 = (y * y - 1) * _inv(D * y * y + 1) % P
if x2 == 0:
return None if sign else 0
x = pow(x2, (P + 3) // 8, P)
if (x * x - x2) % P != 0:
x = x * _I % P
if (x * x - x2) % P != 0:
return None
if (x & 1) != sign:
x = P - x
return x
# Points in extended homogeneous coordinates (X, Y, Z, T) with x = X/Z, y = Y/Z, x*y = T/Z (RFC 8032 §5.1.4).
G_Y = 4 * _inv(5) % P
G_X = _recover_x(G_Y, 0)
G = (G_X, G_Y, 1, G_X * G_Y % P)
IDENTITY = (0, 1, 1, 0)
def _add(a, b):
x1, y1, z1, t1 = a
x2, y2, z2, t2 = b
A = (y1 - x1) * (y2 - x2) % P
B = (y1 + x1) * (y2 + x2) % P
C = t1 * 2 * D * t2 % P
Dd = z1 * 2 * z2 % P
E, F, Gg, H = B - A, Dd - C, Dd + C, B + A
return (E * F % P, Gg * H % P, F * Gg % P, E * H % P)
def _mul(s, pt):
out, acc = IDENTITY, pt
while s > 0:
if s & 1:
out = _add(out, acc)
acc = _add(acc, acc)
s >>= 1
return out
def _equal(a, b):
return (a[0] * b[2] - b[0] * a[2]) % P == 0 and (a[1] * b[2] - b[1] * a[2]) % P == 0
def point_to_string(pt):
z = _inv(pt[2])
x, y = pt[0] * z % P, pt[1] * z % P
return (y | ((x & 1) << 255)).to_bytes(32, "little")
def string_to_point(s):
"""The point a 32-byte string encodes, or None when it is not a valid encoding (RFC 8032 §5.1.3)."""
if len(s) != 32:
return None
y = int.from_bytes(s, "little")
sign = y >> 255
y &= (1 << 255) - 1
x = _recover_x(y, sign)
if x is None:
return None
return (x, y, 1, x * y % P)
def _secret_scalar(sk):
h = hashlib.sha512(sk).digest()
a = bytearray(h[:32])
a[0] &= 248
a[31] &= 63
a[31] |= 64
return int.from_bytes(a, "little"), h[32:]
def public_key(sk):
x, _ = _secret_scalar(sk)
return point_to_string(_mul(x, G))
def encode_to_curve(pk_string, alpha):
"""ECVRF_encode_to_curve_try_and_increment (RFC 9381 §5.4.1.1)."""
for ctr in range(256):
h = hashlib.sha512(SUITE + b"\x01" + pk_string + alpha + bytes([ctr]) + b"\x00").digest()
pt = string_to_point(h[:32])
if pt is not None:
pt = _mul(COFACTOR, pt)
if not _equal(pt, IDENTITY):
return pt
raise ValueError("no curve point found")
def _challenge(points):
"""ECVRF_challenge_generation (RFC 9381 §5.4.3): the first 16 octets of the hash, as a little-endian integer."""
s = SUITE + b"\x02" + b"".join(point_to_string(p) for p in points) + b"\x00"
return int.from_bytes(hashlib.sha512(s).digest()[:16], "little")
def prove(sk, alpha):
"""ECVRF_prove: the 80-byte proof Gamma || c || s for the 32-byte Ed25519 secret `sk` and input `alpha`."""
x, suffix = _secret_scalar(sk)
y = _mul(x, G)
h = encode_to_curve(point_to_string(y), alpha)
h_string = point_to_string(h)
gamma = _mul(x, h)
k = int.from_bytes(hashlib.sha512(suffix + h_string).digest(), "little") % Q
c = _challenge([y, h, gamma, _mul(k, G), _mul(k, h)])
s = (k + c * x) % Q
return point_to_string(gamma) + c.to_bytes(16, "little") + s.to_bytes(32, "little")
def decode_proof(pi):
if len(pi) != 80:
return None
gamma = string_to_point(pi[:32])
if gamma is None:
return None
c = int.from_bytes(pi[32:48], "little")
s = int.from_bytes(pi[48:80], "little")
if s >= Q:
return None
return gamma, c, s
def proof_to_hash(pi):
"""ECVRF_proof_to_hash: the 64-byte output beta of a proof (RFC 9381 §5.2), or None for a malformed proof."""
d = decode_proof(pi)
if d is None:
return None
gamma = d[0]
return hashlib.sha512(SUITE + b"\x03" + point_to_string(_mul(COFACTOR, gamma)) + b"\x00").digest()
def _neg(pt):
return ((-pt[0]) % P, pt[1], pt[2], (-pt[3]) % P)
def verify(pk_string, alpha, pi):
"""ECVRF_verify: beta when `pi` is the proof for `alpha` under the public key, else None. Never raises."""
try:
y = string_to_point(pk_string)
if y is None or _equal(_mul(COFACTOR, y), IDENTITY):
return None
d = decode_proof(pi)
if d is None:
return None
gamma, c, s = d
h = encode_to_curve(pk_string, alpha)
u = _add(_mul(s, G), _neg(_mul(c, y)))
v = _add(_mul(s, h), _neg(_mul(c, gamma)))
if _challenge([y, h, gamma, u, v]) != c:
return None
return proof_to_hash(pi)
except (ValueError, TypeError):
return None