From 0d3f4e886cf771c0d45550d7eec32b5b02ef9603 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Thu, 17 Aug 2023 10:58:58 +0200 Subject: [PATCH] Keycard 3.1 support (#16) * add certificate validation * add factory reset command * do not apply secure channel to factory reset --- command_set.go | 22 +++++++++++ commands.go | 30 +++++++++++++- types/application_info.go | 8 +++- types/certificate.go | 82 +++++++++++++++++++++++++++++++++++++++ types/signature.go | 77 ++++++++++++++++++++++++++++-------- 5 files changed, 200 insertions(+), 19 deletions(-) create mode 100644 types/certificate.go diff --git a/command_set.go b/command_set.go index 7d5d2e5..095d9c6 100644 --- a/command_set.go +++ b/command_set.go @@ -154,6 +154,22 @@ func (cs *CommandSet) Unpair(index uint8) error { return cs.checkOK(resp, err) } +func (cs *CommandSet) Identify() ([]byte, error) { + challenge := make([]byte, 32) + if _, err := rand.Read(challenge); err != nil { + return nil, err + } + + cmd := NewCommandIdentify(challenge) + resp, err := cs.sc.Send(cmd) + + if err = cs.checkOK(resp, err); err != nil { + return nil, err + } + + return types.VerifyIdentity(challenge, resp.Data) +} + func (cs *CommandSet) OpenSecureChannel() error { if cs.ApplicationInfo == nil { return errors.New("cannot open secure channel without setting PairingInfo") @@ -407,6 +423,12 @@ func (cs *CommandSet) StoreData(typ uint8, data []byte) error { return cs.checkOK(resp, err) } +func (cs *CommandSet) FactoryReset() error { + cmd := NewCommandFactoryReset() + resp, err := cs.c.Send(cmd) + return cs.checkOK(resp, err) +} + func (cs *CommandSet) mutualAuthenticate() error { data := make([]byte, 32) if _, err := rand.Read(data); err != nil { diff --git a/commands.go b/commands.go index c986f9e..bd9e027 100644 --- a/commands.go +++ b/commands.go @@ -12,10 +12,12 @@ import ( const ( InsInit = 0xFE + InsFactoryReset = 0xFD InsOpenSecureChannel = 0x10 InsMutuallyAuthenticate = 0x11 InsPair = 0x12 InsUnpair = 0x13 + InsIdentify = 0x14 InsGetStatus = 0xF2 InsGenerateKey = 0xD4 InsRemoveKey = 0xD3 @@ -53,7 +55,10 @@ const ( P1ExportKeyDeriveAndMakeCurrent = 0x02 P2ExportKeyPrivateAndPublic = 0x00 P2ExportKeyPublicOnly = 0x01 + P2ExportKeyExtendedPublic = 0x02 P1LoadKeySeed = 0x03 + P1FactoryResetMagic = 0xAA + P2FactoryResetMagic = 0x55 SwNoAvailablePairingSlots = 0x6A84 ) @@ -98,6 +103,16 @@ func NewCommandUnpair(index uint8) *apdu.Command { ) } +func NewCommandIdentify(challenge []byte) *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsIdentify, + 0, + 0, + challenge, + ) +} + func NewCommandOpenSecureChannel(pairingIndex uint8, pubKey []byte) *apdu.Command { return apdu.NewCommand( globalplatform.ClaGp, @@ -247,13 +262,14 @@ func NewCommandDeriveKey(pathStr string) (*apdu.Command, error) { // Export a key // -// @param {p1} +// @param {p1} // 0x00: current key - returns the key that is currently loaded and ready for signing. Does not use derivation path // 0x01: derive - returns derived key // 0x02: derive and make current - returns derived key and also sets it to the current key // @param {p2} // 0x00: return public and private key pair // 0x01: return only the public key +// 0x02: return extended public key // @param {pathStr} // Derivation path of format "m/x/x/x/x/x", e.g. "m/44'/0'/0'/0/0" func NewCommandExportKey(p1 uint8, p2 uint8, pathStr string) (*apdu.Command, error) { @@ -334,7 +350,7 @@ func NewCommandSign(data []byte, p1 uint8, pathStr string) (*apdu.Command, error globalplatform.ClaGp, InsSign, p1, - 0, + 1, data, ), nil } @@ -359,6 +375,16 @@ func NewCommandStoreData(typ uint8, data []byte) *apdu.Command { ) } +func NewCommandFactoryReset() *apdu.Command { + return apdu.NewCommand( + globalplatform.ClaGp, + InsFactoryReset, + P1FactoryResetMagic, + P2FactoryResetMagic, + []byte{}, + ) +} + // 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) { diff --git a/types/application_info.go b/types/application_info.go index 7eed222..4a3cc26 100644 --- a/types/application_info.go +++ b/types/application_info.go @@ -22,11 +22,13 @@ const ( CapabilityKeyManagement CapabilityCredentialsManagement CapabilityNDEF + CapabilityFactoryReset CapabilityAll = CapabilitySecureChannel | CapabilityKeyManagement | CapabilityCredentialsManagement | - CapabilityNDEF + CapabilityNDEF | + CapabilityFactoryReset ) type ApplicationInfo struct { @@ -62,6 +64,10 @@ func (a *ApplicationInfo) HasNDEFCapability() bool { return a.HasCapability(CapabilityNDEF) } +func (a *ApplicationInfo) HasFactoryResetCapability() bool { + return a.HasCapability(CapabilityFactoryReset) +} + func ParseApplicationInfo(data []byte) (*ApplicationInfo, error) { info := &ApplicationInfo{ Installed: true, diff --git a/types/certificate.go b/types/certificate.go new file mode 100644 index 0000000..8e3f8e6 --- /dev/null +++ b/types/certificate.go @@ -0,0 +1,82 @@ +package types + +import ( + "crypto/sha256" + "errors" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/keycard-go/apdu" +) + +type Certificate struct { + identPub []byte + signature *Signature +} + +var ( + TagCertificate = uint8(0x8A) +) + +func ParseCertificate(data []byte) (*Certificate, error) { + if len(data) != 98 { + return nil, errors.New("certificate must be 98 byte long") + } + + identPub := data[0:33] + sigData := data[33:97] + msg := sha256.Sum256(identPub) + + sig, err := ParseRecoverableSignature(msg[:], sigData) + if err != nil { + return nil, err + } + + return &Certificate{ + identPub: identPub, + signature: sig, + }, nil +} + +func VerifyIdentity(challenge []byte, tlvData []byte) ([]byte, error) { + template, err := apdu.FindTag(tlvData, apdu.Tag{TagSignatureTemplate}) + if err != nil { + return nil, err + } + + certData, err := apdu.FindTag(template, apdu.Tag{TagCertificate}) + if err != nil { + return nil, err + } + + cert, err := ParseCertificate(certData) + if err != nil { + return nil, err + } + + r, s, err := DERSignatureToRS(template) + if err != nil { + return nil, err + } + + sig := append(r, s...) + + if !crypto.VerifySignature(cert.identPub, challenge, sig) { + return nil, errors.New("invalid signature") + } + + return compressPublicKey(cert.signature.pubKey), nil +} + +func compressPublicKey(pubKey []byte) []byte { + if len(pubKey) == 33 { + return pubKey + } + + if (pubKey[63] & 1) == 1 { + pubKey[0] = 3 + } else { + pubKey[0] = 2 + } + + return pubKey[0:33] +} diff --git a/types/signature.go b/types/signature.go index 0e6c72f..82a71d6 100644 --- a/types/signature.go +++ b/types/signature.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "errors" "github.com/ethereum/go-ethereum/crypto" "github.com/status-im/keycard-go/apdu" @@ -9,6 +10,7 @@ import ( var ( TagSignatureTemplate = uint8(0xA0) + TagRawSignature = uint8(0x80) ) type Signature struct { @@ -19,40 +21,59 @@ type Signature struct { } func ParseSignature(message, resp []byte) (*Signature, error) { - pubKey, err := apdu.FindTag(resp, apdu.Tag{TagSignatureTemplate}, apdu.Tag{0x80}) + // check for old template first because TagRawSignature matches the pubkey tag + template, err := apdu.FindTag(resp, apdu.Tag{TagSignatureTemplate}) + if err == nil { + return parseLegacySignature(message, template) + } + + sig, err := apdu.FindTag(resp, apdu.Tag{TagRawSignature}) + if err != nil { return nil, err } - r, err := apdu.FindTagN(resp, 0, apdu.Tag{TagSignatureTemplate}, apdu.Tag{0x30}, apdu.Tag{0x02}) + return ParseRecoverableSignature(message, sig) +} + +func ParseRecoverableSignature(message, sig []byte) (*Signature, error) { + if len(sig) != 65 { + return nil, errors.New("invalid signature") + } + + pubKey, err := crypto.Ecrecover(message, sig) if err != nil { return nil, err } + return &Signature{ + pubKey: pubKey, + r: sig[0:32], + s: sig[32:64], + v: sig[64], + }, nil +} + +func DERSignatureToRS(tlv []byte) ([]byte, []byte, error) { + r, err := apdu.FindTagN(tlv, 0, apdu.Tag{0x30}, apdu.Tag{0x02}) + if err != nil { + return nil, nil, err + } + if len(r) > 32 { r = r[len(r)-32:] } - s, err := apdu.FindTagN(resp, 1, apdu.Tag{TagSignatureTemplate}, apdu.Tag{0x30}, apdu.Tag{0x02}) + s, err := apdu.FindTagN(tlv, 1, apdu.Tag{0x30}, apdu.Tag{0x02}) if err != nil { - return nil, err + return nil, nil, err } if len(s) > 32 { s = s[len(s)-32:] } - v, err := calculateV(message, pubKey, r, s) - if err != nil { - return nil, err - } - - return &Signature{ - pubKey: pubKey, - r: r, - s: s, - v: v, - }, nil + return r, s, nil } func (s *Signature) PubKey() []byte { @@ -71,9 +92,33 @@ func (s *Signature) V() byte { return s.v } +func parseLegacySignature(message, template []byte) (*Signature, error) { + pubKey, err := apdu.FindTag(template, apdu.Tag{0x80}) + if err != nil { + return nil, err + } + + r, s, err := DERSignatureToRS(template) + if err != nil { + return nil, err + } + + v, err := calculateV(message, pubKey, r, s) + if err != nil { + return nil, err + } + + return &Signature{ + pubKey: pubKey, + r: r, + s: s, + v: v, + }, nil +} + func calculateV(message, pubKey, r, s []byte) (v byte, err error) { rs := append(r, s...) - for i := 0; i < 2; i++ { + for i := 0; i < 4; i++ { v = byte(i) sig := append(rs, v) rec, err := crypto.Ecrecover(message, sig)