from __future__ import print_function, absolute_import, unicode_literals
import time
from random import randint
import array
from functools import cmp_to_key

from fido2 import cbor
from fido2.ctap import CtapError

from fido2.ctap2 import ES256, PinProtocolV1, AttestedCredentialData
from fido2.utils import sha256, hmac_sha256
from fido2.attestation import Attestation

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

from .u2f import U2FTests
from .tester import Tester, Test
from .util import shannon_entropy

rp = {"id": "examplo.org", "name": "ExaRP"}
rp2 = {"id": "solokeys.com", "name": "ExaRP"}
user = {"id": b"usee_od", "name": "AB User"}
user1 = {"id": b"1234567890", "name": "Conor Patrick"}
user2 = {"id": b"oiewhfoi", "name": "Han Solo"}
user3 = {"id": b"23ohfpjwo@@", "name": "John Smith"}
challenge = "Y2hhbGxlbmdl"
pin_protocol = 1
key_params = [{"type": "public-key", "alg": ES256.ALGORITHM}]
cdh = b"123456789abcdef0123456789abcdef0"


def VerifyAttestation(attest, data):
    verifier = Attestation.for_type(attest.fmt)
    verifier().verify(attest.att_statement, attest.auth_data, data.hash)


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 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.decode_from(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


class FIDO2Tests(Tester):
    def __init__(self, tester=None):
        super().__init__(tester)
        self.self_test()

    def self_test(self,):
        cbor_key_list_sorted = [
            0,
            1,
            1,
            2,
            3,
            -1,
            -2,
            "b",
            "c",
            "aa",
            "aaa",
            "aab",
            "baa",
            "bbb",
        ]
        with Test("Self test CBOR sorting"):
            TestCborKeysSorted(cbor_key_list_sorted)

        with Test("Self test CBOR sorting integers", catch=ValueError):
            TestCborKeysSorted([1, 0])

        with Test("Self test CBOR sorting major type", catch=ValueError):
            TestCborKeysSorted([-1, 0])

        with Test("Self test CBOR sorting strings", catch=ValueError):
            TestCborKeysSorted(["bb", "a"])

        with Test("Self test CBOR sorting same length strings", catch=ValueError):
            TestCborKeysSorted(["ab", "aa"])

    def run(self,):
        self.test_fido2()

    def test_fido2_simple(self, pin_token=None):
        creds = []
        exclude_list = []
        PIN = pin_token

        fake_id1 = array.array("B", [randint(0, 255) for i in range(0, 150)]).tobytes()
        fake_id2 = array.array("B", [randint(0, 255) for i in range(0, 73)]).tobytes()

        exclude_list.append({"id": fake_id1, "type": "public-key"})
        exclude_list.append({"id": fake_id2, "type": "public-key"})

        t1 = time.time() * 1000
        attest, data = self.client.make_credential(
            rp, user, challenge, pin=PIN, exclude_list=[]
        )
        t2 = time.time() * 1000
        VerifyAttestation(attest, data)
        print("Register time: %d ms" % (t2 - t1))

        cred = attest.auth_data.credential_data
        creds.append(cred)

        allow_list = [{"id": creds[0].credential_id, "type": "public-key"}]
        t1 = time.time() * 1000
        assertions, client_data = self.client.get_assertion(
            rp["id"], challenge, allow_list, pin=PIN
        )
        t2 = time.time() * 1000
        assertions[0].verify(client_data.hash, creds[0].public_key)

        print("Assertion time: %d ms" % (t2 - t1))

    def test_extensions(self,):

        salt1 = b"\x5a" * 32
        salt2 = b"\x96" * 32
        salt3 = b"\x03" * 32

        # self.testReset()

        with Test("Get info has hmac-secret"):
            info = self.ctap.get_info()
            assert "hmac-secret" in info.extensions

        reg = self.testMC(
            "Send MC with hmac-secret ext set to true, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
            other={"extensions": {"hmac-secret": True}, "options": {"rk": True}},
        )

        with Test("Check 'hmac-secret' is set to true in auth_data extensions"):
            assert reg.auth_data.extensions
            assert "hmac-secret" in reg.auth_data.extensions
            assert reg.auth_data.extensions["hmac-secret"] == True

        reg = self.testMC(
            "Send MC with fake extension set to true, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
            other={"extensions": {"tetris": True}},
        )

        with Test("Get shared secret"):
            key_agreement, shared_secret = (
                self.client.pin_protocol._init_shared_secret()
            )
            cipher = Cipher(
                algorithms.AES(shared_secret),
                modes.CBC(b"\x00" * 16),
                default_backend(),
            )

        def get_salt_params(salts):
            enc = cipher.encryptor()
            salt_enc = b""
            for salt in salts:
                salt_enc += enc.update(salt)
            salt_enc += enc.finalize()

            salt_auth = hmac_sha256(shared_secret, salt_enc)[:16]
            return salt_enc, salt_auth

        for salt_list in ((salt1,), (salt1, salt2)):
            salt_enc, salt_auth = get_salt_params(salt_list)

            auth = self.testGA(
                "Send GA request with %d salts hmac-secret, expect success"
                % len(salt_list),
                rp["id"],
                cdh,
                other={
                    "extensions": {
                        "hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}
                    }
                },
                expectedError=CtapError.ERR.SUCCESS,
            )

            with Test(
                "Check that hmac-secret is in auth_data extensions and has %d bytes"
                % (len(salt_list) * 32)
            ):
                ext = auth.auth_data.extensions
                assert ext
                assert "hmac-secret" in ext
                assert isinstance(ext["hmac-secret"], bytes)
                assert len(ext["hmac-secret"]) == len(salt_list) * 32

            with Test("Check that shannon_entropy of hmac-secret is good"):
                ext = auth.auth_data.extensions
                dec = cipher.decryptor()
                key = dec.update(ext["hmac-secret"]) + dec.finalize()

                print(shannon_entropy(ext["hmac-secret"]))
                if len(salt_list) == 1:
                    assert shannon_entropy(ext["hmac-secret"]) > 4.6
                    assert shannon_entropy(key) > 4.6
                if len(salt_list) == 2:
                    assert shannon_entropy(ext["hmac-secret"]) > 5.4
                    assert shannon_entropy(key) > 5.4

        salt_enc, salt_auth = get_salt_params((salt3,))

        auth = self.testGA(
            "Send GA request with hmac-secret missing keyAgreement, expect error",
            rp["id"],
            cdh,
            other={"extensions": {"hmac-secret": {2: salt_enc, 3: salt_auth}}},
        )
        auth = self.testGA(
            "Send GA request with hmac-secret missing saltAuth, expect MISSING_PARAMETER",
            rp["id"],
            cdh,
            other={"extensions": {"hmac-secret": {1: key_agreement, 2: salt_enc}}},
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )
        auth = self.testGA(
            "Send GA request with hmac-secret missing saltEnc, expect MISSING_PARAMETER",
            rp["id"],
            cdh,
            other={"extensions": {"hmac-secret": {1: key_agreement, 3: salt_auth}}},
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        bad_auth = list(salt_auth[:])
        bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1
        bad_auth = bytes(bad_auth)

        auth = self.testGA(
            "Send GA request with hmac-secret containing bad saltAuth, expect EXTENSION_FIRST",
            rp["id"],
            cdh,
            other={
                "extensions": {
                    "hmac-secret": {1: key_agreement, 2: salt_enc, 3: bad_auth}
                }
            },
            expectedError=CtapError.ERR.EXTENSION_FIRST,
        )

        salt4 = b"\x5a" * 16
        salt5 = b"\x96" * 64
        for salt_list in ((salt4,), (salt4, salt5)):
            salt_enc, salt_auth = get_salt_params(salt_list)

            salt_auth = hmac_sha256(shared_secret, salt_enc)[:16]
            auth = self.testGA(
                "Send GA request with incorrect salt length %d, expect INVALID_LENGTH"
                % len(salt_enc),
                rp["id"],
                cdh,
                other={
                    "extensions": {
                        "hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}
                    }
                },
                expectedError=CtapError.ERR.INVALID_LENGTH,
            )

    def test_get_info(self,):
        with Test("Get info"):
            info = self.ctap.get_info()
            print("data:", bytes(info))
            print("decoded:", cbor.decode_from(bytes(info)))

        with Test("Check FIDO2 string is in VERSIONS field"):
            assert "FIDO_2_0" in info.versions

        with Test("Check pin protocols field"):
            if len(info.pin_protocols):
                assert sum(info.pin_protocols) > 0

        with Test("Check options field"):
            for x in info.options:
                assert info.options[x] in [True, False]

        if "uv" in info.options:
            if info.options["uv"]:
                self.testMC(
                    "Send MC request with uv set to true, expect SUCCESS",
                    cdh,
                    rp,
                    user,
                    key_params,
                    other={"options": {"uv": True}},
                    expectedError=CtapError.ERR.SUCCESS,
                )
        if "up" in info.options:
            if info.options["up"]:
                self.testMC(
                    "Send MC request with up set to true, expect INVALID_OPTION",
                    cdh,
                    rp,
                    user,
                    key_params,
                    other={"options": {"up": True}},
                    expectedError=CtapError.ERR.INVALID_OPTION,
                )

    def test_make_credential(self,):

        prev_reg = self.testMC(
            "Send MC request, expect success",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
        )

        allow_list = [
            {
                "id": prev_reg.auth_data.credential_data.credential_id,
                "type": "public-key",
            }
        ]
        with Test("Check attestation format is correct"):
            assert prev_reg.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"]

        with Test("Check auth_data is at least 77 bytes"):
            assert len(prev_reg.auth_data) >= 77

        self.testMC(
            "Send MC request with missing clientDataHash, expect error",
            None,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testMC(
            "Send MC request with integer for clientDataHash, expect error",
            5,
            rp,
            user,
            key_params,
        )

        self.testMC(
            "Send MC request with missing user, expect error",
            cdh,
            rp,
            None,
            key_params,
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testMC(
            "Send MC request with bytearray user, expect error",
            cdh,
            rp,
            b"1234abcd",
            key_params,
        )

        self.testMC(
            "Send MC request with missing RP, expect error",
            cdh,
            None,
            user,
            key_params,
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testMC(
            "Send MC request with bytearray RP, expect error",
            cdh,
            b"1234abcd",
            user,
            key_params,
        )

        self.testMC(
            "Send MC request with missing pubKeyCredParams, expect error",
            cdh,
            rp,
            user,
            None,
        )

        self.testMC(
            "Send MC request with incorrect pubKeyCredParams, expect error",
            cdh,
            rp,
            user,
            b"2356",
        )

        self.testMC(
            "Send MC request with incorrect excludeList, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": 8},
        )

        self.testMC(
            "Send MC request with incorrect extensions, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"extensions": 8},
        )

        self.testMC(
            "Send MC request with incorrect options, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"options": 8},
        )

        self.testMC(
            "Send MC request with bad RP.name",
            cdh,
            {"id": self.host, "name": 8, "icon": "icon"},
            user,
            key_params,
        )

        self.testMC(
            "Send MC request with bad RP.id",
            cdh,
            {"id": 8, "name": "name", "icon": "icon"},
            user,
            key_params,
        )

        self.testMC(
            "Send MC request with bad RP.icon",
            cdh,
            {"id": self.host, "name": "name", "icon": 8},
            user,
            key_params,
        )

        self.testMC(
            "Send MC request with bad user.name",
            cdh,
            rp,
            {"id": b"usee_od", "name": 8},
            key_params,
        )

        self.testMC(
            "Send MC request with bad user.id",
            cdh,
            rp,
            {"id": "usee_od", "name": "name"},
            key_params,
        )

        self.testMC(
            "Send MC request with bad user.displayName",
            cdh,
            rp,
            {"id": "usee_od", "name": "name", "displayName": 8},
            key_params,
        )

        self.testMC(
            "Send MC request with bad user.icon",
            cdh,
            rp,
            {"id": "usee_od", "name": "name", "icon": 8},
            key_params,
        )

        self.testMC(
            "Send MC request with non-map pubKeyCredParams item",
            cdh,
            rp,
            user,
            ["wrong"],
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item missing type field",
            cdh,
            rp,
            user,
            [{"alg": ES256.ALGORITHM}],
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item with bad type field",
            cdh,
            rp,
            user,
            [{"alg": ES256.ALGORITHM, "type": b"public-key"}],
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item missing alg",
            cdh,
            rp,
            user,
            [{"type": "public-key"}],
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item with bad alg",
            cdh,
            rp,
            user,
            [{"alg": "7", "type": "public-key"}],
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item with bogus alg, expect UNSUPPORTED_ALGORITHM",
            cdh,
            rp,
            user,
            [{"alg": 1234, "type": "public-key"}],
            expectedError=CtapError.ERR.UNSUPPORTED_ALGORITHM,
        )

        self.testMC(
            "Send MC request with pubKeyCredParams item with bogus type, expect UNSUPPORTED_ALGORITHM",
            cdh,
            rp,
            user,
            [{"alg": ES256.ALGORITHM, "type": "rot13"}],
            expectedError=CtapError.ERR.UNSUPPORTED_ALGORITHM,
        )

        self.testMC(
            "Send MC request with excludeList item with bogus type, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
            other={"exclude_list": [{"id": b"1234", "type": "rot13"}]},
        )

        self.testMC(
            "Send MC request with excludeList item with bogus type, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
            other={
                "exclude_list": [
                    {"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}
                ]
            },
        )

        self.testMC(
            "Send MC request with excludeList with bad item, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": ["1234"]},
        )

        self.testMC(
            "Send MC request with excludeList with item missing type field, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": [{"id": b"1234"}]},
        )

        self.testMC(
            "Send MC request with excludeList with item missing id field, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": [{"type": "public-key"}]},
        )

        self.testMC(
            "Send MC request with excludeList with item containing bad id field, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": [{"type": "public-key", "id": "1234"}]},
        )

        self.testMC(
            "Send MC request with excludeList with item containing bad type field, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"exclude_list": [{"type": b"public-key", "id": b"1234"}]},
        )

        self.testMC(
            "Send MC request with excludeList containing previous registration, expect CREDENTIAL_EXCLUDED",
            cdh,
            rp,
            user,
            key_params,
            other={
                "exclude_list": [
                    {
                        "type": "public-key",
                        "id": prev_reg.auth_data.credential_data.credential_id,
                    }
                ]
            },
            expectedError=CtapError.ERR.CREDENTIAL_EXCLUDED,
        )

        self.testMC(
            "Send MC request with unknown option, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            other={"options": {"unknown": False}},
            expectedError=CtapError.ERR.SUCCESS,
        )

        self.testReset()

        self.testGA(
            "Send GA request with reset auth, expect NO_CREDENTIALS",
            rp["id"],
            cdh,
            allow_list,
            expectedError=CtapError.ERR.NO_CREDENTIALS,
        )

    def test_get_assertion(self,):

        self.testReset()

        prev_reg = self.testMC(
            "Send MC request, expect success",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.SUCCESS,
        )

        allow_list = [
            {
                "id": prev_reg.auth_data.credential_data.credential_id,
                "type": "public-key",
            }
        ]

        prev_auth = self.testGA(
            "Send GA request, expect success",
            rp["id"],
            cdh,
            allow_list,
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Test auth_data is 37 bytes"):
            assert len(prev_auth.auth_data) == 37

        with Test("Test that auth_data.rpIdHash is correct"):
            assert sha256(rp["id"].encode()) == prev_auth.auth_data.rp_id_hash

        with Test("Check that AT flag is not set"):
            assert (prev_auth.auth_data.flags & 0xF8) == 0

        with Test("Test that user, credential and numberOfCredentials are not present"):
            assert prev_auth.user == None
            assert prev_auth.number_of_credentials == None

        self.testGA(
            "Send GA request with empty allow_list, expect NO_CREDENTIALS",
            rp["id"],
            cdh,
            [],
            expectedError=CtapError.ERR.NO_CREDENTIALS,
        )

        # apply bit flip
        badid = list(prev_reg.auth_data.credential_data.credential_id[:])
        badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1
        badid = bytes(badid)

        self.testGA(
            "Send GA request with corrupt credId in allow_list, expect NO_CREDENTIALS",
            rp["id"],
            cdh,
            [{"id": badid, "type": "public-key"}],
            expectedError=CtapError.ERR.NO_CREDENTIALS,
        )

        self.testGA(
            "Send GA request with missing RPID, expect MISSING_PARAMETER",
            None,
            cdh,
            allow_list,
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testGA(
            "Send GA request with bad RPID, expect error",
            {"type": "wrong"},
            cdh,
            allow_list,
        )

        self.testGA(
            "Send GA request with missing clientDataHash, expect MISSING_PARAMETER",
            rp["id"],
            None,
            allow_list,
            expectedError=CtapError.ERR.MISSING_PARAMETER,
        )

        self.testGA(
            "Send GA request with bad clientDataHash, expect error",
            rp["id"],
            {"type": "wrong"},
            allow_list,
        )

        self.testGA(
            "Send GA request with bad allow_list, expect error",
            rp["id"],
            cdh,
            {"type": "wrong"},
        )

        self.testGA(
            "Send GA request with bad item in allow_list, expect error",
            rp["id"],
            cdh,
            allow_list + ["wrong"],
        )

        self.testGA(
            "Send GA request with unknown option, expect SUCCESS",
            rp["id"],
            cdh,
            allow_list,
            other={"options": {"unknown": True}},
            expectedError=CtapError.ERR.SUCCESS,
        )
        with Test("Get info"):
            info = self.ctap.get_info()

        if "uv" in info.options:
            if info.options["uv"]:
                res = self.testGA(
                    "Send GA request with uv set to true, expect SUCCESS",
                    rp["id"],
                    cdh,
                    allow_list,
                    other={"options": {"uv": True}},
                    expectedError=CtapError.ERR.SUCCESS,
                )
                with Test("Check that UV flag is set in response"):
                    assert res.auth_data.flags & (1 << 2)
        if "up" in info.options:
            if info.options["up"]:
                res = self.testGA(
                    "Send GA request with up set to true, expect SUCCESS",
                    rp["id"],
                    cdh,
                    allow_list,
                    other={"options": {"up": True}},
                    expectedError=CtapError.ERR.SUCCESS,
                )
            with Test("Check that UP flag is set in response"):
                assert res.auth_data.flags & 1

        self.testGA(
            "Send GA request with bogus type item in allow_list, expect SUCCESS",
            rp["id"],
            cdh,
            allow_list + [{"type": "rot13", "id": b"1234"}],
            expectedError=CtapError.ERR.SUCCESS,
        )

        self.testGA(
            "Send GA request with item missing type field in allow_list, expect error",
            rp["id"],
            cdh,
            allow_list + [{"id": b"1234"}],
        )

        self.testGA(
            "Send GA request with item containing bad type field in allow_list, expect error",
            rp["id"],
            cdh,
            allow_list + [{"type": b"public-key", "id": b"1234"}],
        )

        self.testGA(
            "Send GA request with item containing bad id in allow_list, expect error",
            rp["id"],
            cdh,
            allow_list + [{"type": b"public-key", "id": 42}],
        )

        self.testGA(
            "Send GA request with item missing id in allow_list, expect error",
            rp["id"],
            cdh,
            allow_list + [{"type": b"public-key"}],
        )

        self.testReset()

        appid = sha256(rp["id"].encode("utf8"))
        chal = sha256(challenge.encode("utf8"))
        with Test("Send CTAP1 register request"):
            u2f = U2FTests(self)
            reg = u2f.register(chal, appid)
            reg.verify(appid, chal)

        with Test("Authenticate CTAP1"):
            auth = u2f.authenticate(chal, appid, reg.key_handle)
            auth.verify(appid, chal, reg.public_key)

        auth = self.testGA(
            "Authenticate CTAP1 registration with CTAP2",
            rp["id"],
            cdh,
            [{"id": reg.key_handle, "type": "public-key"}],
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check assertion is correct"):
            credential_data = AttestedCredentialData.from_ctap1(
                reg.key_handle, reg.public_key
            )
            auth.verify(cdh, credential_data.public_key)
            assert auth.credential["id"] == reg.key_handle

    def test_rk(self, pin_code=None):

        pin_auth = None
        if pin_code:
            pin_protocol = 1
        else:
            pin_protocol = None
        if pin_code:
            with Test("Set pin code"):
                self.client.pin_protocol.set_pin(pin_code)
                pin_token = self.client.pin_protocol.get_pin_token(pin_code)
                pin_auth = hmac_sha256(pin_token, cdh)[:16]

        self.testMC(
            "Send MC request with rk option set to true, expect SUCCESS",
            cdh,
            rp,
            user,
            key_params,
            other={
                "options": {"rk": True},
                "pin_auth": pin_auth,
                "pin_protocol": pin_protocol,
            },
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Get info"):
            info = self.ctap.get_info()

        options = {"rk": True}
        if "uv" in info.options and info.options["uv"]:
            options["uv"] = False

        for i, x in enumerate([user1, user2, user3]):
            self.testMC(
                "Send MC request with rk option set to true, expect SUCCESS %d/3"
                % (i + 1),
                cdh,
                rp2,
                x,
                key_params,
                other={
                    "options": options,
                    "pin_auth": pin_auth,
                    "pin_protocol": pin_protocol,
                },
                expectedError=CtapError.ERR.SUCCESS,
            )

        auth1 = self.testGA(
            "Send GA request with no allow_list, expect SUCCESS",
            rp2["id"],
            cdh,
            other={"pin_auth": pin_auth, "pin_protocol": pin_protocol},
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check that there are 3 credentials returned"):
            assert auth1.number_of_credentials == 3

        with Test("Get the next 2 assertions"):
            auth2 = self.ctap.get_next_assertion()
            auth3 = self.ctap.get_next_assertion()

        if not pin_code:
            with Test("Check only the user ID was returned"):
                assert "id" in auth1.user.keys() and len(auth1.user.keys()) == 1
                assert "id" in auth2.user.keys() and len(auth2.user.keys()) == 1
                assert "id" in auth3.user.keys() and len(auth3.user.keys()) == 1
        else:
            with Test("Check that all user info was returned"):
                for x in (auth1, auth2, auth3):
                    for y in ("name", "icon", "displayName", "id"):
                        if y not in x.user.keys():
                            print("FAIL: %s was not in user: " % y, x.user)

        with Test("Send an extra getNextAssertion request, expect error"):
            try:
                self.ctap.get_next_assertion()
                assert 0
            except CtapError as e:
                print(e)

    def test_client_pin(self,):
        pin1 = "1234567890"
        self.test_rk(pin1)

        # PinProtocolV1
        res = self.testCP(
            "Test getKeyAgreement, expect SUCCESS",
            pin_protocol,
            PinProtocolV1.CMD.GET_KEY_AGREEMENT,
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Test getKeyAgreement has appropriate fields"):
            key = res[1]
            assert "Is public key" and key[1] == 2
            assert "Is P256" and key[-1] == 1
            assert "Is ALG_ECDH_ES_HKDF_256" and key[3] == -25

            assert "Right key" and len(key[-3]) == 32 and isinstance(key[-3], bytes)

        with Test("Test setting a new pin"):
            pin2 = "qwertyuiop\x11\x22\x33\x00123"
            self.client.pin_protocol.change_pin(pin1, pin2)

        with Test("Test getting new pin_auth"):
            pin_token = self.client.pin_protocol.get_pin_token(pin2)
            pin_auth = hmac_sha256(pin_token, cdh)[:16]

        res_mc = self.testMC(
            "Send MC request with new pin auth",
            cdh,
            rp,
            user,
            key_params,
            other={"pin_auth": pin_auth, "pin_protocol": pin_protocol},
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check UV flag is set"):
            assert res_mc.auth_data.flags & (1 << 2)

        res_ga = self.testGA(
            "Send GA request with pinAuth, expect SUCCESS",
            rp["id"],
            cdh,
            [
                {
                    "type": "public-key",
                    "id": res_mc.auth_data.credential_data.credential_id,
                }
            ],
            other={"pin_auth": pin_auth, "pin_protocol": pin_protocol},
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check UV flag is set"):
            assert res_ga.auth_data.flags & (1 << 2)

        res_ga = self.testGA(
            "Send GA request with no pinAuth, expect SUCCESS",
            rp["id"],
            cdh,
            [
                {
                    "type": "public-key",
                    "id": res_mc.auth_data.credential_data.credential_id,
                }
            ],
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check UV flag is NOT set"):
            assert not (res_ga.auth_data.flags & (1 << 2))

        self.testReset()

        with Test("Test sending zero-length pin_auth, expect PIN_NOT_SET"):
            self.testMC(
                "Send MC request with new pin auth",
                cdh,
                rp,
                user,
                key_params,
                other={"pin_auth": b"", "pin_protocol": pin_protocol},
                expectedError=CtapError.ERR.PIN_NOT_SET,
            )
            self.testGA(
                "Send MC request with new pin auth",
                rp["id"],
                cdh,
                other={"pin_auth": b"", "pin_protocol": pin_protocol},
                expectedError=CtapError.ERR.PIN_NOT_SET,
            )

        with Test("Setting pin code, expect SUCCESS"):
            self.client.pin_protocol.set_pin(pin1)

        with Test("Test sending zero-length pin_auth, expect PIN_INVALID"):
            self.testMC(
                "Send MC request with new pin auth",
                cdh,
                rp,
                user,
                key_params,
                other={"pin_auth": b"", "pin_protocol": pin_protocol},
                expectedError=CtapError.ERR.PIN_INVALID,
            )
            self.testGA(
                "Send MC request with new pin auth",
                rp["id"],
                cdh,
                other={"pin_auth": b"", "pin_protocol": pin_protocol},
                expectedError=CtapError.ERR.PIN_INVALID,
            )

        self.testReset()
        with Test("Setting pin code >63 bytes, expect POLICY_VIOLATION "):
            try:
                self.client.pin_protocol.set_pin("A" * 64)
                assert 0
            except CtapError as e:
                assert e.code == CtapError.ERR.PIN_POLICY_VIOLATION

        with Test("Get pin token when no pin is set, expect PIN_NOT_SET"):
            try:
                self.client.pin_protocol.get_pin_token(pin1)
                assert 0
            except CtapError as e:
                assert e.code == CtapError.ERR.PIN_NOT_SET

        with Test("Get change pin when no pin is set, expect PIN_NOT_SET"):
            try:
                self.client.pin_protocol.change_pin(pin1, "1234")
                assert 0
            except CtapError as e:
                assert e.code == CtapError.ERR.PIN_NOT_SET

        with Test("Setting pin code and get pin_token, expect SUCCESS"):
            self.client.pin_protocol.set_pin(pin1)
            pin_token = self.client.pin_protocol.get_pin_token(pin1)
            pin_auth = hmac_sha256(pin_token, cdh)[:16]

        with Test("Get info and assert that clientPin is set to true"):
            info = self.ctap.get_info()
            assert info.options["clientPin"]

        with Test("Test setting pin again fails"):
            try:
                self.client.pin_protocol.set_pin(pin1)
                assert 0
            except CtapError as e:
                print(e)

        res_mc = self.testMC(
            "Send MC request with no pin_auth, expect PIN_REQUIRED",
            cdh,
            rp,
            user,
            key_params,
            expectedError=CtapError.ERR.PIN_REQUIRED,
        )

        res_mc = self.testGA(
            "Send GA request with no pin_auth, expect NO_CREDENTIALS",
            rp["id"],
            cdh,
            expectedError=CtapError.ERR.NO_CREDENTIALS,
        )

        res = self.testCP(
            "Test getRetries, expect SUCCESS",
            pin_protocol,
            PinProtocolV1.CMD.GET_RETRIES,
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Check there is 8 pin attempts left"):
            assert res[3] == 8

        # Flip 1 bit
        pin_wrong = list(pin1)
        c = pin1[len(pin1) // 2]

        pin_wrong[len(pin1) // 2] = chr(ord(c) ^ 1)
        pin_wrong = "".join(pin_wrong)

        for i in range(1, 3):
            self.testPP(
                "Get pin_token with wrong pin code, expect PIN_INVALID (%d/2)" % i,
                pin_wrong,
                expectedError=CtapError.ERR.PIN_INVALID,
            )
            print("Check there is %d pin attempts left" % (8 - i))
            res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES)
            assert res[3] == (8 - i)
            print("Pass")

        for i in range(1, 3):
            self.testPP(
                "Get pin_token with wrong pin code, expect PIN_AUTH_BLOCKED %d/2" % i,
                pin_wrong,
                expectedError=CtapError.ERR.PIN_AUTH_BLOCKED,
            )

        self.reboot()

        with Test("Get pin_token, expect SUCCESS"):
            pin_token = self.client.pin_protocol.get_pin_token(pin1)
            pin_auth = hmac_sha256(pin_token, cdh)[:16]

        res_mc = self.testMC(
            "Send MC request with correct pin_auth",
            cdh,
            rp,
            user,
            key_params,
            other={"pin_auth": pin_auth, "pin_protocol": pin_protocol},
            expectedError=CtapError.ERR.SUCCESS,
        )

        with Test("Test getRetries resets to 8"):
            res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES)
            assert res[3] == (8)

        for i in range(1, 10):
            err = CtapError.ERR.PIN_INVALID
            if i in (3, 6):
                err = CtapError.ERR.PIN_AUTH_BLOCKED
            elif i >= 8:
                err = [CtapError.ERR.PIN_BLOCKED, CtapError.ERR.PIN_INVALID]
            self.testPP(
                "Lock out authentictor and check correct error codes %d/9" % i,
                pin_wrong,
                expectedError=err,
            )

            attempts = 8 - i
            if i > 8:
                attempts = 0

            with Test("Check there is %d pin attempts left" % attempts):
                res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES)
                assert res[3] == attempts

            if err == CtapError.ERR.PIN_AUTH_BLOCKED:
                self.reboot()

        res_mc = self.testMC(
            "Send MC request with correct pin_auth, expect error",
            cdh,
            rp,
            user,
            key_params,
            other={"pin_auth": pin_auth, "pin_protocol": pin_protocol},
        )

        self.reboot()

        self.testPP(
            "Get pin_token with correct pin code, expect PIN_BLOCKED",
            pin1,
            expectedError=CtapError.ERR.PIN_BLOCKED,
        )

    def test_fido2(self,):

        self.testReset()

        self.test_get_info()

        self.test_get_assertion()

        self.test_make_credential()

        self.test_rk(None)

        self.test_client_pin()

        self.testReset()

        self.test_extensions()

        print("Done")
