break into separate files
This commit is contained in:
parent
a1a75e4ab5
commit
53fb0059a7
67
tools/testing/ctap_test.py
Normal file
67
tools/testing/ctap_test.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#!/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] <[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()
|
11
tools/testing/tests/__init__.py
Normal file
11
tools/testing/tests/__init__.py
Normal file
@ -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
|
725
tools/ctap_test.py → tools/testing/tests/fido2.py
Executable file → Normal file
725
tools/ctap_test.py → tools/testing/tests/fido2.py
Executable file → Normal file
@ -1,26 +1,12 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
from __future__ import print_function, absolute_import, unicode_literals
|
from __future__ import print_function, absolute_import, unicode_literals
|
||||||
import sys, os, time, math
|
import sys, os, time
|
||||||
from random import randint
|
from random import randint
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
import array, struct, socket
|
import array, struct, socket
|
||||||
|
|
||||||
from fido2.hid import CtapHidDevice, CTAPHID
|
|
||||||
from fido2.client import Fido2Client, ClientError
|
|
||||||
from fido2.ctap import CtapError
|
from fido2.ctap import CtapError
|
||||||
from fido2.ctap1 import CTAP1, ApduError, APDU
|
|
||||||
from fido2.ctap2 import ES256, PinProtocolV1
|
from fido2.ctap2 import ES256, PinProtocolV1
|
||||||
from fido2.utils import Timeout, sha256, hmac_sha256
|
from fido2.utils import Timeout, sha256, hmac_sha256
|
||||||
from fido2.attestation import Attestation
|
from fido2.attestation import Attestation
|
||||||
@ -28,18 +14,8 @@ from fido2.attestation import Attestation
|
|||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
from solo.fido2 import force_udp_backend
|
from .tester import Tester, Test
|
||||||
from solo.client import SoloClient
|
from .util import shannon_entropy
|
||||||
|
|
||||||
|
|
||||||
# Set up a FIDO 2 client using the origin https://example.com
|
|
||||||
|
|
||||||
|
|
||||||
def ForceU2F(client, device):
|
|
||||||
client.ctap = CTAP1(device)
|
|
||||||
client.pin_protocol = None
|
|
||||||
client._do_make_credential = client._ctap1_make_credential
|
|
||||||
client._do_get_assertion = client._ctap1_get_assertion
|
|
||||||
|
|
||||||
|
|
||||||
def VerifyAttestation(attest, data):
|
def VerifyAttestation(attest, data):
|
||||||
@ -47,485 +23,12 @@ def VerifyAttestation(attest, data):
|
|||||||
verifier().verify(attest.att_statement, attest.auth_data, data.hash)
|
verifier().verify(attest.att_statement, attest.auth_data, data.hash)
|
||||||
|
|
||||||
|
|
||||||
def shannon_entropy(data):
|
class FIDO2Tests(Tester):
|
||||||
sum = 0.0
|
def __init__(self, tester=None):
|
||||||
total = len(data)
|
super().__init__(tester)
|
||||||
for x in range(0, 256):
|
|
||||||
freq = data.count(x)
|
|
||||||
p = freq / total
|
|
||||||
if p > 0:
|
|
||||||
sum -= p * math.log2(p)
|
|
||||||
return sum
|
|
||||||
|
|
||||||
|
def run(self,):
|
||||||
class Packet(object):
|
self.test_fido2_other()
|
||||||
def __init__(self, data):
|
|
||||||
l = len(data)
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def ToWireFormat(self,):
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def FromWireFormat(pkt_size, data):
|
|
||||||
return Packet(data)
|
|
||||||
|
|
||||||
|
|
||||||
class Test:
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __enter__(self,):
|
|
||||||
print(self.msg)
|
|
||||||
|
|
||||||
def __exit__(self, a, b, c):
|
|
||||||
print("Pass")
|
|
||||||
|
|
||||||
|
|
||||||
class Tester:
|
|
||||||
def __init__(self,):
|
|
||||||
self.origin = "https://examplo.org"
|
|
||||||
self.host = "examplo.org"
|
|
||||||
self.user_count = 10
|
|
||||||
self.is_sim = False
|
|
||||||
|
|
||||||
def find_device(self,):
|
|
||||||
print(list(CtapHidDevice.list_devices()))
|
|
||||||
dev = next(CtapHidDevice.list_devices(), None)
|
|
||||||
if not dev:
|
|
||||||
raise RuntimeError("No FIDO device found")
|
|
||||||
self.dev = dev
|
|
||||||
self.client = Fido2Client(dev, self.origin)
|
|
||||||
self.ctap = self.client.ctap2
|
|
||||||
self.ctap1 = CTAP1(dev)
|
|
||||||
|
|
||||||
# consume timeout error
|
|
||||||
# cmd,resp = self.recv_raw()
|
|
||||||
|
|
||||||
def set_user_count(self, count):
|
|
||||||
self.user_count = count
|
|
||||||
|
|
||||||
def set_sim(self, b):
|
|
||||||
self.is_sim = b
|
|
||||||
|
|
||||||
def send_data(self, cmd, data):
|
|
||||||
if type(data) != type(b""):
|
|
||||||
data = struct.pack("%dB" % len(data), *[ord(x) for x in data])
|
|
||||||
with Timeout(1.0) as event:
|
|
||||||
return self.dev.call(cmd, data, event)
|
|
||||||
|
|
||||||
def send_raw(self, data, cid=None):
|
|
||||||
if cid is None:
|
|
||||||
cid = self.dev._dev.cid
|
|
||||||
elif type(cid) != type(b""):
|
|
||||||
cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid])
|
|
||||||
if type(data) != type(b""):
|
|
||||||
data = struct.pack("%dB" % len(data), *[ord(x) for x in data])
|
|
||||||
data = cid + data
|
|
||||||
l = len(data)
|
|
||||||
if l != 64:
|
|
||||||
pad = "\x00" * (64 - l)
|
|
||||||
pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad])
|
|
||||||
data = data + pad
|
|
||||||
data = list(data)
|
|
||||||
assert len(data) == 64
|
|
||||||
self.dev._dev.InternalSendPacket(Packet(data))
|
|
||||||
|
|
||||||
def send_magic_reboot(self,):
|
|
||||||
"""
|
|
||||||
For use in simulation and testing. Random bytes that authentictor should detect
|
|
||||||
and then restart itself.
|
|
||||||
"""
|
|
||||||
magic_cmd = (
|
|
||||||
b"\xac\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf"
|
|
||||||
+ b"\xf3\x33\x48\x5f\x13\xf9\xb2\xda\x34\xc5\xa8\xa3"
|
|
||||||
+ b"\x40\x52\x66\x97\xa9\xab\x2e\x0b\x39\x4d\x8d\x04"
|
|
||||||
+ b"\x97\x3c\x13\x40\x05\xbe\x1a\x01\x40\xbf\xf6\x04"
|
|
||||||
+ b"\x5b\xb2\x6e\xb7\x7a\x73\xea\xa4\x78\x13\xf6\xb4"
|
|
||||||
+ b"\x9a\x72\x50\xdc"
|
|
||||||
)
|
|
||||||
self.dev._dev.InternalSendPacket(Packet(magic_cmd))
|
|
||||||
|
|
||||||
def cid(self,):
|
|
||||||
return self.dev._dev.cid
|
|
||||||
|
|
||||||
def set_cid(self, cid):
|
|
||||||
if type(cid) not in [type(b""), type(bytearray())]:
|
|
||||||
cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid])
|
|
||||||
self.dev._dev.cid = cid
|
|
||||||
|
|
||||||
def recv_raw(self,):
|
|
||||||
with Timeout(1.0) as t:
|
|
||||||
cmd, payload = self.dev._dev.InternalRecv()
|
|
||||||
return cmd, payload
|
|
||||||
|
|
||||||
def check_error(self, data, err=None):
|
|
||||||
assert len(data) == 1
|
|
||||||
if err is None:
|
|
||||||
if data[0] != 0:
|
|
||||||
raise CtapError(data[0])
|
|
||||||
elif data[0] != err:
|
|
||||||
raise ValueError("Unexpected error: %02x" % data[0])
|
|
||||||
|
|
||||||
def testFunc(self, func, test, *args, **kwargs):
|
|
||||||
with Test(test):
|
|
||||||
res = None
|
|
||||||
expectedError = kwargs.get("expectedError", None)
|
|
||||||
otherArgs = kwargs.get("other", {})
|
|
||||||
try:
|
|
||||||
res = func(*args, **otherArgs)
|
|
||||||
if expectedError != CtapError.ERR.SUCCESS:
|
|
||||||
raise RuntimeError("Expected error to occur for test: %s" % test)
|
|
||||||
except CtapError as e:
|
|
||||||
if expectedError is not None:
|
|
||||||
if e.code != expectedError:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Got error code 0x%x, expected %x" % (e.code, expectedError)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(e)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def testReset(self,):
|
|
||||||
print("Resetting Authenticator...")
|
|
||||||
self.ctap.reset()
|
|
||||||
|
|
||||||
def testMC(self, test, *args, **kwargs):
|
|
||||||
return self.testFunc(self.ctap.make_credential, test, *args, **kwargs)
|
|
||||||
|
|
||||||
def testGA(self, test, *args, **kwargs):
|
|
||||||
return self.testFunc(self.ctap.get_assertion, test, *args, **kwargs)
|
|
||||||
|
|
||||||
def testCP(self, test, *args, **kwargs):
|
|
||||||
return self.testFunc(self.ctap.client_pin, test, *args, **kwargs)
|
|
||||||
|
|
||||||
def testPP(self, test, *args, **kwargs):
|
|
||||||
return self.testFunc(
|
|
||||||
self.client.pin_protocol.get_pin_token, test, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_long_ping(self):
|
|
||||||
amt = 1000
|
|
||||||
pingdata = os.urandom(amt)
|
|
||||||
with Test("Send %d byte ping" % amt):
|
|
||||||
try:
|
|
||||||
t1 = time.time() * 1000
|
|
||||||
r = self.send_data(CTAPHID.PING, pingdata)
|
|
||||||
t2 = time.time() * 1000
|
|
||||||
delt = t2 - t1
|
|
||||||
# if (delt < 140 ):
|
|
||||||
# raise RuntimeError('Fob is too fast (%d ms)' % delt)
|
|
||||||
if delt > 555 * (amt / 1000):
|
|
||||||
raise RuntimeError("Fob is too slow (%d ms)" % delt)
|
|
||||||
if r != pingdata:
|
|
||||||
raise ValueError("Ping data not echo'd")
|
|
||||||
except CtapError as e:
|
|
||||||
raise RuntimeError("ping failed")
|
|
||||||
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def test_hid(self, check_timeouts=False):
|
|
||||||
if check_timeouts:
|
|
||||||
with Test("idle"):
|
|
||||||
try:
|
|
||||||
cmd, resp = self.recv_raw()
|
|
||||||
except socket.timeout:
|
|
||||||
pass
|
|
||||||
|
|
||||||
with Test("init"):
|
|
||||||
r = self.send_data(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11")
|
|
||||||
|
|
||||||
with Test("100 byte ping"):
|
|
||||||
pingdata = os.urandom(100)
|
|
||||||
try:
|
|
||||||
r = self.send_data(CTAPHID.PING, pingdata)
|
|
||||||
if r != pingdata:
|
|
||||||
raise ValueError("Ping data not echo'd")
|
|
||||||
except CtapError as e:
|
|
||||||
print("100 byte Ping failed:", e)
|
|
||||||
raise RuntimeError("ping failed")
|
|
||||||
|
|
||||||
self.test_long_ping()
|
|
||||||
|
|
||||||
with Test("Wink"):
|
|
||||||
r = self.send_data(CTAPHID.WINK, "")
|
|
||||||
|
|
||||||
with Test("CBOR msg with no data"):
|
|
||||||
try:
|
|
||||||
r = self.send_data(CTAPHID.CBOR, "")
|
|
||||||
if len(r) > 1 or r[0] == 0:
|
|
||||||
raise RuntimeError("Cbor is supposed to have payload")
|
|
||||||
except CtapError as e:
|
|
||||||
assert e.code == CtapError.ERR.INVALID_LENGTH
|
|
||||||
|
|
||||||
with Test("No data in U2F msg"):
|
|
||||||
try:
|
|
||||||
r = self.send_data(CTAPHID.MSG, "")
|
|
||||||
print(hexlify(r))
|
|
||||||
if len(r) > 2:
|
|
||||||
raise RuntimeError("MSG is supposed to have payload")
|
|
||||||
except CtapError as e:
|
|
||||||
assert e.code == CtapError.ERR.INVALID_LENGTH
|
|
||||||
|
|
||||||
with Test("Use init command to resync"):
|
|
||||||
r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
|
|
||||||
with Test("Invalid HID command"):
|
|
||||||
try:
|
|
||||||
r = self.send_data(0x66, "")
|
|
||||||
raise RuntimeError("Invalid command did not return error")
|
|
||||||
except CtapError as e:
|
|
||||||
assert e.code == CtapError.ERR.INVALID_COMMAND
|
|
||||||
|
|
||||||
with Test("Sending packet with too large of a length."):
|
|
||||||
self.send_raw("\x81\x1d\xba\x00")
|
|
||||||
cmd, resp = self.recv_raw()
|
|
||||||
self.check_error(resp, CtapError.ERR.INVALID_LENGTH)
|
|
||||||
|
|
||||||
r = self.send_data(CTAPHID.PING, "\x44" * 200)
|
|
||||||
with Test("Sending packets that skip a sequence number."):
|
|
||||||
self.send_raw("\x81\x04\x90")
|
|
||||||
self.send_raw("\x00")
|
|
||||||
self.send_raw("\x01")
|
|
||||||
# skip 2
|
|
||||||
self.send_raw("\x03")
|
|
||||||
cmd, resp = self.recv_raw()
|
|
||||||
self.check_error(resp, CtapError.ERR.INVALID_SEQ)
|
|
||||||
|
|
||||||
with Test("Resync and send ping"):
|
|
||||||
try:
|
|
||||||
r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
pingdata = os.urandom(100)
|
|
||||||
r = self.send_data(CTAPHID.PING, pingdata)
|
|
||||||
if r != pingdata:
|
|
||||||
raise ValueError("Ping data not echo'd")
|
|
||||||
except CtapError as e:
|
|
||||||
raise RuntimeError("resync fail: ", e)
|
|
||||||
|
|
||||||
with Test("Send ping and abort it"):
|
|
||||||
self.send_raw("\x81\x04\x00")
|
|
||||||
self.send_raw("\x00")
|
|
||||||
self.send_raw("\x01")
|
|
||||||
try:
|
|
||||||
r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
except CtapError as e:
|
|
||||||
raise RuntimeError("resync fail: ", e)
|
|
||||||
|
|
||||||
with Test("Send ping and abort it with different cid, expect timeout"):
|
|
||||||
oldcid = self.cid()
|
|
||||||
newcid = "\x11\x22\x33\x44"
|
|
||||||
self.send_raw("\x81\x10\x00")
|
|
||||||
self.send_raw("\x00")
|
|
||||||
self.send_raw("\x01")
|
|
||||||
self.set_cid(newcid)
|
|
||||||
self.send_raw(
|
|
||||||
"\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88"
|
|
||||||
) # init from different cid
|
|
||||||
print("wait for init response")
|
|
||||||
cmd, r = self.recv_raw() # init response
|
|
||||||
assert cmd == 0x86
|
|
||||||
self.set_cid(oldcid)
|
|
||||||
if check_timeouts:
|
|
||||||
# print('wait for timeout')
|
|
||||||
cmd, r = self.recv_raw() # timeout response
|
|
||||||
assert cmd == 0xBF
|
|
||||||
|
|
||||||
with Test("Test timeout"):
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
t1 = time.time() * 1000
|
|
||||||
self.send_raw("\x81\x04\x00")
|
|
||||||
self.send_raw("\x00")
|
|
||||||
self.send_raw("\x01")
|
|
||||||
cmd, r = self.recv_raw() # timeout response
|
|
||||||
t2 = time.time() * 1000
|
|
||||||
delt = t2 - t1
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.TIMEOUT
|
|
||||||
assert delt < 1000 and delt > 400
|
|
||||||
|
|
||||||
with Test("Test not cont"):
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
self.send_raw("\x81\x04\x00")
|
|
||||||
self.send_raw("\x00")
|
|
||||||
self.send_raw("\x01")
|
|
||||||
self.send_raw("\x81\x10\x00") # init packet
|
|
||||||
cmd, r = self.recv_raw() # timeout response
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.INVALID_SEQ
|
|
||||||
|
|
||||||
if check_timeouts:
|
|
||||||
with Test("Check random cont ignored"):
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
self.send_raw("\x01\x10\x00")
|
|
||||||
try:
|
|
||||||
cmd, r = self.recv_raw() # timeout response
|
|
||||||
except socket.timeout:
|
|
||||||
pass
|
|
||||||
|
|
||||||
with Test("Check busy"):
|
|
||||||
t1 = time.time() * 1000
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
oldcid = self.cid()
|
|
||||||
newcid = "\x11\x22\x33\x44"
|
|
||||||
self.send_raw("\x81\x04\x00")
|
|
||||||
self.set_cid(newcid)
|
|
||||||
self.send_raw("\x81\x04\x00")
|
|
||||||
cmd, r = self.recv_raw() # busy response
|
|
||||||
t2 = time.time() * 1000
|
|
||||||
assert t2 - t1 < 100
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
|
||||||
|
|
||||||
self.set_cid(oldcid)
|
|
||||||
cmd, r = self.recv_raw() # timeout response
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.TIMEOUT
|
|
||||||
|
|
||||||
with Test("Check busy interleaved"):
|
|
||||||
cid1 = "\x11\x22\x33\x44"
|
|
||||||
cid2 = "\x01\x22\x33\x44"
|
|
||||||
self.set_cid(cid2)
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
self.set_cid(cid1)
|
|
||||||
self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88")
|
|
||||||
self.send_raw("\x81\x00\x63") # echo 99 bytes first channel
|
|
||||||
|
|
||||||
self.set_cid(cid2) # send ping on 2nd channel
|
|
||||||
self.send_raw("\x81\x00\x63")
|
|
||||||
time.sleep(0.1)
|
|
||||||
self.send_raw("\x00")
|
|
||||||
|
|
||||||
cmd, r = self.recv_raw() # busy response
|
|
||||||
|
|
||||||
self.set_cid(cid1) # finish 1st channel ping
|
|
||||||
self.send_raw("\x00")
|
|
||||||
|
|
||||||
self.set_cid(cid2)
|
|
||||||
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.CHANNEL_BUSY
|
|
||||||
|
|
||||||
self.set_cid(cid1)
|
|
||||||
cmd, r = self.recv_raw() # ping response
|
|
||||||
assert cmd == 0x81
|
|
||||||
assert len(r) == 0x63
|
|
||||||
|
|
||||||
if check_timeouts:
|
|
||||||
with Test("Test idle, wait for timeout"):
|
|
||||||
sys.stdout.flush()
|
|
||||||
try:
|
|
||||||
cmd, resp = self.recv_raw()
|
|
||||||
except socket.timeout:
|
|
||||||
pass
|
|
||||||
|
|
||||||
with Test("Test cid 0 is invalid"):
|
|
||||||
self.set_cid("\x00\x00\x00\x00")
|
|
||||||
self.send_raw(
|
|
||||||
"\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00"
|
|
||||||
)
|
|
||||||
cmd, r = self.recv_raw() # timeout
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
|
||||||
|
|
||||||
with Test("Test invalid broadcast cid use"):
|
|
||||||
self.set_cid("\xff\xff\xff\xff")
|
|
||||||
self.send_raw(
|
|
||||||
"\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff"
|
|
||||||
)
|
|
||||||
cmd, r = self.recv_raw() # timeout
|
|
||||||
assert cmd == 0xBF
|
|
||||||
assert r[0] == CtapError.ERR.INVALID_CHANNEL
|
|
||||||
|
|
||||||
def test_u2f(self,):
|
|
||||||
chal = sha256(b"AAA")
|
|
||||||
appid = sha256(b"BBB")
|
|
||||||
lastc = 0
|
|
||||||
|
|
||||||
regs = []
|
|
||||||
|
|
||||||
with Test("Check version"):
|
|
||||||
assert self.ctap1.get_version() == "U2F_V2"
|
|
||||||
|
|
||||||
with Test("Check bad INS"):
|
|
||||||
try:
|
|
||||||
res = self.ctap1.send_apdu(0, 0, 0, 0, b"")
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == 0x6D00
|
|
||||||
|
|
||||||
with Test("Check bad CLA"):
|
|
||||||
try:
|
|
||||||
res = self.ctap1.send_apdu(1, CTAP1.INS.VERSION, 0, 0, b"abc")
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == 0x6E00
|
|
||||||
|
|
||||||
for i in range(0, self.user_count):
|
|
||||||
with Test(
|
|
||||||
"U2F reg + auth %d/%d (count: %02x)" % (i + 1, self.user_count, lastc)
|
|
||||||
):
|
|
||||||
reg = self.ctap1.register(chal, appid)
|
|
||||||
reg.verify(appid, chal)
|
|
||||||
auth = self.ctap1.authenticate(chal, appid, reg.key_handle)
|
|
||||||
auth.verify(appid, chal, reg.public_key)
|
|
||||||
|
|
||||||
regs.append(reg)
|
|
||||||
# check endianness
|
|
||||||
if lastc:
|
|
||||||
assert (auth.counter - lastc) < 10
|
|
||||||
lastc = auth.counter
|
|
||||||
if lastc > 0x80000000:
|
|
||||||
print("WARNING: counter is unusually high: %04x" % lastc)
|
|
||||||
assert 0
|
|
||||||
|
|
||||||
for i in range(0, self.user_count):
|
|
||||||
with Test(
|
|
||||||
"Checking previous registration %d/%d" % (i + 1, self.user_count)
|
|
||||||
):
|
|
||||||
auth = self.ctap1.authenticate(chal, appid, regs[i].key_handle)
|
|
||||||
auth.verify(appid, chal, regs[i].public_key)
|
|
||||||
|
|
||||||
print("Check that all previous credentials are registered...")
|
|
||||||
for i in range(0, self.user_count):
|
|
||||||
with Test("Check that previous credential %d is registered" % i):
|
|
||||||
try:
|
|
||||||
auth = self.ctap1.authenticate(
|
|
||||||
chal, appid, regs[i].key_handle, check_only=True
|
|
||||||
)
|
|
||||||
except ApduError as e:
|
|
||||||
# Indicates that key handle is registered
|
|
||||||
assert e.code == APDU.USE_NOT_SATISFIED
|
|
||||||
|
|
||||||
with Test("Check an incorrect key handle is not registered"):
|
|
||||||
kh = bytearray(regs[0].key_handle)
|
|
||||||
kh[0] = kh[0] ^ (0x40)
|
|
||||||
try:
|
|
||||||
self.ctap1.authenticate(chal, appid, kh, check_only=True)
|
|
||||||
assert 0
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == APDU.WRONG_DATA
|
|
||||||
|
|
||||||
with Test("Try to sign with incorrect key handle"):
|
|
||||||
try:
|
|
||||||
self.ctap1.authenticate(chal, appid, kh)
|
|
||||||
assert 0
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == APDU.WRONG_DATA
|
|
||||||
|
|
||||||
with Test("Try to sign using an incorrect keyhandle length"):
|
|
||||||
try:
|
|
||||||
kh = regs[0].key_handle
|
|
||||||
self.ctap1.authenticate(chal, appid, kh[: len(kh) // 2])
|
|
||||||
assert 0
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == APDU.WRONG_DATA
|
|
||||||
|
|
||||||
with Test("Try to sign using an incorrect appid"):
|
|
||||||
badid = bytearray(appid)
|
|
||||||
badid[0] = badid[0] ^ (0x40)
|
|
||||||
try:
|
|
||||||
auth = self.ctap1.authenticate(chal, badid, regs[0].key_handle)
|
|
||||||
assert 0
|
|
||||||
except ApduError as e:
|
|
||||||
assert e.code == APDU.WRONG_DATA
|
|
||||||
|
|
||||||
def test_fido2_simple(self, pin_token=None):
|
def test_fido2_simple(self, pin_token=None):
|
||||||
creds = []
|
creds = []
|
||||||
@ -955,7 +458,7 @@ class Tester:
|
|||||||
if self.is_sim:
|
if self.is_sim:
|
||||||
print("Sending restart command...")
|
print("Sending restart command...")
|
||||||
self.send_magic_reboot()
|
self.send_magic_reboot()
|
||||||
time.sleep(0.25)
|
self.delay(0.25)
|
||||||
else:
|
else:
|
||||||
print("Please reboot authentictor and hit enter")
|
print("Please reboot authentictor and hit enter")
|
||||||
input()
|
input()
|
||||||
@ -1098,7 +601,6 @@ class Tester:
|
|||||||
rp,
|
rp,
|
||||||
user,
|
user,
|
||||||
None,
|
None,
|
||||||
expectedError=CtapError.ERR.MISSING_PARAMETER,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.testMC(
|
self.testMC(
|
||||||
@ -1847,208 +1349,3 @@ class Tester:
|
|||||||
assert len(assertions) == len(users)
|
assert len(assertions) == len(users)
|
||||||
for x, y in zip(assertions, creds):
|
for x, y in zip(assertions, creds):
|
||||||
x.verify(client_data.hash, y.public_key)
|
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()
|
|
245
tools/testing/tests/hid.py
Normal file
245
tools/testing/tests/hid.py
Normal file
@ -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
|
70
tools/testing/tests/solo.py
Normal file
70
tools/testing/tests/solo.py
Normal file
@ -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
|
181
tools/testing/tests/tester.py
Normal file
181
tools/testing/tests/tester.py
Normal file
@ -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)
|
103
tools/testing/tests/u2f.py
Normal file
103
tools/testing/tests/u2f.py
Normal file
@ -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
|
12
tools/testing/tests/util.py
Normal file
12
tools/testing/tests/util.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user