p256k1_signer.go raw
1 //go:build !js && !wasm && !tinygo && !wasm32
2
3 package signer
4
5 import (
6 "errors"
7
8 "next.orly.dev/pkg/p256k1/exchange"
9 "next.orly.dev/pkg/p256k1/keys"
10 "next.orly.dev/pkg/p256k1/schnorr"
11 )
12
13 // P256K1Signer implements the I and Gen interfaces using the p256k1 domain packages
14 type P256K1Signer struct {
15 keypair *keys.KeyPair
16 xonlyPub *schnorr.XOnlyPubkey
17 hasSecret bool // Whether we have the secret key (if false, can only verify)
18 sigBuf []byte // Reusable buffer for signatures to avoid allocations
19 ecdhBuf []byte // Reusable buffer for ECDH shared secrets
20 }
21
22 // NewP256K1Signer creates a new P256K1Signer instance
23 func NewP256K1Signer() *P256K1Signer {
24 return &P256K1Signer{
25 hasSecret: false,
26 }
27 }
28
29 // Generate creates a fresh new key pair from system entropy, and ensures it is even (so ECDH works)
30 func (s *P256K1Signer) Generate() error {
31 kp, err := keys.Generate()
32 if err != nil {
33 return err
34 }
35
36 // Ensure even Y coordinate for ECDH compatibility
37 // Get x-only pubkey and check parity
38 xonly, parity, err := schnorr.XOnlyFromPubkey(kp.Pubkey())
39 if err != nil {
40 return err
41 }
42
43 // If parity is 1 (odd Y), negate the secret key
44 if parity == 1 {
45 seckey, err := keys.NegatePrivate(kp.Seckey())
46 if err != nil {
47 return errors.New("failed to negate secret key")
48 }
49 // Recreate keypair with negated secret key
50 kp, err = keys.Create(seckey)
51 if err != nil {
52 return err
53 }
54 // Get x-only pubkey again (should be even now)
55 xonly, _, err = schnorr.XOnlyFromPubkey(kp.Pubkey())
56 if err != nil {
57 return err
58 }
59 }
60
61 s.keypair = kp
62 s.xonlyPub = xonly
63 s.hasSecret = true
64
65 return nil
66 }
67
68 // InitSec initialises the secret (signing) key from the raw bytes, and also derives the public key
69 func (s *P256K1Signer) InitSec(sec []byte) error {
70 if len(sec) != 32 {
71 return errors.New("secret key must be 32 bytes")
72 }
73
74 kp, err := keys.Create(sec)
75 if err != nil {
76 return err
77 }
78
79 // Ensure even Y coordinate for ECDH compatibility
80 xonly, parity, err := schnorr.XOnlyFromPubkey(kp.Pubkey())
81 if err != nil {
82 return err
83 }
84
85 // If parity is 1 (odd Y), negate the secret key and recompute public key
86 // With windowed optimization, this is now much faster than before
87 if parity == 1 {
88 seckey, err := keys.NegatePrivate(kp.Seckey())
89 if err != nil {
90 return errors.New("failed to negate secret key")
91 }
92 // Recreate keypair with negated secret key
93 // This is now optimized with windowed precomputed tables
94 kp, err = keys.Create(seckey)
95 if err != nil {
96 return err
97 }
98 xonly, _, err = schnorr.XOnlyFromPubkey(kp.Pubkey())
99 if err != nil {
100 return err
101 }
102 }
103
104 s.keypair = kp
105 s.xonlyPub = xonly
106 s.hasSecret = true
107
108 return nil
109 }
110
111 // InitPub initializes the public (verification) key from raw bytes, this is expected to be an x-only 32 byte pubkey
112 func (s *P256K1Signer) InitPub(pub []byte) error {
113 if len(pub) != 32 {
114 return errors.New("public key must be 32 bytes")
115 }
116
117 xonly, err := schnorr.ParseXOnlyPubkey(pub)
118 if err != nil {
119 return err
120 }
121
122 s.xonlyPub = xonly
123 s.keypair = nil
124 s.hasSecret = false
125
126 return nil
127 }
128
129 // Sec returns the secret key bytes
130 func (s *P256K1Signer) Sec() []byte {
131 if !s.hasSecret || s.keypair == nil {
132 return nil
133 }
134 return s.keypair.Seckey()
135 }
136
137 // Pub returns the public key bytes (x-only schnorr pubkey)
138 // The returned slice is backed by an internal buffer that may be
139 // reused on subsequent calls. Copy if you need to retain it.
140 func (s *P256K1Signer) Pub() []byte {
141 if s.xonlyPub == nil {
142 return nil
143 }
144 serialized := s.xonlyPub.Serialize()
145 return serialized[:]
146 }
147
148 // Sign creates a signature using the stored secret key
149 // The returned slice is backed by an internal buffer that may be
150 // reused on subsequent calls. Copy if you need to retain it.
151 func (s *P256K1Signer) Sign(msg []byte) (sig []byte, err error) {
152 if !s.hasSecret || s.keypair == nil {
153 return nil, errors.New("no secret key available for signing")
154 }
155
156 if len(msg) != 32 {
157 return nil, errors.New("message must be 32 bytes")
158 }
159
160 // Pre-allocate buffer to reuse across calls
161 if cap(s.sigBuf) < 64 {
162 s.sigBuf = make([]byte, 64)
163 } else {
164 s.sigBuf = s.sigBuf[:64]
165 }
166
167 if err := schnorr.SignRaw(s.sigBuf, msg, s.keypair, nil); err != nil {
168 return nil, err
169 }
170
171 return s.sigBuf, nil
172 }
173
174 // Verify checks a message hash and signature match the stored public key
175 func (s *P256K1Signer) Verify(msg, sig []byte) (valid bool, err error) {
176 if s.xonlyPub == nil {
177 return false, errors.New("no public key available for verification")
178 }
179
180 if len(msg) != 32 {
181 return false, errors.New("message must be 32 bytes")
182 }
183
184 if len(sig) != 64 {
185 return false, errors.New("signature must be 64 bytes")
186 }
187
188 valid = schnorr.VerifyRaw(sig, msg, s.xonlyPub)
189 return valid, nil
190 }
191
192 // Zero wipes the secret key to prevent memory leaks
193 func (s *P256K1Signer) Zero() {
194 if s.keypair != nil {
195 s.keypair.Clear()
196 s.keypair = nil
197 }
198 s.hasSecret = false
199 // Note: x-only pubkey doesn't contain sensitive data, but we can clear it too
200 s.xonlyPub = nil
201 }
202
203 // ECDH returns a shared secret derived using Elliptic Curve Diffie-Hellman on the I secret and provided pubkey
204 // The returned slice is backed by an internal buffer that may be
205 // reused on subsequent calls. Copy if you need to retain it.
206 func (s *P256K1Signer) ECDH(pub []byte) (secret []byte, err error) {
207 if !s.hasSecret || s.keypair == nil {
208 return nil, errors.New("no secret key available for ECDH")
209 }
210
211 if len(pub) != 32 {
212 return nil, errors.New("public key must be 32 bytes")
213 }
214
215 // Convert x-only pubkey (32 bytes) to compressed public key (33 bytes) with even Y
216 var compressedPub [33]byte
217 compressedPub[0] = 0x02 // Even Y
218 copy(compressedPub[1:], pub)
219
220 // Parse the compressed public key
221 pubkey, err := keys.ParsePublic(compressedPub[:])
222 if err != nil {
223 return nil, err
224 }
225
226 // Pre-allocate buffer to reuse across calls
227 if cap(s.ecdhBuf) < 32 {
228 s.ecdhBuf = make([]byte, 32)
229 } else {
230 s.ecdhBuf = s.ecdhBuf[:32]
231 }
232
233 // Compute ECDH shared secret using standard ECDH (hashes the point)
234 if err := exchange.SharedSecretRaw(s.ecdhBuf, pubkey, s.keypair.Seckey()); err != nil {
235 return nil, err
236 }
237
238 return s.ecdhBuf, nil
239 }
240
241 // ECDHRaw returns the raw shared secret point (x-coordinate only, 32 bytes) without hashing.
242 // This is needed for protocols like NIP-44 that do their own key derivation.
243 // The pub parameter can be either:
244 // - 32 bytes (x-only): will be converted to compressed format with 0x02 prefix
245 // - 33 bytes (compressed): will be used as-is
246 func (s *P256K1Signer) ECDHRaw(pub []byte) (sharedX []byte, err error) {
247 if !s.hasSecret || s.keypair == nil {
248 return nil, errors.New("no secret key available for ECDH")
249 }
250
251 var compressedPub [33]byte
252 if len(pub) == 32 {
253 // X-only format - convert to compressed with even Y
254 compressedPub[0] = 0x02
255 copy(compressedPub[1:], pub)
256 } else if len(pub) == 33 {
257 // Already compressed
258 copy(compressedPub[:], pub)
259 } else {
260 return nil, errors.New("public key must be 32 bytes (x-only) or 33 bytes (compressed)")
261 }
262
263 // Parse the compressed public key
264 pubkey, err := keys.ParsePublic(compressedPub[:])
265 if err != nil {
266 // If x-only with even Y failed, try odd Y
267 if len(pub) == 32 {
268 compressedPub[0] = 0x03
269 pubkey, err = keys.ParsePublic(compressedPub[:])
270 if err != nil {
271 return nil, err
272 }
273 } else {
274 return nil, err
275 }
276 }
277
278 // Pre-allocate buffer to reuse across calls
279 if cap(s.ecdhBuf) < 32 {
280 s.ecdhBuf = make([]byte, 32)
281 } else {
282 s.ecdhBuf = s.ecdhBuf[:32]
283 }
284
285 // Compute raw ECDH (x-coordinate only, no hashing)
286 if err := exchange.XOnlySharedSecretRaw(s.ecdhBuf, pubkey, s.keypair.Seckey()); err != nil {
287 return nil, err
288 }
289
290 return s.ecdhBuf, nil
291 }
292
293 // PubCompressed returns the compressed public key (33 bytes with 0x02/0x03 prefix).
294 // This is needed for ECDH operations like NIP-44.
295 func (s *P256K1Signer) PubCompressed() (compressed []byte, err error) {
296 if !s.hasSecret || s.keypair == nil {
297 return nil, errors.New("keypair not initialized")
298 }
299
300 pubkey := s.keypair.Pubkey()
301 buf := keys.SerializePublic(pubkey, keys.Compressed)
302 if len(buf) != 33 {
303 return nil, errors.New("failed to serialize compressed public key")
304 }
305
306 return buf, nil
307 }
308
309 // P256K1Gen implements the Gen interface for nostr BIP-340 key generation
310 type P256K1Gen struct {
311 keypair *keys.KeyPair
312 xonlyPub *schnorr.XOnlyPubkey
313 compressedPub *keys.PublicKey
314 pubBuf []byte // Reusable buffer to avoid allocations in KeyPairBytes
315 }
316
317 // NewP256K1Gen creates a new P256K1Gen instance
318 func NewP256K1Gen() *P256K1Gen {
319 return &P256K1Gen{}
320 }
321
322 // Generate gathers entropy and derives pubkey bytes for matching, this returns the 33 byte compressed form for checking the oddness of the Y coordinate
323 func (g *P256K1Gen) Generate() (pubBytes []byte, err error) {
324 kp, err := keys.Generate()
325 if err != nil {
326 return nil, err
327 }
328
329 g.keypair = kp
330
331 // Get compressed public key (33 bytes)
332 pubkey := kp.Pubkey()
333
334 compressed := keys.SerializePublic(pubkey, keys.Compressed)
335 if len(compressed) != 33 {
336 return nil, errors.New("failed to serialize compressed public key")
337 }
338
339 g.compressedPub = pubkey
340
341 return compressed, nil
342 }
343
344 // Negate flips the public key Y coordinate between odd and even
345 func (g *P256K1Gen) Negate() {
346 if g.keypair == nil {
347 return
348 }
349
350 // Negate the secret key
351 seckey, err := keys.NegatePrivate(g.keypair.Seckey())
352 if err != nil {
353 return
354 }
355
356 // Recreate keypair with negated secret key
357 kp, err := keys.Create(seckey)
358 if err != nil {
359 return
360 }
361
362 g.keypair = kp
363
364 // Update compressed pubkey
365 pubkey := kp.Pubkey()
366 g.compressedPub = pubkey
367
368 // Update x-only pubkey
369 xonly, err := kp.XOnlyPubkey()
370 if err == nil {
371 g.xonlyPub = xonly
372 }
373 }
374
375 // KeyPairBytes returns the raw bytes of the secret and public key, this returns the 32 byte X-only pubkey
376 // The returned pubkey slice is backed by an internal buffer that may be
377 // reused on subsequent calls. Copy if you need to retain it.
378 func (g *P256K1Gen) KeyPairBytes() (secBytes, cmprPubBytes []byte) {
379 if g.keypair == nil {
380 return nil, nil
381 }
382
383 secBytes = g.keypair.Seckey()
384
385 if g.xonlyPub == nil {
386 xonly, err := g.keypair.XOnlyPubkey()
387 if err != nil {
388 return secBytes, nil
389 }
390 g.xonlyPub = xonly
391 }
392
393 // Pre-allocate buffer to reuse across calls
394 if cap(g.pubBuf) < 32 {
395 g.pubBuf = make([]byte, 32)
396 } else {
397 g.pubBuf = g.pubBuf[:32]
398 }
399
400 // Copy the serialized public key into our buffer
401 serialized := g.xonlyPub.Serialize()
402 copy(g.pubBuf, serialized[:])
403 cmprPubBytes = g.pubBuf
404
405 return secBytes, cmprPubBytes
406 }
407