Skip to content

Commit 680908c

Browse files
committed
Merge branch '28-crypto-add-ecdsa-signature-and-recovery-support'
2 parents ed3be2d + bc72c9d commit 680908c

5 files changed

Lines changed: 169 additions & 9 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,17 @@ is_point_on_curve(curve.G, curve) # true, G generator point defines the cyc
9090
H("byte size hash of a string") # hashes string to a byte, given the abelian group is byte size
9191
H₁₆("16 bit hash") # in case you need more hash space
9292
H₈("is same") == H("is same") # true, it is just a alias, smae as H8
93+
94+
## ECDSA Signature and Public Key Recovery Example
95+
priv, pub = genkey(curve) # Key generation
96+
msg = "hello ethereum" # Message to sign
97+
signature = sign(curve, priv, msg) # returns a NamedTuple (r = ..., s = ..., v = ...)
98+
is_valid = verify(curve, pub, signature, msg) # Signature verification true
99+
recovered = ecrecover(curve, msg, signature) # Public key recovery from signature
100+
@assert pub == recovered # Check if recovered public key matches original
93101
```
94102

103+
95104
## Installation
96105
```
97106
using Pkg

examples/ecdsa.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
using TinyCrypto
3+
4+
# ─────────────────────────────────────────────────────────────────────────────
5+
# ECDSA Signature and Public Key Recovery Example
6+
# ─────────────────────────────────────────────────────────────────────────────
7+
8+
priv, pub = genkey(curve) # Key generation
9+
msg = "hello ethereum" # Message to sign
10+
signature = sign(curve, priv, msg) # returns a NamedTuple (r = ..., s = ..., v = ...)
11+
is_valid = verify(curve, pub, signature, msg) # Signature verification true
12+
recovered = ecrecover(curve, msg, signature) # Public key recovery from signature
13+
@assert pub == recovered # Check if recovered public key matches originald
14+
15+
# ─────────────────────────────────────────────────────────────────────────────
16+
# The following steps (0–7) manually walk through the ECDSA signing and verification
17+
# process to illustrate what happens under the hood. The goal is to help understand
18+
# the mathematical structure behind the cryptographic operations shown above.
19+
# ─────────────────────────────────────────────────────────────────────────────
20+
21+
# 1. Define a toy Weierstrass curve over 𝔽₃₁ with G of order 37
22+
curve = Weierstrass(31, 6, 9, 37, 1, (0, 3))
23+
# Weierstrass{𝔽₃₁}: y² = x³ + 6x + 9 | G = (0,3), order = 37, cofactor = 1
24+
25+
# 2. Create a mock blockchain transaction
26+
tx = "hello ethereum"
27+
28+
# 3. Hash it (you fake Keccak256 with H₈ → SHA256 first 8 bytes)
29+
z = Int(H₈(tx)) % curve.order # H₈ returns UInt8; convert to Int for modular arithmetic
30+
31+
# 4. Generate keypair
32+
priv = rand(1:curve.order - 1) # private key is a uniformly random integer in [1, curve order − 1]
33+
pub = priv * curve.G # public key is the scalar multiple of the generator: pub = priv · G
34+
35+
r,s,v = 0,0,0
36+
# 5. Sign: create ephemeral scalar `k` and calculate signature (r, s)
37+
while true
38+
k = rand(1:curve.order - 1) # draw a random ephemeral scalar used once per signature
39+
R = k * curve.G # then construct an ephemeral public point R = k·G on the curve
40+
v = isodd(R.point.y.val) ? 1 : 0 # and compute the parity (Ethereum) bit from LSB of y which is used for ECrecover
41+
42+
r = mod(R.point.x, curve.order) # use x-coordinate of R as first signature component
43+
k_inv = invmod(k, curve.order) # compute modular inverse of ephemeral scalar
44+
s = mod(k_inv * (z + r * priv), curve.order) # second signature component s = k⁻¹(z + r·priv) mod n
45+
46+
if r 0 && s 0 break end # loop until suitable `k` is found
47+
end
48+
49+
# 6. The signature is (r, s, v + 27) `27` roots back to early Bitcoin compact signatures, parity bit is used on Ethereum
50+
# whereas Bitcoin uses recovery ID : = {0,1,2,3}
51+
println("Signature: (r = $r, s = $s, v = $(v + 27))")
52+
53+
# 7. Verification
54+
s_inv = invmod(s, curve.order) # compute inverse of signature scalar s⁻¹ mod n
55+
u₁ = z * s_inv % curve.order # u₁ = z · s⁻¹ mod n
56+
u₂ = r * s_inv % curve.order # u₂ = r · s⁻¹ mod n
57+
P = u₁ * curve.G + u₂ * pub # recover point P = u₁·G + u₂·Q (should equal R)
58+
59+
is_valid = mod(P.point.x, curve.order) == r # check if recovered R.x matches r component
60+
println("Signature valid? ", is_valid)

