Skip to content

Commit

Permalink
Merge branch 'eth-eip712'
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Jun 3, 2022
2 parents 30e115a + a57bf5a commit 9c5dd0d
Show file tree
Hide file tree
Showing 5 changed files with 1,203 additions and 100 deletions.
328 changes: 328 additions & 0 deletions api/firmware/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
package firmware

import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"math/big"
"strconv"
"strings"

"github.com/digitalbitbox/bitbox02-api-go/api/firmware/messages"
"github.com/digitalbitbox/bitbox02-api-go/util/errp"
Expand Down Expand Up @@ -272,3 +277,326 @@ func (device *Device) ETHSignMessage(

return signature, nil
}

func parseType(typ string, types map[string]interface{}) (*messages.ETHSignTypedMessageRequest_MemberType, error) {
if strings.HasSuffix(typ, "]") {
index := strings.LastIndexByte(typ, '[')
typ = typ[:len(typ)-1]
rest, size := typ[:index], typ[index+1:]
var sizeInt uint32
if size != "" {
i, err := strconv.ParseUint(size, 10, 32)
if err != nil {
return nil, errp.WithStack(err)
}
sizeInt = uint32(i)
}
arrayType, err := parseType(rest, types)
if err != nil {
return nil, err
}
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_ARRAY,
Size: sizeInt,
ArrayType: arrayType,
}, nil
}
if strings.HasPrefix(typ, "bytes") {
size := typ[5:]
var sizeInt uint32
if size != "" {
i, err := strconv.ParseUint(size, 10, 32)
if err != nil {
return nil, errp.WithStack(err)
}
sizeInt = uint32(i)
}
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_BYTES,
Size: sizeInt,
}, nil
}

if strings.HasPrefix(typ, "uint") {
size := typ[4:]
if size == "" {
return nil, errp.New("uint must be sized")
}
sizeInt, err := strconv.ParseUint(size, 10, 32)
if err != nil {
return nil, errp.WithStack(err)
}
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_UINT,
Size: uint32(sizeInt) / 8,
}, nil
}
if strings.HasPrefix(typ, "int") {
size := typ[3:]
if size == "" {
return nil, errp.New("int must be sized")
}
sizeInt, err := strconv.ParseUint(size, 10, 32)
if err != nil {
return nil, errp.WithStack(err)
}
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_INT,
Size: uint32(sizeInt) / 8,
}, nil
}
if typ == "bool" {
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_BOOL,
}, nil
}
if typ == "address" {
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_ADDRESS,
}, nil
}
if typ == "string" {
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_STRING,
}, nil
}
if _, ok := types[typ]; ok {
return &messages.ETHSignTypedMessageRequest_MemberType{
Type: messages.ETHSignTypedMessageRequest_STRUCT,
StructName: typ,
}, nil
}
return nil, errp.Newf("Can't recognize type: %s", typ)
}

// Golang's stdlib doesn't support serializing signed integers in big endian (two's complement).
// -x = ~x+1.
func bigendianInt(integer *big.Int) []byte {
if integer.Sign() >= 0 {
return integer.Bytes()
}
bytes := append([]byte{0}, integer.Bytes()...)
for i, v := range bytes {
bytes[i] = ^v
}
return new(big.Int).Add(new(big.Int).SetBytes(bytes), big.NewInt(1)).Bytes()
}

// encodeValue encodes a json decoded typed data value to send to the BitBox02 as part of the
// SignTypedData signing process. There is no strict error checking (e.g. that the size is correct
// according to the type) as the BitBox02 checks for bad input.
func encodeValue(typ *messages.ETHSignTypedMessageRequest_MemberType, value interface{}) ([]byte, error) {
switch typ.Type {
case messages.ETHSignTypedMessageRequest_BYTES:
v := value.(string)
if len(v) >= 2 && v[:2] == "0x" {
return hex.DecodeString(v[2:])
}
return []byte(v), nil
case messages.ETHSignTypedMessageRequest_UINT:
bigint := new(big.Int)
switch v := value.(type) {
case string:
_, ok := bigint.SetString(v, 10)
if !ok {
return nil, errp.Newf("couldn't parse uint: %s", v)
}
case float64:
v64 := uint64(v)
if float64(v64) != v {
return nil, errp.Newf("float64 is not an uint: %v", v)
}
bigint.SetUint64(v64)
default:
return nil, errp.New("wrong type for uint")
}
return bigint.Bytes(), nil
case messages.ETHSignTypedMessageRequest_INT:
bigint := new(big.Int)
switch v := value.(type) {
case string:
_, ok := bigint.SetString(v, 10)
if !ok {
return nil, errp.Newf("couldn't parse uint: %s", v)
}
case float64:
v64 := int64(v)
if float64(v64) != v {
return nil, errp.Newf("float64 is not an uint: %v", v)
}
bigint.SetInt64(v64)
default:
return nil, errp.New("wrong type for uint")
}
return bigendianInt(bigint), nil
case messages.ETHSignTypedMessageRequest_BOOL:
if value.(bool) {
return []byte{1}, nil
}
return []byte{0}, nil
case messages.ETHSignTypedMessageRequest_ADDRESS, messages.ETHSignTypedMessageRequest_STRING:
return []byte(value.(string)), nil
case messages.ETHSignTypedMessageRequest_ARRAY:
size := uint32(len(value.([]interface{})))
result := make([]byte, 4)
binary.BigEndian.PutUint32(result, size)
return result, nil
}

return nil, errp.New("couldn't encode value")
}

