aboutsummaryrefslogtreecommitdiff
path: root/accounts/scwallet
diff options
context:
space:
mode:
authorDeterminant <[email protected]>2020-07-30 14:18:44 -0400
committerDeterminant <[email protected]>2020-07-30 14:18:44 -0400
commit0444e66f640999c15496066637841efcc0433934 (patch)
treec19aec2dced2e9129c880c19c52ca0f87b3d62f6 /accounts/scwallet
parentcffa0954bbdb43821d1b71d00f99fb705cecd25b (diff)
parent1f49826de2bb8bb4f5f99f69fd2beb039b1172d9 (diff)
Merge branch 'multi-coin'
Diffstat (limited to 'accounts/scwallet')
-rw-r--r--accounts/scwallet/README.md102
-rw-r--r--accounts/scwallet/apdu.go87
-rw-r--r--accounts/scwallet/hub.go302
-rw-r--r--accounts/scwallet/securechannel.go346
-rw-r--r--accounts/scwallet/wallet.go1082
5 files changed, 1919 insertions, 0 deletions
diff --git a/accounts/scwallet/README.md b/accounts/scwallet/README.md
new file mode 100644
index 0000000..cfca916
--- /dev/null
+++ b/accounts/scwallet/README.md
@@ -0,0 +1,102 @@
+# Using the smartcard wallet
+
+## Requirements
+
+ * A USB smartcard reader
+ * A keycard that supports the status app
+ * PCSCD version 4.3 running on your system **Only version 4.3 is currently supported**
+
+## Preparing the smartcard
+
+ **WARNING: FOILLOWING THESE INSTRUCTIONS WILL DESTROY THE MASTER KEY ON YOUR CARD. ONLY PROCEED IF NO FUNDS ARE ASSOCIATED WITH THESE ACCOUNTS**
+
+ You can use status' [keycard-cli](https://github.com/status-im/keycard-cli) and you should get _at least_ version 2.1.1 of their [smartcard application](https://github.com/status-im/status-keycard/releases/download/2.2.1/keycard_v2.2.1.cap)
+
+ You also need to make sure that the PCSC daemon is running on your system.
+
+ Then, you can install the application to the card by typing:
+
+ ```
+ keycard install -a keycard_v2.2.1.cap && keycard init
+ ```
+
+ At the end of this process, you will be provided with a PIN, a PUK and a pairing password. Write them down, you'll need them shortly.
+
+ Start `geth` with the `console` command. You will notice the following warning:
+
+ ```
+ WARN [04-09|16:58:38.898] Failed to open wallet url=keycard://044def09 err="smartcard: pairing password needed"
+ ```
+
+ Write down the URL (`keycard://044def09` in this example). Then ask `geth` to open the wallet:
+
+ ```
+ > personal.openWallet("keycard://044def09")
+ Please enter the pairing password:
+ ```
+
+ Enter the pairing password that you have received during card initialization. Same with the PIN that you will subsequently be
+ asked for.
+
+ If everything goes well, you should see your new account when typing `personal` on the console:
+
+ ```
+ > personal
+ WARN [04-09|17:02:07.330] Smartcard wallet account derivation failed url=keycard://044def09 err="Unexpected response status Cla=0x80, Ins=0xd1, Sw=0x6985"
+ {
+ listAccounts: [],
+ listWallets: [{
+ status: "Empty, waiting for initialization",
+ url: "keycard://044def09"
+ }],
+ ...
+ }
+ ```
+
+ So the communication with the card is working, but there is no key associated with this wallet. Let's create it:
+
+ ```
+ > personal.initializeWallet("keycard://044def09")
+ "tilt ... impact"
+ ```
+
+ You should get a list of words, this is your seed so write them down. Your wallet should now be initialized:
+
+ ```
+ > personal.listWallets
+ [{
+ accounts: [{
+ address: "0x678b7cd55c61917defb23546a41803c5bfefbc7a",
+ url: "keycard://044d/m/44'/60'/0'/0/0"
+ }],
+ status: "Online",
+ url: "keycard://044def09"
+ }]
+ ```
+
+ You're all set!
+
+## Usage
+
+ 1. Start `geth` with the `console` command
+ 2. Check the card's URL by checking `personal.listWallets`:
+
+```
+ listWallets: [{
+ status: "Online, can derive public keys",
+ url: "keycard://a4d73015"
+ }]
+```
+
+ 3. Open the wallet, you will be prompted for your pairing password, then PIN:
+
+```
+personal.openWallet("keycard://a4d73015")
+```
+
+ 4. Check that creation was successful by typing e.g. `personal`. Then use it like a regular wallet.
+
+## Known issues
+
+ * Starting geth with a valid card seems to make firefox crash.
+ * PCSC version 4.4 should work, but is currently untested
diff --git a/accounts/scwallet/apdu.go b/accounts/scwallet/apdu.go
new file mode 100644
index 0000000..bd36606
--- /dev/null
+++ b/accounts/scwallet/apdu.go
@@ -0,0 +1,87 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package scwallet
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+)
+
+// commandAPDU represents an application data unit sent to a smartcard.
+type commandAPDU struct {
+ Cla, Ins, P1, P2 uint8 // Class, Instruction, Parameter 1, Parameter 2
+ Data []byte // Command data
+ Le uint8 // Command data length
+}
+
+// serialize serializes a command APDU.
+func (ca commandAPDU) serialize() ([]byte, error) {
+ buf := new(bytes.Buffer)
+
+ if err := binary.Write(buf, binary.BigEndian, ca.Cla); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, ca.Ins); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, ca.P1); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, ca.P2); err != nil {
+ return nil, err
+ }
+ if len(ca.Data) > 0 {
+ if err := binary.Write(buf, binary.BigEndian, uint8(len(ca.Data))); err != nil {
+ return nil, err
+ }
+ if err := binary.Write(buf, binary.BigEndian, ca.Data); err != nil {
+ return nil, err
+ }
+ }
+ if err := binary.Write(buf, binary.BigEndian, ca.Le); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// responseAPDU represents an application data unit received from a smart card.
+type responseAPDU struct {
+ Data []byte // response data
+ Sw1, Sw2 uint8 // status words 1 and 2
+}
+
+// deserialize deserializes a response APDU.
+func (ra *responseAPDU) deserialize(data []byte) error {
+ if len(data) < 2 {
+ return fmt.Errorf("can not deserialize data: payload too short (%d < 2)", len(data))
+ }
+
+ ra.Data = make([]byte, len(data)-2)
+
+ buf := bytes.NewReader(data)
+ if err := binary.Read(buf, binary.BigEndian, &ra.Data); err != nil {
+ return err
+ }
+ if err := binary.Read(buf, binary.BigEndian, &ra.Sw1); err != nil {
+ return err
+ }
+ if err := binary.Read(buf, binary.BigEndian, &ra.Sw2); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/accounts/scwallet/hub.go b/accounts/scwallet/hub.go
new file mode 100644
index 0000000..ceb422c
--- /dev/null
+++ b/accounts/scwallet/hub.go
@@ -0,0 +1,302 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+// This package implements support for smartcard-based hardware wallets such as
+// the one written by Status: https://github.com/status-im/hardware-wallet
+//
+// This implementation of smartcard wallets have a different interaction process
+// to other types of hardware wallet. The process works like this:
+//
+// 1. (First use with a given client) Establish a pairing between hardware
+// wallet and client. This requires a secret value called a 'pairing password'.
+// You can pair with an unpaired wallet with `personal.openWallet(URI, pairing password)`.
+// 2. (First use only) Initialize the wallet, which generates a keypair, stores
+// it on the wallet, and returns it so the user can back it up. You can
+// initialize a wallet with `personal.initializeWallet(URI)`.
+// 3. Connect to the wallet using the pairing information established in step 1.
+// You can connect to a paired wallet with `personal.openWallet(URI, PIN)`.
+// 4. Interact with the wallet as normal.
+
+package scwallet
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/ava-labs/coreth/accounts"
+ "github.com/ava-labs/go-ethereum/common"
+ "github.com/ava-labs/go-ethereum/event"
+ "github.com/ava-labs/go-ethereum/log"
+ pcsc "github.com/gballet/go-libpcsclite"
+)
+
+// Scheme is the URI prefix for smartcard wallets.
+const Scheme = "keycard"
+
+// refreshCycle is the maximum time between wallet refreshes (if USB hotplug
+// notifications don't work).
+const refreshCycle = time.Second
+
+// refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
+const refreshThrottling = 500 * time.Millisecond
+
+// smartcardPairing contains information about a smart card we have paired with
+// or might pair with the hub.
+type smartcardPairing struct {
+ PublicKey []byte `json:"publicKey"`
+ PairingIndex uint8 `json:"pairingIndex"`
+ PairingKey []byte `json:"pairingKey"`
+ Accounts map[common.Address]accounts.DerivationPath `json:"accounts"`
+}
+
+// Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets.
+type Hub struct {
+ scheme string // Protocol scheme prefixing account and wallet URLs.
+
+ context *pcsc.Client
+ datadir string
+ pairings map[string]smartcardPairing
+
+ refreshed time.Time // Time instance when the list of wallets was last refreshed
+ wallets map[string]*Wallet // Mapping from reader names to wallet instances
+ updateFeed event.Feed // Event feed to notify wallet additions/removals
+ updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
+ updating bool // Whether the event notification loop is running
+
+ quit chan chan error
+
+ stateLock sync.RWMutex // Protects the internals of the hub from racey access
+}
+
+func (hub *Hub) readPairings() error {
+ hub.pairings = make(map[string]smartcardPairing)
+ pairingFile, err := os.Open(filepath.Join(hub.datadir, "smartcards.json"))
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ pairingData, err := ioutil.ReadAll(pairingFile)
+ if err != nil {
+ return err
+ }
+ var pairings []smartcardPairing
+ if err := json.Unmarshal(pairingData, &pairings); err != nil {
+ return err
+ }
+
+ for _, pairing := range pairings {
+ hub.pairings[string(pairing.PublicKey)] = pairing
+ }
+ return nil
+}
+
+func (hub *Hub) writePairings() error {
+ pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755)
+ if err != nil {
+ return err
+ }
+ defer pairingFile.Close()
+
+ pairings := make([]smartcardPairing, 0, len(hub.pairings))
+ for _, pairing := range hub.pairings {
+ pairings = append(pairings, pairing)
+ }
+
+ pairingData, err := json.Marshal(pairings)
+ if err != nil {
+ return err
+ }
+
+ if _, err := pairingFile.Write(pairingData); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
+ if pairing, ok := hub.pairings[string(wallet.PublicKey)]; ok {
+ return &pairing
+ }
+ return nil
+}
+
+func (hub *Hub) setPairing(wallet *Wallet, pairing *smartcardPairing) error {
+ if pairing == nil {
+ delete(hub.pairings, string(wallet.PublicKey))
+ } else {
+ hub.pairings[string(wallet.PublicKey)] = *pairing
+ }
+ return hub.writePairings()
+}
+
+// NewHub creates a new hardware wallet manager for smartcards.
+func NewHub(daemonPath string, scheme string, datadir string) (*Hub, error) {
+ context, err := pcsc.EstablishContext(daemonPath, pcsc.ScopeSystem)
+ if err != nil {
+ return nil, err
+ }
+ hub := &Hub{
+ scheme: scheme,
+ context: context,
+ datadir: datadir,
+ wallets: make(map[string]*Wallet),
+ quit: make(chan chan error),
+ }
+ if err := hub.readPairings(); err != nil {
+ return nil, err
+ }
+ hub.refreshWallets()
+ return hub, nil
+}
+
+// Wallets implements accounts.Backend, returning all the currently tracked smart
+// cards that appear to be hardware wallets.
+func (hub *Hub) Wallets() []accounts.Wallet {
+ // Make sure the list of wallets is up to date
+ hub.refreshWallets()
+
+ hub.stateLock.RLock()
+ defer hub.stateLock.RUnlock()
+
+ cpy := make([]accounts.Wallet, 0, len(hub.wallets))
+ for _, wallet := range hub.wallets {
+ cpy = append(cpy, wallet)
+ }
+ sort.Sort(accounts.WalletsByURL(cpy))
+ return cpy
+}
+
+// refreshWallets scans the devices attached to the machine and updates the
+// list of wallets based on the found devices.
+func (hub *Hub) refreshWallets() {
+ // Don't scan the USB like crazy it the user fetches wallets in a loop
+ hub.stateLock.RLock()
+ elapsed := time.Since(hub.refreshed)
+ hub.stateLock.RUnlock()
+
+ if elapsed < refreshThrottling {
+ return
+ }
+ // Retrieve all the smart card reader to check for cards
+ readers, err := hub.context.ListReaders()
+ if err != nil {
+ // This is a perverted hack, the scard library returns an error if no card
+ // readers are present instead of simply returning an empty list. We don't
+ // want to fill the user's log with errors, so filter those out.
+ if err.Error() != "scard: Cannot find a smart card reader." {
+ log.Error("Failed to enumerate smart card readers", "err", err)
+ return
+ }
+ }
+ // Transform the current list of wallets into the new one
+ hub.stateLock.Lock()
+
+ events := []accounts.WalletEvent{}
+ seen := make(map[string]struct{})
+
+ for _, reader := range readers {
+ // Mark the reader as present
+ seen[reader] = struct{}{}
+
+ // If we alreay know about this card, skip to the next reader, otherwise clean up
+ if wallet, ok := hub.wallets[reader]; ok {
+ if err := wallet.ping(); err == nil {
+ continue
+ }
+ wallet.Close()
+ events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
+ delete(hub.wallets, reader)
+ }
+ // New card detected, try to connect to it
+ card, err := hub.context.Connect(reader, pcsc.ShareShared, pcsc.ProtocolAny)
+ if err != nil {
+ log.Debug("Failed to open smart card", "reader", reader, "err", err)
+ continue
+ }
+ wallet := NewWallet(hub, card)
+ if err = wallet.connect(); err != nil {
+ log.Debug("Failed to connect to smart card", "reader", reader, "err", err)
+ card.Disconnect(pcsc.LeaveCard)
+ continue
+ }
+ // Card connected, start tracking in amongs the wallets
+ hub.wallets[reader] = wallet
+ events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
+ }
+ // Remove any wallets no longer present
+ for reader, wallet := range hub.wallets {
+ if _, ok := seen[reader]; !ok {
+ wallet.Close()
+ events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
+ delete(hub.wallets, reader)
+ }
+ }
+ hub.refreshed = time.Now()
+ hub.stateLock.Unlock()
+
+ for _, event := range events {
+ hub.updateFeed.Send(event)
+ }
+}
+
+// Subscribe implements accounts.Backend, creating an async subscription to
+// receive notifications on the addition or removal of smart card wallets.
+func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
+ // We need the mutex to reliably start/stop the update loop
+ hub.stateLock.Lock()
+ defer hub.stateLock.Unlock()
+
+ // Subscribe the caller and track the subscriber count
+ sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
+
+ // Subscribers require an active notification loop, start it
+ if !hub.updating {
+ hub.updating = true
+ go hub.updater()
+ }
+ return sub
+}
+
+// updater is responsible for maintaining an up-to-date list of wallets managed
+// by the smart card hub, and for firing wallet addition/removal events.
+func (hub *Hub) updater() {
+ for {
+ // TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
+ // <-hub.changes
+ time.Sleep(refreshCycle)
+
+ // Run the wallet refresher
+ hub.refreshWallets()
+
+ // If all our subscribers left, stop the updater
+ hub.stateLock.Lock()
+ if hub.updateScope.Count() == 0 {
+ hub.updating = false
+ hub.stateLock.Unlock()
+ return
+ }
+ hub.stateLock.Unlock()
+ }
+}
diff --git a/accounts/scwallet/securechannel.go b/accounts/scwallet/securechannel.go
new file mode 100644
index 0000000..c1b7cff
--- /dev/null
+++ b/accounts/scwallet/securechannel.go
@@ -0,0 +1,346 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package scwallet
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/sha512"
+ "fmt"
+
+ "github.com/ava-labs/go-ethereum/crypto"
+ pcsc "github.com/gballet/go-libpcsclite"
+ "github.com/wsddn/go-ecdh"
+ "golang.org/x/crypto/pbkdf2"
+ "golang.org/x/text/unicode/norm"
+)
+
+const (
+ maxPayloadSize = 223
+ pairP1FirstStep = 0
+ pairP1LastStep = 1
+
+ scSecretLength = 32
+ scBlockSize = 16
+
+ insOpenSecureChannel = 0x10
+ insMutuallyAuthenticate = 0x11
+ insPair = 0x12
+ insUnpair = 0x13
+
+ pairingSalt = "Keycard Pairing Password Salt"
+)
+
+// SecureChannelSession enables secure communication with a hardware wallet.
+type SecureChannelSession struct {
+ card *pcsc.Card // A handle to the smartcard for communication
+ secret []byte // A shared secret generated from our ECDSA keys
+ publicKey []byte // Our own ephemeral public key
+ PairingKey []byte // A permanent shared secret for a pairing, if present
+ sessionEncKey []byte // The current session encryption key
+ sessionMacKey []byte // The current session MAC key
+ iv []byte // The current IV
+ PairingIndex uint8 // The pairing index
+}
+
+// NewSecureChannelSession creates a new secure channel for the given card and public key.
+func NewSecureChannelSession(card *pcsc.Card, keyData []byte) (*SecureChannelSession, error) {
+ // Generate an ECDSA keypair for ourselves
+ gen := ecdh.NewEllipticECDH(crypto.S256())
+ private, public, err := gen.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+
+ cardPublic, ok := gen.Unmarshal(keyData)
+ if !ok {
+ return nil, fmt.Errorf("Could not unmarshal public key from card")
+ }
+
+ secret, err := gen.GenerateSharedSecret(private, cardPublic)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SecureChannelSession{
+ card: card,
+ secret: secret,
+ publicKey: gen.Marshal(public),
+ }, nil
+}
+
+// Pair establishes a new pairing with the smartcard.
+func (s *SecureChannelSession) Pair(pairingPassword []byte) error {
+ secretHash := pbkdf2.Key(norm.NFKD.Bytes(pairingPassword), norm.NFKD.Bytes([]byte(pairingSalt)), 50000, 32, sha256.New)
+
+ challenge := make([]byte, 32)
+ if _, err := rand.Read(challenge); err != nil {
+ return err
+ }
+
+ response, err := s.pair(pairP1FirstStep, challenge)
+ if err != nil {
+ return err
+ }
+
+ md := sha256.New()
+ md.Write(secretHash[:])
+ md.Write(challenge)
+
+ expectedCryptogram := md.Sum(nil)
+ cardCryptogram := response.Data[:32]
+ cardChallenge := response.Data[32:64]
+
+ if !bytes.Equal(expectedCryptogram, cardCryptogram) {
+ return fmt.Errorf("Invalid card cryptogram %v != %v", expectedCryptogram, cardCryptogram)
+ }
+
+ md.Reset()
+ md.Write(secretHash[:])
+ md.Write(cardChallenge)
+ response, err = s.pair(pairP1LastStep, md.Sum(nil))
+ if err != nil {
+ return err
+ }
+
+ md.Reset()
+ md.Write(secretHash[:])
+ md.Write(response.Data[1:])
+ s.PairingKey = md.Sum(nil)
+ s.PairingIndex = response.Data[0]
+
+ return nil
+}
+
+// Unpair disestablishes an existing pairing.
+func (s *SecureChannelSession) Unpair() error {
+ if s.PairingKey == nil {
+ return fmt.Errorf("Cannot unpair: not paired")
+ }
+
+ _, err := s.transmitEncrypted(claSCWallet, insUnpair, s.PairingIndex, 0, []byte{})
+ if err != nil {
+ return err
+ }
+ s.PairingKey = nil
+ // Close channel
+ s.iv = nil
+ return nil
+}
+
+// Open initializes the secure channel.
+func (s *SecureChannelSession) Open() error {
+ if s.iv != nil {
+ return fmt.Errorf("Session already opened")
+ }
+
+ response, err := s.open()
+ if err != nil {
+ return err
+ }
+
+ // Generate the encryption/mac key by hashing our shared secret,
+ // pairing key, and the first bytes returned from the Open APDU.
+ md := sha512.New()
+ md.Write(s.secret)
+ md.Write(s.PairingKey)
+ md.Write(response.Data[:scSecretLength])
+ keyData := md.Sum(nil)
+ s.sessionEncKey = keyData[:scSecretLength]
+ s.sessionMacKey = keyData[scSecretLength : scSecretLength*2]
+
+ // The IV is the last bytes returned from the Open APDU.
+ s.iv = response.Data[scSecretLength:]
+
+ return s.mutuallyAuthenticate()
+}
+
+// mutuallyAuthenticate is an internal method to authenticate both ends of the
+// connection.
+func (s *SecureChannelSession) mutuallyAuthenticate() error {
+ data := make([]byte, scSecretLength)
+ if _, err := rand.Read(data); err != nil {
+ return err
+ }
+
+ response, err := s.transmitEncrypted(claSCWallet, insMutuallyAuthenticate, 0, 0, data)
+ if err != nil {
+ return err
+ }
+ if response.Sw1 != 0x90 || response.Sw2 != 0x00 {
+ return fmt.Errorf("Got unexpected response from MUTUALLY_AUTHENTICATE: 0x%x%x", response.Sw1, response.Sw2)
+ }
+
+ if len(response.Data) != scSecretLength {
+ return fmt.Errorf("Response from MUTUALLY_AUTHENTICATE was %d bytes, expected %d", len(response.Data), scSecretLength)
+ }
+
+ return nil
+}
+
+// open is an internal method that sends an open APDU.
+func (s *SecureChannelSession) open() (*responseAPDU, error) {
+ return transmit(s.card, &commandAPDU{
+ Cla: claSCWallet,
+ Ins: insOpenSecureChannel,
+ P1: s.PairingIndex,
+ P2: 0,
+ Data: s.publicKey,
+ Le: 0,
+ })
+}
+
+// pair is an internal method that sends a pair APDU.
+func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*responseAPDU, error) {
+ return transmit(s.card, &commandAPDU{
+ Cla: claSCWallet,
+ Ins: insPair,
+ P1: p1,
+ P2: 0,
+ Data: data,
+ Le: 0,
+ })
+}
+
+// transmitEncrypted sends an encrypted message, and decrypts and returns the response.
+func (s *SecureChannelSession) transmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*responseAPDU, error) {
+ if s.iv == nil {
+ return nil, fmt.Errorf("Channel not open")
+ }
+
+ data, err := s.encryptAPDU(data)
+ if err != nil {
+ return nil, err
+ }
+ meta := [16]byte{cla, ins, p1, p2, byte(len(data) + scBlockSize)}
+ if err = s.updateIV(meta[:], data); err != nil {
+ return nil, err
+ }
+
+ fulldata := make([]byte, len(s.iv)+len(data))
+ copy(fulldata, s.iv)
+ copy(fulldata[len(s.iv):], data)
+
+ response, err := transmit(s.card, &commandAPDU{
+ Cla: cla,
+ Ins: ins,
+ P1: p1,
+ P2: p2,
+ Data: fulldata,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ rmeta := [16]byte{byte(len(response.Data))}
+ rmac := response.Data[:len(s.iv)]
+ rdata := response.Data[len(s.iv):]
+ plainData, err := s.decryptAPDU(rdata)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = s.updateIV(rmeta[:], rdata); err != nil {
+ return nil, err
+ }
+ if !bytes.Equal(s.iv, rmac) {
+ return nil, fmt.Errorf("Invalid MAC in response")
+ }
+
+ rapdu := &responseAPDU{}
+ rapdu.deserialize(plainData)
+
+ if rapdu.Sw1 != sw1Ok {
+ return nil, fmt.Errorf("Unexpected response status Cla=0x%x, Ins=0x%x, Sw=0x%x%x", cla, ins, rapdu.Sw1, rapdu.Sw2)
+ }
+
+ return rapdu, nil
+}
+
+// encryptAPDU is an internal method that serializes and encrypts an APDU.
+func (s *SecureChannelSession) encryptAPDU(data []byte) ([]byte, error) {
+ if len(data) > maxPayloadSize {
+ return nil, fmt.Errorf("Payload of %d bytes exceeds maximum of %d", len(data), maxPayloadSize)
+ }
+ data = pad(data, 0x80)
+
+ ret := make([]byte, len(data))
+
+ a, err := aes.NewCipher(s.sessionEncKey)
+ if err != nil {
+ return nil, err
+ }
+ crypter := cipher.NewCBCEncrypter(a, s.iv)
+ crypter.CryptBlocks(ret, data)
+ return ret, nil
+}
+
+// pad applies message padding to a 16 byte boundary.
+func pad(data []byte, terminator byte) []byte {
+ padded := make([]byte, (len(data)/16+1)*16)
+ copy(padded, data)
+ padded[len(data)] = terminator
+ return padded
+}
+
+// decryptAPDU is an internal method that decrypts and deserializes an APDU.
+func (s *SecureChannelSession) decryptAPDU(data []byte) ([]byte, error) {
+ a, err := aes.NewCipher(s.sessionEncKey)
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make([]byte, len(data))
+
+ crypter := cipher.NewCBCDecrypter(a, s.iv)
+ crypter.CryptBlocks(ret, data)
+ return unpad(ret, 0x80)
+}
+
+// unpad strips padding from a message.
+func unpad(data []byte, terminator byte) ([]byte, error) {
+ for i := 1; i <= 16; i++ {
+ switch data[len(data)-i] {
+ case 0:
+ continue
+ case terminator:
+ return data[:len(data)-i], nil
+ default:
+ return nil, fmt.Errorf("Expected end of padding, got %d", data[len(data)-i])
+ }
+ }
+ return nil, fmt.Errorf("Expected end of padding, got 0")
+}
+
+// updateIV is an internal method that updates the initialization vector after
+// each message exchanged.
+func (s *SecureChannelSession) updateIV(meta, data []byte) error {
+ data = pad(data, 0)
+ a, err := aes.NewCipher(s.sessionMacKey)
+ if err != nil {
+ return err
+ }
+ crypter := cipher.NewCBCEncrypter(a, make([]byte, 16))
+ crypter.CryptBlocks(meta, meta)
+ crypter.CryptBlocks(data, data)
+ // The first 16 bytes of the last block is the MAC
+ s.iv = data[len(data)-32 : len(data)-16]
+ return nil
+}
diff --git a/accounts/scwallet/wallet.go b/accounts/scwallet/wallet.go
new file mode 100644
index 0000000..f72e685
--- /dev/null
+++ b/accounts/scwallet/wallet.go
@@ -0,0 +1,1082 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package scwallet
+
+import (
+ "bytes"
+ "context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/asn1"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "math/big"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ava-labs/coreth/accounts"
+ "github.com/ava-labs/coreth/core/types"
+ ethereum "github.com/ava-labs/go-ethereum"
+ "github.com/ava-labs/go-ethereum/common"
+ "github.com/ava-labs/go-ethereum/crypto"
+ "github.com/ava-labs/go-ethereum/log"
+ pcsc "github.com/gballet/go-libpcsclite"
+ "github.com/status-im/keycard-go/derivationpath"
+)
+
+// ErrPairingPasswordNeeded is returned if opening the smart card requires pairing with a pairing
+// password. In this case, the calling application should request user input to enter
+// the pairing password and send it back.
+var ErrPairingPasswordNeeded = errors.New("smartcard: pairing password needed")
+
+// ErrPINNeeded is returned if opening the smart card requires a PIN code. In
+// this case, the calling application should request user input to enter the PIN
+// and send it back.
+var ErrPINNeeded = errors.New("smartcard: pin needed")
+
+// ErrPINUnblockNeeded is returned if opening the smart card requires a PIN code,
+// but all PIN attempts have already been exhausted. In this case the calling
+// application should request user input for the PUK and a new PIN code to set
+// fo the card.
+var ErrPINUnblockNeeded = errors.New("smartcard: pin unblock needed")
+
+// ErrAlreadyOpen is returned if the smart card is attempted to be opened, but
+// there is already a paired and unlocked session.
+var ErrAlreadyOpen = errors.New("smartcard: already open")
+
+// ErrPubkeyMismatch is returned if the public key recovered from a signature
+// does not match the one expected by the user.
+var ErrPubkeyMismatch = errors.New("smartcard: recovered public key mismatch")
+
+var (
+ appletAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01, 0x01, 0x01}
+ // DerivationSignatureHash is used to derive the public key from the signature of this hash
+ DerivationSignatureHash = sha256.Sum256(common.Hash{}.Bytes())
+)
+
+// List of APDU command-related constants
+const (
+ claISO7816 = 0
+ claSCWallet = 0x80
+
+ insSelect = 0xA4
+ insGetResponse = 0xC0
+ sw1GetResponse = 0x61
+ sw1Ok = 0x90
+
+ insVerifyPin = 0x20
+ insUnblockPin = 0x22
+ insExportKey = 0xC2
+ insSign = 0xC0
+ insLoadKey = 0xD0
+ insDeriveKey = 0xD1
+ insStatus = 0xF2
+)
+
+// List of ADPU command parameters
+const (
+ P1DeriveKeyFromMaster = uint8(0x00)
+ P1DeriveKeyFromParent = uint8(0x01)
+ P1DeriveKeyFromCurrent = uint8(0x10)
+ statusP1WalletStatus = uint8(0x00)
+ statusP1Path = uint8(0x01)
+ signP1PrecomputedHash = uint8(0x01)
+ signP2OnlyBlock = uint8(0x81)
+ exportP1Any = uint8(0x00)
+ exportP2Pubkey = uint8(0x01)
+)
+
+// Minimum time to wait between self derivation attempts, even it the user is
+// requesting accounts like crazy.
+const selfDeriveThrottling = time.Second
+
+// Wallet represents a smartcard wallet instance.
+type Wallet struct {
+ Hub *Hub // A handle to the Hub that instantiated this wallet.
+ PublicKey []byte // The wallet's public key (used for communication and identification, not signing!)
+
+ lock sync.Mutex // Lock that gates access to struct fields and communication with the card
+ card *pcsc.Card // A handle to the smartcard interface for the wallet.
+ session *Session // The secure communication session with the card
+ log log.Logger // Contextual logger to tag the base with its id
+
+ deriveNextPaths []accounts.DerivationPath // Next derivation paths for account auto-discovery (multiple bases supported)
+ deriveNextAddrs []common.Address // Next derived account addresses for auto-discovery (multiple bases supported)
+ deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
+ deriveReq chan chan struct{} // Channel to request a self-derivation on
+ deriveQuit chan chan error // Channel to terminate the self-deriver with
+}
+
+// NewWallet constructs and returns a new Wallet instance.
+func NewWallet(hub *Hub, card *pcsc.Card) *Wallet {
+ wallet := &Wallet{
+ Hub: hub,
+ card: card,
+ }
+ return wallet
+}
+
+// transmit sends an APDU to the smartcard and receives and decodes the response.
+// It automatically handles requests by the card to fetch the return data separately,
+// and returns an error if the response status code is not success.
+func transmit(card *pcsc.Card, command *commandAPDU) (*responseAPDU, error) {
+ data, err := command.serialize()
+ if err != nil {
+ return nil, err
+ }
+
+ responseData, _, err := card.Transmit(data)
+ if err != nil {
+ return nil, err
+ }
+
+ response := new(responseAPDU)
+ if err = response.deserialize(responseData); err != nil {
+ return nil, err
+ }
+
+ // Are we being asked to fetch the response separately?
+ if response.Sw1 == sw1GetResponse && (command.Cla != claISO7816 || command.Ins != insGetResponse) {
+ return transmit(card, &commandAPDU{
+ Cla: claISO7816,
+ Ins: insGetResponse,
+ P1: 0,
+ P2: 0,
+ Data: nil,
+ Le: response.Sw2,
+ })
+ }
+
+ if response.Sw1 != sw1Ok {
+ return nil, fmt.Errorf("Unexpected insecure response status Cla=0x%x, Ins=0x%x, Sw=0x%x%x", command.Cla, command.Ins, response.Sw1, response.Sw2)
+ }
+
+ return response, nil
+}
+
+// applicationInfo encodes information about the smartcard application - its
+// instance UID and public key.
+type applicationInfo struct {
+ InstanceUID []byte `asn1:"tag:15"`
+ PublicKey []byte `asn1:"tag:0"`
+}
+
+// connect connects to the wallet application and establishes a secure channel with it.
+// must be called before any other interaction with the wallet.
+func (w *Wallet) connect() error {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ appinfo, err := w.doselect()
+ if err != nil {
+ return err
+ }
+
+ channel, err := NewSecureChannelSession(w.card, appinfo.PublicKey)
+ if err != nil {
+ return err
+ }
+
+ w.PublicKey = appinfo.PublicKey
+ w.log = log.New("url", w.URL())
+ w.session = &Session{
+ Wallet: w,
+ Channel: channel,
+ }
+ return nil
+}
+
+// doselect is an internal (unlocked) function to send a SELECT APDU to the card.
+func (w *Wallet) doselect() (*applicationInfo, error) {
+ response, err := transmit(w.card, &commandAPDU{
+ Cla: claISO7816,
+ Ins: insSelect,
+ P1: 4,
+ P2: 0,
+ Data: appletAID,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ appinfo := new(applicationInfo)
+ if _, err := asn1.UnmarshalWithParams(response.Data, appinfo, "tag:4"); err != nil {
+ return nil, err
+ }
+ return appinfo, nil
+}
+
+// ping checks the card's status and returns an error if unsuccessful.
+func (w *Wallet) ping() error {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ // We can't ping if not paired
+ if !w.session.paired() {
+ return nil
+ }
+ if _, err := w.session.walletStatus(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// release releases any resources held by an open wallet instance.
+func (w *Wallet) release() error {
+ if w.session != nil {
+ return w.session.release()
+ }
+ return nil
+}
+
+// pair is an internal (unlocked) function for establishing a new pairing
+// with the wallet.
+func (w *Wallet) pair(puk []byte) error {
+ if w.session.paired() {
+ return fmt.Errorf("Wallet already paired")
+ }
+ pairing, err := w.session.pair(puk)
+ if err != nil {
+ return err
+ }
+ if err = w.Hub.setPairing(w, &pairing); err != nil {
+ return err
+ }
+ return w.session.authenticate(pairing)
+}
+
+// Unpair deletes an existing wallet pairing.
+func (w *Wallet) Unpair(pin []byte) error {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ if !w.session.paired() {
+ return fmt.Errorf("wallet %x not paired", w.PublicKey)
+ }
+ if err := w.session.verifyPin(pin); err != nil {
+ return fmt.Errorf("failed to verify pin: %s", err)
+ }
+ if err := w.session.unpair(); err != nil {
+ return fmt.Errorf("failed to unpair: %s", err)
+ }
+ if err := w.Hub.setPairing(w, nil); err != nil {
+ return err
+ }
+ return nil
+}
+
+// URL retrieves the canonical path under which this wallet is reachable. It is
+// user by upper layers to define a sorting order over all wallets from multiple
+// backends.
+func (w *Wallet) URL() accounts.URL {
+ return accounts.URL{
+ Scheme: w.Hub.scheme,
+ Path: fmt.Sprintf("%x", w.PublicKey[1:5]), // Byte #0 isn't unique; 1:5 covers << 64K cards, bump to 1:9 for << 4M
+ }
+}
+
+// Status returns a textual status to aid the user in the current state of the
+// wallet. It also returns an error indicating any failure the wallet might have
+// encountered.
+func (w *Wallet) Status() (string, error) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ // If the card is not paired, we can only wait
+ if !w.session.paired() {
+ return "Unpaired, waiting for pairing password", nil
+ }
+ // Yay, we have an encrypted session, retrieve the actual status
+ status, err := w.session.walletStatus()
+ if err != nil {
+ return fmt.Sprintf("Failed: %v", err), err
+ }
+ switch {
+ case !w.session.verified && status.PinRetryCount == 0 && status.PukRetryCount == 0:
+ return fmt.Sprintf("Bricked, waiting for full wipe"), nil
+ case !w.session.verified && status.PinRetryCount == 0:
+ return fmt.Sprintf("Blocked, waiting for PUK (%d attempts left) and new PIN", status.PukRetryCount), nil
+ case !w.session.verified:
+ return fmt.Sprintf("Locked, waiting for PIN (%d attempts left)", status.PinRetryCount), nil
+ case !status.Initialized:
+ return fmt.Sprintf("Empty, waiting for initialization"), nil
+ default:
+ return fmt.Sprintf("Online"), nil
+ }
+}
+
+// Open initializes access to a wallet instance. It is not meant to unlock or
+// decrypt account keys, rather simply to establish a connection to hardware
+// wallets and/or to access derivation seeds.
+//
+// The passphrase parameter may or may not be used by the implementation of a
+// particular wallet instance. The reason there is no passwordless open method
+// is to strive towards a uniform wallet handling, oblivious to the different
+// backend providers.
+//
+// Please note, if you open a wallet, you must close it to release any allocated
+// resources (especially important when working with hardware wallets).
+func (w *Wallet) Open(passphrase string) error {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ // If the session is already open, bail out
+ if w.session.verified {
+ return ErrAlreadyOpen
+ }
+ // If the smart card is not yet paired, attempt to do so either from a previous
+ // pairing key or form the supplied PUK code.
+ if !w.session.paired() {
+ // If a previous pairing exists, only ever try to use that
+ if pairing := w.Hub.pairing(w); pairing != nil {
+ if err := w.session.authenticate(*pairing); err != nil {
+ return fmt.Errorf("failed to authenticate card %x: %s", w.PublicKey[:4], err)
+ }
+ // Pairing still ok, fall through to PIN checks
+ } else {
+ // If no passphrase was supplied, request the PUK from the user
+ if passphrase == "" {
+ return ErrPairingPasswordNeeded
+ }
+ // Attempt to pair the smart card with the user supplied PUK
+ if err := w.pair([]byte(passphrase)); err != nil {
+ return err
+ }
+ // Pairing succeeded, fall through to PIN checks. This will of course fail,
+ // but we can't return ErrPINNeeded directly here becase we don't know whether
+ // a PIN check or a PIN reset is needed.
+ passphrase = ""
+ }
+ }
+ // The smart card was successfully paired, retrieve its status to check whether
+ // PIN verification or unblocking is needed.
+ status, err := w.session.walletStatus()
+ if err != nil {
+ return err
+ }
+ // Request the appropriate next authentication data, or use the one supplied
+ switch {
+ case passphrase == "" && status.PinRetryCount > 0:
+ return ErrPINNeeded
+ case passphrase == "":
+ return ErrPINUnblockNeeded
+ case status.PinRetryCount > 0:
+ if !regexp.MustCompile(`^[0-9]{6,}$`).MatchString(passphrase) {
+ w.log.Error("PIN needs to be at least 6 digits")
+ return ErrPINNeeded
+ }
+ if err := w.session.verifyPin([]byte(passphrase)); err != nil {
+ return err
+ }
+ default:
+ if !regexp.MustCompile(`^[0-9]{12,}$`).MatchString(passphrase) {
+ w.log.Error("PUK needs to be at least 12 digits")
+ return ErrPINUnblockNeeded
+ }
+ if err := w.session.unblockPin([]byte(passphrase)); err != nil {
+ return err
+ }
+ }
+ // Smart card paired and unlocked, initialize and register
+ w.deriveReq = make(chan chan struct{})
+ w.deriveQuit = make(chan chan error)
+
+ go w.selfDerive()
+
+ // Notify anyone listening for wallet events that a new device is accessible
+ go w.Hub.updateFeed.Send(accounts.WalletEvent{Wallet: w, Kind: accounts.WalletOpened})
+
+ return nil
+}
+
+// Close stops and closes the wallet, freeing any resources.
+func (w *Wallet) Close() error {
+ // Ensure the wallet was opened
+ w.lock.Lock()
+ dQuit := w.deriveQuit
+ w.lock.Unlock()
+
+ // Terminate the self-derivations
+ var derr error
+ if dQuit != nil {
+ errc := make(chan error)
+ dQuit <- errc
+ derr = <-errc // Save for later, we *must* close the USB
+ }
+ // Terminate the device connection
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ w.deriveQuit = nil
+ w.deriveReq = nil
+
+ if err := w.release(); err != nil {
+ return err
+ }
+ return derr
+}
+
+// selfDerive is an account derivation loop that upon request attempts to find
+// new non-zero accounts.
+func (w *Wallet) selfDerive() {
+ w.log.Debug("Smart card wallet self-derivation started")
+ defer w.log.Debug("Smart card wallet self-derivation stopped")
+
+ // Execute self-derivations until termination or error
+ var (
+ reqc chan struct{}
+ errc chan error
+ err error
+ )
+ for errc == nil && err == nil {
+ // Wait until either derivation or termination is requested
+ select {
+ case errc = <-w.deriveQuit:
+ // Termination requested
+ continue
+ case reqc = <-w.deriveReq:
+ // Account discovery requested
+ }
+ // Derivation needs a chain and device access, skip if either unavailable
+ w.lock.Lock()
+ if w.session == nil || w.deriveChain == nil {
+ w.lock.Unlock()
+ reqc <- struct{}{}
+ continue
+ }
+ pairing := w.Hub.pairing(w)
+
+ // Device lock obtained, derive the next batch of accounts
+ var (
+ paths []accounts.DerivationPath
+ nextAcc accounts.Account
+
+ nextPaths = append([]accounts.DerivationPath{}, w.deriveNextPaths...)
+ nextAddrs = append([]common.Address{}, w.deriveNextAddrs...)
+
+ context = context.Background()
+ )
+ for i := 0; i < len(nextAddrs); i++ {
+ for empty := false; !empty; {
+ // Retrieve the next derived Ethereum account
+ if nextAddrs[i] == (common.Address{}) {
+ if nextAcc, err = w.session.derive(nextPaths[i]); err != nil {
+ w.log.Warn("Smartcard wallet account derivation failed", "err", err)
+ break
+ }
+ nextAddrs[i] = nextAcc.Address
+ }
+ // Check the account's status against the current chain state
+ var (
+ balance *big.Int
+ nonce uint64
+ )
+ balance, err = w.deriveChain.BalanceAt(context, nextAddrs[i], nil)
+ if err != nil {
+ w.log.Warn("Smartcard wallet balance retrieval failed", "err", err)
+ break
+ }
+ nonce, err = w.deriveChain.NonceAt(context, nextAddrs[i], nil)
+ if err != nil {
+ w.log.Warn("Smartcard wallet nonce retrieval failed", "err", err)
+ break
+ }
+ // If the next account is empty, stop self-derivation, but add for the last base path
+ if balance.Sign() == 0 && nonce == 0 {
+ empty = true
+ if i < len(nextAddrs)-1 {
+ break
+ }
+ }
+ // We've just self-derived a new account, start tracking it locally
+ path := make(accounts.DerivationPath, len(nextPaths[i]))
+ copy(path[:], nextPaths[i][:])
+ paths = append(paths, path)
+
+ // Display a log message to the user for new (or previously empty accounts)
+ if _, known := pairing.Accounts[nextAddrs[i]]; !known || !empty || nextAddrs[i] != w.deriveNextAddrs[i] {
+ w.log.Info("Smartcard wallet discovered new account", "address", nextAddrs[i], "path", path, "balance", balance, "nonce", nonce)
+ }
+ pairing.Accounts[nextAddrs[i]] = path
+
+ // Fetch the next potential account
+ if !empty {
+ nextAddrs[i] = common.Address{}
+ nextPaths[i][len(nextPaths[i])-1]++
+ }
+ }
+ }
+ // If there are new accounts, write them out
+ if len(paths) > 0 {
+ err = w.Hub.setPairing(w, pairing)
+ }
+ // Shift the self-derivation forward
+ w.deriveNextAddrs = nextAddrs
+ w.deriveNextPaths = nextPaths
+
+ // Self derivation complete, release device lock
+ w.lock.Unlock()
+
+ // Notify the user of termination and loop after a bit of time (to avoid trashing)
+ reqc <- struct{}{}
+ if err == nil {
+ select {
+ case errc = <-w.deriveQuit:
+ // Termination requested, abort
+ case <-time.After(selfDeriveThrottling):
+ // Waited enough, willing to self-derive again
+ }
+ }
+ }
+ // In case of error, wait for termination
+ if err != nil {
+ w.log.Debug("Smartcard wallet self-derivation failed", "err", err)
+ errc = <-w.deriveQuit
+ }
+ errc <- err
+}
+
+// Accounts retrieves the list of signing accounts the wallet is currently aware
+// of. For hierarchical deterministic wallets, the list will not be exhaustive,
+// rather only contain the accounts explicitly pinned during account derivation.
+func (w *Wallet) Accounts() []accounts.Account {
+ // Attempt self-derivation if it's running
+ reqc := make(chan struct{}, 1)
+ select {
+ case w.deriveReq <- reqc:
+ // Self-derivation request accepted, wait for it
+ <-reqc
+ default:
+ // Self-derivation offline, throttled or busy, skip
+ }
+
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ if pairing := w.Hub.pairing(w); pairing != nil {
+ ret := make([]accounts.Account, 0, len(pairing.Accounts))
+ for address, path := range pairing.Accounts {
+ ret = append(ret, w.makeAccount(address, path))
+ }
+ sort.Sort(accounts.AccountsByURL(ret))
+ return ret
+ }
+ return nil
+}
+
+func (w *Wallet) makeAccount(address common.Address, path accounts.DerivationPath) accounts.Account {
+ return accounts.Account{
+ Address: address,
+ URL: accounts.URL{
+ Scheme: w.Hub.scheme,
+ Path: fmt.Sprintf("%x/%s", w.PublicKey[1:3], path.String()),
+ },
+ }
+}
+
+// Contains returns whether an account is part of this particular wallet or not.
+func (w *Wallet) Contains(account accounts.Account) bool {
+ if pairing := w.Hub.pairing(w); pairing != nil {
+ _, ok := pairing.Accounts[account.Address]
+ return ok
+ }
+ return false
+}
+
+// Initialize installs a keypair generated from the provided key into the wallet.
+func (w *Wallet) Initialize(seed []byte) error {
+ go w.selfDerive()
+ // DO NOT lock at this stage, as the initialize
+ // function relies on Status()
+ return w.session.initialize(seed)
+}
+
+// Derive attempts to explicitly derive a hierarchical deterministic account at
+// the specified derivation path. If requested, the derived account will be added
+// to the wallet's tracked account list.
+func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ account, err := w.session.derive(path)
+ if err != nil {
+ return accounts.Account{}, err
+ }
+
+ if pin {
+ pairing := w.Hub.pairing(w)
+ pairing.Accounts[account.Address] = path
+ if err := w.Hub.setPairing(w, pairing); err != nil {
+ return accounts.Account{}, err
+ }
+ }
+
+ return account, nil
+}
+
+// SelfDerive sets a base account derivation path from which the wallet attempts
+// to discover non zero accounts and automatically add them to list of tracked
+// accounts.
+//
+// Note, self derivaton will increment the last component of the specified path
+// opposed to decending into a child path to allow discovering accounts starting
+// from non zero components.
+//
+// Some hardware wallets switched derivation paths through their evolution, so
+// this method supports providing multiple bases to discover old user accounts
+// too. Only the last base will be used to derive the next empty account.
+//
+// You can disable automatic account discovery by calling SelfDerive with a nil
+// chain state reader.
+func (w *Wallet) SelfDerive(bases []accounts.DerivationPath, chain ethereum.ChainStateReader) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ w.deriveNextPaths = make([]accounts.DerivationPath, len(bases))
+ for i, base := range bases {
+ w.deriveNextPaths[i] = make(accounts.DerivationPath, len(base))
+ copy(w.deriveNextPaths[i][:], base[:])
+ }
+ w.deriveNextAddrs = make([]common.Address, len(bases))
+ w.deriveChain = chain
+}
+
+// SignData requests the wallet to sign the hash of the given data.
+//
+// It looks up the account specified either solely via its address contained within,
+// or optionally with the aid of any location metadata from the embedded URL field.
+//
+// If the wallet requires additional authentication to sign the request (e.g.
+// a password to decrypt the account, or a PIN code o verify the transaction),
+// an AuthNeededError instance will be returned, containing infos for the user
+// about which fields or actions are needed. The user may retry by providing
+// the needed details via SignDataWithPassphrase, or by other means (e.g. unlock
+// the account in a keystore).
+func (w *Wallet) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) {
+ return w.signHash(account, crypto.Keccak256(data))
+}
+
+func (w *Wallet) signHash(account accounts.Account, hash []byte) ([]byte, error) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ path, err := w.findAccountPath(account)
+ if err != nil {
+ return nil, err
+ }
+
+ return w.session.sign(path, hash)
+}
+
+// SignTx requests the wallet to sign the given transaction.
+//
+// It looks up the account specified either solely via its address contained within,
+// or optionally with the aid of any location metadata from the embedded URL field.
+//
+// If the wallet requires additional authentication to sign the request (e.g.
+// a password to decrypt the account, or a PIN code o verify the transaction),
+// an AuthNeededError instance will be returned, containing infos for the user
+// about which fields or actions are needed. The user may retry by providing
+// the needed details via SignTxWithPassphrase, or by other means (e.g. unlock
+// the account in a keystore).
+func (w *Wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ signer := types.NewEIP155Signer(chainID)
+ hash := signer.Hash(tx)
+ sig, err := w.signHash(account, hash[:])
+ if err != nil {
+ return nil, err
+ }
+ return tx.WithSignature(signer, sig)
+}
+
+// SignDataWithPassphrase requests the wallet to sign the given hash with the
+// given passphrase as extra authentication information.
+//
+// It looks up the account specified either solely via its address contained within,
+// or optionally with the aid of any location metadata from the embedded URL field.
+func (w *Wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) {
+ return w.signHashWithPassphrase(account, passphrase, crypto.Keccak256(data))
+}
+
+func (w *Wallet) signHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) {
+ if !w.session.verified {
+ if err := w.Open(passphrase); err != nil {
+ return nil, err
+ }
+ }
+
+ return w.signHash(account, hash)
+}
+
+// SignText requests the wallet to sign the hash of a given piece of data, prefixed
+// by the Ethereum prefix scheme
+// It looks up the account specified either solely via its address contained within,
+// or optionally with the aid of any location metadata from the embedded URL field.
+//
+// If the wallet requires additional authentication to sign the request (e.g.
+// a password to decrypt the account, or a PIN code o verify the transaction),
+// an AuthNeededError instance will be returned, containing infos for the user
+// about which fields or actions are needed. The user may retry by providing
+// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
+// the account in a keystore).
+func (w *Wallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
+ return w.signHash(account, accounts.TextHash(text))
+}
+
+// SignTextWithPassphrase implements accounts.Wallet, attempting to sign the
+// given hash with the given account using passphrase as extra authentication
+func (w *Wallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
+ return w.signHashWithPassphrase(account, passphrase, crypto.Keccak256(accounts.TextHash(text)))
+}
+
+// SignTxWithPassphrase requests the wallet to sign the given transaction, with the
+// given passphrase as extra authentication information.
+//
+// It looks up the account specified either solely via its address contained within,
+// or optionally with the aid of any location metadata from the embedded URL field.
+func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ if !w.session.verified {
+ if err := w.Open(passphrase); err != nil {
+ return nil, err
+ }
+ }
+ return w.SignTx(account, tx, chainID)
+}
+
+// findAccountPath returns the derivation path for the provided account.
+// It first checks for the address in the list of pinned accounts, and if it is
+// not found, attempts to parse the derivation path from the account's URL.
+func (w *Wallet) findAccountPath(account accounts.Account) (accounts.DerivationPath, error) {
+ pairing := w.Hub.pairing(w)
+ if path, ok := pairing.Accounts[account.Address]; ok {
+ return path, nil
+ }
+
+ // Look for the path in the URL
+ if account.URL.Scheme != w.Hub.scheme {
+ return nil, fmt.Errorf("Scheme %s does not match wallet scheme %s", account.URL.Scheme, w.Hub.scheme)
+ }
+
+ parts := strings.SplitN(account.URL.Path, "/", 2)
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("Invalid URL format: %s", account.URL)
+ }
+
+ if parts[0] != fmt.Sprintf("%x", w.PublicKey[1:3]) {
+ return nil, fmt.Errorf("URL %s is not for this wallet", account.URL)
+ }
+
+ return accounts.ParseDerivationPath(parts[1])
+}
+
+// Session represents a secured communication session with the wallet.
+type Session struct {
+ Wallet *Wallet // A handle to the wallet that opened the session
+ Channel *SecureChannelSession // A secure channel for encrypted messages
+ verified bool // Whether the pin has been verified in this session.
+}
+
+// pair establishes a new pairing over this channel, using the provided secret.
+func (s *Session) pair(secret []byte) (smartcardPairing, error) {
+ err := s.Channel.Pair(secret)
+ if err != nil {
+ return smartcardPairing{}, err
+ }
+
+ return smartcardPairing{
+ PublicKey: s.Wallet.PublicKey,
+ PairingIndex: s.Channel.PairingIndex,
+ PairingKey: s.Channel.PairingKey,
+ Accounts: make(map[common.Address]accounts.DerivationPath),
+ }, nil
+}
+
+// unpair deletes an existing pairing.
+func (s *Session) unpair() error {
+ if !s.verified {
+ return fmt.Errorf("Unpair requires that the PIN be verified")
+ }
+ return s.Channel.Unpair()
+}
+
+// verifyPin unlocks a wallet with the provided pin.
+func (s *Session) verifyPin(pin []byte) error {
+ if _, err := s.Channel.transmitEncrypted(claSCWallet, insVerifyPin, 0, 0, pin); err != nil {
+ return err
+ }
+ s.verified = true
+ return nil
+}
+
+// unblockPin unblocks a wallet with the provided puk and resets the pin to the
+// new one specified.
+func (s *Session) unblockPin(pukpin []byte) error {
+ if _, err := s.Channel.transmitEncrypted(claSCWallet, insUnblockPin, 0, 0, pukpin); err != nil {
+ return err
+ }
+ s.verified = true
+ return nil
+}
+
+// release releases resources associated with the channel.
+func (s *Session) release() error {
+ return s.Wallet.card.Disconnect(pcsc.LeaveCard)
+}
+
+// paired returns true if a valid pairing exists.
+func (s *Session) paired() bool {
+ return s.Channel.PairingKey != nil
+}
+
+// authenticate uses an existing pairing to establish a secure channel.
+func (s *Session) authenticate(pairing smartcardPairing) error {
+ if !bytes.Equal(s.Wallet.PublicKey, pairing.PublicKey) {
+ return fmt.Errorf("Cannot pair using another wallet's pairing; %x != %x", s.Wallet.PublicKey, pairing.PublicKey)
+ }
+ s.Channel.PairingKey = pairing.PairingKey
+ s.Channel.PairingIndex = pairing.PairingIndex
+ return s.Channel.Open()
+}
+
+// walletStatus describes a smartcard wallet's status information.
+type walletStatus struct {
+ PinRetryCount int // Number of remaining PIN retries
+ PukRetryCount int // Number of remaining PUK retries
+ Initialized bool // Whether the card has been initialized with a private key
+}
+
+// walletStatus fetches the wallet's status from the card.
+func (s *Session) walletStatus() (*walletStatus, error) {
+ response, err := s.Channel.transmitEncrypted(claSCWallet, insStatus, statusP1WalletStatus, 0, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ status := new(walletStatus)
+ if _, err := asn1.UnmarshalWithParams(response.Data, status, "tag:3"); err != nil {
+ return nil, err
+ }
+ return status, nil
+}
+
+// derivationPath fetches the wallet's current derivation path from the card.
+func (s *Session) derivationPath() (accounts.DerivationPath, error) {
+ response, err := s.Channel.transmitEncrypted(claSCWallet, insStatus, statusP1Path, 0, nil)
+ if err != nil {
+ return nil, err
+ }
+ buf := bytes.NewReader(response.Data)
+ path := make(accounts.DerivationPath, len(response.Data)/4)
+ return path, binary.Read(buf, binary.BigEndian, &path)
+}
+
+// initializeData contains data needed to initialize the smartcard wallet.
+type initializeData struct {
+ PublicKey []byte `asn1:"tag:0"`
+ PrivateKey []byte `asn1:"tag:1"`
+ ChainCode []byte `asn1:"tag:2"`
+}
+
+// initialize initializes the card with new key data.
+func (s *Session) initialize(seed []byte) error {
+ // Check that the wallet isn't currently initialized,
+ // otherwise the key would be overwritten.
+ status, err := s.Wallet.Status()
+ if err != nil {
+ return err
+ }
+ if status == "Online" {
+ return fmt.Errorf("card is already initialized, cowardly refusing to proceed")
+ }
+
+ s.Wallet.lock.Lock()
+ defer s.Wallet.lock.Unlock()
+
+ // HMAC the seed to produce the private key and chain code
+ mac := hmac.New(sha512.New, []byte("Bitcoin seed"))
+ mac.Write(seed)
+ seed = mac.Sum(nil)
+
+ key, err := crypto.ToECDSA(seed[:32])
+ if err != nil {
+ return err
+ }
+
+ id := initializeData{}
+ id.PublicKey = crypto.FromECDSAPub(&key.PublicKey)
+ id.PrivateKey = seed[:32]
+ id.ChainCode = seed[32:]
+ data, err := asn1.Marshal(id)
+ if err != nil {
+ return err
+ }
+
+ // Nasty hack to force the top-level struct tag to be context-specific
+ data[0] = 0xA1
+
+ _, err = s.Channel.transmitEncrypted(claSCWallet, insLoadKey, 0x02, 0, data)
+ return err
+}
+
+// derive derives a new HD key path on the card.
+func (s *Session) derive(path accounts.DerivationPath) (accounts.Account, error) {
+ startingPoint, path, err := derivationpath.Decode(path.String())
+ if err != nil {
+ return accounts.Account{}, err
+ }
+
+ var p1 uint8
+ switch startingPoint {
+ case derivationpath.StartingPointMaster:
+ p1 = P1DeriveKeyFromMaster
+ case derivationpath.StartingPointParent:
+ p1 = P1DeriveKeyFromParent
+ case derivationpath.StartingPointCurrent:
+ p1 = P1DeriveKeyFromCurrent
+ default:
+ return accounts.Account{}, fmt.Errorf("invalid startingPoint %d", startingPoint)
+ }
+
+ data := new(bytes.Buffer)
+ for _, segment := range path {
+ if err := binary.Write(data, binary.BigEndian, segment); err != nil {
+ return accounts.Account{}, err
+ }
+ }
+
+ _, err = s.Channel.transmitEncrypted(claSCWallet, insDeriveKey, p1, 0, data.Bytes())
+ if err != nil {
+ return accounts.Account{}, err
+ }
+
+ response, err := s.Channel.transmitEncrypted(claSCWallet, insSign, 0, 0, DerivationSignatureHash[:])
+ if err != nil {
+ return accounts.Account{}, err
+ }
+
+ sigdata := new(signatureData)
+ if _, err := asn1.UnmarshalWithParams(response.Data, sigdata, "tag:0"); err != nil {
+ return accounts.Account{}, err
+ }
+ rbytes, sbytes := sigdata.Signature.R.Bytes(), sigdata.Signature.S.Bytes()
+ sig := make([]byte, 65)
+ copy(sig[32-len(rbytes):32], rbytes)
+ copy(sig[64-len(sbytes):64], sbytes)
+
+ if err := confirmPublicKey(sig, sigdata.PublicKey); err != nil {
+ return accounts.Account{}, err
+ }
+ pub, err := crypto.UnmarshalPubkey(sigdata.PublicKey)
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ return s.Wallet.makeAccount(crypto.PubkeyToAddress(*pub), path), nil
+}
+
+// keyExport contains information on an exported keypair.
+type keyExport struct {
+ PublicKey []byte `asn1:"tag:0"`
+ PrivateKey []byte `asn1:"tag:1,optional"`
+}
+
+// publicKey returns the public key for the current derivation path.
+func (s *Session) publicKey() ([]byte, error) {
+ response, err := s.Channel.transmitEncrypted(claSCWallet, insExportKey, exportP1Any, exportP2Pubkey, nil)
+ if err != nil {
+ return nil, err
+ }
+ keys := new(keyExport)
+ if _, err := asn1.UnmarshalWithParams(response.Data, keys, "tag:1"); err != nil {
+ return nil, err
+ }
+ return keys.PublicKey, nil
+}
+
+// signatureData contains information on a signature - the signature itself and
+// the corresponding public key.
+type signatureData struct {
+ PublicKey []byte `asn1:"tag:0"`
+ Signature struct {
+ R *big.Int
+ S *big.Int
+ }
+}
+
+// sign asks the card to sign a message, and returns a valid signature after
+// recovering the v value.
+func (s *Session) sign(path accounts.DerivationPath, hash []byte) ([]byte, error) {
+ startTime := time.Now()
+ _, err := s.derive(path)
+ if err != nil {
+ return nil, err
+ }
+ deriveTime := time.Now()
+
+ response, err := s.Channel.transmitEncrypted(claSCWallet, insSign, signP1PrecomputedHash, signP2OnlyBlock, hash)
+ if err != nil {
+ return nil, err
+ }
+ sigdata := new(signatureData)
+ if _, err := asn1.UnmarshalWithParams(response.Data, sigdata, "tag:0"); err != nil {
+ return nil, err
+ }
+ // Serialize the signature
+ rbytes, sbytes := sigdata.Signature.R.Bytes(), sigdata.Signature.S.Bytes()
+ sig := make([]byte, 65)
+ copy(sig[32-len(rbytes):32], rbytes)
+ copy(sig[64-len(sbytes):64], sbytes)
+
+ // Recover the V value.
+ sig, err = makeRecoverableSignature(hash, sig, sigdata.PublicKey)
+ if err != nil {
+ return nil, err
+ }
+ log.Debug("Signed using smartcard", "deriveTime", deriveTime.Sub(startTime), "signingTime", time.Since(deriveTime))
+
+ return sig, nil
+}
+
+// confirmPublicKey confirms that the given signature belongs to the specified key.
+func confirmPublicKey(sig, pubkey []byte) error {
+ _, err := makeRecoverableSignature(DerivationSignatureHash[:], sig, pubkey)
+ return err
+}
+
+// makeRecoverableSignature uses a signature and an expected public key to
+// recover the v value and produce a recoverable signature.
+func makeRecoverableSignature(hash, sig, expectedPubkey []byte) ([]byte, error) {
+ var libraryError error
+ for v := 0; v < 2; v++ {
+ sig[64] = byte(v)
+ if pubkey, err := crypto.Ecrecover(hash, sig); err == nil {
+ if bytes.Equal(pubkey, expectedPubkey) {
+ return sig, nil
+ }
+ } else {
+ libraryError = err
+ }
+ }
+ if libraryError != nil {
+ return nil, libraryError
+ }
+ return nil, ErrPubkeyMismatch
+}