src/TinyCrypto.jl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ include("core/ecpoint.jl")
1414
include("curves/weierstrass.jl")
1515
include("curves/montgomery.jl")
1616
include("curves/edwards.jl")
17-
# Re-export core symbols for public API
18-
export @define, @attach
19-
export H, H₈, H₁₆, H8, H16
20-
export Fp, 𝔽ₚ
21-
export is_prime, primes, mod_inverse
22-
export Point, ∞, Curve, ECPoint, Infinity
23-
export Weierstrass, Montgomery, Edwards, TwistedEdwards
24-
export infinity, is_infinity, is_identity
25-
export point_add, scalar_mult, curve_points, subgroup_points, is_generator, is_point_on_curve, inverse, is_singular
17+
# Re-export core symbols for public API
18+
export @define, @attach
19+
export H, H₈, H₁₆, H8, H16
20+
export Fp, 𝔽ₚ
21+
export is_prime, primes, mod_inverse
22+
export Point, ∞, Curve, ECPoint, Infinity
23+
export Weierstrass, Montgomery, Edwards, TwistedEdwards
24+
export infinity, is_infinity, is_identity
25+
export point_add, scalar_mult, curve_points, subgroup_points, is_generator, is_point_on_curve, inverse, is_singular
26+
27+
include("core/ecdsa.jl")
28+
import .ECDSA: sign, verify, genkey, ecrecover
29+
export genkey, sign, verify, ecrecover
2630
end

