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