You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
535 lines
16 KiB
535 lines
16 KiB
// Package secp provides Go bindings to libsecp256k1 without CGO. |
|
// It uses dynamic library loading via purego to call C functions directly. |
|
package secp |
|
|
|
import ( |
|
_ "embed" |
|
"fmt" |
|
"log" |
|
"os" |
|
"path/filepath" |
|
"runtime" |
|
"sync" |
|
"unsafe" |
|
|
|
"github.com/ebitengine/purego" |
|
) |
|
|
|
//go:embed libsecp256k1.so |
|
var embeddedLibLinux []byte |
|
|
|
// Constants for context flags |
|
const ( |
|
ContextNone = 1 |
|
ContextVerify = 257 // 1 | (1 << 8) |
|
ContextSign = 513 // 1 | (1 << 9) |
|
ContextDeclassify = 1025 // 1 | (1 << 10) |
|
) |
|
|
|
// EC flags |
|
const ( |
|
ECCompressed = 258 // SECP256K1_EC_COMPRESSED |
|
ECUncompressed = 2 // SECP256K1_EC_UNCOMPRESSED |
|
) |
|
|
|
// Size constants |
|
const ( |
|
PublicKeySize = 64 |
|
CompressedPublicKeySize = 33 |
|
UncompressedPublicKeySize = 65 |
|
SignatureSize = 64 |
|
CompactSignatureSize = 64 |
|
PrivateKeySize = 32 |
|
SharedSecretSize = 32 |
|
SchnorrSignatureSize = 64 |
|
RecoverableSignatureSize = 65 |
|
) |
|
|
|
var ( |
|
libHandle uintptr |
|
loadLibOnce sync.Once |
|
loadLibErr error |
|
extractedPath string |
|
extractLibOnce sync.Once |
|
) |
|
|
|
// Function pointers |
|
var ( |
|
contextCreate func(flags uint32) uintptr |
|
contextDestroy func(ctx uintptr) |
|
contextRandomize func(ctx uintptr, seed32 *byte) int32 |
|
ecPubkeyCreate func(ctx uintptr, pubkey *byte, seckey *byte) int32 |
|
ecPubkeySerialize func(ctx uintptr, output *byte, outputlen *uint64, pubkey *byte, flags uint32) int32 |
|
ecPubkeyParse func(ctx uintptr, pubkey *byte, input *byte, inputlen uint64) int32 |
|
ecdsaSign func(ctx uintptr, sig *byte, msg32 *byte, seckey *byte, noncefp uintptr, ndata uintptr) int32 |
|
ecdsaVerify func(ctx uintptr, sig *byte, msg32 *byte, pubkey *byte) int32 |
|
ecdsaSignatureSerializeDer func(ctx uintptr, output *byte, outputlen *uint64, sig *byte) int32 |
|
ecdsaSignatureParseDer func(ctx uintptr, sig *byte, input *byte, inputlen uint64) int32 |
|
ecdsaSignatureSerializeCompact func(ctx uintptr, output64 *byte, sig *byte) int32 |
|
ecdsaSignatureParseCompact func(ctx uintptr, sig *byte, input64 *byte) int32 |
|
ecdsaSignatureNormalize func(ctx uintptr, sigout *byte, sigin *byte) int32 |
|
|
|
// Schnorr functions |
|
schnorrsigSign32 func(ctx uintptr, sig64 *byte, msg32 *byte, keypair *byte, auxrand32 *byte) int32 |
|
schnorrsigVerify func(ctx uintptr, sig64 *byte, msg32 *byte, msglen uint64, pubkey *byte) int32 |
|
keypairCreate func(ctx uintptr, keypair *byte, seckey *byte) int32 |
|
xonlyPubkeyParse func(ctx uintptr, pubkey *byte, input32 *byte) int32 |
|
xonlyPubkeySerialize func(ctx uintptr, output32 *byte, pubkey *byte) int32 |
|
keypairXonlyPub func(ctx uintptr, pubkey *byte, pkParity *int32, keypair *byte) int32 |
|
keypairPub func(ctx uintptr, pubkey *byte, keypair *byte) int32 |
|
|
|
// ECDH functions |
|
ecdh func(ctx uintptr, output *byte, pubkey *byte, seckey *byte, hashfp uintptr, data uintptr) int32 |
|
|
|
// Recovery functions |
|
ecdsaRecoverableSignatureSerializeCompact func(ctx uintptr, output64 *byte, recid *int32, sig *byte) int32 |
|
ecdsaRecoverableSignatureParseCompact func(ctx uintptr, sig *byte, input64 *byte, recid int32) int32 |
|
ecdsaSignRecoverable func(ctx uintptr, sig *byte, msg32 *byte, seckey *byte, noncefp uintptr, ndata uintptr) int32 |
|
ecdsaRecover func(ctx uintptr, pubkey *byte, sig *byte, msg32 *byte) int32 |
|
|
|
// Extrakeys |
|
xonlyPubkeyFromPubkey func(ctx uintptr, xonlyPubkey *byte, pkParity *int32, pubkey *byte) int32 |
|
) |
|
|
|
// extractEmbeddedLibrary extracts the embedded library to a temporary location |
|
func extractEmbeddedLibrary() (path string, err error) { |
|
extractLibOnce.Do(func() { |
|
var libData []byte |
|
var filename string |
|
|
|
// Select the appropriate embedded library for this platform |
|
switch runtime.GOOS { |
|
case "linux": |
|
if len(embeddedLibLinux) == 0 { |
|
err = fmt.Errorf("no embedded library for linux") |
|
return |
|
} |
|
libData = embeddedLibLinux |
|
filename = "libsecp256k1.so" |
|
default: |
|
err = fmt.Errorf("no embedded library for %s", runtime.GOOS) |
|
return |
|
} |
|
|
|
// Create a temporary directory for the library |
|
// Use a deterministic name so we don't create duplicates |
|
tmpDir := filepath.Join(os.TempDir(), "orly-libsecp256k1") |
|
if err = os.MkdirAll(tmpDir, 0755); err != nil { |
|
err = fmt.Errorf("failed to create temp directory: %w", err) |
|
return |
|
} |
|
|
|
// Write the library to the temp directory |
|
extractedPath = filepath.Join(tmpDir, filename) |
|
|
|
// Check if file already exists and is valid |
|
if info, e := os.Stat(extractedPath); e == nil && info.Size() == int64(len(libData)) { |
|
// File exists and has correct size, assume it's valid |
|
return |
|
} |
|
|
|
if err = os.WriteFile(extractedPath, libData, 0755); err != nil { |
|
err = fmt.Errorf("failed to write library to %s: %w", extractedPath, err) |
|
return |
|
} |
|
|
|
log.Printf("INFO: Extracted embedded libsecp256k1 to %s", extractedPath) |
|
}) |
|
|
|
return extractedPath, err |
|
} |
|
|
|
// LoadLibrary loads the libsecp256k1 shared library |
|
func LoadLibrary() (err error) { |
|
loadLibOnce.Do(func() { |
|
var libPath string |
|
|
|
// First, try to extract and use the embedded library |
|
usedEmbedded := false |
|
if embeddedPath, extractErr := extractEmbeddedLibrary(); extractErr == nil { |
|
libHandle, err = purego.Dlopen(embeddedPath, purego.RTLD_NOW|purego.RTLD_GLOBAL) |
|
if err == nil { |
|
libPath = embeddedPath |
|
usedEmbedded = true |
|
} else { |
|
log.Printf("WARN: Failed to load embedded library from %s: %v, falling back to system paths", embeddedPath, err) |
|
} |
|
} else { |
|
log.Printf("WARN: Failed to extract embedded library: %v, falling back to system paths", extractErr) |
|
} |
|
|
|
// If embedded library failed, fall back to system paths |
|
if err != nil { |
|
switch runtime.GOOS { |
|
case "linux": |
|
// Try common library paths |
|
paths := []string{ |
|
"./libsecp256k1.so", // Bundled in repo for linux amd64 |
|
"libsecp256k1.so.5", |
|
"libsecp256k1.so.2", |
|
"libsecp256k1.so.1", |
|
"libsecp256k1.so.0", |
|
"libsecp256k1.so", |
|
"/usr/lib/libsecp256k1.so", |
|
"/usr/local/lib/libsecp256k1.so", |
|
"/usr/lib/x86_64-linux-gnu/libsecp256k1.so", |
|
} |
|
for _, p := range paths { |
|
libHandle, err = purego.Dlopen(p, purego.RTLD_NOW|purego.RTLD_GLOBAL) |
|
if err == nil { |
|
libPath = p |
|
break |
|
} |
|
} |
|
case "darwin": |
|
paths := []string{ |
|
"libsecp256k1.2.dylib", |
|
"libsecp256k1.1.dylib", |
|
"libsecp256k1.0.dylib", |
|
"libsecp256k1.dylib", |
|
"/usr/local/lib/libsecp256k1.dylib", |
|
"/opt/homebrew/lib/libsecp256k1.dylib", |
|
} |
|
for _, p := range paths { |
|
libHandle, err = purego.Dlopen(p, purego.RTLD_NOW|purego.RTLD_GLOBAL) |
|
if err == nil { |
|
libPath = p |
|
break |
|
} |
|
} |
|
case "windows": |
|
paths := []string{ |
|
"libsecp256k1-2.dll", |
|
"libsecp256k1-1.dll", |
|
"libsecp256k1-0.dll", |
|
"libsecp256k1.dll", |
|
"secp256k1.dll", |
|
} |
|
for _, p := range paths { |
|
libHandle, err = purego.Dlopen(p, purego.RTLD_NOW|purego.RTLD_GLOBAL) |
|
if err == nil { |
|
libPath = p |
|
break |
|
} |
|
} |
|
default: |
|
err = fmt.Errorf("unsupported platform: %s", runtime.GOOS) |
|
loadLibErr = err |
|
return |
|
} |
|
} |
|
|
|
if err != nil { |
|
loadLibErr = fmt.Errorf("failed to load libsecp256k1: %w", err) |
|
return |
|
} |
|
|
|
// Register symbols |
|
if err = registerSymbols(); err != nil { |
|
loadLibErr = fmt.Errorf("failed to register symbols from %s: %w", libPath, err) |
|
return |
|
} |
|
|
|
if usedEmbedded { |
|
log.Printf("INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from %s", libPath) |
|
} else { |
|
log.Printf("INFO: Successfully loaded libsecp256k1 v5.0.0 from system path: %s", libPath) |
|
} |
|
loadLibErr = nil |
|
}) |
|
|
|
return loadLibErr |
|
} |
|
|
|
// registerSymbols registers all C function symbols |
|
func registerSymbols() (err error) { |
|
// Core context functions |
|
purego.RegisterLibFunc(&contextCreate, libHandle, "secp256k1_context_create") |
|
purego.RegisterLibFunc(&contextDestroy, libHandle, "secp256k1_context_destroy") |
|
purego.RegisterLibFunc(&contextRandomize, libHandle, "secp256k1_context_randomize") |
|
|
|
// Public key functions |
|
purego.RegisterLibFunc(&ecPubkeyCreate, libHandle, "secp256k1_ec_pubkey_create") |
|
purego.RegisterLibFunc(&ecPubkeySerialize, libHandle, "secp256k1_ec_pubkey_serialize") |
|
purego.RegisterLibFunc(&ecPubkeyParse, libHandle, "secp256k1_ec_pubkey_parse") |
|
|
|
// ECDSA functions |
|
purego.RegisterLibFunc(&ecdsaSign, libHandle, "secp256k1_ecdsa_sign") |
|
purego.RegisterLibFunc(&ecdsaVerify, libHandle, "secp256k1_ecdsa_verify") |
|
purego.RegisterLibFunc(&ecdsaSignatureSerializeDer, libHandle, "secp256k1_ecdsa_signature_serialize_der") |
|
purego.RegisterLibFunc(&ecdsaSignatureParseDer, libHandle, "secp256k1_ecdsa_signature_parse_der") |
|
purego.RegisterLibFunc(&ecdsaSignatureSerializeCompact, libHandle, "secp256k1_ecdsa_signature_serialize_compact") |
|
purego.RegisterLibFunc(&ecdsaSignatureParseCompact, libHandle, "secp256k1_ecdsa_signature_parse_compact") |
|
purego.RegisterLibFunc(&ecdsaSignatureNormalize, libHandle, "secp256k1_ecdsa_signature_normalize") |
|
|
|
// Try to load optional modules - don't fail if they're not available |
|
|
|
// Schnorr module |
|
tryRegister(&schnorrsigSign32, "secp256k1_schnorrsig_sign32") |
|
tryRegister(&schnorrsigVerify, "secp256k1_schnorrsig_verify") |
|
tryRegister(&keypairCreate, "secp256k1_keypair_create") |
|
tryRegister(&xonlyPubkeyParse, "secp256k1_xonly_pubkey_parse") |
|
tryRegister(&xonlyPubkeySerialize, "secp256k1_xonly_pubkey_serialize") |
|
tryRegister(&keypairXonlyPub, "secp256k1_keypair_xonly_pub") |
|
tryRegister(&keypairPub, "secp256k1_keypair_pub") |
|
tryRegister(&xonlyPubkeyFromPubkey, "secp256k1_xonly_pubkey_from_pubkey") |
|
|
|
// ECDH module |
|
tryRegister(&ecdh, "secp256k1_ecdh") |
|
|
|
// Recovery module |
|
tryRegister(&ecdsaRecoverableSignatureSerializeCompact, "secp256k1_ecdsa_recoverable_signature_serialize_compact") |
|
tryRegister(&ecdsaRecoverableSignatureParseCompact, "secp256k1_ecdsa_recoverable_signature_parse_compact") |
|
tryRegister(&ecdsaSignRecoverable, "secp256k1_ecdsa_sign_recoverable") |
|
tryRegister(&ecdsaRecover, "secp256k1_ecdsa_recover") |
|
|
|
return nil |
|
} |
|
|
|
// tryRegister attempts to register a symbol without failing if it doesn't exist |
|
func tryRegister(fptr interface{}, symbol string) { |
|
defer func() { |
|
if r := recover(); r != nil { |
|
// Symbol not found, ignore |
|
} |
|
}() |
|
purego.RegisterLibFunc(fptr, libHandle, symbol) |
|
} |
|
|
|
// Context represents a secp256k1 context |
|
type Context struct { |
|
ctx uintptr |
|
} |
|
|
|
// NewContext creates a new secp256k1 context |
|
func NewContext(flags uint32) (c *Context, err error) { |
|
if err = LoadLibrary(); err != nil { |
|
return |
|
} |
|
|
|
ctx := contextCreate(flags) |
|
if ctx == 0 { |
|
err = fmt.Errorf("failed to create context") |
|
return |
|
} |
|
|
|
c = &Context{ctx: ctx} |
|
runtime.SetFinalizer(c, (*Context).Destroy) |
|
|
|
return |
|
} |
|
|
|
// Destroy destroys the context |
|
func (c *Context) Destroy() { |
|
if c.ctx != 0 { |
|
contextDestroy(c.ctx) |
|
c.ctx = 0 |
|
} |
|
} |
|
|
|
// Randomize randomizes the context with entropy |
|
func (c *Context) Randomize(seed32 []byte) (err error) { |
|
if len(seed32) != 32 { |
|
err = fmt.Errorf("seed must be 32 bytes") |
|
return |
|
} |
|
|
|
ret := contextRandomize(c.ctx, &seed32[0]) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to randomize context") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// CreatePublicKey creates a public key from a private key |
|
func (c *Context) CreatePublicKey(seckey []byte) (pubkey []byte, err error) { |
|
if len(seckey) != PrivateKeySize { |
|
err = fmt.Errorf("private key must be %d bytes", PrivateKeySize) |
|
return |
|
} |
|
|
|
pubkey = make([]byte, PublicKeySize) |
|
ret := ecPubkeyCreate(c.ctx, &pubkey[0], &seckey[0]) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to create public key") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// SerializePublicKey serializes a public key |
|
func (c *Context) SerializePublicKey(pubkey []byte, compressed bool) (output []byte, err error) { |
|
if len(pubkey) != PublicKeySize { |
|
err = fmt.Errorf("public key must be %d bytes", PublicKeySize) |
|
return |
|
} |
|
|
|
var flags uint32 |
|
if compressed { |
|
output = make([]byte, CompressedPublicKeySize) |
|
flags = ECCompressed |
|
} else { |
|
output = make([]byte, UncompressedPublicKeySize) |
|
flags = ECUncompressed |
|
} |
|
|
|
outputLen := uint64(len(output)) |
|
ret := ecPubkeySerialize(c.ctx, &output[0], &outputLen, &pubkey[0], flags) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to serialize public key") |
|
return |
|
} |
|
|
|
output = output[:outputLen] |
|
return |
|
} |
|
|
|
// SerializePublicKeyCompressed serializes a public key in compressed format (33 bytes) |
|
func (c *Context) SerializePublicKeyCompressed(pubkey []byte) (output []byte, err error) { |
|
return c.SerializePublicKey(pubkey, true) |
|
} |
|
|
|
// ParsePublicKey parses a serialized public key |
|
func (c *Context) ParsePublicKey(input []byte) (pubkey []byte, err error) { |
|
pubkey = make([]byte, PublicKeySize) |
|
ret := ecPubkeyParse(c.ctx, &pubkey[0], &input[0], uint64(len(input))) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to parse public key") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// Sign creates an ECDSA signature |
|
func (c *Context) Sign(msg32 []byte, seckey []byte) (sig []byte, err error) { |
|
if len(msg32) != 32 { |
|
err = fmt.Errorf("message must be 32 bytes") |
|
return |
|
} |
|
if len(seckey) != PrivateKeySize { |
|
err = fmt.Errorf("private key must be %d bytes", PrivateKeySize) |
|
return |
|
} |
|
|
|
sig = make([]byte, SignatureSize) |
|
ret := ecdsaSign(c.ctx, &sig[0], &msg32[0], &seckey[0], 0, 0) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to sign message") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// Verify verifies an ECDSA signature |
|
func (c *Context) Verify(msg32 []byte, sig []byte, pubkey []byte) (valid bool, err error) { |
|
if len(msg32) != 32 { |
|
err = fmt.Errorf("message must be 32 bytes") |
|
return |
|
} |
|
if len(sig) != SignatureSize { |
|
err = fmt.Errorf("signature must be %d bytes", SignatureSize) |
|
return |
|
} |
|
if len(pubkey) != PublicKeySize { |
|
err = fmt.Errorf("public key must be %d bytes", PublicKeySize) |
|
return |
|
} |
|
|
|
ret := ecdsaVerify(c.ctx, &sig[0], &msg32[0], &pubkey[0]) |
|
valid = ret == 1 |
|
|
|
return |
|
} |
|
|
|
// SerializeSignatureDER serializes a signature in DER format |
|
func (c *Context) SerializeSignatureDER(sig []byte) (output []byte, err error) { |
|
if len(sig) != SignatureSize { |
|
err = fmt.Errorf("signature must be %d bytes", SignatureSize) |
|
return |
|
} |
|
|
|
output = make([]byte, 72) // Max DER signature size |
|
outputLen := uint64(len(output)) |
|
|
|
ret := ecdsaSignatureSerializeDer(c.ctx, &output[0], &outputLen, &sig[0]) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to serialize signature") |
|
return |
|
} |
|
|
|
output = output[:outputLen] |
|
return |
|
} |
|
|
|
// ParseSignatureDER parses a DER-encoded signature |
|
func (c *Context) ParseSignatureDER(input []byte) (sig []byte, err error) { |
|
sig = make([]byte, SignatureSize) |
|
ret := ecdsaSignatureParseDer(c.ctx, &sig[0], &input[0], uint64(len(input))) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to parse DER signature") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// SerializeSignatureCompact serializes a signature in compact format (64 bytes) |
|
func (c *Context) SerializeSignatureCompact(sig []byte) (output []byte, err error) { |
|
if len(sig) != SignatureSize { |
|
err = fmt.Errorf("signature must be %d bytes", SignatureSize) |
|
return |
|
} |
|
|
|
output = make([]byte, CompactSignatureSize) |
|
ret := ecdsaSignatureSerializeCompact(c.ctx, &output[0], &sig[0]) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to serialize signature") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// ParseSignatureCompact parses a compact (64-byte) signature |
|
func (c *Context) ParseSignatureCompact(input64 []byte) (sig []byte, err error) { |
|
if len(input64) != CompactSignatureSize { |
|
err = fmt.Errorf("compact signature must be %d bytes", CompactSignatureSize) |
|
return |
|
} |
|
|
|
sig = make([]byte, SignatureSize) |
|
ret := ecdsaSignatureParseCompact(c.ctx, &sig[0], &input64[0]) |
|
if ret != 1 { |
|
err = fmt.Errorf("failed to parse compact signature") |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// NormalizeSignature normalizes a signature to lower-S form |
|
func (c *Context) NormalizeSignature(sig []byte) (normalized []byte, wasNormalized bool, err error) { |
|
if len(sig) != SignatureSize { |
|
err = fmt.Errorf("signature must be %d bytes", SignatureSize) |
|
return |
|
} |
|
|
|
normalized = make([]byte, SignatureSize) |
|
ret := ecdsaSignatureNormalize(c.ctx, &normalized[0], &sig[0]) |
|
wasNormalized = ret == 1 |
|
|
|
return |
|
} |
|
|
|
// Utility function to convert *byte to unsafe.Pointer |
|
func bytesToPtr(b []byte) unsafe.Pointer { |
|
if len(b) == 0 { |
|
return nil |
|
} |
|
return unsafe.Pointer(&b[0]) |
|
}
|
|
|