From 53fb0059a7cff3c5a35f655d0bbf82689f3f1da7 Mon Sep 17 00:00:00 2001 From: Conor Patrick Date: Fri, 22 Mar 2019 00:20:20 -0400 Subject: [PATCH] break into separate files --- tools/testing/ctap_test.py | 67 ++ tools/testing/tests/__init__.py | 11 + .../{ctap_test.py => testing/tests/fido2.py} | 725 +----------------- tools/testing/tests/hid.py | 245 ++++++ tools/testing/tests/solo.py | 70 ++ tools/testing/tests/tester.py | 181 +++++ tools/testing/tests/u2f.py | 103 +++ tools/testing/tests/util.py | 12 + 8 files changed, 700 insertions(+), 714 deletions(-) create mode 100644 tools/testing/ctap_test.py create mode 100644 tools/testing/tests/__init__.py rename tools/{ctap_test.py => testing/tests/fido2.py} (64%) mode change 100755 => 100644 create mode 100644 tools/testing/tests/hid.py create mode 100644 tools/testing/tests/solo.py create mode 100644 tools/testing/tests/tester.py create mode 100644 tools/testing/tests/u2f.py create mode 100644 tools/testing/tests/util.py diff --git a/tools/testing/ctap_test.py b/tools/testing/ctap_test.py new file mode 100644 index 0000000..37c1e06 --- /dev/null +++ b/tools/testing/ctap_test.py @@ -0,0 +1,67 @@ +#!/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() + + if "fido2-ext" in sys.argv: + pass + + if "rk" in sys.argv: + pass + + if "ping" in sys.argv: + pass + + # 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/ctap_test.py b/tools/testing/tests/fido2.py old mode 100755 new mode 100644 similarity index 64% rename from tools/ctap_test.py rename to tools/testing/tests/fido2.py index 984879a..ee8a7c7 --- a/tools/ctap_test.py +++ b/tools/testing/tests/fido2.py @@ -1,26 +1,12 @@ -#!/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 +import sys, os, time 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 @@ -28,18 +14,8 @@ 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 +from .tester import Tester, Test +from .util import shannon_entropy def VerifyAttestation(attest, data): @@ -47,485 +23,12 @@ def VerifyAttestation(attest, data): 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 FIDO2Tests(Tester): + def __init__(self, tester=None): + super().__init__(tester) - -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 run(self,): + self.test_fido2_other() def test_fido2_simple(self, pin_token=None): creds = [] @@ -955,7 +458,7 @@ class Tester: if self.is_sim: print("Sending restart command...") self.send_magic_reboot() - time.sleep(0.25) + self.delay(0.25) else: print("Please reboot authentictor and hit enter") input() @@ -1098,7 +601,6 @@ class Tester: rp, user, None, - expectedError=CtapError.ERR.MISSING_PARAMETER, ) self.testMC( @@ -1847,208 +1349,3 @@ class Tester: 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__": - 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: - 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/tests/hid.py b/tools/testing/tests/hid.py new file mode 100644 index 0000000..89a9e70 --- /dev/null +++ b/tools/testing/tests/hid.py @@ -0,0 +1,245 @@ +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_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 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") + self.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..518389a --- /dev/null +++ b/tools/testing/tests/solo.py @@ -0,0 +1,70 @@ +from solo.client import SoloClient + +from fido2.ctap1 import ApduError, APDU + +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"): + 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 diff --git a/tools/testing/tests/tester.py b/tools/testing/tests/tester.py new file mode 100644 index 0000000..83ab4a0 --- /dev/null +++ b/tools/testing/tests/tester.py @@ -0,0 +1,181 @@ +import time + +from fido2.hid import CtapHidDevice, CTAPHID +from fido2.client import Fido2Client, ClientError +from fido2.ctap1 import CTAP1, ApduError, APDU +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): + 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, 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 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...") + try: + self.ctap.reset() + except CtapError as e: + # 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(self, secs): + time.sleep(secs) diff --git a/tools/testing/tests/u2f.py b/tools/testing/tests/u2f.py new file mode 100644 index 0000000..1d0b817 --- /dev/null +++ b/tools/testing/tests/u2f.py @@ -0,0 +1,103 @@ +from fido2.ctap1 import CTAP1, ApduError, APDU +from fido2.utils import sha256 + +from .tester import Tester, Test + + +class U2FTests(Tester): + def __init__(self, tester=None): + super().__init__(tester) + + def run(self,): + self.test_u2f() + + 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 diff --git a/tools/testing/tests/util.py b/tools/testing/tests/util.py new file mode 100644 index 0000000..d6b07ae --- /dev/null +++ b/tools/testing/tests/util.py @@ -0,0 +1,12 @@ +import math + + +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