From ae237db9cad9803f0cf390a85fc4eae4e836d68f Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Tue, 4 Oct 2022 00:00:13 +0200 Subject: [PATCH] Added tests for PIN. Signed-off-by: Pol Henarejos --- tests/pico-fido/test_pin.py | 247 ++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests/pico-fido/test_pin.py diff --git a/tests/pico-fido/test_pin.py b/tests/pico-fido/test_pin.py new file mode 100644 index 0000000..b932d88 --- /dev/null +++ b/tests/pico-fido/test_pin.py @@ -0,0 +1,247 @@ +import os +import pytest +from fido2.ctap import CtapError +from fido2.ctap2.pin import PinProtocolV1 +from fido2.client import ClientPin +from fido2.webauthn import UserVerificationRequirement +from fido2.utils import hmac_sha256 + +from utils import * + + +@pytest.fixture(scope="class") +def client_pin(resetdevice): + return ClientPin(resetdevice.client()._backend.ctap2) + +def test_lockout(device, resetdevice, client_pin): + pin = "TestPin" + client_pin.set_pin(pin) + + pin_token = client_pin.get_pin_token(pin) + + 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] + + with pytest.raises(CtapError) as e: + client_pin.get_pin_token("WrongPin") + assert e.value.code == err or e.value.code in err + + attempts = 8 - i + if i > 8: + attempts = 0 + + res = client_pin.get_pin_retries() + assert res[0] == attempts + + if err == CtapError.ERR.PIN_AUTH_BLOCKED: + device.reboot() + client_pin = ClientPin(resetdevice.client()._backend.ctap2) + + with pytest.raises(CtapError) as e: + device.doMC() + + device.reboot() + client_pin = ClientPin(resetdevice.client()._backend.ctap2) + + with pytest.raises(CtapError) as e: + client_pin.get_pin_token(pin) + assert e.value.code == CtapError.ERR.PIN_BLOCKED + +def test_send_zero_length_pin_auth(device): + device.reset() + with pytest.raises(CtapError) as e: + reg = device.MC(pin_uv_param=b"") + assert e.value.code == CtapError.ERR.PIN_NOT_SET + + with pytest.raises(CtapError) as e: + reg = device.GA(pin_uv_param=b"") + assert e.value.code in (CtapError.ERR.PIN_NOT_SET, CtapError.ERR.NO_CREDENTIALS) + +def test_set_pin(device, client_pin): + device.reset() + client_pin.set_pin("TestPin") + device.reset() + +def test_set_pin_too_big(client_pin): + with pytest.raises(CtapError) as e: + client_pin.set_pin("A" * 64) + assert e.value.code == CtapError.ERR.PIN_POLICY_VIOLATION + +def test_get_pin_token_but_no_pin_set(resetdevice, client_pin): + with pytest.raises(CtapError) as e: + client_pin.get_pin_token("TestPin") + assert e.value.code == CtapError.ERR.PIN_NOT_SET + +def test_change_pin_but_no_pin_set(device, client_pin): + with pytest.raises(CtapError) as e: + client_pin.change_pin("TestPin", "1234") + assert e.value.code == CtapError.ERR.PIN_NOT_SET + +def test_setting_pin_and_get_info(device, client_pin, info): + device.reset() + client_pin.set_pin("TestPin") + + with pytest.raises(CtapError) as e: + client_pin.set_pin("TestPin") + + assert info.options["clientPin"] + + pin_token = client_pin.get_pin_token("TestPin") + + res = client_pin.get_pin_retries() + assert res[0] == 8 + + device.reset() + +PIN1 = "12345678" +PIN2 = "ABCDEF" + +@pytest.fixture(scope="class", params=[PIN1]) +def SetPinRes(request, device, client_pin): + device.reset() + + pin = request.param + + client_pin.set_pin(pin) + + res = device.doMC(user_verification=UserVerificationRequirement.REQUIRED) + return res + + +@pytest.fixture(scope="class") +def CPRes(request, device, client_pin): + res = client_pin.ctap.client_pin(2, ClientPin.CMD.GET_KEY_AGREEMENT) + return res + + +@pytest.fixture(scope="class") +def MCPinRes(device): + res = device.doMC(user_verification=UserVerificationRequirement.REQUIRED) + return res + + +@pytest.fixture(scope="class") +def GAPinRes(device, MCPinRes): + res = device.doGA() + return res + +def test_pin(CPRes): + pass + +def test_set_pin_twice(device, client_pin): + """ Setting pin when a pin is already set should result in error NotAllowed. """ + with pytest.raises(CtapError) as e: + client_pin.set_pin('1234') + client_pin.set_pin('1234') + + assert e.value.code == CtapError.ERR.NOT_ALLOWED + + +def test_get_key_agreement_fields(CPRes): + key = CPRes[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) + +def test_verify_flag(device, SetPinRes): + reg = device.doMC(user_verification=UserVerificationRequirement.REQUIRED)['res'].attestation_object + assert reg.auth_data.flags & (1 << 2) + +def test_get_no_pin_auth(device): + + reg = device.doMC()['res'].attestation_object + allow_list = [ + {"type": "public-key", "id": reg.auth_data.credential_data.credential_id} + ] + auth = device.GA(allow_list=allow_list)['res'] + assert not (auth.auth_data.flags & (1 << 2)) + + with pytest.raises(CtapError) as e: + reg = device.MC() + + assert e.value.code == CtapError.ERR.PUAT_REQUIRED + +def test_zero_length_pin_auth(device): + with pytest.raises(CtapError) as e: + reg = device.MC(pin_uv_param=b"") + assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID + + with pytest.raises(CtapError) as e: + reg = device.GA(pin_uv_param=b"") + assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID + +def test_make_credential_no_pin(device): + with pytest.raises(CtapError) as e: + reg = device.MC() + assert e.value.code == CtapError.ERR.PUAT_REQUIRED + +def test_get_assertion_no_pin(device): + with pytest.raises(CtapError) as e: + reg = device.GA() + assert e.value.code == CtapError.ERR.NO_CREDENTIALS + +def test_change_pin(device, client_pin): + device.reset() + client_pin.set_pin(PIN1) + client_pin.change_pin(PIN1, PIN2) + + pin_token = client_pin.get_pin_token(pin=PIN2) + cdh = os.urandom(32) + pin_auth = hmac_sha256(pin_token, cdh)[:32] + + reg = device.MC(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh)['res'] + + pin_token = client_pin.get_pin_token(pin=PIN2) + pin_auth = hmac_sha256(pin_token, cdh)[:32] + auth = device.GA(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh, allow_list=[{ + "type": "public-key", + "id": reg.auth_data.credential_data.credential_id, + }])['res'] + + assert reg.auth_data.flags & (1 << 2) + assert auth.auth_data.flags & (1 << 2) + + verify(reg, auth, client_data_hash=cdh) + +def test_pin_attempts(device, client_pin): + # Flip 1 bit + pin = PIN1 + device.reset() + client_pin.set_pin(pin) + pin_wrong = list(pin) + c = pin[len(pin) // 2] + + pin_wrong[len(pin) // 2] = chr(ord(c) ^ 1) + pin_wrong = "".join(pin_wrong) + + for i in range(1, 3): + with pytest.raises(CtapError) as e: + pin_token = client_pin.get_pin_token(pin=pin_wrong) + assert e.value.code == CtapError.ERR.PIN_INVALID + + print("Check there is %d pin attempts left" % (8 - i)) + res = client_pin.get_pin_retries() + assert res[0] == (8 - i) + + for i in range(1, 3): + with pytest.raises(CtapError) as e: + client_pin.get_pin_token(pin_wrong) + assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED + + device.reboot() + client_pin = ClientPin(device.client()._backend.ctap2) + + pin_token = client_pin.get_pin_token(pin=pin) + cdh = os.urandom(32) + pin_auth = hmac_sha256(pin_token, cdh)[:32] + + reg = device.MC(pin_uv_param=pin_auth, pin_uv_protocol=2, client_data_hash=cdh)['res'] + + res = client_pin.get_pin_retries() + assert res[0] == (8)