commit 56660433baf653063afc001183bd7b07dbb18830 Author: Andrea Franz Date: Fri Mar 1 18:44:07 2019 +0100 rename to keycard-go diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ef91b5 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# status-hardware-wallet + +`status-hardware-wallet` is a command line tool you can use to initialize a smartcard with the [Status Hardware Wallet](https://github.com/status-im/hardware-wallet). + +## Dependencies + +To install `hardware-wallet-go` you need `go` in your system. + +MacOSX: + +`brew install go` + +## Installation + +`go get github.com/status-im/hardware-wallet-go/cmd/status-hardware-wallet` + +The executable will be installed in `$GOPATH/bin`. +Check your `$GOPATH` with `go env`. + +## Usage + +### Install the hardware wallet applet + +The install command will install an applet to the card. +You can download the status `cap` file from the [status-im/hardware-wallet releases page](https://github.com/status-im/hardware-wallet/releases). + +```bash +status-hardware-wallet install -l debug -a PATH_TO_CAP_FILE +``` + +In case the applet is already installed and you want to force a new installation you can pass the `-f` flag. + +### Card info + +```bash +status-hardware-wallet info -l debug +``` + +The `info` command will print something like this: + +``` +Installed: true +Initialized: false +InstanceUID: 0x +PublicKey: 0x112233... +Version: 0x +AvailableSlots: 0x +KeyUID: 0x +``` + +### Card initialization + + +```bash +status-hardware-wallet init -l debug +``` + +The `init` command initializes the card and generates the secrets needed to pair the card to a device. + +``` +PIN 123456 +PUK 123456789012 +Pairing password: RandomPairingPassword +``` + +### Deleting the applet from the card + +:warning: **WARNING! This command will remove the applet and all the keys from the card.** :warning: + +```bash +status-hardware-wallet delete -l debug +``` + +### Pairing + +```bash +status-hardware-wallet pair -l debug +``` + +The process will ask for `PairingPassword` and `PIN` and will generate a pairing key you can use to interact with the card. diff --git a/initializer.go b/initializer.go new file mode 100644 index 0000000..9ec08b0 --- /dev/null +++ b/initializer.go @@ -0,0 +1,271 @@ +package main + +import ( + "crypto/rand" + "errors" + "fmt" + "os" + + "github.com/status-im/keycard-go/apdu" + "github.com/status-im/keycard-go/globalplatform" + "github.com/status-im/keycard-go/lightwallet" + "github.com/status-im/keycard-go/lightwallet/actions" +) + +var ( + errAppletNotInstalled = errors.New("applet not installed") + errCardNotInitialized = errors.New("card not initialized") + errCardAlreadyInitialized = errors.New("card already initialized") + + ErrNotInitialized = errors.New("card not initialized") +) + +// Initializer defines a struct with methods to install applets and initialize a card. +type Initializer struct { + c globalplatform.Channel +} + +// NewInitializer returns a new Initializer that communicates to Transmitter t. +func NewInitializer(t globalplatform.Transmitter) *Initializer { + return &Initializer{ + c: globalplatform.NewNormalChannel(t), + } +} + +// Install installs the applet from the specified capFile. +func (i *Initializer) Install(capFile *os.File, overwriteApplet bool) error { + info, err := actions.Select(i.c, lightwallet.WalletAID) + if err != nil { + return err + } + + if info.Installed && !overwriteApplet { + return errors.New("applet already installed") + } + + err = i.initGPSecureChannel(lightwallet.CardManagerAID) + if err != nil { + return err + } + + err = i.deleteAID(lightwallet.NdefInstanceAID, lightwallet.WalletInstanceAID, lightwallet.AppletPkgAID) + if err != nil { + return err + } + + err = i.installApplets(capFile) + if err != nil { + return err + } + + return err +} + +func (i *Initializer) Init() (*lightwallet.Secrets, error) { + secrets, err := lightwallet.NewSecrets() + if err != nil { + return nil, err + } + + info, err := actions.Select(i.c, lightwallet.WalletAID) + if err != nil { + return nil, err + } + + if !info.Installed { + return nil, errAppletNotInstalled + } + + if info.Initialized { + return nil, errCardAlreadyInitialized + } + + err = actions.Init(i.c, info.PublicKey, secrets, lightwallet.WalletAID) + if err != nil { + return nil, err + } + + return secrets, nil +} + +func (i *Initializer) Pair(pairingPass, pin string) (*lightwallet.PairingInfo, error) { + appInfo, err := actions.Select(i.c, lightwallet.WalletAID) + if err != nil { + return nil, err + } + + if !appInfo.Initialized { + return nil, ErrNotInitialized + } + + return actions.Pair(i.c, pairingPass, pin) +} + +// Info returns a lightwallet.ApplicationInfo struct with info about the card. +func (i *Initializer) Info() (*lightwallet.ApplicationInfo, error) { + return actions.Select(i.c, lightwallet.WalletAID) +} + +// Status returns +func (i *Initializer) Status(index uint8, key []byte) (*lightwallet.ApplicationStatus, error) { + info, err := actions.Select(i.c, lightwallet.WalletAID) + if err != nil { + return nil, err + } + + if !info.Installed { + return nil, errAppletNotInstalled + } + + if !info.Initialized { + return nil, errCardNotInitialized + } + + sc, err := actions.OpenSecureChannel(i.c, info, index, key) + if err != nil { + return nil, err + } + + return actions.GetStatusApplication(sc) +} + +// Delete deletes the applet and related package from the card. +func (i *Initializer) Delete() error { + err := i.initGPSecureChannel(lightwallet.CardManagerAID) + if err != nil { + return err + } + + return i.deleteAID(lightwallet.NdefInstanceAID, lightwallet.WalletInstanceAID, lightwallet.AppletPkgAID) +} + +func (i *Initializer) initGPSecureChannel(sdaid []byte) error { + // select card manager + err := i.selectAID(sdaid) + if err != nil { + return err + } + + // initialize update + session, err := i.initializeUpdate() + if err != nil { + return err + } + + i.c = globalplatform.NewSecureChannel(session, i.c) + + // external authenticate + return i.externalAuthenticate(session) +} + +func (i *Initializer) selectAID(aid []byte) error { + sel := globalplatform.NewCommandSelect(lightwallet.CardManagerAID) + _, err := i.send("select", sel) + + return err +} + +func (i *Initializer) initializeUpdate() (*globalplatform.Session, error) { + hostChallenge, err := generateHostChallenge() + if err != nil { + return nil, err + } + + init := globalplatform.NewCommandInitializeUpdate(hostChallenge) + resp, err := i.send("initialize update", init) + if err != nil { + return nil, err + } + + // verify cryptogram and initialize session keys + keys := globalplatform.NewKeyProvider(lightwallet.CardTestKey, lightwallet.CardTestKey) + session, err := globalplatform.NewSession(keys, resp, hostChallenge) + + return session, err +} + +func (i *Initializer) externalAuthenticate(session *globalplatform.Session) error { + encKey := session.KeyProvider().Enc() + extAuth, err := globalplatform.NewCommandExternalAuthenticate(encKey, session.CardChallenge(), session.HostChallenge()) + if err != nil { + return err + } + + _, err = i.send("external authenticate", extAuth) + + return err +} + +func (i *Initializer) deleteAID(aids ...[]byte) error { + for _, aid := range aids { + del := globalplatform.NewCommandDelete(aid) + _, err := i.send("delete", del, globalplatform.SwOK, globalplatform.SwReferencedDataNotFound) + if err != nil { + return err + } + } + + return nil +} + +func (i *Initializer) installApplets(capFile *os.File) error { + // install for load + preLoad := globalplatform.NewCommandInstallForLoad(lightwallet.AppletPkgAID, lightwallet.CardManagerAID) + _, err := i.send("install for load", preLoad) + if err != nil { + return err + } + + // load + load, err := globalplatform.NewLoadCommandStream(capFile) + if err != nil { + return err + } + + for load.Next() { + cmd := load.GetCommand() + _, err = i.send(fmt.Sprintf("load %d of 40", load.Index()+1), cmd) + if err != nil { + return err + } + } + + installNdef := globalplatform.NewCommandInstallForInstall(lightwallet.AppletPkgAID, lightwallet.NdefAppletAID, lightwallet.NdefInstanceAID, []byte{}) + _, err = i.send("install for install (ndef)", installNdef) + if err != nil { + return err + } + + installWallet := globalplatform.NewCommandInstallForInstall(lightwallet.AppletPkgAID, lightwallet.WalletAID, lightwallet.WalletInstanceAID, []byte{}) + _, err = i.send("install for install (wallet)", installWallet) + + return err +} + +func (i *Initializer) send(description string, cmd *apdu.Command, allowedResponses ...uint16) (*apdu.Response, error) { + logger.Debug("sending apdu command", "name", description) + resp, err := i.c.Send(cmd) + if err != nil { + return nil, err + } + + if len(allowedResponses) == 0 { + allowedResponses = []uint16{apdu.SwOK} + } + + for _, code := range allowedResponses { + if code == resp.Sw { + return resp, nil + } + } + + err = fmt.Errorf("unexpected response from command %s: %x", description, resp.Sw) + + return nil, err +} + +func generateHostChallenge() ([]byte, error) { + c := make([]byte, 8) + _, err := rand.Read(c) + return c, err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..14e3f34 --- /dev/null +++ b/main.go @@ -0,0 +1,274 @@ +package main + +import ( + "bufio" + "encoding/hex" + "flag" + "fmt" + stdlog "log" + "os" + "strconv" + "strings" + + "github.com/ebfe/scard" + "github.com/ethereum/go-ethereum/log" +) + +type commandFunc func(*Initializer) error + +var ( + logger = log.New("package", "status-go/cmd/keycard") + + commands map[string]commandFunc + command string + + flagCapFile = flag.String("a", "", "applet cap file path") + flagOverwrite = flag.Bool("f", false, "force applet installation if already installed") + flagLogLevel = flag.String("l", "", `Log level, one of: "error", "warn", "info", "debug", and "trace"`) +) + +func initLogger() { + if *flagLogLevel == "" { + *flagLogLevel = "info" + } + + level, err := log.LvlFromString(strings.ToLower(*flagLogLevel)) + if err != nil { + stdlog.Fatal(err) + } + + handler := log.StreamHandler(os.Stderr, log.TerminalFormat(true)) + filteredHandler := log.LvlFilterHandler(level, handler) + log.Root().SetHandler(filteredHandler) +} + +func init() { + commands = map[string]commandFunc{ + "install": commandInstall, + "info": commandInfo, + "delete": commandDelete, + "init": commandInit, + "pair": commandPair, + "status": commandStatus, + } + + if len(os.Args) < 2 { + usage() + } + + command = os.Args[1] + if len(os.Args) > 2 { + flag.CommandLine.Parse(os.Args[2:]) + } + + initLogger() +} + +func usage() { + fmt.Printf("\nUsage:\n keycard COMMAND [FLAGS]\n\nAvailable commands:\n") + for name := range commands { + fmt.Printf(" %s\n", name) + } + fmt.Print("\nFlags:\n\n") + flag.PrintDefaults() + os.Exit(1) +} + +func fail(msg string, ctx ...interface{}) { + logger.Error(msg, ctx...) + os.Exit(1) +} + +func main() { + ctx, err := scard.EstablishContext() + if err != nil { + fail("error establishing card context", "error", err) + } + defer func() { + if err := ctx.Release(); err != nil { + logger.Error("error releasing context", "error", err) + } + }() + + readers, err := ctx.ListReaders() + if err != nil { + fail("error getting readers", "error", err) + } + + if len(readers) == 0 { + fail("couldn't find any reader") + } + + if len(readers) > 1 { + fail("too many readers found") + } + + reader := readers[0] + logger.Debug("using reader", "name", reader) + logger.Debug("connecting to card", "reader", reader) + card, err := ctx.Connect(reader, scard.ShareShared, scard.ProtocolAny) + if err != nil { + fail("error connecting to card", "error", err) + } + defer func() { + if err := card.Disconnect(scard.ResetCard); err != nil { + logger.Error("error disconnecting card", "error", err) + } + }() + + status, err := card.Status() + if err != nil { + fail("error getting card status", "error", err) + } + + switch status.ActiveProtocol { + case scard.ProtocolT0: + logger.Debug("card protocol", "T", "0") + case scard.ProtocolT1: + logger.Debug("card protocol", "T", "1") + default: + logger.Debug("card protocol", "T", "unknown") + } + + i := NewInitializer(card) + if f, ok := commands[command]; ok { + err = f(i) + if err != nil { + logger.Error("error executing command", "command", command, "error", err) + os.Exit(1) + } + os.Exit(0) + } + + fail("unknown command", "command", command) + usage() +} + +func ask(description string) string { + r := bufio.NewReader(os.Stdin) + fmt.Printf("%s: ", description) + text, err := r.ReadString('\n') + if err != nil { + stdlog.Fatal(err) + } + + return strings.TrimSpace(text) +} + +func askHex(description string) []byte { + s := ask(description) + if s[:2] == "0x" { + s = s[2:] + } + + data, err := hex.DecodeString(s) + if err != nil { + stdlog.Fatal(err) + } + + return data +} + +func askUint8(description string) uint8 { + s := ask(description) + i, err := strconv.ParseUint(s, 10, 8) + if err != nil { + stdlog.Fatal(err) + } + + return uint8(i) +} + +func commandInstall(i *Initializer) error { + if *flagCapFile == "" { + logger.Error("you must specify a cap file path with the -f flag\n") + usage() + } + + f, err := os.Open(*flagCapFile) + if err != nil { + fail("error opening cap file", "error", err) + } + defer f.Close() + + fmt.Printf("installation can take a while...\n") + err = i.Install(f, *flagOverwrite) + if err != nil { + fail("installation error", "error", err) + } + + fmt.Printf("applet installed successfully.\n") + return nil +} + +func commandInfo(i *Initializer) error { + info, err := i.Info() + if err != nil { + return err + } + + fmt.Printf("Installed: %+v\n", info.Installed) + fmt.Printf("Initialized: %+v\n", info.Initialized) + fmt.Printf("InstanceUID: 0x%x\n", info.InstanceUID) + fmt.Printf("PublicKey: 0x%x\n", info.PublicKey) + fmt.Printf("Version: 0x%x\n", info.Version) + fmt.Printf("AvailableSlots: 0x%x\n", info.AvailableSlots) + fmt.Printf("KeyUID: 0x%x\n", info.KeyUID) + + return nil +} + +func commandDelete(i *Initializer) error { + err := i.Delete() + if err != nil { + return err + } + + fmt.Printf("applet deleted\n") + + return nil +} + +func commandInit(i *Initializer) error { + secrets, err := i.Init() + if err != nil { + return err + } + + fmt.Printf("PIN %s\n", secrets.Pin()) + fmt.Printf("PUK %s\n", secrets.Puk()) + fmt.Printf("Pairing password: %s\n", secrets.PairingPass()) + + return nil +} + +func commandPair(i *Initializer) error { + pairingPass := ask("Pairing password") + pin := ask("PIN") + info, err := i.Pair(pairingPass, pin) + if err != nil { + return err + } + + fmt.Printf("Pairing key 0x%x\n", info.Key) + fmt.Printf("Pairing Index %d\n", info.Index) + + return nil +} + +func commandStatus(i *Initializer) error { + index := askUint8("Pairing index") + key := askHex("Pairing key") + + appStatus, err := i.Status(index, key) + if err != nil { + return err + } + + fmt.Printf("Pin retry count: %d\n", appStatus.PinRetryCount) + fmt.Printf("PUK retry count: %d\n", appStatus.PUKRetryCount) + fmt.Printf("Key initialized: %v\n", appStatus.KeyInitialized) + fmt.Printf("Public key derivation: %v\n", appStatus.PubKeyDerivation) + + return nil +}