From 3953f279fc6c3aed1a20072554906cdc8fdddd85 Mon Sep 17 00:00:00 2001 From: Sun Yimin Date: Wed, 27 Nov 2024 17:58:05 +0800 Subject: [PATCH] eip-5564 stealth address POC --- ecdh/ecdh.go | 48 +++++++++++++++++ ecdh/sm2ec.go | 57 ++++++++++++++++++++ ecdh/stealth_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 ecdh/stealth_test.go diff --git a/ecdh/ecdh.go b/ecdh/ecdh.go index 81d0f04..6475c60 100644 --- a/ecdh/ecdh.go +++ b/ecdh/ecdh.go @@ -33,6 +33,10 @@ type Curve interface { // private keys can cause ECDH to return an error. NewPrivateKey(key []byte) (*PrivateKey, error) + // GenerateKeyFromScalar generates a PrivateKey from a scalar. + // This is used for testing only now. + GenerateKeyFromScalar(scalar []byte) (*PrivateKey, error) + // NewPublicKey checks that key is valid and returns a PublicKey. // // For NIST curves, this decodes an uncompressed point according to SEC 1, @@ -50,6 +54,20 @@ type Curve interface { // methods in the future without breaking backwards compatibility. ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) + // addPublicKeys adds two public keys and returns the resulting public key. It's exposed + // as the PublicKey.Add method. + addPublicKeys(a, b *PublicKey) (*PublicKey, error) + + // addPrivateKeys adds two private keys and returns the resulting private key. It's exposed + // as the PrivateKey.Add method. + addPrivateKeys(a, b *PrivateKey) (*PrivateKey, error) + + // secretKey generates a shared secret key from a local ephemeral private key and a + // remote public key. It's exposed as the PrivateKey.SecretKey method. + // + // This method is similar to ECDH, but it returns the raw shared secret instead + secretKey(local *PrivateKey, remote *PublicKey) ([]byte, error) + // sm2mqv performs a SM2 specific style ECMQV exchange and return the shared secret. sm2mqv(sLocal, eLocal *PrivateKey, sRemote, eRemote *PublicKey) (*PublicKey, error) @@ -136,6 +154,15 @@ func (uv *PublicKey) SM2SharedKey(isResponder bool, kenLen int, sPub, sRemote *P return sm3.Kdf(buffer[:], kenLen), nil } +// Add adds two public keys and returns the resulting public key. +// k is NOT changed. +func (k *PublicKey) Add(x *PublicKey) (*PublicKey, error) { + if k.curve != x.curve { + return nil, errors.New("ecdh: public keys do not have the same curve") + } + return k.curve.addPublicKeys(k, x) +} + // PrivateKey is an ECDH private key, usually kept secret. // // These keys can be parsed with [smx509.ParsePKCS8PrivateKey] and encoded @@ -165,6 +192,18 @@ func (k *PrivateKey) ECDH(remote *PublicKey) ([]byte, error) { return k.curve.ecdh(k, remote) } +// SecretKey generates a shared secret key from a local ephemeral private key and a +// remote public key. +// +// This method is similar to [ECDH], but it returns the raw shared secret instead +// of the x-coordinate of the shared point. +func (k *PrivateKey) SecretKey(remote *PublicKey) ([]byte, error) { + if k.curve != remote.curve { + return nil, errors.New("ecdh: private key and public key curves do not match") + } + return k.curve.secretKey(k, remote) +} + // SM2MQV performs a SM2 specific style ECMQV exchange and return the shared secret. func (k *PrivateKey) SM2MQV(eLocal *PrivateKey, sRemote, eRemote *PublicKey) (*PublicKey, error) { if k.curve != eLocal.curve || k.curve != sRemote.curve || k.curve != eRemote.curve { @@ -213,3 +252,12 @@ func (k *PrivateKey) PublicKey() *PublicKey { func (k *PrivateKey) Public() crypto.PublicKey { return k.PublicKey() } + +// Add adds two private keys and returns the resulting private key. +// k is NOT changed. +func (k *PrivateKey) Add(x *PrivateKey) (*PrivateKey, error) { + if k.curve != x.curve { + return nil, errors.New("ecdh: private keys do not have the same curve") + } + return k.curve.addPrivateKeys(k, x) +} diff --git a/ecdh/sm2ec.go b/ecdh/sm2ec.go index bc1103b..7ed16b9 100644 --- a/ecdh/sm2ec.go +++ b/ecdh/sm2ec.go @@ -7,6 +7,7 @@ import ( "io" "math/bits" + "github.com/emmansun/gmsm/internal/bigmod" "github.com/emmansun/gmsm/internal/randutil" sm2ec "github.com/emmansun/gmsm/internal/sm2ec" "github.com/emmansun/gmsm/internal/subtle" @@ -83,6 +84,21 @@ func (c *sm2Curve) privateKeyToPublicKey(key *PrivateKey) *PublicKey { } } +func (c *sm2Curve) GenerateKeyFromScalar(scalar []byte) (*PrivateKey, error) { + if size := len(c.scalarOrderMinus1); len(scalar) > size { + scalar = scalar[:size] + } + m, err := bigmod.NewModulus(c.scalarOrderMinus1) + if err != nil { + return nil, err + } + p, err := bigmod.NewNat().SetOverflowingBytes(scalar, m) + if err != nil { + return nil, err + } + return c.NewPrivateKey(p.Bytes(m)) +} + func (c *sm2Curve) NewPublicKey(key []byte) (*PublicKey, error) { // Reject the point at infinity and compressed encodings. if len(key) == 0 || key[0] != 4 { @@ -111,6 +127,47 @@ func (c *sm2Curve) ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) { return p.BytesX() } +func (c *sm2Curve) addPublicKeys(a, b *PublicKey) (*PublicKey, error) { + p1, err := c.newPoint().SetBytes(a.publicKey) + if err != nil { + return nil, err + } + p2, err := c.newPoint().SetBytes(b.publicKey) + if err != nil { + return nil, err + } + p1.Add(p1, p2) + return c.NewPublicKey(p1.Bytes()) +} + +func (c *sm2Curve) addPrivateKeys(a, b *PrivateKey) (*PrivateKey, error) { + m, err := bigmod.NewModulus(c.scalarOrderMinus1) + if err != nil { + return nil, err + } + aNat, err := bigmod.NewNat().SetBytes(a.privateKey, m) + if err != nil { + return nil, err + } + bNat, err := bigmod.NewNat().SetBytes(b.privateKey, m) + if err != nil { + return nil, err + } + aNat = aNat.Add(bNat, m) + return c.NewPrivateKey(aNat.Bytes(m)) +} + +func (c *sm2Curve) secretKey(local *PrivateKey, remote *PublicKey) ([]byte, error) { + p, err := c.newPoint().SetBytes(remote.publicKey) + if err != nil { + return nil, err + } + if _, err := p.ScalarMult(p, local.privateKey); err != nil { + return nil, err + } + return p.Bytes(), nil +} + func (c *sm2Curve) sm2avf(secret *PublicKey) []byte { bytes := secret.publicKey[1:33] var result [32]byte diff --git a/ecdh/stealth_test.go b/ecdh/stealth_test.go new file mode 100644 index 0000000..2b28aae --- /dev/null +++ b/ecdh/stealth_test.go @@ -0,0 +1,122 @@ +package ecdh + +import ( + "crypto/rand" + "testing" + + "github.com/emmansun/gmsm/sm3" +) + +// https://eips.ethereum.org/EIPS/eip-5564, but uses SM3 instead of Keccak256 + +// Generation - Generate stealth address from stealth meta-address +func generateStealthAddress(spendPub, viewPub *PublicKey) (ephemeralPub *PublicKey, stealth *PublicKey, err error) { + // generate ephemeral key pair + ephemeralPriv, err := P256().GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + ephemeralPub = ephemeralPriv.PublicKey() + + // compute shared secret key + R, err := ephemeralPriv.SecretKey(viewPub) + if err != nil { + return nil, nil, err + } + + // the secret key is hashed + sh := sm3.Sum(R[1:]) + + // multiply the hashed shared secret with the generator point + shPriv, err := P256().GenerateKeyFromScalar(sh[:]) + if err != nil { + return nil, nil, err + } + shPublic := shPriv.PublicKey() + + // compute the recipient's stealth public key + stealth, err = shPublic.Add(spendPub) + if err != nil { + return nil, nil, err + } + return ephemeralPub, stealth, nil +} + +// Parsing - Locate one’s own stealth address +func checkStealthAddress(viewPriv *PrivateKey, spendPub, ephemeralPub, stealth *PublicKey) (bool, error) { + // compute shared secret key + R, err := viewPriv.SecretKey(ephemeralPub) + if err != nil { + return false, err + } + // the secret key is hashed + sh := sm3.Sum(R[1:]) + // multiply the hashed shared secret with the generator point + shPriv, err := P256().GenerateKeyFromScalar(sh[:]) + if err != nil { + return false, err + } + shPublic := shPriv.PublicKey() + // compute the derived stealth address + goStealth, err := shPublic.Add(spendPub) + if err != nil { + return false, err + } + // compare the derived stealth address with the provided stealth address + return stealth.Equal(goStealth), nil +} + +// Private key derivation - Generate the stealth address private key from the hashed shared secret and the spending private key. +func computeStealthKey(spendPriv, viewPriv *PrivateKey, ephemeralPub *PublicKey) (*PrivateKey, error) { + // compute shared secret key + R, err := viewPriv.SecretKey(ephemeralPub) + if err != nil { + return nil, err + } + // the secret key is hashed + sh := sm3.Sum(R[1:]) + // multiply the hashed shared secret with the generator point + shPriv, err := P256().GenerateKeyFromScalar(sh[:]) + if err != nil { + return nil, err + } + return spendPriv.Add(shPriv) +} + +func testEIP5564StealthAddress(t *testing.T, spendPriv, viewPriv *PrivateKey) { + t.Helper() + + ephemeralPub, expectedStealth, err := generateStealthAddress(spendPriv.PublicKey(), viewPriv.PublicKey()) + + if err != nil { + t.Fatalf("the recipient's stealth public key: failed to add public keys: %v", err) + } + + passed, err := checkStealthAddress(viewPriv, spendPriv.PublicKey(), ephemeralPub, expectedStealth) + if err != nil { + t.Fatal(err) + } + if !passed { + t.Fatal("mismatched stealth address") + } + + privStealth, err := computeStealthKey(spendPriv, viewPriv, ephemeralPub) + if err != nil { + t.Fatalf("failed to compute stealth key: %v", err) + } + if !privStealth.PublicKey().Equal(expectedStealth) { + t.Fatal("mismatched stealth key") + } +} + +func TestEIP5564StealthAddress(t *testing.T) { + privSpend, err := P256().GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("failed to generate private key: %v", err) + } + privView, err := P256().GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("failed to generate private key: %v", err) + } + testEIP5564StealthAddress(t, privSpend, privView) +}