mirror of https://github.com/Yubico/python-fido2
Remove/fix broken tests.
This commit is contained in:
parent
cfebc3b5e5
commit
f416734853
|
@ -3,12 +3,10 @@ repos:
|
|||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: '^(fido2|test)/_pyu2f/.*'
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 19.10b0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: '^(fido2|test)/_pyu2f/.*'
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.6.2
|
||||
hooks:
|
||||
|
|
|
@ -32,7 +32,7 @@ See the _COPYING_ file for the full license text.
|
|||
|
||||
This project contains source code from pyu2f (https://github.com/google/pyu2f)
|
||||
which is licensed under the Apache License, version 2.0.
|
||||
These files are located in `fido2/_pyu2f/` and `test/_pyu2f/`.
|
||||
These files are located in `fido2/hid/`.
|
||||
See http://www.apache.org/licenses/LICENSE-2.0,
|
||||
or the _COPYING.APLv2_ file for the full license text.
|
||||
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for _pyu2f.hid.freebsd."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
from six.moves import builtins
|
||||
from mock import patch
|
||||
|
||||
if sys.platform.startswith('freebsd'):
|
||||
from fido2._pyu2f import freebsd
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
||||
import unittest2 as unittest # pylint: disable=g-import-not-at-top
|
||||
else:
|
||||
import unittest # pylint: disable=g-import-not-at-top
|
||||
|
||||
|
||||
# These are base64 encoded report descriptors of a Yubico token
|
||||
# and a Logitech keyboard.
|
||||
YUBICO_RD = 'BtDxCQGhAQkgFQAm/wB1CJVAgQIJIRUAJv8AdQiVQJECwA=='
|
||||
KEYBOARD_RD = (
|
||||
'BQEJAqEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVAXUDgQEFAQkwCTEJOBWBJX91CJUDgQbAwA==')
|
||||
|
||||
|
||||
class FakeUhidFreeBSDModule():
|
||||
def enumerate(self):
|
||||
return [{'device': 'uhid0',
|
||||
'path': '/dev/uhid0',
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': 0xc31c,
|
||||
'product_desc': 'Logitech Keyboard'},
|
||||
{'device': 'uhid1',
|
||||
'path': '/dev/uhid1',
|
||||
'vendor_id': 0x1050,
|
||||
'product_id': 0x0407,
|
||||
'product_desc': 'Yubico U2F'}]
|
||||
|
||||
def get_report_data(self, fd, unused_report_id):
|
||||
if fd:
|
||||
return base64.b64decode(YUBICO_RD)
|
||||
else:
|
||||
return base64.b64decode(KEYBOARD_RD)
|
||||
|
||||
|
||||
class FakeOsModule():
|
||||
O_RDONLY = os.O_RDONLY
|
||||
O_RDWR = os.O_RDWR
|
||||
path = os.path
|
||||
|
||||
data_written = None
|
||||
data_to_return = None
|
||||
|
||||
def open(self, path, unused_opts): # pylint: disable=invalid-name
|
||||
if path.find('uhid1') >= 0:
|
||||
return 1 # fd == 1 => magic number to return Yubikey RD below
|
||||
else:
|
||||
return 0
|
||||
|
||||
def write(self, unused_dev, data): # pylint: disable=invalid-name
|
||||
self.data_written = data
|
||||
|
||||
def read(self, unused_dev, unused_length): # pylint: disable=invalid-name
|
||||
return self.data_to_return
|
||||
|
||||
def close(self, unused_dev): # pylint: disable=invalid-name
|
||||
pass
|
||||
|
||||
|
||||
@unittest.skipIf(not sys.platform.startswith('freebsd'),
|
||||
'FreeBSD specific test case')
|
||||
class FreeBSDTest(unittest.TestCase):
|
||||
@patch('fido2._pyu2f.freebsd.os', FakeOsModule())
|
||||
@patch('fido2._pyu2f.freebsd.uhid_freebsd', FakeUhidFreeBSDModule())
|
||||
def testCallEnumerate(self):
|
||||
devs = list(freebsd.FreeBSDHidDevice.Enumerate())
|
||||
devs = sorted(devs, key=lambda k: k['vendor_id'])
|
||||
|
||||
self.assertEqual(len(devs), 2)
|
||||
self.assertEqual(devs[0]['vendor_id'], 0x046d)
|
||||
self.assertEqual(devs[0]['product_id'], 0xc31c)
|
||||
self.assertEqual(devs[1]['vendor_id'], 0x1050)
|
||||
self.assertEqual(devs[1]['product_id'], 0x0407)
|
||||
self.assertEqual(devs[1]['usage_page'], 0xf1d0)
|
||||
self.assertEqual(devs[1]['usage'], 1)
|
||||
|
||||
@patch('fido2._pyu2f.freebsd.uhid_freebsd', FakeUhidFreeBSDModule())
|
||||
def testCallOpen(self):
|
||||
fake_os = FakeOsModule()
|
||||
with patch('fido2._pyu2f.linux.os', fake_os):
|
||||
with patch('fido2._pyu2f.freebsd.os', fake_os):
|
||||
dev = freebsd.FreeBSDHidDevice('/dev/uhid1')
|
||||
self.assertEqual(dev.GetInReportDataLength(), 64)
|
||||
self.assertEqual(dev.GetOutReportDataLength(), 64)
|
||||
|
||||
dev.Write(list(range(0, 64)))
|
||||
# The HidDevice implementation prepends one zero-byte-packet
|
||||
# (64 bytes) representing the report ID + padding
|
||||
self.assertEqual(list(six.iterbytes(fake_os.data_written)),
|
||||
[0]*64 + list(range(0, 64)))
|
||||
|
||||
fake_os.data_to_return = b'x' * 64
|
||||
self.assertEqual(dev.Read(), [120] * 64) # chr(120) = 'x'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,183 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for _pyu2f.hidtransport."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
import mock
|
||||
|
||||
from fido2._pyu2f import hidtransport
|
||||
from . import util
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
def MakeKeyboard(path, usage):
|
||||
d = {}
|
||||
d['vendor_id'] = 1133 # Logitech
|
||||
d['product_id'] = 49948
|
||||
d['path'] = path
|
||||
d['usage'] = usage
|
||||
d['usage_page'] = 1
|
||||
return d
|
||||
|
||||
|
||||
def MakeU2F(path):
|
||||
d = {}
|
||||
d['vendor_id'] = 4176
|
||||
d['product_id'] = 1031
|
||||
d['path'] = path
|
||||
d['usage'] = 1
|
||||
d['usage_page'] = 0xf1d0
|
||||
return d
|
||||
|
||||
|
||||
def RPad(collection, to_size):
|
||||
while len(collection) < to_size:
|
||||
collection.append(0)
|
||||
return collection
|
||||
|
||||
|
||||
class DiscoveryTest(unittest.TestCase):
|
||||
|
||||
def testHidUsageSelector(self):
|
||||
u2f_device = {'usage_page': 0xf1d0, 'usage': 0x01}
|
||||
other_device = {'usage_page': 0x06, 'usage': 0x01}
|
||||
self.assertTrue(hidtransport.HidUsageSelector(u2f_device))
|
||||
self.assertFalse(hidtransport.HidUsageSelector(other_device))
|
||||
|
||||
def testDiscoverLocalDevices(self):
|
||||
|
||||
def FakeHidDevice(path):
|
||||
mock_hid_dev = mock.MagicMock()
|
||||
mock_hid_dev.GetInReportDataLength.return_value = 64
|
||||
mock_hid_dev.GetOutReportDataLength.return_value = 64
|
||||
mock_hid_dev.path = path
|
||||
return mock_hid_dev
|
||||
|
||||
with mock.patch.object(hidtransport, 'hid') as hid_mock:
|
||||
# We mock out init so that it doesn't try to do the whole init
|
||||
# handshake with the device, which isn't what we're trying to test
|
||||
# here.
|
||||
with mock.patch.object(hidtransport.UsbHidTransport, 'InternalInit') as _:
|
||||
hid_mock.Enumerate.return_value = [
|
||||
MakeKeyboard('path1', 6), MakeKeyboard('path2', 2),
|
||||
MakeU2F('path3'), MakeU2F('path4')
|
||||
]
|
||||
mock_hid_dev = mock.MagicMock()
|
||||
mock_hid_dev.GetInReportDataLength.return_value = 64
|
||||
mock_hid_dev.GetOutReportDataLength.return_value = 64
|
||||
hid_mock.Open.side_effect = FakeHidDevice
|
||||
|
||||
# Force the iterator into a list
|
||||
devs = list(hidtransport.DiscoverLocalHIDU2FDevices())
|
||||
|
||||
self.assertEqual(hid_mock.Enumerate.call_count, 1)
|
||||
self.assertEqual(hid_mock.Open.call_count, 2)
|
||||
self.assertEqual(len(devs), 2)
|
||||
|
||||
self.assertEqual(devs[0].hid_device.path, 'path3')
|
||||
self.assertEqual(devs[1].hid_device.path, 'path4')
|
||||
|
||||
|
||||
class TransportTest(unittest.TestCase):
|
||||
|
||||
def testInit(self):
|
||||
fake_hid_dev = util.FakeHidDevice(bytearray([0x00, 0x00, 0x00, 0x01]))
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
self.assertEqual(t.cid, bytearray([0x00, 0x00, 0x00, 0x01]))
|
||||
self.assertEqual(t.u2fhid_version, 0x01)
|
||||
|
||||
def testPing(self):
|
||||
fake_hid_dev = util.FakeHidDevice(bytearray([0x00, 0x00, 0x00, 0x01]))
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
|
||||
reply = t.SendPing(b'1234')
|
||||
self.assertEqual(reply, b'1234')
|
||||
|
||||
def testMsg(self):
|
||||
fake_hid_dev = util.FakeHidDevice(
|
||||
bytearray([0x00, 0x00, 0x00, 0x01]), bytearray([0x01, 0x90, 0x00]))
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
|
||||
reply = t.SendMsgBytes([0x00, 0x01, 0x00, 0x00])
|
||||
self.assertEqual(reply, bytearray([0x01, 0x90, 0x00]))
|
||||
|
||||
def testMsgBusy(self):
|
||||
fake_hid_dev = util.FakeHidDevice(
|
||||
bytearray([0x00, 0x00, 0x00, 0x01]), bytearray([0x01, 0x90, 0x00]))
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
|
||||
# Each call will retry twice: the first attempt will fail after 2 retreis,
|
||||
# the second will succeed on the second retry.
|
||||
fake_hid_dev.SetChannelBusyCount(3)
|
||||
with mock.patch.object(hidtransport, 'time') as _:
|
||||
six.assertRaisesRegex(self, OSError, '^Device Busy', t.SendMsgBytes,
|
||||
[0x00, 0x01, 0x00, 0x00])
|
||||
|
||||
reply = t.SendMsgBytes([0x00, 0x01, 0x00, 0x00])
|
||||
self.assertEqual(reply, bytearray([0x01, 0x90, 0x00]))
|
||||
|
||||
def testFragmentedResponseMsg(self):
|
||||
body = bytearray([x % 256 for x in range(0, 1000)])
|
||||
fake_hid_dev = util.FakeHidDevice(bytearray([0x00, 0x00, 0x00, 0x01]), body)
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
|
||||
reply = t.SendMsgBytes([0x00, 0x01, 0x00, 0x00])
|
||||
# Confirm we properly reassemble the message
|
||||
self.assertEqual(reply, bytearray(x % 256 for x in range(0, 1000)))
|
||||
|
||||
def testFragmentedSendApdu(self):
|
||||
body = bytearray(x % 256 for x in range(0, 1000))
|
||||
fake_hid_dev = util.FakeHidDevice(
|
||||
bytearray([0x00, 0x00, 0x00, 0x01]), [0x90, 0x00])
|
||||
t = hidtransport.UsbHidTransport(fake_hid_dev)
|
||||
|
||||
reply = t.SendMsgBytes(body)
|
||||
self.assertEqual(reply, bytearray([0x90, 0x00]))
|
||||
# 1 init packet from creating transport, 18 packets to send
|
||||
# the fragmented message
|
||||
self.assertEqual(len(fake_hid_dev.received_packets), 18)
|
||||
|
||||
def testInitPacketShape(self):
|
||||
packet = hidtransport.UsbHidTransport.InitPacket(
|
||||
64, bytearray(b'\x00\x00\x00\x01'), 0x83, 2, bytearray(b'\x01\x02'))
|
||||
|
||||
self.assertEqual(packet.ToWireFormat(), RPad(
|
||||
[0, 0, 0, 1, 0x83, 0, 2, 1, 2], 64))
|
||||
|
||||
copy = hidtransport.UsbHidTransport.InitPacket.FromWireFormat(
|
||||
64, packet.ToWireFormat())
|
||||
self.assertEqual(copy.cid, bytearray(b'\x00\x00\x00\x01'))
|
||||
self.assertEqual(copy.cmd, 0x83)
|
||||
self.assertEqual(copy.size, 2)
|
||||
self.assertEqual(copy.payload, bytearray(b'\x01\x02'))
|
||||
|
||||
def testContPacketShape(self):
|
||||
packet = hidtransport.UsbHidTransport.ContPacket(
|
||||
64, bytearray(b'\x00\x00\x00\x01'), 5, bytearray(b'\x01\x02'))
|
||||
|
||||
self.assertEqual(packet.ToWireFormat(), RPad([0, 0, 0, 1, 5, 1, 2], 64))
|
||||
|
||||
copy = hidtransport.UsbHidTransport.ContPacket.FromWireFormat(
|
||||
64, packet.ToWireFormat())
|
||||
self.assertEqual(copy.cid, bytearray(b'\x00\x00\x00\x01'))
|
||||
self.assertEqual(copy.seq, 5)
|
||||
self.assertEqual(copy.payload, RPad(bytearray(b'\x01\x02'), 59))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,130 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for _pyu2f.hid.linux."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
import mock
|
||||
|
||||
import six
|
||||
from six.moves import builtins
|
||||
from fido2._pyu2f import linux
|
||||
|
||||
try:
|
||||
from pyfakefs import fake_filesystem # pylint: disable=g-import-not-at-top
|
||||
except ImportError:
|
||||
try:
|
||||
from fakefs import fake_filesystem # pylint: disable=g-import-not-at-top
|
||||
except ImportError:
|
||||
if sys.platform.startswith('linux'):
|
||||
raise
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
||||
import unittest2 as unittest # pylint: disable=g-import-not-at-top
|
||||
else:
|
||||
import unittest # pylint: disable=g-import-not-at-top
|
||||
|
||||
|
||||
# These are base64 encoded report descriptors of a Yubico token
|
||||
# and a Logitech keyboard.
|
||||
YUBICO_RD = 'BtDxCQGhAQkgFQAm/wB1CJVAgQIJIRUAJv8AdQiVQJECwA=='
|
||||
KEYBOARD_RD = (
|
||||
'BQEJAqEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVAXUDgQEFAQkwCTEJOBWBJX91CJUDgQbAwA==')
|
||||
|
||||
|
||||
def AddDevice(fs, dev_name, product_name,
|
||||
vendor_id, product_id, report_descriptor_b64):
|
||||
uevent = fs.create_file('/sys/class/hidraw/%s/device/uevent' % dev_name)
|
||||
rd = fs.create_file('/sys/class/hidraw/%s/device/report_descriptor' % dev_name)
|
||||
report_descriptor = base64.b64decode(report_descriptor_b64)
|
||||
rd.set_contents(report_descriptor)
|
||||
|
||||
buf = 'HID_NAME=%s\n' % product_name.encode('utf8')
|
||||
buf += 'HID_ID=0001:%08X:%08X\n' % (vendor_id, product_id)
|
||||
uevent.set_contents(buf)
|
||||
|
||||
|
||||
class FakeDeviceOsModule(object):
|
||||
O_RDWR = os.O_RDWR
|
||||
path = os.path
|
||||
|
||||
data_written = None
|
||||
data_to_return = None
|
||||
|
||||
def open(self, unused_path, unused_opts): # pylint: disable=invalid-name
|
||||
return 0
|
||||
|
||||
def write(self, unused_dev, data): # pylint: disable=invalid-name
|
||||
self.data_written = data
|
||||
|
||||
def read(self, unused_dev, unused_length): # pylint: disable=invalid-name
|
||||
return self.data_to_return
|
||||
|
||||
|
||||
@unittest.skipIf(not sys.platform.startswith('linux'),
|
||||
'Linux specific test case')
|
||||
class LinuxTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fs = fake_filesystem.FakeFilesystem()
|
||||
self.fs.create_dir('/sys/class/hidraw')
|
||||
|
||||
def tearDown(self):
|
||||
self.fs.remove_object('/sys/class/hidraw')
|
||||
|
||||
def testCallEnumerate(self):
|
||||
AddDevice(self.fs, 'hidraw1', 'Logitech USB Keyboard',
|
||||
0x046d, 0xc31c, KEYBOARD_RD)
|
||||
AddDevice(self.fs, 'hidraw2', 'Yubico U2F', 0x1050, 0x0407, YUBICO_RD)
|
||||
with mock.patch.object(linux, 'os', fake_filesystem.FakeOsModule(self.fs)):
|
||||
fake_open = fake_filesystem.FakeFileOpen(self.fs)
|
||||
with mock.patch.object(builtins, 'open', fake_open):
|
||||
devs = list(linux.LinuxHidDevice.Enumerate())
|
||||
devs = sorted(devs, key=lambda k: k['vendor_id'])
|
||||
|
||||
self.assertEqual(len(devs), 2)
|
||||
self.assertEqual(devs[0]['vendor_id'], 0x046d)
|
||||
self.assertEqual(devs[0]['product_id'], 0x0c31c)
|
||||
self.assertEqual(devs[1]['vendor_id'], 0x1050)
|
||||
self.assertEqual(devs[1]['product_id'], 0x0407)
|
||||
self.assertEqual(devs[1]['usage_page'], 0xf1d0)
|
||||
self.assertEqual(devs[1]['usage'], 1)
|
||||
|
||||
def testCallOpen(self):
|
||||
AddDevice(self.fs, 'hidraw1', 'Yubico U2F', 0x1050, 0x0407, YUBICO_RD)
|
||||
fake_open = fake_filesystem.FakeFileOpen(self.fs)
|
||||
# The open() builtin is used to read from the fake sysfs
|
||||
with mock.patch.object(builtins, 'open', fake_open):
|
||||
# The os.open function is used to interact with the low
|
||||
# level device. We don't want to use fakefs for that.
|
||||
fake_dev_os = FakeDeviceOsModule()
|
||||
with mock.patch.object(linux, 'os', fake_dev_os):
|
||||
dev = linux.LinuxHidDevice('/dev/hidraw1')
|
||||
self.assertEqual(dev.GetInReportDataLength(), 64)
|
||||
self.assertEqual(dev.GetOutReportDataLength(), 64)
|
||||
|
||||
dev.Write(list(range(0, 64)))
|
||||
# The HidDevice implementation prepends a zero-byte representing the
|
||||
# report ID
|
||||
self.assertEqual(list(six.iterbytes(fake_dev_os.data_written)),
|
||||
[0] + list(range(0, 64)))
|
||||
|
||||
fake_dev_os.data_to_return = b'x' * 64
|
||||
self.assertEqual(dev.Read(), [120] * 64) # chr(120) = 'x'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,144 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for _pyu2f.hid.macos."""
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
import mock
|
||||
|
||||
from fido2._pyu2f import macos
|
||||
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
||||
import unittest2 as unittest # pylint: disable=g-import-not-at-top
|
||||
else:
|
||||
import unittest # pylint: disable=g-import-not-at-top
|
||||
|
||||
|
||||
def init_mock_iokit(mock_iokit):
|
||||
# Device open should always return 0 (success)
|
||||
mock_iokit.IOHIDDeviceOpen = mock.Mock(return_value=0)
|
||||
mock_iokit.IOHIDDeviceSetReport = mock.Mock(return_value=0)
|
||||
mock_iokit.IOHIDDeviceCreate = mock.Mock(return_value='handle')
|
||||
|
||||
|
||||
def init_mock_cf(mock_cf):
|
||||
mock_cf.CFGetTypeID = mock.Mock(return_value=123)
|
||||
mock_cf.CFNumberGetTypeID = mock.Mock(return_value=123)
|
||||
mock_cf.CFStringGetTypeID = mock.Mock(return_value=123)
|
||||
|
||||
|
||||
def init_mock_get_int_property(mock_get_int_property):
|
||||
mock_get_int_property.return_value = 64
|
||||
|
||||
|
||||
@unittest.skipIf(not sys.platform.startswith('darwin'),
|
||||
'MacOS specific test case')
|
||||
class MacOsTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
macos.MacOsHidDevice.__del__ = lambda x: None
|
||||
|
||||
@mock.patch.object(macos.threading, 'Thread')
|
||||
@mock.patch.multiple(macos, iokit=mock.DEFAULT, cf=mock.DEFAULT,
|
||||
GetDeviceIntProperty=mock.DEFAULT)
|
||||
def testInitHidDevice(self, thread, iokit, cf, GetDeviceIntProperty): # pylint: disable=invalid-name
|
||||
init_mock_iokit(iokit)
|
||||
init_mock_cf(cf)
|
||||
init_mock_get_int_property(GetDeviceIntProperty)
|
||||
|
||||
device = macos.MacOsHidDevice('1')
|
||||
|
||||
self.assertEqual(64, device.GetInReportDataLength())
|
||||
self.assertEqual(64, device.GetOutReportDataLength())
|
||||
|
||||
@mock.patch.object(macos.threading, 'Thread')
|
||||
@mock.patch.multiple(macos, iokit=mock.DEFAULT, cf=mock.DEFAULT,
|
||||
GetDeviceIntProperty=mock.DEFAULT)
|
||||
def testCallWriteSuccess(self, thread, iokit, cf, GetDeviceIntProperty): # pylint: disable=invalid-name
|
||||
init_mock_iokit(iokit)
|
||||
init_mock_cf(cf)
|
||||
init_mock_get_int_property(GetDeviceIntProperty)
|
||||
|
||||
device = macos.MacOsHidDevice('1')
|
||||
|
||||
# Write 64 bytes to device
|
||||
data = bytearray(range(64))
|
||||
|
||||
# Write to device
|
||||
device.Write(data)
|
||||
|
||||
# Validate that write calls into IOKit
|
||||
set_report_call_args = iokit.IOHIDDeviceSetReport.call_args
|
||||
self.assertIsNotNone(set_report_call_args)
|
||||
|
||||
set_report_call_pos_args = iokit.IOHIDDeviceSetReport.call_args[0]
|
||||
self.assertEqual(len(set_report_call_pos_args), 5)
|
||||
self.assertEqual(set_report_call_pos_args[0], 'handle')
|
||||
self.assertEqual(set_report_call_pos_args[1], 1)
|
||||
self.assertEqual(set_report_call_pos_args[2], 0)
|
||||
self.assertEqual(set_report_call_pos_args[4], 64)
|
||||
|
||||
report_buffer = set_report_call_pos_args[3]
|
||||
self.assertEqual(len(report_buffer), 64)
|
||||
self.assertEqual(bytearray(report_buffer), data, 'Data sent to '
|
||||
'IOHIDDeviceSetReport should match data sent to the '
|
||||
'device')
|
||||
|
||||
@mock.patch.object(macos.threading, 'Thread')
|
||||
@mock.patch.multiple(macos, iokit=mock.DEFAULT, cf=mock.DEFAULT,
|
||||
GetDeviceIntProperty=mock.DEFAULT)
|
||||
def testCallWriteFailure(self, thread, iokit, cf, GetDeviceIntProperty): # pylint: disable=invalid-name
|
||||
init_mock_iokit(iokit)
|
||||
init_mock_cf(cf)
|
||||
init_mock_get_int_property(GetDeviceIntProperty)
|
||||
|
||||
# Make set report call return failure (non-zero)
|
||||
iokit.IOHIDDeviceSetReport.return_value = -1
|
||||
|
||||
device = macos.MacOsHidDevice('1')
|
||||
|
||||
# Write 64 bytes to device
|
||||
data = bytearray(range(64))
|
||||
|
||||
# Write should throw an OsHidError exception
|
||||
with self.assertRaises(OSError):
|
||||
device.Write(data)
|
||||
|
||||
@mock.patch.object(macos.threading, 'Thread')
|
||||
@mock.patch.multiple(macos, iokit=mock.DEFAULT, cf=mock.DEFAULT,
|
||||
GetDeviceIntProperty=mock.DEFAULT)
|
||||
def testCallReadSuccess(self, thread, iokit, cf, GetDeviceIntProperty): # pylint: disable=invalid-name
|
||||
init_mock_iokit(iokit)
|
||||
init_mock_cf(cf)
|
||||
init_mock_get_int_property(GetDeviceIntProperty)
|
||||
|
||||
device = macos.MacOsHidDevice('1')
|
||||
|
||||
# Call callback for IN report
|
||||
report = (ctypes.c_uint8 * 64)()
|
||||
report[:] = range(64)[:]
|
||||
q = device.read_queue
|
||||
macos.HidReadCallback(q, None, None, None, 0, report, 64)
|
||||
|
||||
# Device read should return the callback data
|
||||
read_result = device.Read()
|
||||
self.assertEqual(read_result, list(range(64)), 'Read data should match data '
|
||||
'passed into the callback')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,115 +0,0 @@
|
|||
# Copyright 2019 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tests for OpenBSD hid interface"""
|
||||
|
||||
import ctypes
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
import mock
|
||||
|
||||
if sys.platform.startswith("openbsd"):
|
||||
from fido2._pyu2f import openbsd
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
||||
import unittest2 as unittest # pylint: disable=g-import-not-at-top
|
||||
else:
|
||||
import unittest # pylint: disable=g-import-not-at-top
|
||||
|
||||
|
||||
class FakeOsModule:
|
||||
data = None
|
||||
dirs = None
|
||||
path = os.path
|
||||
O_RDONLY = 0
|
||||
O_RDWR = 0
|
||||
|
||||
def open(self, path, opts):
|
||||
return 0
|
||||
|
||||
def write(self, fd, data):
|
||||
self.data = data
|
||||
|
||||
def read(self, fd, len_):
|
||||
return self.data
|
||||
|
||||
def close(self, fd):
|
||||
pass
|
||||
|
||||
def listdir(self, dir_):
|
||||
return self.dirs
|
||||
|
||||
|
||||
class FakeLibUsbHid:
|
||||
hiddata = None
|
||||
rdesc = None
|
||||
hiditem_ret = 0
|
||||
hiditem = None
|
||||
|
||||
def hid_get_report_desc(self, fd):
|
||||
return self.rdesc
|
||||
|
||||
def hid_start_parse(self, rdesc, kindset, id_):
|
||||
return self.hiddata
|
||||
|
||||
def hid_report_size(self, rdesc, endpoint, report_id):
|
||||
return 64
|
||||
|
||||
def hid_get_item(self, hiddata, hiditem):
|
||||
if self.hiditem is not None:
|
||||
for (fld, _) in self.hiditem._fields_:
|
||||
setattr(hiditem, fld, getattr(self.hiditem, fld, 0))
|
||||
return self.hiditem_ret
|
||||
|
||||
def hid_dispose_report_desc(self, rdesc):
|
||||
pass
|
||||
|
||||
|
||||
@unittest.skipIf(not sys.platform.startswith("openbsd"), "OpenBSD specific test cases")
|
||||
class OpenBSDTest(unittest.TestCase):
|
||||
def testInvalidEnumerate(self):
|
||||
fake_os = FakeOsModule()
|
||||
fake_os.dirs = ["uhid20", "sd0a"]
|
||||
|
||||
with mock.patch("fido2._pyu2f.openbsd.os", fake_os), mock.patch(
|
||||
"fido2._pyu2f.openbsd.fcntl.ioctl", side_effect=OSError
|
||||
):
|
||||
devs = list(openbsd.OpenBSDHidDevice.Enumerate())
|
||||
self.assertEqual(len(devs), 0)
|
||||
|
||||
def testEnumerate(self):
|
||||
fake_os = FakeOsModule()
|
||||
|
||||
def usb_get_device(fd, _, dev_info):
|
||||
dev_info.udi_vendorNo = ctypes.c_uint16(0x1050)
|
||||
dev_info.udi_vendor = b"Yubico"
|
||||
dev_info.udi_productNo = ctypes.c_uint16(0x0407)
|
||||
dev_info.udi_product = b"YubiKey OTP+FIDO+CCID"
|
||||
|
||||
fake_os.dirs = ["0"]
|
||||
|
||||
with mock.patch("fido2._pyu2f.openbsd.os", fake_os), mock.patch(
|
||||
"fido2._pyu2f.openbsd.fcntl.ioctl", side_effect=usb_get_device
|
||||
):
|
||||
devs = list(openbsd.OpenBSDHidDevice.Enumerate())
|
||||
self.assertEqual(len(devs), 1)
|
||||
dev = devs[0]
|
||||
self.assertEqual(dev["vendor_id"], 0x1050)
|
||||
self.assertEqual(dev["product_id"], 0x0407)
|
||||
self.assertEqual(dev["vendor_string"], "Yubico")
|
||||
self.assertEqual(dev["product_string"], "YubiKey OTP+FIDO+CCID")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -1,65 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for _pyu2f.tests.lib.util."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
|
||||
from . import util
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
||||
import unittest2 as unittest # pylint: disable=g-import-not-at-top
|
||||
else:
|
||||
import unittest # pylint: disable=g-import-not-at-top
|
||||
|
||||
|
||||
class UtilTest(unittest.TestCase):
|
||||
|
||||
def testSimplePing(self):
|
||||
dev = util.FakeHidDevice(cid_to_allocate=None)
|
||||
dev.Write([0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3])
|
||||
self.assertEqual(
|
||||
dev.Read(), [0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3] + [0
|
||||
for _ in range(54)])
|
||||
|
||||
def testErrorBusy(self):
|
||||
dev = util.FakeHidDevice(cid_to_allocate=None)
|
||||
dev.SetChannelBusyCount(2)
|
||||
dev.Write([0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3])
|
||||
self.assertEqual(
|
||||
dev.Read(), [0, 0, 0, 1, 0xbf, 0, 1, 6] + [0 for _ in range(56)])
|
||||
dev.Write([0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3])
|
||||
self.assertEqual(
|
||||
dev.Read(), [0, 0, 0, 1, 0xbf, 0, 1, 6] + [0 for _ in range(56)])
|
||||
dev.Write([0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3])
|
||||
self.assertEqual(
|
||||
dev.Read(), [0, 0, 0, 1, 0x81, 0, 3, 1, 2, 3] + [0
|
||||
for _ in range(54)])
|
||||
|
||||
def testFragmentedApdu(self):
|
||||
dev = util.FakeHidDevice(cid_to_allocate=None, msg_reply=range(85, 0, -1))
|
||||
dev.Write([0, 0, 0, 1, 0x83, 0, 100] + [x for x in range(57)])
|
||||
dev.Write([0, 0, 0, 1, 0] + [x for x in range(57, 100)])
|
||||
self.assertEqual(
|
||||
dev.Read(), [0, 0, 0, 1, 0x83, 0, 85] + [x for x in range(85, 28, -1)])
|
||||
self.assertEqual(
|
||||
dev.Read(),
|
||||
[0, 0, 0, 1, 0] + [x for x in range(28, 0, -1)] + [0
|
||||
for _ in range(31)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,149 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Testing utilties for _pyu2f.
|
||||
|
||||
Testing utilities such as a fake implementation of the pyhidapi device object
|
||||
that implements parts of the U2FHID frame protocol. This makes it easy to tests
|
||||
of higher level abstractions without having to use mock to mock out low level
|
||||
framing details.
|
||||
"""
|
||||
from fido2._pyu2f import base, hidtransport
|
||||
|
||||
|
||||
class UnsupportedCommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FakeHidDevice(base.HidDevice):
|
||||
"""Implements a fake hiddevice per the pyhidapi interface.
|
||||
|
||||
This class implemetns a fake hiddevice that can be patched into
|
||||
code that uses pyhidapi to communicate with a hiddevice. This device
|
||||
impelents part of U2FHID protocol and can be used to test interactions
|
||||
with a security key. It supports arbitrary MSG replies as well as
|
||||
channel allocation, and ping.
|
||||
"""
|
||||
|
||||
def __init__(self, cid_to_allocate, msg_reply=None):
|
||||
self.cid_to_allocate = cid_to_allocate
|
||||
self.msg_reply = msg_reply
|
||||
self.transaction_active = False
|
||||
self.full_packet_received = False
|
||||
self.init_packet = None
|
||||
self.packet_body = None
|
||||
self.reply = None
|
||||
self.seq = 0
|
||||
self.received_packets = []
|
||||
self.busy_count = 0
|
||||
|
||||
def GetInReportDataLength(self):
|
||||
return 64
|
||||
|
||||
def GetOutReportDataLength(self):
|
||||
return 64
|
||||
|
||||
def Write(self, data):
|
||||
"""Write to the device.
|
||||
|
||||
Writes to the fake hid device. This function is stateful: if a transaction
|
||||
is currently open with the client, it will continue to accumulate data
|
||||
for the client->device messages until the expected size is reached.
|
||||
|
||||
Args:
|
||||
data: A list of integers to accept as data payload. It should be 64 bytes
|
||||
in length: just the report data--NO report ID.
|
||||
"""
|
||||
|
||||
if len(data) < 64:
|
||||
data = bytearray(data) + bytearray(0 for i in range(0, 64 - len(data)))
|
||||
|
||||
if not self.transaction_active:
|
||||
self.transaction_active = True
|
||||
self.init_packet = hidtransport.UsbHidTransport.InitPacket.FromWireFormat(
|
||||
64, data)
|
||||
self.packet_body = self.init_packet.payload
|
||||
self.full_packet_received = False
|
||||
self.received_packets.append(self.init_packet)
|
||||
else:
|
||||
cont_packet = hidtransport.UsbHidTransport.ContPacket.FromWireFormat(
|
||||
64, data)
|
||||
self.packet_body += cont_packet.payload
|
||||
self.received_packets.append(cont_packet)
|
||||
|
||||
if len(self.packet_body) >= self.init_packet.size:
|
||||
self.packet_body = self.packet_body[0:self.init_packet.size]
|
||||
self.full_packet_received = True
|
||||
|
||||
def Read(self):
|
||||
"""Read from the device.
|
||||
|
||||
Reads from the fake hid device. This function only works if a transaction
|
||||
is open and a complete write has taken place. If so, it will return the
|
||||
next reply packet. It should be called repeatedly until all expected
|
||||
data has been received. It always reads one report.
|
||||
|
||||
Returns:
|
||||
A list of ints containing the next packet.
|
||||
|
||||
Raises:
|
||||
UnsupportedCommandError: if the requested amount is not 64.
|
||||
"""
|
||||
if not self.transaction_active or not self.full_packet_received:
|
||||
return None
|
||||
|
||||
ret = None
|
||||
if self.busy_count > 0:
|
||||
ret = hidtransport.UsbHidTransport.InitPacket(
|
||||
64, self.init_packet.cid, hidtransport.UsbHidTransport.U2FHID_ERROR,
|
||||
1, hidtransport.UsbHidTransport.ERR_CHANNEL_BUSY)
|
||||
self.busy_count -= 1
|
||||
elif self.reply: # reply in progress
|
||||
next_frame = self.reply[0:59]
|
||||
self.reply = self.reply[59:]
|
||||
|
||||
ret = hidtransport.UsbHidTransport.ContPacket(64, self.init_packet.cid,
|
||||
self.seq, next_frame)
|
||||
self.seq += 1
|
||||
else:
|
||||
self.InternalGenerateReply()
|
||||
first_frame = self.reply[0:57]
|
||||
|
||||
ret = hidtransport.UsbHidTransport.InitPacket(
|
||||
64, self.init_packet.cid, self.init_packet.cmd, len(self.reply),
|
||||
first_frame)
|
||||
self.reply = self.reply[57:]
|
||||
|
||||
if not self.reply: # done after this packet
|
||||
self.reply = None
|
||||
self.transaction_active = False
|
||||
self.seq = 0
|
||||
|
||||
return ret.ToWireFormat()
|
||||
|
||||
def SetChannelBusyCount(self, busy_count): # pylint: disable=invalid-name
|
||||
"""Mark the channel busy for next busy_count read calls."""
|
||||
self.busy_count = busy_count
|
||||
|
||||
def InternalGenerateReply(self): # pylint: disable=invalid-name
|
||||
if self.init_packet.cmd == hidtransport.UsbHidTransport.U2FHID_INIT:
|
||||
nonce = self.init_packet.payload[0:8]
|
||||
self.reply = nonce + self.cid_to_allocate + bytearray(
|
||||
b'\x01\x00\x00\x00\x00')
|
||||
elif self.init_packet.cmd == hidtransport.UsbHidTransport.U2FHID_PING:
|
||||
self.reply = self.init_packet.payload
|
||||
elif self.init_packet.cmd == hidtransport.UsbHidTransport.U2FHID_MSG:
|
||||
self.reply = self.msg_reply
|
||||
else:
|
||||
raise UnsupportedCommandError()
|
|
@ -0,0 +1,24 @@
|
|||
import unittest
|
||||
from binascii import a2b_hex
|
||||
from fido2.hid.base import parse_report_descriptor
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_parse_report_descriptor_1(self):
|
||||
max_in_size, max_out_size = parse_report_descriptor(
|
||||
a2b_hex(
|
||||
"06d0f10901a1010920150026ff007508954081020921150026ff00750895409102c0"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(max_in_size, 64)
|
||||
self.assertEqual(max_out_size, 64)
|
||||
|
||||
def test_parse_report_descriptor_2(self):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_report_descriptor(
|
||||
a2b_hex(
|
||||
"05010902a1010901a10005091901290515002501950575018102950175038101"
|
||||
"05010930093109381581257f750895038106c0c0"
|
||||
)
|
||||
)
|
|
@ -27,10 +27,8 @@
|
|||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.hid import CtapHidDevice
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
|
||||
class HidTest(unittest.TestCase):
|
||||
|
@ -50,49 +48,3 @@ class HidTest(unittest.TestCase):
|
|||
self.assertEqual(dev.ping(msg1), msg1)
|
||||
self.assertEqual(dev.ping(msg2), msg2)
|
||||
self.assertEqual(dev.ping(msg3), msg3)
|
||||
|
||||
def test_call_error(self):
|
||||
dev = mock.Mock()
|
||||
hid_dev = CtapHidDevice(None, dev)
|
||||
dev.InternalRecv = mock.Mock(return_value=(0xBF, bytearray([7])))
|
||||
try:
|
||||
hid_dev.call(0x01)
|
||||
self.fail("call did not raise exception")
|
||||
except CtapError as e:
|
||||
self.assertEqual(e.code, 7)
|
||||
|
||||
def test_call_keepalive(self):
|
||||
dev = mock.Mock()
|
||||
hid_dev = CtapHidDevice(None, dev)
|
||||
on_keepalive = mock.MagicMock()
|
||||
|
||||
dev.InternalRecv = mock.Mock(
|
||||
side_effect=[
|
||||
(0xBB, bytearray([0])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0x81, bytearray(b"done")),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(hid_dev.call(0x01, on_keepalive=on_keepalive), b"done")
|
||||
on_keepalive.assert_called_once_with(0)
|
||||
|
||||
dev.InternalRecv.side_effect = [
|
||||
(0xBB, bytearray([1])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0xBB, bytearray([1])),
|
||||
(0xBB, bytearray([1])),
|
||||
(0xBB, bytearray([1])),
|
||||
(0xBB, bytearray([1])),
|
||||
(0xBB, bytearray([0])),
|
||||
(0x81, bytearray(b"done")),
|
||||
]
|
||||
on_keepalive.reset_mock()
|
||||
self.assertEqual(hid_dev.call(0x01, on_keepalive=on_keepalive), b"done")
|
||||
self.assertEqual(
|
||||
on_keepalive.call_args_list,
|
||||
[mock.call(1), mock.call(0), mock.call(1), mock.call(0)],
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue