268 lines
7.3 KiB
Go
268 lines
7.3 KiB
Go
//go:build !js
|
|
|
|
package p256k1
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
|
|
"github.com/ebitengine/purego"
|
|
)
|
|
|
|
// LibSecp256k1 wraps the native libsecp256k1.so library using purego for CGO-free operation.
|
|
// This provides a way to benchmark against the C implementation without CGO.
|
|
type LibSecp256k1 struct {
|
|
lib uintptr
|
|
ctx uintptr
|
|
loaded bool
|
|
mu sync.RWMutex
|
|
|
|
// Function pointers
|
|
contextCreate func(uint) uintptr
|
|
contextDestroy func(uintptr)
|
|
contextRandomize func(uintptr, *byte) int
|
|
schnorrsigSign32 func(uintptr, *byte, *byte, *byte, *byte) int
|
|
schnorrsigVerify func(uintptr, *byte, *byte, uint, *byte) int
|
|
keypairCreate func(uintptr, *byte, *byte) int
|
|
keypairXonlyPub func(uintptr, *byte, *int, *byte) int
|
|
xonlyPubkeyParse func(uintptr, *byte, *byte) int
|
|
ecPubkeyCreate func(uintptr, *byte, *byte) int
|
|
ecPubkeyParse func(uintptr, *byte, *byte, uint) int
|
|
ecPubkeySerialize func(uintptr, *byte, *uint, *byte, uint) int
|
|
xonlyPubkeySerialize func(uintptr, *byte, *byte) int
|
|
ecdh func(uintptr, *byte, *byte, *byte, uintptr, uintptr) int
|
|
}
|
|
|
|
// Secp256k1 context flags
|
|
// In modern libsecp256k1, SECP256K1_CONTEXT_NONE = 1 is the only valid flag.
|
|
// The old SIGN (256) and VERIFY (257) flags are deprecated.
|
|
const (
|
|
libContextNone = 1
|
|
)
|
|
|
|
// Global instance
|
|
var (
|
|
libSecp *LibSecp256k1
|
|
libSecpOnce sync.Once
|
|
libSecpInitErr error
|
|
)
|
|
|
|
// GetLibSecp256k1 returns the global LibSecp256k1 instance, loading it if necessary.
|
|
// Returns nil and an error if the library cannot be loaded.
|
|
func GetLibSecp256k1() (*LibSecp256k1, error) {
|
|
libSecpOnce.Do(func() {
|
|
libSecp = &LibSecp256k1{}
|
|
// Try multiple paths to find the library
|
|
paths := []string{
|
|
"./libsecp256k1.so",
|
|
"../libsecp256k1.so",
|
|
"/home/mleku/src/p256k1.mleku.dev/libsecp256k1.so",
|
|
"libsecp256k1.so",
|
|
}
|
|
for _, path := range paths {
|
|
err := libSecp.Load(path)
|
|
if err == nil {
|
|
libSecpInitErr = nil
|
|
return
|
|
}
|
|
libSecpInitErr = err
|
|
}
|
|
})
|
|
if libSecpInitErr != nil {
|
|
return nil, libSecpInitErr
|
|
}
|
|
return libSecp, nil
|
|
}
|
|
|
|
// Load loads the libsecp256k1.so library from the given path.
|
|
func (l *LibSecp256k1) Load(path string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if l.loaded {
|
|
return nil
|
|
}
|
|
|
|
lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.lib = lib
|
|
|
|
// Register function pointers
|
|
purego.RegisterLibFunc(&l.contextCreate, lib, "secp256k1_context_create")
|
|
purego.RegisterLibFunc(&l.contextDestroy, lib, "secp256k1_context_destroy")
|
|
purego.RegisterLibFunc(&l.contextRandomize, lib, "secp256k1_context_randomize")
|
|
purego.RegisterLibFunc(&l.schnorrsigSign32, lib, "secp256k1_schnorrsig_sign32")
|
|
purego.RegisterLibFunc(&l.schnorrsigVerify, lib, "secp256k1_schnorrsig_verify")
|
|
purego.RegisterLibFunc(&l.keypairCreate, lib, "secp256k1_keypair_create")
|
|
purego.RegisterLibFunc(&l.keypairXonlyPub, lib, "secp256k1_keypair_xonly_pub")
|
|
purego.RegisterLibFunc(&l.xonlyPubkeyParse, lib, "secp256k1_xonly_pubkey_parse")
|
|
purego.RegisterLibFunc(&l.ecPubkeyCreate, lib, "secp256k1_ec_pubkey_create")
|
|
purego.RegisterLibFunc(&l.ecPubkeyParse, lib, "secp256k1_ec_pubkey_parse")
|
|
purego.RegisterLibFunc(&l.ecPubkeySerialize, lib, "secp256k1_ec_pubkey_serialize")
|
|
purego.RegisterLibFunc(&l.xonlyPubkeySerialize, lib, "secp256k1_xonly_pubkey_serialize")
|
|
purego.RegisterLibFunc(&l.ecdh, lib, "secp256k1_ecdh")
|
|
|
|
// Create context (modern libsecp256k1 uses SECP256K1_CONTEXT_NONE = 1)
|
|
l.ctx = l.contextCreate(libContextNone)
|
|
if l.ctx == 0 {
|
|
return errors.New("failed to create secp256k1 context")
|
|
}
|
|
|
|
// Randomize context for better security
|
|
var seed [32]byte
|
|
// Use zero seed for deterministic benchmarks
|
|
l.contextRandomize(l.ctx, &seed[0])
|
|
|
|
l.loaded = true
|
|
return nil
|
|
}
|
|
|
|
// Close releases the library resources.
|
|
func (l *LibSecp256k1) Close() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if !l.loaded {
|
|
return
|
|
}
|
|
|
|
if l.ctx != 0 {
|
|
l.contextDestroy(l.ctx)
|
|
l.ctx = 0
|
|
}
|
|
|
|
if l.lib != 0 {
|
|
purego.Dlclose(l.lib)
|
|
l.lib = 0
|
|
}
|
|
|
|
l.loaded = false
|
|
}
|
|
|
|
// IsLoaded returns true if the library is loaded.
|
|
func (l *LibSecp256k1) IsLoaded() bool {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.loaded
|
|
}
|
|
|
|
// SchnorrSign signs a 32-byte message using a 32-byte secret key.
|
|
// Returns a 64-byte signature.
|
|
func (l *LibSecp256k1) SchnorrSign(msg32, seckey32 []byte) ([]byte, error) {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if !l.loaded {
|
|
return nil, errors.New("library not loaded")
|
|
}
|
|
if len(msg32) != 32 {
|
|
return nil, errors.New("message must be 32 bytes")
|
|
}
|
|
if len(seckey32) != 32 {
|
|
return nil, errors.New("secret key must be 32 bytes")
|
|
}
|
|
|
|
// Create keypair from secret key
|
|
keypair := make([]byte, 96) // secp256k1_keypair is 96 bytes
|
|
if l.keypairCreate(l.ctx, &keypair[0], &seckey32[0]) != 1 {
|
|
return nil, errors.New("failed to create keypair")
|
|
}
|
|
|
|
// Sign
|
|
sig := make([]byte, 64)
|
|
if l.schnorrsigSign32(l.ctx, &sig[0], &msg32[0], &keypair[0], nil) != 1 {
|
|
return nil, errors.New("signing failed")
|
|
}
|
|
|
|
return sig, nil
|
|
}
|
|
|
|
// SchnorrVerify verifies a Schnorr signature.
|
|
func (l *LibSecp256k1) SchnorrVerify(sig64, msg32, pubkey32 []byte) bool {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if !l.loaded {
|
|
return false
|
|
}
|
|
if len(sig64) != 64 || len(msg32) != 32 || len(pubkey32) != 32 {
|
|
return false
|
|
}
|
|
|
|
// Parse x-only pubkey using secp256k1_xonly_pubkey_parse
|
|
xonlyPubkey := make([]byte, 64) // secp256k1_xonly_pubkey is 64 bytes
|
|
if l.xonlyPubkeyParse(l.ctx, &xonlyPubkey[0], &pubkey32[0]) != 1 {
|
|
return false
|
|
}
|
|
|
|
result := l.schnorrsigVerify(l.ctx, &sig64[0], &msg32[0], 32, &xonlyPubkey[0])
|
|
return result == 1
|
|
}
|
|
|
|
// CreatePubkey derives a public key from a secret key.
|
|
// Returns the 32-byte x-only public key.
|
|
func (l *LibSecp256k1) CreatePubkey(seckey32 []byte) ([]byte, error) {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if !l.loaded {
|
|
return nil, errors.New("library not loaded")
|
|
}
|
|
if len(seckey32) != 32 {
|
|
return nil, errors.New("secret key must be 32 bytes")
|
|
}
|
|
|
|
// Create keypair
|
|
keypair := make([]byte, 96)
|
|
if l.keypairCreate(l.ctx, &keypair[0], &seckey32[0]) != 1 {
|
|
return nil, errors.New("failed to create keypair")
|
|
}
|
|
|
|
// Extract x-only pubkey (internal representation is 64 bytes)
|
|
xonlyPubkey := make([]byte, 64)
|
|
var parity int
|
|
if l.keypairXonlyPub(l.ctx, &xonlyPubkey[0], &parity, &keypair[0]) != 1 {
|
|
return nil, errors.New("failed to extract x-only pubkey")
|
|
}
|
|
|
|
// Serialize to get the 32-byte x-coordinate
|
|
pubkey32 := make([]byte, 32)
|
|
if l.xonlyPubkeySerialize(l.ctx, &pubkey32[0], &xonlyPubkey[0]) != 1 {
|
|
return nil, errors.New("failed to serialize x-only pubkey")
|
|
}
|
|
|
|
return pubkey32, nil
|
|
}
|
|
|
|
// ECDH computes the shared secret using ECDH.
|
|
func (l *LibSecp256k1) ECDH(seckey32, pubkey33 []byte) ([]byte, error) {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if !l.loaded {
|
|
return nil, errors.New("library not loaded")
|
|
}
|
|
if len(seckey32) != 32 {
|
|
return nil, errors.New("secret key must be 32 bytes")
|
|
}
|
|
if len(pubkey33) != 33 && len(pubkey33) != 65 {
|
|
return nil, errors.New("public key must be 33 or 65 bytes")
|
|
}
|
|
|
|
// Parse pubkey
|
|
pubkey := make([]byte, 64) // secp256k1_pubkey is 64 bytes
|
|
if l.ecPubkeyParse(l.ctx, &pubkey[0], &pubkey33[0], uint(len(pubkey33))) != 1 {
|
|
return nil, errors.New("failed to parse public key")
|
|
}
|
|
|
|
// Compute ECDH
|
|
output := make([]byte, 32)
|
|
if l.ecdh(l.ctx, &output[0], &pubkey[0], &seckey32[0], 0, 0) != 1 {
|
|
return nil, errors.New("ECDH failed")
|
|
}
|
|
|
|
return output, nil
|
|
}
|