Skip to content

Commit 2701e65

Browse files
authored
Merge pull request #76 from fireblocks/keyset_id
add support for multiple keysets
2 parents 767ed51 + ec63d6f commit 2701e65

File tree

5 files changed

+47
-34
lines changed

5 files changed

+47
-34
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
env:
2828
OPENSSL_CONF: '${{ github.workspace }}/test/openssl.cnf'
2929
- name: Upload pytest test results
30-
uses: actions/upload-artifact@v3
30+
uses: actions/upload-artifact@v4
3131
with:
3232
name: pytest-results-${{ matrix.python-version }}
3333
path: junit/test-results-${{ matrix.python-version }}.xml

fb_recover_keys.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,16 @@ def main():
115115
Be sure you are in a private location and no one can see your screen.'''
116116
, default = "no")
117117

118-
for algo, info in privkeys.items():
118+
for algo, privkey, chaincode, keyset_id in privkeys:
119119
# info may be either None or tuple
120-
if info:
121-
privkey, chaincode = info
120+
if privkey:
121+
keyset_id_str = " - Key Set " + str(keyset_id)
122122
pub = recover.get_public_key(algo, privkey)
123123
if show_xprv:
124-
print(privkey_descriptions[algo] + ":\t" + recover.encode_extended_key(algo, privkey, chaincode, False))
125-
print(pubkey_descriptions[algo] + ":\t%s\t%s" % (recover.encode_extended_key(algo, pub, chaincode, True), colored("Verified!","green")))
124+
print(privkey_descriptions[algo] + keyset_id_str + ":\t" + recover.encode_extended_key(algo, privkey, chaincode, False))
125+
print(pubkey_descriptions[algo] + keyset_id_str + ":\t%s\t%s" % (recover.encode_extended_key(algo, pub, chaincode, True), colored("Verified!","green")))
126126
else:
127-
print(pubkey_descriptions[algo] + ":\t%s" % (colored("Verification failed","red")))
127+
print(pubkey_descriptions[algo] + keyset_id_str + ":\t%s" % (colored("Verification failed","red")))
128128

129129
if __name__ == "__main__" :
130130
main()

test/test_recovery.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@
44

55
TEST_DIR = pathlib.Path.resolve(pathlib.Path(__file__)).parent
66

7+
def result_list_to_dict(results):
8+
result = {}
9+
for algo, privkey, chaincode, keyset_id in results:
10+
result[algo] = (privkey, chaincode, keyset_id)
11+
return result
712

813
def test_recovery_basic():
9-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup.zip", TEST_DIR / "priv.pem", "Thefireblocks1!")
10-
11-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_ECDSA_SECP256K1']
14+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup.zip", TEST_DIR / "priv.pem", "Thefireblocks1!"))
15+
ecdsa_priv_key, ecdsa_chaincode, keyset_id = result['MPC_ECDSA_SECP256K1']
1216
assert(ecdsa_priv_key == 0x473d1820ca4bf7cf6b018a8520b1ec0849cb99bce4fff45c5598723f67b3bd52)
1317
pub = recover.get_public_key("MPC_ECDSA_SECP256K1", ecdsa_priv_key)
1418
assert(pub == "021d84f3b6d7c6888f81c7cc381b658d85319f27e1ea9c93dff128667fb4b82ba0")
1519
assert(recover.encode_extended_key('MPC_ECDSA_SECP256K1', ecdsa_priv_key, ecdsa_chaincode, False) == "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF9aunJDs4SsrmoxycAo6xxBTHawSz5sYxEy8TpCkv66Sci373DJ")
1620
assert(recover.encode_extended_key('MPC_ECDSA_SECP256K1', pub, ecdsa_chaincode, True) == "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QJJZSgiCXT6sq7wa2jCk5t4Vv1r1E4q1venKghAAdyzieufGyX")
21+
assert(keyset_id == 1)
1722

1823

1924
def test_full_recovery():
20-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_new.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!")
21-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_ECDSA_SECP256K1']
22-
eddsa_priv_key, eddsa_chaincode = result['MPC_EDDSA_ED25519']
25+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_new.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!"))
26+
ecdsa_priv_key, ecdsa_chaincode, keyset_id = result['MPC_ECDSA_SECP256K1']
27+
eddsa_priv_key, eddsa_chaincode, keyset_id_eddsa = result['MPC_EDDSA_ED25519']
2328

2429
assert(ecdsa_priv_key == 0x66b1baf063db6e7152480334ebab0ab098e85f682b784754e46c18c962a1aa9d)
2530
assert(eddsa_priv_key == 0xd74820d02cc2aa09e2d0bcb36aeb92625b3d92c8d202063eab5513fd4453a44)
@@ -35,11 +40,13 @@ def test_full_recovery():
3540
assert(pub == "0050cfee85dabebed78f43e94a1b7afd13c20461ad66efa083779bdeffd22269d9")
3641
assert(recover.encode_extended_key('MPC_EDDSA_ED25519', eddsa_priv_key, eddsa_chaincode, False) == "fprv4LsXPWzhTTp9ax8NGVwbnRFuT3avVQ4ydHNWcu8hCGZd18TRKxgAzbrpY9bLJRe4Y2AyX9TfQdDPbmqEYoDCTju9QFZbUgdsxsmUgfvuEDK")
3742
assert(recover.encode_extended_key('MPC_EDDSA_ED25519', pub, eddsa_chaincode, True) == "fpub8sZZXw2wbqVpURAAA9cCBpv2256rejFtCayHuRAzcYN1qciBxMVmB6UgiDAQTUZh5EP9JZciPQPjKAHyqPYHELqEHWkvo1sxreEJgLyfCJj")
43+
assert(keyset_id == 1)
44+
assert(keyset_id_eddsa == 1)
3845

3946

4047
def test_recovery_old_format():
41-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_old_format.zip", TEST_DIR / "priv.pem", "Thefireblocks1!")
42-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_ECDSA_SECP256K1']
48+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_old_format.zip", TEST_DIR / "priv.pem", "Thefireblocks1!"))
49+
ecdsa_priv_key, ecdsa_chaincode, _ = result['MPC_ECDSA_SECP256K1']
4350

4451
assert(ecdsa_priv_key == 0x473d1820ca4bf7cf6b018a8520b1ec0849cb99bce4fff45c5598723f67b3bd52)
4552
pub = recover.get_public_key("MPC_ECDSA_SECP256K1", ecdsa_priv_key)
@@ -49,9 +56,9 @@ def test_recovery_old_format():
4956

5057

5158
def test_cmp_recovery():
52-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_cmp.zip", TEST_DIR / "priv.pem", "Fireblocks1!")
53-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_CMP_ECDSA_SECP256K1']
54-
eddsa_priv_key, eddsa_chaincode = result['MPC_CMP_EDDSA_ED25519']
59+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_cmp.zip", TEST_DIR / "priv.pem", "Fireblocks1!"))
60+
ecdsa_priv_key, ecdsa_chaincode, _ = result['MPC_CMP_ECDSA_SECP256K1']
61+
eddsa_priv_key, eddsa_chaincode, _ = result['MPC_CMP_EDDSA_ED25519']
5562

5663
assert(ecdsa_priv_key == 0xf57c18e98a24ca0b36fbbd103233aff128b740426da189ce208545d44bbad050)
5764
assert(eddsa_priv_key == 0xa536dc2f2d744ae78eb26fdfb4b9e234a649525e0a1142bf900cd9c26987007)
@@ -76,9 +83,9 @@ def test_one_custom_chaincode_recovery():
7683
Hence all the extracted keys are they same, and differce lies mostly in the extended form of the key,
7784
which encodes the chaincode.
7885
'''
79-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_with_one_custom_chaincode.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!")
80-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_ECDSA_SECP256K1']
81-
eddsa_priv_key, eddsa_chaincode = result['MPC_EDDSA_ED25519']
86+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_with_one_custom_chaincode.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!"))
87+
ecdsa_priv_key, ecdsa_chaincode, _ = result['MPC_ECDSA_SECP256K1']
88+
eddsa_priv_key, eddsa_chaincode, _ = result['MPC_EDDSA_ED25519']
8289

