aboutsummaryrefslogtreecommitdiffstats
path: root/signer/core
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2019-02-13 00:38:46 +0800
committerGitHub <noreply@github.com>2019-02-13 00:38:46 +0800
commitb5d471a73905247406dcbe5ffac6087f80896374 (patch)
tree4ab7f61bfbf3cacb183ddeb95465f19dd13bba7c /signer/core
parent75d292bcf673e1b10ac883f8767d092d2f45fd9a (diff)
downloadgo-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar.gz
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar.bz2
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar.lz
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar.xz
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.tar.zst
go-tangerine-b5d471a73905247406dcbe5ffac6087f80896374.zip
clef: bidirectional communication with UI (#19018)
* clef: initial implementation of bidirectional RPC communication for the UI * signer: fix tests to pass + formatting * clef: fix unused import + formatting * signer: gosimple nitpicks
Diffstat (limited to 'signer/core')
-rw-r--r--signer/core/api.go163
-rw-r--r--signer/core/api_test.go22
-rw-r--r--signer/core/auditlog.go13
-rw-r--r--signer/core/cliui.go5
-rw-r--r--signer/core/stdioui.go8
-rw-r--r--signer/core/types.go25
-rw-r--r--signer/core/uiapi.go201
7 files changed, 277 insertions, 160 deletions
diff --git a/signer/core/api.go b/signer/core/api.go
index 25057dd12..30888f842 100644
--- a/signer/core/api.go
+++ b/signer/core/api.go
@@ -21,7 +21,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
"math/big"
"reflect"
@@ -39,9 +38,9 @@ const (
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
numberOfAccountsToDerive = 10
// ExternalAPIVersion -- see extapi_changelog.md
- ExternalAPIVersion = "5.0.0"
+ ExternalAPIVersion = "6.0.0"
// InternalAPIVersion -- see intapi_changelog.md
- InternalAPIVersion = "3.2.0"
+ InternalAPIVersion = "4.0.0"
)
// ExternalAPI defines the external API through which signing requests are made.
@@ -49,7 +48,7 @@ type ExternalAPI interface {
// List available accounts
List(ctx context.Context) ([]common.Address, error)
// New request to create a new account
- New(ctx context.Context) (accounts.Account, error)
+ New(ctx context.Context) (common.Address, error)
// SignTransaction request to sign the specified transaction
SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error)
// SignData - request to sign the given data (plus prefix)
@@ -58,17 +57,13 @@ type ExternalAPI interface {
SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error)
// EcRecover - recover public key from given message and signature
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
- // Export - request to export an account
- Export(ctx context.Context, addr common.Address) (json.RawMessage, error)
- // Import - request to import an account
- // Should be moved to Internal API, in next phase when we have
- // bi-directional communication
- //Import(ctx context.Context, keyJSON json.RawMessage) (Account, error)
+ // Version info about the APIs
Version(ctx context.Context) (string, error)
}
-// SignerUI specifies what method a UI needs to implement to be able to be used as a UI for the signer
-type SignerUI interface {
+// UIClientAPI specifies what method a UI needs to implement to be able to be used as a
+// UI for the signer
+type UIClientAPI interface {
// ApproveTx prompt the user for confirmation to request to sign Transaction
ApproveTx(request *SignTxRequest) (SignTxResponse, error)
// ApproveSignData prompt the user for confirmation to request to sign data
@@ -95,13 +90,15 @@ type SignerUI interface {
// OnInputRequired is invoked when clef requires user input, for example master password or
// pin-code for unlocking hardware wallets
OnInputRequired(info UserInputRequest) (UserInputResponse, error)
+ // RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication
+ RegisterUIServer(api *UIServerAPI)
}
// SignerAPI defines the actual implementation of ExternalAPI
type SignerAPI struct {
chainID *big.Int
am *accounts.Manager
- UI SignerUI
+ UI UIClientAPI
validator *Validator
rejectMode bool
}
@@ -115,6 +112,37 @@ type Metadata struct {
Origin string `json:"Origin"`
}
+func StartClefAccountManager(ksLocation string, nousb, lightKDF bool) *accounts.Manager {
+ var (
+ backends []accounts.Backend
+ n, p = keystore.StandardScryptN, keystore.StandardScryptP
+ )
+ if lightKDF {
+ n, p = keystore.LightScryptN, keystore.LightScryptP
+ }
+ // support password based accounts
+ if len(ksLocation) > 0 {
+ backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
+ }
+ if !nousb {
+ // Start a USB hub for Ledger hardware wallets
+ if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
+ } else {
+ backends = append(backends, ledgerhub)
+ log.Debug("Ledger support enabled")
+ }
+ // Start a USB hub for Trezor hardware wallets
+ if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
+ } else {
+ backends = append(backends, trezorhub)
+ log.Debug("Trezor support enabled")
+ }
+ }
+ return accounts.NewManager(backends...)
+}
+
// MetadataFromContext extracts Metadata from a given context.Context
func MetadataFromContext(ctx context.Context) Metadata {
m := Metadata{"NA", "NA", "NA", "", ""} // batman
@@ -199,11 +227,11 @@ type (
Password string `json:"password"`
}
ListRequest struct {
- Accounts []Account `json:"accounts"`
- Meta Metadata `json:"meta"`
+ Accounts []accounts.Account `json:"accounts"`
+ Meta Metadata `json:"meta"`
}
ListResponse struct {
- Accounts []Account `json:"accounts"`
+ Accounts []accounts.Account `json:"accounts"`
}
Message struct {
Text string `json:"text"`
@@ -234,38 +262,11 @@ var ErrRequestDenied = errors.New("Request denied")
// key that is generated when a new Account is created.
// noUSB disables USB support that is required to support hardware devices such as
// ledger and trezor.
-func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *AbiDb, lightKDF bool, advancedMode bool) *SignerAPI {
- var (
- backends []accounts.Backend
- n, p = keystore.StandardScryptN, keystore.StandardScryptP
- )
- if lightKDF {
- n, p = keystore.LightScryptN, keystore.LightScryptP
- }
- // support password based accounts
- if len(ksLocation) > 0 {
- backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
- }
+func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, abidb *AbiDb, advancedMode bool) *SignerAPI {
if advancedMode {
log.Info("Clef is in advanced mode: will warn instead of reject")
}
- if !noUSB {
- // Start a USB hub for Ledger hardware wallets
- if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
- log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
- } else {
- backends = append(backends, ledgerhub)
- log.Debug("Ledger support enabled")
- }
- // Start a USB hub for Trezor hardware wallets
- if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
- log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
- } else {
- backends = append(backends, trezorhub)
- log.Debug("Trezor support enabled")
- }
- }
- signer := &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode}
+ signer := &SignerAPI{big.NewInt(chainID), am, ui, NewValidator(abidb), !advancedMode}
if !noUSB {
signer.startUSBListener()
}
@@ -358,12 +359,9 @@ func (api *SignerAPI) startUSBListener() {
// List returns the set of wallet this signer manages. Each wallet can contain
// multiple accounts.
func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) {
- var accs []Account
+ var accs []accounts.Account
for _, wallet := range api.am.Wallets() {
- for _, acc := range wallet.Accounts() {
- acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address}
- accs = append(accs, acc)
- }
+ accs = append(accs, wallet.Accounts()...)
}
result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)})
if err != nil {
@@ -373,7 +371,6 @@ func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) {
return nil, ErrRequestDenied
}
-
addresses := make([]common.Address, 0)
for _, acc := range result.Accounts {
addresses = append(addresses, acc.Address)
@@ -385,10 +382,10 @@ func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) {
// New creates a new password protected Account. The private key is protected with
// the given password. Users are responsible to backup the private key that is stored
// in the keystore location thas was specified when this API was created.
-func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) {
+func (api *SignerAPI) New(ctx context.Context) (common.Address, error) {
be := api.am.Backends(keystore.KeyStoreType)
if len(be) == 0 {
- return accounts.Account{}, errors.New("password based accounts not supported")
+ return common.Address{}, errors.New("password based accounts not supported")
}
var (
resp NewAccountResponse
@@ -398,20 +395,21 @@ func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) {
for i := 0; i < 3; i++ {
resp, err = api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)})
if err != nil {
- return accounts.Account{}, err
+ return common.Address{}, err
}
if !resp.Approved {
- return accounts.Account{}, ErrRequestDenied
+ return common.Address{}, ErrRequestDenied
}
if pwErr := ValidatePasswordFormat(resp.Password); pwErr != nil {
api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", (i + 1), pwErr))
} else {
// No error
- return be[0].(*keystore.KeyStore).NewAccount(resp.Password)
+ acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Password)
+ return acc.Address, err
}
}
// Otherwise fail, with generic error message
- return accounts.Account{}, errors.New("account creation failed")
+ return common.Address{}, errors.New("account creation failed")
}
// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
@@ -521,57 +519,6 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
}
-// Export returns encrypted private key associated with the given address in web3 keystore format.
-func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
- res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(ctx)})
-
- if err != nil {
- return nil, err
- }
- if !res.Approved {
- return nil, ErrRequestDenied
- }
- // Look up the wallet containing the requested signer
- wallet, err := api.am.Find(accounts.Account{Address: addr})
- if err != nil {
- return nil, err
- }
- if wallet.URL().Scheme != keystore.KeyStoreScheme {
- return nil, fmt.Errorf("Account is not a keystore-account")
- }
- return ioutil.ReadFile(wallet.URL().Path)
-}
-
-// Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
-// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
-// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
-// OBS! This method is removed from the public API. It should not be exposed on the external API
-// for a couple of reasons:
-// 1. Even though it is encrypted, it should still be seen as sensitive data
-// 2. It can be used to DoS clef, by using malicious data with e.g. extreme large
-// values for the kdfparams.
-func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
- be := api.am.Backends(keystore.KeyStoreType)
-
- if len(be) == 0 {
- return Account{}, errors.New("password based accounts not supported")
- }
- res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)})
-
- if err != nil {
- return Account{}, err
- }
- if !res.Approved {
- return Account{}, ErrRequestDenied
- }
- acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword)
- if err != nil {
- api.UI.ShowError(err.Error())
- return Account{}, err
- }
- return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil
-}
-
// Returns the external api version. This method does not require user acceptance. Available methods are
// available via enumeration anyway, and this info does not contain user-specific data
func (api *SignerAPI) Version(ctx context.Context) (string, error) {
diff --git a/signer/core/api_test.go b/signer/core/api_test.go
index 0e0c54517..bf4e91eb9 100644
--- a/signer/core/api_test.go
+++ b/signer/core/api_test.go
@@ -28,6 +28,7 @@ import (
"testing"
"time"
+ "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -47,6 +48,8 @@ func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse,
func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
}
+func (ui *HeadlessUI) RegisterUIServer(api *UIServerAPI) {
+}
func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
fmt.Printf("OnApproved()\n")
@@ -91,7 +94,7 @@ func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error)
case "A":
return ListResponse{request.Accounts}, nil
case "1":
- l := make([]Account, 1)
+ l := make([]accounts.Account, 1)
l[0] = request.Accounts[1]
return ListResponse{l}, nil
default:
@@ -138,13 +141,8 @@ func setup(t *testing.T) (*SignerAPI, chan string) {
}
var (
ui = &HeadlessUI{controller}
- api = NewSignerAPI(
- 1,
- tmpDirName(t),
- true,
- ui,
- db,
- true, true)
+ am = StartClefAccountManager(tmpDirName(t), true, true)
+ api = NewSignerAPI(am, 1337, true, ui, db, true)
)
return api, controller
}
@@ -169,22 +167,22 @@ func failCreateAccountWithPassword(control chan string, api *SignerAPI, password
control <- "Y"
control <- password
- acc, err := api.New(context.Background())
+ addr, err := api.New(context.Background())
if err == nil {
t.Fatal("Should have returned an error")
}
- if acc.Address != (common.Address{}) {
+ if addr != (common.Address{}) {
t.Fatal("Empty address should be returned")
}
}
func failCreateAccount(control chan string, api *SignerAPI, t *testing.T) {
control <- "N"
- acc, err := api.New(context.Background())
+ addr, err := api.New(context.Background())
if err != ErrRequestDenied {
t.Fatal(err)
}
- if acc.Address != (common.Address{}) {
+ if addr != (common.Address{}) {
t.Fatal("Empty address should be returned")
}
}
diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go
index d64ba1ef9..9593ad7a5 100644
--- a/signer/core/auditlog.go
+++ b/signer/core/auditlog.go
@@ -18,9 +18,7 @@ package core
import (
"context"
- "encoding/json"
- "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/internal/ethapi"
@@ -40,7 +38,7 @@ func (l *AuditLogger) List(ctx context.Context) ([]common.Address, error) {
return res, e
}
-func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) {
+func (l *AuditLogger) New(ctx context.Context) (common.Address, error) {
return l.api.New(ctx)
}
@@ -86,15 +84,6 @@ func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hex
return b, e
}
-func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
- l.log.Info("Export", "type", "request", "metadata", MetadataFromContext(ctx).String(),
- "addr", addr.Hex())
- j, e := l.api.Export(ctx, addr)
- // In this case, we don't actually log the json-response, which may be extra sensitive
- l.log.Info("Export", "type", "response", "json response size", len(j), "error", e)
- return j, e
-}
-
func (l *AuditLogger) Version(ctx context.Context) (string, error) {
l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String())
data, err := l.api.Version(ctx)
diff --git a/signer/core/cliui.go b/signer/core/cliui.go
index 3ffc6b191..a6c0bdb16 100644
--- a/signer/core/cliui.go
+++ b/signer/core/cliui.go
@@ -39,6 +39,10 @@ func NewCommandlineUI() *CommandlineUI {
return &CommandlineUI{in: bufio.NewReader(os.Stdin)}
}
+func (ui *CommandlineUI) RegisterUIServer(api *UIServerAPI) {
+ // noop
+}
+
// readString reads a single line from stdin, trimming if from spaces, enforcing
// non-emptyness.
func (ui *CommandlineUI) readString() string {
@@ -223,7 +227,6 @@ func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, err
for _, account := range request.Accounts {
fmt.Printf(" [x] %v\n", account.Address.Hex())
fmt.Printf(" URL: %v\n", account.URL)
- fmt.Printf(" Type: %v\n", account.Typ)
}
fmt.Printf("-------------------------------------------\n")
showMetadata(request.Meta)
diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go
index e169a8bc0..cb25bc2d0 100644
--- a/signer/core/stdioui.go
+++ b/signer/core/stdioui.go
@@ -32,12 +32,16 @@ type StdIOUI struct {
}
func NewStdIOUI() *StdIOUI {
- log.Info("NewStdIOUI")
client, err := rpc.DialContext(context.Background(), "stdio://")
if err != nil {
log.Crit("Could not create stdio client", "err", err)
}
- return &StdIOUI{client: *client}
+ ui := &StdIOUI{client: *client}
+ return ui
+}
+
+func (ui *StdIOUI) RegisterUIServer(api *UIServerAPI) {
+ ui.client.RegisterName("clef", api)
}
// dispatch sends a request over the stdio
diff --git a/signer/core/types.go b/signer/core/types.go
index 8acfa7a6a..c1d3b9bf8 100644
--- a/signer/core/types.go
+++ b/signer/core/types.go
@@ -22,36 +22,11 @@ import (
"math/big"
"strings"
- "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
)
-type Accounts []Account
-
-func (as Accounts) String() string {
- var output []string
- for _, a := range as {
- output = append(output, a.String())
- }
- return strings.Join(output, "\n")
-}
-
-type Account struct {
- Typ string `json:"type"`
- URL accounts.URL `json:"url"`
- Address common.Address `json:"address"`
-}
-
-func (a Account) String() string {
- s, err := json.Marshal(a)
- if err == nil {
- return string(s)
- }
- return err.Error()
-}
-
type ValidationInfo struct {
Typ string `json:"type"`
Message string `json:"message"`
diff --git a/signer/core/uiapi.go b/signer/core/uiapi.go
new file mode 100644
index 000000000..6dc68313b
--- /dev/null
+++ b/signer/core/uiapi.go
@@ -0,0 +1,201 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+//
+
+package core
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+// SignerUIAPI implements methods Clef provides for a UI to query, in the bidirectional communication
+// channel.
+// This API is considered secure, since a request can only
+// ever arrive from the UI -- and the UI is capable of approving any action, thus we can consider these
+// requests pre-approved.
+// NB: It's very important that these methods are not ever exposed on the external service
+// registry.
+type UIServerAPI struct {
+ extApi *SignerAPI
+ am *accounts.Manager
+}
+
+// NewUIServerAPI creates a new UIServerAPI
+func NewUIServerAPI(extapi *SignerAPI) *UIServerAPI {
+ return &UIServerAPI{extapi, extapi.am}
+}
+
+// List available accounts. As opposed to the external API definition, this method delivers
+// the full Account object and not only Address.
+// Example call
+// {"jsonrpc":"2.0","method":"clef_listAccounts","params":[], "id":4}
+func (s *UIServerAPI) ListAccounts(ctx context.Context) ([]accounts.Account, error) {
+ var accs []accounts.Account
+ for _, wallet := range s.am.Wallets() {
+ accs = append(accs, wallet.Accounts()...)
+ }
+ return accs, nil
+}
+
+// rawWallet is a JSON representation of an accounts.Wallet interface, with its
+// data contents extracted into plain fields.
+type rawWallet struct {
+ URL string `json:"url"`
+ Status string `json:"status"`
+ Failure string `json:"failure,omitempty"`
+ Accounts []accounts.Account `json:"accounts,omitempty"`
+}
+
+// ListWallets will return a list of wallets that clef manages
+// Example call
+// {"jsonrpc":"2.0","method":"clef_listWallets","params":[], "id":5}
+func (s *UIServerAPI) ListWallets() []rawWallet {
+ wallets := make([]rawWallet, 0) // return [] instead of nil if empty
+ for _, wallet := range s.am.Wallets() {
+ status, failure := wallet.Status()
+
+ raw := rawWallet{
+ URL: wallet.URL().String(),
+ Status: status,
+ Accounts: wallet.Accounts(),
+ }
+ if failure != nil {
+ raw.Failure = failure.Error()
+ }
+ wallets = append(wallets, raw)
+ }
+ return wallets
+}
+
+// DeriveAccount requests a HD wallet to derive a new account, optionally pinning
+// it for later reuse.
+// Example call
+// {"jsonrpc":"2.0","method":"clef_deriveAccount","params":["ledger://","m/44'/60'/0'", false], "id":6}
+func (s *UIServerAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) {
+ wallet, err := s.am.Wallet(url)
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ derivPath, err := accounts.ParseDerivationPath(path)
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ if pin == nil {
+ pin = new(bool)
+ }
+ return wallet.Derive(derivPath, *pin)
+}
+
+// fetchKeystore retrives the encrypted keystore from the account manager.
+func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
+ return am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
+}
+
+// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
+// encrypting it with the passphrase.
+// Example call (should fail on password too short)
+// {"jsonrpc":"2.0","method":"clef_importRawKey","params":["1111111111111111111111111111111111111111111111111111111111111111","test"], "id":6}
+func (s *UIServerAPI) ImportRawKey(privkey string, password string) (accounts.Account, error) {
+ key, err := crypto.HexToECDSA(privkey)
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ if err := ValidatePasswordFormat(password); err != nil {
+ return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err)
+ }
+ // No error
+ return fetchKeystore(s.am).ImportECDSA(key, password)
+}
+
+// OpenWallet initiates a hardware wallet opening procedure, establishing a USB
+// connection and attempting to authenticate via the provided passphrase. Note,
+// the method may return an extra challenge requiring a second open (e.g. the
+// Trezor PIN matrix challenge).
+// Example
+// {"jsonrpc":"2.0","method":"clef_openWallet","params":["ledger://",""], "id":6}
+func (s *UIServerAPI) OpenWallet(url string, passphrase *string) error {
+ wallet, err := s.am.Wallet(url)
+ if err != nil {
+ return err
+ }
+ pass := ""
+ if passphrase != nil {
+ pass = *passphrase
+ }
+ return wallet.Open(pass)
+}
+
+// ChainId returns the chainid in use for Eip-155 replay protection
+// Example call
+// {"jsonrpc":"2.0","method":"clef_chainId","params":[], "id":8}
+func (s *UIServerAPI) ChainId() math.HexOrDecimal64 {
+ return (math.HexOrDecimal64)(s.extApi.chainID.Uint64())
+}
+
+// SetChainId sets the chain id to use when signing transactions.
+// Example call to set Ropsten:
+// {"jsonrpc":"2.0","method":"clef_setChainId","params":["3"], "id":8}
+func (s *UIServerAPI) SetChainId(id math.HexOrDecimal64) math.HexOrDecimal64 {
+ s.extApi.chainID = new(big.Int).SetUint64(uint64(id))
+ return s.ChainId()
+}
+
+// Export returns encrypted private key associated with the given address in web3 keystore format.
+// Example
+// {"jsonrpc":"2.0","method":"clef_export","params":["0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"], "id":4}
+func (s *UIServerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
+ // Look up the wallet containing the requested signer
+ wallet, err := s.am.Find(accounts.Account{Address: addr})
+ if err != nil {
+ return nil, err
+ }
+ if wallet.URL().Scheme != keystore.KeyStoreScheme {
+ return nil, fmt.Errorf("Account is not a keystore-account")
+ }
+ return ioutil.ReadFile(wallet.URL().Path)
+}
+
+// Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
+// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
+// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
+// Example (the address in question has privkey `11...11`):
+// {"jsonrpc":"2.0","method":"clef_import","params":[{"address":"19e7e376e7c213b7e7e7e46cc70a5dd086daff2a","crypto":{"cipher":"aes-128-ctr","ciphertext":"33e4cd3756091d037862bb7295e9552424a391a6e003272180a455ca2a9fb332","cipherparams":{"iv":"b54b263e8f89c42bb219b6279fba5cce"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"e4ca94644fd30569c1b1afbbc851729953c92637b7fe4bb9840bbb31ffbc64a5"},"mac":"f4092a445c2b21c0ef34f17c9cd0d873702b2869ec5df4439a0c2505823217e7"},"id":"216c7eac-e8c1-49af-a215-fa0036f29141","version":3},"test","yaddayadda"], "id":4}
+func (api *UIServerAPI) Import(ctx context.Context, keyJSON json.RawMessage, oldPassphrase, newPassphrase string) (accounts.Account, error) {
+ be := api.am.Backends(keystore.KeyStoreType)
+
+ if len(be) == 0 {
+ return accounts.Account{}, errors.New("password based accounts not supported")
+ }
+ if err := ValidatePasswordFormat(newPassphrase); err != nil {
+ return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err)
+ }
+ return be[0].(*keystore.KeyStore).Import(keyJSON, oldPassphrase, newPassphrase)
+}
+
+// Other methods to be added, not yet implemented are:
+// - Ruleset interaction: add rules, attest rulefiles
+// - Store metadata about accounts, e.g. naming of accounts