From 47d493b7e83c35f0593e60b3e5d49542f9863e2a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Sep 2024 17:35:43 -0300 Subject: [PATCH] make libsecp256k1 available with a build tag. --- README.md | 19 +++++++ event.go | 59 -------------------- libsecp256k1/README.md | 33 ------------ libsecp256k1/benchmark_test.go | 34 ------------ libsecp256k1/bip340.go | 51 ------------------ libsecp256k1/signverify_test.go | 20 ------- libsecp256k1/verify.go | 26 --------- pool.go | 14 ++--- relay.go | 18 +------ signature.go | 69 ++++++++++++++++++++++++ signature_libsecp256k1.go | 95 +++++++++++++++++++++++++++++++++ 11 files changed, 189 insertions(+), 249 deletions(-) delete mode 100644 libsecp256k1/README.md delete mode 100644 libsecp256k1/benchmark_test.go delete mode 100644 libsecp256k1/bip340.go delete mode 100644 libsecp256k1/signverify_test.go delete mode 100644 libsecp256k1/verify.go create mode 100644 signature.go create mode 100644 signature_libsecp256k1.go diff --git a/README.md b/README.md index 8335220..474c2cf 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,25 @@ nostr.InfoLogger = log.New(io.Discard, "", 0) go run example/example.go ``` +### Using [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1) + +[`libsecp256k1`](https://github.com/bitcoin-core/secp256k1) is very fast: + +``` +goos: linux +goarch: amd64 +cpu: Intel(R) Core(TM) i5-2400 CPU @ 3.10GHz +BenchmarkWithoutLibsecp256k1/sign-4 2794 434114 ns/op +BenchmarkWithoutLibsecp256k1/check-4 4352 297416 ns/op +BenchmarkWithLibsecp256k1/sign-4 12559 94607 ns/op +BenchmarkWithLibsecp256k1/check-4 13761 84595 ns/op +PASS +``` + +But to use it you need the host to have it installed as a shared library and CGO to be supported, so we don't compile against it by default. + +To use it, use `-tags=libsecp256k1` whenever you're compiling your program that uses this library. + ## Warning: risk of goroutine bloat (if used incorrectly) Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point. diff --git a/event.go b/event.go index de7f9cb..156ca0d 100644 --- a/event.go +++ b/event.go @@ -5,8 +5,6 @@ import ( "encoding/hex" "fmt" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/mailru/easyjson" ) @@ -85,63 +83,6 @@ func (evt *Event) Serialize() []byte { return dst } -// CheckSignature checks if the signature is valid for the id -// (which is a hash of the serialized event content). -// returns an error if the signature itself is invalid. -func (evt Event) CheckSignature() (bool, error) { - // read and check pubkey - pk, err := hex.DecodeString(evt.PubKey) - if err != nil { - return false, fmt.Errorf("event pubkey '%s' is invalid hex: %w", evt.PubKey, err) - } - - pubkey, err := schnorr.ParsePubKey(pk) - if err != nil { - return false, fmt.Errorf("event has invalid pubkey '%s': %w", evt.PubKey, err) - } - - // read signature - s, err := hex.DecodeString(evt.Sig) - if err != nil { - return false, fmt.Errorf("signature '%s' is invalid hex: %w", evt.Sig, err) - } - sig, err := schnorr.ParseSignature(s) - if err != nil { - return false, fmt.Errorf("failed to parse signature: %w", err) - } - - // check signature - hash := sha256.Sum256(evt.Serialize()) - return sig.Verify(hash[:], pubkey), nil -} - -// Sign signs an event with a given privateKey. -func (evt *Event) Sign(privateKey string, signOpts ...schnorr.SignOption) error { - s, err := hex.DecodeString(privateKey) - if err != nil { - return fmt.Errorf("Sign called with invalid private key '%s': %w", privateKey, err) - } - - if evt.Tags == nil { - evt.Tags = make(Tags, 0) - } - - sk, pk := btcec.PrivKeyFromBytes(s) - pkBytes := pk.SerializeCompressed() - evt.PubKey = hex.EncodeToString(pkBytes[1:]) - - h := sha256.Sum256(evt.Serialize()) - sig, err := schnorr.Sign(sk, h[:], signOpts...) - if err != nil { - return err - } - - evt.ID = hex.EncodeToString(h[:]) - evt.Sig = hex.EncodeToString(sig.Serialize()) - - return nil -} - // IsRegular checks if the given kind is in Regular range. func (evt *Event) IsRegular() bool { return evt.Kind < 10000 && evt.Kind != 0 && evt.Kind != 3 diff --git a/libsecp256k1/README.md b/libsecp256k1/README.md deleted file mode 100644 index 7ec5563..0000000 --- a/libsecp256k1/README.md +++ /dev/null @@ -1,33 +0,0 @@ -This wraps [libsecp256k1](https://github.com/bitcoin-core/secp256k1) with `cgo`. - -It doesn't embed the library or anything smart like that because I don't know how to do it, so you must have it installed in your system. - -It is faster than the pure Go version: - -``` -goos: linux -goarch: amd64 -pkg: github.com/nbd-wtf/go-nostr/libsecp256k1 -cpu: AMD Ryzen 3 3200G with Radeon Vega Graphics -BenchmarkSignatureVerification/btcec-4 145 7873130 ns/op 127069 B/op 579 allocs/op -BenchmarkSignatureVerification/libsecp256k1-4 502 2314573 ns/op 112241 B/op 392 allocs/op -``` - -To use it manually, just import. To use it inside the automatic verification that happens for subscriptions, set it up with a `SimplePool`: - -```go -pool := nostr.NewSimplePool() -pool.SignatureChecker = func (evt nostr.Event) bool { - ok, _ := libsecp256k1.CheckSignature(evt) - return ok -} -``` - -Or directly to the `Relay`: - -```go -relay := nostr.RelayConnect(context.Background(), "wss://relay.nostr.com", nostr.WithSignatureChecker(func (evt nostr.Event) bool { - ok, _ := libsecp256k1.CheckSignature(evt) - return ok -})) -``` diff --git a/libsecp256k1/benchmark_test.go b/libsecp256k1/benchmark_test.go deleted file mode 100644 index d4e4ca5..0000000 --- a/libsecp256k1/benchmark_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package libsecp256k1 - -import ( - "encoding/json" - "testing" - - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/test_common" -) - -func BenchmarkSignatureVerification(b *testing.B) { - events := make([]*nostr.Event, len(test_common.NormalEvents)) - for i, jevt := range test_common.NormalEvents { - evt := &nostr.Event{} - json.Unmarshal([]byte(jevt), evt) - events[i] = evt - } - - b.Run("btcec", func(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, evt := range events { - evt.CheckSignature() - } - } - }) - - b.Run("libsecp256k1", func(b *testing.B) { - for i := 0; i < b.N; i++ { - for _, evt := range events { - CheckSignature(evt) - } - } - }) -} diff --git a/libsecp256k1/bip340.go b/libsecp256k1/bip340.go deleted file mode 100644 index 26d3c6b..0000000 --- a/libsecp256k1/bip340.go +++ /dev/null @@ -1,51 +0,0 @@ -package libsecp256k1 - -/* -#cgo LDFLAGS: -lsecp256k1 -#include -#include -#include -*/ -import "C" - -import ( - "crypto/rand" - "errors" - "unsafe" -) - -var globalSecp256k1Context *C.secp256k1_context - -func init() { - globalSecp256k1Context = C.secp256k1_context_create(C.SECP256K1_CONTEXT_SIGN | C.SECP256K1_CONTEXT_VERIFY) - if globalSecp256k1Context == nil { - panic("failed to create secp256k1 context") - } -} - -func Sign(msg [32]byte, sk [32]byte) ([64]byte, error) { - var sig [64]byte - - var keypair C.secp256k1_keypair - if C.secp256k1_keypair_create(globalSecp256k1Context, &keypair, (*C.uchar)(unsafe.Pointer(&sk[0]))) != 1 { - return sig, errors.New("failed to parse private key") - } - - var random [32]byte - rand.Read(random[:]) - - if C.secp256k1_schnorrsig_sign32(globalSecp256k1Context, (*C.uchar)(unsafe.Pointer(&sig[0])), (*C.uchar)(unsafe.Pointer(&msg[0])), &keypair, (*C.uchar)(unsafe.Pointer(&random[0]))) != 1 { - return sig, errors.New("failed to sign message") - } - - return sig, nil -} - -func Verify(msg [32]byte, sig [64]byte, pk [32]byte) bool { - var xonly C.secp256k1_xonly_pubkey - if C.secp256k1_xonly_pubkey_parse(globalSecp256k1Context, &xonly, (*C.uchar)(unsafe.Pointer(&pk[0]))) != 1 { - return false - } - - return C.secp256k1_schnorrsig_verify(globalSecp256k1Context, (*C.uchar)(unsafe.Pointer(&sig[0])), (*C.uchar)(unsafe.Pointer(&msg[0])), 32, &xonly) == 1 -} diff --git a/libsecp256k1/signverify_test.go b/libsecp256k1/signverify_test.go deleted file mode 100644 index b6606c3..0000000 --- a/libsecp256k1/signverify_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package libsecp256k1 - -import ( - "encoding/json" - "testing" - - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/test_common" - "github.com/stretchr/testify/assert" -) - -func TestEventVerification(t *testing.T) { - for _, jevt := range test_common.NormalEvents { - evt := &nostr.Event{} - json.Unmarshal([]byte(jevt), evt) - ok, _ := CheckSignature(evt) - shouldBe, _ := evt.CheckSignature() - assert.Equal(t, ok, shouldBe, "%s signature must be %s", jevt, shouldBe) - } -} diff --git a/libsecp256k1/verify.go b/libsecp256k1/verify.go deleted file mode 100644 index 43b8486..0000000 --- a/libsecp256k1/verify.go +++ /dev/null @@ -1,26 +0,0 @@ -package libsecp256k1 - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - - "github.com/nbd-wtf/go-nostr" -) - -func CheckSignature(evt *nostr.Event) (bool, error) { - var pk [32]byte - _, err := hex.Decode(pk[:], []byte(evt.PubKey)) - if err != nil { - return false, fmt.Errorf("event pubkey '%s' is invalid hex: %w", evt.PubKey, err) - } - - var sig [64]byte - _, err = hex.Decode(sig[:], []byte(evt.Sig)) - if err != nil { - return false, fmt.Errorf("event signature '%s' is invalid hex: %w", evt.Sig, err) - } - - msg := sha256.Sum256(evt.Serialize()) - return Verify(msg, sig, pk), nil -} diff --git a/pool.go b/pool.go index cf7fb08..7a5605c 100644 --- a/pool.go +++ b/pool.go @@ -27,10 +27,9 @@ type SimplePool struct { eventMiddleware []func(RelayEvent) // custom things not often used - signatureChecker func(Event) bool - penaltyBoxMu sync.Mutex - penaltyBox map[string][2]float64 - userAgent string + penaltyBoxMu sync.Mutex + penaltyBox map[string][2]float64 + userAgent string } type DirectedFilters struct { @@ -161,12 +160,7 @@ func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) { ctx, cancel := context.WithTimeout(pool.Context, time.Second*15) defer cancel() - opts := make([]RelayOption, 0, 1+len(pool.eventMiddleware)) - if pool.signatureChecker != nil { - opts = append(opts, WithSignatureChecker(pool.signatureChecker)) - } - - relay = NewRelay(context.Background(), url, opts...) + relay = NewRelay(context.Background(), url) relay.RequestHeader.Set("User-Agent", pool.userAgent) if err := relay.Connect(ctx); err != nil { diff --git a/relay.go b/relay.go index 6d5a9e4..7615b75 100644 --- a/relay.go +++ b/relay.go @@ -39,7 +39,6 @@ type Relay struct { okCallbacks *xsync.MapOf[string, func(bool, string)] writeQueue chan writeRequest subscriptionChannelCloseQueue chan *Subscription - signatureChecker func(Event) bool // custom things that aren't often used // @@ -62,11 +61,7 @@ func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay { okCallbacks: xsync.NewMapOf[string, func(bool, string)](), writeQueue: make(chan writeRequest), subscriptionChannelCloseQueue: make(chan *Subscription), - signatureChecker: func(e Event) bool { - ok, _ := e.CheckSignature() - return ok - }, - RequestHeader: make(http.Header, 1), + RequestHeader: make(http.Header, 1), } for _, opt := range opts { @@ -93,7 +88,6 @@ type RelayOption interface { var ( _ RelayOption = (WithNoticeHandler)(nil) - _ RelayOption = (WithSignatureChecker)(nil) _ RelayOption = (WithCustomHandler)(nil) ) @@ -105,14 +99,6 @@ func (nh WithNoticeHandler) ApplyRelayOption(r *Relay) { r.noticeHandler = nh } -// WithSignatureChecker must be a function that checks the signature of an -// event and returns true or false. -type WithSignatureChecker func(Event) bool - -func (sc WithSignatureChecker) ApplyRelayOption(r *Relay) { - r.signatureChecker = sc -} - // WithCustomHandler must be a function that handles any relay message that couldn't be // parsed as a standard envelope. type WithCustomHandler func(data []byte) @@ -267,7 +253,7 @@ func (r *Relay) ConnectWithTLS(ctx context.Context, tlsConfig *tls.Config) error // check signature, ignore invalid, except from trusted (AssumeValid) relays if !r.AssumeValid { - if ok := r.signatureChecker(env.Event); !ok { + if ok, _ := env.Event.CheckSignature(); !ok { InfoLogger.Printf("{%s} bad signature on %s\n", r.URL, env.Event.ID) continue } diff --git a/signature.go b/signature.go new file mode 100644 index 0000000..39d9a5e --- /dev/null +++ b/signature.go @@ -0,0 +1,69 @@ +//go:build !libsecp256k1 + +package nostr + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +// CheckSignature checks if the signature is valid for the id +// (which is a hash of the serialized event content). +// returns an error if the signature itself is invalid. +func (evt Event) CheckSignature() (bool, error) { + // read and check pubkey + pk, err := hex.DecodeString(evt.PubKey) + if err != nil { + return false, fmt.Errorf("event pubkey '%s' is invalid hex: %w", evt.PubKey, err) + } + + pubkey, err := schnorr.ParsePubKey(pk) + if err != nil { + return false, fmt.Errorf("event has invalid pubkey '%s': %w", evt.PubKey, err) + } + + // read signature + s, err := hex.DecodeString(evt.Sig) + if err != nil { + return false, fmt.Errorf("signature '%s' is invalid hex: %w", evt.Sig, err) + } + sig, err := schnorr.ParseSignature(s) + if err != nil { + return false, fmt.Errorf("failed to parse signature: %w", err) + } + + // check signature + hash := sha256.Sum256(evt.Serialize()) + return sig.Verify(hash[:], pubkey), nil +} + +// Sign signs an event with a given privateKey. +func (evt *Event) Sign(secretKey string, signOpts ...schnorr.SignOption) error { + s, err := hex.DecodeString(secretKey) + if err != nil { + return fmt.Errorf("Sign called with invalid secret key '%s': %w", secretKey, err) + } + + if evt.Tags == nil { + evt.Tags = make(Tags, 0) + } + + sk, pk := btcec.PrivKeyFromBytes(s) + pkBytes := pk.SerializeCompressed() + evt.PubKey = hex.EncodeToString(pkBytes[1:]) + + h := sha256.Sum256(evt.Serialize()) + sig, err := schnorr.Sign(sk, h[:], signOpts...) + if err != nil { + return err + } + + evt.ID = hex.EncodeToString(h[:]) + evt.Sig = hex.EncodeToString(sig.Serialize()) + + return nil +} diff --git a/signature_libsecp256k1.go b/signature_libsecp256k1.go new file mode 100644 index 0000000..bf5ed7c --- /dev/null +++ b/signature_libsecp256k1.go @@ -0,0 +1,95 @@ +//go:build libsecp256k1 + +package nostr + +/* +#cgo LDFLAGS: -lsecp256k1 +#include +#include +#include +*/ +import "C" + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "unsafe" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +// CheckSignature checks if the signature is valid for the id +// (which is a hash of the serialized event content). +// returns an error if the signature itself is invalid. +func (evt Event) CheckSignature() (bool, error) { + var pk [32]byte + _, err := hex.Decode(pk[:], []byte(evt.PubKey)) + if err != nil { + return false, fmt.Errorf("event pubkey '%s' is invalid hex: %w", evt.PubKey, err) + } + + var sig [64]byte + _, err = hex.Decode(sig[:], []byte(evt.Sig)) + if err != nil { + return false, fmt.Errorf("event signature '%s' is invalid hex: %w", evt.Sig, err) + } + + msg := sha256.Sum256(evt.Serialize()) + + var xonly C.secp256k1_xonly_pubkey + if C.secp256k1_xonly_pubkey_parse(globalSecp256k1Context, &xonly, (*C.uchar)(unsafe.Pointer(&pk[0]))) != 1 { + return false, fmt.Errorf("failed to parse xonly pubkey") + } + + res := C.secp256k1_schnorrsig_verify(globalSecp256k1Context, (*C.uchar)(unsafe.Pointer(&sig[0])), (*C.uchar)(unsafe.Pointer(&msg[0])), 32, &xonly) + return res == 1, nil +} + +// Sign signs an event with a given privateKey. +func (evt *Event) Sign(secretKey string, signOpts ...schnorr.SignOption) error { + sk, err := hex.DecodeString(secretKey) + if err != nil { + return fmt.Errorf("Sign called with invalid secret key '%s': %w", secretKey, err) + } + + if evt.Tags == nil { + evt.Tags = make(Tags, 0) + } + + var keypair C.secp256k1_keypair + if C.secp256k1_keypair_create(globalSecp256k1Context, &keypair, (*C.uchar)(unsafe.Pointer(&sk[0]))) != 1 { + return errors.New("failed to parse private key") + } + + var xonly C.secp256k1_xonly_pubkey + var pk [32]byte + C.secp256k1_keypair_xonly_pub(globalSecp256k1Context, &xonly, nil, &keypair) + C.secp256k1_xonly_pubkey_serialize(globalSecp256k1Context, (*C.uchar)(unsafe.Pointer(&pk[0])), &xonly) + evt.PubKey = hex.EncodeToString(pk[:]) + + h := sha256.Sum256(evt.Serialize()) + + var sig [64]byte + var random [32]byte + rand.Read(random[:]) + if C.secp256k1_schnorrsig_sign32(globalSecp256k1Context, (*C.uchar)(unsafe.Pointer(&sig[0])), (*C.uchar)(unsafe.Pointer(&h[0])), &keypair, (*C.uchar)(unsafe.Pointer(&random[0]))) != 1 { + return errors.New("failed to sign message") + } + + evt.ID = hex.EncodeToString(h[:]) + evt.Sig = hex.EncodeToString(sig[:]) + + return nil +} + +var globalSecp256k1Context *C.secp256k1_context + +func init() { + globalSecp256k1Context = C.secp256k1_context_create(C.SECP256K1_CONTEXT_SIGN | C.SECP256K1_CONTEXT_VERIFY) + if globalSecp256k1Context == nil { + panic("failed to create secp256k1 context") + } +}