From b12de5d861527012f5711daac6f7f1193d11c6b4 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Fri, 25 Nov 2022 21:09:34 +0100 Subject: [PATCH] feat(core): CoSi collective signatures --- core/.changelog.d/450.added | 1 + core/src/all_modules.py | 2 + core/src/apps/cardano/helpers/paths.py | 6 +-- core/src/apps/common/paths.py | 7 +-- core/src/apps/misc/cosi_commit.py | 73 ++++++++++++++++++++++++++ core/src/apps/workflow_handlers.py | 2 + core/src/storage/cache.py | 4 ++ tests/device_tests/misc/test_cosi.py | 12 ++++- tests/ui_tests/fixtures.json | 6 +++ 9 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 core/.changelog.d/450.added create mode 100644 core/src/apps/misc/cosi_commit.py diff --git a/core/.changelog.d/450.added b/core/.changelog.d/450.added new file mode 100644 index 0000000000..b44d2969b4 --- /dev/null +++ b/core/.changelog.d/450.added @@ -0,0 +1 @@ +CoSi collective signatures on Model T. diff --git a/core/src/all_modules.py b/core/src/all_modules.py index c40e665b27..a53315d586 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -341,6 +341,8 @@ apps.misc import apps.misc apps.misc.cipher_key_value import apps.misc.cipher_key_value +apps.misc.cosi_commit +import apps.misc.cosi_commit apps.misc.get_ecdh_session_key import apps.misc.get_ecdh_session_key apps.misc.get_entropy diff --git a/core/src/apps/cardano/helpers/paths.py b/core/src/apps/cardano/helpers/paths.py index a895c690fe..8185e88a35 100644 --- a/core/src/apps/cardano/helpers/paths.py +++ b/core/src/apps/cardano/helpers/paths.py @@ -1,6 +1,6 @@ from micropython import const -from apps.common.paths import HARDENED, PathSchema +from apps.common.paths import HARDENED, PathSchema, unharden # noqa: F401 _SLIP44_ID = const(1815) @@ -28,7 +28,3 @@ CHANGE_OUTPUT_STAKING_PATH_NAME = "Change output staking path" CERTIFICATE_PATH_NAME = "Certificate path" POOL_OWNER_STAKING_PATH_NAME = "Pool owner staking path" WITNESS_PATH_NAME = "Witness path" - - -def unharden(item: int) -> int: - return item ^ (item & HARDENED) diff --git a/core/src/apps/common/paths.py b/core/src/apps/common/paths.py index 9ba0644929..579d77977d 100644 --- a/core/src/apps/common/paths.py +++ b/core/src/apps/common/paths.py @@ -290,9 +290,6 @@ class PathSchema: components = ["m"] append = components.append # local_cache_attribute - def unharden(item: int) -> int: - return item ^ (item & HARDENED) - for component in self.schema: if isinstance(component, Interval): a, b = component.min, component.max @@ -378,3 +375,7 @@ def address_n_to_str(address_n: Iterable[int]) -> str: return "m" return "m/" + "/".join(_path_item(i) for i in address_n) + + +def unharden(item: int) -> int: + return item ^ (item & HARDENED) diff --git a/core/src/apps/misc/cosi_commit.py b/core/src/apps/misc/cosi_commit.py new file mode 100644 index 0000000000..000b9ac265 --- /dev/null +++ b/core/src/apps/misc/cosi_commit.py @@ -0,0 +1,73 @@ +from typing import TYPE_CHECKING + +from trezor.enums import ButtonRequestType +from trezor.messages import CosiCommitment, CosiSign, CosiSignature +from trezor.wire import DataError + +from apps.common.paths import PathSchema, unharden + +if TYPE_CHECKING: + from trezor.messages import CosiCommit + from trezor.wire import Context + +# This module implements the cosigner part of the CoSi collective signatures +# as described in https://dedis.cs.yale.edu/dissent/papers/witness.pdf + + +SCHEMA_SLIP18 = PathSchema.parse("m/10018'/address_index'/*'", slip44_id=()) +# SLIP-26: m/10026'/model'/type'/rotation_index' +# - `model`: ASCII for 1, T, or R, or 0 for common things (keep the ASCII range open for future models). +# - `type`: 0 = bootloader, 1 = vendorheader, 2 = firmware, 3 = definitions, 4 = reserved +# - `rotation_index`: a fixed 0' for now +SCHEMA_SLIP26 = PathSchema.parse("m/10026'/[0-127]'/[0-4]'/0'", slip44_id=()) + + +async def cosi_commit(ctx: Context, msg: CosiCommit) -> CosiSignature: + import storage.cache as storage_cache + from trezor.crypto.curve import ed25519 + from trezor.ui.layouts import confirm_blob + from apps.common import paths + from apps.common.keychain import get_keychain + + keychain = await get_keychain(ctx, "ed25519", [SCHEMA_SLIP18, SCHEMA_SLIP26]) + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + seckey = node.private_key() + pubkey = ed25519.publickey(seckey) + + if not storage_cache.is_set(storage_cache.APP_MISC_COSI_COMMITMENT): + nonce, commitment = ed25519.cosi_commit() + storage_cache.set(storage_cache.APP_MISC_COSI_NONCE, nonce) + storage_cache.set(storage_cache.APP_MISC_COSI_COMMITMENT, commitment) + commitment = storage_cache.get(storage_cache.APP_MISC_COSI_COMMITMENT) + if commitment is None: + raise RuntimeError + + sign_msg = await ctx.call( + CosiCommitment(commitment=commitment, pubkey=pubkey), CosiSign + ) + + if sign_msg.address_n != msg.address_n: + raise DataError("Mismatched address_n") + + title = "CoSi sign message" + if SCHEMA_SLIP18.match(sign_msg.address_n): + index = unharden(msg.address_n[1]) + title = f"CoSi sign index {index}" + + await confirm_blob( + ctx, "cosi_sign", title, sign_msg.data, br_code=ButtonRequestType.ProtectCall + ) + + # clear nonce from cache + nonce = storage_cache.get(storage_cache.APP_MISC_COSI_NONCE) + storage_cache.delete(storage_cache.APP_MISC_COSI_COMMITMENT) + storage_cache.delete(storage_cache.APP_MISC_COSI_NONCE) + if nonce is None: + raise RuntimeError + + signature = ed25519.cosi_sign( + seckey, sign_msg.data, nonce, sign_msg.global_commitment, sign_msg.global_pubkey + ) + return CosiSignature(signature=signature) diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 568d101878..693d62df83 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -84,6 +84,8 @@ def _find_message_handler_module(msg_type: int) -> str: return "apps.misc.cipher_key_value" if msg_type == MessageType.GetFirmwareHash: return "apps.misc.get_firmware_hash" + if msg_type == MessageType.CosiCommit: + return "apps.misc.cosi_commit" if not utils.BITCOIN_ONLY: if msg_type == MessageType.SetU2FCounter: diff --git a/core/src/storage/cache.py b/core/src/storage/cache.py index 333d38b044..10e6652a3e 100644 --- a/core/src/storage/cache.py +++ b/core/src/storage/cache.py @@ -31,6 +31,8 @@ APP_COMMON_SAFETY_CHECKS_TEMPORARY = const(1 | _SESSIONLESS_FLAG) STORAGE_DEVICE_EXPERIMENTAL_FEATURES = const(2 | _SESSIONLESS_FLAG) APP_COMMON_REQUEST_PIN_LAST_UNLOCK = const(3 | _SESSIONLESS_FLAG) APP_COMMON_BUSY_DEADLINE_MS = const(4 | _SESSIONLESS_FLAG) +APP_MISC_COSI_NONCE = const(5 | _SESSIONLESS_FLAG) +APP_MISC_COSI_COMMITMENT = const(6 | _SESSIONLESS_FLAG) # === Homescreen storage === @@ -137,6 +139,8 @@ class SessionlessCache(DataCache): 1, # STORAGE_DEVICE_EXPERIMENTAL_FEATURES 8, # APP_COMMON_REQUEST_PIN_LAST_UNLOCK 8, # APP_COMMON_BUSY_DEADLINE_MS + 32, # APP_MISC_COSI_NONCE + 32, # APP_MISC_COSI_COMMITMENT ) super().__init__() diff --git a/tests/device_tests/misc/test_cosi.py b/tests/device_tests/misc/test_cosi.py index 0f7a0cce98..d94daba837 100644 --- a/tests/device_tests/misc/test_cosi.py +++ b/tests/device_tests/misc/test_cosi.py @@ -20,10 +20,9 @@ import pytest from trezorlib import cosi from trezorlib.debuglink import TrezorClientDebugLink as Client +from trezorlib.exceptions import TrezorFailure from trezorlib.tools import parse_path -pytestmark = pytest.mark.skip_t2 - DIGEST = sha256(b"this is not a pipe").digest() @@ -108,3 +107,12 @@ def test_cosi_sign3(client: Client): ) cosi.verify_combined(signature, DIGEST, global_pk) + + +@pytest.mark.skip_t1 +def test_cosi_different_key(client: Client): + with pytest.raises(TrezorFailure): + commit = cosi.commit(client, parse_path("m/10018h/0h")) + cosi.sign( + client, parse_path("m/10018h/1h"), DIGEST, commit.commitment, commit.pubkey + ) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index af5e524ad5..ca88893407 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -1408,6 +1408,12 @@ "TT_ethereum-test_signtx.py::test_signtx_eip1559[unknown_erc20]": "2189a082fe612b0821072ab5260aca76e77dfeccb592c013409f1ef7812c8226", "TT_ethereum-test_signtx.py::test_signtx_eip1559_access_list": "14f159d0b62d7697bcbcd56ed41b8affa86ffea1d7aa6f2d0c3ce168fb990431", "TT_ethereum-test_signtx.py::test_signtx_eip1559_access_list_larger": "14f159d0b62d7697bcbcd56ed41b8affa86ffea1d7aa6f2d0c3ce168fb990431", +"TT_misc-test_cosi.py::test_cosi_different_key": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", +"TT_misc-test_cosi.py::test_cosi_nonce": "c668c0d63bbf9875ea2c3d4073e9507fc89719a19ded85e6a64ecc03e8b0287d", +"TT_misc-test_cosi.py::test_cosi_pubkey": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", +"TT_misc-test_cosi.py::test_cosi_sign1": "c668c0d63bbf9875ea2c3d4073e9507fc89719a19ded85e6a64ecc03e8b0287d", +"TT_misc-test_cosi.py::test_cosi_sign2": "aea78f19619a1aac8072b41e1dfdade53ca3496af5f27801a9a929a0d65ecce6", +"TT_misc-test_cosi.py::test_cosi_sign3": "efeed01748ee5797c446294fba491d819b3ea60f4238782154cc608558960c3d", "TT_misc-test_msg_cipherkeyvalue.py::test_decrypt": "120f9e8e4cb99d8fbd4fe5f4ce5d6a24e7aa98fafb2329a0fde01b6fa6656361", "TT_misc-test_msg_cipherkeyvalue.py::test_decrypt_badlen": "f03b50df7f4a161078fa903c44f37272961b70358d4014d30a12888e1fd2caf1", "TT_misc-test_msg_cipherkeyvalue.py::test_encrypt": "582b31d707b118bda01c9bd6ffab3b0a8d1ea6fa68583aa9b3032cd7921ae2c3",