diff --git a/tests/standard/fido2/__init__.py b/tests/standard/fido2/__init__.py index 8de700c..e1b9e65 100644 --- a/tests/standard/fido2/__init__.py +++ b/tests/standard/fido2/__init__.py @@ -1,17 +1,80 @@ import time +from functools import cmp_to_key import tests +from fido2 import cbor + +def cbor_key_to_representative(key): + if isinstance(key, int): + if key >= 0: + return (0, key) + return (1, -key) + elif isinstance(key, bytes): + return (2, key) + elif isinstance(key, str): + return (3, key) + else: + raise ValueError(key) -def reset(): - print("Resetting Authenticator...") - dev = tests.get_device() - try: - dev.ctap2.reset() - except CtapError: - # Some authenticators need a power cycle - print("You must power cycle authentictor. Hit enter when done.") - input() - time.sleep(0.2) - dev = tests.get_device(refresh=True) - dev.ctap2.reset() +def cbor_str_cmp(a, b): + if isinstance(a, str) or isinstance(b, str): + a = a.encode("utf8") + b = b.encode("utf8") + + if len(a) == len(b): + for x, y in zip(a, b): + if x != y: + return x - y + return 0 + else: + return len(a) - len(b) + + +def cmp_cbor_keys(a, b): + a = cbor_key_to_representative(a) + b = cbor_key_to_representative(b) + if a[0] != b[0]: + return a[0] - b[0] + if a[0] in (2, 3): + return cbor_str_cmp(a[1], b[1]) + else: + return (a[1] > b[1]) - (a[1] < b[1]) + + +def TestCborKeysSorted(cbor_obj): + # Cbor canonical ordering of keys. + # https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form + + if isinstance(cbor_obj, bytes): + cbor_obj = cbor.loads(cbor_obj)[0] + + if isinstance(cbor_obj, dict): + l = [x for x in cbor_obj] + else: + l = cbor_obj + + l_sorted = sorted(l[:], key=cmp_to_key(cmp_cbor_keys)) + + for i in range(len(l)): + + if not isinstance(l[i], (str, int)): + raise ValueError(f"Cbor map key {l[i]} must be int or str for CTAP2") + + if l[i] != l_sorted[i]: + raise ValueError(f"Cbor map item {i}: {l[i]} is out of order") + + return l + + +# hot patch cbor map parsing to test the order of keys in map +_load_map_old = cbor.load_map + + +def _load_map_new(ai, data): + values, data = _load_map_old(ai, data) + TestCborKeysSorted(values) + return values, data + +cbor.load_map = _load_map_new +cbor._DESERIALIZERS[5] = _load_map_new diff --git a/tests/standard/fido2/test_getinfo.py b/tests/standard/fido2/test_getinfo.py index a848632..9a13e73 100644 --- a/tests/standard/fido2/test_getinfo.py +++ b/tests/standard/fido2/test_getinfo.py @@ -3,6 +3,7 @@ from fido2 import cbor from fido2.ctap import CtapError from tests.utils import * +from tests.standard.fido2 import TestCborKeysSorted def test_get_info(info): @@ -35,3 +36,38 @@ def test_Check_up_option(device, info): with pytest.raises(CtapError) as e: device.sendMC(*FidoRequest(options={"up": True}).toMC()) assert e.value.code == CtapError.ERR.INVALID_OPTION + +def test_self_cbor_sorting(): + cbor_key_list_sorted = [ + 0, + 1, + 1, + 2, + 3, + -1, + -2, + "b", + "c", + "aa", + "aaa", + "aab", + "baa", + "bbb", + ] + TestCborKeysSorted(cbor_key_list_sorted) + +def test_self_cbor_integers(): + with pytest.raises(ValueError) as e: + TestCborKeysSorted([1, 0]) + +def test_self_cbor_major_type(): + with pytest.raises(ValueError) as e: + TestCborKeysSorted([-1, 0]) + +def test_self_cbor_strings(): + with pytest.raises(ValueError) as e: + TestCborKeysSorted(["bb", "a"]) + +def test_self_cbor_same_length_strings(): + with pytest.raises(ValueError) as e: + TestCborKeysSorted(["ab", "aa"]) \ No newline at end of file