1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 23:08:14 +00:00

ripple: simple tx serializer; signing

This supports only fraction of the whole binary format Ripple uses. This
is focused on the Payment transaction type, which is currently solely
supported.
This commit is contained in:
Tomas Susanka 2018-06-28 13:15:14 +02:00 committed by Jan Pochyla
parent b5c2ae49dd
commit 08945c48e1
6 changed files with 355 additions and 3 deletions

View File

@ -1,7 +1,7 @@
from trezor.wire import register, protobuf_workflow
from trezor.messages.wire_types import RippleGetAddress
from trezor.messages.MessageType import RippleGetAddress
from trezor.messages.MessageType import RippleSignTx
from .get_address import get_address
from .serializer import *
def dispatch_RippleGetAddress(*args, **kwargs):
@ -9,5 +9,11 @@ def dispatch_RippleGetAddress(*args, **kwargs):
return get_address(*args, **kwargs)
def dispatch_RippleSignTx(*args, **kwargs):
from .sign_tx import sign_tx
return sign_tx(*args, **kwargs)
def boot():
register(RippleGetAddress, protobuf_workflow, dispatch_RippleGetAddress)
register(RippleSignTx, protobuf_workflow, dispatch_RippleSignTx)

View File

@ -1,9 +1,25 @@
from micropython import const
from trezor.crypto.hashlib import ripemd160, sha256
from . import base58_ripple
# HASH_TX_ID = const(0x54584E00) # 'TXN'
HASH_TX_SIGN = const(0x53545800) # 'STX'
# HASH_TX_SIGN_TESTNET = const(0x73747800) # 'stx'
def address_from_public_key(pubkey: bytes):
# https://developers.ripple.com/basic-data-types.html#specifying-currency-amounts
DIVISIBILITY = const(6) # 1000000 drops equal 1 XRP
# https://developers.ripple.com/transaction-cost.html
MIN_FEE = const(10)
# max is not defined officially but we check to make sure
MAX_FEE = const(1000000) # equals 1 XRP
FLAG_FULLY_CANONICAL = 0x80000000
def address_from_public_key(pubkey: bytes) -> str:
"""Extracts public key from an address
Ripple address is in format:
@ -23,3 +39,9 @@ def address_from_public_key(pubkey: bytes):
address.append(0x00) # 'r'
address.extend(h)
return base58_ripple.encode_check(bytes(address))
def decode_address(address: str):
"""Returns so called Account ID"""
adr = base58_ripple.decode_check(address)
return adr[1:]

23
src/apps/ripple/layout.py Normal file
View File

@ -0,0 +1,23 @@
from apps.common.confirm import require_confirm, require_hold_to_confirm
from apps.common.display_address import split_address
from trezor import ui
from trezor.messages import ButtonRequestType
from trezor.ui.text import Text
from trezor.utils import format_amount
from . import helpers
async def require_confirm_fee(ctx, fee):
text = Text('Confirm fee', ui.ICON_SEND, icon_color=ui.GREEN)
text.normal('Transaction fee:')
text.bold(format_amount(fee, helpers.DIVISIBILITY) + ' XRP')
await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput)
async def require_confirm_tx(ctx, to, value):
text = Text('Confirm sending', ui.ICON_SEND, icon_color=ui.GREEN)
text.bold(format_amount(value, helpers.DIVISIBILITY) + ' XRP')
text.normal('to')
text.mono(*split_address(to))
return await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx)

View File

@ -0,0 +1,156 @@
# Serializes into the Ripple Format
#
# Inspired by https://github.com/miracle2k/ripple-python and https://github.com/ripple/ripple-lib
# Docs at https://wiki.ripple.com/Binary_Format and https://developers.ripple.com/transaction-common-fields.html
#
# The first four bits specify the field type (int16, int32, account..)
# the other four the record type (amount, fee, destination..) and then
# the actual data follow. This currently only supports the Payment
# transaction type and the fields that are required for it.
#
from trezor.messages.RippleSignTx import RippleSignTx
from . import helpers
FIELD_TYPE_INT16 = 1
FIELD_TYPE_INT32 = 2
FIELD_TYPE_AMOUNT = 6
FIELD_TYPE_VL = 7
FIELD_TYPE_ACCOUNT = 8
FIELDS_MAP = {
'account': {
'type': FIELD_TYPE_ACCOUNT,
'key': 1,
},
'amount': {
'type': FIELD_TYPE_AMOUNT,
'key': 1,
},
'destination': {
'type': FIELD_TYPE_ACCOUNT,
'key': 3,
},
'fee': {
'type': FIELD_TYPE_AMOUNT,
'key': 8,
},
'sequence': {
'type': FIELD_TYPE_INT32,
'key': 4,
},
'type': {
'type': FIELD_TYPE_INT16,
'key': 2,
},
'signingPubKey': {
'type': FIELD_TYPE_VL,
'key': 3,
},
'flags': {
'type': FIELD_TYPE_INT32,
'key': 2,
},
'txnSignature': {
'type': FIELD_TYPE_VL,
'key': 4,
},
'lastLedgerSequence': {
'type': FIELD_TYPE_INT32,
'key': 27,
},
}
TRANSACTION_TYPES = {
'Payment': 0,
}
def serialize(msg: RippleSignTx, source_address: str, pubkey=None, signature=None):
w = bytearray()
# must be sorted numerically first by type and then by name
write(w, FIELDS_MAP['type'], TRANSACTION_TYPES['Payment'])
write(w, FIELDS_MAP['flags'], msg.flags)
write(w, FIELDS_MAP['sequence'], msg.sequence)
write(w, FIELDS_MAP['lastLedgerSequence'], msg.last_ledger_sequence)
write(w, FIELDS_MAP['amount'], msg.payment.amount)
write(w, FIELDS_MAP['fee'], msg.fee)
write(w, FIELDS_MAP['signingPubKey'], pubkey)
write(w, FIELDS_MAP['txnSignature'], signature)
write(w, FIELDS_MAP['account'], source_address)
write(w, FIELDS_MAP['destination'], msg.payment.destination)
return w
def write(w: bytearray, field: dict, value):
if value is None:
return
write_type(w, field)
if field['type'] == FIELD_TYPE_INT16:
w.extend(value.to_bytes(2, 'big'))
elif field['type'] == FIELD_TYPE_INT32:
w.extend(value.to_bytes(4, 'big'))
elif field['type'] == FIELD_TYPE_AMOUNT:
w.extend(serialize_amount(value))
elif field['type'] == FIELD_TYPE_ACCOUNT:
write_bytes(w, helpers.decode_address(value))
elif field['type'] == FIELD_TYPE_VL:
write_bytes(w, value)
else:
raise ValueError('Unknown field type')
def write_type(w: bytearray, field: dict):
if field['key'] <= 0xf:
w.append((field['type'] << 4) | field['key'])
else:
# this concerns two-bytes fields such as lastLedgerSequence
w.append(field['type'] << 4)
w.append(field['key'])
def serialize_amount(value: int) -> bytearray:
if value < 0 or isinstance(value, float):
raise ValueError('Only positive integers are supported')
if value > 100000000000: # max allowed value
raise ValueError('Value is larger than 100000000000')
b = bytearray(value.to_bytes(8, 'big'))
# Clear first bit to indicate XRP
b[0] &= 0x7f
# Set second bit to indicate positive number
b[0] |= 0x40
return b
def write_bytes(w: bytearray, value: bytes):
"""Serialize a variable length bytes."""
serialize_varint(w, len(value))
w.extend(value)
def serialize_varint(w, val):
"""https://ripple.com/wiki/Binary_Format#Variable_Length_Data_Encoding"""
def rshift(val, n):
# http://stackoverflow.com/a/5833119/15677
return (val % 0x100000000) >> n
assert val >= 0
b = bytearray()
if val < 192:
b.append(val)
elif val <= 12480:
val -= 193
b.extend([193 + rshift(val, 8), val & 0xff])
elif val <= 918744:
val -= 12481
b.extend([
241 + rshift(val, 16),
rshift(val, 8) & 0xff,
val & 0xff
])
else:
raise ValueError('Variable integer overflow.')
w.extend(b)

