aboutsummaryrefslogtreecommitdiffstats
path: root/internal/secp256k1
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-20 18:52:08 -0800
committerClawd <ai@clawd.bot>2026-02-20 18:52:08 -0800
commitd641f4566f051656bae79e406155c4f7f65ec338 (patch)
tree4cd01f3fa4585cf719116a303473e792ea67e82a /internal/secp256k1
parent84709fd67c02058334519ebee9c110f68b33d9b4 (diff)
embed secp256k1: replace btcec with internal pure-go implementation
This removes all external dependencies by embedding the secp256k1-learn implementation into internal/secp256k1. Changes: - Add internal/secp256k1 with field arithmetic, curve ops, keys, schnorr - Update keys.go to use internal secp256k1 package - Remove btcec/btcutil dependencies (go.mod is now clean) - All tests pass Tradeoffs: - ~10x slower crypto ops vs btcec (acceptable for nostr use case) - Not constant-time (documented limitation) - Zero external dependencies Refs: code.northwest.io/secp256k1-learn
Diffstat (limited to 'internal/secp256k1')
-rw-r--r--internal/secp256k1/field.go92
-rw-r--r--internal/secp256k1/keys.go171
-rw-r--r--internal/secp256k1/point.go171
-rw-r--r--internal/secp256k1/schnorr.go236
4 files changed, 670 insertions, 0 deletions
diff --git a/internal/secp256k1/field.go b/internal/secp256k1/field.go
new file mode 100644
index 0000000..13cdffd
--- /dev/null
+++ b/internal/secp256k1/field.go
@@ -0,0 +1,92 @@
1package secp256k1
2
3import (
4 "fmt"
5 "math/big"
6)
7
8// The prime for secp256k1: 2^256 - 2^32 - 977
9// All field arithmetic happens mod this number
10var P, _ = new(big.Int).SetString(
11 "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F",
12 16,
13)
14
15// FieldElement represents a number in our finite field (mod P)
16type FieldElement struct {
17 value *big.Int
18}
19
20// NewFieldElement creates a field element from a big.Int
21// It automatically reduces mod P
22func NewFieldElement(v *big.Int) *FieldElement {
23 result := new(big.Int).Mod(v, P)
24 return &FieldElement{value: result}
25}
26
27// NewFieldElementFromInt64 is a convenience for small numbers
28func NewFieldElementFromInt64(v int64) *FieldElement {
29 return NewFieldElement(big.NewInt(v))
30}
31
32// Add returns (a + b) mod P
33func (a *FieldElement) Add(b *FieldElement) *FieldElement {
34 result := new(big.Int).Add(a.value, b.value)
35 return NewFieldElement(result)
36}
37
38// Sub returns (a - b) mod P
39func (a *FieldElement) Sub(b *FieldElement) *FieldElement {
40 result := new(big.Int).Sub(a.value, b.value)
41 return NewFieldElement(result)
42}
43
44// Mul returns (a * b) mod P
45func (a *FieldElement) Mul(b *FieldElement) *FieldElement {
46 result := new(big.Int).Mul(a.value, b.value)
47 return NewFieldElement(result)
48}
49
50// Div returns (a / b) mod P
51// Division in a field = multiply by the inverse
52func (a *FieldElement) Div(b *FieldElement) *FieldElement {
53 // a / b = a * b^(-1)
54 // b^(-1) mod P = b^(P-2) mod P (Fermat's little theorem)
55 inverse := b.Inverse()
56 return a.Mul(inverse)
57}
58
59// Inverse returns a^(-1) mod P using Fermat's little theorem
60// a^(-1) = a^(P-2) mod P
61func (a *FieldElement) Inverse() *FieldElement {
62 // P - 2
63 exp := new(big.Int).Sub(P, big.NewInt(2))
64 // a^(P-2) mod P
65 result := new(big.Int).Exp(a.value, exp, P)
66 return &FieldElement{value: result}
67}
68
69// Square returns a² mod P (convenience method)
70func (a *FieldElement) Square() *FieldElement {
71 return a.Mul(a)
72}
73
74// Equal checks if two field elements are the same
75func (a *FieldElement) Equal(b *FieldElement) bool {
76 return a.value.Cmp(b.value) == 0
77}
78
79// IsZero checks if the element is zero
80func (a *FieldElement) IsZero() bool {
81 return a.value.Sign() == 0
82}
83
84// String returns hex representation
85func (a *FieldElement) String() string {
86 return fmt.Sprintf("%064x", a.value)
87}
88
89// Clone returns a copy
90func (a *FieldElement) Clone() *FieldElement {
91 return &FieldElement{value: new(big.Int).Set(a.value)}
92}
diff --git a/internal/secp256k1/keys.go b/internal/secp256k1/keys.go
new file mode 100644
index 0000000..fde5d26
--- /dev/null
+++ b/internal/secp256k1/keys.go
@@ -0,0 +1,171 @@
1package secp256k1
2
3import (
4 "crypto/rand"
5 "fmt"
6 "math/big"
7)
8
9// PrivateKey is a scalar (1 to N-1) used for signing
10type PrivateKey struct {
11 D *big.Int // the secret scalar
12}
13
14// PublicKey is a point on the curve (D * G)
15type PublicKey struct {
16 Point *Point
17}
18
19// GeneratePrivateKey creates a random private key
20func GeneratePrivateKey() (*PrivateKey, error) {
21 // Generate random bytes
22 bytes := make([]byte, 32)
23 _, err := rand.Read(bytes)
24 if err != nil {
25 return nil, fmt.Errorf("failed to generate random bytes: %w", err)
26 }
27
28 // Convert to big.Int and reduce mod N
29 d := new(big.Int).SetBytes(bytes)
30 d.Mod(d, N)
31
32 // Ensure it's not zero (extremely unlikely but must check)
33 if d.Sign() == 0 {
34 d.SetInt64(1)
35 }
36
37 return &PrivateKey{D: d}, nil
38}
39
40// NewPrivateKeyFromBytes creates a private key from 32 bytes
41func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
42 if len(b) != 32 {
43 return nil, fmt.Errorf("private key must be 32 bytes, got %d", len(b))
44 }
45
46 d := new(big.Int).SetBytes(b)
47
48 // Validate: must be in range [1, N-1]
49 if d.Sign() == 0 {
50 return nil, fmt.Errorf("private key cannot be zero")
51 }
52 if d.Cmp(N) >= 0 {
53 return nil, fmt.Errorf("private key must be less than curve order N")
54 }
55
56 return &PrivateKey{D: d}, nil
57}
58
59// NewPrivateKeyFromHex creates a private key from a hex string
60func NewPrivateKeyFromHex(hex string) (*PrivateKey, error) {
61 d, ok := new(big.Int).SetString(hex, 16)
62 if !ok {
63 return nil, fmt.Errorf("invalid hex string")
64 }
65
66 // Validate range
67 if d.Sign() == 0 {
68 return nil, fmt.Errorf("private key cannot be zero")
69 }
70 if d.Cmp(N) >= 0 {
71 return nil, fmt.Errorf("private key must be less than curve order N")
72 }
73
74 return &PrivateKey{D: d}, nil
75}
76
77// PublicKey derives the public key from the private key
78// PublicKey = D * G
79func (priv *PrivateKey) PublicKey() *PublicKey {
80 point := G.ScalarMul(priv.D)
81 return &PublicKey{Point: point}
82}
83
84// Bytes returns the private key as 32 bytes (big-endian, zero-padded)
85func (priv *PrivateKey) Bytes() []byte {
86 b := priv.D.Bytes()
87 // Pad to 32 bytes
88 if len(b) < 32 {
89 padded := make([]byte, 32)
90 copy(padded[32-len(b):], b)
91 return padded
92 }
93 return b
94}
95
96// Hex returns the private key as a 64-character hex string
97func (priv *PrivateKey) Hex() string {
98 return fmt.Sprintf("%064x", priv.D)
99}
100
101// Bytes returns the public key in uncompressed format (65 bytes: 0x04 || x || y)
102func (pub *PublicKey) Bytes() []byte {
103 if pub.Point.IsInfinity() {
104 return []byte{0x00} // shouldn't happen with valid keys
105 }
106
107 result := make([]byte, 65)
108 result[0] = 0x04 // uncompressed prefix
109
110 xBytes := pub.Point.x.value.Bytes()
111 yBytes := pub.Point.y.value.Bytes()
112
113 // Copy x (padded to 32 bytes)
114 copy(result[1+(32-len(xBytes)):33], xBytes)
115 // Copy y (padded to 32 bytes)
116 copy(result[33+(32-len(yBytes)):65], yBytes)
117
118 return result
119}
120
121// BytesCompressed returns the public key in compressed format (33 bytes: prefix || x)
122// Prefix is 0x02 if y is even, 0x03 if y is odd
123func (pub *PublicKey) BytesCompressed() []byte {
124 if pub.Point.IsInfinity() {
125 return []byte{0x00}
126 }
127
128 result := make([]byte, 33)
129
130 // Prefix based on y parity
131 if pub.Point.y.value.Bit(0) == 0 {
132 result[0] = 0x02 // y is even
133 } else {
134 result[0] = 0x03 // y is odd
135 }
136
137 xBytes := pub.Point.x.value.Bytes()
138 copy(result[1+(32-len(xBytes)):33], xBytes)
139
140 return result
141}
142
143// Hex returns the public key as uncompressed hex (130 characters)
144func (pub *PublicKey) Hex() string {
145 return fmt.Sprintf("%x", pub.Bytes())
146}
147
148// HexCompressed returns the public key as compressed hex (66 characters)
149func (pub *PublicKey) HexCompressed() string {
150 return fmt.Sprintf("%x", pub.BytesCompressed())
151}
152
153// Equal checks if two public keys are the same
154func (pub *PublicKey) Equal(other *PublicKey) bool {
155 return pub.Point.Equal(other.Point)
156}
157
158// ParsePublicKeyXOnly parses a 32-byte x-only public key (BIP-340 format)
159func ParsePublicKeyXOnly(xOnlyBytes []byte) (*PublicKey, error) {
160 if len(xOnlyBytes) != 32 {
161 return nil, fmt.Errorf("x-only public key must be 32 bytes, got %d", len(xOnlyBytes))
162 }
163
164 x := new(big.Int).SetBytes(xOnlyBytes)
165 point, err := LiftX(x)
166 if err != nil {
167 return nil, fmt.Errorf("invalid x-only public key: %w", err)
168 }
169
170 return &PublicKey{Point: point}, nil
171}
diff --git a/internal/secp256k1/point.go b/internal/secp256k1/point.go
new file mode 100644
index 0000000..1def176
--- /dev/null
+++ b/internal/secp256k1/point.go
@@ -0,0 +1,171 @@
1package secp256k1
2
3import (
4 "fmt"
5 "math/big"
6)
7
8// secp256k1 curve: y² = x³ + 7
9// The 'a' coefficient is 0, 'b' is 7
10var curveB = NewFieldElementFromInt64(7)
11
12// Generator point G for secp256k1
13var (
14 Gx, _ = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
15 Gy, _ = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
16 G = &Point{
17 x: NewFieldElement(Gx),
18 y: NewFieldElement(Gy),
19 infinity: false,
20 }
21)
22
23// Curve order (number of points on the curve)
24var N, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
25
26// Point represents a point on the secp256k1 curve
27type Point struct {
28 x, y *FieldElement
29 infinity bool // true if this is the point at infinity (identity)
30}
31
32// NewPoint creates a point from x, y coordinates
33// Returns error if the point is not on the curve
34func NewPoint(x, y *FieldElement) (*Point, error) {
35 p := &Point{x: x, y: y, infinity: false}
36 if !p.IsOnCurve() {
37 return nil, fmt.Errorf("point (%s, %s) is not on the curve", x.String(), y.String())
38 }
39 return p, nil
40}
41
42// Infinity returns the point at infinity (identity element)
43func Infinity() *Point {
44 return &Point{infinity: true}
45}
46
47// IsInfinity returns true if this is the point at infinity
48func (p *Point) IsInfinity() bool {
49 return p.infinity
50}
51
52// IsOnCurve checks if the point satisfies y² = x³ + 7
53func (p *Point) IsOnCurve() bool {
54 if p.infinity {
55 return true
56 }
57 // y² = x³ + 7
58 left := p.y.Square() // y²
59 right := p.x.Square().Mul(p.x).Add(curveB) // x³ + 7
60 return left.Equal(right)
61}
62
63// Equal checks if two points are the same
64func (p *Point) Equal(q *Point) bool {
65 if p.infinity && q.infinity {
66 return true
67 }
68 if p.infinity || q.infinity {
69 return false
70 }
71 return p.x.Equal(q.x) && p.y.Equal(q.y)
72}
73
74// Add returns p + q using the elliptic curve addition formulas
75func (p *Point) Add(q *Point) *Point {
76 // Handle infinity (identity element)
77 if p.infinity {
78 return q
79 }
80 if q.infinity {
81 return p
82 }
83
84 // If points are inverses (same x, opposite y), return infinity
85 if p.x.Equal(q.x) && !p.y.Equal(q.y) {
86 return Infinity()
87 }
88
89 // If points are the same, use doubling formula
90 if p.Equal(q) {
91 return p.Double()
92 }
93
94 // Standard addition formula for P ≠ Q:
95 // slope = (y2 - y1) / (x2 - x1)
96 // x3 = slope² - x1 - x2
97 // y3 = slope * (x1 - x3) - y1
98
99 slope := q.y.Sub(p.y).Div(q.x.Sub(p.x))
100 x3 := slope.Square().Sub(p.x).Sub(q.x)
101 y3 := slope.Mul(p.x.Sub(x3)).Sub(p.y)
102
103 return &Point{x: x3, y: y3, infinity: false}
104}
105
106// Double returns 2P (point added to itself)
107func (p *Point) Double() *Point {
108 if p.infinity {
109 return Infinity()
110 }
111
112 // If y = 0, tangent is vertical, return infinity
113 if p.y.IsZero() {
114 return Infinity()
115 }
116
117 // Doubling formula:
118 // slope = (3x² + a) / (2y) -- for secp256k1, a = 0
119 // x3 = slope² - 2x
120 // y3 = slope * (x - x3) - y
121
122 three := NewFieldElementFromInt64(3)
123 two := NewFieldElementFromInt64(2)
124
125 slope := three.Mul(p.x.Square()).Div(two.Mul(p.y))
126 x3 := slope.Square().Sub(two.Mul(p.x))
127 y3 := slope.Mul(p.x.Sub(x3)).Sub(p.y)
128
129 return &Point{x: x3, y: y3, infinity: false}
130}
131
132// ScalarMul returns k * P (point multiplied by scalar)
133// Uses double-and-add algorithm
134func (p *Point) ScalarMul(k *big.Int) *Point {
135 result := Infinity()
136 addend := p
137
138 // Clone k so we don't modify the original
139 scalar := new(big.Int).Set(k)
140
141 for scalar.Sign() > 0 {
142 // If lowest bit is 1, add current addend
143 if scalar.Bit(0) == 1 {
144 result = result.Add(addend)
145 }
146 // Double the addend
147 addend = addend.Double()
148 // Shift scalar right by 1
149 scalar.Rsh(scalar, 1)
150 }
151
152 return result
153}
154
155// Negate returns -P (same x, negated y)
156func (p *Point) Negate() *Point {
157 if p.infinity {
158 return Infinity()
159 }
160 // -y mod P
161 negY := NewFieldElement(new(big.Int).Sub(P, p.y.value))
162 return &Point{x: p.x.Clone(), y: negY, infinity: false}
163}
164
165// String returns a readable representation
166func (p *Point) String() string {
167 if p.infinity {
168 return "Point(infinity)"
169 }
170 return fmt.Sprintf("Point(%s, %s)", p.x.String()[:8]+"...", p.y.String()[:8]+"...")
171}
diff --git a/internal/secp256k1/schnorr.go b/internal/secp256k1/schnorr.go
new file mode 100644
index 0000000..ae7fa75
--- /dev/null
+++ b/internal/secp256k1/schnorr.go
@@ -0,0 +1,236 @@
1package secp256k1
2
3import (
4 "crypto/sha256"
5 "fmt"
6 "math/big"
7)
8
9// Signature represents a Schnorr signature (r, s)
10// r is the x-coordinate of R (32 bytes)
11// s is the scalar response (32 bytes)
12type Signature struct {
13 R *big.Int // x-coordinate of the nonce point
14 S *big.Int // the response scalar
15}
16
17// TaggedHash computes SHA256(SHA256(tag) || SHA256(tag) || msg)
18// This is BIP-340's domain separation technique
19func TaggedHash(tag string, data ...[]byte) []byte {
20 tagHash := sha256.Sum256([]byte(tag))
21 h := sha256.New()
22 h.Write(tagHash[:])
23 h.Write(tagHash[:])
24 for _, d := range data {
25 h.Write(d)
26 }
27 return h.Sum(nil)
28}
29
30// LiftX recovers a point from just its x-coordinate
31// Returns the point with even y (BIP-340 convention)
32func LiftX(x *big.Int) (*Point, error) {
33 // Check x is in valid range
34 if x.Sign() < 0 || x.Cmp(P) >= 0 {
35 return nil, fmt.Errorf("x out of range")
36 }
37
38 // Compute y² = x³ + 7
39 xFe := NewFieldElement(x)
40 ySquared := xFe.Square().Mul(xFe).Add(curveB)
41
42 // Compute y = sqrt(y²) mod p
43 // For secp256k1, sqrt(a) = a^((p+1)/4) mod p
44 exp := new(big.Int).Add(P, big.NewInt(1))
45 exp.Div(exp, big.NewInt(4))
46 y := new(big.Int).Exp(ySquared.value, exp, P)
47
48 // Verify it's actually a square root
49 ySquaredCheck := new(big.Int).Mul(y, y)
50 ySquaredCheck.Mod(ySquaredCheck, P)
51 if ySquaredCheck.Cmp(ySquared.value) != 0 {
52 return nil, fmt.Errorf("x is not on the curve")
53 }
54
55 // BIP-340: use the even y
56 if y.Bit(0) == 1 {
57 y.Sub(P, y)
58 }
59
60 return &Point{
61 x: NewFieldElement(x),
62 y: NewFieldElement(y),
63 infinity: false,
64 }, nil
65}
66
67// hasEvenY returns true if the point's y-coordinate is even
68func hasEvenY(p *Point) bool {
69 if p.infinity {
70 return false
71 }
72 return p.y.value.Bit(0) == 0
73}
74
75// xOnlyBytes returns the 32-byte x-coordinate of a public key
76func (pub *PublicKey) XOnlyBytes() []byte {
77 result := make([]byte, 32)
78 xBytes := pub.Point.x.value.Bytes()
79 copy(result[32-len(xBytes):], xBytes)
80 return result
81}
82
83// Sign creates a Schnorr signature for a message
84// Follows BIP-340 specification
85// aux is optional auxiliary randomness (32 bytes); nil uses zeros
86func Sign(priv *PrivateKey, message []byte, aux ...[]byte) (*Signature, error) {
87 // Get the public key point
88 P := G.ScalarMul(priv.D)
89
90 // BIP-340: if P.y is odd, negate the private key
91 d := new(big.Int).Set(priv.D)
92 if !hasEvenY(P) {
93 d.Sub(N, d)
94 P = P.Negate()
95 }
96
97 // Serialize public key x-coordinate (32 bytes)
98 pBytes := make([]byte, 32)
99 pxBytes := P.x.value.Bytes()
100 copy(pBytes[32-len(pxBytes):], pxBytes)
101
102 // BIP-340 nonce generation:
103 // t = d XOR tagged_hash("BIP0340/aux", aux)
104 // k = tagged_hash("BIP0340/nonce", t || P || m)
105 // For deterministic signing, use aux = 32 zero bytes
106 dBytes := make([]byte, 32)
107 dBytesRaw := d.Bytes()
108 copy(dBytes[32-len(dBytesRaw):], dBytesRaw)
109
110 // Use provided aux or default to 32 zero bytes
111 var auxBytes []byte
112 if len(aux) > 0 && len(aux[0]) == 32 {
113 auxBytes = aux[0]
114 } else {
115 auxBytes = make([]byte, 32)
116 }
117 auxHash := TaggedHash("BIP0340/aux", auxBytes)
118
119 t := make([]byte, 32)
120 for i := 0; i < 32; i++ {
121 t[i] = dBytes[i] ^ auxHash[i]
122 }
123
124 kHash := TaggedHash("BIP0340/nonce", t, pBytes, message)
125 k := new(big.Int).SetBytes(kHash)
126 k.Mod(k, N)
127
128 // k cannot be zero (extremely unlikely)
129 if k.Sign() == 0 {
130 return nil, fmt.Errorf("nonce is zero")
131 }
132
133 // R = k * G
134 R := G.ScalarMul(k)
135
136 // BIP-340: if R.y is odd, negate k
137 if !hasEvenY(R) {
138 k.Sub(N, k)
139 R = R.Negate()
140 }
141
142 // Serialize R.x (32 bytes)
143 rBytes := make([]byte, 32)
144 rxBytes := R.x.value.Bytes()
145 copy(rBytes[32-len(rxBytes):], rxBytes)
146
147 // Compute challenge e = hash(R.x || P.x || m)
148 eHash := TaggedHash("BIP0340/challenge", rBytes, pBytes, message)
149 e := new(big.Int).SetBytes(eHash)
150 e.Mod(e, N)
151
152 // Compute s = k + e * d (mod N)
153 s := new(big.Int).Mul(e, d)
154 s.Add(s, k)
155 s.Mod(s, N)
156
157 return &Signature{
158 R: R.x.value,
159 S: s,
160 }, nil
161}
162
163// Verify checks if a Schnorr signature is valid
164// Follows BIP-340 specification
165func Verify(pub *PublicKey, message []byte, sig *Signature) bool {
166 // Check signature values are in range
167 if sig.R.Sign() < 0 || sig.R.Cmp(P) >= 0 {
168 return false
169 }
170 if sig.S.Sign() < 0 || sig.S.Cmp(N) >= 0 {
171 return false
172 }
173
174 // Lift R from x-coordinate
175 R, err := LiftX(sig.R)
176 if err != nil {
177 return false
178 }
179
180 // Get public key with even y
181 P := pub.Point
182 if !hasEvenY(P) {
183 P = P.Negate()
184 }
185
186 // Serialize for hashing
187 rBytes := make([]byte, 32)
188 rxBytes := sig.R.Bytes()
189 copy(rBytes[32-len(rxBytes):], rxBytes)
190
191 pBytes := make([]byte, 32)
192 pxBytes := P.x.value.Bytes()
193 copy(pBytes[32-len(pxBytes):], pxBytes)
194
195 // Compute challenge e = hash(R.x || P.x || m)
196 eHash := TaggedHash("BIP0340/challenge", rBytes, pBytes, message)
197 e := new(big.Int).SetBytes(eHash)
198 e.Mod(e, N)
199
200 // Verify: s*G == R + e*P
201 sG := G.ScalarMul(sig.S)
202 eP := P.ScalarMul(e)
203 expected := R.Add(eP)
204
205 return sG.Equal(expected)
206}
207
208// Bytes returns the signature as 64 bytes (r || s)
209func (sig *Signature) Bytes() []byte {
210 result := make([]byte, 64)
211
212 rBytes := sig.R.Bytes()
213 sBytes := sig.S.Bytes()
214
215 copy(result[32-len(rBytes):32], rBytes)
216 copy(result[64-len(sBytes):64], sBytes)
217
218 return result
219}
220
221// SignatureFromBytes parses a 64-byte signature
222func SignatureFromBytes(b []byte) (*Signature, error) {
223 if len(b) != 64 {
224 return nil, fmt.Errorf("signature must be 64 bytes")
225 }
226
227 r := new(big.Int).SetBytes(b[:32])
228 s := new(big.Int).SetBytes(b[32:])
229
230 return &Signature{R: r, S: s}, nil
231}
232
233// Hex returns the signature as a 128-character hex string
234func (sig *Signature) Hex() string {
235 return fmt.Sprintf("%x", sig.Bytes())
236}