src/core/ecdsa.jl

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module ECDSA
2+
import Base: sign
3+
using ..TinyCrypto: H₈, invmod, isodd
4+
using ..TinyCrypto: Weierstrass, Point, ECPoint
5+
6+
export genkey, sign, verify
7+
8+
function genkey(curve::Weierstrass)
9+
priv = rand(1:curve.order - 1)
10+
pub = priv * curve.G
11+
return priv, pub
12+
end
13+
14+
function sign(curve::Weierstrass, priv, msg::AbstractString)
15+
z = Int(H₈(msg)) % curve.order # hash message to scalar mod curve order
16+
17+
while true
18+
k = rand(1:curve.order - 1)
19+
R = k * curve.G
20+
r = mod(R.point.x, curve.order)
21+
if r == 0 continue end # Ensure r ≠ 0
22+
k_inv = invmod(k, curve.order)
23+
s = mod(k_inv * (z + r * priv), curve.order)
24+
if s 0
25+
parity = isodd(R.point.y.val) ? 1 : 0
26+
return (r = r, s = s, v = parity)
27+
end
28+
end
29+
end
30+
31+
function verify(curve::Weierstrass, pub, sig, msg::AbstractString)
32+
r, s = sig.r, sig.s
33+
if !(1 <= r < curve.order && 1 <= s < curve.order) return false end
34+
#if !is_point_on_curve(pub, curve) return false end
35+
z = Int(H₈(msg)) % curve.order
36+
s_inv = invmod(s, curve.order)
37+
u₁ = z * s_inv % curve.order
38+
u₂ = r * s_inv % curve.order
39+
P = u₁ * curve.G + u₂ * pub
40+
return mod(P.point.x, curve.order) == r
41+
end
42+
function ecrecover(curve::Weierstrass, msg::AbstractString, r::Int, s::Int, v::Int)
43+
n, π = curve.order, curve.π
44+
z = Int(H₈(msg)) % n
45+
x_candidates = [r + k*n for k in 0:div(π,n) if r + k*n < π]
46+
47+
for x in x_candidates
48+
α = mod(x^3 + curve.a * x + curve.b, π)
49+
y_candidates = [y for y in 0:π-1 if y^2 % π == α]
50+
for y in y_candidates
51+
if isodd(y) == v
52+
R = ECPoint(x, y, curve)
53+
r_inv = invmod(r, n)
54+
sR = s * R
55+
zG = z * curve.G
56+
neg_zG = ECPoint(zG.point.x, -zG.point.y, curve) # handle point negation properly
57+
pub = r_inv * (sR + neg_zG)
58+
return pub
59+
end
60+
end
61+
end
62+
return nothing
63+
end
64+
function ecrecover(curve::Weierstrass, msg::AbstractString, signature::NTuple{3,Int})
65+
r,s,v = signature
66+
return ecrecover(curve, msg, r,s,v)
67+
end
68+
function ecrecover(curve::Weierstrass, msg::AbstractString, signature::NamedTuple{(:r, :s, :v), <:Tuple{<:Integer, <:Integer, <:Integer}})
69+
return ecrecover(curve, msg, signature.r, signature.s, signature.v)
70+
end
71+
end

src/core/ecpoint.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ struct ECPoint{T,C<:Curve}
2424
curve::C
2525
end
2626
ECPoint(curve::C) where {C<:Curve} = ECPoint(typeof(curve.G.point.x), curve)
27+
function ECPoint(x::T, y::T, curve::C) where {T<:Fp, C<:Curve}
28+
return ECPoint(Point{T}(x, y), curve)
29+
end
30+
function ECPoint(x::Integer, y::Integer, curve::C) where {C<:Curve}
31+
T = typeof(curve.G.point.x)
32+
P = Point{T}(T(x), T(y))
33+
if !is_point_on_curve(P, curve)
34+
throw(ArgumentError("Point ($(x), $(y)) is not on the given curve."))
35+
end
36+
return ECPoint(P, curve)
37+
end
38+
39+
2740

2841
# Infinity
2942
is_infinity(P::Point) = P.x === nothing && P.y === nothing
@@ -43,6 +56,9 @@ Base.:*(k::Integer, A::ECPoint{T,C}) where {T,C<:Curve} = is_infinity(A) || is_i
4356
Base.:-(A::ECPoint{T,C}, B::ECPoint{T,C}) where {T,C<:Curve} = A + (-B)
4457
point_neg(P::Point{T}, curve::C) where {T<:Fp, C<:Curve} = error("point_neg not implemented for curve $(typeof(curve))")
4558
Base.:-(A::ECPoint{T,C}) where {T<:Fp, C<:Curve} = is_infinity(A) ? A : ECPoint(point_neg(A.point, A.curve), A.curve)
59+
Base.:()(curve::Curve, P::ECPoint) = in(P, curve)
60+
Base.in(P::ECPoint, curve::Curve) = is_point_on_curve(P.point, curve)
61+
Base.:(P::ECPoint, curve::Curve) = !in(P, curve)
4662

4763
is_infinity(P::ECPoint) = is_infinity(P.point)
4864
is_identity(P::Point{F}, curve::C) where {F<:Fp, C<:Curve} = P == identity(curve)

0 commit comments

Comments
 (0)