Lesson 8.1: ECC Basics, ECDH and ECDSA
This lesson builds elliptic curve cryptography (ECC) from scratch. A curve is the set of points satisfying y^2 = x^3 + ax + b mod p. Two points are added by a geometric rule, a point is multiplied by a number, and from those pieces we build the ECDH key exchange and the ECDSA signature. The environment has no ready-made EC library, so we implement every operation in pure Python on a small curve and run it to see each step. After this lesson you understand what the point at infinity is, why point multiplication is easy while the reverse (ECDLP) is hard, and how ECDSA signs and verifies.
Point addition and double-and-add give k*G. ECDH shares (dA*dB)*G, and ECDSA signs with a secret nonce k.
Prerequisites: Lesson 1.1 (modular arithmetic, inverses), Lesson 1.4 (groups, the field GF(p)), Lesson 7.1 (discrete log, to compare ECDLP with DLP). Lesson 5.1 (hashes) helps with the ECDSA part.
Tools: Python 3 (pure, only hashlib from the standard library). Nothing to install. Every EC operation is hand-written.
Goals
You will understand the Weierstrass curve y^2 = x^3 + ax + b mod p, the point at infinity, and why the points form a group. You will code point addition and scalar multiplication (double-and-add) in pure Python. You will build ECDH (key exchange) and ECDSA (sign, verify) on a small curve and run them. And you will understand the ECDLP and why ECC reaches the same security as RSA/DH with much shorter keys.
Theory
The curve, its points, and the point at infinity
Over a finite field GF(p) (integers modulo a prime p), a short Weierstrass elliptic curve is the set of points (x, y) satisfying:
1
y^2 = x^3 + a*x + b (mod p)
plus one special point called the point at infinity, written O, which acts as the identity element (like 0 for addition). The non-singular condition is 4a^3 + 27b^2 != 0 mod p (if it is 0 the curve is singular, Lesson 8.3).
A concrete example is p = 97, a = 3, b = 2. The point (0, 14) is on the curve because 14^2 = 196 = 2*97 + 2, and 0^3 + 3*0 + 2 = 2, so they match. This finite set of points together with O forms an abelian group under point addition.
Point addition: geometry turned into formulas
The addition rule is chord-and-tangent. To add P + Q, draw the line through P and Q. It meets the curve at a third point, and reflecting that point across the x-axis gives P + Q. Translated into formulas over GF(p):
- If
P = OthenP + Q = Q(and the other way round). - If
P = (x, y)andQ = (x, -y)(mirror images) thenP + Q = O. - The slope
m: ifP != Qthenm = (y2 - y1) / (x2 - x1). IfP = Q(doubling) use the tangentm = (3*x1^2 + a) / (2*y1). Division here means multiplying by the modular inverse (Lesson 1.1). - Then
x3 = m^2 - x1 - x2,y3 = m*(x1 - x3) - y1, all modp.
Note that the addition formula never uses b. This small detail is the root of the invalid curve attack in Lesson 8.3.
Scalar multiplication and the ECDLP
Scalar multiplication adds a point to itself k times: k*P = P + P + ... + P. It is done fast with double-and-add (the same as fast exponentiation, with point addition in place of multiplication). Walk through the bits of k, add on a 1 bit, and double at every step. Computing k*P from k and P is very fast.
The reverse direction is the Elliptic Curve Discrete Logarithm Problem (ECDLP). Given P and Q = k*P, find k. On a well-chosen curve the ECDLP is much harder than the ordinary DLP. There is no efficient index calculus algorithm, so the best algorithm is still about sqrt(group order). As a result, 256-bit ECC is about as secure as 3072-bit RSA/DH. Shorter keys and faster operations are why ECC dominates modern crypto.
ECDH and ECDSA
ECDH is DH moved to a curve. A base point (generator) G is public. Alice picks a secret dA and publishes QA = dA*G. Bob picks dB and publishes QB = dB*G. The shared secret is dA*QB = dB*QA = (dA*dB)*G. Security rests on the ECDLP.
ECDSA is a digital signature on a curve. Let n be the order of G (the number of points in the subgroup generated by G). The secret signing key is d and the public key is Q = d*G. To sign a message with hash z:
- Pick a random nonce
kin[1, n-1]. - Compute
R = k*Gand taker = R.x mod n(ifr = 0, pick a newk). - Compute
s = k^(-1) * (z + r*d) mod n. - The signature is the pair
(r, s).
To verify (r, s) with hash z and public key Q, compute w = s^(-1) mod n, u1 = z*w, u2 = r*w, then X = u1*G + u2*Q. The signature is valid when X.x mod n == r. The nonce k is the weak point of ECDSA. Leaking k or reusing it loses the private key (Lesson 8.2).
Demo
Everything below is a single file that runs as is. The curve is p = 97, a = 3, b = 2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# d81_ecc.py: pure Python ECC on a small curve. Point addition, scalar multiplication, ECDH, ECDSA.
import hashlib
# ===== Curve parameters: y^2 = x^3 + a*x + b mod p =====
p = 97
a, b = 3, 2
INF = None # point at infinity, use None as the identity point O
def is_on_curve(P):
if P is INF:
return True
x, y = P
return (y * y - (x**3 + a * x + b)) % p == 0
def point_add(P, Q):
# add two points with the chord-and-tangent formula
if P is INF:
return Q
if Q is INF:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0:
return INF # P + (-P) = O
if P == Q:
# tangent: slope m = (3x^2 + a) / (2y)
m = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
else:
m = (y2 - y1) * pow(x2 - x1, -1, p) % p
x3 = (m * m - x1 - x2) % p
y3 = (m * (x1 - x3) - y1) % p
return (x3, y3)
def scalar_mul(k, P):
# scalar multiplication by double-and-add
R = INF
Q = P
while k > 0:
if k & 1:
R = point_add(R, Q)
Q = point_add(Q, Q)
k >>= 1
return R
def find_points():
pts = []
for x in range(p):
rhs = (x**3 + a * x + b) % p
for y in range(p):
if (y * y) % p == rhs:
pts.append((x, y))
return pts
if __name__ == "__main__":
pts = find_points()
print("finite points =", len(pts), "| plus the point at infinity ->", len(pts) + 1)
# pick base point G and determine its order n
G = pts[0]
print("G =", G, "| on curve?", is_on_curve(G))
# count the order: the group has prime order 103 so every point != O generates the whole group
n = 1
T = G
while T is not INF:
T = point_add(T, G)
n += 1
print("order of G: n =", n)
# --- point addition and multiplication examples ---
print("2G =", scalar_mul(2, G), "| 3G =", scalar_mul(3, G))
print("nG = O ?", scalar_mul(n, G) is INF)
# ===== ECDH =====
da, db = 17, 71 # private keys of Alice, Bob
Qa, Qb = scalar_mul(da, G), scalar_mul(db, G)
Sa = scalar_mul(da, Qb)
Sb = scalar_mul(db, Qa)
print("ECDH: Alice", Sa, "| Bob", Sb, "| match?", Sa == Sb)
# ===== ECDSA =====
def H(msg):
return int.from_bytes(hashlib.sha256(msg).digest(), "big") % n
d = 42 # secret signing key
Q = scalar_mul(d, G) # public key
def sign(msg, k):
z = H(msg)
R = scalar_mul(k, G)
r = R[0] % n
s = (pow(k, -1, n) * (z + r * d)) % n
return (r, s)
def verify(msg, sig, Q):
r, s = sig
if not (1 <= r < n and 1 <= s < n):
return False
z = H(msg)
w = pow(s, -1, n)
u1, u2 = (z * w) % n, (r * w) % n
X = point_add(scalar_mul(u1, G), scalar_mul(u2, Q))
return X is not INF and X[0] % n == r
msg = b"ecdsa on a small curve"
sig = sign(msg, k=23)
print("signature (r, s) = ", sig)
print("verify correct msg :", verify(msg, sig, Q))
print("verify wrong msg :", verify(b"forged msg", sig, Q))
Output:
1
2
3
4
5
6
7
8
9
finite points = 102 | plus the point at infinity -> 103
G = (0, 14) | on curve? True
order of G: n = 103
2G = (86, 53) | 3G = (38, 59)
nG = O ? True
ECDH: Alice (67, 84) | Bob (67, 84) | match? True
signature (r, s) = (19, 10)
verify correct msg : True
verify wrong msg : False
Reading the result, the group has 103 points (including O), and 103 is prime, so every point other than O generates the whole group and has order n = 103. scalar_mul(n, G) returns O, as the theory says (multiplying by the full order gives the identity). ECDH gives Alice and Bob the same point (67, 84). ECDSA signs and then verifies the right message with True, while a forged message gives False, so the signature does its job.
Note that p = 97 while n = 103. The group order differs from the prime of the field, so do not mix the two numbers. In ECDSA every mod is taken by n (the group order), not by p.
Lab
This is a groundwork lesson with nothing to break yet. Practice before Lessons 8.2 and 8.3:
- List all points of the curve
p = 97, a = 3, b = 2yourself. Check that(x, y)and(x, p-y)always come in pairs (mirror images across the x-axis), and that there is at most one point withy = 0. - Write a function
point_neg(P)that returns-P, and checkP + (-P) = Ofor a few points. - Build ECDH end to end. After you have the shared secret, hash its x coordinate with SHA-256 into an AES-128 key, encrypt a sentence, and decrypt it again.
- Try a standard curve. Take the secp256k1 parameters (p, a=0, b=7, G, n can all be looked up), plug them into the same
point_addandscalar_mulabove (they work on big numbers because they use Python’spow(..., -1, p)), then sign and verify a message. The goal is to see the small-curve code run unchanged on a real curve. - Done when ECDH plus AES runs on the small curve, and signing and verifying works on secp256k1.
Key takeaways
- The curve is
y^2 = x^3 + ax + b mod p, points are added with chord-and-tangent, andOis the identity. The non-singular condition is4a^3 + 27b^2 != 0. - The addition formula does not use
b, onlya. Remember this for Lesson 8.3. - Scalar multiplication
k*Pis fast with double-and-add. The reverse (ECDLP) is hard, and that is the security foundation. - ECDH: the shared secret is
(dA*dB)*G. ECDSA: signing needs a noncek, and every mod is by the group ordern, notp. - The ECDSA nonce
kmust be secret, random, and different each time. This is the weak point (Lesson 8.2).
Common pitfalls
- Mixing up
p(the prime of the field) andn(the group order). Point coordinates are computed modp, while scalars and ECDSA are computed modn. Confusing them breaks every signature. - Forgetting the point at infinity
Oinpoint_add. A missingP is INFbranch or a missingP + (-P) = Obranch makes the code crash or return a wrong point. - Using
(y2 - y1)/(x2 - x1)whenP == Q. Doubling must use the tangent formula, otherwisex2 - x1 = 0and the inverse fails. - Taking
rorsequal to 0 without choosing a new nonce. The signature degenerates and verification fails. Always checkr != 0ands != 0. - Assuming the group order is always prime. This group happens to have prime order 103, which is convenient, but in general the order can be composite. Then
Gmust lie in the subgroup of large prime order (the cofactor issue), otherwise you run into Pohlig-Hellman (Lesson 8.3).
Further reading
- “A gentle introduction to elliptic curve cryptography” by Andrea Corbellini (a blog series), with a very visual explanation of point addition and the ECDLP.
- SafeCurves (safecurves.cr.yp.to), to see which criteria make a curve safe.
- The SEC 2 standard (secp256k1, secp256r1), to look up the parameters of real curves for the lab.
- CryptoHack, the Elliptic Curves track, from “Point Addition” and “Scalar Multiplication” up to ECDH, matching this lesson.