8390
assert(ecdsa_chaincode != eddsa_chaincode)
8491
assert(ecdsa_priv_key == 0x66b1baf063db6e7152480334ebab0ab098e85f682b784754e46c18c962a1aa9d)
@@ -110,9 +117,9 @@ def test_two_custom_chaincode_recovery():
110117
only the extended forms of the keys are different, as they encode the respective chaincodes.
111118
112119
'''
113-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_with_two_custom_chaincode.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!")
114-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_ECDSA_SECP256K1']
115-
eddsa_priv_key, eddsa_chaincode = result['MPC_EDDSA_ED25519']
120+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_with_two_custom_chaincode.zip", TEST_DIR / "priv2.pem", "Thefireblocks1!"))
121+
ecdsa_priv_key, ecdsa_chaincode, _ = result['MPC_ECDSA_SECP256K1']
122+
eddsa_priv_key, eddsa_chaincode, _ = result['MPC_EDDSA_ED25519']
116123

117124
assert(ecdsa_chaincode != eddsa_chaincode)
118125
assert(ecdsa_priv_key == 0x66b1baf063db6e7152480334ebab0ab098e85f682b784754e46c18c962a1aa9d)
@@ -132,9 +139,9 @@ def test_two_custom_chaincode_recovery():
132139

133140

134141
def test_recovery_with_mobile_share_as_json():
135-
result = recover.restore_key_and_chaincode(TEST_DIR / "json_share.zip", TEST_DIR / "priv.pem", "Fireblocks1!")
136-
ecdsa_priv_key, ecdsa_chaincode = result['MPC_CMP_ECDSA_SECP256K1']
137-
eddsa_priv_key, eddsa_chaincode = result['MPC_EDDSA_ED25519']
142+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "json_share.zip", TEST_DIR / "priv.pem", "Fireblocks1!"))
143+
ecdsa_priv_key, ecdsa_chaincode, _ = result['MPC_CMP_ECDSA_SECP256K1']
144+
eddsa_priv_key, eddsa_chaincode, _ = result['MPC_EDDSA_ED25519']
138145

139146
assert(ecdsa_priv_key == 0x83af98f2f2bdea33eb34177b311d89569725a401c1fc4d6046d266b1ca0dc382)
140147
assert(eddsa_priv_key == 0xd5c4d44f0f07aaa0bf18e039f28ec2131935ed696636f48ec46bab58e66296b)
@@ -153,9 +160,9 @@ def test_recovery_with_mobile_share_as_json():
153160

154161

155162
def test_recovery_with_master_keys():
156-
result = recover.restore_key_and_chaincode(TEST_DIR / "backup_with_master_key.zip", TEST_DIR / "priv_ncw.pem", "2#0Iw0Qov@&QP09p", key_pass="SECRET")
157-
ecdsa_priv_key, ecdsa_chaincode = result["MPC_CMP_ECDSA_SECP256K1"]
158-
eddsa_priv_key, eddsa_chaincode = result["MPC_EDDSA_ED25519"]
163+
result = result_list_to_dict(recover.restore_key_and_chaincode(TEST_DIR / "backup_with_master_key.zip", TEST_DIR / "priv_ncw.pem", "2#0Iw0Qov@&QP09p", key_pass="SECRET"))
164+
ecdsa_priv_key, ecdsa_chaincode, _ = result["MPC_CMP_ECDSA_SECP256K1"]
165+
eddsa_priv_key, eddsa_chaincode, _ = result["MPC_EDDSA_ED25519"]
159166

160167
pub = recover.get_public_key("MPC_ECDSA_SECP256K1", ecdsa_priv_key)
161168
assert pub == "033f5e4fb621e4cc777e5b9cdc0ef06c7b55042e9ce6c3bf013daab9fba29b37b8"

utils/metadata.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class SigningKeyMetadata:
1717
public_key: str
1818
algorithm: str
1919
chain_code: bytes
20+
keyset_id: int
2021

2122

2223
@dataclass
@@ -55,13 +56,18 @@ def parse_metadata_file(fp: IO[bytes]) -> RecoveryPackageMetadata:
5556
else:
5657
chain_code_for_this_key = default_chain_code
5758

59+
keyset_id_for_this_key = 1
60+
if "keysetId" in key_metadata:
61+
keyset_id_for_this_key = int(key_metadata["keysetId"])
62+
5863
if len(chain_code_for_this_key) != 32:
5964
raise RecoveryErrorUnknownChainCode()
6065

6166
signing_keys[key_id] = SigningKeyMetadata(
6267
public_key=metadata_public_key,
6368
algorithm=algo,
64-
chain_code=chain_code_for_this_key
69+
chain_code=chain_code_for_this_key,
70+
keyset_id=keyset_id_for_this_key
6571
)
6672

6773
master_keys_in_backup = obj.get("masterKeys", {})

utils/recover.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def calculate_keys(key_id, player_to_data, algo):
150150
raise RecoveryErrorUnknownAlgorithm(algo)
151151

152152
def restore_key_and_chaincode(zip_path, private_pem_path, passphrase, key_pass=None, mobile_key_pem_path = None, mobile_key_pass = None):
153-
privkeys = {}
153+
privkeys = []
154154
players_data = defaultdict(dict)
155155
signing_keys = {}
156156

@@ -237,9 +237,9 @@ def restore_key_and_chaincode(zip_path, private_pem_path, passphrase, key_pass=N
237237
pub_from_metadata = signing_keys[key_id].public_key
238238
if pub_from_metadata != pubkey_str:
239239
print(f"Failed to recover {algo} key, expected public key is: {pub_from_metadata} calculated public key is: {pubkey_str}")
240-
privkeys[algo] = None
240+
privkeys.append((algo, None, None, None))
241241
else:
242-
privkeys[algo] = privkey, chain_code_for_this_key
242+
privkeys.append((algo, privkey, chain_code_for_this_key, signing_keys[key_id].keyset_id))
243243

244244
if len(privkeys) == 0:
245245
raise RecoveryErrorPublicKeyNoMatch()

0 commit comments

Comments
 (0)