Skip to content

Commit

Permalink
Ledger/Speculos TCP APDU transport (#288)
Browse files Browse the repository at this point in the history
* Ledger/Speculos TCP transport

* Ledger transport option documentation
  • Loading branch information
e-asphyx authored Mar 15, 2023
1 parent 7cd50a1 commit 36138c3
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 67 deletions.
24 changes: 24 additions & 0 deletions docs/ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ Example:
close_after: 3600s
```
### Transports
By default Ledger vault uses `usb` transport. Another available transport is `tcp` used primarily for interaction with [Speculos](https://github.com/LedgerHQ/speculos)
emulator. It can be enabled using `transport` option:

```yaml
vaults:
ledger:
driver: ledger
config:
id: 3944f7a0
transport: tcp://127.0.0.1:9999
keys:
- "bip32-ed25519/0'/0'"
- "secp256k1/0'/1'"
close_after: 3600s
```

In addition `signatory-cli ledger` command also accepts `-t` / `--transport` key with the same URL-like syntax:

```sh
signatory-cli ledger --transport 'tcp://127.0.0.1:9999' list
```

## Getting data from ledger for signatory configuration using CLI

Keep tezos-wallet app open for the below commands and for signing any wallet transactions.
Expand Down
49 changes: 31 additions & 18 deletions pkg/vault/ledger/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ Version: {{.Version}}

var listTpl = template.Must(template.New("list").Parse(listTemplateSrc))

func newListCommand() *cobra.Command {
func newListCommand(ctx *ledgerCommandContext) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List connected Ledgers",
RunE: func(cmd *cobra.Command, args []string) error {
devs, err := deviceScanner.scan()
s, err := getScanner(ctx.transport)
if err != nil {
return err
}
devs, err := s.scan()
if err != nil {
return err
}
Expand All @@ -32,7 +36,7 @@ func newListCommand() *cobra.Command {
}
}

func newSetupCommand() *cobra.Command {
func newSetupCommand(ctx *ledgerCommandContext) *cobra.Command {
var (
id string
mainHWM uint32
Expand All @@ -45,7 +49,7 @@ func newSetupCommand() *cobra.Command {
Short: "Authorize a key for baking",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
pkh, err := SetupBaking(id, args[0], chainID, mainHWM, testHWM)
pkh, err := SetupBaking(ctx.transport, id, args[0], chainID, mainHWM, testHWM)
if err != nil {
return err
}
Expand All @@ -61,43 +65,43 @@ func newSetupCommand() *cobra.Command {
return &cmd
}

func newDeuthorizeCommand() *cobra.Command {
func newDeuthorizeCommand(ctx *ledgerCommandContext) *cobra.Command {
var id string
cmd := cobra.Command{
Use: "deauthorize-baking <key id>",
Short: "Deuthorize a key",
RunE: func(cmd *cobra.Command, args []string) error {
return DeauthorizeBaking(id)
return DeauthorizeBaking(ctx.transport, id)
},
}
f := cmd.Flags()
f.StringVarP(&id, "device", "d", "", "Ledger device ID")
return &cmd
}

func newSetHighWatermarkCommand() *cobra.Command {
func newSetHighWatermarkCommand(ctx *ledgerCommandContext) *cobra.Command {
var id string
cmd := cobra.Command{
Use: "set-high-watermark <hwm>",
Short: "Set high water mark",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
hwm, _ := strconv.ParseUint(args[0], 10, 32)
return SetHighWatermark(id, uint32(hwm))
return SetHighWatermark(ctx.transport, id, uint32(hwm))
},
}
f := cmd.Flags()
f.StringVarP(&id, "device", "d", "", "Ledger device ID")
return &cmd
}

func newGetHighWatermarkCommand() *cobra.Command {
func newGetHighWatermarkCommand(ctx *ledgerCommandContext) *cobra.Command {
var id string
cmd := cobra.Command{
Use: "get-high-watermark",
Short: "Get high water mark",
RunE: func(cmd *cobra.Command, args []string) error {
hwm, err := GetHighWatermark(id)
hwm, err := GetHighWatermark(ctx.transport, id)
if err != nil {
return err
}
Expand All @@ -110,13 +114,13 @@ func newGetHighWatermarkCommand() *cobra.Command {
return &cmd
}

func newGetHighWatermarksCommand() *cobra.Command {
func newGetHighWatermarksCommand(ctx *ledgerCommandContext) *cobra.Command {
var id string
cmd := cobra.Command{
Use: "get-high-watermarks",
Short: "Get all high water marks and chain ID",
RunE: func(cmd *cobra.Command, args []string) error {
mainHWM, testHWM, chainID, err := GetHighWatermarks(id)
mainHWM, testHWM, chainID, err := GetHighWatermarks(ctx.transport, id)
if err != nil {
return err
}
Expand All @@ -129,18 +133,27 @@ func newGetHighWatermarksCommand() *cobra.Command {
return &cmd
}

type ledgerCommandContext struct {
transport string
}

func newLedgerCommand() *cobra.Command {
var ctx ledgerCommandContext

cmd := cobra.Command{
Use: "ledger",
Short: "Ledger specific operations",
}

cmd.AddCommand(newListCommand())
cmd.AddCommand(newSetupCommand())
cmd.AddCommand(newDeuthorizeCommand())
cmd.AddCommand(newSetHighWatermarkCommand())
cmd.AddCommand(newGetHighWatermarkCommand())
cmd.AddCommand(newGetHighWatermarksCommand())
f := cmd.PersistentFlags()
f.StringVarP(&ctx.transport, "transport", "t", "", "Transport")

cmd.AddCommand(newListCommand(&ctx))
cmd.AddCommand(newSetupCommand(&ctx))
cmd.AddCommand(newDeuthorizeCommand(&ctx))
cmd.AddCommand(newSetHighWatermarkCommand(&ctx))
cmd.AddCommand(newGetHighWatermarkCommand(&ctx))
cmd.AddCommand(newGetHighWatermarksCommand(&ctx))

return &cmd
}
10 changes: 5 additions & 5 deletions pkg/vault/ledger/integration_test/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestLedger(t *testing.T) {
setup, _ := strconv.ParseBool(os.Getenv("SETUP_BAKING"))

if setup {
pkh, err := ledger.SetupBaking("c4c56423", "bip25519/0'/0'", "", 0, 0)
pkh, err := ledger.SetupBaking("", "c4c56423", "bip25519/0'/0'", "", 0, 0)
require.NoError(t, err)
require.Equal(t, publicKeyHash, pkh)
}
Expand All @@ -72,8 +72,8 @@ func TestLedger(t *testing.T) {
}),
Policy: map[string]*signatory.Policy{
publicKeyHash: {
AllowedOperations: []string{"generic", "block", "endorsement"},
AllowedKinds: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"},
AllowedRequests: []string{"generic", "block", "endorsement"},
AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"},
},
},
}
Expand All @@ -91,8 +91,8 @@ func TestLedger(t *testing.T) {
VaultName: "Ledger",
ID: "bip32-ed25519/44'/1729'/0'/0'",
Policy: &signatory.Policy{
AllowedOperations: []string{"generic", "block", "endorsement"},
AllowedKinds: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"},
AllowedRequests: []string{"generic", "block", "endorsement"},
AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"},
},
Active: true,
},
Expand Down
50 changes: 31 additions & 19 deletions pkg/vault/ledger/ledger/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,55 @@ type LedgerDeviceInfo struct {
LegacyUSBProductID uint16
USBOnly bool
MemorySize int
BlockSize int
BluetoothSpec []*BluetoothSpec
}

var ledgerDevices = []*LedgerDeviceInfo{
&LedgerDeviceInfo{
var (
ledgerBlue = LedgerDeviceInfo{
ID: "blue",
ProductName: "Ledger Blue",
ProductIDMM: 0x00,
LegacyUSBProductID: 0x0000,
USBOnly: true,
MemorySize: 480 * 1024,
BlockSize: 4 * 1024,
},
&LedgerDeviceInfo{
}
ledgerNanoS = LedgerDeviceInfo{
ID: "nanoS",
ProductName: "Ledger Nano S",
ProductIDMM: 0x10,
LegacyUSBProductID: 0x0001,
USBOnly: true,
MemorySize: 320 * 1024,
BlockSize: 4 * 1024,
},
&LedgerDeviceInfo{
}
ledgerNanoSP = LedgerDeviceInfo{
ID: "nanoSP",
ProductName: "Ledger Nano S Plus",
ProductIDMM: 0x50,
LegacyUSBProductID: 0x0005,
USBOnly: true,
MemorySize: 1536 * 1024,
}
ledgerNanoX = LedgerDeviceInfo{
ID: "nanoX",
ProductName: "Ledger Nano X",
ProductIDMM: 0x40,
LegacyUSBProductID: 0x0004,
USBOnly: false,
MemorySize: 2 * 1024 * 1024,
BlockSize: 4 * 1024,
BluetoothSpec: []*BluetoothSpec{
{
ServiceUUID: uuid.MustParse("13d63400-2c97-0004-0000-4c6564676572"),
NotifyUUID: uuid.MustParse("13d63400-2c97-0004-0001-4c6564676572"),
WriteUUID: uuid.MustParse("13d63400-2c97-0004-0002-4c6564676572"),
},
},
},
}
ledgerNanoFTS = LedgerDeviceInfo{
ID: "nanoFTS",
ProductName: "Ledger Nano FTS",
ProductIDMM: 0x60,
LegacyUSBProductID: 0x0006,
USBOnly: false,
MemorySize: 1536 * 1024,
}
)

var ledgerDevices = []*LedgerDeviceInfo{
&ledgerBlue,
&ledgerNanoS,
&ledgerNanoSP,
&ledgerNanoX,
&ledgerNanoFTS,
}
82 changes: 82 additions & 0 deletions pkg/vault/ledger/ledger/tcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ledger

import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strings"
)

type TCPTransport struct {
Addr string
Model string
}

func (t *TCPTransport) Enumerate() ([]*DeviceInfo, error) {
var dev *LedgerDeviceInfo
if t.Model != "" {
for _, d := range ledgerDevices {
if strings.EqualFold(t.Model, d.ID) {
dev = d
break
}
}
if dev == nil {
return nil, fmt.Errorf("ledger: unknown model")
}
} else {
dev = &ledgerNanoS
}
return []*DeviceInfo{
{
Path: t.Addr,
DeviceInfo: dev,
},
}, nil
}

func (t *TCPTransport) Open(path string) (Exchanger, error) {
conn, err := net.Dial("tcp", path)
if err != nil {
return nil, fmt.Errorf("tcp: %w", err)
}
return &tcpRoundTripper{conn: conn}, nil
}

type tcpRoundTripper struct {
conn net.Conn
}

func (t *tcpRoundTripper) Exchange(req *APDUCommand) (*APDUResponse, error) {
data := req.Bytes()
//log.Printf("> %s", hex.EncodeToString(data))

buf := make([]byte, len(data)+4)
binary.BigEndian.PutUint32(buf, uint32(len(data)))
copy(buf[4:], data)

if _, err := t.conn.Write(buf); err != nil {
return nil, fmt.Errorf("tcp: %w", err)
}

var ln [4]byte
if _, err := io.ReadFull(t.conn, ln[:]); err != nil {
return nil, fmt.Errorf("tcp: %w", err)
}
buf = make([]byte, int(binary.BigEndian.Uint32(ln[:])+2))
if _, err := io.ReadFull(t.conn, buf); err != nil {
return nil, fmt.Errorf("tcp: %w", err)
}
//log.Printf("< %s", hex.EncodeToString(buf))
res := parseAPDUResponse(buf)
if res == nil {
return nil, errors.New("ledger: error parsing APDU response")
}
return res, nil
}

func (t *tcpRoundTripper) Close() error { return t.conn.Close() }

var _ Transport = (*TCPTransport)(nil)
2 changes: 1 addition & 1 deletion pkg/vault/ledger/ledger/usbhid.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,4 @@ func (u *USBHIDTransport) Open(path string) (Exchanger, error) {
return &rt, nil
}

var _ Transport = &USBHIDTransport{}
var _ Transport = (*USBHIDTransport)(nil)
2 changes: 2 additions & 0 deletions pkg/vault/ledger/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ecadlabs/signatory/pkg/vault/ledger/ledger"
"github.com/ecadlabs/signatory/pkg/vault/ledger/mnemonic"
"github.com/ecadlabs/signatory/pkg/vault/ledger/tezosapp"
log "github.com/sirupsen/logrus"
)

type deviceInfo struct {
Expand Down Expand Up @@ -103,6 +104,7 @@ func (s *scanner) scan() ([]*deviceInfo, error) {
for _, d := range devs {
app, dev, err := s.openPath(d.Path)
if err != nil {
log.Warnf("%s: %v", d.Path, err)
continue
}
app.Close()
Expand Down
Loading

0 comments on commit 36138c3

Please sign in to comment.