diff --git a/tools/ctap_test.py b/tools/ctap_test.py index 3326c2c..5695f27 100755 --- a/tools/ctap_test.py +++ b/tools/ctap_test.py @@ -12,7 +12,7 @@ # Script for testing correctness of CTAP2/CTAP1 security token from __future__ import print_function, absolute_import, unicode_literals -import sys, os, time +import sys, os, time, math from random import randint from binascii import hexlify import array, struct, socket @@ -21,12 +21,12 @@ 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 * -from fido2.cose import * +from fido2.ctap2 import ES256, PinProtocolV1 from fido2.utils import Timeout, sha256, hmac_sha256 from fido2.attestation import Attestation from solo.fido2 import force_udp_backend +from solo.client import SoloClient # Set up a FIDO 2 client using the origin https://example.com @@ -57,6 +57,17 @@ class Packet(object): 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" @@ -145,273 +156,234 @@ class Tester: def test_long_ping(self): amt = 1000 pingdata = os.urandom(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") - print("1000 byte ping time: %s ms" % delt) - except CtapError as e: - print("7609 byte Ping failed:", e) - raise RuntimeError("ping failed") - print("PASS: 7609 byte ping") - # sys.flush(sys.sto) + 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: - print("Test idle") + 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: - cmd, resp = self.recv_raw() - except socket.timeout: - print("Pass: Idle") - - print("Test init") - r = self.send_data(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") - - 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") - print("PASS: 100 byte ping") + 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() - try: + with Test("Wink"): r = self.send_data(CTAPHID.WINK, "") - print(hexlify(r)) - # assert(len(r) == 0) - except CtapError as e: - print("wink failed:", e) - raise RuntimeError("wink failed") - print("PASS: wink") - # try: - # r = self.send_data(CTAPHID.WINK, 'we9gofrei8g') - # raise RuntimeError('Wink is not supposed to have payload') - # except CtapError as e: - # assert(e.code == CtapError.ERR.INVALID_LENGTH) - # print('PASS: malformed 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 - 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 - print("PASS: no data cbor") + 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 - 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 - print("PASS: no data msg") - - try: + with Test("Use init command to resync"): r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - except CtapError as e: - raise RuntimeError("resync fail: ", e) - print("PASS: resync") - 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 - print("PASS: invalid HID command") + 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 - print("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) - print("PASS: invalid length") + 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) - print("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) - if check_timeouts: + 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() - assert cmd == 0xBF # timeout - print("PASS: invalid sequence") + self.check_error(resp, CtapError.ERR.INVALID_SEQ) - print("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) - print("PASS: resync and ping") - - print("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) - print("PASS: interrupt ping with resync") - - print("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 - - print("PASS: resync and timeout") - - print("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 - print("Pass timeout") - - print("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 - print("PASS: Test not cont") - - if check_timeouts: - print("Check random cont ignored") - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x01\x10\x00") + 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 - except socket.timeout: - pass - print("PASS: random cont") + assert cmd == 0xBF - print("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 - print("PASS: busy") - - print("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: - cmd, r = self.recv_raw() # timeout + 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 - print("PASS: busy interleaved") + 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: - print("Test idle, wait for timeout") - sys.stdout.flush() - try: - cmd, resp = self.recv_raw() - except socket.timeout: - print("Pass: Idle") + 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 - print("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 - print("Pass: cid 0") + 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 - print("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 - print("Pass: cid broadcast") + 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") @@ -420,98 +392,89 @@ class Tester: regs = [] - print("Check version") - assert self.ctap1.get_version() == "U2F_V2" - print("Pass") + with Test("Check version"): + assert self.ctap1.get_version() == "U2F_V2" - print("Check bad INS") - try: - res = self.ctap1.send_apdu(0, 0, 0, 0, b"") - except ApduError as e: - assert e.code == 0x6D00 - print("Pass") + with Test("Check bad INS"): + try: + res = self.ctap1.send_apdu(0, 0, 0, 0, b"") + except ApduError as e: + assert e.code == 0x6D00 - print("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 - print("Pass") + 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): - 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) + 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 + 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 - print( - "U2F reg + auth pass %d/%d (count: %02x)" - % (i + 1, self.user_count, lastc) - ) - - print("Checking previous registrations...") for i in range(0, self.user_count): - auth = self.ctap1.authenticate(chal, appid, regs[i].key_handle) - auth.verify(appid, chal, regs[i].public_key) - print("Auth pass %d/%d" % (i + 1, 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: - auth = self.ctap1.authenticate( - chal, appid, regs[i].key_handle, check_only=True - ) + self.ctap1.authenticate(chal, appid, kh, check_only=True) + assert 0 except ApduError as e: - # Indicates that key handle is registered - assert e.code == APDU.USE_NOT_SATISFIED + assert e.code == APDU.WRONG_DATA - print("Check pass %d/%d" % (i + 1, self.user_count)) + 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 - print("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 - print("Pass") + 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 - print("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 - print("Pass") - - print("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 - print("Pass") - - print("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 - print("Pass") + 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 = [] @@ -527,14 +490,13 @@ class Tester: exclude_list.append({"id": fake_id1, "type": "public-key"}) exclude_list.append({"id": fake_id2, "type": "public-key"}) - print("MC") 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 valid (%d ms)" % (t2 - t1)) + print("Register time: %d ms" % (t2 - t1)) cred = attest.auth_data.credential_data creds.append(cred) @@ -547,7 +509,7 @@ class Tester: t2 = time.time() * 1000 assertions[0].verify(client_data.hash, creds[0].public_key) - print("Assertion valid (%d ms)" % (t2 - t1)) + print("Assertion time: %d ms" % (t2 - t1)) def test_fido2_brute_force(self): creds = [] @@ -567,10 +529,10 @@ class Tester: fake_id1 = array.array( "B", [randint(0, 255) for i in range(0, 150)] - ).tostring() + ).tobytes() fake_id2 = array.array( "B", [randint(0, 255) for i in range(0, 73)] - ).tostring() + ).tobytes() exclude_list.append({"id": fake_id1, "type": "public-key"}) exclude_list.append({"id": fake_id2, "type": "public-key"}) @@ -615,160 +577,151 @@ class Tester: fake_id1 = array.array( "B", [randint(0, 255) for i in range(0, 150)] - ).tostring() + ).tobytes() fake_id2 = array.array( "B", [randint(0, 255) for i in range(0, 73)] - ).tostring() + ).tobytes() exclude_list.append({"id": fake_id1, "type": "public-key"}) exclude_list.append({"id": fake_id2, "type": "public-key"}) # test make credential - print("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) + 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 + # 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) - print("PASS") + cred = attest.auth_data.credential_data + creds.append(cred) + print(cred) if PIN is not None: - print("make credential with wrong pin code") + 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=[] + 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.PIN_INVALID + assert e.code == CtapError.ERR.CREDENTIAL_EXCLUDED except ClientError as e: - assert e.cause.code == CtapError.ERR.PIN_INVALID - print("PASS") - - print("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) - print("PASS") - - print("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 - print("PASS") + assert e.cause.code == CtapError.ERR.CREDENTIAL_EXCLUDED for i, x in enumerate(creds): - print("get assertion %d" % i) - allow_list = [{"id": x.credential_id, "type": "public-key"}] + 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 ) - assertions[0].verify(client_data.hash, x.public_key) - print("PASS") - if PIN is not None: - print("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 - print("PASS") + for ass, cred in zip(assertions, creds): + i += 1 - print("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 - ) + ass.verify(client_data.hash, cred.public_key) + print("%d verified" % i) - for ass, cred in zip(assertions, creds): - i += 1 - - ass.verify(client_data.hash, cred.public_key) - print("%d verified" % i) - print("PASS") - - print("Reset device") - try: - self.ctap.reset() - except CtapError as e: - print("Warning, reset failed: ", e) - pass - print("PASS") + with Test("Reset device"): + try: + self.ctap.reset() + except CtapError as e: + print("Warning, reset failed: ", e) + pass test(self, None) - print("Set a pin code") - PIN = "1122aabbwfg0h9g !@#==" - self.client.pin_protocol.set_pin(PIN) - print("PASS") - - print("Illegally set pin code again") - try: + with Test("Set a pin code"): + PIN = "1122aabbwfg0h9g !@#==" self.client.pin_protocol.set_pin(PIN) - except CtapError as e: - assert e.code == CtapError.ERR.NOT_ALLOWED - print("PASS") - print("Change pin code") - PIN2 = PIN + "_pin2" - self.client.pin_protocol.change_pin(PIN, PIN2) - PIN = PIN2 - print("PASS") + 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 - print("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 - print("PASS") + with Test("Change pin code"): + PIN2 = PIN + "_pin2" + self.client.pin_protocol.change_pin(PIN, PIN2) + PIN = PIN2 - print("MC using wrong pin") - try: - self.test_fido2_simple("abcd3") - except ClientError as e: - assert e.cause.code == CtapError.ERR.PIN_INVALID - print("PASS") + 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 - print("get info") - inf = self.ctap.get_info() - print("PASS") + 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) - print("Re-run make_credential and get_assertion tests with pin code") - test(self, PIN) + with Test("Re-run make_credential and get_assertion tests with pin code"): + test(self, PIN) - print("Reset device") - try: - self.ctap.reset() - except CtapError as e: - print("Warning, reset failed: ", e) - print("PASS") + with Test("Reset device"): + try: + self.ctap.reset() + except CtapError as e: + print("Warning, reset failed: ", e) def test_fido2_other(self,): @@ -786,23 +739,25 @@ class Tester: cdh = b"123456789abcdef0123456789abcdef0" def testFunc(func, test, *args, **kwargs): - print(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: + 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( - "Got error code 0x%x, expected %x" % (e.code, expectedError) + "Expected error to occur for test: %s" % test ) - else: - print(e) - print("Pass") + 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(): @@ -835,25 +790,19 @@ class Tester: testReset() - print("Get info") - info = self.ctap.get_info() + with Test("Get info"): + info = self.ctap.get_info() - print(info) - print("Pass") + with Test("Check FIDO2 string is in VERSIONS field"): + assert "FIDO_2_0" in info.versions - print("Check FIDO2 string is in VERSIONS field") - assert "FIDO_2_0" in info.versions - print("Pass") + with Test("Check pin protocols field"): + if len(info.pin_protocols): + assert sum(info.pin_protocols) > 0 - print("Check pin protocols field") - if len(info.pin_protocols): - assert sum(info.pin_protocols) > 0 - print("Pass") - - print("Check options field") - for x in info.options: - assert info.options[x] in [True, False] - print("Pass") + with Test("Check options field"): + for x in info.options: + assert info.options[x] in [True, False] prev_reg = testMC( "Send MC request, expect success", @@ -864,13 +813,11 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Check attestation format is correct") - assert prev_reg.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] - print("Pass") + with Test("Check attestation format is correct"): + assert prev_reg.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] - print("Check auth_data is at least 77 bytes") - assert len(prev_reg.auth_data) >= 77 - print("Pass") + with Test("Check auth_data is at least 77 bytes"): + assert len(prev_reg.auth_data) >= 77 allow_list = [ { @@ -887,15 +834,39 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Test auth_data is 37 bytes") - assert len(prev_auth.auth_data) == 37 - print("pass") + with Test("Test auth_data is 37 bytes"): + assert len(prev_auth.auth_data) == 37 - print("Test that user, credential and numberOfCredentials are not present") - assert prev_auth.user == None - assert prev_auth.number_of_credentials == None - # assert prev_auth.credential == None # TODO double check this - print("Pass") + 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 + + 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) + + 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, + ) testMC( "Send MC request with missing clientDataHash, expect error", @@ -1276,9 +1247,8 @@ class Tester: other={"options": {"uv": True}}, expectedError=CtapError.ERR.SUCCESS, ) - print("Check that UV flag is set in response") - assert res.auth_data.flags & (1 << 2) - print("Pass") + 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 = testGA( @@ -1289,9 +1259,8 @@ class Tester: other={"options": {"up": True}}, expectedError=CtapError.ERR.SUCCESS, ) - print("Check that UP flag is set in response") + with Test("Check that UP flag is set in response"): assert res.auth_data.flags & 1 - print("Pass") testGA( "Send GA request with bogus type item in allow_list, expect SUCCESS", @@ -1342,11 +1311,10 @@ class Tester: pin_auth = None if pin_code: - print("Setting 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] - print("Pass") + 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] testMC( "Send MC request with rk option set to true, expect SUCCESS", @@ -1382,35 +1350,31 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Check that there are 3 credentials returned") - assert auth1.number_of_credentials == 3 - print("Pass") + with Test("Check that there are 3 credentials returned"): + assert auth1.number_of_credentials == 3 - print("Get the next 2 assertions") - auth2 = self.ctap.get_next_assertion() - auth3 = self.ctap.get_next_assertion() - print("Pass") + with Test("Get the next 2 assertions"): + auth2 = self.ctap.get_next_assertion() + auth3 = self.ctap.get_next_assertion() if not pin_code: - print("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 - print("Pass") + 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: - print("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 - print("Pass") + 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 - print("Send an extra getNextAssertion request, expect error") - try: - auth4 = self.ctap.get_next_assertion() - except CtapError as e: - print(e) - print("Pass") + 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) # @@ -1426,23 +1390,20 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("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()) - print("Pass") + 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()) - print("Test setting a new pin") - pin2 = "qwertyuiop\x11\x22\x33\x00123" - self.client.pin_protocol.change_pin(pin1, pin2) - print("Pass") + with Test("Test setting a new pin"): + pin2 = "qwertyuiop\x11\x22\x33\x00123" + self.client.pin_protocol.change_pin(pin1, pin2) - print("Test getting new pin_auth") - pin_token = self.client.pin_protocol.get_pin_token(pin2) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - print("Pass") + 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 = testMC( "Send MC request with new pin auth", @@ -1454,9 +1415,8 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Check UV flag is set") - assert res_mc.auth_data.flags & (1 << 2) - print("Pass") + with Test("Check UV flag is set"): + assert res_mc.auth_data.flags & (1 << 2) res_ga = testGA( "Send GA request with no allow_list, expect SUCCESS", @@ -1472,15 +1432,13 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Check UV flag is set") - assert res_ga.auth_data.flags & (1 << 2) - print("Pass") + with Test("Check UV flag is set"): + assert res_ga.auth_data.flags & (1 << 2) testReset() - print("Setting pin code, expect SUCCESS") - self.client.pin_protocol.set_pin(pin1) - print("Pass") + with Test("Setting pin code, expect SUCCESS"): + self.client.pin_protocol.set_pin(pin1) testReset() @@ -1491,18 +1449,58 @@ class Tester: # assert e.code == CtapError.ERR.POLICY_VIOLATION # print("Pass") - print("Setting pin code >63 bytes, expect POLICY_VIOLATION ") - try: - self.client.pin_protocol.set_pin("A" * 64) - except CtapError as e: - assert e.code == CtapError.ERR.PIN_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 - print("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] - print("Pass") + 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 = testMC( + "Send MC request with no pin_auth, expect PIN_REQUIRED", + cdh, + rp, + user, + key_params, + expectedError=CtapError.ERR.PIN_REQUIRED, + ) + + res_mc = testGA( + "Send GA request with no pin_auth, expect PIN_REQUIRED", + rp["id"], + cdh, + expectedError=CtapError.ERR.PIN_REQUIRED, + ) res = testCP( "Test getRetries, expect SUCCESS", @@ -1511,9 +1509,8 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Check there is 8 pin attempts left") - assert res[3] == 8 - print("Pass") + with Test("Check there is 8 pin attempts left"): + assert res[3] == 8 # Flip 1 bit pin_wrong = list(pin1) @@ -1542,10 +1539,9 @@ class Tester: reboot() - print("Get pin_token, expect SUCCESS") - pin_token = self.client.pin_protocol.get_pin_token(pin1) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - print("Pass") + 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 = testMC( "Send MC request with correct pin_auth", @@ -1557,10 +1553,9 @@ class Tester: expectedError=CtapError.ERR.SUCCESS, ) - print("Test getRetries resets to 8") - res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES) - assert res[3] == (8) - print("Pass") + 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 @@ -1578,11 +1573,9 @@ class Tester: if i > 8: attempts = 0 - print("Check there is %d pin attempts left" % attempts) - res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES) - assert res[3] == attempts - - print("Pass") + 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() @@ -1619,43 +1612,36 @@ class Tester: ] challenge = "Y2hhbGxlbmdl" PIN = None - print("reset") self.ctap.reset() # if PIN: self.client.pin_protocol.set_pin(PIN) - print("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) - print("Register valid (%d ms)" % (t2 - t1)) - - print("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) - print("Assertion valid (%d ms)" % (t2 - t1)) - - print(assertions[0], client_data) - - print("registering %d users with RK" % len(users)) - for i in range(0, len(users) - 1): + with Test("registering 1 user with RK"): t1 = time.time() * 1000 attest, data = self.client.make_credential( - rp, users[i], challenge, pin=PIN, exclude_list=[], rk=True + rp, users[-1], challenge, pin=PIN, exclude_list=[], rk=True ) t2 = time.time() * 1000 VerifyAttestation(attest, data) - print("Register valid (%d ms)" % (t2 - t1)) - 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 @@ -1670,28 +1656,80 @@ class Tester: print("Assertion(s) valid (%d ms)" % (t2 - t1)) - print("registering 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] - print("Register 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 - print("Assertions: %d, users: %d" % (len(assertions), len(users))) - assert len(assertions) == len(users) - for x, y in zip(assertions, creds): - x.verify(client_data.hash, y.public_key) + 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) - print("Assertion(s) valid (%d ms)" % (t2 - t1)) + 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() + total = len(entropy) + + with Test("Test entropy is close to perfect"): + sum = 0.0 + for x in range(0, 256): + freq = entropy.count(x) + p = freq / total + sum -= p * math.log2(p) + 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 + + 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" @@ -1808,6 +1846,9 @@ if __name__ == "__main__": t.find_device() + if "solo" in sys.argv: + t.test_solo() + if "u2f" in sys.argv: t.test_u2f() @@ -1823,7 +1864,14 @@ if __name__ == "__main__": # hid tests are a bit invasive and should be done last if "hid" in sys.argv: - t.test_hid() + 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()