Elliptic-curve cryptography has a reputation for being harder than RSA, and in one sense it is: there is no single-sentence description like "multiply two primes". But the whole thing rests on one geometric idea and two formulas, and once those are in hand the rest, scalar multiplication, Diffie–Hellman, the reason 256-bit curves are safe and 7-bit curves are not, follows in a few dozen lines of Python. This post builds it from nothing. All code is pure Python with no dependencies, and every number quoted was produced by running it.
A curve and a rule for adding points
An elliptic curve in short Weierstrass form is the set of points
together with one extra point
The rule: to add
Writing the line as
where the slope is
That is the entire group law. With these rules the points form an abelian group: addition is commutative, associative (the hard part to prove, easy to check numerically),
The same rule over a finite field
Nothing in those formulas needs real numbers. Replace the field with
p, a, b = 97, 3, 2 # y^2 = x^3 + 3x + 2 over F_97
O = None # point at infinity
def inv(x):
return pow(x, p - 2, p) # Fermat: x^(p-2) is x^-1 mod p
def add(P, Q):
if P is O: return Q
if Q is O: return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0:
return O # P + (-P)
if P == Q:
lam = (3 * x1 * x1 + a) * inv(2 * y1) % p # tangent
else:
lam = (y2 - y1) * inv(x2 - x1) % p # chord
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
Enumerating every
Scalar multiplication: double-and-add
The operation cryptography actually uses is computing
def mul(k, P):
"""Double-and-add, most significant bit first."""
R = O
for bit in bin(k)[2:]:
R = add(R, R) # always double
if bit == "1":
R = add(R, P) # add only when the bit is set
return R
For
It is also where real implementations part ways with this one. The branch on bit == "1" makes the running time and the power trace depend on the secret inv mid-loop (inversions are done once at the end, in projective coordinates).
Diffie–Hellman on the toy curve
With a generator
G, n = (0, 14), 103 # any non-identity point generates; order is prime
import random
random.seed(7)
d_a, d_b = random.randrange(1, n), random.randrange(1, n)
Q_a, Q_b = mul(d_a, G), mul(d_b, G)
print("Alice d_a =", d_a, "Q_a =", Q_a)
print("Bob d_b =", d_b, "Q_b =", Q_b)
print("shared (Alice) =", mul(d_a, Q_b))
print("shared (Bob) =", mul(d_b, Q_a))
Alice d_a = 42 Q_a = (88, 64)
Bob d_b = 20 Q_b = (15, 67)
shared (Alice) = (64, 70)
shared (Bob) = (64, 70)
Both sides arrive at
Why this curve is worthless
An eavesdropper sees
k, T = 1, G
while T != Q_a:
T = add(T, G)
k += 1
print("recovered d_a =", k, "after", k, "additions")
# recovered d_a = 42 after 42 additions
Forty-two additions, and in the worst case 102. The point is not that brute force exists; it is how the cost scales. The best known generic algorithm, Pollard's rho, recovers a discrete logarithm in a group of prime order
group operations. For
| Curve | Field size | Group order | Rho cost | Notes |
|---|---|---|---|---|
| toy, | 7 bits | 103 | 9 | broken by hand |
| secp256k1 | 256 bits | Bitcoin, Ethereum; Koblitz curve, | ||
| NIST P-256 | 256 bits | TLS default for ECDHE and ECDSA | ||
| Curve25519 | 255 bits | cofactor 8; X25519 in TLS 1.3, SSH, Signal | ||
| NIST P-384 | 384 bits | CNSA suite |
Being large is necessary but not sufficient. Curves whose order equals
WARNING
The code in this post is for understanding, not for use. It leaks the secret through timing, accepts points that are not on the curve, performs no validation of the peer's public key, and uses
randomrather thansecrets. Real implementations (X25519 viacryptographyin Python, or your platform's TLS library) handle all of this and have been reviewed by people whose job it is to find the mistakes. Use them.
The complete toy implementation
# Toy short-Weierstrass curve y^2 = x^3 + a x + b over F_p, pure Python.
p, a, b = 97, 3, 2
O = None # point at infinity
def inv(x):
return pow(x, p - 2, p)
def on_curve(P):
if P is O:
return True
x, y = P
return (y * y - (x**3 + a * x + b)) % p == 0
def add(P, Q):
if P is O:
return Q
if Q is O:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0:
return O
if P == Q:
lam = (3 * x1 * x1 + a) * inv(2 * y1) % p
else:
lam = (y2 - y1) * inv(x2 - x1) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
def mul(k, P):
R = O
for bit in bin(k)[2:]:
R = add(R, R)
if bit == "1":
R = add(R, P)
return R
def order(P):
n, Q = 1, P
while Q is not O:
Q = add(Q, P)
n += 1
return n
points = [O] + [(x, y) for x in range(p) for y in range(p) if on_curve((x, y))]
print("group order #E(F_p) =", len(points)) # 103
G = (0, 14)
print("order of G =", order(G)) # 103
print("3G =", mul(3, G), "=", add(G, add(G, G))) # (38, 59) both ways
print("nG =", mul(103, G)) # None, i.e. the identity
Further reading
- Andrea Corbellini, Elliptic Curve Cryptography: a gentle introduction, a four-part series with interactive plots of the group law.
- Daniel J. Bernstein and Tanja Lange, SafeCurves: choosing safe curves for elliptic-curve cryptography, the criteria beyond "the order is large".
- RFC 7748, Elliptic Curves for Security, which specifies X25519 and X448 and the Montgomery ladder.
- NIST, SP 800-186, Recommendations for Discrete Logarithm-based Cryptography: Elliptic Curve Domain Parameters.
- Certicom Research, SEC 2: Recommended Elliptic Curve Domain Parameters, the source of secp256k1 and friends.
- Standard curve database, parameters and known properties of every widely deployed curve.