diff --git a/common/tests/fixtures/cardano/sign_tx.failed.json b/common/tests/fixtures/cardano/sign_tx.failed.json index 89d52d692..1a3911c26 100644 --- a/common/tests/fixtures/cardano/sign_tx.failed.json +++ b/common/tests/fixtures/cardano/sign_tx.failed.json @@ -2005,6 +2005,50 @@ "error_message": "Invalid certificate path" } }, + { + "description": "Stake deregistration address index larger than 1_000_000", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [ + { + "type": 1, + "path": "m/1852'/1815'/0'/2/1234567" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/190'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "error_message": "Invalid certificate" + } + }, { "description": "Repeated asset name in multiasset token group", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx.json b/common/tests/fixtures/cardano/sign_tx.json index 26cfa63af..52ebc5ada 100644 --- a/common/tests/fixtures/cardano/sign_tx.json +++ b/common/tests/fixtures/cardano/sign_tx.json @@ -2721,6 +2721,169 @@ } ] } + }, + { + "description": "Ordinary transaction with non-zero address index in change output staking path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + }, + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/1", + "amount": "7120787" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "f2c5b7bd408add0234e2302d1b46cc72e0af8a88e6d12add95d230c51febdb04", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "0000edbd08c560a00b41f19c23328a8865fd6d37a7e63f99f2c7b6f25dfe524f48b9ec1763a73c6e83c8454828fab1b698998a7a9befbaf27814e2dfea904702", + "chain_code": null + } + ] + } + }, + { + "description": "Ordinary transaction with non-zero address index in stake registration certificate", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [ + { + "type": 0, + "path": "m/1852'/1815'/0'/2/1" + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "62baf68499258a35809faab713420d7d609dd0a1a3bbc5f6e3332917cf5ddece", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "c59fcfbe4973fa0095b2eb3cdc937127664ad9fbf2b70e4eeac63f8b5de1d6b157b2ef105051762bf6132ba425822de30d8c9974c4edd41418d287b46eef6b0f", + "chain_code": null + } + ] + } + }, + { + "description": "Ordinary transaction with non-zero address index in withdrawal", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [ + { + "path": "m/1852'/1815'/0'/2/1", + "amount": "1000" + } + ], + "auxiliary_data": null, + "inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "ORDINARY_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "ac4bdaaf2288a0108654919e7f89811b32274d54da834162e407635db39de4f4", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "de92697a49b52c0bdeb98278198a3679e50f50cdc310ab03dcdb794d72d86a2791a85eaaaa04700996050c7ff787a704d53873cc95a165c83e2a9947a60ecd04", + "chain_code": null + }, + { + "type": 1, + "pub_key": "3963e13782d53ac894789a145db59830231d11f84d2df25c7f38db320de20cdc", + "signature": "7293fd496e70730531406830e06ac3d412411fd7568ad33469bbd5d0efa0e0bfe465acf2c2329d97ee35cc32aad5d3d3bb5088d1c8c9808e8b09d1e8810a230a", + "chain_code": null + } + ] + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx.plutus.json b/common/tests/fixtures/cardano/sign_tx.plutus.json index dbd8e20d4..8643c0391 100644 --- a/common/tests/fixtures/cardano/sign_tx.plutus.json +++ b/common/tests/fixtures/cardano/sign_tx.plutus.json @@ -1103,6 +1103,60 @@ ] } }, + { + "description": "Plutus transaction with base address device-owned output with non-zero staking path address_index", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "addressType": 0, + "path": "m/1852'/1815'/0'/0/0", + "stakingPath": "m/1852'/1815'/0'/2/1", + "amount": "7120787" + } + ], + "mint": [], + "script_data_hash": "d593fd793c377ac50a3169bb8378ffc257c944da31aa8f355dfa5a4f6ff89e02", + "collateral_inputs": [ + { + "path": "m/1852'/1815'/0'/0/0", + "prev_hash": "1af8fa0b754ff99253d983894e63a2b09cbb56c833ba18c3384210163f63dcfc", + "prev_index": 0 + } + ], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "PLUTUS_TRANSACTION", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "2399f1743e4074d3e18a742898c3e4d5eac66a7284a949ddcd1eac004498720f", + "witnesses": [ + { + "type": 1, + "pub_key": "5d010cf16fdeff40955633d6c565f3844a288a24967cf6b76acbeb271b4f13c1", + "signature": "8bb3d58bff5d999d1fe2fbfe818395be2a4a9e1373ff785148ba925e0aefec9f1dbf43c962ebc14c38b29b5f67935527080b9a2eaa837d9fda8fdce66c922503", + "chain_code": null + } + ] + } + }, { "description": "Plutus transaction with BASE_KEY_SCRIPT address device-owned output", "parameters": { diff --git a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json index 1aa18bdb1..b13d4f77f 100644 --- a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.failed.json @@ -493,7 +493,7 @@ "include_network_id": false }, "result": { - "error_message": "Stakepool registration transaction can only contain staking witnesses" + "error_message": "Stakepool registration transaction can only contain the pool owner witness request" } }, { @@ -1080,7 +1080,7 @@ "include_network_id": false }, "result": { - "error_message": "Stakepool registration transaction can only contain staking witnesses" + "error_message": "Stakepool registration transaction can only contain the pool owner witness request" } }, { @@ -1177,7 +1177,7 @@ "include_network_id": false }, "result": { - "error_message": "Stakepool registration transaction can only contain staking witnesses" + "error_message": "Stakepool registration transaction can only contain the pool owner witness request" } }, { @@ -2068,6 +2068,103 @@ "result": { "error_message": "Invalid tx signing request" } + }, + { + "description": "Sample stake pool registration certificate with additional staking witness request not matching the owner path", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/0" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [ + { + "path": "m/1852'/1815'/0'/2/1" + } + ], + "include_network_id": false + }, + "result": { + "error_message": "Stakepool registration transaction can only contain the pool owner witness request" + } } ] } diff --git a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json index 210411337..033c577cc 100644 --- a/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json +++ b/common/tests/fixtures/cardano/sign_tx_stake_pool_registration.json @@ -546,6 +546,107 @@ } ] } + }, + { + "description": "Sample stake pool registration certificate with a non-zero owner staking path address index", + "parameters": { + "protocol_magic": 764824073, + "network_id": 1, + "fee": 42, + "ttl": 10, + "validity_interval_start": null, + "certificates": [ + { + "type": 3, + "pool_parameters": { + "pool_id": "f61c42cbf7c8c53af3f520508212ad3e72f674f957fe23ff0acb4973", + "vrf_key_hash": "198890ad6c92e80fbdab554dda02da9fb49d001bbd96181f3e07f7a6ab0d0640", + "pledge": 500000000, + "cost": 340000000, + "margin": { + "numerator": 1, + "denominator": 2 + }, + "reward_account": "stake1uya87zwnmax0v6nnn8ptqkl6ydx4522kpsc3l3wmf3yswygwx45el", + "owners": [ + { + "staking_key_path": "m/1852'/1815'/0'/2/1" + }, + { + "staking_key_hash": "3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711" + } + ], + "relays": [ + { + "type": 0, + "ipv4_address": "192.168.0.1", + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv6_address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "port": 1234 + }, + { + "type": 0, + "ipv4_address": "192.168.0.1", + "port": 1234 + }, + { + "type": 1, + "host_name": "www.test.test", + "port": 1234 + }, + { + "type": 2, + "host_name": "www.test2.test" + } + ], + "metadata": { + "url": "https://www.test.test", + "hash": "914c57c1f12bbf4a82b12d977d4f274674856a11ed4b9b95bd70f5d41c5064a6" + } + } + } + ], + "withdrawals": [], + "auxiliary_data": null, + "inputs": [ + { + "path": null, + "prev_hash": "3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7", + "prev_index": 0 + } + ], + "outputs": [ + { + "address": "addr1q84sh2j72ux0l03fxndjnhctdg7hcppsaejafsa84vh7lwgmcs5wgus8qt4atk45lvt4xfxpjtwfhdmvchdf2m3u3hlsd5tq5r", + "amount": "1" + } + ], + "mint": [], + "script_data_hash": null, + "collateral_inputs": [], + "required_signers": [], + "collateral_return": null, + "total_collateral": null, + "reference_inputs": [], + "signing_mode": "POOL_REGISTRATION_AS_OWNER", + "additional_witness_requests": [], + "include_network_id": false + }, + "result": { + "tx_hash": "36d7e5b9acc9487b9190c3bbe8fa0ec60a6fd66b3c0af7cfd8a015a18b00d765", + "witnesses": [ + { + "type": 1, + "pub_key": "3963e13782d53ac894789a145db59830231d11f84d2df25c7f38db320de20cdc", + "signature": "b9c1131865991dbec67da4db2ff315afbf84965c823a893e4821382630d669e7e1b95197743213f24effaf8fee174a4369cfb5db417f20dce4ed40fd42450e0a", + "chain_code": null + } + ] + } } ] } diff --git a/core/src/apps/cardano/README.md b/core/src/apps/cardano/README.md index 6e45ded5b..25ac786a9 100644 --- a/core/src/apps/cardano/README.md +++ b/core/src/apps/cardano/README.md @@ -91,7 +91,7 @@ Since Alonzo era, network id may be included as an item in the transaction body. ## Key types -In Shelley two types of keys are used. Payment key and staking key. Payment keys are derived from _m/1852'/1815'/x/[0,1]/y_ paths and are used for holding/transferring funds. Staking keys are derived from _m/1852'/1815'/x/2/0_ paths, thus there is only one staking key per account. They are used for staking operations - certificates, withdrawals. Shelley addresses are built from the combination of hashes of these keys. +In Shelley two types of keys are used. Payment key and staking key. Payment keys are derived from _m/1852'/1815'/x/[0,1]/y_ paths and are used for holding/transferring funds. Staking keys are derived from _m/1852'/1815'/x/2/y_ paths (in the past, the only allowed value of `y` was `0`). They are used for staking operations - certificates, withdrawals. Shelley addresses are built from the combination of hashes of these keys. [Multi-sig paths (1854')](https://cips.cardano.org/cips/cip1854/) are used to generate keys which should be used in native scripts and also to sign multi-sig transactions. [Minting paths (1855')](https://cips.cardano.org/cips/cip1855/) are used for creating minting policies and for witnessing minting transactions. diff --git a/core/src/apps/cardano/helpers/credential.py b/core/src/apps/cardano/helpers/credential.py index 991cbc958..540498b88 100644 --- a/core/src/apps/cardano/helpers/credential.py +++ b/core/src/apps/cardano/helpers/credential.py @@ -220,5 +220,7 @@ def _do_base_address_credentials_match( from .paths import CHAIN_STAKING_KEY from .utils import to_account_path + # Note: This checks that the account matches and the staking path address_index is 0. + # (Even though other values are allowed, we want to display them to the user.) path_to_staking_path = to_account_path(address_n) + [CHAIN_STAKING_KEY, 0] return address_n_staking == path_to_staking_path diff --git a/core/src/apps/cardano/helpers/paths.py b/core/src/apps/cardano/helpers/paths.py index 8185e88a3..cfcf300d3 100644 --- a/core/src/apps/cardano/helpers/paths.py +++ b/core/src/apps/cardano/helpers/paths.py @@ -15,14 +15,17 @@ SCHEMA_PUBKEY = PathSchema.parse("m/[44,1852,1854]'/coin_type'/account'/*", _SLI SCHEMA_MINT = PathSchema.parse(f"m/1855'/coin_type'/[0-{HARDENED - 1}]'", _SLIP44_ID) SCHEMA_PAYMENT = PathSchema.parse("m/[44,1852]'/coin_type'/account'/[0,1]/address_index", _SLIP44_ID) # staking is only allowed on Shelley paths with suffix /2/0 -SCHEMA_STAKING = PathSchema.parse("m/1852'/coin_type'/account'/2/0", _SLIP44_ID) -SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse(f"m/1852'/coin_type'/[0-{HARDENED - 1}]'/2/0", _SLIP44_ID) +SCHEMA_STAKING = PathSchema.parse("m/1852'/coin_type'/account'/2/address_index", _SLIP44_ID) +SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse(f"m/1852'/coin_type'/[0-{HARDENED - 1}]'/2/address_index", _SLIP44_ID) # fmt: on ACCOUNT_PATH_INDEX = const(2) ACCOUNT_PATH_LENGTH = const(3) CHAIN_STAKING_KEY = const(2) +ADDRESS_INDEX_PATH_INDEX = const(4) +RECOMMENDED_ADDRESS_INDEX = const(0) # https://cips.cardano.org/cips/cip11/ + CHANGE_OUTPUT_PATH_NAME = "Change output path" CHANGE_OUTPUT_STAKING_PATH_NAME = "Change output staking path" CERTIFICATE_PATH_NAME = "Certificate path" diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index bb1c5f335..4b7c69aeb 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -707,12 +707,19 @@ async def confirm_withdrawal( def _format_stake_credential( path: list[int], script_hash: bytes | None, key_hash: bytes | None ) -> tuple[str, str]: - from .helpers.utils import to_account_path + from .helpers.paths import ADDRESS_INDEX_PATH_INDEX, RECOMMENDED_ADDRESS_INDEX if path: + account_number = format_account_number(path) + address_index = path[ADDRESS_INDEX_PATH_INDEX] + if address_index == RECOMMENDED_ADDRESS_INDEX: + return ( + f"for account {account_number}:", + address_n_to_str(path), + ) return ( - f"for account {format_account_number(path)}:", - address_n_to_str(to_account_path(path)), + f"for account {account_number} and index {address_index}:", + address_n_to_str(path), ) elif key_hash: return ("for key hash:", bech32.encode(bech32.HRP_STAKE_KEY_HASH, key_hash)) diff --git a/core/src/apps/cardano/sign_tx/pool_owner_signer.py b/core/src/apps/cardano/sign_tx/pool_owner_signer.py index fb3e33dfe..4c42a9f16 100644 --- a/core/src/apps/cardano/sign_tx/pool_owner_signer.py +++ b/core/src/apps/cardano/sign_tx/pool_owner_signer.py @@ -78,9 +78,12 @@ class PoolOwnerSigner(Signer): from ..helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT super()._validate_witness_request(witness_request) - if not SCHEMA_STAKING_ANY_ACCOUNT.match(witness_request.path): + if not ( + SCHEMA_STAKING_ANY_ACCOUNT.match(witness_request.path) + and witness_request.path == self.pool_owner_path + ): raise ProcessError( - "Stakepool registration transaction can only contain staking witnesses" + "Stakepool registration transaction can only contain the pool owner witness request" ) def _is_network_id_verifiable(self) -> bool: diff --git a/core/src/apps/cardano/sign_tx/signer.py b/core/src/apps/cardano/sign_tx/signer.py index 9627c8736..9e81c4422 100644 --- a/core/src/apps/cardano/sign_tx/signer.py +++ b/core/src/apps/cardano/sign_tx/signer.py @@ -88,6 +88,9 @@ class Signer: self.account_path_checker = AccountPathChecker() + # There should be at most one pool owner given as a path. + self.pool_owner_path = None + # Inputs, outputs and fee are mandatory, count the number of optional fields present. tx_dict_items_count = 3 + sum( ( @@ -747,6 +750,7 @@ class Signer: if owner.staking_key_path: owners_as_path_count += 1 + self.pool_owner_path = owner.staking_key_path certificates.assert_cond(owners_as_path_count == 1)