Commit d0cd9b4a authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

integrate clef signer (#677)

* add clef as an external signer
* adjust signing in memory
parent ec3f1675
...@@ -42,6 +42,8 @@ const ( ...@@ -42,6 +42,8 @@ const (
optionNamePaymentTolerance = "payment-tolerance" optionNamePaymentTolerance = "payment-tolerance"
optionNameResolverEndpoints = "resolver-options" optionNameResolverEndpoints = "resolver-options"
optionNameGatewayMode = "gateway-mode" optionNameGatewayMode = "gateway-mode"
optionNameClefSignerEnable = "clef-signer-enable"
optionNameClefSignerEndpoint = "clef-signer-endpoint"
) )
func init() { func init() {
...@@ -186,4 +188,6 @@ func (c *command) setAllFlags(cmd *cobra.Command) { ...@@ -186,4 +188,6 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Uint64(optionNamePaymentTolerance, 10000, "excess debt above payment threshold in BZZ where you disconnect from your peer") cmd.Flags().Uint64(optionNamePaymentTolerance, 10000, "excess debt above payment threshold in BZZ where you disconnect from your peer")
cmd.Flags().StringSlice(optionNameResolverEndpoints, []string{}, "resolver connection string, see help for format") cmd.Flags().StringSlice(optionNameResolverEndpoints, []string{}, "resolver connection string, see help for format")
cmd.Flags().Bool(optionNameGatewayMode, false, "disable a set of sensitive features in the api") cmd.Flags().Bool(optionNameGatewayMode, false, "disable a set of sensitive features in the api")
cmd.Flags().Bool(optionNameClefSignerEnable, false, "enable clef signer")
cmd.Flags().String(optionNameClefSignerEndpoint, "", "clef signer endpoint")
} }
...@@ -16,13 +16,16 @@ import ( ...@@ -16,13 +16,16 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/ethereum/go-ethereum/accounts/external"
"github.com/ethersphere/bee/pkg/crypto" "github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/clef"
"github.com/ethersphere/bee/pkg/keystore" "github.com/ethersphere/bee/pkg/keystore"
filekeystore "github.com/ethersphere/bee/pkg/keystore/file" filekeystore "github.com/ethersphere/bee/pkg/keystore/file"
memkeystore "github.com/ethersphere/bee/pkg/keystore/mem" memkeystore "github.com/ethersphere/bee/pkg/keystore/mem"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/node" "github.com/ethersphere/bee/pkg/node"
"github.com/ethersphere/bee/pkg/resolver/multiresolver" "github.com/ethersphere/bee/pkg/resolver/multiresolver"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
...@@ -123,23 +126,60 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz ...@@ -123,23 +126,60 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
} }
} }
swarmPrivateKey, created, err := keystore.Key("swarm", password) var signer crypto.Signer
if err != nil { var address swarm.Address
return fmt.Errorf("swarm key: %w", err)
}
address, err := crypto.NewOverlayAddress(swarmPrivateKey.PublicKey, c.config.GetUint64(optionNameNetworkID)) if c.config.GetBool(optionNameClefSignerEnable) {
if err != nil { endpoint := c.config.GetString(optionNameClefSignerEndpoint)
return err if endpoint == "" {
} endpoint, err = clef.DefaultIpcPath()
if err != nil {
return err
}
}
externalSigner, err := external.NewExternalSigner(endpoint)
if err != nil {
return err
}
signer, err = clef.NewSigner(externalSigner, crypto.Recover)
if err != nil {
return err
}
if created { publicKey, err := signer.PublicKey()
logger.Infof("new swarm network address created: %s", address) if err != nil {
return err
}
address, err = crypto.NewOverlayAddress(*publicKey, c.config.GetUint64(optionNameNetworkID))
if err != nil {
return err
}
logger.Infof("using swarm network address through clef: %s", address)
} else { } else {
logger.Infof("using existing swarm network address: %s", address) swarmPrivateKey, created, err := keystore.Key("swarm", password)
if err != nil {
return fmt.Errorf("swarm key: %w", err)
}
signer = crypto.NewDefaultSigner(swarmPrivateKey)
publicKey := swarmPrivateKey.PublicKey
address, err = crypto.NewOverlayAddress(publicKey, c.config.GetUint64(optionNameNetworkID))
if err != nil {
return err
}
if created {
logger.Infof("new swarm network address created: %s", address)
} else {
logger.Infof("using existing swarm network address: %s", address)
}
} }
b, err := node.NewBee(c.config.GetString(optionNameP2PAddr), address, keystore, swarmPrivateKey, c.config.GetUint64(optionNameNetworkID), logger, node.Options{ b, err := node.NewBee(c.config.GetString(optionNameP2PAddr), address, keystore, signer, c.config.GetUint64(optionNameNetworkID), logger, node.Options{
DataDir: c.config.GetString(optionNameDataDir), DataDir: c.config.GetString(optionNameDataDir),
DBCapacity: c.config.GetUint64(optionNameDBCapacity), DBCapacity: c.config.GetUint64(optionNameDBCapacity),
Password: password, Password: password,
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package clef
import (
"crypto/ecdsa"
"errors"
"os"
"path/filepath"
"runtime"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethersphere/bee/pkg/crypto"
)
var (
ErrNoAccounts = errors.New("no accounts found in clef")
clefRecoveryMessage = []byte("public key recovery message")
)
// ExternalSignerInterface is the interface for the clef client from go-ethereum
type ExternalSignerInterface interface {
SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error)
Accounts() []accounts.Account
}
type clefSigner struct {
clef ExternalSignerInterface
account accounts.Account // the account this signer will use
pubKey *ecdsa.PublicKey // the public key for the account
}
// DefaultIpcPath returns the os-dependent default ipc path for clef
func DefaultIpcPath() (string, error) {
socket := "clef.ipc"
// on windows clef uses top level pipes
if runtime.GOOS == "windows" {
return `\\.\pipe\` + socket, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
// on mac os clef defaults to ~/Library/Signer/clef.ipc
if runtime.GOOS == "darwin" {
return filepath.Join(home, "Library", "Signer", socket), nil
}
// on unix clef defaults to ~/.clef/clef.ipc
return filepath.Join(home, ".clef", socket), nil
}
// NewSigner creates a new connection to the signer at endpoint
// As clef does not expose public keys it signs a test message to recover the public key
func NewSigner(clef ExternalSignerInterface, recoverFunc crypto.RecoverFunc) (signer crypto.Signer, err error) {
// get the list of available ethereum accounts
clefAccounts := clef.Accounts()
if len(clefAccounts) == 0 {
return nil, ErrNoAccounts
}
// pick the first account as the one we use
account := clefAccounts[0]
// clef currently does not expose the public key
// sign some data so we can recover it
sig, err := clef.SignData(account, accounts.MimetypeTextPlain, clefRecoveryMessage)
if err != nil {
return nil, err
}
pubKey, err := recoverFunc(sig, clefRecoveryMessage)
if err != nil {
return nil, err
}
return &clefSigner{
clef: clef,
account: account,
pubKey: pubKey,
}, nil
}
// PublicKey returns the public key recovered during creation
func (c *clefSigner) PublicKey() (*ecdsa.PublicKey, error) {
return c.pubKey, nil
}
// SignData signs with the text/plain type which is the standard Ethereum prefix method
func (c *clefSigner) Sign(data []byte) ([]byte, error) {
return c.clef.SignData(c.account, accounts.MimetypeTextPlain, data)
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package clef_test
import (
"bytes"
"crypto/ecdsa"
"errors"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/clef"
)
type mockClef struct {
accounts []accounts.Account
signature []byte
signedMimeType string
signedData []byte
signedAccount accounts.Account
}
func (m *mockClef) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
m.signedAccount = account
m.signedMimeType = mimeType
m.signedData = data
return m.signature, nil
}
func (m *mockClef) Accounts() []accounts.Account {
return m.accounts
}
func TestNewClefSigner(t *testing.T) {
ethAddress := common.HexToAddress("0x31415b599f636129AD03c196cef9f8f8b184D5C7")
testSignature := make([]byte, 65)
key, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
publicKey := &key.PublicKey
mock := &mockClef{
accounts: []accounts.Account{
{
Address: ethAddress,
},
},
signature: testSignature,
}
signer, err := clef.NewSigner(mock, func(signature, data []byte) (*ecdsa.PublicKey, error) {
if !bytes.Equal(testSignature, signature) {
t.Fatalf("wrong data used for recover. expected %v got %v", testSignature, signature)
}
if !bytes.Equal(clef.ClefRecoveryMessage, data) {
t.Fatalf("wrong data used for recover. expected %v got %v", clef.ClefRecoveryMessage, data)
}
return publicKey, nil
})
if err != nil {
t.Fatal(err)
}
if mock.signedAccount.Address != ethAddress {
t.Fatalf("wrong account used for signing. expected %v got %v", ethAddress, mock.signedAccount.Address)
}
if mock.signedMimeType != accounts.MimetypeTextPlain {
t.Fatalf("wrong mime type used for signing. expected %v got %v", accounts.MimetypeTextPlain, mock.signedMimeType)
}
if !bytes.Equal(mock.signedData, clef.ClefRecoveryMessage) {
t.Fatalf("wrong data used for signing. expected %v got %v", clef.ClefRecoveryMessage, mock.signedData)
}
signerPublicKey, err := signer.PublicKey()
if err != nil {
t.Fatal(err)
}
if signerPublicKey != publicKey {
t.Fatalf("wrong public key. expected %v got %v", publicKey, signerPublicKey)
}
}
func TestClefNoAccounts(t *testing.T) {
mock := &mockClef{
accounts: []accounts.Account{},
}
_, err := clef.NewSigner(mock, nil)
if err == nil {
t.Fatal("expected ErrNoAccounts error if no accounts")
}
if !errors.Is(err, clef.ErrNoAccounts) {
t.Fatalf("expected ErrNoAccounts error but got %v", err)
}
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package clef
var (
ClefRecoveryMessage = clefRecoveryMessage
)
...@@ -17,6 +17,9 @@ import ( ...@@ -17,6 +17,9 @@ import (
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
) )
// RecoverFunc is a function to recover the public key from a signature
type RecoverFunc func(signature, data []byte) (*ecdsa.PublicKey, error)
const ( const (
AddressSize = 20 AddressSize = 20
) )
......
...@@ -6,19 +6,48 @@ package crypto ...@@ -6,19 +6,48 @@ package crypto
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
) )
var (
ErrInvalidLength = errors.New("invalid signature length")
)
type Signer interface { type Signer interface {
Sign(data []byte) ([]byte, error) Sign(data []byte) ([]byte, error)
PublicKey() (*ecdsa.PublicKey, error) PublicKey() (*ecdsa.PublicKey, error)
} }
// addEthereumPrefix adds the ethereum prefix to the data
func addEthereumPrefix(data []byte) []byte {
return []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data))
}
// hashWithEthereumPrefix returns the hash that should be signed for the given data
func hashWithEthereumPrefix(data []byte) ([]byte, error) {
return LegacyKeccak256(addEthereumPrefix(data))
}
// Recover verifies signature with the data base provided. // Recover verifies signature with the data base provided.
// It is using `btcec.RecoverCompact` function // It is using `btcec.RecoverCompact` function
func Recover(signature, data []byte) (*ecdsa.PublicKey, error) { func Recover(signature, data []byte) (*ecdsa.PublicKey, error) {
p, _, err := btcec.RecoverCompact(btcec.S256(), signature, data) if len(signature) != 65 {
return nil, ErrInvalidLength
}
// Convert to btcec input format with 'recovery id' v at the beginning.
btcsig := make([]byte, 65)
btcsig[0] = signature[64]
copy(btcsig[1:], signature)
hash, err := hashWithEthereumPrefix(data)
if err != nil {
return nil, err
}
p, _, err := btcec.RecoverCompact(btcec.S256(), btcsig, hash)
return (*ecdsa.PublicKey)(p), err return (*ecdsa.PublicKey)(p), err
} }
...@@ -37,5 +66,19 @@ func (d *defaultSigner) PublicKey() (*ecdsa.PublicKey, error) { ...@@ -37,5 +66,19 @@ func (d *defaultSigner) PublicKey() (*ecdsa.PublicKey, error) {
} }
func (d *defaultSigner) Sign(data []byte) (signature []byte, err error) { func (d *defaultSigner) Sign(data []byte) (signature []byte, err error) {
return btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), data, true) hash, err := hashWithEthereumPrefix(data)
if err != nil {
return nil, err
}
signature, err = btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), hash, true)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := signature[0]
copy(signature, signature[1:])
signature[64] = v
return signature, nil
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package crypto_test package crypto_test
import ( import (
"errors"
"testing" "testing"
"github.com/ethersphere/bee/pkg/crypto" "github.com/ethersphere/bee/pkg/crypto"
...@@ -44,4 +45,14 @@ func TestDefaultSigner(t *testing.T) { ...@@ -44,4 +45,14 @@ func TestDefaultSigner(t *testing.T) {
t.Fatal("expected different public key") t.Fatal("expected different public key")
} }
}) })
t.Run("OK - recover with short signature", func(t *testing.T) {
_, err := crypto.Recover([]byte("invalid"), testBytes)
if err == nil {
t.Fatal("expected invalid length error but got none")
}
if !errors.Is(err, crypto.ErrInvalidLength) {
t.Fatalf("expected invalid length error but got %v", err)
}
})
} }
...@@ -6,7 +6,6 @@ package node ...@@ -6,7 +6,6 @@ package node
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"io" "io"
"log" "log"
...@@ -94,7 +93,7 @@ type Options struct { ...@@ -94,7 +93,7 @@ type Options struct {
GatewayMode bool GatewayMode bool
} }
func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, swarmPrivateKey *ecdsa.PrivateKey, networkID uint64, logger logging.Logger, o Options) (*Bee, error) { func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, signer crypto.Signer, networkID uint64, logger logging.Logger, o Options) (*Bee, error) {
tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{ tracer, tracerCloser, err := tracing.NewTracer(&tracing.Options{
Enabled: o.TracingEnabled, Enabled: o.TracingEnabled,
Endpoint: o.TracingEndpoint, Endpoint: o.TracingEndpoint,
...@@ -135,7 +134,6 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service, ...@@ -135,7 +134,6 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
} }
b.stateStoreCloser = stateStore b.stateStoreCloser = stateStore
addressbook := addressbook.New(stateStore) addressbook := addressbook.New(stateStore)
signer := crypto.NewDefaultSigner(swarmPrivateKey)
p2ps, err := libp2p.New(p2pCtx, signer, networkID, swarmAddress, addr, addressbook, stateStore, logger, tracer, libp2p.Options{ p2ps, err := libp2p.New(p2pCtx, signer, networkID, swarmAddress, addr, addressbook, stateStore, logger, tracer, libp2p.Options{
PrivateKey: libp2pPrivateKey, PrivateKey: libp2pPrivateKey,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment