diff --git a/tools/ctap_test.py b/tools/ctap_test.py deleted file mode 100755 index f96c0f9..0000000 --- a/tools/ctap_test.py +++ /dev/null @@ -1,2056 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2019 SoloKeys Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. -# - -# Script for testing correctness of CTAP2/CTAP1 security token - -from __future__ import print_function, absolute_import, unicode_literals -import sys, os, time, math -from random import randint -from binascii import hexlify -import array, struct, socket - -from fido2.hid import CtapHidDevice, CTAPHID -from fido2.client import Fido2Client, ClientError -from fido2.ctap import CtapError -from fido2.ctap1 import CTAP1, ApduError, APDU -from fido2.ctap2 import ES256, PinProtocolV1 -from fido2.utils import Timeout, 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 solo.fido2 import force_udp_backend -from solo.client import SoloClient - - -# Set up a FIDO 2 client using the origin https://example.com - - -def ForceU2F(client, device): - client.ctap = CTAP1(device) - client.pin_protocol = None - client._do_make_credential = client._ctap1_make_credential - client._do_get_assertion = client._ctap1_get_assertion - - -def VerifyAttestation(attest, data): - verifier = Attestation.for_type(attest.fmt) - verifier().verify(attest.att_statement, attest.auth_data, data.hash) - - -def shannon_entropy(data): - sum = 0.0 - total = len(data) - for x in range(0, 256): - freq = data.count(x) - p = freq / total - if p > 0: - sum -= p * math.log2(p) - return sum - - -class Packet(object): - def __init__(self, data): - l = len(data) - self.data = data - - def ToWireFormat(self,): - return self.data - - @staticmethod - def FromWireFormat(pkt_size, data): - return Packet(data) - - -class Test: - def __init__(self, msg): - self.msg = msg - - def __enter__(self,): - print(self.msg) - - def __exit__(self, a, b, c): - print("Pass") - - -class Tester: - def __init__(self,): - self.origin = "https://examplo.org" - self.host = "examplo.org" - self.user_count = 10 - self.is_sim = False - - def find_device(self,): - print(list(CtapHidDevice.list_devices())) - dev = next(CtapHidDevice.list_devices(), None) - if not dev: - raise RuntimeError("No FIDO device found") - self.dev = dev - self.client = Fido2Client(dev, self.origin) - self.ctap = self.client.ctap2 - self.ctap1 = CTAP1(dev) - - # consume timeout error - # cmd,resp = self.recv_raw() - - def set_user_count(self, count): - self.user_count = count - - def set_sim(self, b): - self.is_sim = b - - def send_data(self, cmd, data): - if type(data) != type(b""): - data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) - with Timeout(1.0) as event: - return self.dev.call(cmd, data, event) - - def send_raw(self, data, cid=None): - if cid is None: - cid = self.dev._dev.cid - elif type(cid) != type(b""): - cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) - if type(data) != type(b""): - data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) - data = cid + data - l = len(data) - if l != 64: - pad = "\x00" * (64 - l) - pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad]) - data = data + pad - data = list(data) - assert len(data) == 64 - self.dev._dev.InternalSendPacket(Packet(data)) - - def send_magic_reboot(self,): - """ - For use in simulation and testing. Random bytes that authentictor should detect - and then restart itself. - """ - magic_cmd = ( - b"\xac\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf" - + b"\xf3\x33\x48\x5f\x13\xf9\xb2\xda\x34\xc5\xa8\xa3" - + b"\x40\x52\x66\x97\xa9\xab\x2e\x0b\x39\x4d\x8d\x04" - + b"\x97\x3c\x13\x40\x05\xbe\x1a\x01\x40\xbf\xf6\x04" - + b"\x5b\xb2\x6e\xb7\x7a\x73\xea\xa4\x78\x13\xf6\xb4" - + b"\x9a\x72\x50\xdc" - ) - self.dev._dev.InternalSendPacket(Packet(magic_cmd)) - - def cid(self,): - return self.dev._dev.cid - - def set_cid(self, cid): - if type(cid) not in [type(b""), type(bytearray())]: - cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) - self.dev._dev.cid = cid - - def recv_raw(self,): - with Timeout(1.0) as t: - cmd, payload = self.dev._dev.InternalRecv() - return cmd, payload - - def check_error(self, data, err=None): - assert len(data) == 1 - if err is None: - if data[0] != 0: - raise CtapError(data[0]) - elif data[0] != err: - raise ValueError("Unexpected error: %02x" % data[0]) - - def testFunc(self, func, test, *args, **kwargs): - with Test(test): - res = None - expectedError = kwargs.get("expectedError", None) - otherArgs = kwargs.get("other", {}) - try: - res = func(*args, **otherArgs) - if expectedError != CtapError.ERR.SUCCESS: - raise RuntimeError("Expected error to occur for test: %s" % test) - except CtapError as e: - if expectedError is not None: - if e.code != expectedError: - raise RuntimeError( - "Got error code 0x%x, expected %x" % (e.code, expectedError) - ) - else: - print(e) - return res - - def testReset(self,): - print("Resetting Authenticator...") - self.ctap.reset() - - def testMC(self, test, *args, **kwargs): - return self.testFunc(self.ctap.make_credential, test, *args, **kwargs) - - def testGA(self, test, *args, **kwargs): - return self.testFunc(self.ctap.get_assertion, test, *args, **kwargs) - - def testCP(self, test, *args, **kwargs): - return self.testFunc(self.ctap.client_pin, test, *args, **kwargs) - - def testPP(self, test, *args, **kwargs): - return self.testFunc( - self.client.pin_protocol.get_pin_token, test, *args, **kwargs - ) - - def test_long_ping(self): - amt = 1000 - pingdata = os.urandom(amt) - with Test("Send %d byte ping" % amt): - try: - t1 = time.time() * 1000 - r = self.send_data(CTAPHID.PING, pingdata) - t2 = time.time() * 1000 - delt = t2 - t1 - # if (delt < 140 ): - # raise RuntimeError('Fob is too fast (%d ms)' % delt) - if delt > 555 * (amt / 1000): - raise RuntimeError("Fob is too slow (%d ms)" % delt) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError as e: - raise RuntimeError("ping failed") - - sys.stdout.flush() - - def test_hid(self, check_timeouts=False): - if check_timeouts: - with Test("idle"): - try: - cmd, resp = self.recv_raw() - except socket.timeout: - pass - - with Test("init"): - r = self.send_data(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") - - with Test("100 byte ping"): - pingdata = os.urandom(100) - try: - r = self.send_data(CTAPHID.PING, pingdata) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError as e: - print("100 byte Ping failed:", e) - raise RuntimeError("ping failed") - - self.test_long_ping() - - with Test("Wink"): - r = self.send_data(CTAPHID.WINK, "") - - with Test("CBOR msg with no data"): - try: - r = self.send_data(CTAPHID.CBOR, "") - if len(r) > 1 or r[0] == 0: - raise RuntimeError("Cbor is supposed to have payload") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_LENGTH - - with Test("No data in U2F msg"): - try: - r = self.send_data(CTAPHID.MSG, "") - print(hexlify(r)) - if len(r) > 2: - raise RuntimeError("MSG is supposed to have payload") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_LENGTH - - with Test("Use init command to resync"): - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - - with Test("Invalid HID command"): - try: - r = self.send_data(0x66, "") - raise RuntimeError("Invalid command did not return error") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_COMMAND - - with Test("Sending packet with too large of a length."): - self.send_raw("\x81\x1d\xba\x00") - cmd, resp = self.recv_raw() - self.check_error(resp, CtapError.ERR.INVALID_LENGTH) - - r = self.send_data(CTAPHID.PING, "\x44" * 200) - with Test("Sending packets that skip a sequence number."): - self.send_raw("\x81\x04\x90") - self.send_raw("\x00") - self.send_raw("\x01") - # skip 2 - self.send_raw("\x03") - cmd, resp = self.recv_raw() - self.check_error(resp, CtapError.ERR.INVALID_SEQ) - - with Test("Resync and send ping"): - try: - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - pingdata = os.urandom(100) - r = self.send_data(CTAPHID.PING, pingdata) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError as e: - raise RuntimeError("resync fail: ", e) - - with Test("Send ping and abort it"): - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - try: - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - except CtapError as e: - raise RuntimeError("resync fail: ", e) - - with Test("Send ping and abort it with different cid, expect timeout"): - oldcid = self.cid() - newcid = "\x11\x22\x33\x44" - self.send_raw("\x81\x10\x00") - self.send_raw("\x00") - self.send_raw("\x01") - self.set_cid(newcid) - self.send_raw( - "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88" - ) # init from different cid - print("wait for init response") - cmd, r = self.recv_raw() # init response - assert cmd == 0x86 - self.set_cid(oldcid) - if check_timeouts: - # print('wait for timeout') - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - - with Test("Test timeout"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - t1 = time.time() * 1000 - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - cmd, r = self.recv_raw() # timeout response - t2 = time.time() * 1000 - delt = t2 - t1 - assert cmd == 0xBF - assert r[0] == CtapError.ERR.TIMEOUT - assert delt < 1000 and delt > 400 - - with Test("Test not cont"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - self.send_raw("\x81\x10\x00") # init packet - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_SEQ - - if check_timeouts: - with Test("Check random cont ignored"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x01\x10\x00") - try: - cmd, r = self.recv_raw() # timeout response - except socket.timeout: - pass - - with Test("Check busy"): - t1 = time.time() * 1000 - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - oldcid = self.cid() - newcid = "\x11\x22\x33\x44" - self.send_raw("\x81\x04\x00") - self.set_cid(newcid) - self.send_raw("\x81\x04\x00") - cmd, r = self.recv_raw() # busy response - t2 = time.time() * 1000 - assert t2 - t1 < 100 - assert cmd == 0xBF - assert r[0] == CtapError.ERR.CHANNEL_BUSY - - self.set_cid(oldcid) - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - assert r[0] == CtapError.ERR.TIMEOUT - - with Test("Check busy interleaved"): - cid1 = "\x11\x22\x33\x44" - cid2 = "\x01\x22\x33\x44" - self.set_cid(cid2) - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.set_cid(cid1) - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x81\x00\x63") # echo 99 bytes first channel - - self.set_cid(cid2) # send ping on 2nd channel - self.send_raw("\x81\x00\x63") - time.sleep(0.1) - self.send_raw("\x00") - - cmd, r = self.recv_raw() # busy response - - self.set_cid(cid1) # finish 1st channel ping - self.send_raw("\x00") - - self.set_cid(cid2) - - assert cmd == 0xBF - assert r[0] == CtapError.ERR.CHANNEL_BUSY - - self.set_cid(cid1) - cmd, r = self.recv_raw() # ping response - assert cmd == 0x81 - assert len(r) == 0x63 - - if check_timeouts: - with Test("Test idle, wait for timeout"): - sys.stdout.flush() - try: - cmd, resp = self.recv_raw() - except socket.timeout: - pass - - with Test("Test cid 0 is invalid"): - self.set_cid("\x00\x00\x00\x00") - self.send_raw( - "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00" - ) - cmd, r = self.recv_raw() # timeout - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_CHANNEL - - with Test("Test invalid broadcast cid use"): - self.set_cid("\xff\xff\xff\xff") - self.send_raw( - "\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff" - ) - cmd, r = self.recv_raw() # timeout - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_CHANNEL - - def test_u2f(self,): - chal = sha256(b"AAA") - appid = sha256(b"BBB") - lastc = 0 - - regs = [] - - with Test("Check version"): - assert self.ctap1.get_version() == "U2F_V2" - - with Test("Check bad INS"): - try: - res = self.ctap1.send_apdu(0, 0, 0, 0, b"") - except ApduError as e: - assert e.code == 0x6D00 - - with Test("Check bad CLA"): - try: - res = self.ctap1.send_apdu(1, CTAP1.INS.VERSION, 0, 0, b"abc") - except ApduError as e: - assert e.code == 0x6E00 - - for i in range(0, self.user_count): - with Test( - "U2F reg + auth %d/%d (count: %02x)" % (i + 1, self.user_count, lastc) - ): - reg = self.ctap1.register(chal, appid) - reg.verify(appid, chal) - auth = self.ctap1.authenticate(chal, appid, reg.key_handle) - auth.verify(appid, chal, reg.public_key) - - regs.append(reg) - # check endianness - if lastc: - assert (auth.counter - lastc) < 10 - lastc = auth.counter - if lastc > 0x80000000: - print("WARNING: counter is unusually high: %04x" % lastc) - assert 0 - - for i in range(0, self.user_count): - with Test( - "Checking previous registration %d/%d" % (i + 1, self.user_count) - ): - auth = self.ctap1.authenticate(chal, appid, regs[i].key_handle) - auth.verify(appid, chal, regs[i].public_key) - - print("Check that all previous credentials are registered...") - for i in range(0, self.user_count): - with Test("Check that previous credential %d is registered" % i): - try: - auth = self.ctap1.authenticate( - chal, appid, regs[i].key_handle, check_only=True - ) - except ApduError as e: - # Indicates that key handle is registered - assert e.code == APDU.USE_NOT_SATISFIED - - with Test("Check an incorrect key handle is not registered"): - kh = bytearray(regs[0].key_handle) - kh[0] = kh[0] ^ (0x40) - try: - self.ctap1.authenticate(chal, appid, kh, check_only=True) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign with incorrect key handle"): - try: - self.ctap1.authenticate(chal, appid, kh) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign using an incorrect keyhandle length"): - try: - kh = regs[0].key_handle - self.ctap1.authenticate(chal, appid, kh[: len(kh) // 2]) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign using an incorrect appid"): - badid = bytearray(appid) - badid[0] = badid[0] ^ (0x40) - try: - auth = self.ctap1.authenticate(chal, badid, regs[0].key_handle) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - def test_fido2_simple(self, pin_token=None): - creds = [] - exclude_list = [] - rp = {"id": self.host, "name": "ExaRP"} - user = {"id": b"usee_od", "name": "AB User"} - challenge = "Y2hhbGxlbmdl" - 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_fido2_brute_force(self): - creds = [] - exclude_list = [] - rp = {"id": self.host, "name": "ExaRP"} - user = {"id": b"usee_od", "name": "AB User"} - PIN = None - abc = "abcdefghijklnmopqrstuvwxyz" - abc += abc.upper() - - self.ctap.reset() - - for i in range(0, 2048 ** 2): - creds = [] - - challenge = "".join([abc[randint(0, len(abc) - 1)] for x in range(0, 32)]) - - 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"}) - - # for i in range(0,2048**2): - for i in range(0, 1): - t1 = time.time() * 1000 - attest, data = self.client.make_credential( - rp, user, challenge, pin=PIN, exclude_list=[] - ) - print(attest.auth_data.counter) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - print("Register valid (%d ms)" % (t2 - t1)) - sys.stdout.flush() - - cred = attest.auth_data.credential_data - creds.append(cred) - - # for i in range(0,2048**2): - for i in range(0, 1): - 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(assertions[0].auth_data.counter) - - print("Assertion valid (%d ms)" % (t2 - t1)) - sys.stdout.flush() - - def test_fido2(self): - def test(self, pincode=None): - creds = [] - exclude_list = [] - rp = {"id": self.host, "name": "ExaRP"} - user = {"id": b"usee_od", "name": "AB User"} - challenge = "Y2hhbGxlbmdl" - PIN = pincode - - 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"}) - - # test make credential - with Test("make %d credentials" % self.user_count): - lastc = 0 - for i in range(0, self.user_count): - attest, data = self.client.make_credential( - rp, user, challenge, pin=PIN, exclude_list=[] - ) - VerifyAttestation(attest, data) - - # verify counter is correct - if lastc > 0: - assert attest.auth_data.counter - lastc < 10 - assert attest.auth_data.counter - lastc > 0 - assert attest.auth_data.counter < 0x10000 - lastc = attest.auth_data.counter - - cred = attest.auth_data.credential_data - creds.append(cred) - print(cred) - - if PIN is not None: - with Test("make credential with wrong pin code"): - try: - attest, data = self.client.make_credential( - rp, user, challenge, pin=PIN + " ", exclude_list=[] - ) - except CtapError as e: - assert e.code == CtapError.ERR.PIN_INVALID - except ClientError as e: - assert e.cause.code == CtapError.ERR.PIN_INVALID - - with Test("make credential with exclude list"): - attest, data = self.client.make_credential( - rp, user, challenge, pin=PIN, exclude_list=exclude_list - ) - VerifyAttestation(attest, data) - cred = attest.auth_data.credential_data - creds.append(cred) - - with Test("make credential with exclude list including real credential"): - real_excl = [{"id": cred.credential_id, "type": "public-key"}] - try: - attest, data = self.client.make_credential( - rp, - user, - challenge, - pin=PIN, - exclude_list=exclude_list + real_excl, - ) - raise RuntimeError("Exclude list did not return expected error") - except CtapError as e: - assert e.code == CtapError.ERR.CREDENTIAL_EXCLUDED - except ClientError as e: - assert e.cause.code == CtapError.ERR.CREDENTIAL_EXCLUDED - - for i, x in enumerate(creds): - with Test("get assertion %d" % i): - allow_list = [{"id": x.credential_id, "type": "public-key"}] - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, allow_list, pin=PIN - ) - assertions[0].verify(client_data.hash, x.public_key) - - if PIN is not None: - with Test("get assertion with wrong pin code"): - try: - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, allow_list, pin=PIN + " " - ) - except CtapError as e: - assert e.code == CtapError.ERR.PIN_INVALID - except ClientError as e: - assert e.cause.code == CtapError.ERR.PIN_INVALID - - with Test("get multiple assertions"): - allow_list = [ - {"id": x.credential_id, "type": "public-key"} for x in creds - ] - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, allow_list, pin=PIN - ) - - for ass, cred in zip(assertions, creds): - i += 1 - - ass.verify(client_data.hash, cred.public_key) - print("%d verified" % i) - - with Test("Reset device"): - try: - self.ctap.reset() - except CtapError as e: - print("Warning, reset failed: ", e) - pass - - test(self, None) - - with Test("Set a pin code"): - PIN = "1122aabbwfg0h9g !@#==" - self.client.pin_protocol.set_pin(PIN) - - with Test("Illegally set pin code again"): - try: - self.client.pin_protocol.set_pin(PIN) - except CtapError as e: - assert e.code == CtapError.ERR.NOT_ALLOWED - - with Test("Change pin code"): - PIN2 = PIN + "_pin2" - self.client.pin_protocol.change_pin(PIN, PIN2) - PIN = PIN2 - - with Test("Change pin code using wrong pin"): - try: - self.client.pin_protocol.change_pin(PIN.replace("a", "b"), "1234") - except CtapError as e: - assert e.code == CtapError.ERR.PIN_INVALID - - with Test("MC using wrong pin"): - try: - self.test_fido2_simple("abcd3") - except ClientError as e: - assert e.cause.code == CtapError.ERR.PIN_INVALID - - with Test("get info"): - inf = self.ctap.get_info() - - self.test_fido2_simple(PIN) - - with Test("Re-run make_credential and get_assertion tests with pin code"): - test(self, PIN) - - with Test("Reset device"): - try: - self.ctap.reset() - except CtapError as e: - print("Warning, reset failed: ", e) - - def test_extensions(self,): - creds = [] - exclude_list = [] - rp = {"id": self.host, "name": "ExaRP"} - user = {"id": b"usee_od", "name": "AB User"} - challenge = "Y2hhbGxlbmdl" - pin_protocol = 1 - key_params = [{"type": "public-key", "alg": ES256.ALGORITHM}] - cdh = b"123456789abcdef0123456789abcdef0" - - 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 type(ext["hmac-secret"]) == type(b"") - 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_fido2_other(self,): - - creds = [] - exclude_list = [] - rp = {"id": self.host, "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 reboot(): - if self.is_sim: - print("Sending restart command...") - self.send_magic_reboot() - time.sleep(0.25) - else: - print("Please reboot authentictor and hit enter") - input() - self.find_device() - - self.testReset() - - with Test("Get info"): - info = self.ctap.get_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] - - prev_reg = self.testMC( - "Send MC request, expect success", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - ) - - 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 - - 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.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, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - 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 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, - ) - - 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, - ) - - 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, - ) - - 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() - - def testRk(pin_code=None): - self.testGA( - "Send GA request with reset auth, expect NO_CREDENTIALS", - rp["id"], - cdh, - allow_list, - expectedError=CtapError.ERR.NO_CREDENTIALS, - ) - - pin_auth = 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}, - expectedError=CtapError.ERR.SUCCESS, - ) - - 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}, - expectedError=CtapError.ERR.SUCCESS, - ) - - auth1 = self.testGA( - "Send GA request with no allow_list, expect SUCCESS", - rp2["id"], - cdh, - other={"options": options, "pin_auth": pin_auth}, - 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"): - assert y in x.user.keys() - assert len(x.user.keys()) == 4 - - with Test("Send an extra getNextAssertion request, expect error"): - try: - auth4 = self.ctap.get_next_assertion() - assert 0 - except CtapError as e: - print(e) - - testRk(None) - # - # print("Assuming authenticator does NOT have a display.") - pin1 = "1234567890" - testRk("1234567890") - - # 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 right alg" and key[3] == -7 - assert "Right key" and len(key[-3]) == 32 and type(key[-3]) == type(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}, - 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 no allow_list, expect SUCCESS", - rp["id"], - cdh, - [ - { - "type": "public-key", - "id": res_mc.auth_data.credential_data.credential_id, - } - ], - other={"pin_auth": pin_auth}, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check UV flag is set"): - assert res_ga.auth_data.flags & (1 << 2) - - self.testReset() - - with Test("Setting pin code, expect SUCCESS"): - self.client.pin_protocol.set_pin(pin1) - - self.testReset() - - # print("Setting pin code <4 bytes, expect POLICY_VIOLATION ") - # try: - # self.client.pin_protocol.set_pin("123") - # except CtapError as e: - # assert e.code == CtapError.ERR.POLICY_VIOLATION - # print("Pass") - - 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 PIN_REQUIRED", - rp["id"], - cdh, - expectedError=CtapError.ERR.PIN_REQUIRED, - ) - - 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, - ) - - 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}, - 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 >= 9: - err = CtapError.ERR.PIN_BLOCKED - 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: - reboot() - - res_mc = self.testMC( - "Send MC request with correct pin_auth, expect PIN_BLOCKED", - cdh, - rp, - user, - key_params, - other={"pin_auth": pin_auth}, - expectedError=CtapError.ERR.PIN_BLOCKED, - ) - - reboot() - - self.testPP( - "Get pin_token with correct pin code, expect PIN_BLOCKED", - pin1, - expectedError=CtapError.ERR.PIN_BLOCKED, - ) - - self.testReset() - - print("Done") - - def test_rk(self,): - creds = [] - rp = {"id": self.host, "name": "ExaRP"} - - users = [ - {"id": b"user" + os.urandom(16), "name": "Username%d" % i} - for i in range(0, self.user_count) - ] - challenge = "Y2hhbGxlbmdl" - PIN = None - self.ctap.reset() - # if PIN: self.client.pin_protocol.set_pin(PIN) - - with Test("registering 1 user with RK"): - t1 = time.time() * 1000 - attest, data = self.client.make_credential( - rp, users[-1], challenge, pin=PIN, exclude_list=[], rk=True - ) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - creds.append(attest.auth_data.credential_data) - - with Test("1 assertion"): - t1 = time.time() * 1000 - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, pin=PIN - ) - t2 = time.time() * 1000 - assertions[0].verify(client_data.hash, creds[0].public_key) - - with Test("registering %d users with RK" % len(users)): - for i in range(0, len(users) - 1): - t1 = time.time() * 1000 - attest, data = self.client.make_credential( - rp, users[i], challenge, pin=PIN, exclude_list=[], rk=True - ) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - creds.append(attest.auth_data.credential_data) - - t1 = time.time() * 1000 - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, pin=PIN - ) - t2 = time.time() * 1000 - - print("Got %d assertions for %d users" % (len(assertions), len(users))) - assert len(assertions) == len(users) - - for x, y in zip(assertions, creds): - x.verify(client_data.hash, y.public_key) - - print("Assertion(s) valid (%d ms)" % (t2 - t1)) - - with Test("register a duplicate user "): - t1 = time.time() * 1000 - attest, data = self.client.make_credential( - rp, users[1], challenge, pin=PIN, exclude_list=[], rk=True - ) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - creds = creds[:2] + creds[3:] + [attest.auth_data.credential_data] - - t1 = time.time() * 1000 - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, pin=PIN - ) - t2 = time.time() * 1000 - with Test("check %d assertions, %d users" % (len(assertions), len(users))): - assert len(assertions) == len(users) - for x, y in zip(assertions, creds): - x.verify(client_data.hash, y.public_key) - - def test_solo(self,): - """ - Solo specific tests - """ - # RNG command - sc = SoloClient() - sc.find_device(self.dev) - sc.use_u2f() - memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) - - total = 1024 * 16 - with Test("Gathering %d random bytes..." % total): - entropy = b"" - while len(entropy) < total: - entropy += sc.get_rng() - - with Test("Test entropy is close to perfect"): - sum = shannon_entropy(entropy) - assert sum > 7.98 - print("Entropy is %.5f bits per byte." % sum) - - with Test("Test Solo version command"): - assert len(sc.solo_version()) == 3 - - with Test("Test bootloader is not active"): - try: - sc.write_flash(memmap[0], b"1234") - except ApduError: - pass - - sc.exchange = sc.exchange_fido2 - with Test("Test Solo version and random commands with fido2 layer"): - assert len(sc.solo_version()) == 3 - sc.get_rng() - - def test_bootloader(self,): - sc = SoloClient() - sc.find_device(self.dev) - sc.use_u2f() - - memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) - data = b"A" * 64 - - with Test("Test version command"): - assert len(sc.bootloader_version()) == 3 - - with Test("Test write command"): - sc.write_flash(memmap[0], data) - - for addr in (memmap[0] - 8, memmap[0] - 4, memmap[1], memmap[1] - 8): - with Test("Test out of bounds write command at 0x%04x" % addr): - try: - sc.write_flash(addr, data) - except CtapError as e: - assert e.code == CtapError.ERR.NOT_ALLOWED - - def test_responses(self,): - PIN = "1234" - RPID = self.host - for dev in CtapHidDevice.list_devices(): - print("dev", dev) - client = Fido2Client(dev, RPID) - ctap = client.ctap2 - # ctap.reset() - try: - if PIN: - client.pin_protocol.set_pin(PIN) - except: - pass - - inf = ctap.get_info() - # print (inf) - print("versions: ", inf.versions) - print("aaguid: ", inf.aaguid) - print("rk: ", inf.options["rk"]) - print("clientPin: ", inf.options["clientPin"]) - print("max_message_size: ", inf.max_msg_size) - - # rp = {'id': 'SelectDevice', 'name': 'SelectDevice'} - rp = {"id": RPID, "name": "ExaRP"} - user = {"id": os.urandom(10), "name": "SelectDevice"} - user = {"id": b"21first one", "name": "single User"} - challenge = "Y2hhbGxlbmdl" - - if 1: - attest, data = client.make_credential( - rp, user, challenge, exclude_list=[], pin=PIN, rk=True - ) - - cred = attest.auth_data.credential_data - creds = [cred] - - allow_list = [{"id": creds[0].credential_id, "type": "public-key"}] - allow_list = [] - assertions, client_data = client.get_assertion( - rp["id"], challenge, pin=PIN - ) - assertions[0].verify(client_data.hash, creds[0].public_key) - - if 0: - print("registering 1 user with RK") - t1 = time.time() * 1000 - attest, data = client.make_credential( - rp, user, challenge, pin=PIN, exclude_list=[], rk=True - ) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - creds = [attest.auth_data.credential_data] - print("Register valid (%d ms)" % (t2 - t1)) - - print("1 assertion") - t1 = time.time() * 1000 - assertions, client_data = client.get_assertion( - rp["id"], challenge, pin=PIN - ) - t2 = time.time() * 1000 - assertions[0].verify(client_data.hash, creds[0].public_key) - print("Assertion valid (%d ms)" % (t2 - t1)) - - # print('fmt:',attest.fmt) - # print('rp_id_hash',attest.auth_data.rp_id_hash) - # print('flags:', hex(attest.auth_data.flags)) - # print('count:', hex(attest.auth_data.counter)) - print("flags MC:", attest.auth_data) - print("flags GA:", assertions[0].auth_data) - # print('cred_id:',attest.auth_data.credential_data.credential_id) - # print('pubkey:',attest.auth_data.credential_data.public_key) - # print('aaguid:',attest.auth_data.credential_data.aaguid) - # print('cred data:',attest.auth_data.credential_data) - # print('auth_data:',attest.auth_data) - # print('auth_data:',attest.auth_data) - # print('alg:',attest.att_statement['alg']) - # print('sig:',attest.att_statement['sig']) - # print('x5c:',attest.att_statement['x5c']) - # print('data:',data) - - print("assertion:", assertions[0]) - print("clientData:", client_data) - - print() - # break - - -def test_find_brute_force(): - i = 0 - while 1: - t1 = time.time() * 1000 - t = Tester() - t.find_device() - t2 = time.time() * 1000 - print("connected %d (%d ms)" % (i, t2 - t1)) - i += 1 - time.sleep(0.01) - - -if __name__ == "__main__": - tests = ("solo", "u2f", "fido2", "fido2-ext", "rk", "hid", "ping", "bootloader") - - if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} [sim] <{'|'.join(sorted(tests))}>") - sys.exit(0) - - t = Tester() - t.set_user_count(3) - - if "sim" in sys.argv: - print("Using UDP backend.") - force_udp_backend() - t.set_sim(True) - t.set_user_count(10) - - t.find_device() - - if "solo" in sys.argv: - t.test_solo() - - if "u2f" in sys.argv: - t.test_u2f() - - if "fido2" in sys.argv: - t.test_fido2() - t.test_fido2_other() - - if "fido2-ext" in sys.argv: - t.test_extensions() - - if "rk" in sys.argv: - t.test_rk() - - if "ping" in sys.argv: - t.test_long_ping() - - # hid tests are a bit invasive and should be done last - if "hid" in sys.argv: - t.test_hid(check_timeouts=t.is_sim) - - if "bootloader" in sys.argv: - if t.is_sim: - raise RuntimeError("Cannot test bootloader in simulation yet.") - print("Put device in bootloader mode and then hit enter") - input() - t.test_bootloader() - - # t.test_responses() - # test_find_brute_force() - # t.test_fido2_brute_force() diff --git a/tools/testing/main.py b/tools/testing/main.py new file mode 100644 index 0000000..7e105e0 --- /dev/null +++ b/tools/testing/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2019 SoloKeys Developers +# +# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +# copied, modified, or distributed except according to those terms. +# + +# Script for testing correctness of CTAP2/CTAP1 security token + +import sys + +from solo.fido2 import force_udp_backend +from tests import Tester, FIDO2Tests, U2FTests, HIDTests, SoloTests + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: %s [sim] <[u2f]|[fido2]|[rk]|[hid]|[ping]>") + sys.exit(0) + + t = Tester() + t.set_user_count(3) + + if "sim" in sys.argv: + print("Using UDP backend.") + force_udp_backend() + t.set_sim(True) + t.set_user_count(10) + + t.find_device() + + if "solo" in sys.argv: + SoloTests(t).run() + + if "u2f" in sys.argv: + U2FTests(t).run() + + if "fido2" in sys.argv: + # t.test_fido2() + FIDO2Tests(t).run() + + # hid tests are a bit invasive and should be done last + if "hid" in sys.argv: + HIDTests(t).run() + + if "bootloader" in sys.argv: + if t.is_sim: + raise RuntimeError("Cannot test bootloader in simulation yet.") + # print("Put device in bootloader mode and then hit enter") + # input() + # t.test_bootloader() + + # t.test_responses() + # t.test_fido2_brute_force() diff --git a/tools/testing/tests/__init__.py b/tools/testing/tests/__init__.py new file mode 100644 index 0000000..23cf5a8 --- /dev/null +++ b/tools/testing/tests/__init__.py @@ -0,0 +1,11 @@ +from . import fido2 +from . import hid +from . import solo +from . import u2f +from . import tester + +FIDO2Tests = fido2.FIDO2Tests +HIDTests = hid.HIDTests +U2FTests = u2f.U2FTests +SoloTests = solo.SoloTests +Tester = tester.Tester diff --git a/tools/testing/tests/fido2.py b/tools/testing/tests/fido2.py new file mode 100644 index 0000000..999f7d5 --- /dev/null +++ b/tools/testing/tests/fido2.py @@ -0,0 +1,1096 @@ +from __future__ import print_function, absolute_import, unicode_literals +import time +from random import randint +import array + + +from fido2.ctap import CtapError + +from fido2.ctap2 import ES256, PinProtocolV1 +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 .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) + + +class FIDO2Tests(Tester): + def __init__(self, tester=None): + super().__init__(tester) + + 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() + + 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 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"}], + ) + + 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 + if key[3] != -7: + print("WARNING: algorithm returned is not for ES256 (-7): ", key[3]) + 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, + ) + + # 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 set"): + assert res_ga.auth_data.flags & (1 << 2) + + self.testReset() + + with Test("Setting pin code, expect SUCCESS"): + self.client.pin_protocol.set_pin(pin1) + + 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") diff --git a/tools/testing/tests/hid.py b/tools/testing/tests/hid.py new file mode 100644 index 0000000..74d9d92 --- /dev/null +++ b/tools/testing/tests/hid.py @@ -0,0 +1,252 @@ +import sys, os, time +from binascii import hexlify + +from fido2.hid import CTAPHID +from fido2.ctap import CtapError + +from .tester import Tester, Test + + +class HIDTests(Tester): + def __init__(self, tester=None): + super().__init__(tester) + self.check_timeouts = False + + def set_check_timeouts(self, en): + self.check_timeouts = en + + def run(self,): + self.test_long_ping() + self.test_hid(self.check_timeouts) + + def test_long_ping(self): + amt = 1000 + pingdata = os.urandom(amt) + with Test("Send %d byte ping" % amt): + try: + t1 = time.time() * 1000 + r = self.send_data(CTAPHID.PING, pingdata) + t2 = time.time() * 1000 + delt = t2 - t1 + # if (delt < 140 ): + # raise RuntimeError('Fob is too fast (%d ms)' % delt) + if delt > 555 * (amt / 1000): + raise RuntimeError("Fob is too slow (%d ms)" % delt) + if r != pingdata: + raise ValueError("Ping data not echo'd") + except CtapError: + raise RuntimeError("ping failed") + + sys.stdout.flush() + + def test_hid(self, check_timeouts=False): + if check_timeouts: + with Test("idle"): + try: + cmd, resp = self.recv_raw() + except socket.timeout: + pass + + with Test("init"): + r = self.send_data(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") + + with Test("100 byte ping"): + pingdata = os.urandom(100) + try: + r = self.send_data(CTAPHID.PING, pingdata) + if r != pingdata: + raise ValueError("Ping data not echo'd") + except CtapError as e: + print("100 byte Ping failed:", e) + raise RuntimeError("ping failed") + + self.test_long_ping() + + with Test("Wink"): + r = self.send_data(CTAPHID.WINK, "") + + with Test("CBOR msg with no data"): + try: + r = self.send_data(CTAPHID.CBOR, "") + if len(r) > 1 or r[0] == 0: + raise RuntimeError("Cbor is supposed to have payload") + except CtapError as e: + assert e.code == CtapError.ERR.INVALID_LENGTH + + with Test("No data in U2F msg"): + try: + r = self.send_data(CTAPHID.MSG, "") + print(hexlify(r)) + if len(r) > 2: + raise RuntimeError("MSG is supposed to have payload") + except CtapError as e: + assert e.code == CtapError.ERR.INVALID_LENGTH + + with Test("Use init command to resync"): + r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + + with Test("Invalid HID command"): + try: + r = self.send_data(0x66, "") + raise RuntimeError("Invalid command did not return error") + except CtapError as e: + assert e.code == CtapError.ERR.INVALID_COMMAND + + with Test("Sending packet with too large of a length."): + self.send_raw("\x81\x1d\xba\x00") + cmd, resp = self.recv_raw() + Tester.check_error(resp, CtapError.ERR.INVALID_LENGTH) + + r = self.send_data(CTAPHID.PING, "\x44" * 200) + with Test("Sending packets that skip a sequence number."): + self.send_raw("\x81\x04\x90") + self.send_raw("\x00") + self.send_raw("\x01") + # skip 2 + self.send_raw("\x03") + cmd, resp = self.recv_raw() + Tester.check_error(resp, CtapError.ERR.INVALID_SEQ) + + with Test("Resync and send ping"): + try: + r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + pingdata = os.urandom(100) + r = self.send_data(CTAPHID.PING, pingdata) + if r != pingdata: + raise ValueError("Ping data not echo'd") + except CtapError as e: + raise RuntimeError("resync fail: ", e) + + with Test("Send ping and abort it"): + self.send_raw("\x81\x04\x00") + self.send_raw("\x00") + self.send_raw("\x01") + try: + r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + except CtapError as e: + raise RuntimeError("resync fail: ", e) + + with Test("Send ping and abort it with different cid, expect timeout"): + oldcid = self.cid() + newcid = "\x11\x22\x33\x44" + self.send_raw("\x81\x10\x00") + self.send_raw("\x00") + self.send_raw("\x01") + self.set_cid(newcid) + self.send_raw( + "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88" + ) # init from different cid + print("wait for init response") + cmd, r = self.recv_raw() # init response + assert cmd == 0x86 + self.set_cid(oldcid) + if check_timeouts: + # print('wait for timeout') + cmd, r = self.recv_raw() # timeout response + assert cmd == 0xBF + + with Test("Test timeout"): + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + t1 = time.time() * 1000 + self.send_raw("\x81\x04\x00") + self.send_raw("\x00") + self.send_raw("\x01") + cmd, r = self.recv_raw() # timeout response + t2 = time.time() * 1000 + delt = t2 - t1 + assert cmd == 0xBF + assert r[0] == CtapError.ERR.TIMEOUT + assert delt < 1000 and delt > 400 + + with Test("Test not cont"): + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + self.send_raw("\x81\x04\x00") + self.send_raw("\x00") + self.send_raw("\x01") + self.send_raw("\x81\x10\x00") # init packet + cmd, r = self.recv_raw() # timeout response + assert cmd == 0xBF + assert r[0] == CtapError.ERR.INVALID_SEQ + + if check_timeouts: + with Test("Check random cont ignored"): + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + self.send_raw("\x01\x10\x00") + try: + cmd, r = self.recv_raw() # timeout response + except socket.timeout: + pass + + with Test("Check busy"): + t1 = time.time() * 1000 + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + oldcid = self.cid() + newcid = "\x11\x22\x33\x44" + self.send_raw("\x81\x04\x00") + self.set_cid(newcid) + self.send_raw("\x81\x04\x00") + cmd, r = self.recv_raw() # busy response + t2 = time.time() * 1000 + assert t2 - t1 < 100 + assert cmd == 0xBF + assert r[0] == CtapError.ERR.CHANNEL_BUSY + + self.set_cid(oldcid) + cmd, r = self.recv_raw() # timeout response + assert cmd == 0xBF + assert r[0] == CtapError.ERR.TIMEOUT + + with Test("Check busy interleaved"): + cid1 = "\x11\x22\x33\x44" + cid2 = "\x01\x22\x33\x44" + self.set_cid(cid2) + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + self.set_cid(cid1) + self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + self.send_raw("\x81\x00\x63") # echo 99 bytes first channel + + self.set_cid(cid2) # send ping on 2nd channel + self.send_raw("\x81\x00\x63") + Tester.delay(0.1) + self.send_raw("\x00") + + cmd, r = self.recv_raw() # busy response + + self.set_cid(cid1) # finish 1st channel ping + self.send_raw("\x00") + + self.set_cid(cid2) + + assert cmd == 0xBF + assert r[0] == CtapError.ERR.CHANNEL_BUSY + + self.set_cid(cid1) + cmd, r = self.recv_raw() # ping response + assert cmd == 0x81 + assert len(r) == 0x63 + + if check_timeouts: + with Test("Test idle, wait for timeout"): + sys.stdout.flush() + try: + cmd, resp = self.recv_raw() + except socket.timeout: + pass + + with Test("Test cid 0 is invalid"): + self.set_cid("\x00\x00\x00\x00") + self.send_raw( + "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00" + ) + cmd, r = self.recv_raw() # timeout + assert cmd == 0xBF + assert r[0] == CtapError.ERR.INVALID_CHANNEL + + with Test("Test invalid broadcast cid use"): + self.set_cid("\xff\xff\xff\xff") + self.send_raw( + "\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff" + ) + cmd, r = self.recv_raw() # timeout + assert cmd == 0xBF + assert r[0] == CtapError.ERR.INVALID_CHANNEL diff --git a/tools/testing/tests/solo.py b/tools/testing/tests/solo.py new file mode 100644 index 0000000..e59421e --- /dev/null +++ b/tools/testing/tests/solo.py @@ -0,0 +1,70 @@ +from solo.client import SoloClient + +from fido2.ctap1 import ApduError + +from .util import shannon_entropy +from .tester import Tester, Test + + +class SoloTests(Tester): + def __init__(self, tester=None): + super().__init__(tester) + + def run(self,): + self.test_solo() + + def test_solo(self,): + """ + Solo specific tests + """ + # RNG command + sc = SoloClient() + sc.find_device(self.dev) + sc.use_u2f() + memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) + + total = 1024 * 16 + with Test("Gathering %d random bytes..." % total): + entropy = b"" + while len(entropy) < total: + entropy += sc.get_rng() + + with Test("Test entropy is close to perfect"): + s = shannon_entropy(entropy) + assert s > 7.98 + print("Entropy is %.5f bits per byte." % s) + + with Test("Test Solo version command"): + assert len(sc.solo_version()) == 3 + + with Test("Test bootloader is not active"): + try: + sc.write_flash(memmap[0], b"1234") + except ApduError: + pass + + sc.exchange = sc.exchange_fido2 + with Test("Test Solo version and random commands with fido2 layer"): + assert len(sc.solo_version()) == 3 + sc.get_rng() + + def test_bootloader(self,): + sc = SoloClient() + sc.find_device(self.dev) + sc.use_u2f() + + memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) + data = b"A" * 64 + + with Test("Test version command"): + assert len(sc.bootloader_version()) == 3 + + with Test("Test write command"): + sc.write_flash(memmap[0], data) + + for addr in (memmap[0] - 8, memmap[0] - 4, memmap[1], memmap[1] - 8): + with Test("Test out of bounds write command at 0x%04x" % addr): + try: + sc.write_flash(addr, data) + except CtapError as e: + assert e.code == CtapError.ERR.NOT_ALLOWED diff --git a/tools/testing/tests/tester.py b/tools/testing/tests/tester.py new file mode 100644 index 0000000..e0ae813 --- /dev/null +++ b/tools/testing/tests/tester.py @@ -0,0 +1,197 @@ +import time, struct + +from fido2.hid import CtapHidDevice +from fido2.client import Fido2Client +from fido2.ctap1 import CTAP1 +from fido2.utils import Timeout + +from fido2.ctap import CtapError + + +def ForceU2F(client, device): + client.ctap = CTAP1(device) + client.pin_protocol = None + client._do_make_credential = client._ctap1_make_credential + client._do_get_assertion = client._ctap1_get_assertion + + +class Packet(object): + def __init__(self, data): + self.data = data + + def ToWireFormat(self,): + return self.data + + @staticmethod + def FromWireFormat(pkt_size, data): + return Packet(data) + + +class Test: + def __init__(self, msg): + self.msg = msg + + def __enter__(self,): + print(self.msg) + + def __exit__(self, a, b, c): + print("Pass") + + +class Tester: + def __init__(self, tester=None): + self.origin = "https://examplo.org" + self.host = "examplo.org" + self.user_count = 10 + self.is_sim = False + if tester: + self.initFromTester(tester) + + def initFromTester(self, tester): + self.user_count = tester.user_count + self.is_sim = tester.is_sim + self.dev = tester.dev + self.ctap = tester.ctap + self.ctap1 = tester.ctap1 + self.client = tester.client + + def find_device(self,): + print(list(CtapHidDevice.list_devices())) + dev = next(CtapHidDevice.list_devices(), None) + if not dev: + raise RuntimeError("No FIDO device found") + self.dev = dev + self.client = Fido2Client(dev, self.origin) + self.ctap = self.client.ctap2 + self.ctap1 = CTAP1(dev) + + # consume timeout error + # cmd,resp = self.recv_raw() + + def set_user_count(self, count): + self.user_count = count + + def set_sim(self, b): + self.is_sim = b + + def reboot(self,): + if self.is_sim: + print("Sending restart command...") + self.send_magic_reboot() + Tester.delay(0.25) + else: + print("Please reboot authentictor and hit enter") + input() + self.find_device() + + def send_data(self, cmd, data): + if not isinstance(data, bytes): + data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) + with Timeout(1.0) as event: + return self.dev.call(cmd, data, event) + + def send_raw(self, data, cid=None): + if cid is None: + cid = self.dev._dev.cid + elif not isinstance(cid, bytes): + cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) + if not isinstance(data, bytes): + data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) + data = cid + data + l = len(data) + if l != 64: + pad = "\x00" * (64 - l) + pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad]) + data = data + pad + data = list(data) + assert len(data) == 64 + self.dev._dev.InternalSendPacket(Packet(data)) + + def send_magic_reboot(self,): + """ + For use in simulation and testing. Random bytes that authentictor should detect + and then restart itself. + """ + magic_cmd = ( + b"\xac\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf" + + b"\xf3\x33\x48\x5f\x13\xf9\xb2\xda\x34\xc5\xa8\xa3" + + b"\x40\x52\x66\x97\xa9\xab\x2e\x0b\x39\x4d\x8d\x04" + + b"\x97\x3c\x13\x40\x05\xbe\x1a\x01\x40\xbf\xf6\x04" + + b"\x5b\xb2\x6e\xb7\x7a\x73\xea\xa4\x78\x13\xf6\xb4" + + b"\x9a\x72\x50\xdc" + ) + self.dev._dev.InternalSendPacket(Packet(magic_cmd)) + + def cid(self,): + return self.dev._dev.cid + + def set_cid(self, cid): + if not isinstance(cid, (bytes, bytearray)): + cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) + self.dev._dev.cid = cid + + def recv_raw(self,): + with Timeout(1.0): + cmd, payload = self.dev._dev.InternalRecv() + return cmd, payload + + def check_error(data, err=None): + assert len(data) == 1 + if err is None: + if data[0] != 0: + raise CtapError(data[0]) + elif data[0] != err: + raise ValueError("Unexpected error: %02x" % data[0]) + + def testFunc(self, func, test, *args, **kwargs): + with Test(test): + res = None + expectedError = kwargs.get("expectedError", None) + otherArgs = kwargs.get("other", {}) + try: + res = func(*args, **otherArgs) + if expectedError != CtapError.ERR.SUCCESS: + raise RuntimeError("Expected error to occur for test: %s" % test) + except CtapError as e: + if expectedError is not None: + cond = e.code != expectedError + if isinstance(expectedError, list): + cond = e.code not in expectedError + else: + expectedError = [expectedError] + if cond: + raise RuntimeError( + f"Got error code {hex(e.code)}, expected {[hex(x) for x in expectedError]}" + ) + else: + print(e) + return res + + def testReset(self,): + print("Resetting Authenticator...") + try: + self.ctap.reset() + except CtapError: + # Some authenticators need a power cycle + print("You must power cycle authentictor. Hit enter when done.") + input() + time.sleep(0.2) + self.find_device() + self.ctap.reset() + + def testMC(self, test, *args, **kwargs): + return self.testFunc(self.ctap.make_credential, test, *args, **kwargs) + + def testGA(self, test, *args, **kwargs): + return self.testFunc(self.ctap.get_assertion, test, *args, **kwargs) + + def testCP(self, test, *args, **kwargs): + return self.testFunc(self.ctap.client_pin, test, *args, **kwargs) + + def testPP(self, test, *args, **kwargs): + return self.testFunc( + self.client.pin_protocol.get_pin_token, test, *args, **kwargs + ) + + def delay(secs): + time.sleep(secs) diff --git a/tools/testing/tests/u2f.py b/tools/testing/tests/u2f.py new file mode 100644 index 0000000..d1e8033 --- /dev/null +++ b/tools/testing/tests/u2f.py @@ -0,0 +1,121 @@ +from fido2.ctap1 import CTAP1, ApduError, APDU +from fido2.utils import sha256 +from fido2.client import _call_polling + +from .tester import Tester, Test + + +class U2FTests(Tester): + def __init__(self, tester=None): + super().__init__(tester) + + def run(self,): + self.test_u2f() + + def register(self, chal, appid): + reg_data = _call_polling(0.25, None, None, self.ctap1.register, chal, appid) + return reg_data + + def authenticate(self, chal, appid, key_handle, check_only=False): + auth_data = _call_polling( + 0.25, + None, + None, + self.ctap1.authenticate, + chal, + appid, + key_handle, + check_only=check_only, + ) + return auth_data + + def test_u2f(self,): + chal = sha256(b"AAA") + appid = sha256(b"BBB") + lastc = 0 + + regs = [] + + with Test("Check version"): + assert self.ctap1.get_version() == "U2F_V2" + + with Test("Check bad INS"): + try: + self.ctap1.send_apdu(0, 0, 0, 0, b"") + except ApduError as e: + assert e.code == 0x6D00 + + with Test("Check bad CLA"): + try: + self.ctap1.send_apdu(1, CTAP1.INS.VERSION, 0, 0, b"abc") + except ApduError as e: + assert e.code == 0x6E00 + + for i in range(0, self.user_count): + with Test( + "U2F reg + auth %d/%d (count: %02x)" % (i + 1, self.user_count, lastc) + ): + reg = self.register(chal, appid) + reg.verify(appid, chal) + auth = self.authenticate(chal, appid, reg.key_handle) + auth.verify(appid, chal, reg.public_key) + + regs.append(reg) + # check endianness + if lastc: + assert (auth.counter - lastc) < 10 + lastc = auth.counter + if lastc > 0x80000000: + print("WARNING: counter is unusually high: %04x" % lastc) + assert 0 + + for i in range(0, self.user_count): + with Test( + "Checking previous registration %d/%d" % (i + 1, self.user_count) + ): + auth = self.authenticate(chal, appid, regs[i].key_handle) + auth.verify(appid, chal, regs[i].public_key) + + print("Check that all previous credentials are registered...") + for i in range(0, self.user_count): + with Test("Check that previous credential %d is registered" % i): + try: + auth = self.ctap1.authenticate( + chal, appid, regs[i].key_handle, check_only=True + ) + except ApduError as e: + # Indicates that key handle is registered + assert e.code == APDU.USE_NOT_SATISFIED + + with Test("Check an incorrect key handle is not registered"): + kh = bytearray(regs[0].key_handle) + kh[0] = kh[0] ^ (0x40) + try: + self.ctap1.authenticate(chal, appid, kh, check_only=True) + assert 0 + except ApduError as e: + assert e.code == APDU.WRONG_DATA + + with Test("Try to sign with incorrect key handle"): + try: + self.ctap1.authenticate(chal, appid, kh) + assert 0 + except ApduError as e: + assert e.code == APDU.WRONG_DATA + + with Test("Try to sign using an incorrect keyhandle length"): + try: + kh = regs[0].key_handle + self.ctap1.authenticate(chal, appid, kh[: len(kh) // 2]) + assert 0 + except ApduError as e: + assert e.code == APDU.WRONG_DATA + + with Test("Try to sign using an incorrect appid"): + badid = bytearray(appid) + badid[0] = badid[0] ^ (0x40) + try: + auth = self.ctap1.authenticate(chal, badid, regs[0].key_handle) + assert 0 + except ApduError as e: + assert e.code == APDU.WRONG_DATA diff --git a/tools/testing/tests/util.py b/tools/testing/tests/util.py new file mode 100644 index 0000000..94c3c45 --- /dev/null +++ b/tools/testing/tests/util.py @@ -0,0 +1,12 @@ +import math + + +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