add support for metadata

This commit is contained in:
Michele Balistreri 2022-08-02 12:18:52 +02:00
parent b0e0482ba9
commit e9b2ca2eac
11 changed files with 335 additions and 41 deletions

View File

@ -13,17 +13,17 @@ If you only need a tool to initialize your card, check out [keycard-cli](https:/
- [x] PAIR
- [x] UNPAIR
- [x] GET STATUS
- [ ] SET NDEF
- [x] STORE DATA
- [x] GET DATA
- [x] VERIFY PIN
- [x] CHANGE PIN
- [ ] UNBLOCK PIN
- [ ] LOAD KEY
- [x] UNBLOCK PIN
- [x] LOAD KEY
- [x] DERIVE KEY
- [x] GENERATE MNEMONIC
- [x] REMOVE KEY
- [x] GENERATE KEY
- [x] INIT
- [ ] DUPLICATE KEY
- [x] SIGN
- [ ] SET PINLESS PATH
- [x] EXPORT KEY

View File

@ -50,7 +50,7 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) {
)
for {
tag, buf, err = parseTag(buf)
tag, err = parseTag(buf)
switch {
case err == io.EOF:
return []byte{}, &ErrTagNotFound{target}
@ -58,7 +58,7 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) {
return nil, err
}
length, buf, err = parseLength(buf)
length, err = ParseLength(buf)
if err != nil {
return nil, err
}
@ -87,59 +87,83 @@ func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) {
}
}
func parseLength(buf *bytes.Buffer) (uint32, *bytes.Buffer, error) {
func ParseLength(buf *bytes.Buffer) (uint32, error) {
length, err := buf.ReadByte()
if err != nil {
return 0, nil, err
return 0, err
}
if length == 0x80 {
return 0, nil, ErrUnsupportedLenth80
return 0, ErrUnsupportedLenth80
}
if length > 0x80 {
lengthSize := length - 0x80
if lengthSize > 3 {
return 0, nil, ErrLengthTooBig
return 0, ErrLengthTooBig
}
data := make([]byte, lengthSize)
_, err = buf.Read(data)
if err != nil {
return 0, nil, err
return 0, err
}
num := make([]byte, 4)
copy(num[4-lengthSize:], data)
return binary.BigEndian.Uint32(num), buf, nil
return binary.BigEndian.Uint32(num), nil
}
return uint32(length), buf, nil
return uint32(length), nil
}
func parseTag(buf *bytes.Buffer) (Tag, *bytes.Buffer, error) {
func WriteLength(buf *bytes.Buffer, length uint32) {
if length < 0x80 {
buf.WriteByte(byte(length))
} else if length < 0x100 {
buf.WriteByte(0x81)
buf.WriteByte(byte(length))
} else if length < 0x10000 {
buf.WriteByte(0x82)
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length))
} else if length < 0x1000000 {
buf.WriteByte(0x83)
buf.WriteByte(byte(length >> 16))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length))
} else {
buf.WriteByte(0x84)
buf.WriteByte(byte(length >> 24))
buf.WriteByte(byte(length >> 16))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length))
}
}
func parseTag(buf *bytes.Buffer) (Tag, error) {
tag := make(Tag, 0)
b, err := buf.ReadByte()
if err != nil {
return nil, nil, err
return nil, err
}
tag = append(tag, b)
if b&0x1F != 0x1F {
return tag, buf, nil
return tag, nil
}
for {
b, err = buf.ReadByte()
if err != nil {
return nil, nil, err
return nil, err
}
tag = append(tag, b)
if b&0x80 != 0x80 {
return tag, buf, nil
return tag, nil
}
}
}

View File

