aboutsummaryrefslogtreecommitdiffstats
path: root/internal/secp256k1/keys.go
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/keys.go
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/keys.go')
-rw-r--r--internal/secp256k1/keys.go171
1 files changed, 171 insertions, 0 deletions
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}