Files
p256k1/libsecp256k1_purego.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
}