implement applet secure channel

This commit is contained in:
Andrea Franz 2018-11-06 18:38:13 +01:00
parent 8be3d6ebcb
commit 6e7e217179
No known key found for this signature in database
GPG Key ID: 4F0D2F2D9DE7F29D
10 changed files with 349 additions and 39 deletions

View File

@ -2,10 +2,12 @@ package main
import (
"bufio"
"encoding/hex"
"flag"
"fmt"
stdlog "log"
"os"
"strconv"
"strings"
"github.com/ebfe/scard"
@ -51,6 +53,7 @@ func init() {
"delete": commandDelete,
"init": commandInit,
"pair": commandPair,
"status": commandStatus,
}
}
@ -150,6 +153,30 @@ func ask(description string) string {
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 *actionsets.Installer) error {
if *flagCapFile == "" {
logger.Error("you must specify a cap file path with the -f flag\n")
@ -221,8 +248,15 @@ func commandPair(i *actionsets.Installer) error {
return err
}
fmt.Printf("Pairing key %x\n", info.Key)
fmt.Printf("Pairing key 0x%x\n", info.Key)
fmt.Printf("Pairing Index %d\n", info.Index)
return nil
}
func commandStatus(i *actionsets.Installer) error {
index := askUint8("Pairing index")
key := askHex("Pairing key")
return i.Status(index, key)
}

View File

@ -3,8 +3,6 @@ package crypto
import (
"bytes"
"crypto/cipher"
/* #nosec */
"crypto/des"
)
@ -25,7 +23,6 @@ func DeriveKey(cardKey []byte, seq []byte, purpose []byte) ([]byte, error) {
copy(derivation, purpose[:2])
copy(derivation[2:], seq[:2])
/* #nosec */
block, err := des.NewTripleDESCipher(key24)
if err != nil {
return nil, err
@ -57,13 +54,11 @@ func VerifyCryptogram(encKey, hostChallenge, cardChallenge, cardCryptogram []byt
func MacFull3DES(key, data, iv []byte) ([]byte, error) {
data = AppendDESPadding(data)
/* #nosec */
desBlock, err := des.NewCipher(resizeKey8(key))
if err != nil {
return nil, err
}
/* #nosec */
des3Block, err := des.NewTripleDESCipher(resizeKey24(key))
if err != nil {
return nil, err
@ -90,7 +85,6 @@ func MacFull3DES(key, data, iv []byte) ([]byte, error) {
// EncryptICV encrypts an ICV with the specified macKey.
// The ICV is usually the mac of the previous command sent in the current session.
func EncryptICV(macKey, icv []byte) ([]byte, error) {
/* #nosec */
block, err := des.NewCipher(resizeKey8(macKey))
if err != nil {
return nil, err
@ -107,7 +101,6 @@ func EncryptICV(macKey, icv []byte) ([]byte, error) {
func Mac3DES(key, data, iv []byte) ([]byte, error) {
key24 := resizeKey24(key)
/* #nosec */
block, err := des.NewTripleDESCipher(key24)
if err != nil {
return nil, err
@ -123,13 +116,11 @@ func Mac3DES(key, data, iv []byte) ([]byte, error) {
// AppendDESPadding appends an 0x80 bytes to data and other zero bytes to make the result length multiple of 8.
func AppendDESPadding(data []byte) []byte {
length := len(data) + 1
for ; length%8 != 0; length++ {
}
newData := make([]byte, length)
blockSize := 8
paddingSize := blockSize - (len(data) % blockSize)
newData := make([]byte, len(data)+paddingSize)
copy(newData, data)
copy(newData[len(data):], []byte{0x80})
newData[len(data)] = 0x80
return newData
}

View File

@ -30,6 +30,16 @@ func TestAppendDESPadding(t *testing.T) {
result := AppendDESPadding(data)
expected := "AABB800000000000"
assert.Equal(t, expected, hexutils.BytesToHex(result))
data = hexutils.HexToBytes("01020304050607")
result = AppendDESPadding(data)
expected = "0102030405060780"
assert.Equal(t, expected, hexutils.BytesToHex(result))
data = hexutils.HexToBytes("0102030405060708")
result = AppendDESPadding(data)
expected = "01020304050607088000000000000000"
assert.Equal(t, expected, hexutils.BytesToHex(result))
}
func TestVerifyCryptogram(t *testing.T) {

View File

@ -3,7 +3,6 @@ package actions
import (
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
@ -142,23 +141,33 @@ func OpenSecureChannel(c globalplatform.Channel, appInfo *lightwallet.Applicatio
return nil, err
}
salt := resp.Data[:32]
iv := resp.Data[32:]
h := sha512.New()
h.Write(sc.Secret())
h.Write(pairingKey)
h.Write(salt)
data := h.Sum(nil)
encKey := data[:32]
macKey := data[32:]
encKey, macKey, iv := crypto.DeriveSessionKeys(sc.Secret(), pairingKey, resp.Data)
sc.Init(iv, encKey, macKey)
err = mutualAuthenticate(sc)
if err != nil {
return nil, err
}
return sc, nil
}
func mutualAuthenticate(sc *lightwallet.SecureChannel) error {
data := make([]byte, 32)
if _, err := rand.Read(data); err != nil {
return err
}
cmd := lightwallet.NewCommandMutuallyAuthenticate(data)
resp, err := sc.Send(cmd)
return checkOKResponse(err, resp)
}
func Status(index uint8, key []byte) error {
return nil
}
func parseApplicationInfo(data []byte, info *lightwallet.ApplicationInfo) (*lightwallet.ApplicationInfo, error) {
if data[0] != lightwallet.TagApplicationInfoTemplate {
return nil, ErrUnknownApplicationInfoTemplate

View File

@ -21,6 +21,10 @@ var (
walletAID = []byte{0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x57, 0x61, 0x6C, 0x6C, 0x65, 0x74, 0x41, 0x70, 0x70}
ndefAppletAID = []byte{0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x57, 0x61, 0x6C, 0x6C, 0x65, 0x74, 0x4E, 0x46, 0x43}
ndefInstanceAID = []byte{0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01}
errAppletNotInstalled = errors.New("applet not installed")
errCardNotInitialized = errors.New("card not initialized")
errCardAlreadyInitialized = errors.New("card already initialized")
)
// Installer defines a struct with methods to install an applet to a smartcard.
@ -76,11 +80,11 @@ func (i *Installer) Init() (*lightwallet.Secrets, error) {
}
if !info.Installed {
return nil, fmt.Errorf("applet not installed")
return nil, errAppletNotInstalled
}
if info.Initialized {
return nil, fmt.Errorf("card already initialized")
return nil, errCardAlreadyInitialized
}
err = actions.Init(i.c, info.PublicKey, secrets, walletAID)
@ -100,11 +104,34 @@ func (i *Installer) Pair(pairingPass, pin string) (*lightwallet.PairingInfo, err
return actions.Pair(i.c, pairingPass, pin)
}
// Info returns if the applet is already installed in the card.
// Info returns a lightwallet.ApplicationInfo struct with info about the card.
func (i *Installer) Info() (*lightwallet.ApplicationInfo, error) {
return actions.Select(i.c, walletAID)
}
// Status returns
func (i *Installer) Status(index uint8, key []byte) error {
info, err := actions.Select(i.c, walletAID)
if err != nil {
return err
}
if !info.Installed {
return errAppletNotInstalled
}
if !info.Initialized {
return errCardNotInitialized
}
sc, err := actions.OpenSecureChannel(i.c, info, index, key)
if err != nil {
return err
}
return nil
}
// Delete deletes the applet and related package from the card.
func (i *Installer) Delete() error {
err := i.initGPSecureChannel(cardManagerAID)

View File

@ -6,9 +6,10 @@ import (
)
const (
InsInit = uint8(0xFE)
InsOpenSecureChannel = uint8(0x10)
InsPair = uint8(0x12)
InsInit = uint8(0xFE)
InsOpenSecureChannel = uint8(0x10)
InsMutuallyAuthenticate = uint8(0x11)
InsPair = uint8(0x12)
TagSelectResponsePreInitialized = uint8(0x80)
TagApplicationInfoTemplate = uint8(0xA4)
@ -56,3 +57,13 @@ func NewCommandOpenSecureChannel(pairingIndex uint8, pubKey []byte) *apdu.Comman
pubKey,
)
}
func NewCommandMutuallyAuthenticate(data []byte) *apdu.Command {
return apdu.NewCommand(
globalplatform.ClaGp,
InsMutuallyAuthenticate,
uint8(0x00),
uint8(0x00),
data,
)
}

View File

@ -7,6 +7,7 @@ import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"errors"
"github.com/ethereum/go-ethereum/crypto"
@ -63,10 +64,83 @@ func OneShotEncrypt(pubKeyData, secret, data []byte) ([]byte, error) {
return encrypted, nil
}
func appendPadding(blockSize int, data []byte) []byte {
paddingSize := blockSize - (len(data)+1)%blockSize
zeroes := bytes.Repeat([]byte{0x00}, paddingSize)
padding := append([]byte{0x80}, zeroes...)
func DeriveSessionKeys(secret, pairingKey, cardData []byte) ([]byte, []byte, []byte) {
salt := cardData[:32]
iv := cardData[32:]
return append(data, padding...)
h := sha512.New()
h.Write(secret)
h.Write(pairingKey)
h.Write(salt)
data := h.Sum(nil)
encKey := data[:32]
macKey := data[32:]
return encKey, macKey, iv
}
func EncryptData(data []byte, encKey []byte, iv []byte) ([]byte, error) {
data = appendPadding(16, data)
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
ciphertext := make([]byte, len(data))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, data)
return ciphertext, nil
}
func DecryptData(data []byte, encKey []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
plaintext := make([]byte, len(data))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plaintext, data)
return removePadding(16, plaintext), nil
}
func CalculateMac(meta []byte, data []byte, macKey []byte) ([]byte, error) {
data = appendPadding(16, data)
block, err := aes.NewCipher(macKey)
if err != nil {
return nil, err
}
mode := cipher.NewCBCEncrypter(block, make([]byte, 16))
mode.CryptBlocks(meta, meta)
mode.CryptBlocks(data, data)
mac := data[len(data)-32 : len(data)-16]
return mac, nil
}
func appendPadding(blockSize int, data []byte) []byte {
paddingSize := blockSize - (len(data) % blockSize)
newData := make([]byte, len(data)+paddingSize)
copy(newData, data)
newData[len(data)] = 0x80
return newData
}
func removePadding(blockSize int, data []byte) []byte {
i := len(data) - 1
for ; i > len(data)-blockSize; i-- {
if data[i] == 0x80 {
break
}
}
return data[:i]
}

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/hardware-wallet-go/hexutils"
"github.com/stretchr/testify/assert"
)
@ -18,3 +19,68 @@ func TestECDH(t *testing.T) {
assert.Equal(t, sharedSecret1, sharedSecret2)
}
func TestDeriveSessionKeys(t *testing.T) {
secret := hexutils.HexToBytes("B410E816DA313545151807E25A830201FA389913A977066AB0C6DE0E8631E400")
pairingKey := hexutils.HexToBytes("544FF0B9B0737E4BFC4ECDFCE09F522B837051BBE4FFCEC494FA420D8525670E")
cardData := hexutils.HexToBytes("1D7C033E75E10EC578AB538F69F1B02538571BA3831441F1649E3F24B5B3E3E71D7BC2D6A3D02FC8CB2FBB3FD8711BB5")
encKey, macKey, iv := DeriveSessionKeys(secret, pairingKey, cardData)
expectedIV := "1D7BC2D6A3D02FC8CB2FBB3FD8711BB5"
expectedEncKey := "4FF496554C01BAE0A52323E3481B448C99D43982118D95C6918FE0354D224B90"
expectedMacKey := "185811013138EA1B4FFDBBFA7343EF2DBE3E54C2C231885E867F792448AC2FE5"
assert.Equal(t, expectedIV, hexutils.BytesToHex(iv))
assert.Equal(t, expectedEncKey, hexutils.BytesToHex(encKey))
assert.Equal(t, expectedMacKey, hexutils.BytesToHex(macKey))
}
func TestEncryptData(t *testing.T) {
data := hexutils.HexToBytes("A8A686D0E3290459BCB36088A8FD04A76BF13283BE4B1EAE2E1248EF609F94DC")
encKey := hexutils.HexToBytes("44D689AB4B18206F7EEE5439FB9A71A8A617406BA5259728D1EBC2786D24896C")
iv := hexutils.HexToBytes("9D3EF41EF1D221DD98A54AD5470F58F2")
encryptedData, err := EncryptData(data, encKey, iv)
assert.NoError(t, err)
expected := "FFB41FED5F71A2B57A6AE62D5D5ECD1C12616F6464637DD0A7A930920ACBA55867A7E12CC4F06B089AF34FF4ED4BAB08"
assert.Equal(t, expected, hexutils.BytesToHex(encryptedData))
}
func TestDecryptData(t *testing.T) {
encData := hexutils.HexToBytes("73B58B66372E3446E14A9F54BA59666DB432E9DD87D24F9B0525180EE52DA2106E0C70EED7CD42B5B313E4443D6AC90D")
encKey := hexutils.HexToBytes("D93D8E6164196D5C5B5F84F10E4B90D98F8D282ED145513ED666AA55C9871E79")
iv := hexutils.HexToBytes("F959B1220333046D3C47D61B1E1B891B")
data, err := DecryptData(encData, encKey, iv)
assert.NoError(t, err)
expected := "2E21F9F2B2C2CC9038D518A5C6B490613E7955BD19D19108B77786986B7ABFE69000"
assert.Equal(t, expected, hexutils.BytesToHex(data))
}
func TestRemovePadding(t *testing.T) {
scenarios := []struct {
data string
expected string
}{
{
"0180000000000000",
"01",
},
{
"0102800000000000",
"0102",
},
{
"01020304050607080102030405800000",
"01020304050607080102030405",
},
}
for _, s := range scenarios {
res := removePadding(8, hexutils.HexToBytes(s.data))
assert.Equal(t, s.expected, hexutils.BytesToHex(res))
}
}

View File

@ -1,7 +1,9 @@
package lightwallet
import (
"bytes"
"crypto/ecdsa"
"errors"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/hardware-wallet-go/apdu"
@ -9,6 +11,8 @@ import (
"github.com/status-im/hardware-wallet-go/lightwallet/crypto"
)
var ErrInvalidResponseMAC = errors.New("invalid response MAC")
type SecureChannel struct {
c globalplatform.Channel
secret []byte
@ -57,7 +61,52 @@ func (sc *SecureChannel) RawPublicKey() []byte {
}
func (sc *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) {
return sc.c.Send(cmd)
encData, err := crypto.EncryptData(cmd.Data, sc.encKey, sc.iv)
if err != nil {
return nil, err
}
meta := []byte{cmd.Cla, cmd.Ins, cmd.P1, cmd.P2, byte(len(encData) + 16), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
if err = sc.updateIV(meta, encData); err != nil {
return nil, err
}
newData := append(sc.iv, encData...)
cmd.Data = newData
resp, err := sc.c.Send(cmd)
if err != nil {
return nil, err
}
if resp.Sw != globalplatform.SwOK {
return nil, apdu.NewErrBadResponse(resp.Sw, "unexpected sw in secure channel")
}
rmeta := []byte{byte(len(resp.Data)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
rmac := resp.Data[:len(sc.iv)]
rdata := resp.Data[len(sc.iv):]
plainData, err := crypto.DecryptData(rdata, sc.encKey, sc.iv)
if err = sc.updateIV(rmeta, rdata); err != nil {
return nil, err
}
if !bytes.Equal(sc.iv, rmac) {
return nil, ErrInvalidResponseMAC
}
return apdu.ParseResponse(plainData)
}
func (sc *SecureChannel) updateIV(meta, data []byte) error {
mac, err := crypto.CalculateMac(meta, data, sc.macKey)
if err != nil {
return err
}
sc.iv = mac
return nil
}
func (sc *SecureChannel) OneShotEncrypt(secrets *Secrets) ([]byte, error) {

View File

@ -0,0 +1,39 @@
package lightwallet
import (
"testing"
"github.com/status-im/hardware-wallet-go/apdu"
"github.com/status-im/hardware-wallet-go/hexutils"
"github.com/stretchr/testify/assert"
)
type fakeChannel struct {
lastCmd *apdu.Command
}
func (fc *fakeChannel) Send(cmd *apdu.Command) (*apdu.Response, error) {
fc.lastCmd = cmd
return nil, nil
}
func TestSecureChannel_Send(t *testing.T) {
c := &fakeChannel{}
sc := &SecureChannel{
c: c,
encKey: hexutils.HexToBytes("FDBCB1637597CF3F8F5E8263007D4E45F64C12D44066D4576EB1443D60AEF441"),
macKey: hexutils.HexToBytes("2FB70219E6635EE0958AB3F7A428BA87E8CD6E6F873A5725A55F25B102D0F1F7"),
iv: hexutils.HexToBytes("627E64358FA9BDCDAD4442BD8006E0A5"),
}
data := hexutils.HexToBytes("D545A5E95963B6BCED86A6AE826D34C5E06AC64A1217EFFA1415A96674A82500")
cmd := NewCommandMutuallyAuthenticate(data)
sc.Send(cmd)
expectedData := "BA796BF8FAD1FD50407B87127B94F5023EF8903AE926EAD8A204F961B8A0EDAEE7CCCFE7F7F6380CE2C6F188E598E4468B7DEDD0E807C18CCBDA71A55F3E1F9A"
assert.Equal(t, expectedData, hexutils.BytesToHex(c.lastCmd.Data))
expectedIV := "BA796BF8FAD1FD50407B87127B94F502"
assert.Equal(t, expectedIV, hexutils.BytesToHex(sc.iv))
}