diff --git a/lightwallet/actions/actions.go b/lightwallet/actions/actions.go index f4413ef..6501e40 100644 --- a/lightwallet/actions/actions.go +++ b/lightwallet/actions/actions.go @@ -1,34 +1,57 @@ package actions import ( + "bytes" + "crypto/rand" + "crypto/sha256" "errors" + "fmt" "github.com/status-im/smartcard-go/apdu" "github.com/status-im/smartcard-go/globalplatform" "github.com/status-im/smartcard-go/lightwallet" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/text/unicode/norm" ) +const pairingSalt = "Status Hardware Wallet Lite" + var ( ErrAlreadyInitialized = errors.New("card already initialized") ErrNotInitialized = errors.New("card not initialized") ErrUnknownApplicationInfoTemplate = errors.New("unknown application info template") + ErrInvalidCardCryptogram = errors.New("invalid card cryptogram") ) -type ApplicationInfo struct { - InstanceUID []byte - PublicKey []byte - Version []byte - AvailableSlots []byte - KeyUID []byte -} - -func Select(c globalplatform.Channel, aid []byte) (*ApplicationInfo, error) { +func SelectNotInitialized(c globalplatform.Channel, aid []byte) ([]byte, error) { sel := globalplatform.NewCommandSelect(aid) resp, err := c.Send(sel) if err != nil { return nil, err } + if err = checkOKResponse(resp); err != nil { + return nil, err + } + + if resp.Data[0] != lightwallet.TagSelectResponsePreInitialized { + return nil, ErrAlreadyInitialized + } + + return resp.Data[2:], nil +} + +func SelectInitialized(c globalplatform.Channel, aid []byte) (*lightwallet.ApplicationInfo, error) { + sel := globalplatform.NewCommandSelect(aid) + resp, err := c.Send(sel) + if err != nil { + return nil, err + } + + if err = checkOKResponse(resp); err != nil { + return nil, err + } + if resp.Data[0] == lightwallet.TagSelectResponsePreInitialized { return nil, ErrNotInitialized } @@ -36,19 +59,8 @@ func Select(c globalplatform.Channel, aid []byte) (*ApplicationInfo, error) { return parseApplicationInfo(resp) } -func Init(c globalplatform.Channel, secrets *lightwallet.Secrets, aid []byte) error { - sel := globalplatform.NewCommandSelect(aid) - resp, err := c.Send(sel) - if err != nil { - return err - } - - if resp.Data[0] != lightwallet.TagSelectResponsePreInitialized { - return ErrAlreadyInitialized - } - - cardKeyData := resp.Data[2:] - secureChannel, err := lightwallet.NewSecureChannel(c, cardKeyData) +func Init(c globalplatform.Channel, cardPubKey []byte, secrets *lightwallet.Secrets, aid []byte) error { + secureChannel, err := lightwallet.NewSecureChannel(c, cardPubKey) if err != nil { return err } @@ -59,12 +71,87 @@ func Init(c globalplatform.Channel, secrets *lightwallet.Secrets, aid []byte) er } init := lightwallet.NewCommandInit(data) - _, err = c.Send(init) + resp, err := c.Send(init) + if err != nil { + return err + } - return err + return checkOKResponse(resp) } -func parseApplicationInfo(resp *apdu.Response) (*ApplicationInfo, error) { +func Pair(c globalplatform.Channel, pairingPass string, pin string) (*lightwallet.PairingInfo, error) { + secretHash := pbkdf2.Key(norm.NFKD.Bytes([]byte(pairingPass)), norm.NFKD.Bytes([]byte(pairingSalt)), 50000, 32, sha256.New) + + challenge := make([]byte, 32) + if _, err := rand.Read(challenge); err != nil { + return nil, err + } + + cmd := lightwallet.NewCommandPairFirstStep(challenge) + resp, err := c.Send(cmd) + if err != nil { + return nil, err + } + + if err = checkOKResponse(resp); err != nil { + return nil, err + } + + h := sha256.New() + h.Write(secretHash[:]) + h.Write(challenge) + + expectedCryptogram := h.Sum(nil) + cardCryptogram := resp.Data[:32] + cardChallenge := resp.Data[32:] + + if !bytes.Equal(expectedCryptogram, cardCryptogram) { + return nil, ErrInvalidCardCryptogram + } + + h.Reset() + h.Write(secretHash[:]) + h.Write(cardChallenge) + cmd = lightwallet.NewCommandPairFinalStep(h.Sum(nil)) + resp, err = c.Send(cmd) + if err != nil { + return nil, err + } + + if err = checkOKResponse(resp); err != nil { + return nil, err + } + + h.Reset() + h.Write(secretHash[:]) + h.Write(resp.Data[1:]) + + pairingKey := h.Sum(nil) + pairingIndex := resp.Data[0] + + return &lightwallet.PairingInfo{ + PairingKey: pairingKey, + PairingIndex: int(pairingIndex), + }, nil +} + +func OpenSecureChannel(c globalplatform.Channel, appInfo *lightwallet.ApplicationInfo, pairingIndex uint8, pairingKey []byte) error { + sc, err := lightwallet.NewSecureChannel(c, appInfo.PublicKey) + + cmd := lightwallet.NewCommandOpenSecureChannel(pairingIndex, sc.RawPublicKey()) + resp, err := c.Send(cmd) + if err != nil { + return err + } + + if err = checkOKResponse(resp); err != nil { + return err + } + + return nil +} + +func parseApplicationInfo(resp *apdu.Response) (*lightwallet.ApplicationInfo, error) { if resp.Data[0] != lightwallet.TagApplicationInfoTemplate { return nil, ErrUnknownApplicationInfoTemplate } @@ -94,7 +181,7 @@ func parseApplicationInfo(resp *apdu.Response) (*ApplicationInfo, error) { return nil, err } - return &ApplicationInfo{ + return &lightwallet.ApplicationInfo{ InstanceUID: instanceUID, PublicKey: pubKey, Version: appVersion, @@ -102,3 +189,17 @@ func parseApplicationInfo(resp *apdu.Response) (*ApplicationInfo, error) { KeyUID: keyUID, }, nil } + +func checkOKResponse(resp *apdu.Response) error { + return checkResponse(resp, apdu.SwOK) +} + +func checkResponse(resp *apdu.Response, allowedResponses ...uint16) error { + for _, code := range allowedResponses { + if code == resp.Sw { + return nil + } + } + + return fmt.Errorf("unexpected response: %x", resp.Sw) +} diff --git a/lightwallet/actionsets/installer.go b/lightwallet/actionsets/installer.go index 7470baa..04f134f 100644 --- a/lightwallet/actionsets/installer.go +++ b/lightwallet/actionsets/installer.go @@ -37,7 +37,7 @@ func NewInstaller(t globalplatform.Transmitter) *Installer { // Install installs the applet from the specified capFile. func (i *Installer) Install(capFile *os.File, overwriteApplet bool) error { - err := i.initSecureChannel(cardManagerAID) + err := i.initGPSecureChannel(cardManagerAID) if err != nil { return err } @@ -70,7 +70,12 @@ func (i *Installer) Init() (*lightwallet.Secrets, error) { return nil, err } - err = actions.Init(i.c, secrets, walletAID) + cardPubKey, err := actions.SelectNotInitialized(i.c, walletAID) + if err != nil { + return nil, err + } + + err = actions.Init(i.c, cardPubKey, secrets, walletAID) if err != nil { return nil, err } @@ -78,9 +83,18 @@ func (i *Installer) Init() (*lightwallet.Secrets, error) { return secrets, nil } +func (i *Installer) Pair(pairingPass, pin string) (*lightwallet.PairingInfo, error) { + _, err := actions.SelectInitialized(i.c, walletAID) + if err != nil { + return nil, err + } + + return actions.Pair(i.c, pairingPass, pin) +} + // Info returns if the applet is already installed in the card. func (i *Installer) Info() (bool, error) { - err := i.initSecureChannel(cardManagerAID) + err := i.initGPSecureChannel(cardManagerAID) if err != nil { return false, err } @@ -90,7 +104,7 @@ func (i *Installer) Info() (bool, error) { // Delete deletes the applet and related package from the card. func (i *Installer) Delete() error { - err := i.initSecureChannel(cardManagerAID) + err := i.initGPSecureChannel(cardManagerAID) if err != nil { return err } @@ -112,7 +126,7 @@ func (i *Installer) isAppletInstalled() (bool, error) { return true, nil } -func (i *Installer) initSecureChannel(sdaid []byte) error { +func (i *Installer) initGPSecureChannel(sdaid []byte) error { // select card manager err := i.selectAID(sdaid) if err != nil { diff --git a/lightwallet/commands.go b/lightwallet/commands.go index bf082cc..0a39970 100644 --- a/lightwallet/commands.go +++ b/lightwallet/commands.go @@ -6,10 +6,15 @@ import ( ) const ( - InsInit = uint8(0xFE) + InsInit = uint8(0xFE) + InsOpenSecureChannel = uint8(0x10) + InsPair = uint8(0x12) TagSelectResponsePreInitialized = uint8(0x80) TagApplicationInfoTemplate = uint8(0xA4) + + P1PairingFirstStep = uint8(0x00) + P1PairingFinalStep = uint8(0x01) ) func NewCommandInit(data []byte) *apdu.Command { @@ -21,3 +26,33 @@ func NewCommandInit(data []byte) *apdu.Command { data, ) } + +func NewCommandPairFirstStep(challenge []byte) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsPair, + P1PairingFirstStep, + uint8(0x00), + challenge, + ) +} + +func NewCommandPairFinalStep(cryptogramHash []byte) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsPair, + P1PairingFinalStep, + uint8(0x00), + cryptogramHash, + ) +} + +func NewCommandOpenSecureChannel(pairingIndex uint8, pubKey []byte) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsOpenSecureChannel, + pairingIndex, + uint8(0x00), + pubKey, + ) +} diff --git a/lightwallet/secure_channel.go b/lightwallet/secure_channel.go index 89e28cb..04bc3ba 100644 --- a/lightwallet/secure_channel.go +++ b/lightwallet/secure_channel.go @@ -10,9 +10,9 @@ import ( ) type SecureChannel struct { - c globalplatform.Channel - secret []byte - pubKey *ecdsa.PublicKey + c globalplatform.Channel + secret []byte + publicKey *ecdsa.PublicKey } func NewSecureChannel(c globalplatform.Channel, cardKeyData []byte) (*SecureChannel, error) { @@ -29,18 +29,26 @@ func NewSecureChannel(c globalplatform.Channel, cardKeyData []byte) (*SecureChan secret := crypto.GenerateECDHSharedSecret(key, cardPubKey) return &SecureChannel{ - c: c, - secret: secret, - pubKey: &key.PublicKey, + c: c, + secret: secret, + publicKey: &key.PublicKey, }, nil } +func (sc *SecureChannel) PublicKey() *ecdsa.PublicKey { + return sc.publicKey +} + +func (sc *SecureChannel) RawPublicKey() []byte { + return ethcrypto.FromECDSAPub(sc.publicKey) +} + func (sc *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { return sc.c.Send(cmd) } func (sc *SecureChannel) OneShotEncrypt(secrets *Secrets) ([]byte, error) { - pubKeyData := ethcrypto.FromECDSAPub(sc.pubKey) + pubKeyData := ethcrypto.FromECDSAPub(sc.publicKey) data := append([]byte(secrets.Pin()), []byte(secrets.Puk())...) data = append(data, secrets.PairingToken()...) diff --git a/lightwallet/types.go b/lightwallet/types.go new file mode 100644 index 0000000..8ec9a00 --- /dev/null +++ b/lightwallet/types.go @@ -0,0 +1,16 @@ +package lightwallet + +type ApplicationInfo struct { + InstanceUID []byte + PublicKey []byte + Version []byte + AvailableSlots []byte + // KeyUID is the sha256 of of the master public key on the card. + // It's empty if the card doesn't contain any key. + KeyUID []byte +} + +type PairingInfo struct { + PairingKey []byte + PairingIndex int +}