diff --git a/accounts/scwallet/apdu.go b/accounts/scwallet/apdu.go index c7055c0cc..db0cf0dca 100644 --- a/accounts/scwallet/apdu.go +++ b/accounts/scwallet/apdu.go @@ -21,29 +21,15 @@ import ( "encoding/binary" ) -const ( - claISO7816 = 0 - - insSelect = 0xA4 - insGetResponse = 0xC0 - insPair = 0x12 - insUnpair = 0x13 - insOpenSecureChannel = 0x10 - insMutuallyAuthenticate = 0x11 - - sw1GetResponse = 0x61 - sw1Ok = 0x90 -) - -// CommandAPDU represents an application data unit sent to a smartcard. -type CommandAPDU struct { +// 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) { +func (ca commandAPDU) serialize() ([]byte, error) { buf := new(bytes.Buffer) if err := binary.Write(buf, binary.BigEndian, ca.Cla); err != nil { @@ -72,14 +58,14 @@ func (ca CommandAPDU) serialize() ([]byte, error) { return buf.Bytes(), nil } -// ResponseAPDU represents an application data unit received from a smart card. -type ResponseAPDU struct { +// 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 { +func (ra *responseAPDU) deserialize(data []byte) error { ra.Data = make([]byte, len(data)-2) buf := bytes.NewReader(data) diff --git a/accounts/scwallet/hub.go b/accounts/scwallet/hub.go index 996de75eb..d265458b9 100644 --- a/accounts/scwallet/hub.go +++ b/accounts/scwallet/hub.go @@ -36,6 +36,7 @@ import ( "encoding/json" "io/ioutil" "os" + "sort" "sync" "time" @@ -51,7 +52,7 @@ const Scheme = "pcsc" // refreshCycle is the maximum time between wallet refreshes (if USB hotplug // notifications don't work). -const refreshCycle = 5 * time.Second +const refreshCycle = time.Second // refreshThrottling is the minimum time between wallet refreshes to avoid thrashing. const refreshThrottling = 500 * time.Millisecond @@ -132,7 +133,7 @@ func (hub *Hub) writePairings() error { return pairingFile.Close() } -func (hub *Hub) getPairing(wallet *Wallet) *smartcardPairing { +func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing { pairing, ok := hub.pairings[string(wallet.PublicKey)] if ok { return &pairing @@ -182,13 +183,7 @@ func (hub *Hub) Wallets() []accounts.Wallet { for _, wallet := range hub.wallets { cpy = append(cpy, wallet) } - for i := 0; i < len(cpy); i++ { - for j := i + 1; j < len(cpy); j++ { - if cpy[i].URL().Cmp(cpy[j].URL()) > 0 { - cpy[i], cpy[j] = cpy[j], cpy[i] - } - } - } + sort.Sort(accounts.WalletsByURL(cpy)) return cpy } diff --git a/accounts/scwallet/securechannel.go b/accounts/scwallet/securechannel.go index 752daf9b6..acafaf967 100644 --- a/accounts/scwallet/securechannel.go +++ b/accounts/scwallet/securechannel.go @@ -17,7 +17,6 @@ package scwallet import ( - //"crypto/ecdsa" "bytes" "crypto/aes" "crypto/cipher" @@ -25,10 +24,10 @@ import ( "crypto/sha256" "crypto/sha512" "fmt" - //"math/big" + "github.com/ebfe/scard" "github.com/ethereum/go-ethereum/crypto" - ecdh "github.com/wsddn/go-ecdh" + "github.com/wsddn/go-ecdh" ) const ( @@ -38,6 +37,11 @@ const ( scSecretLength = 32 scBlockSize = 16 + + insOpenSecureChannel = 0x10 + insMutuallyAuthenticate = 0x11 + insPair = 0x12 + insUnpair = 0x13 ) // SecureChannelSession enables secure communication with a hardware wallet. @@ -192,8 +196,8 @@ func (s *SecureChannelSession) mutuallyAuthenticate() error { } // open is an internal method that sends an open APDU. -func (s *SecureChannelSession) open() (*ResponseAPDU, error) { - return transmit(s.card, &CommandAPDU{ +func (s *SecureChannelSession) open() (*responseAPDU, error) { + return transmit(s.card, &commandAPDU{ Cla: claSCWallet, Ins: insOpenSecureChannel, P1: s.PairingIndex, @@ -204,8 +208,8 @@ func (s *SecureChannelSession) open() (*ResponseAPDU, error) { } // 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{ +func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*responseAPDU, error) { + return transmit(s.card, &commandAPDU{ Cla: claSCWallet, Ins: insPair, P1: p1, @@ -216,7 +220,7 @@ func (s *SecureChannelSession) pair(p1 uint8, data []byte) (*ResponseAPDU, error } // TransmitEncrypted sends an encrypted message, and decrypts and returns the response. -func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*ResponseAPDU, error) { +func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []byte) (*responseAPDU, error) { if s.iv == nil { return nil, fmt.Errorf("Channel not open") } @@ -234,7 +238,7 @@ func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []b copy(fulldata, s.iv) copy(fulldata[len(s.iv):], data) - response, err := transmit(s.card, &CommandAPDU{ + response, err := transmit(s.card, &commandAPDU{ Cla: cla, Ins: ins, P1: p1, @@ -260,7 +264,7 @@ func (s *SecureChannelSession) TransmitEncrypted(cla, ins, p1, p2 byte, data []b return nil, fmt.Errorf("Invalid MAC in response") } - rapdu := &ResponseAPDU{} + rapdu := &responseAPDU{} rapdu.deserialize(plainData) if rapdu.Sw1 != sw1Ok { diff --git a/accounts/scwallet/wallet.go b/accounts/scwallet/wallet.go index b85f41187..3327e7f63 100644 --- a/accounts/scwallet/wallet.go +++ b/accounts/scwallet/wallet.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "math/big" + "sort" "strings" "sync" "time" @@ -51,6 +52,12 @@ var ErrPUKNeeded = errors.New("smartcard: puk needed") // 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") @@ -65,8 +72,16 @@ var ( ) const ( - claSCWallet = 0x80 + claISO7816 = 0 + claSCWallet = 0x80 + + insSelect = 0xA4 + insGetResponse = 0xC0 + sw1GetResponse = 0x61 + sw1Ok = 0x90 + insVerifyPin = 0x20 + insUnblockPin = 0x22 insExportKey = 0xC2 insSign = 0xC0 insLoadKey = 0xD0 @@ -117,7 +132,7 @@ func NewWallet(hub *Hub, card *scard.Card) *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 *scard.Card, command *CommandAPDU) (*ResponseAPDU, error) { +func transmit(card *scard.Card, command *commandAPDU) (*responseAPDU, error) { data, err := command.serialize() if err != nil { return nil, err @@ -128,14 +143,14 @@ func transmit(card *scard.Card, command *CommandAPDU) (*ResponseAPDU, error) { return nil, err } - response := new(ResponseAPDU) + 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{ + return transmit(card, &commandAPDU{ Cla: claISO7816, Ins: insGetResponse, P1: 0, @@ -186,7 +201,7 @@ func (w *Wallet) connect() error { // 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{ + response, err := transmit(w.card, &commandAPDU{ Cla: claISO7816, Ins: insSelect, P1: 4, @@ -209,13 +224,11 @@ func (w *Wallet) ping() error { w.lock.Lock() defer w.lock.Unlock() + // We can't ping if not paired if !w.session.paired() { - // We can't ping if not paired return nil } - - _, err := w.session.getWalletStatus() - if err != nil { + if _, err := w.session.walletStatus(); err != nil { return err } return nil @@ -235,16 +248,13 @@ 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) } @@ -285,19 +295,26 @@ 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", nil + return "Unpaired, waiting for PUK", nil } - - status, err := w.session.getWalletStatus() + // Yay, we have an encrypted session, retrieve the actual status + status, err := w.session.walletStatus() if err != nil { - return "Error", err + return fmt.Sprintf("Failed: %v", err), err } - - if w.session.verified { - return fmt.Sprintf("Open, %s", status), nil - } else { - return fmt.Sprintf("Locked, %s", status), nil + switch { + case !w.session.verified && status.PinRetryCount == 0: + return fmt.Sprintf("Blocked, waiting for PUK and new PIN"), 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 + case status.SupportsPKDerivation: + return fmt.Sprintf("Online, can derive public keys"), nil + default: + return fmt.Sprintf("Online, cannot derive public keys"), nil } } @@ -324,29 +341,46 @@ func (w *Wallet) Open(passphrase string) error { // 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.getPairing(w); pairing != nil { + 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) } - return ErrPINNeeded + // Pairing still ok, fall through to PIN checks + } else { + // If no passphrase was supplied, request the PUK from the user + if passphrase == "" { + return ErrPUKNeeded + } + // 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 = "" } - // If no passphrase was supplied, request the PUK from the user - if passphrase == "" { - return ErrPUKNeeded - } - // Attempt to pair the smart card with the user supplied PUK - if err := w.pair([]byte(passphrase)); err != nil { + } + // 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 err := w.session.verifyPin([]byte(passphrase)); err != nil { + return err + } + default: + if err := w.session.unblockPin([]byte(passphrase)); err != nil { return err } - return ErrPINNeeded // We always need the PIN after the PUK - } - // The smart card was successfully paired, request a PIN code or use the one - // supplied by the user - if passphrase == "" { - return ErrPINNeeded - } - if err := w.session.verifyPin([]byte(passphrase)); err != nil { - return err } // Smart card paired and unlocked, initialize and register w.deriveReq = make(chan chan struct{}) @@ -415,7 +449,7 @@ func (w *Wallet) selfDerive() { reqc <- struct{}{} continue } - pairing := w.Hub.getPairing(w) + pairing := w.Hub.pairing(w) // Device lock obtained, derive the next batch of accounts var ( @@ -519,18 +553,12 @@ func (w *Wallet) Accounts() []accounts.Account { w.lock.Lock() defer w.lock.Unlock() - if pairing := w.Hub.getPairing(w); pairing != nil { + 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)) } - for i := 0; i < len(ret); i++ { - for j := i + 1; j < len(ret); j++ { - if ret[i].URL.Cmp(ret[j].URL) > 0 { - ret[i], ret[j] = ret[j], ret[i] - } - } - } + sort.Sort(accounts.AccountsByURL(ret)) return ret } return nil @@ -548,7 +576,7 @@ func (w *Wallet) makeAccount(address common.Address, path accounts.DerivationPat // 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.getPairing(w); pairing != nil { + if pairing := w.Hub.pairing(w); pairing != nil { _, ok := pairing.Accounts[account.Address] return ok } @@ -576,7 +604,7 @@ func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Accoun } if pin { - pairing := w.Hub.getPairing(w) + pairing := w.Hub.pairing(w) pairing.Accounts[account.Address] = path if err := w.Hub.setPairing(w, pairing); err != nil { return accounts.Account{}, err @@ -684,7 +712,7 @@ func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase strin // 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.getPairing(w) + pairing := w.Hub.pairing(w) if path, ok := pairing.Accounts[account.Address]; ok { return path, nil } @@ -745,6 +773,16 @@ func (s *Session) verifyPin(pin []byte) error { 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(scard.LeaveCard) @@ -773,32 +811,25 @@ type walletStatus struct { SupportsPKDerivation bool // Whether the card supports doing public key derivation itself } -func (w walletStatus) String() string { - return fmt.Sprintf("pinRetryCount=%d, pukRetryCount=%d, initialized=%t, supportsPkDerivation=%t", w.PinRetryCount, w.PukRetryCount, w.Initialized, w.SupportsPKDerivation) -} - -// getWalletStatus fetches the wallet's status from the card. -func (s *Session) getWalletStatus() (*walletStatus, error) { +// 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 } -// getDerivationPath fetches the wallet's current derivation path from the card. -func (s *Session) getDerivationPath() (accounts.DerivationPath, error) { +// 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) @@ -845,11 +876,11 @@ func (s *Session) derive(path accounts.DerivationPath) (accounts.Account, error) // start again. remainingPath := path - pubkey, err := s.getPublicKey() + pubkey, err := s.publicKey() if err != nil { return accounts.Account{}, err } - currentPath, err := s.getDerivationPath() + currentPath, err := s.derivationPath() if err != nil { return accounts.Account{}, err } @@ -910,7 +941,6 @@ func (s *Session) deriveKeyAssisted(reset bool, pathComponent uint32) ([]byte, e if _, err := asn1.UnmarshalWithParams(response.Data, keyinfo, "tag:2"); err != nil { return nil, err } - rbytes, sbytes := keyinfo.Signature.R.Bytes(), keyinfo.Signature.S.Bytes() sig := make([]byte, 65) copy(sig[32-len(rbytes):32], rbytes) @@ -920,12 +950,10 @@ func (s *Session) deriveKeyAssisted(reset bool, pathComponent uint32) ([]byte, e if err != nil { return nil, err } - _, err = s.Channel.TransmitEncrypted(claSCWallet, insDeriveKey, deriveP1Assisted|deriveP1Append, deriveP2PublicKey, pubkey) if err != nil { return nil, err } - return pubkey, nil } @@ -935,18 +963,16 @@ type keyExport struct { PrivateKey []byte `asn1:"tag:1,optional"` } -// getPublicKey returns the public key for the current derivation path. -func (s *Session) getPublicKey() ([]byte, error) { +// 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 } @@ -974,12 +1000,10 @@ func (s *Session) sign(path accounts.DerivationPath, hash []byte) ([]byte, error 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) @@ -991,7 +1015,6 @@ func (s *Session) sign(path accounts.DerivationPath, hash []byte) ([]byte, error if err != nil { return nil, err } - log.Debug("Signed using smartcard", "deriveTime", deriveTime.Sub(startTime), "signingTime", time.Since(deriveTime)) return sig, nil diff --git a/accounts/sort.go b/accounts/sort.go new file mode 100644 index 000000000..f46762114 --- /dev/null +++ b/accounts/sort.go @@ -0,0 +1,31 @@ +// 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 . + +package accounts + +// AccountsByURL implements sort.Interface for []Account based on the URL field. +type AccountsByURL []Account + +func (a AccountsByURL) Len() int { return len(a) } +func (a AccountsByURL) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a AccountsByURL) Less(i, j int) bool { return a[i].URL.Cmp(a[j].URL) < 0 } + +// WalletsByURL implements sort.Interface for []Wallet based on the URL field. +type WalletsByURL []Wallet + +func (w WalletsByURL) Len() int { return len(w) } +func (w WalletsByURL) Swap(i, j int) { w[i], w[j] = w[j], w[i] } +func (w WalletsByURL) Less(i, j int) bool { return w[i].URL().Cmp(w[j].URL()) < 0 } diff --git a/console/bridge.go b/console/bridge.go index 03d97e0ef..e90991dc6 100644 --- a/console/bridge.go +++ b/console/bridge.go @@ -141,6 +141,24 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { } } + case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()): + // PIN unblock requested, fetch PUK and new PIN from the user + var pukpin string + if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil { + throwJSException(err.Error()) + } else { + pukpin = input + } + if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil { + throwJSException(err.Error()) + } else { + pukpin += input + } + passwd, _ = otto.ToValue(pukpin) + if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { + throwJSException(err.Error()) + } + case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()): // PIN input requested, fetch from the user and call open again if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {