From 08945c48e1ef8b0283390237a30e35c76671240f Mon Sep 17 00:00:00 2001 From: Tomas Susanka Date: Thu, 28 Jun 2018 13:15:14 +0200 Subject: [PATCH] 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. --- src/apps/ripple/__init__.py | 10 +- src/apps/ripple/helpers.py | 24 ++++- src/apps/ripple/layout.py | 23 ++++ src/apps/ripple/serialize.py | 156 +++++++++++++++++++++++++++ src/apps/ripple/sign_tx.py | 68 ++++++++++++ tests/test_apps.ripple.serializer.py | 77 +++++++++++++ 6 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 src/apps/ripple/layout.py create mode 100644 src/apps/ripple/serialize.py create mode 100644 src/apps/ripple/sign_tx.py create mode 100644 tests/test_apps.ripple.serializer.py diff --git a/src/apps/ripple/__init__.py b/src/apps/ripple/__init__.py index c9ef9c2a29..cd31ff63cb 100644 --- a/src/apps/ripple/__init__.py +++ b/src/apps/ripple/__init__.py @@ -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) diff --git a/src/apps/ripple/helpers.py b/src/apps/ripple/helpers.py index a7249acd0e..36cf1fb2c2 100644 --- a/src/apps/ripple/helpers.py +++ b/src/apps/ripple/helpers.py @@ -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:] diff --git a/src/apps/ripple/layout.py b/src/apps/ripple/layout.py new file mode 100644 index 0000000000..f31917f160 --- /dev/null +++ b/src/apps/ripple/layout.py @@ -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) diff --git a/src/apps/ripple/serialize.py b/src/apps/ripple/serialize.py new file mode 100644 index 0000000000..2d6582e9ac --- /dev/null +++ b/src/apps/ripple/serialize.py @@ -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) diff --git a/src/apps/ripple/sign_tx.py b/src/apps/ripple/sign_tx.py new file mode 100644 index 0000000000..dfc2bbdebb --- /dev/null +++ b/src/apps/ripple/sign_tx.py @@ -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)") diff --git a/tests/test_apps.ripple.serializer.py b/tests/test_apps.ripple.serializer.py new file mode 100644 index 0000000000..354993f5f4 --- /dev/null +++ b/tests/test_apps.ripple.serializer.py @@ -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()