View File

@ -0,0 +1,68 @@
from apps.common import seed
from trezor.crypto import der
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha512
from trezor.messages.RippleSignTx import RippleSignTx
from trezor.messages.RippleSignedTx import RippleSignedTx
from trezor.wire import ProcessError
from .serialize import serialize
from . import helpers
from . import layout
async def sign_tx(ctx, msg: RippleSignTx):
validate(msg)
node = await seed.derive_node(ctx, msg.address_n)
source_address = helpers.address_from_public_key(node.public_key())
set_canonical_flag(msg)
tx = serialize(msg, source_address, pubkey=node.public_key())
to_sign = get_network_prefix() + tx
check_fee(msg.fee)
await layout.require_confirm_fee(ctx, msg.fee)
await layout.require_confirm_tx(ctx, msg.payment.destination, msg.payment.amount)
signature = ecdsa_sign(node.private_key(), first_half_of_sha512(to_sign))
tx = serialize(msg, source_address, pubkey=node.public_key(), signature=signature)
return RippleSignedTx(signature, tx)
def check_fee(fee: int):
if fee < helpers.MIN_FEE or fee > helpers.MAX_FEE:
raise ProcessError('Fee must be in the range of 10 to 10,000 drops')
def get_network_prefix():
"""Network prefix is prepended before the transaction and public key is included"""
return helpers.HASH_TX_SIGN.to_bytes(4, 'big')
def first_half_of_sha512(b):
"""First half of SHA512, which Ripple uses"""
hash = sha512(b)
return hash.digest()[:32]
def ecdsa_sign(private_key: bytes, digest: bytes) -> bytes:
"""Signs and encodes signature into DER format"""
signature = secp256k1.sign(private_key, digest)
sig_der = der.encode_seq((signature[1:33], signature[33:65]))
return sig_der
def set_canonical_flag(msg: RippleSignTx):
"""
Our ECDSA implementation already returns fully-canonical signatures,
so we're enforcing it in the transaction using the designated flag
- see https://wiki.ripple.com/Transaction_Malleability#Using_Fully-Canonical_Signatures
- see https://github.com/trezor/trezor-crypto/blob/3e8974ff8871263a70b7fbb9a27a1da5b0d810f7/ecdsa.c#L791
"""
if msg.flags is None:
msg.flags = 0
msg.flags |= helpers.FLAG_FULLY_CANONICAL
def validate(msg: RippleSignTx):
if None in (msg.fee, msg.sequence, msg.payment, msg.payment.amount, msg.payment.destination):
raise ProcessError("Some of the required fields are missing (fee, sequence, payment.amount, payment.destination)")

