Merge branch 'master' into patch-3

pull/236/head
Wessel dR 2019-08-06 13:43:13 +02:00 zatwierdzone przez GitHub
commit 6b8e575fac
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 2 dodań i 2123 usunięć

Wyświetl plik

@ -94,10 +94,7 @@ Run the Solo application:
./main
```
In another shell, you can run client software, for example our tests:
```bash
python tools/ctap_test.py sim fido2
```
In another shell, you can run our [test suite](https://github.com/solokeys/fido2-tests).
You can find more details in our [documentation](https://docs.solokeys.io/solo/), including how to build on the the NUCLEO-L432KC development board.

Wyświetl plik

@ -11,6 +11,7 @@ nav:
- FIDO2 Implementation: solo/fido2-impl.md
- Metadata Statements: solo/metadata-statements.md
- Build instructions: solo/building.md
- Running on Nucleo32 board: solo/nucleo32-board.md
- Signed update process: solo/signed-updates.md
- Code documentation: solo/code-overview.md
- Contributing Code: solo/contributing.md

Wyświetl plik

@ -1,64 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2019 SoloKeys Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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] [nfc] <[u2f]|[fido2]|[rk]|[hid]|[ping]>")
print(" sim - test via UDP simulation backend only")
print(" nfc - test via NFC interface only")
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)
nfcOnly = False
if "nfc" in sys.argv:
nfcOnly = True
t.find_device(nfcOnly)
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()

Wyświetl plik

@ -1,11 +0,0 @@
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

Wyświetl plik

@ -1,252 +0,0 @@
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

Wyświetl plik

@ -1,83 +0,0 @@
from solo.client import SoloClient
from solo.commands import SoloExtension
from fido2.ctap1 import ApduError
from fido2.utils import sha256
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
req = SoloClient.format_request(SoloExtension.version, 0, b"A" * 16)
a = sc.ctap2.get_assertion(
sc.host, b"B" * 32, [{"id": req, "type": "public-key"}]
)
with Test("Test custom command returned valid assertion"):
assert a.auth_data.rp_id_hash == sha256(sc.host.encode("utf8"))
assert a.credential["id"] == req
assert (a.auth_data.flags & 0x5) == 0x5
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

Wyświetl plik

@ -1,230 +0,0 @@
import time, struct
from fido2.hid import CtapHidDevice
from fido2.client import Fido2Client
from fido2.attestation import Attestation
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, catch=None):
self.msg = msg
self.catch = catch
def __enter__(self,):
print(self.msg)
def __exit__(self, a, b, c):
if self.catch is None:
print("Pass")
elif isinstance(b, self.catch):
print("Pass")
return b
else:
raise RuntimeError(f"Expected exception {self.catch} did not occur.")
class Tester:
def __init__(self, tester=None):
self.origin = "https://examplo.org"
self.host = "examplo.org"
self.user_count = 10
self.is_sim = False
self.nfc_interface_only = 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
self.nfc_interface_only = tester.nfc_interface_only
def find_device(self, nfcInterfaceOnly=False):
dev = None
self.nfc_interface_only = nfcInterfaceOnly
if not nfcInterfaceOnly:
print("--- HID ---")
print(list(CtapHidDevice.list_devices()))
dev = next(CtapHidDevice.list_devices(), None)
if not dev:
from fido2.pcsc import CtapPcscDevice
print("--- NFC ---")
print(list(CtapPcscDevice.list_devices()))
dev = next(CtapPcscDevice.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(self.nfc_interface_only)
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.nfc_interface_only)
self.ctap.reset()
def testMC(self, test, *args, **kwargs):
attestation_object = self.testFunc(
self.ctap.make_credential, test, *args, **kwargs
)
if attestation_object:
verifier = Attestation.for_type(attestation_object.fmt)
client_data = args[0]
verifier().verify(
attestation_object.att_statement,
attestation_object.auth_data,
client_data,
)
return attestation_object
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)

Wyświetl plik

@ -1,133 +0,0 @@
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"")
assert False
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")
assert False
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)
self.reboot()
for i in range(0, self.user_count):
with Test(
"Post reboot, 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

Wyświetl plik

@ -1,12 +0,0 @@
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