diff --git a/tests/vendor/solo/test_solo.py b/tests/vendor/solo/test_solo.py index 1a70ac7..ea20ef8 100644 --- a/tests/vendor/solo/test_solo.py +++ b/tests/vendor/solo/test_solo.py @@ -1,14 +1,35 @@ import sys import pytest +import ecdsa +import hashlib from fido2.ctap1 import ApduError from fido2.ctap2 import CtapError -from fido2.utils import sha256 -from solo.client import SoloClient +from fido2.utils import hmac_sha256, sha256 +try: + from solo.client import SoloClient +except: + from solo.devices.solo_v1 import Client as SoloClient + from solo.commands import SoloExtension from tests.utils import shannon_entropy, verify, FidoRequest +# Is there no RFC for this? +def keypair_from_seed(seed: bytes): + assert isinstance(seed, bytes) + assert len(seed) == 32 + + P256 = ecdsa.NIST256p + scalar = int.from_bytes(seed, "little") + iterations = 0 + while not (1 <= scalar < P256.order): + seed = sha256(seed) + scalar = int.from_bytes(seed, "little") + iterations += 1 + + keypair = ecdsa.SigningKey.from_secret_exponent(scalar, P256) + return keypair, iterations @pytest.fixture(scope="module", params=["u2f"]) def solo(request, device): @@ -76,7 +97,24 @@ class TestSolo(object): def test_load_external_key_wrong_length(self,solo, ): ext_key_cmd = 0x62 with pytest.raises(CtapError) as e: - solo.send_data_hid(ext_key_cmd, b'\x01\x00\x00\x00' + b'wrong length'*2) + solo.send_data_hid(ext_key_cmd, b'\x01' + b'wrong length'*2) + assert(e.value.code == CtapError.ERR.INVALID_LENGTH) + + @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") + def test_load_external_key_wrong_length_ext_state(self,solo, ): + + key_A = b'A' * 32 + key_B = b'B' * 32 + ext_state1 = b"C" * 43 + ext_state2 = b"C" * 44 + version = b'\x01' + + ext_key_cmd = 0x62 + print ('Enter user presence THREE times.') + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state1) + + with pytest.raises(CtapError) as e: + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state2) assert(e.value.code == CtapError.ERR.INVALID_LENGTH) @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") @@ -84,7 +122,7 @@ class TestSolo(object): ext_key_cmd = 0x62 verify(MCRes, GARes) print ('Enter user presence THREE times.') - solo.send_data_hid(ext_key_cmd, b'\x01\x00\x00\x00' + b'Z' * 96) + solo.send_data_hid(ext_key_cmd, b'\x01' + b'Z' * 32 + b'dicekeys key') # Old credential should not exist now. with pytest.raises(CtapError) as e: @@ -93,14 +131,18 @@ class TestSolo(object): assert(e.value.code == CtapError.ERR.NO_CREDENTIALS) + @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") def test_load_external_key(self,solo, device,): - key_A = b'A' * 96 - key_B = b'B' * 96 + key_A = b'A' * 32 + key_B = b'B' * 32 + ext_state = b"I'm a dicekey key" + version = b'\x01' + ext_key_cmd = 0x62 print ('Enter user presence THREE times.') - solo.send_data_hid(ext_key_cmd, b'\x01\x00\x00\x00' + key_A) + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) # New credential works. mc_A_req = FidoRequest() @@ -114,14 +156,89 @@ class TestSolo(object): # Load up Key B and verify cred A doesn't exist. print ('Enter user presence THREE times.') - solo.send_data_hid(ext_key_cmd, b'\x01\x00\x00\x00' + key_B) + solo.send_data_hid(ext_key_cmd, version + key_B + ext_state) with pytest.raises(CtapError) as e: ga_A_res = device.sendGA(*FidoRequest(ga_A_req).toGA()) assert(e.value.code == CtapError.ERR.NO_CREDENTIALS) # Load up Key A and verify cred A is back. print ('Enter user presence THREE times.') - solo.send_data_hid(ext_key_cmd, b'\x01\x00\x00\x00' + key_A) + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) ga_A_res = device.sendGA(*FidoRequest(ga_A_req).toGA()) verify(mc_A_res, ga_A_res, ga_A_req.cdh) + @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") + def test_ext_state_in_credential_id(self,solo, device,): + + key_A = b'A' * 32 + ext_state = b"I'm a dicekey key abc1234!!@@##" + version = b'\x01' + + ext_key_cmd = 0x62 + print ('Enter user presence THREE times.') + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) + + # New credential works. + mc_A_req = FidoRequest() + mc_A_res = device.sendMC(*mc_A_req.toMC()) + + assert ext_state in mc_A_res.auth_data.credential_data.credential_id + + @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") + def test_backup_credential_is_generated_correctly(self,solo, device,): + + key_A = b'A' * 32 + ext_state = b"I'm a dicekey key!" + version = b'\x01' + + ext_key_cmd = 0x62 + print ('Enter user presence THREE times.') + solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) + + # New credential works. + mc_A_req = FidoRequest() + mc_A_res = device.sendMC(*mc_A_req.toMC()) + + credId = mc_A_res.auth_data.credential_data.credential_id + uniqueId = credId[1:1+32] + + extStateInCredId = credId[33:33 + len(ext_state)] + + soloExtStateFieldSize = 43 + credMacInCredId = credId[33 + soloExtStateFieldSize: 33 + soloExtStateFieldSize + 32] + + rpIdHash = sha256(mc_A_req.rp['id'].encode('utf8')) + print("rp:", mc_A_req.rp['id']) + from binascii import hexlify + print("rpIdHash:", hexlify(rpIdHash)) + print("uniqueId:", hexlify(uniqueId)) + + assert version[0] == credId[0] + assert ext_state == extStateInCredId + + credMac = hmac_sha256(key_A, rpIdHash + version + uniqueId + ext_state) + print("recomputed mac:", hexlify(credMac)) + print("recv'd mac :", hexlify(credMacInCredId)) + + assert credMac == credMacInCredId + + credentialSeed = hmac_sha256(key_A, credMac) + print('Computed private key:', hexlify(credentialSeed)) + key_pair = keypair_from_seed(credentialSeed) + assert key_pair[1] == 0 + key_pair = key_pair[0] + + allow_list = [{"id": mc_A_res.auth_data.credential_data.credential_id, "type": "public-key"}] + ga_req = FidoRequest(allow_list = allow_list) + + ga_res = device.sendGA(*ga_req.toGA()) + + verify(mc_A_res, ga_res, ga_req.cdh) + + key_pair.verifying_key.verify( + ga_res.signature, + ga_res.auth_data + ga_req.cdh, + sigdecode=ecdsa.util.sigdecode_der, + hashfunc=hashlib.sha256 + ) +