From d4317d1536fe43510abcf433d6dbe92c8a8a0939 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 9 Jun 2020 19:19:00 +0200 Subject: [PATCH] core/bitcoin: Implement generation and verification of SLIP-0019 proofs of ownership. --- core/src/apps/bitcoin/__init__.py | 1 + core/src/apps/bitcoin/common.py | 3 + core/src/apps/bitcoin/get_ownership_proof.py | 92 +++++++++++++++ core/src/apps/bitcoin/ownership.py | 114 +++++++++++++++++++ core/src/apps/bitcoin/sign_tx/bitcoin.py | 7 +- 5 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 core/src/apps/bitcoin/get_ownership_proof.py create mode 100644 core/src/apps/bitcoin/ownership.py diff --git a/core/src/apps/bitcoin/__init__.py b/core/src/apps/bitcoin/__init__.py index 5377055ad..38637db4b 100644 --- a/core/src/apps/bitcoin/__init__.py +++ b/core/src/apps/bitcoin/__init__.py @@ -5,6 +5,7 @@ from trezor.messages import MessageType def boot() -> None: wire.add(MessageType.GetPublicKey, __name__, "get_public_key") wire.add(MessageType.GetAddress, __name__, "get_address") + wire.add(MessageType.GetOwnershipProof, __name__, "get_ownership_proof") wire.add(MessageType.SignTx, __name__, "sign_tx") wire.add(MessageType.SignMessage, __name__, "sign_message") wire.add(MessageType.VerifyMessage, __name__, "verify_message") diff --git a/core/src/apps/bitcoin/common.py b/core/src/apps/bitcoin/common.py index 70746e3db..c2518b43f 100644 --- a/core/src/apps/bitcoin/common.py +++ b/core/src/apps/bitcoin/common.py @@ -12,6 +12,9 @@ if False: from trezor.messages.TxInputType import EnumTypeInputScriptType from trezor.messages.TxOutputType import EnumTypeOutputScriptType +# Default signature hash type in Bitcoin which signs all inputs and all outputs of the transaction. +SIGHASH_ALL = const(0x01) + # supported witness version for bech32 addresses _BECH32_WITVER = const(0x00) diff --git a/core/src/apps/bitcoin/get_ownership_proof.py b/core/src/apps/bitcoin/get_ownership_proof.py new file mode 100644 index 000000000..e4186167b --- /dev/null +++ b/core/src/apps/bitcoin/get_ownership_proof.py @@ -0,0 +1,92 @@ +from ubinascii import hexlify + +from trezor import ui, wire +from trezor.messages.GetOwnershipProof import GetOwnershipProof +from trezor.messages.OwnershipProof import OwnershipProof +from trezor.ui.text import Text + +from apps.common import coininfo +from apps.common.confirm import require_confirm +from apps.common.paths import validate_path + +from . import addresses, common, scripts +from .keychain import with_keychain +from .ownership import generate_proof, get_identifier + +if False: + from apps.common.seed import Keychain + +# Maximum number of characters per line in monospace font. +_MAX_MONO_LINE = 18 + + +@with_keychain +async def get_ownership_proof( + ctx, msg: GetOwnershipProof, keychain: Keychain, coin: coininfo.CoinInfo +) -> OwnershipProof: + await validate_path( + ctx, + addresses.validate_full_path, + keychain, + msg.address_n, + coin.curve_name, + coin=coin, + script_type=msg.script_type, + ) + + if msg.script_type not in common.INTERNAL_INPUT_SCRIPT_TYPES: + raise wire.DataError("Invalid script type") + + if msg.script_type in common.SEGWIT_INPUT_SCRIPT_TYPES and not coin.segwit: + raise wire.DataError("Segwit not enabled on this coin") + + node = keychain.derive(msg.address_n) + address = addresses.get_address(msg.script_type, coin, node, msg.multisig) + script_pubkey = scripts.output_derive_script(address, coin) + ownership_id = get_identifier(script_pubkey, keychain) + + # If the scriptPubKey is multisig, then the caller has to provide + # ownership IDs, otherwise providing an ID is optional. + if msg.multisig: + if ownership_id not in msg.ownership_ids: + raise wire.DataError("Missing ownership identifier") + elif msg.ownership_ids: + if msg.ownership_ids != [ownership_id]: + raise wire.DataError("Invalid ownership identifier") + else: + msg.ownership_ids = [ownership_id] + + # In order to set the "user confirmation" bit in the proof, the user must actually confirm. + if msg.user_confirmation: + text = Text("Proof of ownership", ui.ICON_CONFIG) + text.normal("Do you want to create a") + if not msg.commitment_data: + text.normal("proof of ownership?") + else: + hex_data = hexlify(msg.commitment_data).decode() + text.normal("proof of ownership for:") + if len(hex_data) > 3 * _MAX_MONO_LINE: + text.mono(hex_data[0:_MAX_MONO_LINE]) + text.mono( + hex_data[_MAX_MONO_LINE : 3 * _MAX_MONO_LINE // 2 - 1] + + "..." + + hex_data[-3 * _MAX_MONO_LINE // 2 + 2 : -_MAX_MONO_LINE] + ) + text.mono(hex_data[-_MAX_MONO_LINE:]) + else: + text.mono(hex_data) + + await require_confirm(ctx, text) + + ownership_proof, signature = generate_proof( + node, + msg.script_type, + msg.multisig, + coin, + msg.user_confirmation, + msg.ownership_ids, + script_pubkey, + msg.commitment_data, + ) + + return OwnershipProof(ownership_proof=ownership_proof, signature=signature) diff --git a/core/src/apps/bitcoin/ownership.py b/core/src/apps/bitcoin/ownership.py new file mode 100644 index 000000000..5d0c28601 --- /dev/null +++ b/core/src/apps/bitcoin/ownership.py @@ -0,0 +1,114 @@ +from trezor import utils, wire +from trezor.crypto import bip32, hashlib, hmac + +from apps.common import seed +from apps.common.readers import BytearrayReader, read_bitcoin_varint +from apps.common.writers import ( + empty_bytearray, + write_bitcoin_varint, + write_bytes_fixed, + write_uint8, +) + +from . import common +from .scripts import read_bip322_signature_proof, write_bip322_signature_proof +from .verification import SignatureVerifier + +if False: + from typing import List, Optional, Tuple + from trezor.messages.MultisigRedeemScriptType import MultisigRedeemScriptType + from trezor.messages.TxInputType import EnumTypeInputScriptType + from apps.common.coininfo import CoinInfo + +# This module implements the SLIP-0019 proof of ownership format. + +_VERSION_MAGIC = b"SL\x00\x19" +_FLAG_USER_CONFIRMED = 0x01 +_OWNERSHIP_ID_LEN = 32 +_OWNERSHIP_ID_KEY_PATH = [b"SLIP-0019", b"Ownership identification key"] + + +def generate_proof( + node: bip32.HDNode, + script_type: EnumTypeInputScriptType, + multisig: MultisigRedeemScriptType, + coin: CoinInfo, + user_confirmed: bool, + ownership_ids: List[bytes], + script_pubkey: bytes, + commitment_data: Optional[bytes], +) -> Tuple[bytes, bytes]: + flags = 0 + if user_confirmed: + flags |= _FLAG_USER_CONFIRMED + + proof = empty_bytearray(4 + 1 + 1 + len(ownership_ids) * _OWNERSHIP_ID_LEN) + + write_bytes_fixed(proof, _VERSION_MAGIC, 4) + write_uint8(proof, flags) + write_bitcoin_varint(proof, len(ownership_ids)) + for ownership_id in ownership_ids: + write_bytes_fixed(proof, ownership_id, _OWNERSHIP_ID_LEN) + + sighash = hashlib.sha256(proof) + sighash.update(script_pubkey) + if commitment_data: + sighash.update(commitment_data) + signature = common.ecdsa_sign(node, sighash.digest()) + public_key = node.public_key() + write_bip322_signature_proof( + proof, script_type, multisig, coin, public_key, signature + ) + + return proof, signature + + +def verify_nonownership( + proof: bytes, + script_pubkey: bytes, + commitment_data: bytes, + keychain: seed.Keychain, + coin: CoinInfo, +) -> bool: + try: + r = BytearrayReader(proof) + if r.read(4) != _VERSION_MAGIC: + raise wire.DataError("Unknown format of proof of ownership") + + flags = r.get() + if flags & 0b1111_1110: + raise wire.DataError("Unknown flags in proof of ownership") + + # Determine whether our ownership ID appears in the proof. + id_count = read_bitcoin_varint(r) + ownership_id = get_identifier(script_pubkey, keychain) + not_owned = True + for _ in range(id_count): + if utils.consteq(ownership_id, r.read(_OWNERSHIP_ID_LEN)): + not_owned = False + + # Verify the BIP-322 SignatureProof. + + proof_body = proof[: r.offset] + sighash = hashlib.sha256(proof_body) + sighash.update(script_pubkey) + sighash.update(commitment_data) + script_sig, witness = read_bip322_signature_proof(r) + + # We don't call verifier.ensure_hash_type() to avoid possible compatibility + # issues between implementations, because the hash type doesn't influence + # the digest and the value to use is not defined in BIP-322. + verifier = SignatureVerifier(script_pubkey, script_sig, witness, coin) + verifier.verify(sighash.digest()) + except (ValueError, IndexError): + raise wire.DataError("Invalid proof of ownership") + + return not_owned + + +def get_identifier(script_pubkey: bytes, keychain: seed.Keychain) -> bytes: + # k = Key(m/"SLIP-0019"/"Ownership identification key") + node = keychain.derive(_OWNERSHIP_ID_KEY_PATH) + + # id = HMAC-SHA256(key = k, msg = scriptPubKey) + return hmac.Hmac(node.key(), script_pubkey, hashlib.sha256).digest() diff --git a/core/src/apps/bitcoin/sign_tx/bitcoin.py b/core/src/apps/bitcoin/sign_tx/bitcoin.py index 3a8a487cb..0ed75990c 100644 --- a/core/src/apps/bitcoin/sign_tx/bitcoin.py +++ b/core/src/apps/bitcoin/sign_tx/bitcoin.py @@ -18,7 +18,7 @@ from apps.common import coininfo, seed from apps.common.writers import write_bitcoin_varint from .. import addresses, common, multisig, scripts, writers -from ..common import ecdsa_sign +from ..common import SIGHASH_ALL, ecdsa_sign from ..verification import SignatureVerifier from . import helpers, progress, tx_weight from .matchcheck import MultisigFingerprintChecker, WalletPathChecker @@ -27,9 +27,6 @@ if False: from typing import List, Optional, Set, Tuple, Union from trezor.crypto.bip32 import HDNode -# Default signature hash type in Bitcoin which signs all inputs and all outputs of the transaction. -_SIGHASH_ALL = const(0x01) - # the chain id used for change _BIP32_CHANGE_CHAIN = const(1) @@ -486,7 +483,7 @@ class Bitcoin: # === def get_sighash_type(self, txi: TxInputType) -> int: - return _SIGHASH_ALL + return SIGHASH_ALL def get_hash_type(self, txi: TxInputType) -> int: """ Return the nHashType flags."""