diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00000.png b/tests/ragger/snapshots/nanos/get_pk_700/00000.png deleted file mode 100644 index 63778c4..0000000 Binary files a/tests/ragger/snapshots/nanos/get_pk_700/00000.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00001.png b/tests/ragger/snapshots/nanos/get_pk_700/00001.png deleted file mode 100644 index 63fe23c..0000000 Binary files a/tests/ragger/snapshots/nanos/get_pk_700/00001.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00002.png b/tests/ragger/snapshots/nanos/get_pk_700/00002.png deleted file mode 100644 index b79ecb4..0000000 Binary files a/tests/ragger/snapshots/nanos/get_pk_700/00002.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00003.png b/tests/ragger/snapshots/nanos/get_pk_700/00003.png deleted file mode 100644 index 0b61bde..0000000 Binary files a/tests/ragger/snapshots/nanos/get_pk_700/00003.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00000.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00000.png new file mode 100644 index 0000000..1c4d4d7 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00000.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00001.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00001.png new file mode 100644 index 0000000..e3a34c7 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00001.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00002.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00002.png new file mode 100644 index 0000000..5e9d109 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00002.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00003.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00003.png new file mode 100644 index 0000000..b7d21a9 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00003.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00004.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00004.png new file mode 100644 index 0000000..24be343 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00004.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00005.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00005.png new file mode 100644 index 0000000..f032e45 Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00005.png differ diff --git a/tests/ragger/snapshots/nanos/test_get_eth2_pk/00006.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00006.png new file mode 100644 index 0000000..4118dbf Binary files /dev/null and b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00006.png differ diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00004.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00007.png similarity index 100% rename from tests/ragger/snapshots/nanos/get_pk_700/00004.png rename to tests/ragger/snapshots/nanos/test_get_eth2_pk/00007.png diff --git a/tests/ragger/snapshots/nanos/get_pk_700/00005.png b/tests/ragger/snapshots/nanos/test_get_eth2_pk/00008.png similarity index 100% rename from tests/ragger/snapshots/nanos/get_pk_700/00005.png rename to tests/ragger/snapshots/nanos/test_get_eth2_pk/00008.png diff --git a/tests/ragger/snapshots/nanosp/get_pk_700/00000.png b/tests/ragger/snapshots/nanosp/get_pk_700/00000.png deleted file mode 100644 index a487005..0000000 Binary files a/tests/ragger/snapshots/nanosp/get_pk_700/00000.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanosp/get_pk_700/00001.png b/tests/ragger/snapshots/nanosp/get_pk_700/00001.png deleted file mode 100644 index 5b381b6..0000000 Binary files a/tests/ragger/snapshots/nanosp/get_pk_700/00001.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanosp/test_get_eth2_pk b/tests/ragger/snapshots/nanosp/test_get_eth2_pk new file mode 120000 index 0000000..dc7c371 --- /dev/null +++ b/tests/ragger/snapshots/nanosp/test_get_eth2_pk @@ -0,0 +1 @@ +../nanox/test_get_eth2_pk \ No newline at end of file diff --git a/tests/ragger/snapshots/nanox/get_pk_700/00000.png b/tests/ragger/snapshots/nanox/get_pk_700/00000.png deleted file mode 100644 index a487005..0000000 Binary files a/tests/ragger/snapshots/nanox/get_pk_700/00000.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanox/get_pk_700/00001.png b/tests/ragger/snapshots/nanox/get_pk_700/00001.png deleted file mode 100644 index 5b381b6..0000000 Binary files a/tests/ragger/snapshots/nanox/get_pk_700/00001.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanox/get_pk_700/00002.png b/tests/ragger/snapshots/nanox/get_pk_700/00002.png deleted file mode 100644 index 53ae651..0000000 Binary files a/tests/ragger/snapshots/nanox/get_pk_700/00002.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanox/get_pk_700/00003.png b/tests/ragger/snapshots/nanox/get_pk_700/00003.png deleted file mode 100644 index 6578872..0000000 Binary files a/tests/ragger/snapshots/nanox/get_pk_700/00003.png and /dev/null differ diff --git a/tests/ragger/snapshots/nanox/test_get_eth2_pk/00000.png b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00000.png new file mode 100644 index 0000000..bb3289d Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00000.png differ diff --git a/tests/ragger/snapshots/nanox/test_get_eth2_pk/00001.png b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00001.png new file mode 100644 index 0000000..aa05c8c Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00001.png differ diff --git a/tests/ragger/snapshots/nanox/test_get_eth2_pk/00002.png b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00002.png new file mode 100644 index 0000000..5eb4dd4 Binary files /dev/null and b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00002.png differ diff --git a/tests/ragger/snapshots/nanosp/get_pk_700/00002.png b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00003.png similarity index 100% rename from tests/ragger/snapshots/nanosp/get_pk_700/00002.png rename to tests/ragger/snapshots/nanox/test_get_eth2_pk/00003.png diff --git a/tests/ragger/snapshots/nanosp/get_pk_700/00003.png b/tests/ragger/snapshots/nanox/test_get_eth2_pk/00004.png similarity index 100% rename from tests/ragger/snapshots/nanosp/get_pk_700/00003.png rename to tests/ragger/snapshots/nanox/test_get_eth2_pk/00004.png diff --git a/tests/ragger/snapshots/stax/get_pk_700/00000.png b/tests/ragger/snapshots/stax/get_pk_700/00000.png deleted file mode 100644 index 7593390..0000000 Binary files a/tests/ragger/snapshots/stax/get_pk_700/00000.png and /dev/null differ diff --git a/tests/ragger/snapshots/stax/get_pk_700/00001.png b/tests/ragger/snapshots/stax/get_pk_700/00001.png deleted file mode 100644 index aa683fa..0000000 Binary files a/tests/ragger/snapshots/stax/get_pk_700/00001.png and /dev/null differ diff --git a/tests/ragger/snapshots/stax/test_get_eth2_pk/00000.png b/tests/ragger/snapshots/stax/test_get_eth2_pk/00000.png new file mode 100644 index 0000000..832d70e Binary files /dev/null and b/tests/ragger/snapshots/stax/test_get_eth2_pk/00000.png differ diff --git a/tests/ragger/snapshots/stax/get_pk_700/00002.png b/tests/ragger/snapshots/stax/test_get_eth2_pk/00001.png similarity index 100% rename from tests/ragger/snapshots/stax/get_pk_700/00002.png rename to tests/ragger/snapshots/stax/test_get_eth2_pk/00001.png diff --git a/tests/ragger/staking_deposit/key_handling/key_derivation/path.py b/tests/ragger/staking_deposit/key_handling/key_derivation/path.py new file mode 100644 index 0000000..b9e2e2b --- /dev/null +++ b/tests/ragger/staking_deposit/key_handling/key_derivation/path.py @@ -0,0 +1,40 @@ +from typing import List + +from bip_utils import Bip39SeedGenerator +from bip_utils.utils.mnemonic import Mnemonic + +from .tree import ( + derive_master_SK, + derive_child_SK, +) + + +def path_to_nodes(path: str) -> List[int]: + """ + Maps from a path string to a list of indices where each index represents the corresponding level in the path. + """ + path = path.replace(' ', '') + if not set(path).issubset(set('m1234567890/')): + raise ValueError(f"Invalid path {path}") + + indices = path.split('/') + + if indices[0] != 'm': + raise ValueError(f"The first character of path should be `m`. Got {indices[0]}.") + indices.pop(0) + + return [int(index) for index in indices] + + +def mnemonic_and_path_to_key(mnemonic: str, path: str) -> int: + """ + Return the SK at position `path`, derived from `mnemonic`. The password is to be + compliant with BIP39 mnemonics that use passwords, but is not used by this CLI outside of tests. + """ + #seed = get_seed(mnemonic) + seed = Bip39SeedGenerator(Mnemonic.FromString(mnemonic)).Generate() + + sk = derive_master_SK(seed) + for node in path_to_nodes(path): + sk = derive_child_SK(parent_SK=sk, index=node) + return sk diff --git a/tests/ragger/staking_deposit/key_handling/key_derivation/tree.py b/tests/ragger/staking_deposit/key_handling/key_derivation/tree.py new file mode 100644 index 0000000..53fcd0e --- /dev/null +++ b/tests/ragger/staking_deposit/key_handling/key_derivation/tree.py @@ -0,0 +1,85 @@ +from staking_deposit.utils.crypto import ( + HKDF, + SHA256, +) +from py_ecc.optimized_bls12_381 import curve_order as bls_curve_order +from typing import List + + +def _flip_bits_256(input: int) -> int: + """ + Flips 256 bits worth of `input`. + """ + return input ^ (2**256 - 1) + + +def _IKM_to_lamport_SK(*, IKM: bytes, salt: bytes) -> List[bytes]: + """ + Derives the lamport SK for a given `IKM` and `salt`. + + Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#ikm_to_lamport_sk + """ + OKM = HKDF(salt=salt, IKM=IKM, L=8160) + lamport_SK = [OKM[i: i + 32] for i in range(0, 8160, 32)] + return lamport_SK + + +def _parent_SK_to_lamport_PK(*, parent_SK: int, index: int) -> bytes: + """ + Derives the `index`th child's lamport PK from the `parent_SK`. + + Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#parent_sk_to_lamport_pk + """ + salt = index.to_bytes(4, byteorder='big') + IKM = parent_SK.to_bytes(32, byteorder='big') + lamport_0 = _IKM_to_lamport_SK(IKM=IKM, salt=salt) + not_IKM = _flip_bits_256(parent_SK).to_bytes(32, byteorder='big') + lamport_1 = _IKM_to_lamport_SK(IKM=not_IKM, salt=salt) + lamport_SKs = lamport_0 + lamport_1 + lamport_PKs = [SHA256(sk) for sk in lamport_SKs] + compressed_PK = SHA256(b''.join(lamport_PKs)) + return compressed_PK + + +def _HKDF_mod_r(*, IKM: bytes, key_info: bytes=b'') -> int: + """ + Hashes the IKM using HKDF and returns the answer as an int modulo r, the BLS field order. + + Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#hkdf_mod_r + """ + L = 48 # `ceil((3 * ceil(log2(r))) / 16)`, where `r` is the order of the BLS 12-381 curve + salt = b'BLS-SIG-KEYGEN-SALT-' + SK = 0 + while SK == 0: + salt = SHA256(salt) + okm = HKDF( + salt=salt, + IKM=IKM + b'\x00', # add postfix `I2OSP(0, 1)` + L=L, + info=key_info + L.to_bytes(2, 'big'), + ) + SK = int.from_bytes(okm, byteorder='big') % bls_curve_order + return SK + + +def derive_child_SK(*, parent_SK: int, index: int) -> int: + """ + Given a parent SK `parent_SK`, return the child SK at the supplied `index`. + + Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#derive_child_sk + """ + if index < 0 or index >= 2**32: + raise IndexError(f"`index` should be greater than or equal to 0 and less than 2**32. Got index={index}.") + lamport_PK = _parent_SK_to_lamport_PK(parent_SK=parent_SK, index=index) + return _HKDF_mod_r(IKM=lamport_PK) + + +def derive_master_SK(seed: bytes) -> int: + """ + Given a seed, derive the master SK. + + Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#derive_master_sk + """ + if len(seed) < 32: + raise ValueError(f"`len(seed)` should be greater than or equal to 32. Got {len(seed)}.") + return _HKDF_mod_r(IKM=seed) diff --git a/tests/ragger/staking_deposit/utils/crypto.py b/tests/ragger/staking_deposit/utils/crypto.py new file mode 100644 index 0000000..48a9d76 --- /dev/null +++ b/tests/ragger/staking_deposit/utils/crypto.py @@ -0,0 +1,55 @@ +from typing import Any + +from Crypto.Hash import ( + SHA256 as _sha256, + SHA512 as _sha512, +) +from Crypto.Protocol.KDF import ( + scrypt as _scrypt, + HKDF as _HKDF, + PBKDF2 as _PBKDF2, +) +from Crypto.Cipher import ( + AES as _AES +) + + +def SHA256(x: bytes) -> bytes: + return _sha256.new(x).digest() + + +def scrypt(*, password: str, salt: str, n: int, r: int, p: int, dklen: int) -> bytes: + if n * r * p < 2**20: # 128 MB memory usage + raise ValueError("The Scrypt parameters chosen are not secure.") + if n >= 2**(128 * r / 8): + raise ValueError("The given `n` should be less than `2**(128 * r / 8)`." + f"\tGot `n={n}`, r={r}, 2**(128 * r / 8)={2**(128 * r / 8)}") + res = _scrypt(password=password, salt=salt, key_len=dklen, N=n, r=r, p=p) + return res if isinstance(res, bytes) else res[0] # PyCryptodome can return Tuple[bytes] + + +def PBKDF2(*, password: bytes, salt: bytes, dklen: int, c: int, prf: str) -> bytes: + if 'sha' not in prf: + raise ValueError(f"String 'sha' is not in `prf`({prf})") + if 'sha256' in prf and c < 2**18: + ''' + Verify the number of rounds of SHA256-PBKDF2. SHA512 not checked as use in BIP39 + does not require, and therefore doesn't use, safe parameters (c=2048). + + Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed + ''' + raise ValueError("The PBKDF2 parameters chosen are not secure.") + _hash = _sha256 if 'sha256' in prf else _sha512 + res = _PBKDF2(password=password, salt=salt, dkLen=dklen, count=c, hmac_hash_module=_hash) # type: ignore + return res if isinstance(res, bytes) else res[0] # PyCryptodome can return Tuple[bytes] + + +def HKDF(*, salt: bytes, IKM: bytes, L: int, info: bytes=b'') -> bytes: + res = _HKDF(master=IKM, key_len=L, salt=salt, hashmod=_sha256, context=info) + return res if isinstance(res, bytes) else res[0] # PyCryptodome can return Tuple[bytes] + + +def AES_128_CTR(*, key: bytes, iv: bytes) -> Any: + if len(key) != 16: + raise ValueError(f"The key length should be 16. Got {len(key)}.") + return _AES.new(key=key, mode=_AES.MODE_CTR, initial_value=iv, nonce=b'') diff --git a/tests/ragger/test_get_address.py b/tests/ragger/test_get_address.py index 2d51ee9..0d102aa 100644 --- a/tests/ragger/test_get_address.py +++ b/tests/ragger/test_get_address.py @@ -1,9 +1,11 @@ -from typing import Optional, Tuple +from typing import Optional import pytest -from client.client import EthAppClient, StatusWord -import client.response_parser as ResponseParser +from py_ecc.bls import G2ProofOfPossession as bls +from staking_deposit.key_handling.key_derivation.path import mnemonic_and_path_to_key + +from ragger.bip.seed import SPECULOS_MNEMONIC from ragger.error import ExceptionRAPDU from ragger.firmware import Firmware from ragger.backend import BackendInterface @@ -12,6 +14,9 @@ from ragger.bip import calculate_public_key_and_chaincode, CurveChoice from constants import ROOT_SNAPSHOT_PATH +from client.client import EthAppClient, StatusWord +import client.response_parser as ResponseParser + @pytest.fixture(name="with_chaincode", params=[True, False]) def with_chaincode_fixture(request) -> bool: @@ -25,7 +30,8 @@ def chain_fixture(request) -> Optional[int]: def get_moves(firmware: Firmware, chain: Optional[int] = None, - reject: bool = False): + reject: bool = False, + pk_eth2: bool = False): moves = [] if firmware.is_nano: @@ -36,9 +42,14 @@ def get_moves(firmware: Firmware, moves += [NavInsID.RIGHT_CLICK] if reject: moves += [NavInsID.RIGHT_CLICK] + if pk_eth2: + if firmware.device == "nanos": + moves += [NavInsID.RIGHT_CLICK] * 2 + moves += [NavInsID.RIGHT_CLICK] moves += [NavInsID.BOTH_CLICK] else: - moves += [NavInsID.USE_CASE_REVIEW_TAP] + if not pk_eth2: + moves += [NavInsID.USE_CASE_REVIEW_TAP] if chain is not None and chain > 1: moves += [NavInsID.USE_CASE_ADDRESS_CONFIRMATION_TAP] if reject: @@ -90,18 +101,21 @@ def test_get_pk(firmware: Firmware, assert chaincode.hex() == ref_chaincode -def test_get_pk2(firmware: Firmware, - backend: BackendInterface, - navigator: Navigator): +def test_get_eth2_pk(firmware: Firmware, + backend: BackendInterface, + navigator: Navigator, + test_name: str): app_client = EthAppClient(backend) - path="m/44'/700'/1'/0/0" - with app_client.get_public_addr(bip32_path=path, chaincode=True): + path="m/12381/3600/0/0" + with app_client.get_eth2_public_addr(bip32_path=path): navigator.navigate_and_compare(ROOT_SNAPSHOT_PATH, - "get_pk_700", - get_moves(firmware)) - pk, _, chaincode = ResponseParser.pk_addr(app_client.response().data, True) - ref_pk, ref_chaincode = calculate_public_key_and_chaincode(curve=CurveChoice.Secp256k1, - path=path) - assert pk.hex() == ref_pk - assert chaincode.hex() == ref_chaincode + test_name, + get_moves(firmware, pk_eth2=True)) + + pk = app_client.response().data + ref_pk = bls.SkToPk(mnemonic_and_path_to_key(SPECULOS_MNEMONIC, path)) + if firmware.name == "stax": + pk = pk[1:49] + + assert pk == ref_pk