From 577edbb62f7a3d6f3e9d68be5d18f736733ea384 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Mon, 3 Oct 2022 16:10:36 +0200 Subject: [PATCH] Adding hmac-secret tests. Signed-off-by: Pol Henarejos --- tests/conftest.py | 38 ++++-- tests/pico-fido/test_hmac_secret.py | 199 ++++++++++++++++++++++++++++ tests/utils.py | 59 +++++++++ 3 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 tests/pico-fido/test_hmac_secret.py create mode 100644 tests/utils.py diff --git a/tests/conftest.py b/tests/conftest.py index acb0bb6..c44150c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from fido2.server import Fido2Server from fido2.ctap import CtapError from fido2.webauthn import CollectedClientData, AttestedCredentialData from getpass import getpass +from utils import * import sys import pytest import os @@ -237,14 +238,27 @@ class Device(): type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32) ) rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id'] - result = self.__client._backend.do_get_assertion( - client_data=client_data, - rp_id=rp_id, - allow_list=allow_list, - extensions=extensions, - user_verification=user_verification, - event=event - ) + try: + result = self.__client._backend.do_get_assertion( + client_data=client_data, + rp_id=rp_id, + allow_list=allow_list, + extensions=extensions, + user_verification=user_verification, + event=event + ) + except ClientError as e: + if (e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED): + client_pin = ClientPin(self.__client._backend.ctap2) + client_pin.set_pin(DEFAULT_PIN) + result = self.__client._backend.do_get_assertion( + client_data=client_data, + rp_id=rp_id, + allow_list=allow_list, + extensions=extensions, + user_verification=user_verification, + event=event + ) return {'res':result,'req':{'client_data':client_data, 'rp_id':rp_id}} @@ -272,10 +286,10 @@ def GARes(device, MCRes, *args): res = device.doGA(allow_list=[ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], *args) - credential_data = AttestedCredentialData(MCRes['res'].attestation_object.auth_data.credential_data) + assertions = res['res'].get_assertions() for a in assertions: - a.verify(res['req']['client_data'].hash, credential_data.public_key) + verify(MCRes['res'].attestation_object, a, res['req']['client_data'].hash) return res @pytest.fixture(scope="session") @@ -287,6 +301,6 @@ def GARes_DC(device, MCRes_DC, *args): res = device.GA(allow_list=[ {"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], *args) - credential_data = AttestedCredentialData(MCRes_DC['res'].attestation_object.auth_data.credential_data) - res['res'].verify(res['req']['client_data_hash'], credential_data.public_key) + verify(MCRes_DC['res'].attestation_object, res['res'], res['req']['client_data_hash']) + return res diff --git a/tests/pico-fido/test_hmac_secret.py b/tests/pico-fido/test_hmac_secret.py new file mode 100644 index 0000000..11834aa --- /dev/null +++ b/tests/pico-fido/test_hmac_secret.py @@ -0,0 +1,199 @@ +import pytest +from fido2.ctap import CtapError +from fido2.ctap2.extensions import HmacSecretExtension +from fido2.utils import hmac_sha256 +from fido2.ctap2.pin import PinProtocolV2 +from fido2.webauthn import UserVerificationRequirement +from utils import * + +salt1 = b"\xa5" * 32 +salt2 = b"\x96" * 32 +salt3 = b"\x03" * 32 +salt4 = b"\x5a" * 16 +salt5 = b"\x96" * 64 + + +@pytest.fixture(scope="class") +def MCHmacSecret(resetdevice): + res = resetdevice.doMC(extensions={"hmacCreateSecret": True},rk=True) + return res['res'].attestation_object + +@pytest.fixture(scope="class") +def hmac(resetdevice): + return HmacSecretExtension(resetdevice.client()._backend.ctap2, pin_protocol=PinProtocolV2()) + +def test_hmac_secret_make_credential(MCHmacSecret): + assert MCHmacSecret.auth_data.extensions + assert "hmac-secret" in MCHmacSecret.auth_data.extensions + assert MCHmacSecret.auth_data.extensions["hmac-secret"] == True + +def test_hmac_secret_info(info): + assert "hmac-secret" in info.extensions + +def test_fake_extension(device): + device.doMC(extensions={"tetris": True}) + + +@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) +def test_hmac_secret_entropy(device, MCHmacSecret, hmac, salts +): + hout = {'salt1':salts[0]} + if (len(salts) > 1): + hout['salt2'] = salts[1] + + auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0) + ext = auth.extension_results + assert ext + assert "hmacGetSecret" in ext + assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 + + #print(shannon_entropy(auth.authenticator_data.extensions['hmac-secret'])) + if len(salts) == 1: + assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 4.6 + assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6 + if len(salts) == 2: + assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 5.4 + assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.6 + assert shannon_entropy(ext["hmacGetSecret"]['output2']) > 4.6 + +def get_output(device, MCHmacSecret, hmac, salts): + hout = {'salt1':salts[0]} + if (len(salts) > 1): + hout['salt2'] = salts[1] + + auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0) + + ext = auth.extension_results + assert ext + assert "hmacGetSecret" in ext + assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 + + if len(salts) == 2: + return ext["hmacGetSecret"]['output1'], ext["hmacGetSecret"]['output2'] + else: + return ext["hmacGetSecret"]['output1'] + +def test_hmac_secret_sanity(device, MCHmacSecret, hmac): + output1 = get_output(device, MCHmacSecret, hmac, (salt1,)) + output12 = get_output( + device, MCHmacSecret, hmac, (salt1, salt2) + ) + output21 = get_output( + device, MCHmacSecret, hmac, (salt2, salt1) + ) + + assert output12[0] == output1 + assert output21[1] == output1 + assert output21[0] == output12[1] + assert output12[0] != output12[1] + +def test_missing_keyAgreement(device, hmac): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) + + with pytest.raises(CtapError): + device.GA(extensions={"hmac-secret": {2: hout[2], 3: hout[3]}}) + +def test_missing_saltAuth(device, hmac): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) + + with pytest.raises(CtapError) as e: + device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2]}}) + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_missing_saltEnc(device, hmac): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) + + with pytest.raises(CtapError) as e: + device.GA(extensions={"hmac-secret": {1: hout[1], 3: hout[3]}}) + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_auth(device, hmac, MCHmacSecret): + + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) + bad_auth = list(hout[3][:]) + bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1 + bad_auth = bytes(bad_auth) + + with pytest.raises(CtapError) as e: + device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2], 3: bad_auth, 4: 2}}) + assert e.value.code == CtapError.ERR.EXTENSION_FIRST + +@pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)]) +def test_invalid_salt_length(device, hmac, salts): + with pytest.raises(ValueError) as e: + if (len(salts) == 2): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + else: + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + + device.doGA(extensions={"hmacGetSecret": hout}) + +@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) +def test_get_next_assertion_has_extension( + device, hmac, salts +): + """ Check that get_next_assertion properly returns extension information for multiple accounts. """ + if (len(salts) == 2): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + else: + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + accounts = 3 + regs = [] + auths = [] + rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"} + fixed_users = [generate_random_user() for _ in range(accounts)] + for i in range(accounts): + res = device.doMC(extensions={"hmacCreateSecret": True}, + rk=True, + rp=rp, + user=fixed_users[i])['res'].attestation_object + regs.append(res) + + hout = {'salt1':salts[0]} + if (len(salts) > 1): + hout['salt2'] = salts[1] + + ga = device.doGA(extensions={"hmacGetSecret": hout}, rp_id=rp['id']) + auths = ga['res'].get_assertions() + + for x in auths: + assert x.auth_data.flags & (1 << 7) # has extension + ext = x.auth_data.extensions + assert ext + assert "hmac-secret" in ext + assert isinstance(ext["hmac-secret"], bytes) + assert len(ext["hmac-secret"]) == len(salts) * 32 + 16 + key = hmac.process_get_output(x) + + + +def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac): + salts = [salt1] + if (len(salts) == 2): + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + else: + hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + + auth_no_uv = device.GA(extensions={"hmac-secret": hout})['res'] + assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0 + + ext_no_uv = auth_no_uv.auth_data.extensions + assert ext_no_uv + assert "hmac-secret" in ext_no_uv + assert isinstance(ext_no_uv["hmac-secret"], bytes) + assert len(ext_no_uv["hmac-secret"]) == len(salts) * 32 + 16 + + # Now get same auth with UV + hout = {'salt1':salts[0]} + if (len(salts) > 1): + hout['salt2'] = salts[1] + auth_uv = device.doGA(extensions={"hmacGetSecret": hout}, user_verification=UserVerificationRequirement.REQUIRED)['res'].get_response(0) + + assert auth_uv.authenticator_data.flags & (1 << 2) + ext_uv = auth_uv.extension_results + assert ext_uv + assert "hmacGetSecret" in ext_uv + assert len(ext_uv["hmacGetSecret"]) == len(salts) + + # Now see if the hmac-secrets are different + assert ext_no_uv["hmac-secret"][:32] != ext_uv["hmacGetSecret"]['output1'] diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..6448ed8 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,59 @@ +from fido2.webauthn import AttestedCredentialData +import random +import string +import secrets +import math + +def verify(MC, GA, client_data_hash): + credential_data = AttestedCredentialData(MC.auth_data.credential_data) + GA.verify(client_data_hash, credential_data.public_key) + + +def generate_random_user(): + # https://www.w3.org/TR/webauthn/#user-handle + user_id_length = random.randint(1, 64) + user_id = secrets.token_bytes(user_id_length) + + # https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity + name = "User name" + icon = "https://www.w3.org/TR/webauthn/" + display_name = "Displayed " + name + + return {"id": user_id, "name": name, "icon": icon, "displayName": display_name} + +counter = 1 +def generate_user_maximum(): + """ + Generate RK with the maximum lengths of the fields, according to the minimal requirements of the FIDO2 spec + """ + global counter + + # https://www.w3.org/TR/webauthn/#user-handle + user_id_length = 64 + user_id = secrets.token_bytes(user_id_length) + + # https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity + name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(64)) + + name = f"{counter}: {name}" + icon = "https://www.w3.org/TR/webauthn/" + "A" * 128 + display_name = "Displayed " + name + + name = name[:64] + display_name = display_name[:64] + icon = icon[:128] + + counter += 1 + + return {"id": user_id, "name": name, "icon": icon, "displayName": display_name} + +def shannon_entropy(data): + s = 0.0 + total = len(data) + for x in range(0, 256): + freq = data.count(x) + p = freq / total + if p > 0: + s -= p * math.log2(p) + return s +