Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
88d3e3f73e
|
|||
|
eaac3cdc19
|
|||
|
36fc05b1c2
|
|||
|
c753049cfd
|
|||
|
ae170fc069
|
51
.github/workflows/go.yml
vendored
51
.github/workflows/go.yml
vendored
@@ -32,25 +32,14 @@ jobs:
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Install libsecp256k1 (runtime optional)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y autoconf automake libtool pkg-config
|
||||
|
||||
# Build and install libsecp256k1 for runtime performance boost
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git /tmp/secp256k1
|
||||
cd /tmp/secp256k1
|
||||
./autogen.sh
|
||||
./configure --enable-module-recovery --enable-module-ecdh --enable-module-schnorrsig --enable-module-extrakeys
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
- name: Build (Pure Go + purego)
|
||||
run: CGO_ENABLED=0 go build -v ./...
|
||||
|
||||
- name: Test (Pure Go + purego)
|
||||
run: CGO_ENABLED=0 go test -v $(go list ./... | xargs -n1 sh -c 'ls $0/*_test.go 1>/dev/null 2>&1 && echo $0' | grep .)
|
||||
run: |
|
||||
# Copy the libsecp256k1.so to root directory so tests can find it
|
||||
cp pkg/crypto/p8k/libsecp256k1.so .
|
||||
CGO_ENABLED=0 go test -v $(go list ./... | xargs -n1 sh -c 'ls $0/*_test.go 1>/dev/null 2>&1 && echo $0' | grep .)
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -66,21 +55,6 @@ jobs:
|
||||
with:
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Install libsecp256k1 (for bundling with releases)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y autoconf automake libtool pkg-config
|
||||
|
||||
# Build and install libsecp256k1
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git /tmp/secp256k1
|
||||
cd /tmp/secp256k1
|
||||
./autogen.sh
|
||||
./configure --enable-module-recovery --enable-module-ecdh --enable-module-schnorrsig --enable-module-extrakeys
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
- name: Build Release Binaries (Pure Go + purego)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
@@ -88,24 +62,17 @@ jobs:
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "Building release binaries for version $VERSION (pure Go + purego)"
|
||||
|
||||
# Copy libsecp256k1.so for Linux (runtime optional, for performance)
|
||||
if [ -f pkg/crypto/p8k/libsecp256k1.so ]; then
|
||||
cp pkg/crypto/p8k/libsecp256k1.so release-binaries/libsecp256k1-linux-amd64.so
|
||||
elif [ -f /usr/local/lib/libsecp256k1.so.2 ]; then
|
||||
cp /usr/local/lib/libsecp256k1.so.2 release-binaries/libsecp256k1-linux-amd64.so
|
||||
fi
|
||||
|
||||
# Create directory for binaries
|
||||
mkdir -p release-binaries
|
||||
|
||||
# Build for Linux AMD64 (pure Go with purego)
|
||||
echo "Building Linux AMD64 (pure Go + purego)..."
|
||||
# Copy the pre-compiled libsecp256k1.so for Linux AMD64
|
||||
cp pkg/crypto/p8k/libsecp256k1.so release-binaries/libsecp256k1-linux-amd64.so
|
||||
|
||||
# Build for Linux AMD64 (pure Go + purego dynamic loading)
|
||||
echo "Building Linux AMD64 (pure Go + purego dynamic loading)..."
|
||||
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||
go build -ldflags "-s -w" -o release-binaries/orly-${VERSION}-linux-amd64 .
|
||||
|
||||
# Note: Only building orly binary as requested
|
||||
# Other cmd utilities (aggregator, benchmark, convert, policytest, stresstest) are development tools
|
||||
|
||||
# Create checksums
|
||||
cd release-binaries
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/crypto/ec/schnorr"
|
||||
"next.orly.dev/pkg/encoders/envelopes/authenvelope"
|
||||
"next.orly.dev/pkg/encoders/envelopes/countenvelope"
|
||||
"next.orly.dev/pkg/utils/normalize"
|
||||
@@ -28,7 +29,7 @@ func (l *Listener) HandleCount(msg []byte) (err error) {
|
||||
log.D.C(func() string { return fmt.Sprintf("COUNT sub=%s filters=%d", env.Subscription, len(env.Filters)) })
|
||||
|
||||
// If ACL is active, auth is required, or AuthToWrite is enabled, send a challenge (same as REQ path)
|
||||
if acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite {
|
||||
if len(l.authedPubkey.Load()) != schnorr.PubKeyBytesLen && (acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite) {
|
||||
if err = authenvelope.NewChallengeWith(l.challenge.Load()).Write(l); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
|
||||
},
|
||||
)
|
||||
// send a challenge to the client to auth if an ACL is active, auth is required, or AuthToWrite is enabled
|
||||
if acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite {
|
||||
if len(l.authedPubkey.Load()) == 0 && (acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite) {
|
||||
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
|
||||
Write(l); chk.E(err) {
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ package secp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
@@ -158,6 +159,7 @@ func LoadLibrary() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("INFO: Successfully loaded libsecp256k1 v5.0.0 from %s", libPath)
|
||||
loadLibErr = nil
|
||||
})
|
||||
|
||||
|
||||
@@ -5,43 +5,71 @@ import (
|
||||
"crypto/rand"
|
||||
|
||||
"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"
|
||||
"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 {
|
||||
// libsecp256k1 implementation
|
||||
ctx *secp.Context
|
||||
secKey []byte
|
||||
pubKey []byte
|
||||
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
|
||||
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) {
|
||||
var ctx *secp.Context
|
||||
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}
|
||||
return
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// MustNew creates a new P8K signer and panics on error
|
||||
func MustNew() (s *Signer) {
|
||||
var err error
|
||||
if s, err = New(); err != nil {
|
||||
func MustNew() *Signer {
|
||||
s, err := New()
|
||||
if err != nil {
|
||||
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
|
||||
// ECDH works).
|
||||
func (s *Signer) Generate() (err error) {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.Generate()
|
||||
}
|
||||
|
||||
s.secKey = make([]byte, 32)
|
||||
if _, err = rand.Read(s.secKey); err != nil {
|
||||
return
|
||||
@@ -70,6 +98,10 @@ func (s *Signer) Generate() (err error) {
|
||||
// InitSec initialises the secret (signing) key from the raw bytes, and also
|
||||
// derives the public key because it can.
|
||||
func (s *Signer) InitSec(sec []byte) (err error) {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.InitSec(sec)
|
||||
}
|
||||
|
||||
if len(sec) != 32 {
|
||||
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
|
||||
// expected to be an x-only 32 byte pubkey.
|
||||
func (s *Signer) InitPub(pub []byte) (err error) {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.InitPub(pub)
|
||||
}
|
||||
|
||||
if len(pub) != 32 {
|
||||
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.
|
||||
func (s *Signer) Sec() []byte {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.Sec()
|
||||
}
|
||||
return s.secKey
|
||||
}
|
||||
|
||||
// Pub returns the public key bytes (x-only schnorr pubkey).
|
||||
func (s *Signer) Pub() []byte {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.Pub()
|
||||
}
|
||||
return s.pubKey
|
||||
}
|
||||
|
||||
// PubCompressed returns the compressed public key (33 bytes with 0x02/0x03 prefix).
|
||||
// This is needed for ECDH operations like NIP-44.
|
||||
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 {
|
||||
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.
|
||||
func (s *Signer) Sign(msg []byte) (sig []byte, err error) {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.Sign(msg)
|
||||
}
|
||||
|
||||
if len(s.keypair) == 0 {
|
||||
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.
|
||||
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 {
|
||||
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.
|
||||
func (s *Signer) Zero() {
|
||||
if s.fallback != nil {
|
||||
s.fallback.Zero()
|
||||
return
|
||||
}
|
||||
|
||||
if s.secKey != nil {
|
||||
for i := range s.secKey {
|
||||
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
|
||||
// - 33 bytes (compressed): will be used as-is
|
||||
func (s *Signer) ECDHRaw(pub []byte) (sharedX []byte, err error) {
|
||||
if s.fallback != nil {
|
||||
return s.fallback.ECDHRaw(pub)
|
||||
}
|
||||
|
||||
if s.secKey == nil {
|
||||
return nil, errorf.E("secret key not initialized")
|
||||
}
|
||||
@@ -238,3 +305,178 @@ func (s *Signer) ECDHRaw(pub []byte) (sharedX []byte, err error) {
|
||||
|
||||
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)
|
||||
|
||||
// Parse the x-only public key into a full public key for verification
|
||||
if s.pubKey, err = schnorr.ParsePubKey(pub); err != nil {
|
||||
return errorf.E("failed to parse public key: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.25.3
|
||||
v0.25.7
|
||||
Reference in New Issue
Block a user