@ -5,43 +5,71 @@ import (
"crypto/rand"
"crypto/rand"
"lol.mleku.dev/errorf"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/crypto/ec/schnorr"
"next.orly.dev/pkg/crypto/ec/secp256k1"
secp "next.orly.dev/pkg/crypto/p8k"
secp "next.orly.dev/pkg/crypto/p8k"
"next.orly.dev/pkg/interfaces/signer"
"next.orly.dev/pkg/interfaces/signer"
)
)
// Signer implements the signer.I interface using p8k.mleku.dev
// Signer implements the signer.I interface using p8k.mleku.dev or pure Go fallback
type Signer struct {
type Signer struct {
// libsecp256k1 implementation
ctx * secp . Context
ctx * secp . Context
secKey [ ] byte
secKey [ ] byte
pubKey [ ] byte
pubKey [ ] byte
keypair secp . Keypair
keypair secp . Keypair
// Pure Go fallback implementation
fallback * FallbackSigner
}
// FallbackSigner implements the signer.I interface using pure Go btcec/secp256k1
type FallbackSigner struct {
privKey * secp256k1 . SecretKey
pubKey * secp256k1 . PublicKey
xonlyPub [ ] byte
}
}
// Ensure Signer implements signer.I
// Ensure Signer implements signer.I
var _ signer . I = ( * Signer ) ( nil )
var _ signer . I = ( * Signer ) ( nil )
// New creates a new P8K signer
// New creates a new P8K signer, falling back to pure Go implementation if libsecp256k1 is unavailable
func New ( ) ( s * Signer , err error ) {
func New ( ) ( s * Signer , err error ) {
var ctx * secp . Context
var ctx * secp . Context
if ctx , err = secp . NewContext ( secp . ContextSign | secp . ContextVerify ) ; err != nil {
if ctx , err = secp . NewContext ( secp . ContextSign | secp . ContextVerify ) ; err != nil {
return
// Fallback to pure Go implementation
fallback , fallbackErr := newFallbackSigner ( )
if fallbackErr != nil {
return nil , fallbackErr
}
s = & Signer { fallback : fallback }
return s , nil
}
}
s = & Signer { ctx : ctx }
s = & Signer { ctx : ctx }
return
return s , nil
}
}
// MustNew creates a new P8K signer and panics on error
// MustNew creates a new P8K signer and panics on error
func MustNew ( ) ( s * Signer ) {
func MustNew ( ) * Signer {
var err error
s , err := New ( )
if s , err = New ( ) ; err != nil {
if err != nil {
panic ( err )
panic ( err )
}
}
return
return s
}
// newFallbackSigner creates a new fallback signer using pure Go implementation
func newFallbackSigner ( ) ( * FallbackSigner , error ) {
return & FallbackSigner { } , nil
}
}
// Generate creates a fresh new key pair from system entropy, and ensures it is even (so
// Generate creates a fresh new key pair from system entropy, and ensures it is even (so
// ECDH works).
// ECDH works).
func ( s * Signer ) Generate ( ) ( err error ) {
func ( s * Signer ) Generate ( ) ( err error ) {
if s . fallback != nil {
return s . fallback . Generate ( )
}
s . secKey = make ( [ ] byte , 32 )
s . secKey = make ( [ ] byte , 32 )
if _ , err = rand . Read ( s . secKey ) ; err != nil {
if _ , err = rand . Read ( s . secKey ) ; err != nil {
return
return
@ -70,6 +98,10 @@ func (s *Signer) Generate() (err error) {
// InitSec initialises the secret (signing) key from the raw bytes, and also
// InitSec initialises the secret (signing) key from the raw bytes, and also
// derives the public key because it can.
// derives the public key because it can.
func ( s * Signer ) InitSec ( sec [ ] byte ) ( err error ) {
func ( s * Signer ) InitSec ( sec [ ] byte ) ( err error ) {
if s . fallback != nil {
return s . fallback . InitSec ( sec )
}
if len ( sec ) != 32 {
if len ( sec ) != 32 {
return errorf . E ( "secret key must be 32 bytes" )
return errorf . E ( "secret key must be 32 bytes" )
}
}
@ -100,6 +132,10 @@ func (s *Signer) InitSec(sec []byte) (err error) {
// InitPub initializes the public (verification) key from raw bytes, this is
// InitPub initializes the public (verification) key from raw bytes, this is
// expected to be an x-only 32 byte pubkey.
// expected to be an x-only 32 byte pubkey.
func ( s * Signer ) InitPub ( pub [ ] byte ) ( err error ) {
func ( s * Signer ) InitPub ( pub [ ] byte ) ( err error ) {
if s . fallback != nil {
return s . fallback . InitPub ( pub )
}
if len ( pub ) != 32 {
if len ( pub ) != 32 {
return errorf . E ( "public key must be 32 bytes" )
return errorf . E ( "public key must be 32 bytes" )
}
}
@ -111,17 +147,31 @@ func (s *Signer) InitPub(pub []byte) (err error) {
// Sec returns the secret key bytes.
// Sec returns the secret key bytes.
func ( s * Signer ) Sec ( ) [ ] byte {
func ( s * Signer ) Sec ( ) [ ] byte {
if s . fallback != nil {
return s . fallback . Sec ( )
}
return s . secKey
return s . secKey
}
}
// Pub returns the public key bytes (x-only schnorr pubkey).
// Pub returns the public key bytes (x-only schnorr pubkey).
func ( s * Signer ) Pub ( ) [ ] byte {
func ( s * Signer ) Pub ( ) [ ] byte {
if s . fallback != nil {
return s . fallback . Pub ( )
}
return s . pubKey
return s . pubKey
}
}
// PubCompressed returns the compressed public key (33 bytes with 0x02/0x03 prefix).
// PubCompressed returns the compressed public key (33 bytes with 0x02/0x03 prefix).
// This is needed for ECDH operations like NIP-44.
// This is needed for ECDH operations like NIP-44.
func ( s * Signer ) PubCompressed ( ) ( compressed [ ] byte , err error ) {
func ( s * Signer ) PubCompressed ( ) ( compressed [ ] byte , err error ) {
if s . fallback != nil {
// For fallback, we need to derive the compressed key from the x-only key
if s . fallback . pubKey == nil {
return nil , errorf . E ( "public key not initialized" )
}
return s . fallback . pubKey . SerializeCompressed ( ) , nil
}
if len ( s . keypair ) == 0 {
if len ( s . keypair ) == 0 {
return nil , errorf . E ( "keypair not initialized" )
return nil , errorf . E ( "keypair not initialized" )
}
}
@ -142,6 +192,10 @@ func (s *Signer) PubCompressed() (compressed []byte, err error) {
// Sign creates a signature using the stored secret key.
// Sign creates a signature using the stored secret key.
func ( s * Signer ) Sign ( msg [ ] byte ) ( sig [ ] byte , err error ) {
func ( s * Signer ) Sign ( msg [ ] byte ) ( sig [ ] byte , err error ) {
if s . fallback != nil {
return s . fallback . Sign ( msg )
}
if len ( s . keypair ) == 0 {
if len ( s . keypair ) == 0 {
return nil , errorf . E ( "keypair not initialized" )
return nil , errorf . E ( "keypair not initialized" )
}
}
@ -162,6 +216,10 @@ func (s *Signer) Sign(msg []byte) (sig []byte, err error) {
// Verify checks a message hash and signature match the stored public key.
// Verify checks a message hash and signature match the stored public key.
func ( s * Signer ) Verify ( msg , sig [ ] byte ) ( valid bool , err error ) {
func ( s * Signer ) Verify ( msg , sig [ ] byte ) ( valid bool , err error ) {
if s . fallback != nil {
return s . fallback . Verify ( msg , sig )
}
if s . pubKey == nil {
if s . pubKey == nil {
return false , errorf . E ( "public key not initialized" )
return false , errorf . E ( "public key not initialized" )
}
}
@ -175,6 +233,11 @@ func (s *Signer) Verify(msg, sig []byte) (valid bool, err error) {
// Zero wipes the secret key to prevent memory leaks.
// Zero wipes the secret key to prevent memory leaks.
func ( s * Signer ) Zero ( ) {
func ( s * Signer ) Zero ( ) {
if s . fallback != nil {
s . fallback . Zero ( )
return
}
if s . secKey != nil {
if s . secKey != nil {
for i := range s . secKey {
for i := range s . secKey {
s . secKey [ i ] = 0
s . secKey [ i ] = 0
@ -199,6 +262,10 @@ func (s *Signer) ECDH(pub []byte) (secret []byte, err error) {
// - 32 bytes (x-only): will be converted to compressed format by trying 0x02 then 0x03
// - 32 bytes (x-only): will be converted to compressed format by trying 0x02 then 0x03
// - 33 bytes (compressed): will be used as-is
// - 33 bytes (compressed): will be used as-is
func ( s * Signer ) ECDHRaw ( pub [ ] byte ) ( sharedX [ ] byte , err error ) {
func ( s * Signer ) ECDHRaw ( pub [ ] byte ) ( sharedX [ ] byte , err error ) {
if s . fallback != nil {
return s . fallback . ECDHRaw ( pub )
}
if s . secKey == nil {
if s . secKey == nil {
return nil , errorf . E ( "secret key not initialized" )
return nil , errorf . E ( "secret key not initialized" )
}
}
@ -238,3 +305,173 @@ func (s *Signer) ECDHRaw(pub []byte) (sharedX []byte, err error) {
return
return
}
}
// FallbackSigner method implementations
// Generate creates a fresh new key pair from system entropy
func ( s * FallbackSigner ) Generate ( ) ( err error ) {
// Generate a new private key
if s . privKey , err = secp256k1 . GenerateSecretKey ( ) ; err != nil {
return errorf . E ( "failed to generate private key: %w" , err )
}
// Derive public key
if s . pubKey = s . privKey . PubKey ( ) ; s . pubKey == nil {
return errorf . E ( "failed to derive public key" )
}
// Get x-only public key (32 bytes) - compressed without the 0x02/0x03 prefix
compressed := s . pubKey . SerializeCompressed ( )
s . xonlyPub = make ( [ ] byte , 32 )
copy ( s . xonlyPub , compressed [ 1 : ] )
return nil
}
// InitSec initializes the secret key from raw bytes
func ( s * FallbackSigner ) InitSec ( sec [ ] byte ) ( err error ) {
if len ( sec ) != 32 {
return errorf . E ( "secret key must be 32 bytes" )
}
// Create private key from bytes
s . privKey = secp256k1 . SecKeyFromBytes ( sec )
if s . privKey . Key . IsZero ( ) {
return errorf . E ( "invalid secret key" )
}
// Derive public key
if s . pubKey = s . privKey . PubKey ( ) ; s . pubKey == nil {
return errorf . E ( "failed to derive public key" )
}
// Get x-only public key (32 bytes) - compressed without the 0x02/0x03 prefix
compressed := s . pubKey . SerializeCompressed ( )
s . xonlyPub = make ( [ ] byte , 32 )
copy ( s . xonlyPub , compressed [ 1 : ] )
return nil
}
// InitPub initializes the public key from raw bytes (x-only 32 bytes)
func ( s * FallbackSigner ) InitPub ( pub [ ] byte ) ( err error ) {
if len ( pub ) != 32 {
return errorf . E ( "public key must be 32 bytes" )
}
s . xonlyPub = make ( [ ] byte , 32 )
copy ( s . xonlyPub , pub )
return nil
}
// Sec returns the secret key bytes
func ( s * FallbackSigner ) Sec ( ) [ ] byte {
if s . privKey == nil {
return nil
}
return s . privKey . Serialize ( )
}
// Pub returns the public key bytes (x-only schnorr pubkey)
func ( s * FallbackSigner ) Pub ( ) [ ] byte {
return s . xonlyPub
}
// Sign creates a signature using the stored secret key
func ( s * FallbackSigner ) Sign ( msg [ ] byte ) ( sig [ ] byte , err error ) {
if s . privKey == nil {
return nil , errorf . E ( "private key not initialized" )
}
// Generate auxiliary randomness for BIP-340
var auxRand [ 32 ] byte
if _ , err = rand . Read ( auxRand [ : ] ) ; err != nil {
return nil , errorf . E ( "failed to generate aux randomness: %w" , err )
}
// Sign using Schnorr
var schnorrSig * schnorr . Signature
if schnorrSig , err = schnorr . Sign ( s . privKey , msg , schnorr . CustomNonce ( auxRand ) ) ; err != nil {
return nil , errorf . E ( "failed to sign: %w" , err )
}
return schnorrSig . Serialize ( ) , nil
}
// Verify checks a message hash and signature match the stored public key
func ( s * FallbackSigner ) Verify ( msg , sig [ ] byte ) ( valid bool , err error ) {
if s . pubKey == nil {
return false , errorf . E ( "public key not initialized" )
}
// Parse signature
var schnorrSig * schnorr . Signature
if schnorrSig , err = schnorr . ParseSignature ( sig ) ; err != nil {
return false , errorf . E ( "failed to parse signature: %w" , err )
}
// Verify signature
valid = schnorrSig . Verify ( msg , s . pubKey )
return valid , nil
}
// Zero wipes the secret key
func ( s * FallbackSigner ) Zero ( ) {
if s . privKey != nil {
privKeyBytes := s . privKey . Serialize ( )
for i := range privKeyBytes {
privKeyBytes [ i ] = 0
}
s . privKey = nil
}
if s . xonlyPub != nil {
for i := range s . xonlyPub {
s . xonlyPub [ i ] = 0
}
}
}
// ECDH returns a shared secret
func ( s * FallbackSigner ) ECDH ( pub [ ] byte ) ( secret [ ] byte , err error ) {
return s . ECDHRaw ( pub )
}
// ECDHRaw returns the raw shared secret (x-coordinate only)
func ( s * FallbackSigner ) ECDHRaw ( pub [ ] byte ) ( sharedX [ ] byte , err error ) {
if s . privKey == nil {
return nil , errorf . E ( "private key not initialized" )
}
var pubKeyFull [ ] byte
if len ( pub ) == 33 {
// Already compressed format
pubKeyFull = pub
} else if len ( pub ) == 32 {
// X-only format: try with 0x02 (even y), then 0x03 (odd y)
pubKeyFull = make ( [ ] byte , 33 )
pubKeyFull [ 0 ] = 0x02 // compressed even y
copy ( pubKeyFull [ 1 : ] , pub )
} else {
return nil , errorf . E ( "public key must be 32 bytes (x-only) or 33 bytes (compressed), got %d bytes" , len ( pub ) )
}
// Parse the public key
var parsedPub * secp256k1 . PublicKey
if parsedPub , err = secp256k1 . ParsePubKey ( pubKeyFull ) ; err != nil {
// If 32-byte x-only and even y failed, try odd y
if len ( pub ) == 32 {
pubKeyFull [ 0 ] = 0x03
if parsedPub , err = secp256k1 . ParsePubKey ( pubKeyFull ) ; err != nil {
return nil , err
}
} else {
return nil , err
}
}
// Compute ECDH
sharedX = secp256k1 . GenerateSharedSecret ( s . privKey , parsedPub )
return sharedX , nil
}