Files

259 lines
9.5 KiB
Python

import sys
import pytest
from cryptography.exceptions import InvalidSignature
from fido2.ctap import CtapError
from fido2.utils import hmac_sha256, sha256
from tests.utils import *
class TestGetAssertion(object):
def test_get_assertion(self, device, MCRes, GARes):
verify(MCRes, GARes)
def test_assertion_auth_data(self, GARes):
assert len(GARes.auth_data) == 37
assert sha256(GARes.request.rp["id"].encode()) == GARes.auth_data.rp_id_hash
def test_Check_that_AT_flag_is_not_set(self, GARes):
assert (GARes.auth_data.flags & 0xF8) == 0
def test_that_user_credential_and_numberOfCredentials_are_not_present(self, GARes):
assert GARes.user == None
assert GARes.number_of_credentials == None
def test_empty_allowList(self, device):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(allow_list=[]).toGA())
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_get_assertion_allow_list_filtering_and_buffering(self, device):
""" Check that authenticator filters and stores items in allow list correctly """
allow_list = []
rp1 = {"id": "rp1.com", "name": "rp1.com"}
rp2 = {"id": "rp2.com", "name": "rp2.com"}
req1 = FidoRequest(rp=rp1)
req2 = FidoRequest(rp=rp2)
rp1_registrations = []
rp2_registrations = []
rp1_assertions = []
rp2_assertions = []
l1 = 4
for i in range(0, l1):
res = device.sendMC(*req1.toMC())
rp1_registrations.append(res)
allow_list.append({
"id": res.auth_data.credential_data.credential_id[:],
"type": "public-key",
})
l2 = 6
for i in range(0, l2):
res = device.sendMC(*req2.toMC())
rp2_registrations.append(res)
allow_list.append({
"id": res.auth_data.credential_data.credential_id[:],
"type": "public-key",
})
req1 = FidoRequest(req1, allow_list = allow_list)
req2 = FidoRequest(req2, allow_list = allow_list)
# CTAP 2.1: If allowlist is passed, only one (any) applicable
# credential signs, and numberOfCredentials = None is returned.
# <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#:~:text=If%20the%20allowList%20parameter%20is%20present%3A,Go%20to%20Step%2013>
#
# CTAP 2.0: Expects the authenticator to return the total number
# even when allowlist is passed (and hence keep the credential IDs
# cached.
# Should authenticate to all credentials matching rp1
ga_res1 = device.sendGA(*req1.toGA())
rp1_assertions.append(ga_res1)
if ga_res1.number_of_credentials != None:
for _ in range(l1 - 1):
rp1_assertions.append(device.ctap2.get_next_assertion())
# Should authenticate to all credentials matching rp2
ga_res2 = device.sendGA(*req2.toGA())
rp2_assertions.append(ga_res2)
if ga_res2.number_of_credentials != None:
for _ in range(l2 - 1):
rp2_assertions.append(device.ctap2.get_next_assertion())
counts = (
ga_res1.number_of_credentials,
ga_res2.number_of_credentials)
assert counts in [(None, None), (l1, l2)]
if counts != (None, None):
# Assertions return in order of most recently created credential.
rp1_assertions.reverse()
rp2_assertions.reverse()
for (reg, auth) in zip(rp1_registrations, rp1_assertions):
verify(reg, auth, req1.cdh)
for (reg, auth) in zip(rp2_registrations, rp2_assertions):
verify(reg, auth, req2.cdh)
else:
rp1_verifs = 0
for reg in rp1_registrations:
try:
verify(reg, ga_res1, req1.cdh)
rp1_verifs += 1
except InvalidSignature:
pass
assert rp1_verifs == 1
rp2_verifs = 0
for reg in rp2_registrations:
try:
verify(reg, ga_res2, req2.cdh)
rp2_verifs += 1
except InvalidSignature:
pass
assert rp2_verifs == 1
def test_corrupt_credId(self, device, MCRes):
# apply bit flip
badid = list(MCRes.auth_data.credential_data.credential_id[:])
badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1
badid = bytes(badid)
allow_list = [{"id": badid, "type": "public-key"}]
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(allow_list=allow_list).toGA())
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_mismatched_rp(self, device, GARes):
rp_id = GARes.request.rp["id"][:]
rp_name = GARes.request.rp["name"][:]
rp_id += ".com"
mismatch_rp = {"id": rp_id, "name": rp_name}
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, rp=mismatch_rp).toGA())
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_missing_rp(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, rp=None).toGA())
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_rp(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, rp={"id": {"type": "wrong"}}).toGA())
def test_missing_cdh(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, cdh=None).toGA())
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_cdh(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, cdh={"type": "wrong"}).toGA())
def test_bad_allow_list(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(*FidoRequest(GARes, allow_list={"type": "wrong"}).toGA())
def test_bad_allow_list_item(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(
*FidoRequest(
GARes, allow_list=["wrong"] + GARes.request.allow_list
).toGA()
)
def test_unknown_option(self, device, GARes):
device.sendGA(*FidoRequest(GARes, options={"unknown": True}).toGA())
@pytest.mark.skipif(
"trezor" in sys.argv,
reason="User verification flag is intentionally set to true on Trezor even when user verification is not configured. (Otherwise some services refuse registration without giving a reason.)",
)
def test_option_uv(self, device, info, GARes):
if "uv" in info.options:
if info.options["uv"]:
res = device.sendGA(*FidoRequest(GARes, options={"uv": True}).toGA())
assert res.auth_data.flags & (1 << 2)
def test_option_up(self, device, info, GARes):
if "up" in info.options:
if info.options["up"]:
res = device.sendGA(*FidoRequest(GARes, options={"up": True}).toGA())
assert res.auth_data.flags & (1 << 0)
def test_allow_list_fake_item(self, device, GARes):
device.sendGA(
*FidoRequest(
GARes,
allow_list=[{"type": "rot13", "id": b"1234"}]
+ GARes.request.allow_list,
).toGA()
)
def test_allow_list_missing_field(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(
*FidoRequest(
GARes, allow_list=[{"id": b"1234"}] + GARes.request.allow_list
).toGA()
)
def test_allow_list_field_wrong_type(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(
*FidoRequest(
GARes,
allow_list=[{"type": b"public-key", "id": b"1234"}]
+ GARes.request.allow_list,
).toGA()
)
def test_allow_list_id_wrong_type(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(
*FidoRequest(
GARes,
allow_list=[{"type": "public-key", "id": 42}]
+ GARes.request.allow_list,
).toGA()
)
def test_allow_list_missing_id(self, device, GARes):
with pytest.raises(CtapError) as e:
device.sendGA(
*FidoRequest(
GARes,
allow_list=[{"type": "public-key"}] + GARes.request.allow_list,
).toGA()
)
def test_user_presence_option_false(self, device, MCRes, GARes):
res = device.sendGA(*FidoRequest(GARes, options={"up": False}).toGA())
try:
verify(MCRes, res, GARes.request.cdh)
except InvalidSignature:
if "trezor" not in sys.argv:
raise
if "--nfc" not in sys.argv:
assert (res.auth_data.flags & 1) == 0
@pytest.mark.skipif("trezor" in sys.argv, reason="Reboot is not supported on Trezor.")
class TestGetAssertionAfterBoot(object):
def test_assertion_after_reboot(self, rebootedDevice, MCRes, GARes):
credential_data = AttestedCredentialData(MCRes.auth_data.credential_data)
verify(MCRes, GARes)