From 703e6d8805ca234a7185d316a2f03ce8f16f11cd Mon Sep 17 00:00:00 2001 From: Phil Keeling Date: Tue, 4 Jan 2022 19:33:25 +0000 Subject: [PATCH] Improve performance by avoiding scalar mult during search phase of key. --- internal/ed25519/ed25519.go | 111 +++++++++++++++++++++++++++++ internal/ed25519/ed25519_test.go | 16 +++++ internal/ed25519/iterator.go | 114 ++++++++++++++++++++++++++++++ internal/ed25519/iterator_test.go | 22 ++++++ internal/ed25519/scalaradd.go | 33 +++++++++ miner.go | 36 ++++++++-- onionaddress.go | 36 ++-------- onionaddress_test.go | 28 -------- 8 files changed, 334 insertions(+), 62 deletions(-) create mode 100644 internal/ed25519/ed25519.go create mode 100644 internal/ed25519/ed25519_test.go create mode 100644 internal/ed25519/iterator.go create mode 100644 internal/ed25519/iterator_test.go create mode 100644 internal/ed25519/scalaradd.go diff --git a/internal/ed25519/ed25519.go b/internal/ed25519/ed25519.go new file mode 100644 index 0000000..88eafc5 --- /dev/null +++ b/internal/ed25519/ed25519.go @@ -0,0 +1,111 @@ +package ed25519 + +import ( + "bytes" + cryptorand "crypto/rand" + "crypto/sha512" + "errors" + "fmt" + "io" + + "github.com/oasisprotocol/curve25519-voi/curve" + "github.com/oasisprotocol/curve25519-voi/curve/scalar" +) + +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = 32 + + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = 64 + + // SeedSize is the size, in bytes, of private key seeds. + SeedSize = 32 +) + +// PrivateKey is the type of Ed25519 private keys. +type PrivateKey []byte + +// PublicKey is the type of Ed25519 public keys. +type PublicKey []byte + +// KeyPair is a type with both Ed25519 keys. +type KeyPair struct { + // PublicKey is the public key of the Ed25519 key pair. + PublicKey PublicKey + + // PrivateKey is the private key of the Ed25519 key pair. + PrivateKey PrivateKey +} + +// Validate performs sanity checks to ensure that the public and private keys match. +func (kp *KeyPair) Validate() error { + pk, err := getPublicKeyFromPrivateKey(kp.PrivateKey) + if err != nil { + return fmt.Errorf("could not compute public key from private key: %w", err) + } + + if !bytes.Equal(kp.PublicKey, pk) { + return errors.New("keys do not match") + } + + return nil +} + +func GenerateKey(rand io.Reader) (*KeyPair, error) { + if rand == nil { + rand = cryptorand.Reader + } + + seed := make([]byte, SeedSize) + if _, err := io.ReadFull(rand, seed); err != nil { + return nil, err + } + + sk := make([]byte, PrivateKeySize) + newKeyFromSeed(sk, seed) + + // Private key does not contain the public key in this implementation, so we + // need to compute it instead. + pk, err := getPublicKeyFromPrivateKey(sk) + if err != nil { + return nil, err + } + + return &KeyPair{ + PublicKey: pk, + PrivateKey: sk, + }, nil +} + +func newKeyFromSeed(sk, seed []byte) { + if l := len(seed); l != SeedSize { + panic(fmt.Sprintf("bad seed length: %d", l)) + } + + digest := sha512.Sum512(seed) + clampSecretKey(&digest) + copy(sk, digest[:]) +} + +func getPublicKeyFromPrivateKey(sk []byte) ([]byte, error) { + if l := len(sk); l != PrivateKeySize { + panic(fmt.Errorf("bad private key length: %d", len(sk))) + } + + sc, err := scalar.NewFromBits(sk[:scalar.ScalarSize]) + if err != nil { + return nil, err + } + + pk := curve.NewCompressedEdwardsY() + pk.SetEdwardsPoint(curve.NewEdwardsPoint().MulBasepoint(curve.ED25519_BASEPOINT_TABLE, sc)) + + return pk[:], nil +} + +func clampSecretKey(sk *[64]byte) { + sk[0] &= 248 + sk[31] &= 63 + sk[31] |= 64 +} diff --git a/internal/ed25519/ed25519_test.go b/internal/ed25519/ed25519_test.go new file mode 100644 index 0000000..4e38980 --- /dev/null +++ b/internal/ed25519/ed25519_test.go @@ -0,0 +1,16 @@ +package ed25519_test + +import ( + "testing" + + "github.com/innix/shrek/internal/ed25519" +) + +func BenchmarkGenerateNewKey(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := ed25519.GenerateKey(nil) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/ed25519/iterator.go b/internal/ed25519/iterator.go new file mode 100644 index 0000000..516a5c8 --- /dev/null +++ b/internal/ed25519/iterator.go @@ -0,0 +1,114 @@ +package ed25519 + +import ( + "errors" + "fmt" + "io" + "math" + + "github.com/oasisprotocol/curve25519-voi/curve" + "github.com/oasisprotocol/curve25519-voi/curve/scalar" +) + +type keyIterator struct { + kp *KeyPair + eightPt *curve.EdwardsPoint + + pt *curve.EdwardsPoint + sc *scalar.Scalar + + counter uint64 +} + +// NewKeyIterator creates and initializes a new Ed25519 key iterator. +// The iterator is NOT thread safe; you must create a separate iterator for +// each worker instead of sharing a single instance. +func NewKeyIterator(rand io.Reader) (*keyIterator, error) { + eightPt := curve.NewEdwardsPoint() + eightPt = eightPt.MulBasepoint(curve.ED25519_BASEPOINT_TABLE, scalar.NewFromUint64(8)) + + it := &keyIterator{ + eightPt: eightPt, + } + if _, err := it.init(rand); err != nil { + return nil, err + } + + return it, nil +} + +func (it *keyIterator) Next() bool { + const maxCounter = math.MaxUint64 - 8 + + if it.counter > uint64(maxCounter) { + return false + } + + it.pt = it.pt.Add(it.pt, it.eightPt) + it.counter += 8 + + return true +} + +func (it *keyIterator) PublicKey() PublicKey { + var pk curve.CompressedEdwardsY + pk.SetEdwardsPoint(it.pt) + + return pk[:] +} + +func (it *keyIterator) PrivateKey() (PrivateKey, error) { + sc := scalar.New().Set(it.sc) + + if it.counter > 0 { + if err := scalarAdd(sc, it.counter); err != nil { + return nil, err + } + } + + sk := make([]byte, PrivateKeySize) + if err := sc.ToBytes(sk[:scalar.ScalarSize]); err != nil { + return nil, fmt.Errorf("could not pack scalar into byte array: %w", err) + } + copy(sk[scalar.ScalarSize:], it.kp.PrivateKey[scalar.ScalarSize:]) + + // Sanity check. + if !((sk[0] & 248) == sk[0]) || !(((sk[31] & 63) | 64) == sk[31]) { + return nil, errors.New("sanity check on private key failed") + } + + return sk, nil +} + +func (it *keyIterator) init(rand io.Reader) (*KeyPair, error) { + kp, err := GenerateKey(rand) + if err != nil { + return nil, err + } + + // Parse private key. + sk, err := scalar.NewFromBits(kp.PrivateKey[:scalar.ScalarSize]) + if err != nil { + return nil, fmt.Errorf("could not parse scalar from private key: %w", err) + } + + // Parse public key. + cpt, err := curve.NewCompressedEdwardsYFromBytes(kp.PublicKey) + if err != nil { + return nil, fmt.Errorf("could not parse point from public key: %w", err) + } + pk := curve.NewEdwardsPoint() + if _, err := pk.SetCompressedY(cpt); err != nil { + return nil, fmt.Errorf("could not decompress point from public key: %w", err) + } + + // Cache data so it can be used later. + it.kp = kp + it.sc = sk + it.pt = pk + + // Reset counter. + it.counter = 0 + + return kp, nil +} diff --git a/internal/ed25519/iterator_test.go b/internal/ed25519/iterator_test.go new file mode 100644 index 0000000..defea61 --- /dev/null +++ b/internal/ed25519/iterator_test.go @@ -0,0 +1,22 @@ +package ed25519_test + +import ( + "testing" + + "github.com/innix/shrek/internal/ed25519" +) + +func BenchmarkKeyIterator_PublicKeyAndNext(b *testing.B) { + it, err := ed25519.NewKeyIterator(nil) + if err != nil { + b.Fatalf("Could not create key iterator: %v", err) + } + + for i := 0; i < b.N; i++ { + _ = it.PublicKey() + if err != nil { + b.Fatal(err) + } + it.Next() + } +} diff --git a/internal/ed25519/scalaradd.go b/internal/ed25519/scalaradd.go new file mode 100644 index 0000000..3878bbd --- /dev/null +++ b/internal/ed25519/scalaradd.go @@ -0,0 +1,33 @@ +package ed25519 + +import ( + "github.com/oasisprotocol/curve25519-voi/curve/scalar" +) + +func scalarAdd(dst *scalar.Scalar, v uint64) error { + var dstb [32]byte + + if err := dst.ToBytes(dstb[:]); err != nil { + return err + } + + scalarAddBytes(&dstb, v) + + if _, err := dst.SetBits(dstb[:]); err != nil { + return err + } + + return nil +} + +func scalarAddBytes(dst *[32]byte, v uint64) { + var carry uint32 + + for i := 0; i < 32; i++ { + carry += uint32(dst[i]) + uint32(v&0xFF) + dst[i] = byte(carry & 0xFF) + carry >>= 8 + + v >>= 8 + } +} diff --git a/miner.go b/miner.go index 330f7ed..7aa5413 100644 --- a/miner.go +++ b/miner.go @@ -2,17 +2,32 @@ package shrek import ( "context" + "errors" "fmt" "io" + + "github.com/innix/shrek/internal/ed25519" ) func MineOnionHostName(ctx context.Context, rand io.Reader, m Matcher) (*OnionAddress, error) { hostname := make([]byte, EncodedPublicKeySize) - for ctx.Err() == nil { - addr, err := GenerateOnionAddress(rand) - if err != nil { - return nil, fmt.Errorf("could not generate key pair: %w", err) + it, err := ed25519.NewKeyIterator(rand) + if err != nil { + return nil, fmt.Errorf("could not create key iterator: %w", err) + } + + for more := true; ctx.Err() == nil; more = it.Next() { + if !more { + return nil, errors.New("searched entire address space and no match was found") + } + + addr := &OnionAddress{ + PublicKey: it.PublicKey(), + + // The private key is not needed to generate the hostname. So to avoid pointless + // computation, we wait until a match has been found first. + SecretKey: nil, } // The approximate encoder only generates the first 51 bytes of the hostname accurately; @@ -34,6 +49,19 @@ func MineOnionHostName(ctx context.Context, rand io.Reader, m Matcher) (*OnionAd continue } + // Compute private key after a match has been found. + sk, err := it.PrivateKey() + if err != nil { + return nil, fmt.Errorf("could not compute private key: %w", err) + } + addr.SecretKey = sk + + // Sanity check keys retrieved from iterator. + kp := &ed25519.KeyPair{PublicKey: addr.PublicKey, PrivateKey: addr.SecretKey} + if err := kp.Validate(); err != nil { + return nil, fmt.Errorf("key validation failed: %w", err) + } + return addr, nil } diff --git a/onionaddress.go b/onionaddress.go index 046afa9..fe39bdd 100644 --- a/onionaddress.go +++ b/onionaddress.go @@ -2,15 +2,13 @@ package shrek import ( "bytes" - "crypto/ed25519" - "crypto/sha512" "encoding/base32" "fmt" "io" "os" "path/filepath" - ed25519voi "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" + "github.com/innix/shrek/internal/ed25519" "golang.org/x/crypto/sha3" ) @@ -78,26 +76,14 @@ func (addr *OnionAddress) HostNameApprox(hostname []byte) { } func GenerateOnionAddress(rand io.Reader) (*OnionAddress, error) { - publicKey, secretKey, err := ed25519voi.GenerateKey(rand) + kp, err := ed25519.GenerateKey(rand) if err != nil { - return nil, err + return nil, fmt.Errorf("could not generate key pair: %w", err) } return &OnionAddress{ - PublicKey: ed25519.PublicKey(publicKey), - SecretKey: ed25519.PrivateKey(secretKey), - }, nil -} - -func GenerateOnionAddressSlow(rand io.Reader) (*OnionAddress, error) { - publicKey, secretKey, err := ed25519.GenerateKey(rand) - if err != nil { - return nil, err - } - - return &OnionAddress{ - PublicKey: publicKey, - SecretKey: secretKey, + PublicKey: kp.PublicKey, + SecretKey: kp.PrivateKey, }, nil } @@ -130,9 +116,8 @@ func SaveOnionAddress(dir string, addr *OnionAddress) error { return fmt.Errorf("could not save public key to file: %w", err) } - sk := expandSecretKey(addr.SecretKey) skFile := filepath.Join(dir, "hs_ed25519_secret_key") - skData := append([]byte("== ed25519v1-secret: type0 ==\x00\x00\x00"), sk[:]...) + skData := append([]byte("== ed25519v1-secret: type0 ==\x00\x00\x00"), addr.SecretKey...) if err := os.WriteFile(skFile, skData, fileMode); err != nil { return fmt.Errorf("could not save secret key to file: %w", err) } @@ -145,12 +130,3 @@ func SaveOnionAddress(dir string, addr *OnionAddress) error { return nil } - -func expandSecretKey(secretKey ed25519.PrivateKey) [64]byte { - h := sha512.Sum512(secretKey[:32]) - h[0] &= 248 - h[31] &= 127 - h[31] |= 64 - - return h -} diff --git a/onionaddress_test.go b/onionaddress_test.go index 166521b..3899512 100644 --- a/onionaddress_test.go +++ b/onionaddress_test.go @@ -150,25 +150,6 @@ func TestGenerateOnionAddress(t *testing.T) { } } -func TestGenerateOnionAddressSlow(t *testing.T) { - t.Parallel() - - // Perform several iterations to ensure function is stateless and deterministic. - for i := 0; i < 3; i++ { - addr, err := shrek.GenerateOnionAddressSlow(bytes.NewBufferString(seed)) - if err != nil { - t.Fatalf("Could not generate the prerequisite onion address: %v", err) - } - - if !bytes.Equal(addr.PublicKey, seedPublicKey) { - t.Errorf("Unexpected public key, got: %v, wanted: %v", addr.PublicKey, seedPublicKey) - } - if !bytes.Equal(addr.SecretKey, seedSecretKey) { - t.Errorf("Unexpected secret key, got: %v, wanted: %v", addr.SecretKey, seedSecretKey) - } - } -} - func BenchmarkOnionAddress_HostName(b *testing.B) { addr, err := shrek.GenerateOnionAddress(bytes.NewBufferString(seed)) if err != nil { @@ -203,12 +184,3 @@ func BenchmarkGenerateOnionAddress(b *testing.B) { } } } - -func BenchmarkGenerateOnionAddressSlow(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := shrek.GenerateOnionAddressSlow(nil) - if err != nil { - b.Fatal(err) - } - } -}