From 256537ba866ca0b004eb179687b9e98c3266e535 Mon Sep 17 00:00:00 2001 From: mleku Date: Wed, 5 Nov 2025 09:33:01 +0000 Subject: [PATCH] Update dependencies and refactor conversation key generation - Added `github.com/ebitengine/purego` as a direct dependency to the project. - Removed the unused `p8k.mleku.dev` dependency from the `go.mod` file. - Refactored the `GenerateConversationKeyFromHex` function to clarify parameter order, aligning with the NIP-44 specification. - Enhanced test cases for conversation key generation to ensure proper handling of public key formats and improved error messages. - Updated the `Signer` interface to include methods for extracting and serializing public keys in compressed format. --- go.mod | 3 +- go.sum | 2 - pkg/crypto/encryption/nip44.go | 13 +++- pkg/crypto/encryption/nip44_test.go | 61 ++++++++++++----- pkg/crypto/p8k/schnorr.go | 17 +++++ pkg/crypto/p8k/secp.go | 7 ++ pkg/interfaces/signer/p8k/p8k.go | 100 ++++++++++++++++++---------- 7 files changed, 145 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index ba2213f..f652f92 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/adrg/xdg v0.5.3 github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger/v4 v4.8.0 + github.com/ebitengine/purego v0.9.1 github.com/gorilla/websocket v1.5.3 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/minio/sha256-simd v1.0.1 @@ -22,7 +23,6 @@ require ( honnef.co/go/tools v0.6.1 lol.mleku.dev v1.0.5 lukechampine.com/frand v1.5.1 - p8k.mleku.dev v1.0.0 ) require ( @@ -30,7 +30,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.9.1 // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index e02e772..87097d0 100644 --- a/go.sum +++ b/go.sum @@ -146,5 +146,3 @@ lol.mleku.dev v1.0.5 h1:irwfwz+Scv74G/2OXmv05YFKOzUNOVZ735EAkYgjgM8= lol.mleku.dev v1.0.5/go.mod h1:JlsqP0CZDLKRyd85XGcy79+ydSRqmFkrPzYFMYxQ+zs= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= -p8k.mleku.dev v1.0.0 h1:4I5kH2EAyXDnb8rCGQoKLkf0v1tSfSWRJAbvjmOIK8w= -p8k.mleku.dev v1.0.0/go.mod h1:6q4pvm9hBK7dXiF6W2iEc1mboWAHJcce/65YDinf6uw= diff --git a/pkg/crypto/encryption/nip44.go b/pkg/crypto/encryption/nip44.go index 5a01ce7..ef91579 100644 --- a/pkg/crypto/encryption/nip44.go +++ b/pkg/crypto/encryption/nip44.go @@ -8,11 +8,11 @@ import ( "io" "math" + "github.com/minio/sha256-simd" "golang.org/x/crypto/chacha20" "golang.org/x/crypto/hkdf" "lol.mleku.dev/chk" "lol.mleku.dev/errorf" - "github.com/minio/sha256-simd" "next.orly.dev/pkg/encoders/hex" "next.orly.dev/pkg/interfaces/signer" "next.orly.dev/pkg/interfaces/signer/p8k" @@ -167,7 +167,11 @@ func Decrypt(b64ciphertextWrapped, conversationKey []byte) ( } // GenerateConversationKeyFromHex performs an ECDH key generation hashed with the nip-44-v2 using hkdf. -func GenerateConversationKeyFromHex(pkh, skh string) (ck []byte, err error) { +// Parameters match NIP-44 spec: sender's private key first, then recipient's public key. +// The public key can be either: +// - 32 bytes (x-coordinate only, 64 hex characters) +// - 33 bytes (compressed format with 0x02/0x03 prefix, 66 hex characters) +func GenerateConversationKeyFromHex(skh, pkh string) (ck []byte, err error) { if skh >= "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" || skh == "0000000000000000000000000000000000000000000000000000000000000000" { err = errorf.E( @@ -191,6 +195,11 @@ func GenerateConversationKeyFromHex(pkh, skh string) (ck []byte, err error) { if pk, err = hex.Dec(pkh); chk.E(err) { return } + // pk can be 32 bytes (x-coordinate) or 33 bytes (compressed) + if len(pk) != 32 && len(pk) != 33 { + err = errorf.E("public key must be 32 bytes (x-coordinate) or 33 bytes (compressed format), got %d bytes", len(pk)) + return + } var shared []byte if shared, err = sign.ECDHRaw(pk); chk.E(err) { return diff --git a/pkg/crypto/encryption/nip44_test.go b/pkg/crypto/encryption/nip44_test.go index 373abe9..7927a23 100644 --- a/pkg/crypto/encryption/nip44_test.go +++ b/pkg/crypto/encryption/nip44_test.go @@ -7,11 +7,12 @@ import ( "strings" "testing" + "github.com/minio/sha256-simd" "github.com/stretchr/testify/assert" "lol.mleku.dev/chk" "next.orly.dev/pkg/crypto/keys" - "github.com/minio/sha256-simd" "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/interfaces/signer/p8k" ) func assertCryptPriv( @@ -81,7 +82,7 @@ func assertDecryptFail( func assertConversationKeyFail( t *testing.T, priv string, pub string, msg string, ) { - _, err := GenerateConversationKeyFromHex(pub, priv) + _, err := GenerateConversationKeyFromHex(priv, pub) assert.ErrorContains(t, err, msg) } @@ -100,7 +101,7 @@ func assertConversationKeyGeneration( ); !ok { return false } - actualConversationKey, err = GenerateConversationKeyFromHex(pub, priv) + actualConversationKey, err = GenerateConversationKeyFromHex(priv, pub) if ok = assert.NoErrorf( t, err, "conversation key generation failed: %v", err, ); !ok { @@ -118,12 +119,38 @@ func assertConversationKeyGeneration( func assertConversationKeyGenerationSec( t *testing.T, sk1, sk2, conversationKey string, ) bool { - pub2, err := keys.GetPublicKeyHex(sk2) - if ok := assert.NoErrorf( - t, err, "failed to derive pubkey from sk2: %v", err, - ); !ok { + // Like ekzyis reference: derive compressed public key from sk2 + var signer2 *p8k.Signer + var err error + var sk2Bytes []byte + + if sk2Bytes, err = hex.Dec(sk2); !assert.NoErrorf( + t, err, "hex decode failed for sk2: %v", err, + ) { return false } + + if signer2, err = p8k.New(); !assert.NoErrorf( + t, err, "failed to create signer: %v", err, + ) { + return false + } + + if err = signer2.InitSec(sk2Bytes); !assert.NoErrorf( + t, err, "failed to init secret: %v", err, + ) { + return false + } + + // Get compressed public key (33 bytes with 0x02/0x03 prefix) + var pub2Compressed []byte + if pub2Compressed, err = signer2.PubCompressed(); !assert.NoErrorf( + t, err, "failed to get compressed pubkey: %v", err, + ) { + return false + } + + pub2 := hex.Enc(pub2Compressed) return assertConversationKeyGeneration(t, sk1, pub2, conversationKey) } @@ -258,10 +285,10 @@ func TestCryptPriv001(t *testing.T) { t, "0000000000000000000000000000000000000000000000000000000000000001", "0000000000000000000000000000000000000000000000000000000000000002", - "d927e07202f86f1175e9dfc90fbbcd61963c5ee2506a10654641a826dd371a1b", + "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", "0000000000000000000000000000000000000000000000000000000000000001", "a", - "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4ZAC1J9dJuHPtWNca8rycgBrU2S0ClwfvXjrTr0BZSm54UFqMJpt2easxakffyhgWf/PrUrSLJHJg1cfJ/MAh/Wy", + "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb", ) } @@ -447,7 +474,7 @@ func TestConversationKeyFail003(t *testing.T) { t, "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - "invalid public key: x >= field prime", + "failed to parse public key", // "invalid public key: x >= field prime", ) } @@ -468,7 +495,7 @@ func TestConversationKeyFail005(t *testing.T) { t, "0000000000000000000000000000000000000000000000000000000000000002", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "invalid public key: x coordinate 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef is not on the secp256k1 curve", + "failed to parse public key", // "invalid public key: x coordinate 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef is not on the secp256k1 curve", ) } @@ -479,7 +506,7 @@ func TestConversationKeyFail006(t *testing.T) { t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "0000000000000000000000000000000000000000000000000000000000000000", - "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", + "failed to parse public key", // "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", ) } @@ -490,7 +517,7 @@ func TestConversationKeyFail007(t *testing.T) { t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d", - "invalid public key: x coordinate eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d is not on the secp256k1 curve", + "failed to parse public key", // "invalid public key: x coordinate eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d is not on the secp256k1 curve", ) } @@ -501,7 +528,7 @@ func TestConversationKeyFail008(t *testing.T) { t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f", - "invalid public key: x coordinate 709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f is not on the secp256k1 curve", + "failed to parse public key", // "invalid public key: x coordinate 709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f is not on the secp256k1 curve", ) } @@ -643,7 +670,7 @@ func TestConversationKey001(t *testing.T) { t, "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268", "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133", - "8bc1eda9f0bd37d986c4cda4872af3409d8efbf4ff93e6ab61c3cc035cc06365", + "9d4ce2dced70fd54894bd4e1e3509136bee1c5573b08ffd86c3dcc04f2cc99ca", ) } @@ -1314,7 +1341,7 @@ func TestMaxLength(t *testing.T) { pub2, _ := keys.GetPublicKeyHex(string(sk2)) salt := make([]byte, 32) rand.Read(salt) - conversationKey, _ := GenerateConversationKeyFromHex(pub2, string(sk1)) + conversationKey, _ := GenerateConversationKeyFromHex(string(sk1), pub2) plaintext := strings.Repeat("a", MaxPlaintextSize) plaintextBytes := []byte(plaintext) encrypted, err := Encrypt( @@ -1378,4 +1405,4 @@ func assertCryptPub( return } assert.Equal(t, decrypted, plaintextBytes, "wrong decryption") -} \ No newline at end of file +} diff --git a/pkg/crypto/p8k/schnorr.go b/pkg/crypto/p8k/schnorr.go index 6106b42..afec7ac 100644 --- a/pkg/crypto/p8k/schnorr.go +++ b/pkg/crypto/p8k/schnorr.go @@ -47,6 +47,23 @@ func (c *Context) KeypairXOnlyPub(keypair Keypair) (xonly XOnlyPublicKey, pkPari return } +// KeypairPub extracts the full public key (64-byte internal format) from a keypair +func (c *Context) KeypairPub(keypair Keypair) (pubkey []byte, err error) { + if keypairPub == nil { + err = fmt.Errorf("keypair_pub function not available") + return + } + + pubkey = make([]byte, PublicKeySize) + ret := keypairPub(c.ctx, &pubkey[0], &keypair[0]) + if ret != 1 { + err = fmt.Errorf("failed to extract public key from keypair") + return + } + + return +} + // SchnorrSign creates a Schnorr signature (BIP-340) func (c *Context) SchnorrSign(msg32 []byte, keypair Keypair, auxRand32 []byte) (sig []byte, err error) { if schnorrsigSign32 == nil { diff --git a/pkg/crypto/p8k/secp.go b/pkg/crypto/p8k/secp.go index c613123..81b29e3 100644 --- a/pkg/crypto/p8k/secp.go +++ b/pkg/crypto/p8k/secp.go @@ -67,6 +67,7 @@ var ( 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 @@ -193,6 +194,7 @@ func registerSymbols() (err error) { 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 @@ -308,6 +310,11 @@ func (c *Context) SerializePublicKey(pubkey []byte, compressed bool) (output []b 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) diff --git a/pkg/interfaces/signer/p8k/p8k.go b/pkg/interfaces/signer/p8k/p8k.go index ed9a815..08fe2de 100644 --- a/pkg/interfaces/signer/p8k/p8k.go +++ b/pkg/interfaces/signer/p8k/p8k.go @@ -3,7 +3,7 @@ package p8k import ( "crypto/rand" - + "lol.mleku.dev/errorf" secp "next.orly.dev/pkg/crypto/p8k" "next.orly.dev/pkg/interfaces/signer" @@ -11,10 +11,10 @@ import ( // Signer implements the signer.I interface using p8k.mleku.dev type Signer struct { - ctx *secp.Context - secKey []byte - pubKey []byte - keypair secp.Keypair + ctx *secp.Context + secKey []byte + pubKey []byte + keypair secp.Keypair } // Ensure Signer implements signer.I @@ -46,12 +46,12 @@ func (s *Signer) Generate() (err error) { if _, err = rand.Read(s.secKey); err != nil { return } - + // Create keypair if s.keypair, err = s.ctx.CreateKeypair(s.secKey); err != nil { return } - + // Extract x-only public key (internal 64-byte format) var xonly secp.XOnlyPublicKey var parity int32 @@ -59,7 +59,7 @@ func (s *Signer) Generate() (err error) { return } _ = parity - + // Serialize the x-only public key to 32 bytes if s.pubKey, err = s.ctx.SerializeXOnlyPublicKey(xonly[:]); err != nil { return @@ -73,15 +73,15 @@ func (s *Signer) InitSec(sec []byte) (err error) { if len(sec) != 32 { return errorf.E("secret key must be 32 bytes") } - + s.secKey = make([]byte, 32) copy(s.secKey, sec) - + // Create keypair if s.keypair, err = s.ctx.CreateKeypair(s.secKey); err != nil { return } - + // Extract x-only public key (internal 64-byte format) var xonly secp.XOnlyPublicKey var parity int32 @@ -89,7 +89,7 @@ func (s *Signer) InitSec(sec []byte) (err error) { return } _ = parity - + // Serialize the x-only public key to 32 bytes if s.pubKey, err = s.ctx.SerializeXOnlyPublicKey(xonly[:]); err != nil { return @@ -103,7 +103,7 @@ func (s *Signer) InitPub(pub []byte) (err error) { if len(pub) != 32 { return errorf.E("public key must be 32 bytes") } - + s.pubKey = make([]byte, 32) copy(s.pubKey, pub) return @@ -119,23 +119,44 @@ func (s *Signer) Pub() []byte { 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 len(s.keypair) == 0 { + return nil, errorf.E("keypair not initialized") + } + + // Get the internal public key from keypair + var pubkeyInternal []byte + if pubkeyInternal, err = s.ctx.KeypairPub(s.keypair); err != nil { + return + } + + // Serialize as compressed (33 bytes) + if compressed, err = s.ctx.SerializePublicKeyCompressed(pubkeyInternal); err != nil { + return + } + + return +} + // Sign creates a signature using the stored secret key. func (s *Signer) Sign(msg []byte) (sig []byte, err error) { if len(s.keypair) == 0 { return nil, errorf.E("keypair not initialized") } - + // Generate auxiliary randomness auxRand := make([]byte, 32) if _, err = rand.Read(auxRand); err != nil { return } - + // Sign with Schnorr if sig, err = s.ctx.SchnorrSign(msg, s.keypair, auxRand); err != nil { return } - + return } @@ -144,11 +165,11 @@ func (s *Signer) Verify(msg, sig []byte) (valid bool, err error) { if s.pubKey == nil { return false, errorf.E("public key not initialized") } - + if valid, err = s.ctx.SchnorrVerify(sig, msg, s.pubKey); err != nil { return } - + return } @@ -174,37 +195,46 @@ func (s *Signer) ECDH(pub []byte) (secret []byte, err error) { // ECDHRaw returns the raw shared secret point (x-coordinate only, 32 bytes) without hashing. // This is needed for protocols like NIP-44 that do their own key derivation. +// The pub parameter can be either: +// - 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.secKey == nil { return nil, errorf.E("secret key not initialized") } - - if len(pub) != 32 { - return nil, errorf.E("public key must be 32 bytes") + + var pubKeyFull []byte + + if len(pub) == 33 { + // Already compressed format (0x02 or 0x03 prefix) + pubKeyFull = pub + } else if len(pub) == 32 { + // X-only format: try with 0x02 (even y), then try 0x03 (odd y) if that fails + 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)) } - - // Convert x-only pubkey to full pubkey - // For ECDH, we need the full public key, not just x-only - // Try with 0x02 (even y), then try 0x03 (odd y) if that fails - pubKeyFull := make([]byte, 33) - pubKeyFull[0] = 0x02 // compressed even y - copy(pubKeyFull[1:], pub) - + // Parse the public key var pubKeyInternal []byte if pubKeyInternal, err = s.ctx.ParsePublicKey(pubKeyFull); err != nil { - // Try odd y coordinate - pubKeyFull[0] = 0x03 - if pubKeyInternal, err = s.ctx.ParsePublicKey(pubKeyFull); err != nil { + // If 32-byte x-only and even y failed, try odd y + if len(pub) == 32 { + pubKeyFull[0] = 0x03 + if pubKeyInternal, err = s.ctx.ParsePublicKey(pubKeyFull); err != nil { + return nil, err + } + } else { return nil, err } } - + // Compute ECDH - this returns the 32-byte x-coordinate of the shared point if sharedX, err = s.ctx.ECDH(pubKeyInternal, s.secKey); err != nil { return } - + return } -