@ -89,7 +89,7 @@ func TestParseLength(t *testing.T) {
for _, s := range scenarios {
buf := bytes.NewBuffer(s.data)
length, _, err := parseLength(buf)
length, err := ParseLength(buf)
if s.err == nil {
assert.NoError(t, err)
assert.Equal(t, s.expectedLength, length)
@ -128,7 +128,7 @@ func TestParseTag(t *testing.T) {
for _, s := range scenarios {
buf := bytes.NewBuffer(s.rawTag)
tag, _, err := parseTag(buf)
tag, err := parseTag(buf)
require.Nil(t, err)
assert.Equal(t, s.expectedTag, tag)
}

View File

@ -301,15 +301,15 @@ func (cs *CommandSet) DeriveKey(path string) error {
func (cs *CommandSet) ExportKey(derive bool, makeCurrent bool, onlyPublic bool, path string) ([]byte, []byte, error) {
var p1 uint8
if derive == false {
if !derive {
p1 = P1ExportKeyCurrent
} else if makeCurrent == false {
} else if !makeCurrent {
p1 = P1ExportKeyDerive
} else {
p1 = P1ExportKeyDeriveAndMakeCurrent
}
var p2 uint8
if onlyPublic == true {
if onlyPublic {
p2 = P2ExportKeyPublicOnly
} else {
p2 = P2ExportKeyPrivateAndPublic
@ -391,6 +391,22 @@ func (cs *CommandSet) LoadSeed(seed []byte) ([]byte, error) {
return resp.Data, nil
}
func (cs *CommandSet) GetData(typ uint8) ([]byte, error) {
cmd := NewCommandGetData(typ)
resp, err := cs.sc.Send(cmd)
if err = cs.checkOK(resp, err); err != nil {
return nil, err
}
return resp.Data, nil
}
func (cs *CommandSet) StoreData(typ uint8, data []byte) error {
cmd := NewCommandStoreData(typ, data)
resp, err := cs.sc.Send(cmd)
return cs.checkOK(resp, err)
}
func (cs *CommandSet) mutualAuthenticate() error {
data := make([]byte, 32)
if _, err := rand.Read(data); err != nil {

View File

@ -26,8 +26,10 @@ const (
InsExportKey = 0xC2
InsSign = 0xC0
InsSetPinlessPath = 0xC1
InsGetData = 0xCA
InsLoadKey = 0xD0
InsGenerateMnemonic = 0xD2
InsStoreData = 0xE2
P1PairingFirstStep = 0x00
P1PairingFinalStep = 0x01
@ -43,6 +45,9 @@ const (
P1SignDerive = 0x01
P1SignDeriveAndMakeCurrent = 0x02
P1SignPinless = 0x03
P1StoreDataPublic = 0x00
P1StoreDataNDEF = 0x01
P1StoreDataCash = 0x02
P1ExportKeyCurrent = 0x00
P1ExportKeyDerive = 0x01
P1ExportKeyDeriveAndMakeCurrent = 0x02
@ -333,6 +338,26 @@ func NewCommandSign(data []byte, p1 uint8, pathStr string) (*apdu.Command, error
), nil
}
func NewCommandGetData(typ uint8) *apdu.Command {
return apdu.NewCommand(
globalplatform.ClaGp,
InsGetData,
typ,
0,
[]byte{},
)
}
func NewCommandStoreData(typ uint8, data []byte) *apdu.Command {
return apdu.NewCommand(
globalplatform.ClaGp,
InsStoreData,
typ,
0,
data,
)
}
// Internal function. Get the type of starting point for the derivation path.
// Used for both DeriveKey and ExportKey
func derivationP1FromStartingPoint(s derivationpath.StartingPoint) (uint8, error) {

View File

@ -73,8 +73,6 @@ func (d *decoder) parse() (StartingPoint, []uint32, error) {
return d.start, d.path, err
}
}
return d.start, d.path, nil
}
func (d *decoder) readByte() (byte, error) {
@ -168,7 +166,7 @@ func (d *decoder) parseSeparator() error {
return d.saveSegment()
}
return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b))
return fmt.Errorf("expected %c, got %c", tokenSeparator, b)
}
func (d *decoder) parseSegment() error {

View File

@ -8,20 +8,20 @@ import (
)
func Encode(rawPath []uint32) string {
segments := []string{string(tokenMaster)}
segments := []string{string(rune(tokenMaster))}
for _, i := range rawPath {
suffix := ""
if i >= hardenedStart {
i = i - hardenedStart
suffix = string(tokenHardened)
suffix = string(rune(tokenHardened))
}
segments = append(segments, fmt.Sprintf("%d%s", i, suffix))
}
return strings.Join(segments, string(tokenSeparator))
return strings.Join(segments, string(rune(tokenSeparator)))
}
func EncodeFromBytes(data []byte) (string, error) {

View File

@ -56,7 +56,7 @@ func TestNewCommandExternalAuthenticate(t *testing.T) {
func TestNewCommandDelete(t *testing.T) {
aid := hexutils.HexToBytes("0102030405")
cmd := NewCommandDelete(aid)
cmd := NewCommandDelete(aid, P2DeleteObject)
assert.Equal(t, uint8(0x80), cmd.Cla)
assert.Equal(t, uint8(0xE4), cmd.Ins)
assert.Equal(t, uint8(0x00), cmd.P1)

View File

@ -96,19 +96,29 @@ func (sc *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) {
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
}
var plainData []byte
if !bytes.Equal(sc.iv, rmac) {
return nil, ErrInvalidResponseMAC
}
if sc.open {
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):]
logger.Debug("apdu response decrypted", "hex", hexutils.BytesToHexWithSpaces(plainData))
if plainData, err = crypto.DecryptData(rdata, sc.encKey, sc.iv); err != nil {
return nil, err
}
if err = sc.updateIV(rmeta, rdata); err != nil {
return nil, err
}
if !bytes.Equal(sc.iv, rmac) {
return nil, ErrInvalidResponseMAC
}
logger.Debug("apdu response decrypted", "hex", hexutils.BytesToHexWithSpaces(plainData))
} else {
plainData = resp.Data
}
return apdu.ParseResponse(plainData)
}

201
types/metadata.go Normal file
View File

@ -0,0 +1,201 @@
package types
import (
"bytes"
"container/list"
"errors"
"io"
"github.com/status-im/keycard-go/apdu"
)
type Metadata struct {
name string
paths *list.List
}
func EmptyMetadata() *Metadata {
return &Metadata{"", list.New()}
}
func NewMetadata(name string, paths []uint32) (*Metadata, error) {
m := EmptyMetadata()
if err := m.SetName(name); err != nil {
return nil, err
}
for i := 0; i < len(paths); i++ {
m.AddPath(paths[i])
}
return m, nil
}
func ParseMetadata(data []byte) (*Metadata, error) {
buf := bytes.NewBuffer(data)
header, err := buf.ReadByte()
if err != nil {
return nil, err
}
version := header >> 5
if version != 1 {
return nil, errors.New("invalid version")
}
namelen := int(header & 0x1f)
cardName := string(buf.Next(namelen))
list := list.New()
for {
start, err := apdu.ParseLength(buf)
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
count, err := apdu.ParseLength(buf)
if err != nil {
return nil, err
}
for i := start; i <= (start + count); i++ {
insertOrderedNoDups(list, i)
}
}
return &Metadata{cardName, list}, nil
}
func insertOrderedNoDups(list *list.List, num uint32) {
le := list.Back()
for le != nil {
val := le.Value.(uint32)
if num > val {
break
} else if num == val {
return
}
le = le.Prev()
}
if le == nil {
list.PushFront(num)
} else {
list.InsertAfter(num, le)
}
}
func (m *Metadata) Name() string {
return m.name
}
func (m *Metadata) SetName(name string) error {
if len(name) > 20 {
return errors.New("name longer than 20 chars")
}
m.name = name
return nil
}
func (m *Metadata) Paths() []uint32 {
listlen := m.paths.Len()
paths := make([]uint32, listlen)
e := m.paths.Front()
for i := 0; i < listlen; i++ {
paths[i] = e.Value.(uint32)
e = e.Next()
}
return paths
}
func (m *Metadata) AddPath(path uint32) {
insertOrderedNoDups(m.paths, path)
}
func (m *Metadata) RemovePath(path uint32) {
for le := m.paths.Front(); le != nil; le = le.Next() {
if path == le.Value.(uint32) {
m.paths.Remove(le)
return
}
}
}
func (m *Metadata) Serialize() []byte {
buf := new(bytes.Buffer)
buf.WriteByte(0x20 | byte(len(m.name)))
buf.WriteString(m.name)
le := m.paths.Front()
if le == nil {
return buf.Bytes()
}
start := le.Value.(uint32)
len := uint32(0)
for le = le.Next(); le != nil; le = le.Next() {
w := le.Value.(uint32)
if w == (start + len + 1) {
len++
} else {
apdu.WriteLength(buf, start)
apdu.WriteLength(buf, len)
start = w
len = 0
}
}
apdu.WriteLength(buf, start)
apdu.WriteLength(buf, len)
return buf.Bytes()
}
/*
public byte[] toByteArray() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] name = this.cardName.getBytes(Charset.forName("US-ASCII"));
os.write(0x20 | name.length);
os.write(name, 0, name.length);
if (wallets.isEmpty()) {
return os.toByteArray();
}
long start = wallets.first();
int len = 0;
for (Long w : wallets.tailSet(start + 1)) {
if (w == (start + len + 1)) {
len++;
} else {
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
len = 0;
start = w;
}
}
TinyBERTLV.writeNum(os, (int) start);
TinyBERTLV.writeNum(os, len);
return os.toByteArray();
}
*/

20
types/metadata_test.go Normal file
View File

@ -0,0 +1,20 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseMetadata(t *testing.T) {
m, err := ParseMetadata([]byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01})
assert.NoError(t, err)
assert.Equal(t, "123", m.Name())
assert.Equal(t, []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29}, m.Paths())
}
func TestSerialize(t *testing.T) {
m, err := NewMetadata("123", []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29})
assert.NoError(t, err)
assert.Equal(t, []byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01}, m.Serialize())
}