func getValue(what *messages.ETHTypedMessageValueResponse, msg map[string]interface{}) ([]byte, error) {
types := msg["types"].(map[string]interface{})

var value interface{}
var typ *messages.ETHSignTypedMessageRequest_MemberType

switch what.RootObject {
case messages.ETHTypedMessageValueResponse_DOMAIN:
value = msg["domain"]
var err error
typ, err = parseType("EIP712Domain", types)
if err != nil {
return nil, err
}
case messages.ETHTypedMessageValueResponse_MESSAGE:
value = msg["message"]
var err error
typ, err = parseType(msg["primaryType"].(string), types)
if err != nil {
return nil, err
}
default:
return nil, errp.Newf("unknown root: %v", what.RootObject)
}
for _, element := range what.Path {
switch typ.Type {
case messages.ETHSignTypedMessageRequest_STRUCT:
structMember := types[typ.StructName].([]interface{})[element].(map[string]interface{})
value = value.(map[string]interface{})[structMember["name"].(string)]
var err error
typ, err = parseType(structMember["type"].(string), types)
if err != nil {
return nil, err
}
case messages.ETHSignTypedMessageRequest_ARRAY:
value = value.([]interface{})[element]
typ = typ.ArrayType
default:
return nil, errp.New("path element does not point to struct or array")
}
}
return encodeValue(typ, value)
}

// ETHSignTypedMessage signs an Ethereum EIP-612 typed message. 27 is added to the recID to denote
// an uncompressed pubkey.
func (device *Device) ETHSignTypedMessage(
chainID uint64,
keypath []uint32,
jsonMsg []byte,
) ([]byte, error) {
if !device.version.AtLeast(semver.NewSemVer(9, 12, 0)) {
return nil, UnsupportedError("9.12.0")
}

var msg map[string]interface{}
if err := json.Unmarshal(jsonMsg, &msg); err != nil {
return nil, errp.WithStack(err)
}

hostNonce, err := generateHostNonce()
if err != nil {
return nil, err
}

types := msg["types"].(map[string]interface{})
var parsedTypes []*messages.ETHSignTypedMessageRequest_StructType
for key, value := range types {
var members []*messages.ETHSignTypedMessageRequest_Member
for _, member := range value.([]interface{}) {
memberS := member.(map[string]interface{})
parsedType, err := parseType(memberS["type"].(string), types)
if err != nil {
return nil, err
}
members = append(members, &messages.ETHSignTypedMessageRequest_Member{
Name: memberS["name"].(string),
Type: parsedType,
})
}
parsedTypes = append(parsedTypes, &messages.ETHSignTypedMessageRequest_StructType{
Name: key,
Members: members,
})
}
request := &messages.ETHRequest{
Request: &messages.ETHRequest_SignTypedMsg{
SignTypedMsg: &messages.ETHSignTypedMessageRequest{
ChainId: chainID,
Keypath: keypath,
Types: parsedTypes,
PrimaryType: msg["primaryType"].(string),
HostNonceCommitment: &messages.AntiKleptoHostNonceCommitment{
Commitment: antikleptoHostCommit(hostNonce),
},
},
},
}
response, err := device.queryETH(request)
if err != nil {
return nil, err
}

typedMsgValueResponse, ok := response.Response.(*messages.ETHResponse_TypedMsgValue)
for ok {
value, err := getValue(typedMsgValueResponse.TypedMsgValue, msg)
if err != nil {
return nil, err
}
response, err = device.queryETH(&messages.ETHRequest{
Request: &messages.ETHRequest_TypedMsgValue{
TypedMsgValue: &messages.ETHTypedMessageValueRequest{
Value: value,
},
},
})
if err != nil {
return nil, err
}
typedMsgValueResponse, ok = response.Response.(*messages.ETHResponse_TypedMsgValue)
}

signerCommitment, ok := response.Response.(*messages.ETHResponse_AntikleptoSignerCommitment)
if !ok {
return nil, errp.New("unexpected response")
}
response, err = device.queryETH(&messages.ETHRequest{
Request: &messages.ETHRequest_AntikleptoSignature{
AntikleptoSignature: &messages.AntiKleptoSignatureRequest{
HostNonce: hostNonce,
},
},
})
if err != nil {
return nil, err
}

signResponse, ok := response.Response.(*messages.ETHResponse_Sign)
if !ok {
return nil, errp.New("unexpected response")
}
signature := signResponse.Sign.Signature
err = antikleptoVerify(
hostNonce,
signerCommitment.AntikleptoSignerCommitment.Commitment,
signature[:64],
)
if err != nil {
return nil, err
}
// 27 is the magic constant to add to the recoverable ID to denote an uncompressed pubkey.
signature[64] += 27
return signature, nil
}

0 comments on commit 9c5dd0d

Please sign in to comment.