View File

@ -0,0 +1,77 @@
from common import *
from apps.ripple.serialize import serialize
from apps.ripple.serialize import serialize_amount
from apps.ripple.sign_tx import get_network_prefix
from trezor.messages.RippleSignTx import RippleSignTx
from trezor.messages.RipplePayment import RipplePayment
class TestRippleSerializer(unittest.TestCase):
def test_amount(self):
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L2494
assert serialize_amount(0) == unhexlify('4000000000000000')
assert serialize_amount(1) == unhexlify('4000000000000001')
assert serialize_amount(93493429243) == unhexlify('40000015c4a483fb')
with self.assertRaises(ValueError):
serialize_amount(1000000000000000000) # too large
with self.assertRaises(ValueError):
serialize_amount(-1) # negative not supported
with self.assertRaises(ValueError):
serialize_amount(1.1) # float numbers not supported
def test_transactions(self):
# from https://github.com/miracle2k/ripple-python
source_address = 'r3P9vH81KBayazSTrQj6S25jW6kDb779Gi'
payment = RipplePayment(200000000, 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV')
common = RippleSignTx(None, 10, None, 1, None, payment)
assert serialize(common, source_address) == unhexlify('120000240000000161400000000bebc20068400000000000000a811450f97a072f1c4357f1ad84566a609479d927c9428314550fc62003e785dc231a1058a05e56e3f09cf4e6')
source_address = 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV'
payment = RipplePayment(1, 'r3P9vH81KBayazSTrQj6S25jW6kDb779Gi')
common = RippleSignTx(None, 99, None, 99, None, payment)
assert serialize(common, source_address) == unhexlify('12000024000000636140000000000000016840000000000000638114550fc62003e785dc231a1058a05e56e3f09cf4e6831450f97a072f1c4357f1ad84566a609479d927c942')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1579
source_address = 'r9TeThyi5xiuUUrFjtPKZiHcDxs7K9H6Rb'
payment = RipplePayment(25000000, 'r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C')
common = RippleSignTx(None, 10, 0, 2, None, payment)
assert serialize(common, source_address) == unhexlify('120000220000000024000000026140000000017d784068400000000000000a81145ccb151f6e9d603f394ae778acf10d3bece874f68314e851bbbe79e328e43d68f43445368133df5fba5a')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1651
source_address = 'rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e'
payment = RipplePayment(200000, 'rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ')
common = RippleSignTx(None, 15, 0, 144, None, payment)
# 201b005ee9ba removed from the test vector because last ledger sequence is not supported
assert serialize(common, source_address) == unhexlify('12000022000000002400000090614000000000030d4068400000000000000f8114aa1bd19d9e87be8069fdbf6843653c43837c03c6831467fe6ec28e0464dd24fb2d62a492aac697cfad02')
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/fixtures/data-driven-tests.json#L1732
source_address = 'r4BPgS7DHebQiU31xWELvZawwSG2fSPJ7C'
payment = RipplePayment(25000000, 'rBqSFEFg2B6GBMobtxnU1eLA1zbNC9NDGM')
common = RippleSignTx(None, 12, 0, 1, None, payment)
# 2ef72d50ca removed from the test vector because destination tag is not supported
assert serialize(common, source_address) == unhexlify('120000220000000024000000016140000000017d784068400000000000000c8114e851bbbe79e328e43d68f43445368133df5fba5a831476dac5e814cd4aa74142c3ab45e69a900e637aa2')
def test_transactions_for_signing(self):
# https://github.com/ripple/ripple-binary-codec/blob/4581f1b41e712f545ba08be15e188a557c731ecf/test/signing-data-encoding-test.js
source_address = 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ'
payment = RipplePayment(1000, 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')
common = RippleSignTx(None, 10, 2147483648, 1, None, payment)
tx = serialize(common, source_address, pubkey=unhexlify('ed5f5ac8b98974a3ca843326d9b88cebd0560177b973ee0b149f782cfaa06dc66a'))
tx = get_network_prefix() + tx
assert tx[0:4] == unhexlify('53545800') # signing prefix
assert tx[4:7] == unhexlify('120000') # transaction type
assert tx[7:12] == unhexlify('2280000000') # flags
assert tx[12:17] == unhexlify('2400000001') # sequence
assert tx[17:26] == unhexlify('6140000000000003e8') # amount
assert tx[26:35] == unhexlify('68400000000000000a') # fee
assert tx[35:70] == unhexlify('7321ed5f5ac8b98974a3ca843326d9b88cebd0560177b973ee0b149f782cfaa06dc66a') # singing pub key
assert tx[70:92] == unhexlify('81145b812c9d57731e27a2da8b1830195f88ef32a3b6') # account
assert tx[92:114] == unhexlify('8314b5f762798a53d543a014caf8b297cff8f2f937e8') # destination
assert len(tx[114:]) == 0 # that's it
if __name__ == '__main__':
unittest.main()