Reformat with Black.

This commit is contained in:
Dain Nilsson 2019-10-01 09:25:29 +02:00
parent 09db42e08e
commit 226b0021f1
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
40 changed files with 2153 additions and 1805 deletions

View File

@ -3,5 +3,8 @@ repos:
rev: v2.0.0
hooks:
- id: flake8
- id: double-quote-string-fixer
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
exclude: '^(fido2|test)/_pyu2f/.*'

View File

@ -1,4 +1,3 @@
from fido2.pcsc import CtapPcscDevice
import time
@ -14,18 +13,24 @@ class Acr122uPcscDevice(object):
"""
try:
result, sw1, sw2 = self.pcsc.apdu_exchange(b'\xff\x00\x48\x00\x00')
result, sw1, sw2 = self.pcsc.apdu_exchange(b"\xff\x00\x48\x00\x00")
if len(result) > 0:
str_result = result + bytes([sw1]) + bytes([sw2])
str_result = str_result.decode('utf-8')
str_result = str_result.decode("utf-8")
return str_result
except Exception as e:
print('Get version error:', e)
print("Get version error:", e)
pass
return 'n/a'
return "n/a"
def led_control(self, red=False, green=False,
blink_count=0, red_end_blink=False, green_end_blink=False):
def led_control(
self,
red=False,
green=False,
blink_count=0,
red_end_blink=False,
green_end_blink=False,
):
"""
Reader's led control
:param red: boolean. red led on
@ -40,33 +45,37 @@ class Acr122uPcscDevice(object):
try:
if blink_count > 0:
cbyte = 0b00001100 + \
(0b01 if red_end_blink else 0b00) + \
(0b10 if green_end_blink else 0b00)
cbyte |= (0b01000000 if red else 0b00000000) + \
(0b10000000 if green else 0b00000000)
cbyte = (
0b00001100
+ (0b01 if red_end_blink else 0b00)
+ (0b10 if green_end_blink else 0b00)
)
cbyte |= (0b01000000 if red else 0b00000000) + (
0b10000000 if green else 0b00000000
)
else:
cbyte = 0b00001100 + \
(0b01 if red else 0b00) + \
(0b10 if green else 0b00)
cbyte = 0b00001100 + (0b01 if red else 0b00) + (0b10 if green else 0b00)
apdu = b'\xff\x00\x40' + \
bytes([cbyte & 0xff]) + \
b'\4' + b'\5\3' + \
bytes([blink_count]) + \
b'\0'
apdu = (
b"\xff\x00\x40"
+ bytes([cbyte & 0xFF])
+ b"\4"
+ b"\5\3"
+ bytes([blink_count])
+ b"\0"
)
self.pcsc.apdu_exchange(apdu)
except Exception as e:
print('LED control error:', e)
print("LED control error:", e)
dev = next(CtapPcscDevice.list_devices())
print('CONNECT: %s' % dev)
print("CONNECT: %s" % dev)
pcsc_device = Acr122uPcscDevice(dev)
pcsc_device.led_control(False, True, 0)
print('version: %s' % pcsc_device.reader_version())
print("version: %s" % pcsc_device.reader_version())
pcsc_device.led_control(True, False, 0)
time.sleep(1)
pcsc_device.led_control(False, True, 3)

View File

@ -45,7 +45,7 @@ from fido2.pcsc import CtapPcscDevice
class Acr122uSamPcscDevice(CtapPcscDevice):
def __init__(self, connection, name):
self.ats = b''
self.ats = b""
self.vparity = False
self.max_block_len = 29
@ -58,14 +58,14 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
# setup reader
if not self.set_auto_iso14443_4_activation():
raise Exception('Set automatic iso-14443-4 activation error')
raise Exception("Set automatic iso-14443-4 activation error")
if not self.set_default_retry_timeout():
raise Exception('Set default retry timeout error')
raise Exception("Set default retry timeout error")
self.ats = self.get_ats()
if self.ats == b'':
raise Exception('No card in field')
if self.ats == b"":
raise Exception("No card in field")
self._select()
@ -78,21 +78,18 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
"""
# print('>> %s' % b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(
list(six.iterbytes(apdu)),
protocol
)
resp, sw1, sw2 = self._conn.transmit(list(six.iterbytes(apdu)), protocol)
response = bytes(bytearray(resp))
# print('<< [0x%04x] %s' % (sw1 * 0x100 + sw2, b2a_hex(response)))
return response, sw1, sw2
def pseudo_apdu_ex(self, apdu, protocol=None):
req = b'\xff\x00\x00\x00' + bytes([len(apdu) & 0xff]) + apdu
req = b"\xff\x00\x00\x00" + bytes([len(apdu) & 0xFF]) + apdu
resp, sw1, sw2 = self.apdu_plain(req, protocol)
if sw1 != 0x61:
return resp, sw1, sw2
return self.apdu_plain(b'\xff\xc0\x00\x00' + bytes([sw2]), protocol)
return self.apdu_plain(b"\xff\xc0\x00\x00" + bytes([sw2]), protocol)
# override base method
# commands in PN 532 User manual (UM0701-02)
@ -101,26 +98,25 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
# chaining ISO 14443-4:2001
# page 20. 7.5.2 Chaining
def apdu_exchange(self, apdu, protocol=None):
all_response = b''
all_response = b""
alen = 0
while True:
vapdu = apdu[alen:alen + self.max_block_len]
vapdu = apdu[alen : alen + self.max_block_len]
# input chaining
chaining = alen + len(vapdu) < len(apdu)
vb = 0x02 | (0x01 if self.vparity else 0x00) | \
(0x10 if chaining else 0x00)
vb = 0x02 | (0x01 if self.vparity else 0x00) | (0x10 if chaining else 0x00)
# 7.3.9 InCommunicateThru
resp, sw1, sw2 = \
self.pseudo_apdu_ex(b'\xd4\x42' + bytes([vb]) + vapdu, protocol)
resp, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x42" + bytes([vb]) + vapdu, protocol
)
self.vparity = not self.vparity
if len(resp) > 2 and resp[2] > 0:
print('Error: 0x%02x' % resp[2])
return b'', 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or \
resp[0] != 0xd5 or resp[1] != 0x43:
return b'', 0x67, 0x00
print("Error: 0x%02x" % resp[2])
return b"", 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or resp[0] != 0xD5 or resp[1] != 0x43:
return b"", 0x67, 0x00
alen += len(vapdu)
@ -131,34 +127,32 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
if resp[3] & 0x10 == 0:
return resp[4:-2], resp[-2], resp[-1]
else:
if resp[3] != 0xf2:
if resp[3] != 0xF2:
all_response = resp[4:]
else:
return b'', 0x90, 0x00
return b"", 0x90, 0x00
while True:
if len(resp) > 3 and resp[3] == 0xf2:
if len(resp) > 3 and resp[3] == 0xF2:
# WTX
answer = resp[3:5]
else:
# ACK
answer = bytes([0xa2 | (0x01 if self.vparity else 0x00)])
answer = bytes([0xA2 | (0x01 if self.vparity else 0x00)])
self.vparity = not self.vparity
# 7.3.9 InCommunicateThru
resp, sw1, sw2 = \
self.pseudo_apdu_ex(b'\xd4\x42' + answer, protocol)
resp, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x42" + answer, protocol)
if len(resp) > 2 and resp[2] > 0:
print('Error: 0x%02x' % resp[2])
return b'', 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or \
resp[0] != 0xd5 or resp[1] != 0x43:
return b'', 0x67, 0x00
print("Error: 0x%02x" % resp[2])
return b"", 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or resp[0] != 0xD5 or resp[1] != 0x43:
return b"", 0x67, 0x00
response_chaining = len(resp) > 3 and resp[3] & 0x10 != 0
# if I block
if len(resp) > 3 and resp[3] & 0xe0 == 0x00:
if len(resp) > 3 and resp[3] & 0xE0 == 0x00:
all_response += resp[4:]
if not response_chaining:
@ -168,52 +162,52 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
def get_ats(self, verbose=False):
self.field_reset()
self.ats = b''
resp, sw1, sw2 = self.pseudo_apdu_ex(b'\xd4\x4a\x01\x00')
self.ats = b""
resp, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x4a\x01\x00")
if sw1 == 0x90 and len(resp) > 8 and resp[2] > 0x00:
if verbose:
print('ATQA 0x%02x%02x' % (resp[4], resp[5]))
print('SAK 0x%02x' % resp[6])
print("ATQA 0x%02x%02x" % (resp[4], resp[5]))
print("SAK 0x%02x" % resp[6])
uid_len = resp[7]
if verbose:
print('UID [%d] %s' % (uid_len, resp[8:8 + uid_len].hex()))
self.ats = resp[8 + uid_len:]
print("UID [%d] %s" % (uid_len, resp[8 : 8 + uid_len].hex()))
self.ats = resp[8 + uid_len :]
if verbose:
print('ATS [%d] %s' % (len(self.ats), self.ats.hex()))
print("ATS [%d] %s" % (len(self.ats), self.ats.hex()))
self.vparity = False
return self.ats
return b''
return b""
def set_default_retry_timeout(self):
result, sw1, sw2 = self.pseudo_apdu_ex(b'\xd4\x32\x05\x00\x00\x00')
if sw1 != 0x90 or sw2 != 0x00 or result != b'\xd5\x33':
print('set default retry time error')
result, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x32\x05\x00\x00\x00")
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set default retry time error")
return False
# 14443 timeout. UM0701-02 PN432 user manual. page 101.
# RFU, fATR_RES_Timeout, fRetryTimeout
# 0b 102ms, 0c - 204ms, 0d - 409ms, 0f - 1.6s
result, sw1, sw2 = self.pseudo_apdu_ex(b'\xd4\x32\x02\x00\x0c\x0f')
if sw1 != 0x90 or sw2 != 0x00 or result != b'\xd5\x33':
print('set fRetryTimeout error')
result, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x32\x02\x00\x0c\x0f")
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set fRetryTimeout error")
return False
return True
def set_auto_iso14443_4_activation(self, activate=True):
result, sw1, sw2 = \
self.pseudo_apdu_ex(
b'\xd4\x12' + bytes([0x34 if activate else 0x24]))
if sw1 != 0x90 or sw2 != 0x00 or result != b'\xd5\x13':
print('set automatic iso-14443-4 activation error')
result, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x12" + bytes([0x34 if activate else 0x24])
)
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x13":
print("set automatic iso-14443-4 activation error")
return False
return True
def field_control(self, field_on=True):
result, sw1, sw2 = \
self.pseudo_apdu_ex(
b'\xd4\x32\x01' + bytes([0x01 if field_on else 0x00]))
if sw1 != 0x90 or sw2 != 0x00 or result != b'\xd5\x33':
print('set field state error')
result, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x32\x01" + bytes([0x01 if field_on else 0x00])
)
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set field state error")
return False
return True
@ -232,18 +226,24 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
"""
try:
result, sw1, sw2 = self.apdu_plain(b'\xff\x00\x48\x00\x00')
result, sw1, sw2 = self.apdu_plain(b"\xff\x00\x48\x00\x00")
if len(result) > 0:
str_result = result + bytes([sw1]) + bytes([sw2])
str_result = str_result.decode('utf-8')
str_result = str_result.decode("utf-8")
return str_result
except Exception as e:
print('Get version error:', e)
print("Get version error:", e)
pass
return 'n/a'
return "n/a"
def led_control(self, red=False, green=False,
blink_count=0, red_end_blink=False, green_end_blink=False):
def led_control(
self,
red=False,
green=False,
blink_count=0,
red_end_blink=False,
green_end_blink=False,
):
"""
Reader's led control
:param red: boolean. red led on
@ -258,33 +258,37 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
try:
if blink_count > 0:
cbyte = 0b00001100 + \
(0b01 if red_end_blink else 0b00) + \
(0b10 if green_end_blink else 0b00)
cbyte |= (0b01000000 if red else 0b00000000) + \
(0b10000000 if green else 0b00000000)
cbyte = (
0b00001100
+ (0b01 if red_end_blink else 0b00)
+ (0b10 if green_end_blink else 0b00)
)
cbyte |= (0b01000000 if red else 0b00000000) + (
0b10000000 if green else 0b00000000
)
else:
cbyte = 0b00001100 + \
(0b01 if red else 0b00) + \
(0b10 if green else 0b00)
cbyte = 0b00001100 + (0b01 if red else 0b00) + (0b10 if green else 0b00)
apdu = b'\xff\x00\x40' + \
bytes([cbyte & 0xff]) + \
b'\4' + b'\5\3' + \
bytes([blink_count]) + \
b'\0'
apdu = (
b"\xff\x00\x40"
+ bytes([cbyte & 0xFF])
+ b"\4"
+ b"\5\3"
+ bytes([blink_count])
+ b"\0"
)
self.apdu_plain(apdu)
except Exception as e:
print('LED control error:', e)
print("LED control error:", e)
dev = next(Acr122uSamPcscDevice.list_devices())
print('CONNECT: %s' % dev)
print('version: %s' % dev.reader_version())
print('atr: %s' % bytes(dev.get_atr()).hex())
print('ats: %s' % dev.ats.hex())
print("CONNECT: %s" % dev)
print("version: %s" % dev.reader_version())
print("atr: %s" % bytes(dev.get_atr()).hex())
print("ats: %s" % dev.ats.hex())
# uncomment if you want to see parameters from card's selection
# dev.get_ats(True)
@ -292,19 +296,19 @@ print('ats: %s' % dev.ats.hex())
dev.led_control(False, True, 0)
chal = sha256(b'AAA')
appid = sha256(b'BBB')
chal = sha256(b"AAA")
appid = sha256(b"BBB")
ctap1 = CTAP1(dev)
print('ctap1 version:', ctap1.get_version())
print("ctap1 version:", ctap1.get_version())
reg = ctap1.register(chal, appid)
print('u2f register:', reg)
print("u2f register:", reg)
reg.verify(appid, chal)
print('Register message verify OK')
print("Register message verify OK")
auth = ctap1.authenticate(chal, appid, reg.key_handle)
print('u2f authenticate: ', auth)
print("u2f authenticate: ", auth)
res = auth.verify(appid, chal, reg.public_key)
print('Authenticate message verify OK')
print("Authenticate message verify OK")
dev.led_control()

View File

@ -1,4 +1,3 @@
from fido2.pcsc import CtapPcscDevice
import time
@ -15,154 +14,157 @@ class Acr1252uPcscDevice(object):
def reader_version(self):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x18\x00')
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x18\x00")
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == len(res) - 5:
strres = res[5:5+reslen].decode('utf-8')
strres = res[5 : 5 + reslen].decode("utf-8")
return strres
except Exception as e:
print('Get version error:', e)
return 'n/a'
print("Get version error:", e)
return "n/a"
def reader_serial_number(self):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x33\x00')
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x33\x00")
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == len(res) - 5:
strres = res[5:5+reslen].decode('utf-8')
strres = res[5 : 5 + reslen].decode("utf-8")
return strres
except Exception as e:
print('Get serial number error:', e)
return 'n/a'
print("Get serial number error:", e)
return "n/a"
def led_control(self, red=False, green=False):
try:
cbyte = (0b01 if red else 0b00) + (0b10 if green else 0b00)
result = self.pcsc.control_exchange(C_CODE,
b'\xe0\x00\x00\x29\x01' +
bytes([cbyte]))
result = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x29\x01" + bytes([cbyte])
)
if len(result) > 0 and result.find(b'\xe1\x00\x00\x00') == 0:
if len(result) > 0 and result.find(b"\xe1\x00\x00\x00") == 0:
result_length = result[4]
if result_length == 1:
ex_red = bool(result[5] & 0b01)
ex_green = bool(result[5] & 0b10)
return True, ex_red, ex_green
except Exception as e:
print('LED control error:', e)
print("LED control error:", e)
return False, False, False
def led_status(self):
try:
result = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x29\x00')
result = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x29\x00")
if len(result) > 0 and result.find(b'\xe1\x00\x00\x00') == 0:
if len(result) > 0 and result.find(b"\xe1\x00\x00\x00") == 0:
result_length = result[4]
if result_length == 1:
ex_red = bool(result[5] & 0b01)
ex_green = bool(result[5] & 0b10)
return True, ex_red, ex_green
except Exception as e:
print('LED status error:', e)
print("LED status error:", e)
return False, False, False
def get_polling_settings(self):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x23\x00')
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x23\x00")
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print('Get polling settings error:', e)
print("Get polling settings error:", e)
return False, 0
def set_polling_settings(self, settings):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x23\x01' +
bytes([settings & 0xff]))
res = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x23\x01" + bytes([settings & 0xFF])
)
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print('Set polling settings error:', e)
print("Set polling settings error:", e)
return False, 0
def get_picc_operation_parameter(self):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x20\x00')
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x20\x00")
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print('Get PICC Operating Parameter error:', e)
print("Get PICC Operating Parameter error:", e)
return False, 0
def set_picc_operation_parameter(self, param):
try:
res = self.pcsc.control_exchange(C_CODE, b'\xe0\x00\x00\x20\x01' +
bytes([param]))
res = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x20\x01" + bytes([param])
)
if len(res) > 0 and res.find(b'\xe1\x00\x00\x00') == 0:
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print('Set PICC Operating Parameter error:', e)
print("Set PICC Operating Parameter error:", e)
return False, 0
dev = next(CtapPcscDevice.list_devices())
print('CONNECT: %s' % dev)
print("CONNECT: %s" % dev)
pcsc_device = Acr1252uPcscDevice(dev)
if pcsc_device is not None:
print('version: %s' % pcsc_device.reader_version())
print('serial number: %s' % pcsc_device.reader_serial_number())
print('')
print("version: %s" % pcsc_device.reader_version())
print("serial number: %s" % pcsc_device.reader_serial_number())
print("")
result, settings = pcsc_device.set_polling_settings(0x8B)
print('write polling settings: %r 0x%x' % (result, settings))
print("write polling settings: %r 0x%x" % (result, settings))
result, settings = pcsc_device.get_polling_settings()
print('polling settings: %r 0x%x' % (result, settings))
set_desc = [[0, 'Auto PICC Polling'],
[1, 'Turn off Antenna Field if no PICC is found'],
[2, 'Turn off Antenna Field if the PICC is inactive'],
[3, 'Activate the PICC when detected'],
[7, 'Enforce ISO 14443-A Part 4']]
print("polling settings: %r 0x%x" % (result, settings))
set_desc = [
[0, "Auto PICC Polling"],
[1, "Turn off Antenna Field if no PICC is found"],
[2, "Turn off Antenna Field if the PICC is inactive"],
[3, "Activate the PICC when detected"],
[7, "Enforce ISO 14443-A Part 4"],
]
for x in set_desc:
print(x[1], 'on' if settings & (1 << x[0]) else 'off')
print(x[1], "on" if settings & (1 << x[0]) else "off")
interval_desc = [250, 500, 1000, 2500]
print('PICC Poll Interval for PICC',
interval_desc[(settings >> 4) & 0b11],
'ms')
print('')
print("PICC Poll Interval for PICC", interval_desc[(settings >> 4) & 0b11], "ms")
print("")
print('PICC operation parameter: %r 0x%x' %
pcsc_device.get_picc_operation_parameter())
print('')
print(
"PICC operation parameter: %r 0x%x" % pcsc_device.get_picc_operation_parameter()
)
print("")
result, red, green = pcsc_device.led_control(True, False)
print('led control result:', result, 'red:', red, 'green:', green)
print("led control result:", result, "red:", red, "green:", green)
result, red, green = pcsc_device.led_status()
print('led state result:', result, 'red:', red, 'green:', green)
print("led state result:", result, "red:", red, "green:", green)
time.sleep(1)
pcsc_device.led_control(False, False)

View File

@ -43,85 +43,76 @@ use_nfc = False
# Locate a device
dev = next(CtapHidDevice.list_devices(), None)
if dev is not None:
print('Use USB HID channel.')
print("Use USB HID channel.")
else:
try:
from fido2.pcsc import CtapPcscDevice
dev = next(CtapPcscDevice.list_devices(), None)
print('Use NFC channel.')
print("Use NFC channel.")
use_nfc = True
except Exception as e:
print('NFC channel search error:', e)
print("NFC channel search error:", e)
if not dev:
print('No FIDO device found')
print("No FIDO device found")
sys.exit(1)
# Set up a FIDO 2 client using the origin https://example.com
client = Fido2Client(dev, 'https://example.com')
client = Fido2Client(dev, "https://example.com")
# Prepare parameters for makeCredential
rp = {'id': 'example.com', 'name': 'Example RP'}
user = {'id': b'user_id', 'name': 'A. User'}
challenge = 'Y2hhbGxlbmdl'
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
challenge = "Y2hhbGxlbmdl"
# Prompt for PIN if needed
pin = None
if client.info.options.get('clientPin'):
pin = getpass('Please enter PIN:')
if client.info.options.get("clientPin"):
pin = getpass("Please enter PIN:")
else:
print('no pin')
print("no pin")
# Create a credential
if not use_nfc:
print('\nTouch your authenticator device now...\n')
attestation_object, client_data = client.make_credential(
rp, user, challenge, pin=pin
)
print("\nTouch your authenticator device now...\n")
attestation_object, client_data = client.make_credential(rp, user, challenge, pin=pin)
print('New credential created!')
print("New credential created!")
print('CLIENT DATA:', client_data)
print('ATTESTATION OBJECT:', attestation_object)
print("CLIENT DATA:", client_data)
print("ATTESTATION OBJECT:", attestation_object)
print()
print('CREDENTIAL DATA:', attestation_object.auth_data.credential_data)
print("CREDENTIAL DATA:", attestation_object.auth_data.credential_data)
# Verify signature
verifier = Attestation.for_type(attestation_object.fmt)
verifier().verify(
attestation_object.att_statement,
attestation_object.auth_data,
client_data.hash
attestation_object.att_statement, attestation_object.auth_data, client_data.hash
)
print('Attestation signature verified!')
print("Attestation signature verified!")
credential = attestation_object.auth_data.credential_data
# Prepare parameters for getAssertion
challenge = 'Q0hBTExFTkdF' # Use a new challenge for each call.
allow_list = [{
'type': 'public-key',
'id': credential.credential_id
}]
challenge = "Q0hBTExFTkdF" # Use a new challenge for each call.
allow_list = [{"type": "public-key", "id": credential.credential_id}]
# Authenticate the credential
if not use_nfc:
print('\nTouch your authenticator device now...\n')
print("\nTouch your authenticator device now...\n")
assertions, client_data = client.get_assertion(
rp['id'], challenge, allow_list, pin=pin
)
assertions, client_data = client.get_assertion(rp["id"], challenge, allow_list, pin=pin)
print('Credential authenticated!')
print("Credential authenticated!")
assertion = assertions[0] # Only one cred in allowList, only one response.
print('CLIENT DATA:', client_data)
print("CLIENT DATA:", client_data)
print()
print('ASSERTION DATA:', assertion)
print("ASSERTION DATA:", assertion)
# Verify signature
assertion.verify(client_data.hash, credential.public_key)
print('Assertion signature verified!')
print("Assertion signature verified!")

View File

@ -34,6 +34,7 @@ from __future__ import print_function, absolute_import, unicode_literals
from fido2.hid import CtapHidDevice, CAPABILITY
from fido2.ctap2 import CTAP2
try:
from fido2.pcsc import CtapPcscDevice
except ImportError:
@ -49,20 +50,20 @@ def enumerate_devices():
for dev in enumerate_devices():
print('CONNECT: %s' % dev)
print('CTAPHID protocol version: %d' % dev.version)
print("CONNECT: %s" % dev)
print("CTAPHID protocol version: %d" % dev.version)
if dev.capabilities & CAPABILITY.CBOR:
ctap2 = CTAP2(dev)
info = ctap2.get_info()
print('DEVICE INFO: %s' % info)
print("DEVICE INFO: %s" % info)
else:
print('Device does not support CBOR')
print("Device does not support CBOR")
if dev.capabilities & CAPABILITY.WINK:
dev.wink()
print('WINK sent!')
print("WINK sent!")
else:
print('Device does not support WINK')
print("Device does not support WINK")
dev.close()

View File

@ -56,32 +56,32 @@ def enumerate_devices():
# Locate a device
for dev in enumerate_devices():
client = Fido2Client(dev, 'https://example.com')
client = Fido2Client(dev, "https://example.com")
if HmacSecretExtension.NAME in client.info.extensions:
break
else:
print('No Authenticator with the HmacSecret extension found!')
print("No Authenticator with the HmacSecret extension found!")
sys.exit(1)
use_nfc = CtapPcscDevice and isinstance(dev, CtapPcscDevice)
# Prepare parameters for makeCredential
rp = {'id': 'example.com', 'name': 'Example RP'}
user = {'id': b'user_id', 'name': 'A. User'}
challenge = 'Y2hhbGxlbmdl'
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
challenge = "Y2hhbGxlbmdl"
# Prompt for PIN if needed
pin = None
if client.info.options.get('clientPin'):
pin = getpass('Please enter PIN:')
if client.info.options.get("clientPin"):
pin = getpass("Please enter PIN:")
else:
print('no pin')
print("no pin")
hmac_ext = HmacSecretExtension(client.ctap2)
# Create a credential
if not use_nfc:
print('\nTouch your authenticator device now...\n')
print("\nTouch your authenticator device now...\n")
attestation_object, client_data = client.make_credential(
rp, user, challenge, extensions=hmac_ext.create_dict(), pin=pin
)
@ -90,47 +90,43 @@ attestation_object, client_data = client.make_credential(
hmac_result = hmac_ext.results_for(attestation_object.auth_data)
credential = attestation_object.auth_data.credential_data
print('New credential created, with the HmacSecret extension.')
print("New credential created, with the HmacSecret extension.")
# Prepare parameters for getAssertion
challenge = 'Q0hBTExFTkdF' # Use a new challenge for each call.
allow_list = [{
'type': 'public-key',
'id': credential.credential_id
}]
challenge = "Q0hBTExFTkdF" # Use a new challenge for each call.
allow_list = [{"type": "public-key", "id": credential.credential_id}]
# Generate a salt for HmacSecret:
salt = os.urandom(32)
print('Authenticate with salt:', b2a_hex(salt))
print("Authenticate with salt:", b2a_hex(salt))
# Authenticate the credential
if not use_nfc:
print('\nTouch your authenticator device now...\n')
print("\nTouch your authenticator device now...\n")
assertions, client_data = client.get_assertion(
rp['id'], challenge, allow_list, extensions=hmac_ext.get_dict(salt), pin=pin
rp["id"], challenge, allow_list, extensions=hmac_ext.get_dict(salt), pin=pin
)
assertion = assertions[0] # Only one cred in allowList, only one response.
hmac_res = hmac_ext.results_for(assertion.auth_data)
print('Authenticated, secret:', b2a_hex(hmac_res[0]))
print("Authenticated, secret:", b2a_hex(hmac_res[0]))
# Authenticate again, using two salts to generate two secrets:
# Generate a second salt for HmacSecret:
salt2 = os.urandom(32)
print('Authenticate with second salt:', b2a_hex(salt2))
print("Authenticate with second salt:", b2a_hex(salt2))
if not use_nfc:
print('\nTouch your authenticator device now...\n')
print("\nTouch your authenticator device now...\n")
# The first salt is reused, which should result in the same secret.
assertions, client_data = client.get_assertion(
rp['id'], challenge, allow_list, extensions=hmac_ext.get_dict(salt, salt2),
pin=pin
rp["id"], challenge, allow_list, extensions=hmac_ext.get_dict(salt, salt2), pin=pin
)
assertion = assertions[0] # Only one cred in allowList, only one response.
hmac_res = hmac_ext.results_for(assertion.auth_data)
print('Old secret:', b2a_hex(hmac_res[0]))
print('New secret:', b2a_hex(hmac_res[1]))
print("Old secret:", b2a_hex(hmac_res[0]))
print("New secret:", b2a_hex(hmac_res[1]))

View File

@ -40,15 +40,15 @@ import sys
# Locate a device
devs = list(CtapHidDevice.list_devices())
if not devs:
print('No FIDO device found')
print("No FIDO device found")
sys.exit(1)
clients = [Fido2Client(d, 'https://example.com') for d in devs]
clients = [Fido2Client(d, "https://example.com") for d in devs]
# Prepare parameters for makeCredential
rp = {'id': 'example.com', 'name': 'Example RP'}
user = {'id': b'user_id', 'name': 'A. User'}
challenge = 'Y2hhbGxlbmdl'
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
challenge = "Y2hhbGxlbmdl"
cancel = Event()
attestation, client_data = None, None
@ -58,7 +58,7 @@ has_prompted = False
def on_keepalive(status):
global has_prompted # Don't prompt for each device.
if status == STATUS.UPNEEDED and not has_prompted:
print('\nTouch your authenticator device now...\n')
print("\nTouch your authenticator device now...\n")
has_prompted = True
@ -74,10 +74,10 @@ def work(client):
else:
return
cancel.set()
print('New credential created!')
print('ATTESTATION OBJECT:', attestation)
print("New credential created!")
print("ATTESTATION OBJECT:", attestation)
print()
print('CREDENTIAL DATA:', attestation.auth_data.credential_data)
print("CREDENTIAL DATA:", attestation.auth_data.credential_data)
threads = []
@ -90,4 +90,4 @@ for t in threads:
t.join()
if not cancel.is_set():
print('Operation timed out!')
print("Operation timed out!")

View File

@ -46,92 +46,91 @@ from flask import Flask, session, request, redirect, abort
import os
app = Flask(__name__, static_url_path='')
app = Flask(__name__, static_url_path="")
app.secret_key = os.urandom(32) # Used for session.
rp = RelyingParty('localhost', 'Demo server')
rp = RelyingParty("localhost", "Demo server")
# By using the U2FFido2Server class, we can support existing credentials
# registered by the legacy u2f.register API for an appId.
server = U2FFido2Server('https://localhost:5000', rp)
server = U2FFido2Server("https://localhost:5000", rp)
# Registered credentials are stored globally, in memory only. Single user
# support, state is lost when the server terminates.
credentials = []
@app.route('/')
@app.route("/")
def index():
return redirect('/index-u2f.html')
return redirect("/index-u2f.html")
@app.route('/api/register/begin', methods=['POST'])
@app.route("/api/register/begin", methods=["POST"])
def register_begin():
registration_data, state = server.register_begin({
'id': b'user_id',
'name': 'a_user',
'displayName': 'A. User',
'icon': 'https://example.com/image.png'
}, credentials)
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
"icon": "https://example.com/image.png",
},
credentials,
)
session['state'] = state
print('\n\n\n\n')
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print('\n\n\n\n')
print("\n\n\n\n")
return cbor.encode(registration_data)
@app.route('/api/register/complete', methods=['POST'])
@app.route("/api/register/complete", methods=["POST"])
def register_complete():
data = cbor.decode(request.get_data())
client_data = ClientData(data['clientDataJSON'])
att_obj = AttestationObject(data['attestationObject'])
print('clientData', client_data)
print('AttestationObject:', att_obj)
client_data = ClientData(data["clientDataJSON"])
att_obj = AttestationObject(data["attestationObject"])
print("clientData", client_data)
print("AttestationObject:", att_obj)
auth_data = server.register_complete(
session['state'],
client_data,
att_obj
)
auth_data = server.register_complete(session["state"], client_data, att_obj)
credentials.append(auth_data.credential_data)
print('REGISTERED CREDENTIAL:', auth_data.credential_data)
return cbor.encode({'status': 'OK'})
print("REGISTERED CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
@app.route('/api/authenticate/begin', methods=['POST'])
@app.route("/api/authenticate/begin", methods=["POST"])
def authenticate_begin():
if not credentials:
abort(404)
auth_data, state = server.authenticate_begin(credentials)
session['state'] = state
session["state"] = state
return cbor.encode(auth_data)
@app.route('/api/authenticate/complete', methods=['POST'])
@app.route("/api/authenticate/complete", methods=["POST"])
def authenticate_complete():
if not credentials:
abort(404)
data = cbor.decode(request.get_data())
credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON'])
auth_data = AuthenticatorData(data['authenticatorData'])
signature = data['signature']
print('clientData', client_data)
print('AuthenticatorData', auth_data)
credential_id = data["credentialId"]
client_data = ClientData(data["clientDataJSON"])
auth_data = AuthenticatorData(data["authenticatorData"])
signature = data["signature"]
print("clientData", client_data)
print("AuthenticatorData", auth_data)
server.authenticate_complete(
session.pop('state'),
session.pop("state"),
credentials,
credential_id,
client_data,
auth_data,
signature
signature,
)
print('ASSERTION OK')
return cbor.encode({'status': 'OK'})
print("ASSERTION OK")
return cbor.encode({"status": "OK"})
###############################################################################
@ -142,42 +141,43 @@ def authenticate_complete():
# registered using the WebAuthn APIs.
###############################################################################
@app.route('/api/u2f/begin', methods=['POST'])
def u2f_begin():
registration_data, state = server.register_begin({
'id': b'user_id',
'name': 'a_user',
'displayName': 'A. User',
'icon': 'https://example.com/image.png'
}, credentials)
session['state'] = state
print('\n\n\n\n')
print(registration_data)
print('\n\n\n\n')
return cbor.encode(
websafe_encode(registration_data['publicKey']['challenge'])
@app.route("/api/u2f/begin", methods=["POST"])
def u2f_begin():
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
"icon": "https://example.com/image.png",
},
credentials,
)
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print("\n\n\n\n")
return cbor.encode(websafe_encode(registration_data["publicKey"]["challenge"]))
@app.route('/api/u2f/complete', methods=['POST'])
@app.route("/api/u2f/complete", methods=["POST"])
def u2f_complete():
data = cbor.decode(request.get_data())
client_data = ClientData.from_b64(data['clientData'])
reg_data = RegistrationData.from_b64(data['registrationData'])
print('clientData', client_data)
print('U2F RegistrationData:', reg_data)
att_obj = AttestationObject.from_ctap1(
sha256(b'https://localhost:5000'), reg_data)
print('AttestationObject:', att_obj)
client_data = ClientData.from_b64(data["clientData"])
reg_data = RegistrationData.from_b64(data["registrationData"])
print("clientData", client_data)
print("U2F RegistrationData:", reg_data)
att_obj = AttestationObject.from_ctap1(sha256(b"https://localhost:5000"), reg_data)
print("AttestationObject:", att_obj)
auth_data = att_obj.auth_data
credentials.append(auth_data.credential_data)
print('REGISTERED U2F CREDENTIAL:', auth_data.credential_data)
return cbor.encode({'status': 'OK'})
print("REGISTERED U2F CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
if __name__ == '__main__':
if __name__ == "__main__":
print(__doc__)
app.run(ssl_context='adhoc', debug=True)
app.run(ssl_context="adhoc", debug=True)

View File

@ -44,10 +44,10 @@ from flask import Flask, session, request, redirect, abort
import os
app = Flask(__name__, static_url_path='')
app = Flask(__name__, static_url_path="")
app.secret_key = os.urandom(32) # Used for session.
rp = RelyingParty('localhost', 'Demo server')
rp = RelyingParty("localhost", "Demo server")
server = Fido2Server(rp)
@ -56,81 +56,81 @@ server = Fido2Server(rp)
credentials = []
@app.route('/')
@app.route("/")
def index():
return redirect('/index.html')
return redirect("/index.html")
@app.route('/api/register/begin', methods=['POST'])
@app.route("/api/register/begin", methods=["POST"])
def register_begin():
registration_data, state = server.register_begin({
'id': b'user_id',
'name': 'a_user',
'displayName': 'A. User',
'icon': 'https://example.com/image.png'
}, credentials, user_verification='discouraged')
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
"icon": "https://example.com/image.png",
},
credentials,
user_verification="discouraged",
)
session['state'] = state
print('\n\n\n\n')
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print('\n\n\n\n')
print("\n\n\n\n")
return cbor.encode(registration_data)
@app.route('/api/register/complete', methods=['POST'])
@app.route("/api/register/complete", methods=["POST"])
def register_complete():
data = cbor.decode(request.get_data())
client_data = ClientData(data['clientDataJSON'])
att_obj = AttestationObject(data['attestationObject'])
print('clientData', client_data)
print('AttestationObject:', att_obj)
client_data = ClientData(data["clientDataJSON"])
att_obj = AttestationObject(data["attestationObject"])
print("clientData", client_data)
print("AttestationObject:", att_obj)
auth_data = server.register_complete(
session['state'],
client_data,
att_obj
)
auth_data = server.register_complete(session["state"], client_data, att_obj)
credentials.append(auth_data.credential_data)
print('REGISTERED CREDENTIAL:', auth_data.credential_data)
return cbor.encode({'status': 'OK'})
print("REGISTERED CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
@app.route('/api/authenticate/begin', methods=['POST'])
@app.route("/api/authenticate/begin", methods=["POST"])
def authenticate_begin():
if not credentials:
abort(404)
auth_data, state = server.authenticate_begin(credentials)
session['state'] = state
session["state"] = state
return cbor.encode(auth_data)
@app.route('/api/authenticate/complete', methods=['POST'])
@app.route("/api/authenticate/complete", methods=["POST"])
def authenticate_complete():
if not credentials:
abort(404)
data = cbor.decode(request.get_data())
credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON'])
auth_data = AuthenticatorData(data['authenticatorData'])
signature = data['signature']
print('clientData', client_data)
print('AuthenticatorData', auth_data)
credential_id = data["credentialId"]
client_data = ClientData(data["clientDataJSON"])
auth_data = AuthenticatorData(data["authenticatorData"])
signature = data["signature"]
print("clientData", client_data)
print("AuthenticatorData", auth_data)
server.authenticate_complete(
session.pop('state'),
session.pop("state"),
credentials,
credential_id,
client_data,
auth_data,
signature
signature,
)
print('ASSERTION OK')
return cbor.encode({'status': 'OK'})
print("ASSERTION OK")
return cbor.encode({"status": "OK"})
if __name__ == '__main__':
if __name__ == "__main__":
print(__doc__)
app.run(ssl_context='adhoc', debug=True)
app.run(ssl_context="adhoc", debug=True)

View File

@ -6,15 +6,15 @@ import sys
dev = next(CtapPcscDevice.list_devices(), None)
if not dev:
print('No NFC u2f device found')
print("No NFC u2f device found")
sys.exit(1)
chal = sha256(b'AAA')
appid = sha256(b'BBB')
chal = sha256(b"AAA")
appid = sha256(b"BBB")
ctap1 = CTAP1(dev)
print('version:', ctap1.get_version())
print("version:", ctap1.get_version())
# True - make extended APDU and send it to key
# ISO 7816-3:2006. page 33, 12.1.3 Decoding conventions for command APDUs
@ -24,15 +24,15 @@ print('version:', ctap1.get_version())
dev.use_ext_apdu = False
reg = ctap1.register(chal, appid)
print('register:', reg)
print("register:", reg)
reg.verify(appid, chal)
print('Register message verify OK')
print("Register message verify OK")
auth = ctap1.authenticate(chal, appid, reg.key_handle)
print('authenticate result: ', auth)
print("authenticate result: ", auth)
res = auth.verify(appid, chal, reg.public_key)
print('Authenticate message verify OK')
print("Authenticate message verify OK")

View File

@ -30,10 +30,12 @@ import six
if six.PY2:
@six.add_metaclass(abc.ABCMeta)
class ABC(object):
pass
abc.ABC = ABC
__version__ = '0.7.2-dev0'
__version__ = "0.7.2-dev0"

View File

@ -52,10 +52,10 @@ class InvalidSignature(InvalidAttestation):
class UnsupportedType(InvalidAttestation):
def __init__(self, auth_data):
super(UnsupportedType, self).__init__(
'This attestation format is not supported!')
"This attestation format is not supported!"
)
self.auth_data = auth_data
@ -67,7 +67,7 @@ class Attestation(abc.ABC):
@staticmethod
def for_type(fmt):
for cls in Attestation.__subclasses__():
if getattr(cls, 'FORMAT', None) == fmt:
if getattr(cls, "FORMAT", None) == fmt:
return cls
return UnsupportedAttestation
@ -78,32 +78,33 @@ class UnsupportedAttestation(Attestation):
class NoneAttestation(Attestation):
FORMAT = 'none'
FORMAT = "none"
def verify(self, statement, auth_data, client_data_hash):
if statement != {}:
raise InvalidData('None Attestation requires empty statement.')
raise InvalidData("None Attestation requires empty statement.")
class FidoU2FAttestation(Attestation):
FORMAT = 'fido-u2f'
FORMAT = "fido-u2f"
def verify(self, statement, auth_data, client_data_hash):
cd = auth_data.credential_data
pk = b'\x04' + cd.public_key[-2] + cd.public_key[-3]
pk = b"\x04" + cd.public_key[-2] + cd.public_key[-3]
FidoU2FAttestation.verify_signature(
auth_data.rp_id_hash,
client_data_hash,
cd.credential_id,
pk,
statement['x5c'][0],
statement['sig']
statement["x5c"][0],
statement["sig"],
)
@staticmethod
def verify_signature(app_param, client_param, key_handle, public_key,
cert_bytes, signature):
m = b'\0' + app_param + client_param + key_handle + public_key
def verify_signature(
app_param, client_param, key_handle, public_key, cert_bytes, signature
):
m = b"\0" + app_param + client_param + key_handle + public_key
cert = x509.load_der_x509_certificate(cert_bytes, default_backend())
try:
ES256.from_cryptography_key(cert.public_key()).verify(m, signature)
@ -112,44 +113,43 @@ class FidoU2FAttestation(Attestation):
# GS Root R2 (https://pki.goog/)
_GSR2_DER = a2b_hex(b'308203ba308202a2a003020102020b0400000000010f8626e60d300d06092a864886f70d0101050500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3036313231353038303030305a170d3231313231353038303030305a304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e30820122300d06092a864886f70d01010105000382010f003082010a0282010100a6cf240ebe2e6f28994542c4ab3e21549b0bd37f8470fa12b3cbbf875fc67f86d3b2305cd6fdadf17bdce5f86096099210f5d053defb7b7e7388ac52887b4aa6ca49a65ea8a78c5a11bc7a82ebbe8ce9b3ac962507974a992a072fb41e77bf8a0fb5027c1b96b8c5b93a2cbcd612b9eb597de2d006865f5e496ab5395e8834ecbc780c0898846ca8cd4bb4a07d0c794df0b82dcb21cad56c5b7de1a02984a1f9d39449cb24629120bcdd0bd5d9ccf9ea270a2b7391c69d1bacc8cbe8e0a0f42f908b4dfbb0361bf6197a85e06df26113885c9fe0930a51978a5aceafabd5f7aa09aa60bddcd95fdf72a960135e0001c94afa3fa4ea070321028e82ca03c29b8f0203010001a3819c308199300e0603551d0f0101ff040403020106300f0603551d130101ff040530030101ff301d0603551d0e041604149be20757671c1ec06a06de59b49a2ddfdc19862e30360603551d1f042f302d302ba029a0278625687474703a2f2f63726c2e676c6f62616c7369676e2e6e65742f726f6f742d72322e63726c301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e300d06092a864886f70d01010505000382010100998153871c68978691ece04ab8440bab81ac274fd6c1b81c4378b30c9afcea2c3c6e611b4d4b29f59f051d26c1b8e983006245b6a90893b9a9334b189ac2f887884edbdd71341ac154da463fe0d32aab6d5422f53a62cd206fba2989d7dd91eed35ca23ea15b41f5dfe564432de9d539abd2a2dfb78bd0c080191c45c02d8ce8f82da4745649c505b54f15de6e44783987a87ebbf3791891bbf46f9dc1f08c358c5d01fbc36db9ef446d7946317e0afea982c1ffefab6e20c450c95f9d4d9b178c0ce501c9a0416a7353faa550b46e250ffb4c18f4fd52d98e69b1e8110fde88d8fb1d49f7aade95cf2078c26012db25408c6afc7e4238406412f79e81e1932e') # noqa
_GSR2_DER = a2b_hex(
b"308203ba308202a2a003020102020b0400000000010f8626e60d300d06092a864886f70d0101050500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3036313231353038303030305a170d3231313231353038303030305a304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e30820122300d06092a864886f70d01010105000382010f003082010a0282010100a6cf240ebe2e6f28994542c4ab3e21549b0bd37f8470fa12b3cbbf875fc67f86d3b2305cd6fdadf17bdce5f86096099210f5d053defb7b7e7388ac52887b4aa6ca49a65ea8a78c5a11bc7a82ebbe8ce9b3ac962507974a992a072fb41e77bf8a0fb5027c1b96b8c5b93a2cbcd612b9eb597de2d006865f5e496ab5395e8834ecbc780c0898846ca8cd4bb4a07d0c794df0b82dcb21cad56c5b7de1a02984a1f9d39449cb24629120bcdd0bd5d9ccf9ea270a2b7391c69d1bacc8cbe8e0a0f42f908b4dfbb0361bf6197a85e06df26113885c9fe0930a51978a5aceafabd5f7aa09aa60bddcd95fdf72a960135e0001c94afa3fa4ea070321028e82ca03c29b8f0203010001a3819c308199300e0603551d0f0101ff040403020106300f0603551d130101ff040530030101ff301d0603551d0e041604149be20757671c1ec06a06de59b49a2ddfdc19862e30360603551d1f042f302d302ba029a0278625687474703a2f2f63726c2e676c6f62616c7369676e2e6e65742f726f6f742d72322e63726c301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e300d06092a864886f70d01010505000382010100998153871c68978691ece04ab8440bab81ac274fd6c1b81c4378b30c9afcea2c3c6e611b4d4b29f59f051d26c1b8e983006245b6a90893b9a9334b189ac2f887884edbdd71341ac154da463fe0d32aab6d5422f53a62cd206fba2989d7dd91eed35ca23ea15b41f5dfe564432de9d539abd2a2dfb78bd0c080191c45c02d8ce8f82da4745649c505b54f15de6e44783987a87ebbf3791891bbf46f9dc1f08c358c5d01fbc36db9ef446d7946317e0afea982c1ffefab6e20c450c95f9d4d9b178c0ce501c9a0416a7353faa550b46e250ffb4c18f4fd52d98e69b1e8110fde88d8fb1d49f7aade95cf2078c26012db25408c6afc7e4238406412f79e81e1932e" # noqa E501
)
class AndroidSafetynetAttestation(Attestation):
FORMAT = 'android-safetynet'
FORMAT = "android-safetynet"
def __init__(self, allow_rooted=False, ca=_GSR2_DER):
self.allow_rooted = allow_rooted
self._ca = x509.load_der_x509_certificate(ca, default_backend())
def verify(self, statement, auth_data, client_data_hash):
jwt = statement['response']
header, payload, sig = (websafe_decode(x) for x in jwt.split(b'.'))
data = json.loads(payload.decode('utf8'))
if not self.allow_rooted and data['ctsProfileMatch'] is not True:
raise InvalidData('ctsProfileMatch must be true!')
jwt = statement["response"]
header, payload, sig = (websafe_decode(x) for x in jwt.split(b"."))
data = json.loads(payload.decode("utf8"))
if not self.allow_rooted and data["ctsProfileMatch"] is not True:
raise InvalidData("ctsProfileMatch must be true!")
expected_nonce = sha256(auth_data + client_data_hash)
if not bytes_eq(expected_nonce, websafe_decode(data['nonce'])):
raise InvalidData('Nonce does not match!')
if not bytes_eq(expected_nonce, websafe_decode(data["nonce"])):
raise InvalidData("Nonce does not match!")
data = json.loads(header.decode('utf8'))
data = json.loads(header.decode("utf8"))
certs = [
x509.load_der_x509_certificate(
websafe_decode(x), default_backend())
for x in data['x5c']
x509.load_der_x509_certificate(websafe_decode(x), default_backend())
for x in data["x5c"]
]
certs.append(self._ca)
cert = certs.pop(0)
cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if cn[0].value != 'attest.android.com':
raise InvalidData('Certificate not issued to attest.android.com!')
if cn[0].value != "attest.android.com":
raise InvalidData("Certificate not issued to attest.android.com!")
CoseKey.for_name(
data['alg']
).from_cryptography_key(
cert.public_key()
).verify(jwt.rsplit(b'.', 1)[0], sig)
CoseKey.for_name(data["alg"]).from_cryptography_key(cert.public_key()).verify(
jwt.rsplit(b".", 1)[0], sig
)
while certs:
child = cert
@ -160,76 +160,74 @@ class AndroidSafetynetAttestation(Attestation):
child.signature,
child.tbs_certificate_bytes,
padding.PKCS1v15(),
child.signature_hash_algorithm
child.signature_hash_algorithm,
)
elif isinstance(pub, ec.EllipticCurvePublicKey):
pub.verify(
child.signature,
child.tbs_certificate_bytes,
ec.ECDSA(child.signature_hash_algorithm)
ec.ECDSA(child.signature_hash_algorithm),
)
OID_AAGUID = x509.ObjectIdentifier('1.3.6.1.4.1.45724.1.1.4')
OID_AAGUID = x509.ObjectIdentifier("1.3.6.1.4.1.45724.1.1.4")
def _validate_attestation_certificate(cert, aaguid):
if cert.version != x509.Version.v3:
raise InvalidData('Attestation certificate must use version 3!')
raise InvalidData("Attestation certificate must use version 3!")
c = cert.subject.get_attributes_for_oid(x509.NameOID.COUNTRY_NAME)
if not c:
raise InvalidData('Subject must have C set!')
raise InvalidData("Subject must have C set!")
o = cert.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)
if not o:
raise InvalidData('Subject must have O set!')
ous = cert.subject.get_attributes_for_oid(
x509.NameOID.ORGANIZATIONAL_UNIT_NAME
)
raise InvalidData("Subject must have O set!")
ous = cert.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATIONAL_UNIT_NAME)
if not ous:
raise InvalidData('Subject must have OU = "Authenticator Attestation"!')
ou = ous[0]
if ou.value != 'Authenticator Attestation':
if ou.value != "Authenticator Attestation":
raise InvalidData('Subject must have OU = "Authenticator Attestation"!')
cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if not cn:
raise InvalidData('Subject must have CN set!')
raise InvalidData("Subject must have CN set!")
bc = cert.extensions.get_extension_for_class(x509.BasicConstraints)
if bc.value.ca:
raise InvalidData('Attestation certificate must have CA=false!')
raise InvalidData("Attestation certificate must have CA=false!")
try:
ext = cert.extensions.get_extension_for_oid(OID_AAGUID)
if ext.critical:
raise InvalidData('AAGUID extension must not be marked as critical')
raise InvalidData("AAGUID extension must not be marked as critical")
ext_aaguid = ext.value.value[2:]
if ext_aaguid != aaguid:
raise InvalidData('AAGUID in Authenticator data does not '
'match attestation certificate!')
raise InvalidData(
"AAGUID in Authenticator data does not "
"match attestation certificate!"
)
except x509.ExtensionNotFound:
pass # If missing, ignore
class PackedAttestation(Attestation):
FORMAT = 'packed'
FORMAT = "packed"
def verify(self, statement, auth_data, client_data_hash):
if 'ecdaaKeyId' in statement:
raise NotImplementedError('ECDAA not implemented')
alg = statement['alg']
x5c = statement.get('x5c')
if "ecdaaKeyId" in statement:
raise NotImplementedError("ECDAA not implemented")
alg = statement["alg"]
x5c = statement.get("x5c")
if x5c:
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
_validate_attestation_certificate(cert,
auth_data.credential_data.aaguid)
_validate_attestation_certificate(cert, auth_data.credential_data.aaguid)
pub_key = CoseKey.for_alg(alg).from_cryptography_key(
cert.public_key())
pub_key = CoseKey.for_alg(alg).from_cryptography_key(cert.public_key())
else:
pub_key = CoseKey.parse(auth_data.credential_data.public_key)
if pub_key.ALGORITHM != alg:
raise InvalidData('Wrong algorithm of public key!')
raise InvalidData("Wrong algorithm of public key!")
try:
pub_key.verify(auth_data + client_data_hash, statement['sig'])
pub_key.verify(auth_data + client_data_hash, statement["sig"])
except _InvalidSignature:
raise InvalidSignature()

View File

@ -42,24 +42,24 @@ def dump_int(data, mt=0):
mt = mt << 5
if data <= 23:
args = ('>B', mt | data)
elif data <= 0xff:
args = ('>BB', mt | 24, data)
elif data <= 0xffff:
args = ('>BH', mt | 25, data)
elif data <= 0xffffffff:
args = ('>BI', mt | 26, data)
args = (">B", mt | data)
elif data <= 0xFF:
args = (">BB", mt | 24, data)
elif data <= 0xFFFF:
args = (">BH", mt | 25, data)
elif data <= 0xFFFFFFFF:
args = (">BI", mt | 26, data)
else:
args = ('>BQ', mt | 27, data)
args = (">BQ", mt | 27, data)
return struct.pack(*args)
def dump_bool(data):
return b'\xf5' if data else b'\xf4'
return b"\xf5" if data else b"\xf4"
def dump_list(data):
return dump_int(len(data), mt=4) + b''.join([encode(x) for x in data])
return dump_int(len(data), mt=4) + b"".join([encode(x) for x in data])
def _sort_keys(entry):
@ -70,7 +70,7 @@ def _sort_keys(entry):
def dump_dict(data):
items = [(encode(k), encode(v)) for k, v in data.items()]
items.sort(key=_sort_keys)
return dump_int(len(items), mt=5) + b''.join([k+v for (k, v) in items])
return dump_int(len(items), mt=5) + b"".join([k + v for (k, v) in items])
def dump_bytes(data):
@ -78,7 +78,7 @@ def dump_bytes(data):
def dump_text(data):
data_bytes = data.encode('utf8')
data_bytes = data.encode("utf8")
return dump_int(len(data_bytes), mt=3) + data_bytes
@ -88,7 +88,7 @@ _SERIALIZERS = [
(dict, dump_dict),
(list, dump_list),
(six.text_type, dump_text),
(six.binary_type, dump_bytes)
(six.binary_type, dump_bytes),
]
@ -96,7 +96,7 @@ def encode(data):
for k, v in _SERIALIZERS:
if isinstance(data, k):
return v(data)
raise ValueError('Unsupported value: {}'.format(data))
raise ValueError("Unsupported value: {}".format(data))
def load_int(ai, data):
@ -105,12 +105,12 @@ def load_int(ai, data):
elif ai == 24:
return six.indexbytes(data, 0), data[1:]
elif ai == 25:
return struct.unpack_from('>H', data)[0], data[2:]
return struct.unpack_from(">H", data)[0], data[2:]
elif ai == 26:
return struct.unpack_from('>I', data)[0], data[4:]
return struct.unpack_from(">I", data)[0], data[4:]
elif ai == 27:
return struct.unpack_from('>Q', data)[0], data[8:]
raise ValueError('Invalid additional information')
return struct.unpack_from(">Q", data)[0], data[8:]
raise ValueError("Invalid additional information")
def load_nint(ai, data):
@ -129,7 +129,7 @@ def load_bytes(ai, data):
def load_text(ai, data):
enc, rest = load_bytes(ai, data)
return enc.decode('utf8'), rest
return enc.decode("utf8"), rest
def load_array(ai, data):
@ -158,7 +158,7 @@ _DESERIALIZERS = {
3: load_text,
4: load_array,
5: load_map,
7: load_bool
7: load_bool,
}
@ -169,6 +169,6 @@ def decode_from(data):
def decode(data):
value, rest = decode_from(data)
if rest != b'':
raise ValueError('Extraneous data')
if rest != b"":
raise ValueError("Extraneous data")
return value

View File

@ -30,8 +30,7 @@ from __future__ import absolute_import, unicode_literals
from .hid import STATUS
from .ctap import CtapError
from .ctap1 import CTAP1, APDU, ApduError
from .ctap2 import (CTAP2, PinProtocolV1, AttestationObject, AssertionResponse,
Info)
from .ctap2 import CTAP2, PinProtocolV1, AttestationObject, AssertionResponse, Info
from .cose import ES256
from .rpid import verify_rp_id, verify_app_id
from .utils import Timeout, sha256, hmac_sha256, websafe_decode, websafe_encode
@ -50,7 +49,7 @@ class ClientData(bytes):
@property
def challenge(self):
return websafe_decode(self.get('challenge'))
return websafe_decode(self.get("challenge"))
@property
def b64(self):
@ -92,40 +91,45 @@ class ClientError(Exception):
self.cause = cause
def __repr__(self):
r = 'Client error: {0} - {0.name}'.format(self.code)
r = "Client error: {0} - {0.name}".format(self.code)
if self.cause:
r += '. Caused by {}'.format(self.cause)
r += ". Caused by {}".format(self.cause)
return r
def _ctap2client_err(e):
if e.code in [CtapError.ERR.CREDENTIAL_EXCLUDED,
CtapError.ERR.NO_CREDENTIALS]:
if e.code in [CtapError.ERR.CREDENTIAL_EXCLUDED, CtapError.ERR.NO_CREDENTIALS]:
ce = ClientError.ERR.DEVICE_INELIGIBLE
elif e.code in [CtapError.ERR.KEEPALIVE_CANCEL,
CtapError.ERR.ACTION_TIMEOUT,
CtapError.ERR.USER_ACTION_TIMEOUT]:
elif e.code in [
CtapError.ERR.KEEPALIVE_CANCEL,
CtapError.ERR.ACTION_TIMEOUT,
CtapError.ERR.USER_ACTION_TIMEOUT,
]:
ce = ClientError.ERR.TIMEOUT
elif e.code in [CtapError.ERR.UNSUPPORTED_ALGORITHM,
CtapError.ERR.UNSUPPORTED_OPTION,
CtapError.ERR.UNSUPPORTED_EXTENSION,
CtapError.ERR.KEY_STORE_FULL]:
elif e.code in [
CtapError.ERR.UNSUPPORTED_ALGORITHM,
CtapError.ERR.UNSUPPORTED_OPTION,
CtapError.ERR.UNSUPPORTED_EXTENSION,
CtapError.ERR.KEY_STORE_FULL,
]:
ce = ClientError.ERR.CONFIGURATION_UNSUPPORTED
elif e.code in [CtapError.ERR.INVALID_COMMAND,
CtapError.ERR.CBOR_UNEXPECTED_TYPE,
CtapError.ERR.INVALID_CBOR,
CtapError.ERR.MISSING_PARAMETER,
CtapError.ERR.INVALID_OPTION,
CtapError.ERR.PIN_REQUIRED,
CtapError.ERR.PIN_INVALID,
CtapError.ERR.PIN_BLOCKED,
CtapError.ERR.PIN_NOT_SET,
CtapError.ERR.PIN_POLICY_VIOLATION,
CtapError.ERR.PIN_TOKEN_EXPIRED,
CtapError.ERR.PIN_AUTH_INVALID,
CtapError.ERR.PIN_AUTH_BLOCKED,
CtapError.ERR.REQUEST_TOO_LARGE,
CtapError.ERR.OPERATION_DENIED]:
elif e.code in [
CtapError.ERR.INVALID_COMMAND,
CtapError.ERR.CBOR_UNEXPECTED_TYPE,
CtapError.ERR.INVALID_CBOR,
CtapError.ERR.MISSING_PARAMETER,
CtapError.ERR.INVALID_OPTION,
CtapError.ERR.PIN_REQUIRED,
CtapError.ERR.PIN_INVALID,
CtapError.ERR.PIN_BLOCKED,
CtapError.ERR.PIN_NOT_SET,
CtapError.ERR.PIN_POLICY_VIOLATION,
CtapError.ERR.PIN_TOKEN_EXPIRED,
CtapError.ERR.PIN_AUTH_INVALID,
CtapError.ERR.PIN_AUTH_BLOCKED,
CtapError.ERR.REQUEST_TOO_LARGE,
CtapError.ERR.OPERATION_DENIED,
]:
ce = ClientError.ERR.BAD_REQUEST
else:
ce = ClientError.ERR.OTHER_ERROR
@ -153,8 +157,8 @@ def _call_polling(poll_delay, timeout, on_keepalive, func, *args, **kwargs):
@unique
class U2F_TYPE(six.text_type, Enum):
REGISTER = 'navigator.id.finishEnrollment'
SIGN = 'navigator.id.getAssertion'
REGISTER = "navigator.id.finishEnrollment"
SIGN = "navigator.id.getAssertion"
class U2fClient(object):
@ -172,19 +176,25 @@ class U2fClient(object):
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def register(self, app_id, register_requests, registered_keys, timeout=None,
on_keepalive=None):
def register(
self,
app_id,
register_requests,
registered_keys,
timeout=None,
on_keepalive=None,
):
self._verify_app_id(app_id)
version = self.ctap.get_version()
dummy_param = b'\0' * 32
dummy_param = b"\0" * 32
for key in registered_keys:
if key['version'] != version:
if key["version"] != version:
continue
key_app_id = key.get('appId', app_id)
key_app_id = key.get("appId", app_id)
app_param = sha256(key_app_id.encode())
self._verify_app_id(key_app_id)
key_handle = websafe_decode(key['keyHandle'])
key_handle = websafe_decode(key["keyHandle"])
try:
self.ctap.authenticate(dummy_param, app_param, key_handle, True)
raise ClientError.ERR.DEVICE_INELIGIBLE() # Bad response
@ -195,49 +205,49 @@ class U2fClient(object):
raise _ctap2client_err(e)
for request in register_requests:
if request['version'] == version:
challenge = request['challenge']
if request["version"] == version:
challenge = request["challenge"]
break
else:
raise ClientError.ERR.DEVICE_INELIGIBLE()
client_data = ClientData.build(
typ=U2F_TYPE.REGISTER,
challenge=challenge,
origin=self.origin
typ=U2F_TYPE.REGISTER, challenge=challenge, origin=self.origin
)
app_param = sha256(app_id.encode())
reg_data = _call_polling(
self.poll_delay, timeout, on_keepalive, self.ctap.register,
client_data.hash, app_param
self.poll_delay,
timeout,
on_keepalive,
self.ctap.register,
client_data.hash,
app_param,
)
return {
'registrationData': reg_data.b64,
'clientData': client_data.b64
}
return {"registrationData": reg_data.b64, "clientData": client_data.b64}
def sign(self, app_id, challenge, registered_keys, timeout=None,
on_keepalive=None):
def sign(self, app_id, challenge, registered_keys, timeout=None, on_keepalive=None):
client_data = ClientData.build(
typ=U2F_TYPE.SIGN,
challenge=challenge,
origin=self.origin
typ=U2F_TYPE.SIGN, challenge=challenge, origin=self.origin
)
version = self.ctap.get_version()
for key in registered_keys:
if key['version'] == version:
key_app_id = key.get('appId', app_id)
if key["version"] == version:
key_app_id = key.get("appId", app_id)
self._verify_app_id(key_app_id)
key_handle = websafe_decode(key['keyHandle'])
key_handle = websafe_decode(key["keyHandle"])
app_param = sha256(key_app_id.encode())
try:
signature_data = _call_polling(
self.poll_delay, timeout, on_keepalive,
self.ctap.authenticate, client_data.hash, app_param,
key_handle
self.poll_delay,
timeout,
on_keepalive,
self.ctap.authenticate,
client_data.hash,
app_param,
key_handle,
)
break
except ClientError:
@ -246,19 +256,19 @@ class U2fClient(object):
raise ClientError.ERR.DEVICE_INELIGIBLE()
return {
'clientData': client_data.b64,
'signatureData': signature_data.b64,
'keyHandle': key['keyHandle']
"clientData": client_data.b64,
"signatureData": signature_data.b64,
"keyHandle": key["keyHandle"],
}
@unique
class WEBAUTHN_TYPE(six.text_type, Enum):
MAKE_CREDENTIAL = 'webauthn.create'
GET_ASSERTION = 'webauthn.get'
MAKE_CREDENTIAL = "webauthn.create"
GET_ASSERTION = "webauthn.get"
_CTAP1_INFO = b'\xa2\x01\x81\x66\x55\x32\x46\x5f\x56\x32\x03\x50' + b'\0' * 16
_CTAP1_INFO = b"\xa2\x01\x81\x66\x55\x32\x46\x5f\x56\x32\x03\x50" + b"\0" * 16
class Fido2Client(object):
@ -289,30 +299,65 @@ class Fido2Client(object):
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def make_credential(self, rp, user, challenge, algos=[ES256.ALGORITHM],
exclude_list=None, extensions=None, rk=False, uv=False,
pin=None, timeout=None, on_keepalive=None):
def make_credential(
self,
rp,
user,
challenge,
algos=[ES256.ALGORITHM],
exclude_list=None,
extensions=None,
rk=False,
uv=False,
pin=None,
timeout=None,
on_keepalive=None,
):
self._verify_rp_id(rp['id'])
self._verify_rp_id(rp["id"])
client_data = ClientData.build(
type=WEBAUTHN_TYPE.MAKE_CREDENTIAL,
clientExtensions={},
challenge=challenge,
origin=self.origin
origin=self.origin,
)
try:
return self._do_make_credential(
client_data, rp, user, algos, exclude_list, extensions, rk, uv,
pin, timeout, on_keepalive
), client_data
return (
self._do_make_credential(
client_data,
rp,
user,
algos,
exclude_list,
extensions,
rk,
uv,
pin,
timeout,
on_keepalive,
),
client_data,
)
except CtapError as e:
raise _ctap2client_err(e)
def _ctap2_make_credential(self, client_data, rp, user, algos, exclude_list,
extensions, rk, uv, pin, timeout, on_keepalive):
key_params = [{'type': 'public-key', 'alg': alg} for alg in algos]
def _ctap2_make_credential(
self,
client_data,
rp,
user,
algos,
exclude_list,
extensions,
rk,
uv,
pin,
timeout,
on_keepalive,
):
key_params = [{"type": "public-key", "alg": alg} for alg in algos]
pin_auth = None
pin_protocol = None
@ -320,17 +365,17 @@ class Fido2Client(object):
pin_protocol = self.pin_protocol.VERSION
pin_token = self.pin_protocol.get_pin_token(pin)
pin_auth = hmac_sha256(pin_token, client_data.hash)[:16]
elif self.info.options.get('clientPin'):
raise ValueError('PIN required!')
elif self.info.options.get("clientPin"):
raise ValueError("PIN required!")
if not (rk or uv):
options = None
else:
options = {}
if rk:
options['rk'] = True
options["rk"] = True
if uv:
options['uv'] = True
options["uv"] = True
# Filter out credential IDs which are too long
max_len = self.info.max_cred_id_length
@ -340,42 +385,83 @@ class Fido2Client(object):
# Reject the request if too many credentials remain.
max_creds = self.info.max_creds_in_list
if max_creds and len(exclude_list or ()) > max_creds:
raise ClientError.ERR.BAD_REQUEST('exclude_list too long')
raise ClientError.ERR.BAD_REQUEST("exclude_list too long")
return self.ctap2.make_credential(client_data.hash, rp, user,
key_params, exclude_list,
extensions, options, pin_auth,
pin_protocol, timeout, on_keepalive)
return self.ctap2.make_credential(
client_data.hash,
rp,
user,
key_params,
exclude_list,
extensions,
options,
pin_auth,
pin_protocol,
timeout,
on_keepalive,
)
def _ctap1_make_credential(self, client_data, rp, user, algos, exclude_list,
extensions, rk, uv, pin, timeout, on_keepalive):
def _ctap1_make_credential(
self,
client_data,
rp,
user,
algos,
exclude_list,
extensions,
rk,
uv,
pin,
timeout,
on_keepalive,
):
if rk or uv or ES256.ALGORITHM not in algos:
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
app_param = sha256(rp['id'].encode())
app_param = sha256(rp["id"].encode())
dummy_param = b'\0' * 32
dummy_param = b"\0" * 32
for cred in exclude_list or []:
key_handle = cred['id']
key_handle = cred["id"]
try:
self.ctap1.authenticate(
dummy_param, app_param, key_handle, True)
self.ctap1.authenticate(dummy_param, app_param, key_handle, True)
raise ClientError.ERR.OTHER_ERROR() # Shouldn't happen
except ApduError as e:
if e.code == APDU.USE_NOT_SATISFIED:
_call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap1.register, dummy_param, dummy_param)
_call_polling(
self.ctap1_poll_delay,
timeout,
on_keepalive,
self.ctap1.register,
dummy_param,
dummy_param,
)
raise ClientError.ERR.DEVICE_INELIGIBLE()
return AttestationObject.from_ctap1(
app_param,
_call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap1.register, client_data.hash, app_param)
_call_polling(
self.ctap1_poll_delay,
timeout,
on_keepalive,
self.ctap1.register,
client_data.hash,
app_param,
),
)
def get_assertion(self, rp_id, challenge, allow_list=None, extensions=None,
up=True, uv=False, pin=None, timeout=None,
on_keepalive=None):
def get_assertion(
self,
rp_id,
challenge,
allow_list=None,
extensions=None,
up=True,
uv=False,
pin=None,
timeout=None,
on_keepalive=None,
):
self._verify_rp_id(rp_id)
@ -383,33 +469,53 @@ class Fido2Client(object):
type=WEBAUTHN_TYPE.GET_ASSERTION,
clientExtensions={},
challenge=challenge,
origin=self.origin
origin=self.origin,
)
try:
return self._do_get_assertion(
client_data, rp_id, allow_list, extensions, up, uv, pin,
timeout, on_keepalive
), client_data
return (
self._do_get_assertion(
client_data,
rp_id,
allow_list,
extensions,
up,
uv,
pin,
timeout,
on_keepalive,
),
client_data,
)
except CtapError as e:
raise _ctap2client_err(e)
def _ctap2_get_assertion(self, client_data, rp_id, allow_list, extensions,
up, uv, pin, timeout, on_keepalive):
def _ctap2_get_assertion(
self,
client_data,
rp_id,
allow_list,
extensions,
up,
uv,
pin,
timeout,
on_keepalive,
):
pin_auth = None
pin_protocol = None
if pin:
pin_protocol = self.pin_protocol.VERSION
pin_token = self.pin_protocol.get_pin_token(pin)
pin_auth = hmac_sha256(pin_token, client_data.hash)[:16]
elif self.info.options.get('clientPin'):
raise ValueError('PIN required!')
elif self.info.options.get("clientPin"):
raise ValueError("PIN required!")
options = {}
if not up:
options['up'] = False
options["up"] = False
if uv:
options['uv'] = True
options["uv"] = True
if len(options) == 0:
options = None
@ -423,15 +529,32 @@ class Fido2Client(object):
# Reject the request if too many credentials remain.
max_creds = self.info.max_creds_in_list
if max_creds and len(allow_list) > max_creds:
raise ClientError.ERR.BAD_REQUEST('allow_list too long')
raise ClientError.ERR.BAD_REQUEST("allow_list too long")
return self.ctap2.get_assertions(
rp_id, client_data.hash, allow_list, extensions, options, pin_auth,
pin_protocol, timeout, on_keepalive
rp_id,
client_data.hash,
allow_list,
extensions,
options,
pin_auth,
pin_protocol,
timeout,
on_keepalive,
)
def _ctap1_get_assertion(self, client_data, rp_id, allow_list, extensions,
up, uv, pin, timeout, on_keepalive):
def _ctap1_get_assertion(
self,
client_data,
rp_id,
allow_list,
extensions,
up,
uv,
pin,
timeout,
on_keepalive,
):
if (not up) or uv or not allow_list:
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
@ -440,12 +563,15 @@ class Fido2Client(object):
for cred in allow_list:
try:
auth_resp = _call_polling(
self.ctap1_poll_delay, timeout, on_keepalive,
self.ctap1.authenticate, client_param, app_param, cred['id']
self.ctap1_poll_delay,
timeout,
on_keepalive,
self.ctap1.authenticate,
client_param,
app_param,
cred["id"],
)
return [
AssertionResponse.from_ctap1(app_param, cred, auth_resp)
]
return [AssertionResponse.from_ctap1(app_param, cred, auth_resp)]
except ClientError as e:
if e.code == ClientError.ERR.TIMEOUT:
raise # Other errors are ignored so we move to the next.

View File

@ -44,6 +44,7 @@ class CoseKey(dict):
:param _: The COSE key paramters.
:cvar ALGORITHM: COSE algorithm identifier.
"""
ALGORITHM = None
def verify(self, message, signature):
@ -52,7 +53,7 @@ class CoseKey(dict):
:param message: The message which was signed.
:param signature: The signature to check.
"""
raise NotImplementedError('Signature verification not supported.')
raise NotImplementedError("Signature verification not supported.")
@classmethod
def from_cryptography_key(cls, public_key):
@ -61,7 +62,7 @@ class CoseKey(dict):
:param public_key: Either an EC or RSA public key.
:return: A CoseKey.
"""
raise NotImplementedError('Creation from cryptography not supported.')
raise NotImplementedError("Creation from cryptography not supported.")
@staticmethod
def for_alg(alg):
@ -95,7 +96,7 @@ class CoseKey(dict):
"""Create a CoseKey from a dict"""
alg = cose.get(3)
if not alg:
raise ValueError('COSE alg identifier must be provided.')
raise ValueError("COSE alg identifier must be provided.")
return CoseKey.for_alg(alg)(cose)
@staticmethod
@ -117,7 +118,7 @@ class ES256(CoseKey):
def verify(self, message, signature):
if self[-1] != 1:
raise ValueError('Unsupported elliptic curve')
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256R1()
).public_key(default_backend()).verify(
@ -127,13 +128,15 @@ class ES256(CoseKey):
@classmethod
def from_cryptography_key(cls, public_key):
pn = public_key.public_numbers()
return cls({
1: 2,
3: cls.ALGORITHM,
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32)
})
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32),
}
)
@classmethod
def from_ctap1(cls, data):
@ -142,58 +145,42 @@ class ES256(CoseKey):
:param data: A 65 byte SECP256R1 public key.
:return: A ES256 key.
"""
return cls({
1: 2,
3: cls.ALGORITHM,
-1: 1,
-2: data[1:33],
-3: data[33:65]
})
return cls({1: 2, 3: cls.ALGORITHM, -1: 1, -2: data[1:33], -3: data[33:65]})
class RS256(CoseKey):
ALGORITHM = -257
def verify(self, message, signature):
rsa.RSAPublicNumbers(
bytes2int(self[-2]), bytes2int(self[-1])
).public_key(default_backend()).verify(
signature, message, padding.PKCS1v15(), hashes.SHA256()
)
rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key(
default_backend()
).verify(signature, message, padding.PKCS1v15(), hashes.SHA256())
@classmethod
def from_cryptography_key(cls, public_key):
pn = public_key.public_numbers()
return cls({
1: 3,
3: cls.ALGORITHM,
-1: int2bytes(pn.n),
-2: int2bytes(pn.e)
})
return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
class PS256(CoseKey):
ALGORITHM = -37
def verify(self, message, signature):
rsa.RSAPublicNumbers(
bytes2int(self[-2]), bytes2int(self[-1])
).public_key(default_backend()).verify(
signature, message, padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
), hashes.SHA256()
rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key(
default_backend()
).verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256(),
)
@classmethod
def from_cryptography_key(cls, public_key):
pn = public_key.public_numbers()
return cls({
1: 3,
3: cls.ALGORITHM,
-1: int2bytes(pn.n),
-2: int2bytes(pn.e)
})
return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
class EdDSA(CoseKey):
@ -201,19 +188,18 @@ class EdDSA(CoseKey):
def verify(self, message, signature):
if self[-1] != 6:
raise ValueError('Unsupported elliptic curve')
ed25519.Ed25519PublicKey.from_public_bytes(self[-2]).verify(
signature, message
)
raise ValueError("Unsupported elliptic curve")
ed25519.Ed25519PublicKey.from_public_bytes(self[-2]).verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
return cls({
1: 1,
3: cls.ALGORITHM,
-1: 6,
-2: public_key.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
),
})
return cls(
{
1: 1,
3: cls.ALGORITHM,
-1: 6,
-2: public_key.public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
),
}
)

View File

@ -44,7 +44,7 @@ class CtapDevice(abc.ABC):
"""
@abc.abstractmethod
def call(self, cmd, data=b'', event=None, on_keepalive=None):
def call(self, cmd, data=b"", event=None, on_keepalive=None):
"""Sends a command to the authenticator, and reads the response.
:param cmd: The integer value of the command.
@ -119,13 +119,13 @@ class CtapError(Exception):
VENDOR_LAST = 0xFF
def __str__(self):
return '0x%02X - %s' % (self.value, self.name)
return "0x%02X - %s" % (self.value, self.name)
def __init__(self, code):
try:
code = CtapError.ERR(code)
message = 'CTAP error: %s' % code
message = "CTAP error: %s" % code
except ValueError:
message = 'CTAP error: 0x%02X' % code
message = "CTAP error: 0x%02X" % code
self.code = code
super(CtapError, self).__init__(message)

View File

@ -40,9 +40,10 @@ import six
@unique
class APDU(IntEnum):
"""APDU response codes."""
OK = 0x9000
USE_NOT_SATISFIED = 0x6985
WRONG_DATA = 0x6a80
WRONG_DATA = 0x6A80
class ApduError(Exception):
@ -53,13 +54,15 @@ class ApduError(Exception):
:param data: APDU response body.
"""
def __init__(self, code, data=b''):
def __init__(self, code, data=b""):
self.code = code
self.data = data
def __repr__(self):
return 'APDU error: 0x{:04X} {:d} bytes of data'.format(
self.code, len(self.data))
return "APDU error: 0x{:04X} {:d} bytes of data".format(
self.code, len(self.data)
)
class RegistrationData(bytes):
@ -72,25 +75,27 @@ class RegistrationData(bytes):
encoded.
:ivar signature: Attestation signature.
"""
def __init__(self, _):
super(RegistrationData, self).__init__()
if six.indexbytes(self, 0) != 0x05:
raise ValueError('Reserved byte != 0x05')
raise ValueError("Reserved byte != 0x05")
self.public_key = self[1:66]
kh_len = six.indexbytes(self, 66)
self.key_handle = self[67:67+kh_len]
self.key_handle = self[67 : 67 + kh_len]
cert_offs = 67 + kh_len
cert_len = six.indexbytes(self, cert_offs + 1)
if cert_len > 0x80:
n_bytes = cert_len - 0x80
cert_len = bytes2int(self[cert_offs+2:cert_offs+2+n_bytes]) \
+ n_bytes
cert_len = (
bytes2int(self[cert_offs + 2 : cert_offs + 2 + n_bytes]) + n_bytes
)
cert_len += 2
self.certificate = self[cert_offs:cert_offs+cert_len]
self.signature = self[cert_offs+cert_len:]
self.certificate = self[cert_offs : cert_offs + cert_len]
self.signature = self[cert_offs + cert_len :]
@property
def b64(self):
@ -105,22 +110,30 @@ class RegistrationData(bytes):
:param client_param: SHA256 hash of the ClientData used for the request.
"""
FidoU2FAttestation.verify_signature(
app_param, client_param, self.key_handle, self.public_key,
self.certificate, self.signature)
app_param,
client_param,
self.key_handle,
self.public_key,
self.certificate,
self.signature,
)
def __repr__(self):
return ("RegistrationData(public_key: h'%s', key_handle: h'%s', "
"certificate: h'%s', signature: h'%s')") % tuple(
b2a_hex(x).decode() for x in (
self.public_key,
self.key_handle,
self.certificate,
self.signature
)
)
return (
"RegistrationData(public_key: h'%s', key_handle: h'%s', "
"certificate: h'%s', signature: h'%s')"
) % tuple(
b2a_hex(x).decode()
for x in (
self.public_key,
self.key_handle,
self.certificate,
self.signature,
)
)
def __str__(self):
return '%r' % self
return "%r" % self
@classmethod
def from_b64(cls, data):
@ -140,11 +153,12 @@ class SignatureData(bytes):
:ivar counter: Signature counter.
:ivar signature: Cryptographic signature.
"""
def __init__(self, _):
super(SignatureData, self).__init__()
self.user_presence = six.indexbytes(self, 0)
self.counter = struct.unpack('>I', self[1:5])[0]
self.counter = struct.unpack(">I", self[1:5])[0]
self.signature = self[5:]
@property
@ -164,12 +178,12 @@ class SignatureData(bytes):
ES256.from_ctap1(public_key).verify(m, self.signature)
def __repr__(self):
return ('SignatureData(user_presence: 0x%02x, counter: %d, '
"signature: h'%s'") % (self.user_presence, self.counter,
b2a_hex(self.signature))
return (
"SignatureData(user_presence: 0x%02x, counter: %d, " "signature: h'%s'"
) % (self.user_presence, self.counter, b2a_hex(self.signature))
def __str__(self):
return '%r' % self
return "%r" % self
@classmethod
def from_b64(cls, data):
@ -186,6 +200,7 @@ class CTAP1(object):
:param device: A CtapHidDevice handle supporting CTAP1.
"""
@unique
class INS(IntEnum):
REGISTER = 0x01
@ -196,7 +211,7 @@ class CTAP1(object):
self.device = device
def send_apdu(self, cla=0, ins=0, p1=0, p2=0, data=b''):
def send_apdu(self, cla=0, ins=0, p1=0, p2=0, data=b""):
"""Packs and sends an APDU for use in CTAP1 commands.
This is a low-level method mainly used internally. Avoid calling it
directly if possible, and use the get_version, register, and
@ -211,13 +226,12 @@ class CTAP1(object):
:raise: ApduError
"""
size = len(data)
size_h = size >> 16 & 0xff
size_l = size & 0xffff
apdu = struct.pack('>BBBBBH', cla, ins, p1, p2, size_h, size_l) \
+ data + b'\0\0'
size_h = size >> 16 & 0xFF
size_l = size & 0xFFFF
apdu = struct.pack(">BBBBBH", cla, ins, p1, p2, size_h, size_l) + data + b"\0\0"
response = self.device.call(CTAPHID.MSG, apdu)
status = struct.unpack('>H', response[-2:])[0]
status = struct.unpack(">H", response[-2:])[0]
data = response[:-2]
if status != APDU.OK:
raise ApduError(status, data)
@ -242,8 +256,7 @@ class CTAP1(object):
response = self.send_apdu(ins=CTAP1.INS.REGISTER, data=data)
return RegistrationData(response)
def authenticate(self, client_param, app_param, key_handle,
check_only=False):
def authenticate(self, client_param, app_param, key_handle, check_only=False):
"""Authenticate a previously registered credential.
:param client_param: SHA256 hash of the ClientData used for the request.
@ -253,8 +266,9 @@ class CTAP1(object):
determine if a key handle is known.
:return: The authentication response from the authenticator.
"""
data = client_param + app_param \
+ struct.pack('>B', len(key_handle)) + key_handle
data = (
client_param + app_param + struct.pack(">B", len(key_handle)) + key_handle
)
p1 = 0x07 if check_only else 0x03
response = self.send_apdu(ins=CTAP1.INS.AUTHENTICATE, p1=p1, data=data)
return SignatureData(response)

View File

@ -89,7 +89,7 @@ class Info(bytes):
MAX_CREDS_IN_LIST = 0x07
MAX_CRED_ID_LENGTH = 0x08
TRANSPORTS = 0x09
ALGORITHMS = 0x0a
ALGORITHMS = 0x0A
@classmethod
def get(cls, key):
@ -101,15 +101,13 @@ class Info(bytes):
def __init__(self, _):
super(Info, self).__init__()
data = dict((Info.KEY.get(k), v) for (k, v) in
cbor.decode(self).items())
data = dict((Info.KEY.get(k), v) for (k, v) in cbor.decode(self).items())
self.versions = data[Info.KEY.VERSIONS]
self.extensions = data.get(Info.KEY.EXTENSIONS, [])
self.aaguid = data[Info.KEY.AAGUID]
self.options = data.get(Info.KEY.OPTIONS, {})
self.max_msg_size = data.get(Info.KEY.MAX_MSG_SIZE, 1024)
self.pin_protocols = data.get(
Info.KEY.PIN_PROTOCOLS, [])
self.pin_protocols = data.get(Info.KEY.PIN_PROTOCOLS, [])
self.max_creds_in_list = data.get(Info.KEY.MAX_CREDS_IN_LIST)
self.max_cred_id_length = data.get(Info.KEY.MAX_CRED_ID_LENGTH)
self.transports = data.get(Info.KEY.TRANSPORTS, [])
@ -117,24 +115,24 @@ class Info(bytes):
self.data = data
def __repr__(self):
r = 'Info(versions: %r' % self.versions
r = "Info(versions: %r" % self.versions
if self.extensions:
r += ', extensions: %r' % self.extensions
r += ', aaguid: %s' % hexstr(self.aaguid)
r += ", extensions: %r" % self.extensions
r += ", aaguid: %s" % hexstr(self.aaguid)
if self.options:
r += ', options: %r' % self.options
r += ', max_message_size: %d' % self.max_msg_size
r += ", options: %r" % self.options
r += ", max_message_size: %d" % self.max_msg_size
if self.pin_protocols:
r += ', pin_protocols: %r' % self.pin_protocols
r += ", pin_protocols: %r" % self.pin_protocols
if self.max_creds_in_list:
r += ', max_credential_count_in_list: %d' % self.max_creds_in_list
r += ", max_credential_count_in_list: %d" % self.max_creds_in_list
if self.max_cred_id_length:
r += ', max_credential_id_length: %d' % self.max_cred_id_length
r += ", max_credential_id_length: %d" % self.max_cred_id_length
if self.transports:
r += ', transports: %r' % self.transports
r += ", transports: %r" % self.transports
if self.algorithms:
r += ', algorithms: %r' % self.algorithms
return r + ')'
r += ", algorithms: %r" % self.algorithms
return r + ")"
def __str__(self):
return self.__repr__()
@ -157,13 +155,12 @@ class AttestedCredentialData(bytes):
self.credential_id = parsed[1]
self.public_key = parsed[2]
if parsed[3]:
raise ValueError('Wrong length')
raise ValueError("Wrong length")
def __repr__(self):
return ('AttestedCredentialData(aaguid: %s, credential_id: %s, '
'public_key: %s') % (hexstr(self.aaguid),
hexstr(self.credential_id),
self.public_key)
return (
"AttestedCredentialData(aaguid: %s, credential_id: %s, " "public_key: %s"
) % (hexstr(self.aaguid), hexstr(self.credential_id), self.public_key)
def __str__(self):
return self.__repr__()
@ -177,9 +174,9 @@ class AttestedCredentialData(bytes):
:return: AAGUID, credential ID, public key, and remaining data.
"""
aaguid = data[:16]
c_len = struct.unpack('>H', data[16:18])[0]
cred_id = data[18:18+c_len]
pub_key, rest = cbor.decode_from(data[18+c_len:])
c_len = struct.unpack(">H", data[16:18])[0]
cred_id = data[18 : 18 + c_len]
pub_key, rest = cbor.decode_from(data[18 + c_len :])
return aaguid, cred_id, CoseKey.parse(pub_key), rest
@classmethod
@ -191,8 +188,12 @@ class AttestedCredentialData(bytes):
:param public_key: A COSE formatted public key.
:return: The attested credential data.
"""
return cls(aaguid + struct.pack('>H', len(credential_id))
+ credential_id + cbor.encode(public_key))
return cls(
aaguid
+ struct.pack(">H", len(credential_id))
+ credential_id
+ cbor.encode(public_key)
)
@classmethod
def unpack_from(cls, data):
@ -219,9 +220,7 @@ class AttestedCredentialData(bytes):
:rtype: AttestedCredentialData
"""
return cls.create(
b'\0'*16, # AAGUID
key_handle,
ES256.from_ctap1(public_key)
b"\0" * 16, key_handle, ES256.from_ctap1(public_key) # AAGUID
)
@ -254,12 +253,11 @@ class AuthenticatorData(bytes):
self.rp_id_hash = self[:32]
self.flags = six.indexbytes(self, 32)
self.counter = struct.unpack('>I', self[33:33+4])[0]
self.counter = struct.unpack(">I", self[33 : 33 + 4])[0]
rest = self[37:]
if self.flags & AuthenticatorData.FLAG.ATTESTED:
self.credential_data, rest = \
AttestedCredentialData.unpack_from(self[37:])
self.credential_data, rest = AttestedCredentialData.unpack_from(self[37:])
else:
self.credential_data = None
@ -269,11 +267,10 @@ class AuthenticatorData(bytes):
self.extensions = None
if rest:
raise ValueError('Wrong length')
raise ValueError("Wrong length")
@classmethod
def create(cls, rp_id_hash, flags, counter, credential_data=b'',
extensions=None):
def create(cls, rp_id_hash, flags, counter, credential_data=b"", extensions=None):
"""Create an AuthenticatorData instance.
:param rp_id_hash: SHA256 hash of the RP ID.
@ -285,8 +282,10 @@ class AuthenticatorData(bytes):
:return: The authenticator data.
"""
return cls(
rp_id_hash + struct.pack('>BI', flags, counter) + credential_data +
(cbor.encode(extensions) if extensions is not None else b'')
rp_id_hash
+ struct.pack(">BI", flags, counter)
+ credential_data
+ (cbor.encode(extensions) if extensions is not None else b"")
)
def is_user_present(self):
@ -322,13 +321,16 @@ class AuthenticatorData(bytes):
return bool(self.flags & AuthenticatorData.FLAG.EXTENSION_DATA)
def __repr__(self):
r = 'AuthenticatorData(rp_id_hash: %s, flags: 0x%02x, counter: %d' %\
(hexstr(self.rp_id_hash), self.flags, self.counter)
r = "AuthenticatorData(rp_id_hash: %s, flags: 0x%02x, counter: %d" % (
hexstr(self.rp_id_hash),
self.flags,
self.counter,
)
if self.credential_data:
r += ', credential_data: %s' % self.credential_data
r += ", credential_data: %s" % self.credential_data
if self.extensions:
r += ', extensions: %s' % self.extensions
return r + ')'
r += ", extensions: %s" % self.extensions
return r + ")"
def __str__(self):
return self.__repr__()
@ -367,7 +369,7 @@ class AttestationObject(bytes):
"""
if isinstance(key, int):
return cls(key)
name = re.sub('([a-z])([A-Z])', r'\1_\2', key).upper()
name = re.sub("([a-z])([A-Z])", r"\1_\2", key).upper()
return getattr(cls, name)
@property
@ -377,25 +379,28 @@ class AttestationObject(bytes):
:return: The Webauthn string used for a key.
:rtype: str
"""
value = ''.join(w.capitalize() for w in self.name.split('_'))
value = "".join(w.capitalize() for w in self.name.split("_"))
return value[0].lower() + value[1:]
def __init__(self, _):
super(AttestationObject, self).__init__()
data = dict((AttestationObject.KEY.for_key(k), v) for (k, v) in
cbor.decode(self).items())
data = dict(
(AttestationObject.KEY.for_key(k), v)
for (k, v) in cbor.decode(self).items()
)
self.fmt = data[AttestationObject.KEY.FMT]
self.auth_data = AuthenticatorData(data[AttestationObject.KEY.AUTH_DATA]
)
self.auth_data = AuthenticatorData(data[AttestationObject.KEY.AUTH_DATA])
data[AttestationObject.KEY.AUTH_DATA] = self.auth_data
self.att_statement = data[
AttestationObject.KEY.ATT_STMT]
self.att_statement = data[AttestationObject.KEY.ATT_STMT]
self.data = data
def __repr__(self):
return 'AttestationObject(fmt: %r, auth_data: %r, att_statement: %r)' %\
(self.fmt, self.auth_data, self.att_statement)
return "AttestationObject(fmt: %r, auth_data: %r, att_statement: %r)" % (
self.fmt,
self.auth_data,
self.att_statement,
)
def __str__(self):
return self.__repr__()
@ -433,14 +438,13 @@ class AttestationObject(bytes):
0x41,
0,
AttestedCredentialData.from_ctap1(
registration.key_handle,
registration.public_key
)
registration.key_handle, registration.public_key
),
),
{ # att_statement
'x5c': [registration.certificate],
'sig': registration.signature
}
"x5c": [registration.certificate],
"sig": registration.signature,
},
)
def with_int_keys(self):
@ -459,8 +463,9 @@ class AttestationObject(bytes):
:return: The attestation object, using str keys.
:rtype: AttestationObject
"""
return AttestationObject(cbor.encode(
dict((k.string_key, v) for k, v in self.data.items())))
return AttestationObject(
cbor.encode(dict((k.string_key, v) for k, v in self.data.items()))
)
class AssertionResponse(bytes):
@ -486,27 +491,27 @@ class AssertionResponse(bytes):
def __init__(self, _):
super(AssertionResponse, self).__init__()
data = dict((AssertionResponse.KEY(k), v) for (k, v) in
cbor.decode(self).items())
self.credential = data.get(
AssertionResponse.KEY.CREDENTIAL)
self.auth_data = AuthenticatorData(
data[AssertionResponse.KEY.AUTH_DATA])
data = dict(
(AssertionResponse.KEY(k), v) for (k, v) in cbor.decode(self).items()
)
self.credential = data.get(AssertionResponse.KEY.CREDENTIAL)
self.auth_data = AuthenticatorData(data[AssertionResponse.KEY.AUTH_DATA])
self.signature = data[AssertionResponse.KEY.SIGNATURE]
self.user = data.get(
AssertionResponse.KEY.USER)
self.number_of_credentials = data.get(
AssertionResponse.KEY.N_CREDS)
self.user = data.get(AssertionResponse.KEY.USER)
self.number_of_credentials = data.get(AssertionResponse.KEY.N_CREDS)
self.data = data
def __repr__(self):
r = 'AssertionResponse(credential: %r, auth_data: %r, signature: %s' %\
(self.credential, self.auth_data, hexstr(self.signature))
r = "AssertionResponse(credential: %r, auth_data: %r, signature: %s" % (
self.credential,
self.auth_data,
hexstr(self.signature),
)
if self.user:
r += ', user: %s' % self.user
r += ", user: %s" % self.user
if self.number_of_credentials is not None:
r += ', number_of_credentials: %d' % self.number_of_credentials
return r + ')'
r += ", number_of_credentials: %d" % self.number_of_credentials
return r + ")"
def __str__(self):
return self.__repr__()
@ -531,8 +536,7 @@ class AssertionResponse(bytes):
:param n_creds: The number of responses available.
:return: The assertion response.
"""
return cls(cbor.encode(args(credential, auth_data, signature, user,
n_creds)))
return cls(cbor.encode(args(credential, auth_data, signature, user, n_creds)))
@classmethod
def from_ctap1(cls, app_param, credential, authentication):
@ -547,11 +551,9 @@ class AssertionResponse(bytes):
return cls.create(
credential,
AuthenticatorData.create(
app_param,
authentication.user_presence & 0x01,
authentication.counter
app_param, authentication.user_presence & 0x01, authentication.counter
),
authentication.signature
authentication.signature,
)
@ -576,12 +578,13 @@ class CTAP2(object):
def __init__(self, device, strict_cbor=True):
if not device.capabilities & CAPABILITY.CBOR:
raise ValueError('Device does not support CTAP2.')
raise ValueError("Device does not support CTAP2.")
self.device = device
self._strict_cbor = strict_cbor
def send_cbor(self, cmd, data=None, timeout=None, parse=cbor.decode,
on_keepalive=None):
def send_cbor(
self, cmd, data=None, timeout=None, parse=cbor.decode, on_keepalive=None
):
"""Sends a CBOR message to the device, and waits for a response.
The optional parameter 'timeout' can either be a numeric time in seconds
@ -598,12 +601,11 @@ class CTAP2(object):
:return: The result of calling the parse function on the response data
(defaults to the CBOR decoded value).
"""
request = struct.pack('>B', cmd)
request = struct.pack(">B", cmd)
if data is not None:
request += cbor.encode(data)
with Timeout(timeout) as event:
response = self.device.call(CTAPHID.CBOR, request, event,
on_keepalive)
response = self.device.call(CTAPHID.CBOR, request, event, on_keepalive)
status = six.indexbytes(response, 0)
if status != 0x00:
raise CtapError(status)
@ -615,15 +617,26 @@ class CTAP2(object):
if expected != enc:
enc_h = b2a_hex(enc)
exp_h = b2a_hex(expected)
raise ValueError('Non-canonical CBOR from Authenticator.\n'
'Got: {}\n'.format(enc_h) +
'Expected: {}'.format(exp_h))
raise ValueError(
"Non-canonical CBOR from Authenticator.\n"
"Got: {}\n".format(enc_h) + "Expected: {}".format(exp_h)
)
return parse(enc)
def make_credential(self, client_data_hash, rp, user, key_params,
exclude_list=None, extensions=None, options=None,
pin_auth=None, pin_protocol=None, timeout=None,
on_keepalive=None):
def make_credential(
self,
client_data_hash,
rp,
user,
key_params,
exclude_list=None,
extensions=None,
options=None,
pin_auth=None,
pin_protocol=None,
timeout=None,
on_keepalive=None,
):
"""CTAP2 makeCredential operation.
:param client_data_hash: SHA256 hash of the ClientData.
@ -641,21 +654,36 @@ class CTAP2(object):
messages from the authenticator.
:return: The new credential.
"""
return self.send_cbor(CTAP2.CMD.MAKE_CREDENTIAL, args(
client_data_hash,
rp,
user,
key_params,
exclude_list,
extensions,
options,
pin_auth,
pin_protocol
), timeout, AttestationObject, on_keepalive)
return self.send_cbor(
CTAP2.CMD.MAKE_CREDENTIAL,
args(
client_data_hash,
rp,
user,
key_params,
exclude_list,
extensions,
options,
pin_auth,
pin_protocol,
),
timeout,
AttestationObject,
on_keepalive,
)
def get_assertion(self, rp_id, client_data_hash, allow_list=None,
extensions=None, options=None, pin_auth=None,
pin_protocol=None, timeout=None, on_keepalive=None):
def get_assertion(
self,
rp_id,
client_data_hash,
allow_list=None,
extensions=None,
options=None,
pin_auth=None,
pin_protocol=None,
timeout=None,
on_keepalive=None,
):
"""CTAP2 getAssertion command.
:param rp_id: The RP ID of the credential.
@ -671,15 +699,21 @@ class CTAP2(object):
messages from the authenticator.
:return: The new assertion.
"""
return self.send_cbor(CTAP2.CMD.GET_ASSERTION, args(
rp_id,
client_data_hash,
allow_list,
extensions,
options,
pin_auth,
pin_protocol
), timeout, AssertionResponse, on_keepalive)
return self.send_cbor(
CTAP2.CMD.GET_ASSERTION,
args(
rp_id,
client_data_hash,
allow_list,
extensions,
options,
pin_auth,
pin_protocol,
),
timeout,
AssertionResponse,
on_keepalive,
)
def get_info(self):
"""CTAP2 getInfo command.
@ -688,8 +722,15 @@ class CTAP2(object):
"""
return self.send_cbor(CTAP2.CMD.GET_INFO, parse=Info)
def client_pin(self, pin_protocol, sub_cmd, key_agreement=None,
pin_auth=None, new_pin_enc=None, pin_hash_enc=None):
def client_pin(
self,
pin_protocol,
sub_cmd,
key_agreement=None,
pin_auth=None,
new_pin_enc=None,
pin_hash_enc=None,
):
"""CTAP2 clientPin command, used for various PIN operations.
:param pin_protocol: The PIN protocol version to use.
@ -700,14 +741,17 @@ class CTAP2(object):
:param pin_hash_enc: The pinHashEnc parameter.
:return: The response of the command, decoded.
"""
return self.send_cbor(CTAP2.CMD.CLIENT_PIN, args(
pin_protocol,
sub_cmd,
key_agreement,
pin_auth,
new_pin_enc,
pin_hash_enc
))
return self.send_cbor(
CTAP2.CMD.CLIENT_PIN,
args(
pin_protocol,
sub_cmd,
key_agreement,
pin_auth,
new_pin_enc,
pin_hash_enc,
),
)
def reset(self, timeout=None, on_keepalive=None):
"""CTAP2 reset command, erases all credentials and PIN.
@ -717,19 +761,18 @@ class CTAP2(object):
:param on_keepalive: Optional callback function to handle keep-alive
messages from the authenticator.
"""
self.send_cbor(CTAP2.CMD.RESET, timeout=timeout,
on_keepalive=on_keepalive)
self.send_cbor(CTAP2.CMD.RESET, timeout=timeout, on_keepalive=on_keepalive)
def get_next_assertion(self):
"""CTAP2 getNextAssertion command.
:return: The next available assertion response.
"""
return self.send_cbor(CTAP2.CMD.GET_NEXT_ASSERTION,
parse=AssertionResponse)
return self.send_cbor(CTAP2.CMD.GET_NEXT_ASSERTION, parse=AssertionResponse)
def credential_mgmt(self, sub_cmd, sub_cmd_params=None, pin_protocol=None,
pin_auth=None):
def credential_mgmt(
self, sub_cmd, sub_cmd_params=None, pin_protocol=None, pin_auth=None
):
"""CTAP2 credentialManagement command, used to manage resident
credentials.
@ -738,12 +781,10 @@ class CTAP2(object):
:param pin_protocol: PIN protocol version used.
:pin_auth:
"""
return self.send_cbor(CTAP2.CMD.CREDENTIAL_MGMT, args(
sub_cmd,
sub_cmd_params,
pin_protocol,
pin_auth
))
return self.send_cbor(
CTAP2.CMD.CREDENTIAL_MGMT,
args(sub_cmd, sub_cmd_params, pin_protocol, pin_auth),
)
def get_assertions(self, *args, **kwargs):
"""Convenience method to get list of assertions.
@ -751,20 +792,22 @@ class CTAP2(object):
See get_assertion and get_assertion_next for details.
"""
first = self.get_assertion(*args, **kwargs)
rest = [self.get_assertion_next()
for _ in range(1, first.number_of_credentials or 1)]
rest = [
self.get_assertion_next()
for _ in range(1, first.number_of_credentials or 1)
]
return [first] + rest
def _pad_pin(pin):
if not isinstance(pin, six.string_types):
raise ValueError('PIN of wrong type, expecting %s' % six.string_types)
raise ValueError("PIN of wrong type, expecting %s" % six.string_types)
if len(pin) < 4:
raise ValueError('PIN must be >= 4 characters')
pin = pin.encode('utf8').ljust(64, b'\0')
pin += b'\0' * (-(len(pin) - 16) % 16)
raise ValueError("PIN must be >= 4 characters")
pin = pin.encode("utf8").ljust(64, b"\0")
pin += b"\0" * (-(len(pin) - 16) % 16)
if len(pin) > 255:
raise ValueError('PIN must be <= 255 bytes')
raise ValueError("PIN must be <= 255 bytes")
return pin
@ -775,8 +818,9 @@ class PinProtocolV1(object):
:cvar VERSION: The version number of the PIV protocol.
:cvar IV: An all-zero IV used for some cryptographic operations.
"""
VERSION = 1
IV = b'\x00' * 16
IV = b"\x00" * 16
@unique
class CMD(IntEnum):
@ -799,15 +843,11 @@ class PinProtocolV1(object):
be = default_backend()
sk = ec.generate_private_key(ec.SECP256R1(), be)
pn = sk.public_key().public_numbers()
key_agreement = {
1: 2,
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32)
}
key_agreement = {1: 2, -1: 1, -2: int2bytes(pn.x, 32), -3: int2bytes(pn.y, 32)}
resp = self.ctap.client_pin(PinProtocolV1.VERSION,
PinProtocolV1.CMD.GET_KEY_AGREEMENT)
resp = self.ctap.client_pin(
PinProtocolV1.VERSION, PinProtocolV1.CMD.GET_KEY_AGREEMENT
)
pk = resp[PinProtocolV1.RESULT.KEY_AGREEMENT]
x = bytes2int(pk[-2])
y = bytes2int(pk[-3])
@ -832,10 +872,12 @@ class PinProtocolV1(object):
enc = cipher.encryptor()
pin_hash_enc = enc.update(pin_hash) + enc.finalize()
resp = self.ctap.client_pin(PinProtocolV1.VERSION,
PinProtocolV1.CMD.GET_PIN_TOKEN,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc)
resp = self.ctap.client_pin(
PinProtocolV1.VERSION,
PinProtocolV1.CMD.GET_PIN_TOKEN,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc,
)
dec = cipher.decryptor()
return dec.update(resp[PinProtocolV1.RESULT.PIN_TOKEN]) + dec.finalize()
@ -844,8 +886,9 @@ class PinProtocolV1(object):
:return: The number or PIN attempts until the authenticator is locked.
"""
resp = self.ctap.client_pin(PinProtocolV1.VERSION,
PinProtocolV1.CMD.GET_RETRIES)
resp = self.ctap.client_pin(
PinProtocolV1.VERSION, PinProtocolV1.CMD.GET_RETRIES
)
return resp[PinProtocolV1.RESULT.RETRIES]
def set_pin(self, pin):
@ -863,10 +906,13 @@ class PinProtocolV1(object):
enc = cipher.encryptor()
pin_enc = enc.update(pin) + enc.finalize()
pin_auth = hmac_sha256(shared_secret, pin_enc)[:16]
self.ctap.client_pin(PinProtocolV1.VERSION, PinProtocolV1.CMD.SET_PIN,
key_agreement=key_agreement,
new_pin_enc=pin_enc,
pin_auth=pin_auth)
self.ctap.client_pin(
PinProtocolV1.VERSION,
PinProtocolV1.CMD.SET_PIN,
key_agreement=key_agreement,
new_pin_enc=pin_enc,
pin_auth=pin_auth,
)
def change_pin(self, old_pin, new_pin):
"""Change the PIN of the authenticator.
@ -887,12 +933,14 @@ class PinProtocolV1(object):
enc = cipher.encryptor()
new_pin_enc = enc.update(new_pin) + enc.finalize()
pin_auth = hmac_sha256(shared_secret, new_pin_enc + pin_hash_enc)[:16]
self.ctap.client_pin(PinProtocolV1.VERSION,
PinProtocolV1.CMD.CHANGE_PIN,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc,
new_pin_enc=new_pin_enc,
pin_auth=pin_auth)
self.ctap.client_pin(
PinProtocolV1.VERSION,
PinProtocolV1.CMD.CHANGE_PIN,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc,
new_pin_enc=new_pin_enc,
pin_auth=pin_auth,
)
class CredentialManagement(object):
@ -929,7 +977,7 @@ class CredentialManagement(object):
CREDENTIAL_ID = 0x07
PUBLIC_KEY = 0x08
TOTAL_CREDENTIALS = 0x09
CRED_PROTECT = 0x0a
CRED_PROTECT = 0x0A
def __init__(self, ctap, pin_protocol, pin_token):
self.ctap = ctap
@ -937,16 +985,13 @@ class CredentialManagement(object):
self.pin_token = pin_token
def _call(self, sub_cmd, params=None, auth=True):
kwargs = {
'sub_cmd': sub_cmd,
'sub_cmd_params': params
}
kwargs = {"sub_cmd": sub_cmd, "sub_cmd_params": params}
if auth:
msg = struct.pack('>B', sub_cmd)
msg = struct.pack(">B", sub_cmd)
if params is not None:
msg += cbor.encode(params)
kwargs['pin_protocol'] = self.pin_protocol
kwargs['pin_auth'] = hmac_sha256(self.pin_token, msg)[:16]
kwargs["pin_protocol"] = self.pin_protocol
kwargs["pin_auth"] = hmac_sha256(self.pin_token, msg)[:16]
return self.ctap.credential_mgmt(**kwargs)
def get_metadata(self):
@ -978,10 +1023,7 @@ class CredentialManagement(object):
:return: A dict containing RP, and RP_ID_HASH.
"""
return self._call(
CredentialManagement.CMD.ENUMERATE_RPS_NEXT,
auth=False
)
return self._call(CredentialManagement.CMD.ENUMERATE_RPS_NEXT, auth=False)
def enumerate_rps(self):
"""Convenience method to enumerate all RPs.
@ -992,11 +1034,7 @@ class CredentialManagement(object):
n_rps = first[CredentialManagement.RESULT.TOTAL_RPS]
if n_rps == 0:
return []
rest = [self.enumerate_rps_next()
for _ in range(
1,
n_rps
)]
rest = [self.enumerate_rps_next() for _ in range(1, n_rps)]
return [first] + rest
def enumerate_creds_begin(self, rp_id_hash):
@ -1012,7 +1050,7 @@ class CredentialManagement(object):
"""
return self._call(
CredentialManagement.CMD.ENUMERATE_CREDS_BEGIN,
{CredentialManagement.SUB_PARAMETER.RP_ID_HASH: rp_id_hash}
{CredentialManagement.SUB_PARAMETER.RP_ID_HASH: rp_id_hash},
)
def enumerate_creds_next(self):
@ -1023,10 +1061,7 @@ class CredentialManagement(object):
:return: A dict containing USER, CREDENTIAL_ID, and PUBLIC_KEY.
"""
return self._call(
CredentialManagement.CMD.ENUMERATE_CREDS_NEXT,
auth=False
)
return self._call(CredentialManagement.CMD.ENUMERATE_CREDS_NEXT, auth=False)
def enumerate_creds(self, *args, **kwargs):
"""Convenience method to enumerate all resident credentials for an RP.
@ -1039,11 +1074,12 @@ class CredentialManagement(object):
if e.code == CtapError.ERR.NO_CREDENTIALS:
return []
raise # Other error
rest = [self.enumerate_creds_next()
for _ in range(
1,
first.get(CredentialManagement.RESULT.TOTAL_CREDENTIALS, 1)
)]
rest = [
self.enumerate_creds_next()
for _ in range(
1, first.get(CredentialManagement.RESULT.TOTAL_CREDENTIALS, 1)
)
]
return [first] + rest
def delete_cred(self, cred_id):
@ -1053,5 +1089,5 @@ class CredentialManagement(object):
"""
return self._call(
CredentialManagement.CMD.DELETE_CREDENTIAL,
{CredentialManagement.SUB_PARAMETER.CREDENTIAL_ID: cred_id}
{CredentialManagement.SUB_PARAMETER.CREDENTIAL_ID: cred_id},
)

View File

@ -91,7 +91,7 @@ class HmacSecretExtension(Extension):
Implements the hmac-secret CTAP2 extension.
"""
NAME = 'hmac-secret'
NAME = "hmac-secret"
SALT_LEN = 32
def __init__(self, ctap):
@ -102,13 +102,13 @@ class HmacSecretExtension(Extension):
def create_result(self, data):
if data is not True:
raise ValueError('hmac-secret extension not supported')
raise ValueError("hmac-secret extension not supported")
def get_data(self, salt1, salt2=b''):
def get_data(self, salt1, salt2=b""):
if len(salt1) != self.SALT_LEN:
raise ValueError('Wrong length for salt1')
raise ValueError("Wrong length for salt1")
if salt2 and len(salt2) != self.SALT_LEN:
raise ValueError('Wrong length for salt2')
raise ValueError("Wrong length for salt2")
key_agreement, shared_secret = self._pin_protocol.get_shared_secret()
self._agreement = key_agreement
@ -120,13 +120,13 @@ class HmacSecretExtension(Extension):
return {
1: key_agreement,
2: salt_enc,
3: hmac_sha256(shared_secret, salt_enc)[:16]
3: hmac_sha256(shared_secret, salt_enc)[:16],
}
def get_result(self, data):
dec = self._pin_protocol._get_cipher(self._secret).decryptor()
salt = dec.update(data) + dec.finalize()
return (
salt[:HmacSecretExtension.SALT_LEN],
salt[HmacSecretExtension.SALT_LEN:]
salt[: HmacSecretExtension.SALT_LEN],
salt[HmacSecretExtension.SALT_LEN :],
)

View File

@ -1,4 +1,3 @@
from __future__ import absolute_import
from .ctap import CtapDevice, CtapError, STATUS
@ -19,8 +18,8 @@ class CTAPHID(IntEnum):
CBOR = 0x10
CANCEL = 0x11
ERROR = 0x3f
KEEPALIVE = 0x3b
ERROR = 0x3F
KEEPALIVE = 0x3B
VENDOR_FIRST = 0x40
@ -62,7 +61,7 @@ class CtapHidDevice(CtapDevice):
self._dev = dev
def __repr__(self):
return 'CtapHidDevice(%s)' % self.descriptor['path']
return "CtapHidDevice(%s)" % self.descriptor["path"]
@property
def version(self):
@ -82,7 +81,7 @@ class CtapHidDevice(CtapDevice):
"""Capabilities supported by the device."""
return self._dev.capabilities
def call(self, cmd, data=b'', event=None, on_keepalive=None):
def call(self, cmd, data=b"", event=None, on_keepalive=None):
event = event or Event()
self._dev.InternalSend(TYPE_INIT | cmd, bytearray(data))
last_ka = None
@ -106,14 +105,14 @@ class CtapHidDevice(CtapDevice):
else:
raise CtapError(CtapError.ERR.INVALID_COMMAND)
self.call(CTAPHID.CANCEL, b'', _SingleEvent())
self.call(CTAPHID.CANCEL, b"", _SingleEvent())
raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL)
def wink(self):
"""Causes the authenticator to blink."""
self.call(CTAPHID.WINK)
def ping(self, msg=b'Hello FIDO'):
def ping(self, msg=b"Hello FIDO"):
"""Sends data to the authenticator, which echoes it back.
:param msg: The data to send.
@ -123,7 +122,7 @@ class CtapHidDevice(CtapDevice):
def lock(self, lock_time=10):
"""Locks the channel."""
self.call(CTAPHID.LOCK, struct.pack('>B', lock_time))
self.call(CTAPHID.LOCK, struct.pack(">B", lock_time))
def close(self):
del self._dev
@ -134,7 +133,7 @@ class CtapHidDevice(CtapDevice):
for d in hidtransport.hid.Enumerate():
if selector(d):
try:
dev = hidtransport.hid.Open(d['path'])
dev = hidtransport.hid.Open(d["path"])
yield cls(d, hidtransport.UsbHidTransport(dev))
except OSError:
# Insufficient permissions to access device

View File

@ -37,7 +37,7 @@ import struct
import six
AID_FIDO = b'\xa0\x00\x00\x06\x47\x2f\x00\x01'
AID_FIDO = b"\xa0\x00\x00\x06\x47\x2f\x00\x01"
SW_SUCCESS = (0x90, 0x00)
SW_UPDATE = (0x91, 0x00)
SW1_MORE_DATA = 0x61
@ -45,6 +45,7 @@ SW1_MORE_DATA = 0x61
class CardSelectException(Exception):
"""can't select u2f/fido2 application on the card"""
pass
@ -60,12 +61,12 @@ class CtapNfcDevice(CtapDevice):
result, sw1, sw2 = self._dev.select_applet(AID_FIDO)
if (sw1, sw2) != SW_SUCCESS:
raise CardSelectException('Select error')
raise CardSelectException("Select error")
if result == b'U2F_V2':
if result == b"U2F_V2":
self._capabilities |= CAPABILITY.NMSG
try: # Probe for CTAP2 by calling GET_INFO
self.call(CTAPHID.CBOR, b'\x04')
self.call(CTAPHID.CBOR, b"\x04")
self._capabilities |= CAPABILITY.CBOR
except CtapError:
pass
@ -75,7 +76,7 @@ class CtapNfcDevice(CtapDevice):
return self._dev
def __repr__(self):
return 'CtapNfcDevice(%s)' % self._dev.reader.name
return "CtapNfcDevice(%s)" % self._dev.reader.name
@property
def version(self):
@ -89,19 +90,19 @@ class CtapNfcDevice(CtapDevice):
"""Capabilities supported by the device."""
return self._capabilities
def _chain_apdus(self, cla, ins, p1, p2, data=b''):
def _chain_apdus(self, cla, ins, p1, p2, data=b""):
while len(data) > 250:
to_send, data = data[:250], data[250:]
header = struct.pack('!BBBBB', 0x90, ins, p1, p2, len(to_send))
header = struct.pack("!BBBBB", 0x90, ins, p1, p2, len(to_send))
resp, sw1, sw2 = self._dev.apdu_exchange(header + to_send)
if (sw1, sw2) != SW_SUCCESS:
return resp, sw1, sw2
apdu = struct.pack('!BBBB', cla, ins, p1, p2)
apdu = struct.pack("!BBBB", cla, ins, p1, p2)
if data:
apdu += struct.pack('!B', len(data)) + data
resp, sw1, sw2 = self._dev.apdu_exchange(apdu + b'\x00')
apdu += struct.pack("!B", len(data)) + data
resp, sw1, sw2 = self._dev.apdu_exchange(apdu + b"\x00")
while sw1 == SW1_MORE_DATA:
apdu = b'\x00\xc0\x00\x00' + struct.pack('!B', sw2) # sw2 == le
apdu = b"\x00\xc0\x00\x00" + struct.pack("!B", sw2) # sw2 == le
lres, sw1, sw2 = self._dev.apdu_exchange(apdu)
resp += lres
return resp, sw1, sw2
@ -109,18 +110,18 @@ class CtapNfcDevice(CtapDevice):
def _call_apdu(self, apdu):
if len(apdu) >= 7 and six.indexbytes(apdu, 4) == 0:
# Extended APDU
data_len = struct.unpack('!H', apdu[5:7])[0]
data = apdu[7:7+data_len]
data_len = struct.unpack("!H", apdu[5:7])[0]
data = apdu[7 : 7 + data_len]
else:
# Short APDU
data_len = six.indexbytes(apdu, 4)
data = apdu[5:5+data_len]
data = apdu[5 : 5 + data_len]
(cla, ins, p1, p2) = six.iterbytes(apdu[:4])
resp, sw1, sw2 = self._chain_apdus(cla, ins, p1, p2, data)
return resp + struct.pack('!BB', sw1, sw2)
return resp + struct.pack("!BB", sw1, sw2)
def _call_cbor(self, data=b'', event=None, on_keepalive=None):
def _call_cbor(self, data=b"", event=None, on_keepalive=None):
event = event or Event()
# NFCCTAP_MSG
resp, sw1, sw2 = self._chain_apdus(0x80, 0x10, 0x80, 0x00, data)
@ -138,7 +139,7 @@ class CtapNfcDevice(CtapDevice):
on_keepalive(ka_status)
# NFCCTAP_GETRESPONSE
resp, sw1, sw2 = self._chain_apdus(0x80, 0x11, 0x00, 0x00, b'')
resp, sw1, sw2 = self._chain_apdus(0x80, 0x11, 0x00, 0x00, b"")
if (sw1, sw2) != SW_SUCCESS:
raise CtapError(CtapError.ERR.OTHER) # TODO: Map from SW error
@ -147,7 +148,7 @@ class CtapNfcDevice(CtapDevice):
raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL)
def call(self, cmd, data=b'', event=None, on_keepalive=None):
def call(self, cmd, data=b"", event=None, on_keepalive=None):
if cmd == CTAPHID.MSG:
return self._call_apdu(data)
elif cmd == CTAPHID.CBOR:
@ -156,7 +157,7 @@ class CtapNfcDevice(CtapDevice):
raise CtapError(CtapError.ERR.INVALID_COMMAND)
@classmethod # selector='CL'
def list_devices(cls, selector='', pcsc_device=PCSCDevice):
def list_devices(cls, selector="", pcsc_device=PCSCDevice):
"""
Returns list of readers in the system. Iterator.
:param selector:

View File

@ -41,7 +41,7 @@ import six
import logging
AID_FIDO = b'\xa0\x00\x00\x06\x47\x2f\x00\x01'
AID_FIDO = b"\xa0\x00\x00\x06\x47\x2f\x00\x01"
SW_SUCCESS = (0x90, 0x00)
SW_UPDATE = (0x91, 0x00)
SW1_MORE_DATA = 0x61
@ -66,14 +66,14 @@ class CtapPcscDevice(CtapDevice):
self._select()
try: # Probe for CTAP2 by calling GET_INFO
self.call(CTAPHID.CBOR, b'\x04')
self.call(CTAPHID.CBOR, b"\x04")
self._capabilities |= CAPABILITY.CBOR
except CtapError:
if self._capabilities == 0:
raise ValueError('Unsupported device')
raise ValueError("Unsupported device")
def __repr__(self):
return 'CtapPcscDevice(%s)' % self._name
return "CtapPcscDevice(%s)" % self._name
@property
def version(self):
@ -98,17 +98,14 @@ class CtapPcscDevice(CtapDevice):
:return: byte string. response from card
"""
logger.debug('apdu %s', b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(
list(six.iterbytes(apdu)),
protocol
)
logger.debug("apdu %s", b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(list(six.iterbytes(apdu)), protocol)
response = bytes(bytearray(resp))
logger.debug('response [0x%04X] %s', sw1 << 8 + sw2, b2a_hex(response))
logger.debug("response [0x%04X] %s", sw1 << 8 + sw2, b2a_hex(response))
return response, sw1, sw2
def control_exchange(self, control_code, control_data=b''):
def control_exchange(self, control_code, control_data=b""):
"""Sends control sequence to reader's driver.
:param control_code: int. code to send to reader driver.
@ -116,45 +113,39 @@ class CtapPcscDevice(CtapDevice):
:return: byte string. response
"""
logger.debug('control %s', b2a_hex(control_data))
response = self._conn.control(
control_code,
list(six.iterbytes(control_data))
)
logger.debug("control %s", b2a_hex(control_data))
response = self._conn.control(control_code, list(six.iterbytes(control_data)))
response = bytes(bytearray(response))
logger.debug('response %s', b2a_hex(response))
logger.debug("response %s", b2a_hex(response))
return response
def _select(self):
apdu = b'\x00\xa4\x04\x00' + struct.pack('!B', len(AID_FIDO)) + AID_FIDO
apdu = b"\x00\xa4\x04\x00" + struct.pack("!B", len(AID_FIDO)) + AID_FIDO
resp, sw1, sw2 = self.apdu_exchange(apdu)
if (sw1, sw2) != SW_SUCCESS:
raise ValueError('FIDO applet selection failure.')
if resp == b'U2F_V2':
raise ValueError("FIDO applet selection failure.")
if resp == b"U2F_V2":
self._capabilities |= 0x08
def _chain_apdus(self, cla, ins, p1, p2, data=b''):
def _chain_apdus(self, cla, ins, p1, p2, data=b""):
if self.use_ext_apdu:
header = struct.pack(
'!BBBBBH', cla, ins, p1, p2, 0x00, len(data))
resp, sw1, sw2 = self.apdu_exchange(
header + data)
header = struct.pack("!BBBBBH", cla, ins, p1, p2, 0x00, len(data))
resp, sw1, sw2 = self.apdu_exchange(header + data)
return resp, sw1, sw2
else:
while len(data) > 250:
to_send, data = data[:250], data[250:]
header = struct.pack('!BBBBB',
0x10 | cla, ins, p1, p2, len(to_send))
header = struct.pack("!BBBBB", 0x10 | cla, ins, p1, p2, len(to_send))
resp, sw1, sw2 = self.apdu_exchange(header + to_send)
if (sw1, sw2) != SW_SUCCESS:
return resp, sw1, sw2
apdu = struct.pack('!BBBB', cla, ins, p1, p2)
apdu = struct.pack("!BBBB", cla, ins, p1, p2)
if data:
apdu += struct.pack('!B', len(data)) + data
resp, sw1, sw2 = self.apdu_exchange(apdu + b'\x00')
apdu += struct.pack("!B", len(data)) + data
resp, sw1, sw2 = self.apdu_exchange(apdu + b"\x00")
while sw1 == SW1_MORE_DATA:
apdu = b'\x00\xc0\x00\x00' + struct.pack('!B', sw2) # sw2 == le
apdu = b"\x00\xc0\x00\x00" + struct.pack("!B", sw2) # sw2 == le
lres, sw1, sw2 = self.apdu_exchange(apdu)
resp += lres
return resp, sw1, sw2
@ -162,18 +153,18 @@ class CtapPcscDevice(CtapDevice):
def _call_apdu(self, apdu):
if len(apdu) >= 7 and six.indexbytes(apdu, 4) == 0:
# Extended APDU
data_len = struct.unpack('!H', apdu[5:7])[0]
data = apdu[7:7+data_len]
data_len = struct.unpack("!H", apdu[5:7])[0]
data = apdu[7 : 7 + data_len]
else:
# Short APDU
data_len = six.indexbytes(apdu, 4)
data = apdu[5:5+data_len]
data = apdu[5 : 5 + data_len]
(cla, ins, p1, p2) = six.iterbytes(apdu[:4])
resp, sw1, sw2 = self._chain_apdus(cla, ins, p1, p2, data)
return resp + struct.pack('!BB', sw1, sw2)
return resp + struct.pack("!BB", sw1, sw2)
def _call_cbor(self, data=b'', event=None, on_keepalive=None):
def _call_cbor(self, data=b"", event=None, on_keepalive=None):
event = event or Event()
# NFCCTAP_MSG
resp, sw1, sw2 = self._chain_apdus(0x80, 0x10, 0x80, 0x00, data)
@ -200,7 +191,7 @@ class CtapPcscDevice(CtapDevice):
raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL)
def call(self, cmd, data=b'', event=None, on_keepalive=None):
def call(self, cmd, data=b"", event=None, on_keepalive=None):
if cmd == CTAPHID.CBOR:
return self._call_cbor(data, event, on_keepalive)
elif cmd == CTAPHID.MSG:
@ -212,13 +203,13 @@ class CtapPcscDevice(CtapDevice):
self._conn.disconnect()
@classmethod
def list_devices(cls, name=''):
def list_devices(cls, name=""):
for reader in _list_readers():
if name in reader.name:
try:
yield cls(reader.createConnection(), reader.name)
except Exception as e:
logger.debug('Error %r', e)
logger.debug("Error %r", e)
def _list_readers():

View File

@ -42,11 +42,13 @@ import six
from six.moves.urllib.parse import urlparse
tld_fname = os.path.join(os.path.dirname(__file__), 'public_suffix_list.dat')
with open(tld_fname, 'rb') as f:
suffixes = [entry for entry in (line.decode('utf8').strip()
for line in f.readlines())
if entry and not entry.startswith('//')]
tld_fname = os.path.join(os.path.dirname(__file__), "public_suffix_list.dat")
with open(tld_fname, "rb") as f:
suffixes = [
entry
for entry in (line.decode("utf8").strip() for line in f.readlines())
if entry and not entry.startswith("//")
]
def verify_rp_id(rp_id, origin):
@ -64,12 +66,12 @@ def verify_rp_id(rp_id, origin):
origin = origin.decode()
url = urlparse(origin)
if url.scheme != 'https':
if url.scheme != "https":
return False
host = url.hostname
if host == rp_id:
return True
if host.endswith('.' + rp_id) and rp_id not in suffixes:
if host.endswith("." + rp_id) and rp_id not in suffixes:
return True
return False
@ -84,6 +86,6 @@ def verify_app_id(app_id, origin):
if isinstance(app_id, six.binary_type):
app_id = app_id.decode()
url = urlparse(app_id)
if url.scheme != 'https':
if url.scheme != "https":
return False
return verify_rp_id(url.hostname, origin)

View File

@ -46,22 +46,22 @@ def _verify_origin_for_rp(rp_id):
@unique
class ATTESTATION(six.text_type, Enum):
NONE = 'none'
INDIRECT = 'indirect'
DIRECT = 'direct'
NONE = "none"
INDIRECT = "indirect"
DIRECT = "direct"
@unique
class USER_VERIFICATION(six.text_type, Enum):
DISCOURAGED = 'discouraged'
PREFERRED = 'preferred'
REQUIRED = 'required'
DISCOURAGED = "discouraged"
PREFERRED = "preferred"
REQUIRED = "required"
@unique
class AUTHENTICATOR_ATTACHMENT(six.text_type, Enum):
PLATFORM = 'platform'
CROSS_PLATFORM = 'cross-platform'
PLATFORM = "platform"
CROSS_PLATFORM = "cross-platform"
class RelyingParty(object):
@ -87,8 +87,11 @@ class RelyingParty(object):
def _default_attestations():
return [cls() for cls in Attestation.__subclasses__()
if getattr(cls, 'FORMAT', 'none') != 'none']
return [
cls()
for cls in Attestation.__subclasses__()
if getattr(cls, "FORMAT", "none") != "none"
]
class Fido2Server(object):
@ -104,11 +107,11 @@ class Fido2Server(object):
"""
def __init__(
self,
rp,
attestation=ATTESTATION.NONE,
verify_origin=None,
attestation_types=None,
self,
rp,
attestation=ATTESTATION.NONE,
verify_origin=None,
attestation_types=None,
):
self.rp = rp
self._verify = verify_origin or _verify_origin_for_rp(rp.ident)
@ -117,9 +120,14 @@ class Fido2Server(object):
self.allowed_algorithms = CoseKey.supported_algorithms()
self._attestation_types = attestation_types or _default_attestations()
def register_begin(self, user, credentials=None, resident_key=False,
user_verification=USER_VERIFICATION.PREFERRED,
authenticator_attachment=None):
def register_begin(
self,
user,
credentials=None,
resident_key=False,
user_verification=USER_VERIFICATION.PREFERRED,
authenticator_attachment=None,
):
"""Return a PublicKeyCredentialCreationOptions registration object and
the internal state dictionary that needs to be passed as is to the
corresponding `register_complete` call.
@ -132,45 +140,42 @@ class Fido2Server(object):
or None to not provide a preference (and get both types).
:return: Registration data, internal state."""
if not self.allowed_algorithms:
raise ValueError('Server has no allowed algorithms.')
raise ValueError("Server has no allowed algorithms.")
uv = USER_VERIFICATION(user_verification)
challenge = os.urandom(32)
# Serialize RP
rp_data = {'id': self.rp.ident, 'name': self.rp.name}
rp_data = {"id": self.rp.ident, "name": self.rp.name}
if self.rp.icon:
rp_data['icon'] = self.rp.icon
rp_data["icon"] = self.rp.icon
authenticator_selection = {
'requireResidentKey': resident_key,
'userVerification': uv
"requireResidentKey": resident_key,
"userVerification": uv,
}
if authenticator_attachment:
authenticator_selection['authenticatorAttachment'] = \
AUTHENTICATOR_ATTACHMENT(authenticator_attachment)
authenticator_selection[
"authenticatorAttachment"
] = AUTHENTICATOR_ATTACHMENT(authenticator_attachment)
data = {
'publicKey': {
'rp': rp_data,
'user': user,
'challenge': challenge,
'pubKeyCredParams': [
{
'type': 'public-key',
'alg': alg
} for alg in self.allowed_algorithms
"publicKey": {
"rp": rp_data,
"user": user,
"challenge": challenge,
"pubKeyCredParams": [
{"type": "public-key", "alg": alg}
for alg in self.allowed_algorithms
],
'excludeCredentials': [
{
'type': 'public-key',
'id': cred.credential_id
} for cred in credentials or []
"excludeCredentials": [
{"type": "public-key", "id": cred.credential_id}
for cred in credentials or []
],
'timeout': int(self.timeout * 1000),
'attestation': self.attestation,
'authenticatorSelection': authenticator_selection
"timeout": int(self.timeout * 1000),
"attestation": self.attestation,
"authenticatorSelection": authenticator_selection,
}
}
@ -187,28 +192,33 @@ class Fido2Server(object):
:param client_data: The client data.
:param attestation_object: The attestation object.
:return: The authenticator data"""
if client_data.get('type') != WEBAUTHN_TYPE.MAKE_CREDENTIAL:
raise ValueError('Incorrect type in ClientData.')
if not self._verify(client_data.get('origin')):
raise ValueError('Invalid origin in ClientData.')
if not constant_time.bytes_eq(websafe_decode(state['challenge']),
client_data.challenge):
raise ValueError('Wrong challenge in response.')
if not constant_time.bytes_eq(self.rp.id_hash,
attestation_object.auth_data.rp_id_hash):
raise ValueError('Wrong RP ID hash in response.')
if client_data.get("type") != WEBAUTHN_TYPE.MAKE_CREDENTIAL:
raise ValueError("Incorrect type in ClientData.")
if not self._verify(client_data.get("origin")):
raise ValueError("Invalid origin in ClientData.")
if not constant_time.bytes_eq(
websafe_decode(state["challenge"]), client_data.challenge
):
raise ValueError("Wrong challenge in response.")
if not constant_time.bytes_eq(
self.rp.id_hash, attestation_object.auth_data.rp_id_hash
):
raise ValueError("Wrong RP ID hash in response.")
if not attestation_object.auth_data.is_user_present():
raise ValueError('User Present flag not set.')
raise ValueError("User Present flag not set.")
if state['user_verification'] is USER_VERIFICATION.REQUIRED and \
not attestation_object.auth_data.is_user_verified():
if (
state["user_verification"] is USER_VERIFICATION.REQUIRED
and not attestation_object.auth_data.is_user_verified()
):
raise ValueError(
'User verification required, but User Verified flag not set.')
"User verification required, but User Verified flag not set."
)
if self.attestation != ATTESTATION.NONE:
att_verifier = UnsupportedAttestation()
for at in self._attestation_types:
if getattr(at, 'FORMAT', None) == attestation_object.fmt:
if getattr(at, "FORMAT", None) == attestation_object.fmt:
att_verifier = at
break
# An unsupported format causes an exception to be thrown, which
@ -217,15 +227,16 @@ class Fido2Server(object):
att_verifier.verify(
attestation_object.att_statement,
attestation_object.auth_data,
client_data.hash
client_data.hash,
)
# We simply ignore attestation if self.attestation == 'none', as not all
# clients strip the attestation.
return attestation_object.auth_data
def authenticate_begin(self, credentials,
user_verification=USER_VERIFICATION.PREFERRED):
def authenticate_begin(
self, credentials, user_verification=USER_VERIFICATION.PREFERRED
):
"""Return a PublicKeyCredentialRequestOptions assertion object and
the internal state dictionary that needs to be passed as is to the
corresponding `authenticate_complete` call.
@ -237,17 +248,15 @@ class Fido2Server(object):
challenge = os.urandom(32)
data = {
'publicKey': {
'rpId': self.rp.ident,
'challenge': challenge,
'allowCredentials': [
{
'type': 'public-key',
'id': cred.credential_id
} for cred in credentials
"publicKey": {
"rpId": self.rp.ident,
"challenge": challenge,
"allowCredentials": [
{"type": "public-key", "id": cred.credential_id}
for cred in credentials
],
'timeout': int(self.timeout * 1000),
'userVerification': uv
"timeout": int(self.timeout * 1000),
"userVerification": uv,
}
}
@ -255,8 +264,9 @@ class Fido2Server(object):
return data, state
def authenticate_complete(self, state, credentials, credential_id,
client_data, auth_data, signature):
def authenticate_complete(
self, state, credentials, credential_id, client_data, auth_data, signature
):
"""Verify the correctness of the assertion data received from
the client.
@ -267,37 +277,39 @@ class Fido2Server(object):
:param client_data: The client data.
:param auth_data: The authenticator data.
:param signature: The signature provided by the client."""
if client_data.get('type') != WEBAUTHN_TYPE.GET_ASSERTION:
raise ValueError('Incorrect type in ClientData.')
if not self._verify(client_data.get('origin')):
raise ValueError('Invalid origin in ClientData.')
if websafe_decode(state['challenge']) != client_data.challenge:
raise ValueError('Wrong challenge in response.')
if client_data.get("type") != WEBAUTHN_TYPE.GET_ASSERTION:
raise ValueError("Incorrect type in ClientData.")
if not self._verify(client_data.get("origin")):
raise ValueError("Invalid origin in ClientData.")
if websafe_decode(state["challenge"]) != client_data.challenge:
raise ValueError("Wrong challenge in response.")
if not constant_time.bytes_eq(self.rp.id_hash, auth_data.rp_id_hash):
raise ValueError('Wrong RP ID hash in response.')
raise ValueError("Wrong RP ID hash in response.")
if not auth_data.is_user_present():
raise ValueError('User Present flag not set.')
raise ValueError("User Present flag not set.")
if state['user_verification'] is USER_VERIFICATION.REQUIRED and \
not auth_data.is_user_verified():
if (
state["user_verification"] is USER_VERIFICATION.REQUIRED
and not auth_data.is_user_verified()
):
raise ValueError(
'User verification required, but user verified flag not set.')
"User verification required, but user verified flag not set."
)
for cred in credentials:
if cred.credential_id == credential_id:
try:
cred.public_key.verify(auth_data + client_data.hash,
signature)
cred.public_key.verify(auth_data + client_data.hash, signature)
except InvalidSignature:
raise ValueError('Invalid signature.')
raise ValueError("Invalid signature.")
return cred
raise ValueError('Unknown credential ID.')
raise ValueError("Unknown credential ID.")
@staticmethod
def _make_internal_state(challenge, user_verification):
return {
'challenge': websafe_encode(challenge),
'user_verification': user_verification,
"challenge": websafe_encode(challenge),
"user_verification": user_verification,
}
@ -314,37 +326,28 @@ class U2FFido2Server(Fido2Server):
For other parameters, see Fido2Server.
"""
def __init__(self, app_id, rp, verify_u2f_origin=None, *args,
**kwargs):
def __init__(self, app_id, rp, verify_u2f_origin=None, *args, **kwargs):
super(U2FFido2Server, self).__init__(rp, *args, **kwargs)
kwargs['attestation_types'] = [FidoU2FAttestation()]
kwargs["attestation_types"] = [FidoU2FAttestation()]
if verify_u2f_origin:
kwargs['verify_origin'] = verify_u2f_origin
kwargs["verify_origin"] = verify_u2f_origin
else:
kwargs['verify_origin'] = lambda o: verify_app_id(app_id, o)
kwargs["verify_origin"] = lambda o: verify_app_id(app_id, o)
self._app_id = app_id
self._app_id_server = Fido2Server(RelyingParty(app_id), *args, **kwargs)
def register_begin(self, *args, **kwargs):
req, state = super(U2FFido2Server, self).register_begin(
*args,
**kwargs
)
req['publicKey'].setdefault('extensions', {})['appidExclude'] = \
self._app_id
req, state = super(U2FFido2Server, self).register_begin(*args, **kwargs)
req["publicKey"].setdefault("extensions", {})["appidExclude"] = self._app_id
return req, state
def authenticate_begin(self, *args, **kwargs):
req, state = super(U2FFido2Server, self).authenticate_begin(
*args,
**kwargs
)
req['publicKey'].setdefault('extensions', {})['appid'] = self._app_id
req, state = super(U2FFido2Server, self).authenticate_begin(*args, **kwargs)
req["publicKey"].setdefault("extensions", {})["appid"] = self._app_id
return req, state
def authenticate_complete(self, *args, **kwargs):
try:
return super(U2FFido2Server, self)\
.authenticate_complete(*args, **kwargs)
return super(U2FFido2Server, self).authenticate_complete(*args, **kwargs)
except ValueError:
return self._app_id_server.authenticate_complete(*args, **kwargs)

View File

@ -39,13 +39,13 @@ from numbers import Number
import six
__all__ = [
'Timeout',
'websafe_encode',
'websafe_decode',
'sha256',
'hmac_sha256',
'bytes2int',
'int2bytes'
"Timeout",
"websafe_encode",
"websafe_decode",
"sha256",
"hmac_sha256",
"bytes2int",
"int2bytes",
]
@ -89,8 +89,8 @@ def int2bytes(value, minlen=-1):
:return: The value encoded as a big endian byte string.
"""
ba = []
while value > 0xff:
ba.append(0xff & value)
while value > 0xFF:
ba.append(0xFF & value)
value >>= 8
ba.append(value)
ba.extend([0] * (minlen - len(ba)))
@ -106,8 +106,8 @@ def websafe_decode(data):
:return: The decoded bytes.
"""
if isinstance(data, six.text_type):
data = data.encode('ascii')
data += b'=' * (-len(data) % 4)
data = data.encode("ascii")
data += b"=" * (-len(data) % 4)
return urlsafe_b64decode(data)
@ -117,7 +117,7 @@ def websafe_encode(data):
:param data: The input to encode.
:return: The encoded string.
"""
return urlsafe_b64encode(data).replace(b'=', b'').decode('ascii')
return urlsafe_b64encode(data).replace(b"=", b"").decode("ascii")
class Timeout(object):
@ -132,8 +132,7 @@ class Timeout(object):
if isinstance(time_or_event, Number):
self.event = Event()
self.timer = Timer(time_or_event,
self.event.set)
self.timer = Timer(time_or_event, self.event.set)
else:
self.event = time_or_event
self.timer = None

View File

@ -2,4 +2,5 @@
universal = 1
[flake8]
max-line-length = 80
max-line-length = 88
ignore = E203, W503

View File

@ -30,57 +30,56 @@ from setuptools import setup, find_packages, __version__
import re
import sys
if StrictVersion(__version__) < StrictVersion('20.2'):
sys.exit('Your setuptools version does not support PEP 508.\n'
'Please install setuptools 20.2 or later.')
if StrictVersion(__version__) < StrictVersion("20.2"):
sys.exit(
"Your setuptools version does not support PEP 508.\n"
"Please install setuptools 20.2 or later."
)
def get_version():
with open('fido2/__init__.py', 'r') as f:
with open("fido2/__init__.py", "r") as f:
match = re.search(r"(?m)^__version__\s*=\s*['\"](.+)['\"]$", f.read())
return match.group(1)
setup(
name='fido2',
name="fido2",
version=get_version(),
packages=find_packages(exclude=['test', 'test.*']),
packages=find_packages(exclude=["test", "test.*"]),
include_package_data=True,
author='Dain Nilsson',
author_email='dain@yubico.com',
description='Python based FIDO 2.0 library',
url='https://github.com/Yubico/python-fido2',
python_requires='>=2.7.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
author="Dain Nilsson",
author_email="dain@yubico.com",
description="Python based FIDO 2.0 library",
url="https://github.com/Yubico/python-fido2",
python_requires=">=2.7.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*",
install_requires=[
'six',
'cryptography>=1.5',
"six",
"cryptography>=1.5",
'uhid-freebsd>=1.2.1;platform_system=="FreeBSD"',
],
extras_require={
':python_version < "3.4"': ['enum34'],
'pcsc': ['pyscard']
},
test_suite='test',
tests_require=['mock>=1.0.1', 'pyfakefs>=3.4;platform_system=="Linux"'],
extras_require={':python_version < "3.4"': ["enum34"], "pcsc": ["pyscard"]},
test_suite="test",
tests_require=["mock>=1.0.1", 'pyfakefs>=3.4;platform_system=="Linux"'],
classifiers=[
'License :: OSI Approved :: BSD License',
'License :: OSI Approved :: Apache Software License',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
'Operating System :: MacOS',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Topic :: Internet',
'Topic :: Security :: Cryptography',
'Topic :: Software Development :: Libraries :: Python Modules',
]
"License :: OSI Approved :: BSD License",
"License :: OSI Approved :: Apache Software License",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Topic :: Internet",
"Topic :: Security :: Cryptography",
"Topic :: Software Development :: Libraries :: Python Modules",
],
)

File diff suppressed because one or more lines are too long

View File

@ -36,24 +36,24 @@ import unittest
_TEST_VECTORS = [
('00', 0),
('01', 1),
('0a', 10),
('17', 23),
('1818', 24),
('1819', 25),
('1864', 100),
('1903e8', 1000),
('1a000f4240', 1000000),
('1b000000e8d4a51000', 1000000000000),
('1bffffffffffffffff', 18446744073709551615),
("00", 0),
("01", 1),
("0a", 10),
("17", 23),
("1818", 24),
("1819", 25),
("1864", 100),
("1903e8", 1000),
("1a000f4240", 1000000),
("1b000000e8d4a51000", 1000000000000),
("1bffffffffffffffff", 18446744073709551615),
# ('c249010000000000000000', 18446744073709551616),
('3bffffffffffffffff', -18446744073709551616),
("3bffffffffffffffff", -18446744073709551616),
# ('c349010000000000000000', -18446744073709551617),
('20', -1),
('29', -10),
('3863', -100),
('3903e7', -1000),
("20", -1),
("29", -10),
("3863", -100),
("3903e7", -1000),
# ('f90000', 0.0),
# ('f98000', -0.0),
# ('f93c00', 1.0),
@ -76,8 +76,8 @@ _TEST_VECTORS = [
# ('fb7ff0000000000000', None),
# ('fb7ff8000000000000', None),
# ('fbfff0000000000000', None),
('f4', False),
('f5', True),
("f4", False),
("f5", True),
# ('f6', None),
# ('f7', None),
# ('f0', None),
@ -89,24 +89,56 @@ _TEST_VECTORS = [
# ('d74401020304', None),
# ('d818456449455446', None),
# ('d82076687474703a2f2f7777772e6578616d706c652e636f6d', None),
('40', b''),
('4401020304', b'\1\2\3\4'),
('60', ''),
('6161', 'a'),
('6449455446', 'IETF'),
('62225c', '"\\'),
('62c3bc', 'ü'),
('63e6b0b4', ''),
('64f0908591', '𐅑'),
('80', []),
('83010203', [1, 2, 3]),
('8301820203820405', [1, [2, 3], [4, 5]]),
('98190102030405060708090a0b0c0d0e0f101112131415161718181819', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]), # noqa
('a0', {}),
('a201020304', {1: 2, 3: 4}),
('a26161016162820203', {'a': 1, 'b': [2, 3]}),
('826161a161626163', ['a', {'b': 'c'}]),
('a56161614161626142616361436164614461656145', {'c': 'C', 'd': 'D', 'a': 'A', 'b': 'B', 'e': 'E'}), # noqa
("40", b""),
("4401020304", b"\1\2\3\4"),
("60", ""),
("6161", "a"),
("6449455446", "IETF"),
("62225c", '"\\'),
("62c3bc", "ü"),
("63e6b0b4", ""),
("64f0908591", "𐅑"),
("80", []),
("83010203", [1, 2, 3]),
("8301820203820405", [1, [2, 3], [4, 5]]),
(
"98190102030405060708090a0b0c0d0e0f101112131415161718181819",
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
],
),
("a0", {}),
("a201020304", {1: 2, 3: 4}),
("a26161016162820203", {"a": 1, "b": [2, 3]}),
("826161a161626163", ["a", {"b": "c"}]),
(
"a56161614161626142616361436164614461656145",
{"c": "C", "d": "D", "a": "A", "b": "B", "e": "E"},
),
# ('5f42010243030405ff', None),
# ('7f657374726561646d696e67ff', 'streaming'),
# ('9fff', []),
@ -114,7 +146,7 @@ _TEST_VECTORS = [
# ('9f01820203820405ff', [1, [2, 3], [4, 5]]),
# ('83018202039f0405ff', [1, [2, 3], [4, 5]]),
# ('83019f0203ff820405', [1, [2, 3], [4, 5]]),
# ('9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]), # noqa
# ('9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]), # noqa E501
# ('bf61610161629f0203ffff', {'a': 1, 'b': [2, 3]}),
# ('826161bf61626163ff', ['a', {'b': 'c'}]),
# ('bf6346756ef563416d7421ff', {'Amt': -2, 'Fun': True}),
@ -134,11 +166,11 @@ class TestCborTestVectors(unittest.TestCase):
def test_vectors(self):
for (data, value) in _TEST_VECTORS:
try:
self.assertEqual(cbor.decode_from(a2b_hex(data)), (value, b''))
self.assertEqual(cbor.decode_from(a2b_hex(data)), (value, b""))
self.assertEqual(cbor.decode(a2b_hex(data)), value)
self.assertEqual(cbor2hex(value), data)
except Exception:
print('\nERROR in test vector, %s' % data)
print("\nERROR in test vector, %s" % data)
raise
@ -149,53 +181,37 @@ class TestFidoCanonical(unittest.TestCase):
"""
def test_integers(self):
self.assertEqual(cbor2hex(0), '00')
self.assertEqual(cbor2hex(0), '00')
self.assertEqual(cbor2hex(23), '17')
self.assertEqual(cbor2hex(24), '1818')
self.assertEqual(cbor2hex(255), '18ff')
self.assertEqual(cbor2hex(256), '190100')
self.assertEqual(cbor2hex(65535), '19ffff')
self.assertEqual(cbor2hex(65536), '1a00010000')
self.assertEqual(cbor2hex(4294967295), '1affffffff')
self.assertEqual(cbor2hex(4294967296), '1b0000000100000000')
self.assertEqual(cbor2hex(-1), '20')
self.assertEqual(cbor2hex(-24), '37')
self.assertEqual(cbor2hex(-25), '3818')
self.assertEqual(cbor2hex(0), "00")
self.assertEqual(cbor2hex(0), "00")
self.assertEqual(cbor2hex(23), "17")
self.assertEqual(cbor2hex(24), "1818")
self.assertEqual(cbor2hex(255), "18ff")
self.assertEqual(cbor2hex(256), "190100")
self.assertEqual(cbor2hex(65535), "19ffff")
self.assertEqual(cbor2hex(65536), "1a00010000")
self.assertEqual(cbor2hex(4294967295), "1affffffff")
self.assertEqual(cbor2hex(4294967296), "1b0000000100000000")
self.assertEqual(cbor2hex(-1), "20")
self.assertEqual(cbor2hex(-24), "37")
self.assertEqual(cbor2hex(-25), "3818")
def test_key_order(self):
self.assertEqual(cbor2hex({
'3': 0,
b'2': 0,
1: 0
}), 'a30100413200613300')
self.assertEqual(cbor2hex({"3": 0, b"2": 0, 1: 0}), "a30100413200613300")
self.assertEqual(cbor2hex({
'3': 0,
b'': 0,
256: 0
}), 'a3190100004000613300')
self.assertEqual(cbor2hex({"3": 0, b"": 0, 256: 0}), "a3190100004000613300")
self.assertEqual(cbor2hex({
4294967296: 0,
255: 0,
256: 0,
0: 0
}), 'a4000018ff00190100001b000000010000000000')
self.assertEqual(
cbor2hex({4294967296: 0, 255: 0, 256: 0, 0: 0}),
"a4000018ff00190100001b000000010000000000",
)
self.assertEqual(cbor2hex({
b'22': 0,
b'3': 0,
b'111': 0
}), 'a3413300423232004331313100')
self.assertEqual(
cbor2hex({b"22": 0, b"3": 0, b"111": 0}), "a3413300423232004331313100"
)
self.assertEqual(cbor2hex({
b'001': 0,
b'003': 0,
b'002': 0
}), 'a3433030310043303032004330303300')
self.assertEqual(
cbor2hex({b"001": 0, b"003": 0, b"002": 0}),
"a3433030310043303032004330303300",
)
self.assertEqual(cbor2hex({
True: 0,
False: 0
}), 'a2f400f500')
self.assertEqual(cbor2hex({True: 0, False: 0}), "a2f400f500")

View File

@ -42,57 +42,72 @@ from fido2.client import ClientData, U2fClient, ClientError, Fido2Client
class TestClientData(unittest.TestCase):
def test_client_data(self):
client_data = ClientData(b'{"typ":"navigator.id.finishEnrollment","challenge":"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}') # noqa
client_data = ClientData(
b'{"typ":"navigator.id.finishEnrollment","challenge":"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}' # noqa E501
)
self.assertEqual(client_data.hash, a2b_hex('4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb')) # noqa
self.assertEqual(client_data.get('origin'), 'http://example.com')
self.assertEqual(
client_data.hash,
a2b_hex("4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"),
)
self.assertEqual(client_data.get("origin"), "http://example.com")
self.assertEqual(client_data, ClientData.from_b64(client_data.b64))
self.assertEqual(client_data.data, {
'typ': 'navigator.id.finishEnrollment',
'challenge': 'vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo',
'cid_pubkey': {
'kty': 'EC',
'crv': 'P-256',
'x': 'HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8',
'y': 'XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4'
self.assertEqual(
client_data.data,
{
"typ": "navigator.id.finishEnrollment",
"challenge": "vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo",
"cid_pubkey": {
"kty": "EC",
"crv": "P-256",
"x": "HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8",
"y": "XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4",
},
"origin": "http://example.com",
},
'origin': 'http://example.com'
})
)
APP_ID = 'https://foo.example.com'
REG_DATA = RegistrationData(a2b_hex(b'0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871')) # noqa
SIG_DATA = SignatureData(a2b_hex(b'0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f')) # noqa
APP_ID = "https://foo.example.com"
REG_DATA = RegistrationData(
a2b_hex(
b"0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" # noqa E501
)
)
SIG_DATA = SignatureData(
a2b_hex(
b"0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f" # noqa E501
)
)
class TestU2fClient(unittest.TestCase):
def test_register_wrong_app_id(self):
client = U2fClient(None, APP_ID)
try:
client.register(
'https://bar.example.com',
[{'version': 'U2F_V2', 'challenge': 'foobar'}],
"https://bar.example.com",
[{"version": "U2F_V2", "challenge": "foobar"}],
[],
timeout=1)
self.fail('register did not raise error')
timeout=1,
)
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.BAD_REQUEST)
def test_register_unsupported_version(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_XXX'
client.ctap.get_version.return_value = "U2F_XXX"
try:
client.register(
APP_ID, [{'version': 'U2F_V2', 'challenge': 'foobar'}], [],
timeout=1)
self.fail('register did not raise error')
APP_ID, [{"version": "U2F_V2", "challenge": "foobar"}], [], timeout=1
)
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
@ -101,35 +116,38 @@ class TestU2fClient(unittest.TestCase):
def test_register_existing_key(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.USE_NOT_SATISFIED)
try:
client.register(
APP_ID, [{'version': 'U2F_V2', 'challenge': 'foobar'}],
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
timeout=1)
self.fail('register did not raise error')
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
timeout=1,
)
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
# Check keyHandle
self.assertEqual(client.ctap.authenticate.call_args[0][2], b'key')
self.assertEqual(client.ctap.authenticate.call_args[0][2], b"key")
# Ensure check-only was set
self.assertTrue(client.ctap.authenticate.call_args[0][3])
def test_register(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.return_value = REG_DATA
resp = client.register(
APP_ID, [{'version': 'U2F_V2', 'challenge': 'foobar'}],
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}]
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
)
client.ctap.get_version.assert_called_with()
@ -137,25 +155,24 @@ class TestU2fClient(unittest.TestCase):
client.ctap.register.assert_called_once()
client_param, app_param = client.ctap.register.call_args[0]
self.assertEqual(sha256(websafe_decode(resp['clientData'])),
client_param)
self.assertEqual(websafe_decode(resp['registrationData']),
REG_DATA)
self.assertEqual(sha256(websafe_decode(resp["clientData"])), client_param)
self.assertEqual(websafe_decode(resp["registrationData"]), REG_DATA)
self.assertEqual(sha256(APP_ID.encode()), app_param)
def test_register_await_timeout(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.side_effect = ApduError(APDU.USE_NOT_SATISFIED)
client.poll_delay = 0.01
try:
client.register(
APP_ID, [{'version': 'U2F_V2', 'challenge': 'foobar'}],
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
timeout=0.1
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
timeout=0.1,
)
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.TIMEOUT)
@ -163,22 +180,23 @@ class TestU2fClient(unittest.TestCase):
def test_register_await_touch(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.side_effect = [
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
REG_DATA
REG_DATA,
]
event = Event()
event.wait = mock.MagicMock()
resp = client.register(
APP_ID, [{'version': 'U2F_V2', 'challenge': 'foobar'}],
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
timeout=event
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
timeout=event,
)
event.wait.assert_called()
@ -188,37 +206,35 @@ class TestU2fClient(unittest.TestCase):
client.ctap.register.assert_called()
client_param, app_param = client.ctap.register.call_args[0]
self.assertEqual(sha256(websafe_decode(resp['clientData'])),
client_param)
self.assertEqual(websafe_decode(resp['registrationData']),
REG_DATA)
self.assertEqual(sha256(websafe_decode(resp["clientData"])), client_param)
self.assertEqual(websafe_decode(resp["registrationData"]), REG_DATA)
self.assertEqual(sha256(APP_ID.encode()), app_param)
def test_sign_wrong_app_id(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
try:
client.sign(
'http://foo.example.com', 'challenge',
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}]
"http://foo.example.com",
"challenge",
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
)
self.fail('sign did not raise error')
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.BAD_REQUEST)
def test_sign_unsupported_version(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_XXX'
client.ctap.get_version.return_value = "U2F_XXX"
try:
client.sign(
APP_ID, 'challenge',
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}]
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
self.fail('sign did not raise error')
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
@ -227,15 +243,14 @@ class TestU2fClient(unittest.TestCase):
def test_sign_missing_key(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
try:
client.sign(
APP_ID, 'challenge',
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
self.fail('sign did not raise error')
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
@ -243,83 +258,81 @@ class TestU2fClient(unittest.TestCase):
client.ctap.authenticate.assert_called_once()
_, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b'key')
self.assertEqual(key_handle, b"key")
def test_sign(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.return_value = SIG_DATA
resp = client.sign(
APP_ID, 'challenge',
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
client_param, app_param, key_handle = \
client.ctap.authenticate.call_args[0]
client_param, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(client_param,
sha256(websafe_decode(resp['clientData'])))
self.assertEqual(client_param, sha256(websafe_decode(resp["clientData"])))
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b'key')
self.assertEqual(websafe_decode(resp['signatureData']),
SIG_DATA)
self.assertEqual(key_handle, b"key")
self.assertEqual(websafe_decode(resp["signatureData"]), SIG_DATA)
def test_sign_await_touch(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = 'U2F_V2'
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = [
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
SIG_DATA
SIG_DATA,
]
event = Event()
event.wait = mock.MagicMock()
resp = client.sign(
APP_ID, 'challenge',
[{'version': 'U2F_V2', 'keyHandle': 'a2V5'}],
timeout=event
APP_ID,
"challenge",
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
timeout=event,
)
event.wait.assert_called()
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called()
client_param, app_param, key_handle = \
client.ctap.authenticate.call_args[0]
client_param, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(client_param,
sha256(websafe_decode(resp['clientData'])))
self.assertEqual(client_param, sha256(websafe_decode(resp["clientData"])))
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b'key')
self.assertEqual(websafe_decode(resp['signatureData']), SIG_DATA)
self.assertEqual(key_handle, b"key")
self.assertEqual(websafe_decode(resp["signatureData"]), SIG_DATA)
rp = {'id': 'example.com', 'name': 'Example RP'}
user = {'id': b'user_id', 'name': 'A. User'}
challenge = 'Y2hhbGxlbmdl'
_INFO_NO_PIN = a2b_hex('a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101') # noqa
_MC_RESP = a2b_hex('a301667061636b6564025900c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000001cf8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a529003a363616c67266373696758483046022100cc1ef43edf07de8f208c21619c78a565ddcf4150766ad58781193be8e0a742ed022100f1ed7c7243e45b7d8e5bda6b1abf10af7391789d1ef21b70bd69fed48dba4cb163783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3') # noqa
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
challenge = "Y2hhbGxlbmdl"
_INFO_NO_PIN = a2b_hex(
"a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101" # noqa E501
)
_MC_RESP = a2b_hex(
"a301667061636b6564025900c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000001cf8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a529003a363616c67266373696758483046022100cc1ef43edf07de8f208c21619c78a565ddcf4150766ad58781193be8e0a742ed022100f1ed7c7243e45b7d8e5bda6b1abf10af7391789d1ef21b70bd69fed48dba4cb163783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3" # noqa E501
)
class TestFido2Client(unittest.TestCase):
def test_ctap1_info(self):
dev = mock.Mock()
dev.capabilities = 0
client = Fido2Client(dev, APP_ID)
self.assertEqual(client.info.versions, ['U2F_V2'])
self.assertEqual(client.info.versions, ["U2F_V2"])
self.assertEqual(client.info.pin_protocols, [])
@mock.patch('fido2.client.CTAP2')
@mock.patch("fido2.client.CTAP2")
def test_make_credential_wrong_app_id(self, PatchedCTAP2):
dev = mock.Mock()
dev.capabilities = CAPABILITY.CBOR
@ -329,41 +342,35 @@ class TestFido2Client(unittest.TestCase):
client = Fido2Client(dev, APP_ID)
try:
client.make_credential(
{'id': 'bar.example.com', 'name': 'Invalid RP'},
{"id": "bar.example.com", "name": "Invalid RP"},
user,
challenge,
timeout=1
timeout=1,
)
self.fail('make_credential did not raise error')
self.fail("make_credential did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.BAD_REQUEST)
@mock.patch('fido2.client.CTAP2')
@mock.patch("fido2.client.CTAP2")
def test_make_credential_existing_key(self, PatchedCTAP2):
dev = mock.Mock()
dev.capabilities = CAPABILITY.CBOR
ctap2 = mock.MagicMock()
ctap2.get_info.return_value = Info(_INFO_NO_PIN)
ctap2.make_credential.side_effect = CtapError(
CtapError.ERR.CREDENTIAL_EXCLUDED)
ctap2.make_credential.side_effect = CtapError(CtapError.ERR.CREDENTIAL_EXCLUDED)
PatchedCTAP2.return_value = ctap2
client = Fido2Client(dev, APP_ID)
try:
client.make_credential(
rp,
user,
challenge,
timeout=1
)
self.fail('make_credential did not raise error')
client.make_credential(rp, user, challenge, timeout=1)
self.fail("make_credential did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
ctap2.get_info.assert_called_with()
ctap2.make_credential.assert_called_once()
@mock.patch('fido2.client.CTAP2')
@mock.patch("fido2.client.CTAP2")
def test_make_credential_ctap2(self, PatchedCTAP2):
dev = mock.Mock()
dev.capabilities = CAPABILITY.CBOR
@ -374,10 +381,7 @@ class TestFido2Client(unittest.TestCase):
client = Fido2Client(dev, APP_ID)
attestation, client_data = client.make_credential(
rp,
user,
challenge,
timeout=1
rp, user, challenge, timeout=1
)
self.assertIsInstance(attestation, AttestationObject)
@ -388,19 +392,19 @@ class TestFido2Client(unittest.TestCase):
client_data.hash,
rp,
user,
[{'type': 'public-key', 'alg': -7}],
[{"type": "public-key", "alg": -7}],
None,
None,
None,
None,
None,
1,
None
None,
)
self.assertEqual(client_data.get('origin'), APP_ID)
self.assertEqual(client_data.get('type'), 'webauthn.create')
self.assertEqual(client_data.get('challenge'), challenge)
self.assertEqual(client_data.get("origin"), APP_ID)
self.assertEqual(client_data.get("type"), "webauthn.create")
self.assertEqual(client_data.get("challenge"), challenge)
def test_make_credential_ctap1(self):
dev = mock.Mock()
@ -408,26 +412,22 @@ class TestFido2Client(unittest.TestCase):
client = Fido2Client(dev, APP_ID)
client.ctap1 = mock.MagicMock()
client.ctap1.get_version.return_value = 'U2F_V2'
client.ctap1.get_version.return_value = "U2F_V2"
client.ctap1.register.return_value = REG_DATA
attestation, client_data = client.make_credential(
rp,
user,
challenge,
timeout=1
rp, user, challenge, timeout=1
)
self.assertIsInstance(attestation, AttestationObject)
self.assertIsInstance(client_data, ClientData)
client.ctap1.register.assert_called_with(
client_data.hash,
sha256(rp['id'].encode()),
client_data.hash, sha256(rp["id"].encode())
)
self.assertEqual(client_data.get('origin'), APP_ID)
self.assertEqual(client_data.get('type'), 'webauthn.create')
self.assertEqual(client_data.get('challenge'), challenge)
self.assertEqual(client_data.get("origin"), APP_ID)
self.assertEqual(client_data.get("type"), "webauthn.create")
self.assertEqual(client_data.get("challenge"), challenge)
self.assertEqual(attestation.fmt, 'fido-u2f')
self.assertEqual(attestation.fmt, "fido-u2f")

View File

@ -36,66 +36,96 @@ from binascii import a2b_hex
import unittest
_ES256_KEY = a2b_hex(b'A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C') # noqa
_RS256_KEY = a2b_hex(b'A401030339010020590100B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD2143010001') # noqa
_EdDSA_KEY = a2b_hex(b'a4010103272006215820ee9b21803405d3cf45601e58b6f4c06ea93862de87d3af903c5870a5016e86f5') # noqa
_ES256_KEY = a2b_hex(
b"A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C" # noqa E501
)
_RS256_KEY = a2b_hex(
b"A401030339010020590100B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD2143010001" # noqa E501
)
_EdDSA_KEY = a2b_hex(
b"a4010103272006215820ee9b21803405d3cf45601e58b6f4c06ea93862de87d3af903c5870a5016e86f5" # noqa E501
)
class TestCoseKey(unittest.TestCase):
def test_ES256_parse_verify(self):
key = CoseKey.parse(cbor.decode(_ES256_KEY))
self.assertIsInstance(key, ES256)
self.assertEqual(key, {
1: 2,
3: -7,
-1: 1,
-2: a2b_hex(b'A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1'), # noqa
-3: a2b_hex(b'FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C') # noqa
})
self.assertEqual(
key,
{
1: 2,
3: -7,
-1: 1,
-2: a2b_hex(
b"A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1"
),
-3: a2b_hex(
b"FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C"
),
},
)
key.verify(
a2b_hex(b'0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002C' + # noqa
b'7B89F12A9088B0F5EE0EF8F6718BCCC374249C31AEEBAEB79BD0450132CD536C'), # noqa
a2b_hex(b'304402202B3933FE954A2D29DE691901EB732535393D4859AAA80D58B08741598109516D0220236FBE6B52326C0A6B1CFDC6BF0A35BDA92A6C2E41E40C3A1643428D820941E0') # noqa
a2b_hex(
b"0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002C" # noqa E501
+ b"7B89F12A9088B0F5EE0EF8F6718BCCC374249C31AEEBAEB79BD0450132CD536C"
),
a2b_hex(
b"304402202B3933FE954A2D29DE691901EB732535393D4859AAA80D58B08741598109516D0220236FBE6B52326C0A6B1CFDC6BF0A35BDA92A6C2E41E40C3A1643428D820941E0" # noqa E501
),
)
def test_RS256_parse_verify(self):
key = CoseKey.parse(cbor.decode(_RS256_KEY))
self.assertIsInstance(key, RS256)
self.assertEqual(key, {
1: 3,
3: -257,
-1: a2b_hex(b'B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD'), # noqa
-2: a2b_hex(b'010001') # noqa
})
self.assertEqual(
key,
{
1: 3,
3: -257,
-1: a2b_hex(
b"B610DCE84B65029FAE24F7BF8A1730D37BC91435642A628E691E9B030BF3F7CEC59FF91CBE82C54DE16C136FA4FA8A58939B5A950B32E03073592FEC8D8B33601C04F70E5E2D5CF7B4E805E1990EA5A86928A1B390EB9026527933ACC03E6E41DC0BE40AA5EB7B9B460743E4DD80895A758FB3F3F794E5E9B8310D3A60C28F2410D95CF6E732749A243A30475267628B456DE770BC2185BBED1D451ECB0062A3D132C0E4D842E0DDF93A444A3EE33A85C2E913156361713155F1F1DC64E8E68ED176466553BBDE669EB82810B104CB4407D32AE6316C3BD6F382EC3AE2C5FD49304986D64D92ED11C25B6C5CF1287233545A987E9A3E169F99790603DBA5C8AD" # noqa E501
),
-2: a2b_hex(b"010001"),
},
)
key.verify(
a2b_hex(b'0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002E' + # noqa
b'CC9340FD84950987BA667DBE9B2C97C7241E15E2B54869A0DD1CE2013C4064B8'), # noqa
a2b_hex(b'071B707D11F0E7F62861DFACA89C4E674321AD8A6E329FDD40C7D6971348FBB0514E7B2B0EFE215BAAC0365C4124A808F8180D6575B710E7C01DAE8F052D0C5A2CE82F487C656E7AD824F3D699BE389ADDDE2CBF39E87A8955E93202BAE8830AB4139A7688DFDAD849F1BB689F3852BA05BED70897553CC44704F6941FD1467AD6A46B4DAB503716D386FE7B398E78E0A5A8C4040539D2C9BFA37E4D94F96091FFD1D194DE2CA58E9124A39757F013801421E09BD261ADA31992A8B0386A80AF51A87BD0CEE8FDAB0D4651477670D4C7B245489BED30A57B83964DB79418D5A4F5F2E5ABCA274426C9F90B007A962AE15DFF7343AF9E110746E2DB9226D785C6') # noqa
a2b_hex(
b"0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000002E" # noqa E501
+ b"CC9340FD84950987BA667DBE9B2C97C7241E15E2B54869A0DD1CE2013C4064B8"
),
a2b_hex(
b"071B707D11F0E7F62861DFACA89C4E674321AD8A6E329FDD40C7D6971348FBB0514E7B2B0EFE215BAAC0365C4124A808F8180D6575B710E7C01DAE8F052D0C5A2CE82F487C656E7AD824F3D699BE389ADDDE2CBF39E87A8955E93202BAE8830AB4139A7688DFDAD849F1BB689F3852BA05BED70897553CC44704F6941FD1467AD6A46B4DAB503716D386FE7B398E78E0A5A8C4040539D2C9BFA37E4D94F96091FFD1D194DE2CA58E9124A39757F013801421E09BD261ADA31992A8B0386A80AF51A87BD0CEE8FDAB0D4651477670D4C7B245489BED30A57B83964DB79418D5A4F5F2E5ABCA274426C9F90B007A962AE15DFF7343AF9E110746E2DB9226D785C6" # noqa E501
),
)
def test_EdDSA_parse_verify(self):
key = CoseKey.parse(cbor.decode(_EdDSA_KEY))
self.assertIsInstance(key, EdDSA)
self.assertEqual(key, {
1: 1,
3: -8,
-1: 6,
-2: a2b_hex('EE9B21803405D3CF45601E58B6F4C06EA93862DE87D3AF903C5870A5016E86F5') # noqa
})
self.assertEqual(
key,
{
1: 1,
3: -8,
-1: 6,
-2: a2b_hex(
"EE9B21803405D3CF45601E58B6F4C06EA93862DE87D3AF903C5870A5016E86F5"
),
},
)
try:
key.verify(
a2b_hex(b'a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947010000000500a11a323057d1103784ddff99a354ddd42348c2f00e88d8977b916cabf92268'), # noqa
a2b_hex(b'e8c927ef1a57c738ff4ba8d6f90e06d837a5219eee47991f96b126b0685d512520c9c2eedebe4b88ff2de2b19cb5f8686efc7c4261e9ed1cb3ac5de50869be0a') # noqa
a2b_hex(
b"a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947010000000500a11a323057d1103784ddff99a354ddd42348c2f00e88d8977b916cabf92268" # noqa E501
),
a2b_hex(
b"e8c927ef1a57c738ff4ba8d6f90e06d837a5219eee47991f96b126b0685d512520c9c2eedebe4b88ff2de2b19cb5f8686efc7c4261e9ed1cb3ac5de50869be0a" # noqa E501
),
)
except UnsupportedAlgorithm:
self.skipTest('EdDSA support missing')
self.skipTest("EdDSA support missing")
def test_unsupported_key(self):
key = CoseKey.parse({1: 4711, 3: 4712, -1: b'123', -2: b'456'})
key = CoseKey.parse({1: 4711, 3: 4712, -1: b"123", -2: b"456"})
self.assertIsInstance(key, UnsupportedKey)
self.assertEqual(key, {
1: 4711,
3: 4712,
-1: b'123',
-2: b'456'
})
self.assertEqual(key, {1: 4711, 3: 4712, -1: b"123", -2: b"456"})

View File

@ -34,82 +34,140 @@ import mock
class TestCTAP1(unittest.TestCase):
def test_send_apdu_ok(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = b'response\x90\x00'
ctap.device.call.return_value = b"response\x90\x00"
self.assertEqual(b'response', ctap.send_apdu(1, 2, 3, 4, b'foobar'))
ctap.device.call.assert_called_with(0x03, b'\1\2\3\4\0\0\6foobar\0\0')
self.assertEqual(b"response", ctap.send_apdu(1, 2, 3, 4, b"foobar"))
ctap.device.call.assert_called_with(0x03, b"\1\2\3\4\0\0\6foobar\0\0")
def test_send_apdu_err(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = b'err\x6a\x80'
ctap.device.call.return_value = b"err\x6a\x80"
try:
ctap.send_apdu(1, 2, 3, 4, b'foobar')
self.fail('send_apdu did not raise error')
ctap.send_apdu(1, 2, 3, 4, b"foobar")
self.fail("send_apdu did not raise error")
except ApduError as e:
self.assertEqual(e.code, 0x6a80)
self.assertEqual(e.data, b'err')
ctap.device.call.assert_called_with(0x03, b'\1\2\3\4\0\0\6foobar\0\0')
self.assertEqual(e.code, 0x6A80)
self.assertEqual(e.data, b"err")
ctap.device.call.assert_called_with(0x03, b"\1\2\3\4\0\0\6foobar\0\0")
def test_get_version(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = b'U2F_V2\x90\x00'
ctap.device.call.return_value = b"U2F_V2\x90\x00"
self.assertEqual('U2F_V2', ctap.get_version())
ctap.device.call.assert_called_with(0x03, b'\0\3\0\0\0\0\0\0\0')
self.assertEqual("U2F_V2", ctap.get_version())
ctap.device.call.assert_called_with(0x03, b"\0\3\0\0\0\0\0\0\0")
def test_register(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = a2b_hex('0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871') + b'\x90\x00' # noqa
ctap.device.call.return_value = (
a2b_hex(
"0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" # noqa E501
)
+ b"\x90\x00"
)
client_param = a2b_hex(b'4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb') # noqa
app_param = a2b_hex(b'f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4') # noqa
client_param = a2b_hex(
b"4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"
)
app_param = a2b_hex(
b"f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4"
)
resp = ctap.register(client_param, app_param)
ctap.device.call.assert_called_with(
0x03,
b'\0\1\0\0\0\0\x40' +
client_param +
app_param +
b'\0\0'
0x03, b"\0\1\0\0\0\0\x40" + client_param + app_param + b"\0\0"
)
self.assertEqual(
resp.public_key,
a2b_hex(
"04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9" # noqa E501
),
)
self.assertEqual(
resp.key_handle,
a2b_hex(
"2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25" # noqa E501
),
)
self.assertEqual(
resp.certificate,
a2b_hex(
"3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df" # noqa E501
),
)
self.assertEqual(
resp.signature,
a2b_hex(
"304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" # noqa E501
),
)
self.assertEqual(resp.public_key, a2b_hex('04b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9')) # noqa
self.assertEqual(resp.key_handle, a2b_hex('2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25')) # noqa
self.assertEqual(resp.certificate, a2b_hex('3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df')) # noqa
self.assertEqual(resp.signature, a2b_hex('304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871')) # noqa
resp.verify(app_param, client_param)
def test_authenticate(self):
ctap = CTAP1(mock.MagicMock())
ctap.device.call.return_value = a2b_hex('0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f') + b'\x90\x00' # noqa
ctap.device.call.return_value = (
a2b_hex(
"0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f" # noqa E501
)
+ b"\x90\x00"
)
client_param = a2b_hex(b'ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57') # noqa
app_param = a2b_hex(b'4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca') # noqa
key_handle = b'\3'*64
client_param = a2b_hex(
b"ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"
)
app_param = a2b_hex(
b"4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca"
)
key_handle = b"\3" * 64
resp = ctap.authenticate(client_param, app_param, key_handle)
ctap.device.call.assert_called_with(0x03, b'\0\2\3\0\0\0\x81' +
client_param + app_param + b'\x40' +
key_handle + b'\0\0')
ctap.device.call.assert_called_with(
0x03,
b"\0\2\3\0\0\0\x81"
+ client_param
+ app_param
+ b"\x40"
+ key_handle
+ b"\0\0",
)
self.assertEqual(resp.user_presence, 1)
self.assertEqual(resp.counter, 1)
self.assertEqual(resp.signature, a2b_hex('304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f')) # noqa
self.assertEqual(
resp.signature,
a2b_hex(
"304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f" # noqa E501
),
)
public_key = a2b_hex(b'04d368f1b665bade3c33a20f1e429c7750d5033660c019119d29aa4ba7abc04aa7c80a46bbe11ca8cb5674d74f31f8a903f6bad105fb6ab74aefef4db8b0025e1d') # noqa
public_key = a2b_hex(
b"04d368f1b665bade3c33a20f1e429c7750d5033660c019119d29aa4ba7abc04aa7c80a46bbe11ca8cb5674d74f31f8a903f6bad105fb6ab74aefef4db8b0025e1d" # noqa E501
)
resp.verify(app_param, client_param, public_key)
key_handle = b'\4'*8
key_handle = b"\4" * 8
ctap.authenticate(client_param, app_param, key_handle)
ctap.device.call.assert_called_with(0x03, b'\0\2\3\0\0\0\x49' +
client_param + app_param + b'\x08' +
key_handle + b'\0\0')
ctap.device.call.assert_called_with(
0x03,
b"\0\2\3\0\0\0\x49"
+ client_param
+ app_param
+ b"\x08"
+ key_handle
+ b"\0\0",
)
ctap.authenticate(client_param, app_param, key_handle, True)
ctap.device.call.assert_called_with(0x03, b'\0\2\7\0\0\0\x49' +
client_param + app_param + b'\x08' +
key_handle + b'\0\0')
ctap.device.call.assert_called_with(
0x03,
b"\0\2\7\0\0\0\x49"
+ client_param
+ app_param
+ b"\x08"
+ key_handle
+ b"\0\0",
)

View File

@ -28,9 +28,15 @@
from __future__ import absolute_import, unicode_literals
from fido2.ctap1 import RegistrationData
from fido2.ctap2 import (CTAP2, PinProtocolV1, Info, AttestedCredentialData,
AuthenticatorData, AttestationObject,
AssertionResponse)
from fido2.ctap2 import (
CTAP2,
PinProtocolV1,
Info,
AttestedCredentialData,
AuthenticatorData,
AttestationObject,
AssertionResponse,
)
from fido2.attestation import Attestation
from fido2 import cbor
from binascii import a2b_hex
@ -40,49 +46,63 @@ from cryptography.hazmat.primitives.asymmetric import ec
import unittest
import mock
_AAGUID = a2b_hex('F8A011F38C0A4D15800617111F9EDC7D')
_INFO = a2b_hex('a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101') # noqa
_INFO_EXTRA_KEY = a2b_hex('A70182665532465F5632684649444F5F325F3002826375766D6B686D61632D7365637265740350F8A011F38C0A4D15800617111F9EDC7D04A462726BF5627570F564706C6174F469636C69656E7450696EF4051904B00681010708') # noqa
_AAGUID = a2b_hex("F8A011F38C0A4D15800617111F9EDC7D")
_INFO = a2b_hex(
"a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101" # noqa E501
)
_INFO_EXTRA_KEY = a2b_hex(
"A70182665532465F5632684649444F5F325F3002826375766D6B686D61632D7365637265740350F8A011F38C0A4D15800617111F9EDC7D04A462726BF5627570F564706C6174F469636C69656E7450696EF4051904B00681010708" # noqa E501
)
class TestInfo(unittest.TestCase):
def test_parse_bytes(self):
info = Info(_INFO)
self.assertEqual(info.versions, ['U2F_V2', 'FIDO_2_0'])
self.assertEqual(info.extensions, ['uvm', 'hmac-secret'])
self.assertEqual(info.versions, ["U2F_V2", "FIDO_2_0"])
self.assertEqual(info.extensions, ["uvm", "hmac-secret"])
self.assertEqual(info.aaguid, _AAGUID)
self.assertEqual(info.options, {
'rk': True,
'up': True,
'plat': False,
'clientPin': False
})
self.assertEqual(
info.options, {"rk": True, "up": True, "plat": False, "clientPin": False}
)
self.assertEqual(info.max_msg_size, 1200)
self.assertEqual(info.pin_protocols, [1])
self.assertEqual(info.data, {
Info.KEY.VERSIONS: ['U2F_V2', 'FIDO_2_0'],
Info.KEY.EXTENSIONS: ['uvm', 'hmac-secret'],
Info.KEY.AAGUID: _AAGUID,
Info.KEY.OPTIONS: {
'clientPin': False,
'plat': False,
'rk': True,
'up': True
self.assertEqual(
info.data,
{
Info.KEY.VERSIONS: ["U2F_V2", "FIDO_2_0"],
Info.KEY.EXTENSIONS: ["uvm", "hmac-secret"],
Info.KEY.AAGUID: _AAGUID,
Info.KEY.OPTIONS: {
"clientPin": False,
"plat": False,
"rk": True,
"up": True,
},
Info.KEY.MAX_MSG_SIZE: 1200,
Info.KEY.PIN_PROTOCOLS: [1],
},
Info.KEY.MAX_MSG_SIZE: 1200,
Info.KEY.PIN_PROTOCOLS: [1]
})
)
def test_info_with_extra_field(self):
info = Info(_INFO_EXTRA_KEY)
self.assertEqual(info.versions, ['U2F_V2', 'FIDO_2_0'])
self.assertEqual(info.versions, ["U2F_V2", "FIDO_2_0"])
self.assertEqual(info.data[7], 8)
_ATT_CRED_DATA = a2b_hex('f8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290') # noqa
_CRED_ID = a2b_hex('fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783') # noqa
_PUB_KEY = {1: 2, 3: -7, -1: 1, -2: a2b_hex('643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf'), -3: a2b_hex('171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290')} # noqa
_ATT_CRED_DATA = a2b_hex(
"f8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290" # noqa E501
)
_CRED_ID = a2b_hex(
"fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783" # noqa E501
)
_PUB_KEY = {
1: 2,
3: -7,
-1: 1,
-2: a2b_hex("643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf"),
-3: a2b_hex("171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a5290"),
}
class TestAttestedCredentialData(unittest.TestCase):
@ -97,9 +117,15 @@ class TestAttestedCredentialData(unittest.TestCase):
self.assertEqual(_ATT_CRED_DATA, data)
_AUTH_DATA_MC = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12410000001CF8A011F38C0A4D15800617111F9EDC7D0040FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783A5010203262001215820643566C206DD00227005FA5DE69320616CA268043A38F08BDE2E9DC45A5CAFAF225820171353B2932434703726AAE579FA6542432861FE591E481EA22D63997E1A5290') # noqa
_AUTH_DATA_GA = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000001D') # noqa
_RP_ID_HASH = a2b_hex('0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12') # noqa
_AUTH_DATA_MC = a2b_hex(
"0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12410000001CF8A011F38C0A4D15800617111F9EDC7D0040FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783A5010203262001215820643566C206DD00227005FA5DE69320616CA268043A38F08BDE2E9DC45A5CAFAF225820171353B2932434703726AAE579FA6542432861FE591E481EA22D63997E1A5290" # noqa E501
)
_AUTH_DATA_GA = a2b_hex(
"0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12010000001D"
)
_RP_ID_HASH = a2b_hex(
"0021F5FC0B85CD22E60623BCD7D1CA48948909249B4776EB515154E57B66AE12"
)
class TestAuthenticatorData(unittest.TestCase):
@ -120,44 +146,72 @@ class TestAuthenticatorData(unittest.TestCase):
self.assertIsNone(data.extensions)
_MC_RESP = a2b_hex('a301667061636b65640258c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000001cf8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a529003a363616c67266373696758483046022100cc1ef43edf07de8f208c21619c78a565ddcf4150766ad58781193be8e0a742ed022100f1ed7c7243e45b7d8e5bda6b1abf10af7391789d1ef21b70bd69fed48dba4cb163783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3') # noqa
_GA_RESP = a2b_hex('a301a26269645840fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b1578364747970656a7075626c69632d6b65790258250021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12010000001d035846304402206765cbf6e871d3af7f01ae96f06b13c90f26f54b905c5166a2c791274fc2397102200b143893586cc799fba4da83b119eaea1bd80ac3ce88fcedb3efbd596a1f4f63') # noqa
_CRED_ID = a2b_hex('FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783') # noqa
_CRED = {'type': 'public-key', 'id': _CRED_ID}
_SIGNATURE = a2b_hex('304402206765CBF6E871D3AF7F01AE96F06B13C90F26F54B905C5166A2C791274FC2397102200B143893586CC799FBA4DA83B119EAEA1BD80AC3CE88FCEDB3EFBD596A1F4F63') # noqa
_MC_RESP = a2b_hex(
"a301667061636b65640258c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12410000001cf8a011f38c0a4d15800617111f9edc7d0040fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b15783a5010203262001215820643566c206dd00227005fa5de69320616ca268043a38f08bde2e9dc45a5cafaf225820171353b2932434703726aae579fa6542432861fe591e481ea22d63997e1a529003a363616c67266373696758483046022100cc1ef43edf07de8f208c21619c78a565ddcf4150766ad58781193be8e0a742ed022100f1ed7c7243e45b7d8e5bda6b1abf10af7391789d1ef21b70bd69fed48dba4cb163783563815901973082019330820138a003020102020900859b726cb24b4c29300a06082a8648ce3d0403023047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e301e170d3136313230343131353530305a170d3236313230323131353530305a3047310b300906035504061302555331143012060355040a0c0b59756269636f205465737431223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3059301306072a8648ce3d020106082a8648ce3d03010703420004ad11eb0e8852e53ad5dfed86b41e6134a18ec4e1af8f221a3c7d6e636c80ea13c3d504ff2e76211bb44525b196c44cb4849979cf6f896ecd2bb860de1bf4376ba30d300b30090603551d1304023000300a06082a8648ce3d0403020349003046022100e9a39f1b03197525f7373e10ce77e78021731b94d0c03f3fda1fd22db3d030e7022100c4faec3445a820cf43129cdb00aabefd9ae2d874f9c5d343cb2f113da23723f3" # noqa E501
)
_GA_RESP = a2b_hex(
"a301a26269645840fe3aac036d14c1e1c65518b698dd1da8f596bc33e11072813466c6bf3845691509b80fb76d59309b8d39e0a93452688f6ca3a39a76f3fc52744fb73948b1578364747970656a7075626c69632d6b65790258250021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae12010000001d035846304402206765cbf6e871d3af7f01ae96f06b13c90f26f54b905c5166a2c791274fc2397102200b143893586cc799fba4da83b119eaea1bd80ac3ce88fcedb3efbd596a1f4f63" # noqa E501
)
_CRED_ID = a2b_hex(
"FE3AAC036D14C1E1C65518B698DD1DA8F596BC33E11072813466C6BF3845691509B80FB76D59309B8D39E0A93452688F6CA3A39A76F3FC52744FB73948B15783" # noqa E501
)
_CRED = {"type": "public-key", "id": _CRED_ID}
_SIGNATURE = a2b_hex(
"304402206765CBF6E871D3AF7F01AE96F06B13C90F26F54B905C5166A2C791274FC2397102200B143893586CC799FBA4DA83B119EAEA1BD80AC3CE88FCEDB3EFBD596A1F4F63" # noqa E501
)
class TestAttestationObject(unittest.TestCase):
def test_string_keys(self):
self.assertEqual(AttestationObject.KEY.FMT.string_key, 'fmt')
self.assertEqual(AttestationObject.KEY.AUTH_DATA.string_key, 'authData')
self.assertEqual(AttestationObject.KEY.ATT_STMT.string_key, 'attStmt')
self.assertEqual(AttestationObject.KEY.FMT.string_key, "fmt")
self.assertEqual(AttestationObject.KEY.AUTH_DATA.string_key, "authData")
self.assertEqual(AttestationObject.KEY.ATT_STMT.string_key, "attStmt")
def test_fido_u2f_attestation(self):
att = AttestationObject.from_ctap1(
a2b_hex(b'1194228DA8FDBDEEFD261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE'), # noqa
RegistrationData(a2b_hex(b'0504E87625896EE4E46DC032766E8087962F36DF9DFE8B567F3763015B1990A60E1427DE612D66418BDA1950581EBC5C8C1DAD710CB14C22F8C97045F4612FB20C91403EBD89BF77EC509755EE9C2635EFAAAC7B2B9C5CEF1736C3717DA48534C8C6B654D7FF945F50B5CC4E78055BDD396B64F78DA2C5F96200CCD415CD08FE4200383082024A30820132A0030201020204046C8822300D06092A864886F70D01010B0500302E312C302A0603550403132359756269636F2055324620526F6F742043412053657269616C203435373230303633313020170D3134303830313030303030305A180F32303530303930343030303030305A302C312A302806035504030C2159756269636F205532462045452053657269616C203234393138323332343737303059301306072A8648CE3D020106082A8648CE3D030107034200043CCAB92CCB97287EE8E639437E21FCD6B6F165B2D5A3F3DB131D31C16B742BB476D8D1E99080EB546C9BBDF556E6210FD42785899E78CC589EBE310F6CDB9FF4A33B3039302206092B0601040182C40A020415312E332E362E312E342E312E34313438322E312E323013060B2B0601040182E51C020101040403020430300D06092A864886F70D01010B050003820101009F9B052248BC4CF42CC5991FCAABAC9B651BBE5BDCDC8EF0AD2C1C1FFB36D18715D42E78B249224F92C7E6E7A05C49F0E7E4C881BF2E94F45E4A21833D7456851D0F6C145A29540C874F3092C934B43D222B8962C0F410CEF1DB75892AF116B44A96F5D35ADEA3822FC7146F6004385BCB69B65C99E7EB6919786703C0D8CD41E8F75CCA44AA8AB725AD8E799FF3A8696A6F1B2656E631B1E40183C08FDA53FA4A8F85A05693944AE179A1339D002D15CABD810090EC722EF5DEF9965A371D415D624B68A2707CAD97BCDD1785AF97E258F33DF56A031AA0356D8E8D5EBCADC74E071636C6B110ACE5CC9B90DFEACAE640FF1BB0F1FE5DB4EFF7A95F060733F530450220324779C68F3380288A1197B6095F7A6EB9B1B1C127F66AE12A99FE8532EC23B9022100E39516AC4D61EE64044D50B415A6A4D4D84BA6D895CB5AB7A1AA7D081DE341FA')) # noqa
a2b_hex(
b"1194228DA8FDBDEEFD261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE"
),
RegistrationData(
a2b_hex(
b"0504E87625896EE4E46DC032766E8087962F36DF9DFE8B567F3763015B1990A60E1427DE612D66418BDA1950581EBC5C8C1DAD710CB14C22F8C97045F4612FB20C91403EBD89BF77EC509755EE9C2635EFAAAC7B2B9C5CEF1736C3717DA48534C8C6B654D7FF945F50B5CC4E78055BDD396B64F78DA2C5F96200CCD415CD08FE4200383082024A30820132A0030201020204046C8822300D06092A864886F70D01010B0500302E312C302A0603550403132359756269636F2055324620526F6F742043412053657269616C203435373230303633313020170D3134303830313030303030305A180F32303530303930343030303030305A302C312A302806035504030C2159756269636F205532462045452053657269616C203234393138323332343737303059301306072A8648CE3D020106082A8648CE3D030107034200043CCAB92CCB97287EE8E639437E21FCD6B6F165B2D5A3F3DB131D31C16B742BB476D8D1E99080EB546C9BBDF556E6210FD42785899E78CC589EBE310F6CDB9FF4A33B3039302206092B0601040182C40A020415312E332E362E312E342E312E34313438322E312E323013060B2B0601040182E51C020101040403020430300D06092A864886F70D01010B050003820101009F9B052248BC4CF42CC5991FCAABAC9B651BBE5BDCDC8EF0AD2C1C1FFB36D18715D42E78B249224F92C7E6E7A05C49F0E7E4C881BF2E94F45E4A21833D7456851D0F6C145A29540C874F3092C934B43D222B8962C0F410CEF1DB75892AF116B44A96F5D35ADEA3822FC7146F6004385BCB69B65C99E7EB6919786703C0D8CD41E8F75CCA44AA8AB725AD8E799FF3A8696A6F1B2656E631B1E40183C08FDA53FA4A8F85A05693944AE179A1339D002D15CABD810090EC722EF5DEF9965A371D415D624B68A2707CAD97BCDD1785AF97E258F33DF56A031AA0356D8E8D5EBCADC74E071636C6B110ACE5CC9B90DFEACAE640FF1BB0F1FE5DB4EFF7A95F060733F530450220324779C68F3380288A1197B6095F7A6EB9B1B1C127F66AE12A99FE8532EC23B9022100E39516AC4D61EE64044D50B415A6A4D4D84BA6D895CB5AB7A1AA7D081DE341FA" # noqa E501
)
),
)
Attestation.for_type(att.fmt)().verify(
att.att_statement,
att.auth_data,
a2b_hex(b'687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141') # noqa
a2b_hex(
b"687134968222EC17202E42505F8ED2B16AE22F16BB05B88C25DB9E602645F141"
),
)
def test_packed_attestation(self):
att = AttestationObject(a2b_hex(b'a301667061636b65640258c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae124100000003f8a011f38c0a4d15800617111f9edc7d004060a386206a3aacecbdbb22d601853d955fdc5d11adfbd1aa6a950d966b348c7663d40173714a9f987df6461beadfb9cd6419ffdfe4d4cf2eec1aa605a4f59bdaa50102032620012158200edb27580389494d74d2373b8f8c2e8b76fa135946d4f30d0e187e120b423349225820e03400d189e85a55de9ab0f538ed60736eb750f5f0306a80060fe1b13010560d03a363616c6726637369675847304502200d15daf337d727ab4719b4027114a2ac43cd565d394ced62c3d9d1d90825f0b3022100989615e7394c87f4ad91f8fdae86f7a3326df332b3633db088aac76bffb9a46b63783563815902bb308202b73082019fa00302010202041d31330d300d06092a864886f70d01010b0500302a3128302606035504030c1f59756269636f2050726576696577204649444f204174746573746174696f6e301e170d3138303332383036333932345a170d3139303332383036333932345a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203438393736333539373059301306072a8648ce3d020106082a8648ce3d030107034200047d71e8367cafd0ea6cf0d61e4c6a416ba5bb6d8fad52db2389ad07969f0f463bfdddddc29d39d3199163ee49575a3336c04b3309d607f6160c81e023373e0197a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e323013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101009b904ceadbe1f1985486fead02baeaa77e5ab4e6e52b7e6a2666a4dc06e241578169193b63dadec5b2b78605a128b2e03f7fe2a98eaeb4219f52220995f400ce15d630cf0598ba662d7162459f1ad1fc623067376d4e4091be65ac1a33d8561b9996c0529ec1816d1710786384d5e8783aa1f7474cb99fe8f5a63a79ff454380361c299d67cb5cc7c79f0d8c09f8849b0500f6d625408c77cbbc26ddee11cb581beb7947137ad4f05aaf38bd98da10042ddcac277604a395a5b3eaa88a5c8bb27ab59c8127d59d6bbba5f11506bf7b75fda7561a0837c46f025fd54dcf1014fc8d17c859507ac57d4b1dea99485df0ba8f34d00103c3eef2ef3bbfec7a6613de')) # noqa
att = AttestationObject(
a2b_hex(
b"a301667061636b65640258c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae124100000003f8a011f38c0a4d15800617111f9edc7d004060a386206a3aacecbdbb22d601853d955fdc5d11adfbd1aa6a950d966b348c7663d40173714a9f987df6461beadfb9cd6419ffdfe4d4cf2eec1aa605a4f59bdaa50102032620012158200edb27580389494d74d2373b8f8c2e8b76fa135946d4f30d0e187e120b423349225820e03400d189e85a55de9ab0f538ed60736eb750f5f0306a80060fe1b13010560d03a363616c6726637369675847304502200d15daf337d727ab4719b4027114a2ac43cd565d394ced62c3d9d1d90825f0b3022100989615e7394c87f4ad91f8fdae86f7a3326df332b3633db088aac76bffb9a46b63783563815902bb308202b73082019fa00302010202041d31330d300d06092a864886f70d01010b0500302a3128302606035504030c1f59756269636f2050726576696577204649444f204174746573746174696f6e301e170d3138303332383036333932345a170d3139303332383036333932345a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203438393736333539373059301306072a8648ce3d020106082a8648ce3d030107034200047d71e8367cafd0ea6cf0d61e4c6a416ba5bb6d8fad52db2389ad07969f0f463bfdddddc29d39d3199163ee49575a3336c04b3309d607f6160c81e023373e0197a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e323013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101009b904ceadbe1f1985486fead02baeaa77e5ab4e6e52b7e6a2666a4dc06e241578169193b63dadec5b2b78605a128b2e03f7fe2a98eaeb4219f52220995f400ce15d630cf0598ba662d7162459f1ad1fc623067376d4e4091be65ac1a33d8561b9996c0529ec1816d1710786384d5e8783aa1f7474cb99fe8f5a63a79ff454380361c299d67cb5cc7c79f0d8c09f8849b0500f6d625408c77cbbc26ddee11cb581beb7947137ad4f05aaf38bd98da10042ddcac277604a395a5b3eaa88a5c8bb27ab59c8127d59d6bbba5f11506bf7b75fda7561a0837c46f025fd54dcf1014fc8d17c859507ac57d4b1dea99485df0ba8f34d00103c3eef2ef3bbfec7a6613de" # noqa E501
)
)
Attestation.for_type(att.fmt)().verify(
att.att_statement,
att.auth_data,
a2b_hex(b'985B6187D042FB1258892ED637CEC88617DDF5F6632351A545617AA2B75261BF') # noqa
a2b_hex(
b"985B6187D042FB1258892ED637CEC88617DDF5F6632351A545617AA2B75261BF"
),
)
def test_different_keys(self):
att = AttestationObject(a2b_hex(b'a363666d74667061636b65646761747453746d74a363616c6726637369675847304502200d15daf337d727ab4719b4027114a2ac43cd565d394ced62c3d9d1d90825f0b3022100989615e7394c87f4ad91f8fdae86f7a3326df332b3633db088aac76bffb9a46b63783563815902bb308202b73082019fa00302010202041d31330d300d06092a864886f70d01010b0500302a3128302606035504030c1f59756269636f2050726576696577204649444f204174746573746174696f6e301e170d3138303332383036333932345a170d3139303332383036333932345a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203438393736333539373059301306072a8648ce3d020106082a8648ce3d030107034200047d71e8367cafd0ea6cf0d61e4c6a416ba5bb6d8fad52db2389ad07969f0f463bfdddddc29d39d3199163ee49575a3336c04b3309d607f6160c81e023373e0197a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e323013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101009b904ceadbe1f1985486fead02baeaa77e5ab4e6e52b7e6a2666a4dc06e241578169193b63dadec5b2b78605a128b2e03f7fe2a98eaeb4219f52220995f400ce15d630cf0598ba662d7162459f1ad1fc623067376d4e4091be65ac1a33d8561b9996c0529ec1816d1710786384d5e8783aa1f7474cb99fe8f5a63a79ff454380361c299d67cb5cc7c79f0d8c09f8849b0500f6d625408c77cbbc26ddee11cb581beb7947137ad4f05aaf38bd98da10042ddcac277604a395a5b3eaa88a5c8bb27ab59c8127d59d6bbba5f11506bf7b75fda7561a0837c46f025fd54dcf1014fc8d17c859507ac57d4b1dea99485df0ba8f34d00103c3eef2ef3bbfec7a6613de68617574684461746158c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae124100000003f8a011f38c0a4d15800617111f9edc7d004060a386206a3aacecbdbb22d601853d955fdc5d11adfbd1aa6a950d966b348c7663d40173714a9f987df6461beadfb9cd6419ffdfe4d4cf2eec1aa605a4f59bdaa50102032620012158200edb27580389494d74d2373b8f8c2e8b76fa135946d4f30d0e187e120b423349225820e03400d189e85a55de9ab0f538ed60736eb750f5f0306a80060fe1b13010560d')) # noqa
att = AttestationObject(
a2b_hex(
b"a363666d74667061636b65646761747453746d74a363616c6726637369675847304502200d15daf337d727ab4719b4027114a2ac43cd565d394ced62c3d9d1d90825f0b3022100989615e7394c87f4ad91f8fdae86f7a3326df332b3633db088aac76bffb9a46b63783563815902bb308202b73082019fa00302010202041d31330d300d06092a864886f70d01010b0500302a3128302606035504030c1f59756269636f2050726576696577204649444f204174746573746174696f6e301e170d3138303332383036333932345a170d3139303332383036333932345a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203438393736333539373059301306072a8648ce3d020106082a8648ce3d030107034200047d71e8367cafd0ea6cf0d61e4c6a416ba5bb6d8fad52db2389ad07969f0f463bfdddddc29d39d3199163ee49575a3336c04b3309d607f6160c81e023373e0197a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e323013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101009b904ceadbe1f1985486fead02baeaa77e5ab4e6e52b7e6a2666a4dc06e241578169193b63dadec5b2b78605a128b2e03f7fe2a98eaeb4219f52220995f400ce15d630cf0598ba662d7162459f1ad1fc623067376d4e4091be65ac1a33d8561b9996c0529ec1816d1710786384d5e8783aa1f7474cb99fe8f5a63a79ff454380361c299d67cb5cc7c79f0d8c09f8849b0500f6d625408c77cbbc26ddee11cb581beb7947137ad4f05aaf38bd98da10042ddcac277604a395a5b3eaa88a5c8bb27ab59c8127d59d6bbba5f11506bf7b75fda7561a0837c46f025fd54dcf1014fc8d17c859507ac57d4b1dea99485df0ba8f34d00103c3eef2ef3bbfec7a6613de68617574684461746158c40021f5fc0b85cd22e60623bcd7d1ca48948909249b4776eb515154e57b66ae124100000003f8a011f38c0a4d15800617111f9edc7d004060a386206a3aacecbdbb22d601853d955fdc5d11adfbd1aa6a950d966b348c7663d40173714a9f987df6461beadfb9cd6419ffdfe4d4cf2eec1aa605a4f59bdaa50102032620012158200edb27580389494d74d2373b8f8c2e8b76fa135946d4f30d0e187e120b423349225820e03400d189e85a55de9ab0f538ed60736eb750f5f0306a80060fe1b13010560d" # noqa E501
)
)
Attestation.for_type(att.fmt)().verify(
att.att_statement,
att.auth_data,
a2b_hex(b'985B6187D042FB1258892ED637CEC88617DDF5F6632351A545617AA2B75261BF') # noqa
a2b_hex(
b"985B6187D042FB1258892ED637CEC88617DDF5F6632351A545617AA2B75261BF"
),
)
att2 = att.with_int_keys()
@ -170,46 +224,44 @@ class TestAttestationObject(unittest.TestCase):
class TestCTAP2(unittest.TestCase):
def test_send_cbor_ok(self):
ctap = CTAP2(mock.MagicMock())
ctap.device.call.return_value = b'\0' + cbor.encode({1: b'response'})
ctap.device.call.return_value = b"\0" + cbor.encode({1: b"response"})
self.assertEqual({1: b'response'}, ctap.send_cbor(2, b'foobar'))
self.assertEqual({1: b"response"}, ctap.send_cbor(2, b"foobar"))
ctap.device.call.assert_called_with(
0x10,
b'\2' + cbor.encode(b'foobar'),
None,
None
0x10, b"\2" + cbor.encode(b"foobar"), None, None
)
def test_get_info(self):
ctap = CTAP2(mock.MagicMock())
ctap.device.call.return_value = b'\0' + _INFO
ctap.device.call.return_value = b"\0" + _INFO
info = ctap.get_info()
ctap.device.call.assert_called_with(0x10, b'\4', None, None)
ctap.device.call.assert_called_with(0x10, b"\4", None, None)
self.assertIsInstance(info, Info)
def test_make_credential(self):
ctap = CTAP2(mock.MagicMock())
ctap.device.call.return_value = b'\0' + _MC_RESP
ctap.device.call.return_value = b"\0" + _MC_RESP
resp = ctap.make_credential(1, 2, 3, 4)
ctap.device.call.assert_called_with(
0x10, b'\1' + cbor.encode({1: 1, 2: 2, 3: 3, 4: 4}), None, None)
0x10, b"\1" + cbor.encode({1: 1, 2: 2, 3: 3, 4: 4}), None, None
)
self.assertIsInstance(resp, AttestationObject)
self.assertEqual(resp, _MC_RESP)
self.assertEqual(resp.fmt, 'packed')
self.assertEqual(resp.fmt, "packed")
self.assertEqual(resp.auth_data, _AUTH_DATA_MC)
self.assertSetEqual(set(resp.att_statement.keys()),
{'alg', 'sig', 'x5c'})
self.assertSetEqual(set(resp.att_statement.keys()), {"alg", "sig", "x5c"})
def test_get_assertion(self):
ctap = CTAP2(mock.MagicMock())
ctap.device.call.return_value = b'\0' + _GA_RESP
ctap.device.call.return_value = b"\0" + _GA_RESP
resp = ctap.get_assertion(1, 2)
ctap.device.call.assert_called_with(
0x10, b'\2' + cbor.encode({1: 1, 2: 2}), None, None)
0x10, b"\2" + cbor.encode({1: 1, 2: 2}), None, None
)
self.assertIsInstance(resp, AssertionResponse)
self.assertEqual(resp, _GA_RESP)
@ -220,37 +272,28 @@ class TestCTAP2(unittest.TestCase):
self.assertIsNone(resp.number_of_credentials)
EC_PRIV = 0x7452e599fee739d8a653f6a507343d12d382249108a651402520b72f24fe7684
EC_PUB_X = a2b_hex('44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F') # noqa
EC_PUB_Y = a2b_hex('EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9') # noqa
DEV_PUB_X = a2b_hex('0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168') # noqa
DEV_PUB_Y = a2b_hex('D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47') # noqa
SHARED = a2b_hex('c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c') # noqa
TOKEN_ENC = a2b_hex('7A9F98E31B77BE90F9C64D12E9635040')
TOKEN = a2b_hex('aff12c6dcfbf9df52f7a09211e8865cd')
PIN_HASH_ENC = a2b_hex('afe8327ce416da8ee3d057589c2ce1a9')
EC_PRIV = 0x7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684
EC_PUB_X = a2b_hex("44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F")
EC_PUB_Y = a2b_hex("EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9")
DEV_PUB_X = a2b_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168")
DEV_PUB_Y = a2b_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47")
SHARED = a2b_hex("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c")
TOKEN_ENC = a2b_hex("7A9F98E31B77BE90F9C64D12E9635040")
TOKEN = a2b_hex("aff12c6dcfbf9df52f7a09211e8865cd")
PIN_HASH_ENC = a2b_hex("afe8327ce416da8ee3d057589c2ce1a9")
class TestPinProtocolV1(unittest.TestCase):
@mock.patch('cryptography.hazmat.primitives.asymmetric.ec.generate_private_key') # noqa
@mock.patch("cryptography.hazmat.primitives.asymmetric.ec.generate_private_key")
def test_establish_shared_secret(self, patched_generate):
prot = PinProtocolV1(mock.MagicMock())
patched_generate.return_value = ec.derive_private_key(
EC_PRIV,
ec.SECP256R1(),
default_backend()
EC_PRIV, ec.SECP256R1(), default_backend()
)
prot.ctap.client_pin.return_value = {
1: {
1: 2,
3: -25,
-1: 1,
-2: DEV_PUB_X,
-3: DEV_PUB_Y
}
1: {1: 2, 3: -25, -1: 1, -2: DEV_PUB_X, -3: DEV_PUB_Y}
}
key_agreement, shared = prot.get_shared_secret()
@ -262,48 +305,51 @@ class TestPinProtocolV1(unittest.TestCase):
def test_get_pin_token(self):
prot = PinProtocolV1(mock.MagicMock())
prot.get_shared_secret = mock.Mock(return_value=({}, SHARED))
prot.ctap.client_pin.return_value = {
2: TOKEN_ENC
}
prot.ctap.client_pin.return_value = {2: TOKEN_ENC}
self.assertEqual(prot.get_pin_token('1234'), TOKEN)
self.assertEqual(prot.get_pin_token("1234"), TOKEN)
prot.ctap.client_pin.assert_called_once()
self.assertEqual(prot.ctap.client_pin.call_args[1]['pin_hash_enc'],
PIN_HASH_ENC)
self.assertEqual(
prot.ctap.client_pin.call_args[1]["pin_hash_enc"], PIN_HASH_ENC
)
def test_set_pin(self):
prot = PinProtocolV1(mock.MagicMock())
prot.get_shared_secret = mock.Mock(return_value=({}, SHARED))
prot.set_pin('1234')
prot.set_pin("1234")
prot.ctap.client_pin.assert_called_with(
1,
3,
key_agreement={},
new_pin_enc=a2b_hex('0222fc42c6dd76a274a7057858b9b29d98e8a722ec2dc6668476168c5320473cec9907b4cd76ce7943c96ba5683943211d84471e64d9c51e54763488cd66526a'), # noqa
pin_auth=a2b_hex('7b40c084ccc5794194189ab57836475f')
new_pin_enc=a2b_hex(
"0222fc42c6dd76a274a7057858b9b29d98e8a722ec2dc6668476168c5320473cec9907b4cd76ce7943c96ba5683943211d84471e64d9c51e54763488cd66526a" # noqa E501
),
pin_auth=a2b_hex("7b40c084ccc5794194189ab57836475f"),
)
def test_change_pin(self):
prot = PinProtocolV1(mock.MagicMock())
prot.get_shared_secret = mock.Mock(return_value=({}, SHARED))
prot.change_pin('1234', '4321')
prot.change_pin("1234", "4321")
prot.ctap.client_pin.assert_called_with(
1,
4,
key_agreement={},
new_pin_enc=a2b_hex('4280e14aac4fcbf02dd079985f0c0ffc9ea7d5f9c173fd1a4c843826f7590cb3c2d080c6923e2fe6d7a52c31ea1309d3fcca3dedae8a2ef14b6330cafc79339e'), # noqa
pin_auth=a2b_hex('fb97e92f3724d7c85e001d7f93e6490a'),
pin_hash_enc=a2b_hex('afe8327ce416da8ee3d057589c2ce1a9')
new_pin_enc=a2b_hex(
"4280e14aac4fcbf02dd079985f0c0ffc9ea7d5f9c173fd1a4c843826f7590cb3c2d080c6923e2fe6d7a52c31ea1309d3fcca3dedae8a2ef14b6330cafc79339e" # noqa E501
),
pin_auth=a2b_hex("fb97e92f3724d7c85e001d7f93e6490a"),
pin_hash_enc=a2b_hex("afe8327ce416da8ee3d057589c2ce1a9"),
)
def test_short_pin(self):
prot = PinProtocolV1(mock.MagicMock())
with self.assertRaises(ValueError):
prot.set_pin('123')
prot.set_pin("123")
def test_long_pin(self):
prot = PinProtocolV1(mock.MagicMock())
with self.assertRaises(ValueError):
prot.set_pin('1'*256)
prot.set_pin("1" * 256)

View File

@ -40,12 +40,12 @@ class HidTest(unittest.TestCase):
assert len(devs) == 1
return devs[0]
except Exception:
self.skipTest('Tests require a single FIDO HID device')
self.skipTest("Tests require a single FIDO HID device")
def test_ping(self):
msg1 = b'hello world!'
msg2 = b' '
msg3 = b''
msg1 = b"hello world!"
msg2 = b" "
msg3 = b""
dev = self.get_device()
self.assertEqual(dev.ping(msg1), msg1)
self.assertEqual(dev.ping(msg2), msg2)
@ -54,10 +54,10 @@ class HidTest(unittest.TestCase):
def test_call_error(self):
dev = mock.Mock()
hid_dev = CtapHidDevice(None, dev)
dev.InternalRecv = mock.Mock(return_value=(0xbf, bytearray([7])))
dev.InternalRecv = mock.Mock(return_value=(0xBF, bytearray([7])))
try:
hid_dev.call(0x01)
self.fail('call did not raise exception')
self.fail("call did not raise exception")
except CtapError as e:
self.assertEqual(e.code, 7)
@ -66,31 +66,33 @@ class HidTest(unittest.TestCase):
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'))
])
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')
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'))
(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(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)]
[mock.call(1), mock.call(0), mock.call(1), mock.call(0)],
)

View File

@ -32,69 +32,54 @@ import mock
import sys
from fido2.hid import CTAPHID
sys.modules['smartcard'] = mock.Mock()
sys.modules['smartcard.Exceptions'] = mock.Mock()
sys.modules['smartcard.System'] = mock.Mock()
sys.modules['smartcard.pcsc'] = mock.Mock()
sys.modules['smartcard.pcsc.PCSCExceptions'] = mock.Mock()
sys.modules['smartcard.pcsc.PCSCContext'] = mock.Mock()
from fido2.pcsc import CtapPcscDevice # noqa
sys.modules["smartcard"] = mock.Mock()
sys.modules["smartcard.Exceptions"] = mock.Mock()
sys.modules["smartcard.System"] = mock.Mock()
sys.modules["smartcard.pcsc"] = mock.Mock()
sys.modules["smartcard.pcsc.PCSCExceptions"] = mock.Mock()
sys.modules["smartcard.pcsc.PCSCContext"] = mock.Mock()
from fido2.pcsc import CtapPcscDevice # noqa E402
class PcscTest(unittest.TestCase):
def test_pcsc_call_cbor(self):
connection = mock.Mock()
connection.transmit.side_effect = [
(b'U2F_V2', 0x90, 0x00),
(b'', 0x90, 0x00)
]
connection.transmit.side_effect = [(b"U2F_V2", 0x90, 0x00), (b"", 0x90, 0x00)]
CtapPcscDevice(connection, 'Mock')
CtapPcscDevice(connection, "Mock")
connection.transmit.assert_called_with(
[0x80, 0x10, 0x80, 0x00, 0x01, 0x04, 0x00],
None
[0x80, 0x10, 0x80, 0x00, 0x01, 0x04, 0x00], None
)
def test_pcsc_call_u2f(self):
connection = mock.Mock()
connection.transmit.side_effect = [
(b'U2F_V2', 0x90, 0x00),
(b'', 0x90, 0x00),
(b'u2f_resp', 0x90, 0x00)
(b"U2F_V2", 0x90, 0x00),
(b"", 0x90, 0x00),
(b"u2f_resp", 0x90, 0x00),
]
dev = CtapPcscDevice(connection, 'Mock')
res = dev.call(CTAPHID.MSG,
b'\x00\x01\x00\x00\x05' +
b'\x01' * 5 +
b'\x00')
dev = CtapPcscDevice(connection, "Mock")
res = dev.call(CTAPHID.MSG, b"\x00\x01\x00\x00\x05" + b"\x01" * 5 + b"\x00")
connection.transmit.assert_called_with(
[0x00, 0x01, 0x00, 0x00, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00],
None
[0x00, 0x01, 0x00, 0x00, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00], None
)
self.assertEqual(res, b'u2f_resp\x90\x00')
self.assertEqual(res, b"u2f_resp\x90\x00")
def test_pcsc_call_version_2(self):
connection = mock.Mock()
connection.transmit.side_effect = [
(b'U2F_V2', 0x90, 0x00),
(b'', 0x90, 0x00),
]
connection.transmit.side_effect = [(b"U2F_V2", 0x90, 0x00), (b"", 0x90, 0x00)]
dev = CtapPcscDevice(connection, 'Mock')
dev = CtapPcscDevice(connection, "Mock")
self.assertEqual(dev.version, 2)
def test_pcsc_call_version_1(self):
connection = mock.Mock()
connection.transmit.side_effect = [
(b'U2F_V2', 0x90, 0x00),
(b'', 0x63, 0x85),
]
connection.transmit.side_effect = [(b"U2F_V2", 0x90, 0x00), (b"", 0x63, 0x85)]
dev = CtapPcscDevice(connection, 'Mock')
dev = CtapPcscDevice(connection, "Mock")
self.assertEqual(dev.version, 1)

View File

@ -34,107 +34,102 @@ import unittest
class TestAppId(unittest.TestCase):
def test_valid_ids(self):
self.assertTrue(verify_app_id('https://example.com',
'https://register.example.com'))
self.assertTrue(verify_app_id('https://example.com',
'https://fido.example.com'))
self.assertTrue(verify_app_id('https://example.com',
'https://www.example.com:444'))
self.assertTrue(
verify_app_id("https://example.com", "https://register.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://fido.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://www.example.com:444")
)
self.assertTrue(verify_app_id(
'https://companyA.hosting.example.com',
'https://fido.companyA.hosting.example.com'
))
self.assertTrue(verify_app_id(
'https://companyA.hosting.example.com',
'https://xyz.companyA.hosting.example.com'
))
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://fido.companyA.hosting.example.com",
)
)
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://xyz.companyA.hosting.example.com",
)
)
def test_valid_ids_mixed_type(self):
self.assertTrue(verify_app_id(b'https://example.com',
'https://register.example.com'))
self.assertTrue(verify_app_id('https://example.com',
b'https://fido.example.com'))
self.assertTrue(verify_app_id(b'https://example.com',
b'https://www.example.com:444'))
self.assertTrue(
verify_app_id(b"https://example.com", "https://register.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", b"https://fido.example.com")
)
self.assertTrue(
verify_app_id(b"https://example.com", b"https://www.example.com:444")
)
def test_invalid_ids(self):
self.assertFalse(verify_app_id('https://example.com',
'http://example.com'))
self.assertFalse(verify_app_id('https://example.com',
'http://www.example.com'))
self.assertFalse(verify_app_id('https://example.com',
'https://example-test.com'))
self.assertFalse(verify_app_id("https://example.com", "http://example.com"))
self.assertFalse(verify_app_id("https://example.com", "http://www.example.com"))
self.assertFalse(
verify_app_id("https://example.com", "https://example-test.com")
)
self.assertFalse(verify_app_id(
'https://companyA.hosting.example.com',
'https://register.example.com'
))
self.assertFalse(verify_app_id(
'https://companyA.hosting.example.com',
'https://companyB.hosting.example.com'
))
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com", "https://register.example.com"
)
)
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com",
"https://companyB.hosting.example.com",
)
)
def test_invalid_ids_mixed_type(self):
self.assertFalse(verify_app_id(b'https://example.com',
'http://example.com'))
self.assertFalse(verify_app_id('https://example.com',
b'http://www.example.com'))
self.assertFalse(verify_app_id(b'https://example.com',
b'https://example-test.com'))
self.assertFalse(verify_app_id(b"https://example.com", "http://example.com"))
self.assertFalse(
verify_app_id("https://example.com", b"http://www.example.com")
)
self.assertFalse(
verify_app_id(b"https://example.com", b"https://example-test.com")
)
def test_effective_tld_names(self):
self.assertFalse(verify_app_id(
'https://appspot.com',
'https://foo.appspot.com'
))
self.assertFalse(verify_app_id(
'https://co.uk',
'https://example.co.uk'
))
self.assertFalse(
verify_app_id("https://appspot.com", "https://foo.appspot.com")
)
self.assertFalse(verify_app_id("https://co.uk", "https://example.co.uk"))
class TestRpId(unittest.TestCase):
def test_valid_ids(self):
self.assertTrue(verify_rp_id('example.com',
'https://register.example.com'))
self.assertTrue(verify_rp_id('example.com',
'https://fido.example.com'))
self.assertTrue(verify_rp_id('example.com',
'https://www.example.com:444'))
self.assertTrue(verify_rp_id("example.com", "https://register.example.com"))
self.assertTrue(verify_rp_id("example.com", "https://fido.example.com"))
self.assertTrue(verify_rp_id("example.com", "https://www.example.com:444"))
def test_valid_ids_mixed_type(self):
self.assertTrue(verify_rp_id(b'example.com',
'https://register.example.com'))
self.assertTrue(verify_rp_id('example.com',
b'https://fido.example.com'))
self.assertTrue(verify_rp_id(b'example.com',
b'https://www.example.com:444'))
self.assertTrue(verify_rp_id(b"example.com", "https://register.example.com"))
self.assertTrue(verify_rp_id("example.com", b"https://fido.example.com"))
self.assertTrue(verify_rp_id(b"example.com", b"https://www.example.com:444"))
def test_invalid_ids(self):
self.assertFalse(verify_rp_id('example.com',
'http://example.com'))
self.assertFalse(verify_rp_id('example.com',
'http://www.example.com'))
self.assertFalse(verify_rp_id('example.com',
'https://example-test.com'))
self.assertFalse(verify_rp_id("example.com", "http://example.com"))
self.assertFalse(verify_rp_id("example.com", "http://www.example.com"))
self.assertFalse(verify_rp_id("example.com", "https://example-test.com"))
self.assertFalse(verify_rp_id(
'companyA.hosting.example.com',
'https://register.example.com'
))
self.assertFalse(verify_rp_id(
'companyA.hosting.example.com',
'https://companyB.hosting.example.com'
))
self.assertFalse(
verify_rp_id("companyA.hosting.example.com", "https://register.example.com")
)
self.assertFalse(
verify_rp_id(
"companyA.hosting.example.com", "https://companyB.hosting.example.com"
)
)
def test_invalid_ids_mixed_type(self):
self.assertFalse(verify_rp_id(b'example.com',
'http://example.com'))
self.assertFalse(verify_rp_id('example.com',
b'http://www.example.com'))
self.assertFalse(verify_rp_id(b'example.com',
b'https://example-test.com'))
self.assertFalse(verify_rp_id(b"example.com", "http://example.com"))
self.assertFalse(verify_rp_id("example.com", b"http://www.example.com"))
self.assertFalse(verify_rp_id(b"example.com", b"https://example-test.com"))

View File

@ -7,127 +7,161 @@ import six
from fido2.client import WEBAUTHN_TYPE, ClientData
from fido2.ctap2 import AttestedCredentialData, AuthenticatorData
from fido2.server import USER_VERIFICATION, Fido2Server, RelyingParty,\
U2FFido2Server
from fido2.server import USER_VERIFICATION, Fido2Server, RelyingParty, U2FFido2Server
from .test_ctap2 import _ATT_CRED_DATA, _CRED_ID
from .utils import U2FDevice
class TestRelyingParty(unittest.TestCase):
def test_id_hash(self):
rp = RelyingParty('example.com')
rp_id_hash = (b'\xa3y\xa6\xf6\xee\xaf\xb9\xa5^7\x8c\x11\x804\xe2u\x1eh/'
b'\xab\x9f-0\xab\x13\xd2\x12U\x86\xce\x19G')
rp = RelyingParty("example.com")
rp_id_hash = (
b"\xa3y\xa6\xf6\xee\xaf\xb9\xa5^7\x8c\x11\x804\xe2u\x1eh/"
b"\xab\x9f-0\xab\x13\xd2\x12U\x86\xce\x19G"
)
self.assertEqual(rp.id_hash, rp_id_hash)
class TestFido2Server(unittest.TestCase):
def test_register_begin_rp_no_icon(self):
rp = RelyingParty('example.com', 'Example')
rp = RelyingParty("example.com", "Example")
server = Fido2Server(rp)
request, state = server.register_begin({})
self.assertEqual(request['publicKey']['rp'],
{'id': 'example.com', 'name': 'Example'})
self.assertEqual(
request["publicKey"]["rp"], {"id": "example.com", "name": "Example"}
)
def test_register_begin_rp_icon(self):
rp = RelyingParty('example.com', 'Example',
'http://example.com/icon.svg')
rp = RelyingParty("example.com", "Example", "http://example.com/icon.svg")
server = Fido2Server(rp)
request, state = server.register_begin({})
data = {'id': 'example.com', 'name': 'Example',
'icon': 'http://example.com/icon.svg'}
self.assertEqual(request['publicKey']['rp'], data)
data = {
"id": "example.com",
"name": "Example",
"icon": "http://example.com/icon.svg",
}
self.assertEqual(request["publicKey"]["rp"], data)
def test_authenticate_complete_invalid_signature(self):
rp = RelyingParty('example.com', 'Example')
rp = RelyingParty("example.com", "Example")
server = Fido2Server(rp)
state = {'challenge': 'GAZPACHO!',
'user_verification': USER_VERIFICATION.PREFERRED}
client_data_dict = {'challenge': 'GAZPACHO!',
'origin': 'https://example.com',
'type': WEBAUTHN_TYPE.GET_ASSERTION}
client_data = ClientData(json.dumps(client_data_dict).encode('utf-8'))
_AUTH_DATA = a2b_hex('A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947010000001D') # noqa
with six.assertRaisesRegex(self, ValueError, 'Invalid signature.'):
state = {
"challenge": "GAZPACHO!",
"user_verification": USER_VERIFICATION.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
_AUTH_DATA = a2b_hex(
"A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947010000001D"
)
with six.assertRaisesRegex(self, ValueError, "Invalid signature."):
server.authenticate_complete(
state, [AttestedCredentialData(_ATT_CRED_DATA)], _CRED_ID,
client_data, AuthenticatorData(_AUTH_DATA), b'INVALID')
state,
[AttestedCredentialData(_ATT_CRED_DATA)],
_CRED_ID,
client_data,
AuthenticatorData(_AUTH_DATA),
b"INVALID",
)
class TestU2FFido2Server(unittest.TestCase):
def test_u2f(self):
rp = RelyingParty('example.com', 'Example',
'http://example.com/icon.svg')
app_id = b'https://example.com'
server = U2FFido2Server(app_id=app_id.decode('ascii'), rp=rp)
rp = RelyingParty("example.com", "Example", "http://example.com/icon.svg")
app_id = b"https://example.com"
server = U2FFido2Server(app_id=app_id.decode("ascii"), rp=rp)
state = {'challenge': 'GAZPACHO!',
'user_verification': USER_VERIFICATION.PREFERRED}
client_data_dict = {'challenge': 'GAZPACHO!',
'origin': 'https://example.com',
'type': WEBAUTHN_TYPE.GET_ASSERTION}
client_data = ClientData(json.dumps(client_data_dict).encode('utf-8'))
state = {
"challenge": "GAZPACHO!",
"user_verification": USER_VERIFICATION.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
param = b'TOMATO GIVES '
param = b"TOMATO GIVES "
device = U2FDevice(param, app_id)
auth_data = AttestedCredentialData.from_ctap1(
param, device.public_key_bytes)
auth_data = AttestedCredentialData.from_ctap1(param, device.public_key_bytes)
authenticator_data, signature = device.sign(client_data)
server.authenticate_complete(
state, [auth_data], device.credential_id,
client_data, authenticator_data, signature)
state,
[auth_data],
device.credential_id,
client_data,
authenticator_data,
signature,
)
def test_u2f_facets(self):
rp = RelyingParty('example.com', 'Example',
'http://example.com/icon.svg')
app_id = b'https://www.example.com/facets.json'
rp = RelyingParty("example.com", "Example", "http://example.com/icon.svg")
app_id = b"https://www.example.com/facets.json"
def verify_u2f_origin(origin):
return origin in (
'https://oauth.example.com',
'https://admin.example.com'
)
server = U2FFido2Server(app_id=app_id.decode('ascii'), rp=rp,
verify_u2f_origin=verify_u2f_origin)
return origin in ("https://oauth.example.com", "https://admin.example.com")
state = {'challenge': 'GAZPACHO!',
'user_verification': USER_VERIFICATION.PREFERRED}
client_data_dict = {'challenge': 'GAZPACHO!',
'origin': 'https://oauth.example.com',
'type': WEBAUTHN_TYPE.GET_ASSERTION}
client_data = ClientData(json.dumps(client_data_dict).encode('utf-8'))
server = U2FFido2Server(
app_id=app_id.decode("ascii"), rp=rp, verify_u2f_origin=verify_u2f_origin
)
param = b'TOMATO GIVES '
state = {
"challenge": "GAZPACHO!",
"user_verification": USER_VERIFICATION.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://oauth.example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
param = b"TOMATO GIVES "
device = U2FDevice(param, app_id)
auth_data = AttestedCredentialData.from_ctap1(
param, device.public_key_bytes)
auth_data = AttestedCredentialData.from_ctap1(param, device.public_key_bytes)
authenticator_data, signature = device.sign(client_data)
server.authenticate_complete(
state, [auth_data], device.credential_id,
client_data, authenticator_data, signature)
state,
[auth_data],
device.credential_id,
client_data,
authenticator_data,
signature,
)
# Now with something not whitelisted
client_data_dict = {'challenge': 'GAZPACHO!',
'origin': 'https://publicthingy.example.com',
'type': WEBAUTHN_TYPE.GET_ASSERTION}
client_data = ClientData(json.dumps(client_data_dict).encode('utf-8'))
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://publicthingy.example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
authenticator_data, signature = device.sign(client_data)
with six.assertRaisesRegex(self, ValueError, 'Invalid origin in '
'ClientData.'):
with six.assertRaisesRegex(
self, ValueError, "Invalid origin in " "ClientData."
):
server.authenticate_complete(
state, [auth_data], device.credential_id,
client_data, authenticator_data, signature)
state,
[auth_data],
device.credential_id,
client_data,
authenticator_data,
signature,
)

View File

@ -32,64 +32,69 @@ import unittest
from binascii import a2b_hex
from threading import Event
from fido2.utils import (
Timeout,
hmac_sha256,
sha256,
websafe_encode,
websafe_decode
)
from fido2.utils import Timeout, hmac_sha256, sha256, websafe_encode, websafe_decode
class TestSha256(unittest.TestCase):
def test_sha256_vectors(self):
self.assertEqual(sha256(b'abc'), a2b_hex(b'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')) # noqa
self.assertEqual(sha256(b'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'), a2b_hex(b'248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1')) # noqa
self.assertEqual(
sha256(b"abc"),
a2b_hex(
b"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
),
)
self.assertEqual(
sha256(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
a2b_hex(
b"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
),
)
class TestHmacSha256(unittest.TestCase):
def test_hmac_sha256_vectors(self):
self.assertEqual(hmac_sha256(
b'\x0b'*20,
b'Hi There'
), a2b_hex(b'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7')) # noqa
self.assertEqual(
hmac_sha256(b"\x0b" * 20, b"Hi There"),
a2b_hex(
b"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
),
)
self.assertEqual(hmac_sha256(
b'Jefe',
b'what do ya want for nothing?'
), a2b_hex(b'5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843')) # noqa
self.assertEqual(
hmac_sha256(b"Jefe", b"what do ya want for nothing?"),
a2b_hex(
b"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
),
)
class TestWebSafe(unittest.TestCase):
# Base64 vectors adapted from https://tools.ietf.org/html/rfc4648#section-10
def test_websafe_decode(self):
self.assertEqual(websafe_decode(b''), b'')
self.assertEqual(websafe_decode(b'Zg'), b'f')
self.assertEqual(websafe_decode(b'Zm8'), b'fo')
self.assertEqual(websafe_decode(b'Zm9v'), b'foo')
self.assertEqual(websafe_decode(b'Zm9vYg'), b'foob')
self.assertEqual(websafe_decode(b'Zm9vYmE'), b'fooba')
self.assertEqual(websafe_decode(b'Zm9vYmFy'), b'foobar')
self.assertEqual(websafe_decode(b""), b"")
self.assertEqual(websafe_decode(b"Zg"), b"f")
self.assertEqual(websafe_decode(b"Zm8"), b"fo")
self.assertEqual(websafe_decode(b"Zm9v"), b"foo")
self.assertEqual(websafe_decode(b"Zm9vYg"), b"foob")
self.assertEqual(websafe_decode(b"Zm9vYmE"), b"fooba")
self.assertEqual(websafe_decode(b"Zm9vYmFy"), b"foobar")
def test_websafe_decode_unicode(self):
self.assertEqual(websafe_decode(u''), b'')
self.assertEqual(websafe_decode(u'Zm9vYmFy'), b'foobar')
self.assertEqual(websafe_decode(u""), b"")
self.assertEqual(websafe_decode(u"Zm9vYmFy"), b"foobar")
def test_websafe_encode(self):
self.assertEqual(websafe_encode(b''), u'')
self.assertEqual(websafe_encode(b'f'), u'Zg')
self.assertEqual(websafe_encode(b'fo'), u'Zm8')
self.assertEqual(websafe_encode(b'foo'), u'Zm9v')
self.assertEqual(websafe_encode(b'foob'), u'Zm9vYg')
self.assertEqual(websafe_encode(b'fooba'), u'Zm9vYmE')
self.assertEqual(websafe_encode(b'foobar'), u'Zm9vYmFy')
self.assertEqual(websafe_encode(b""), u"")
self.assertEqual(websafe_encode(b"f"), u"Zg")
self.assertEqual(websafe_encode(b"fo"), u"Zm8")
self.assertEqual(websafe_encode(b"foo"), u"Zm9v")
self.assertEqual(websafe_encode(b"foob"), u"Zm9vYg")
self.assertEqual(websafe_encode(b"fooba"), u"Zm9vYmE")
self.assertEqual(websafe_encode(b"foobar"), u"Zm9vYmFy")
class TestTimeout(unittest.TestCase):
def test_event(self):
event = Event()
timeout = Timeout(event)

View File

@ -12,7 +12,9 @@ from fido2.ctap2 import AuthenticatorData
class U2FDevice(object):
_priv_key_bytes = a2b_hex('308184020100301006072a8648ce3d020106052b8104000a046d306b02010104201672f5ceb963e07d475f5db9a975b7ad42ac3bf71ccddfb6539cc651a1156a6ba144034200045a4be44eb94eebff3ed665dde31deb74a060fabd214c5f5713aa043efa805dac8f45e0e17afe2eafbd1802c413c1e485fd854af9f06ef20938398f6795f12e0e') # noqa
_priv_key_bytes = a2b_hex(
"308184020100301006072a8648ce3d020106052b8104000a046d306b02010104201672f5ceb963e07d475f5db9a975b7ad42ac3bf71ccddfb6539cc651a1156a6ba144034200045a4be44eb94eebff3ed665dde31deb74a060fabd214c5f5713aa043efa805dac8f45e0e17afe2eafbd1802c413c1e485fd854af9f06ef20938398f6795f12e0e" # noqa E501
)
def __init__(self, credential_id, app_id):
assert isinstance(credential_id, six.binary_type)
@ -28,23 +30,22 @@ class U2FDevice(object):
self.app_id = app_id
self.priv_key = ec.derive_private_key(
bytes2int(priv_key_params), ec.SECP256R1(),
default_backend())
bytes2int(priv_key_params), ec.SECP256R1(), default_backend()
)
self.pub_key = self.priv_key.public_key()
self.public_key_bytes = self.pub_key.public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.UncompressedPoint)
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
)
self.credential_id = self.key_handle = credential_id
def sign(self, client_data):
authenticator_data = AuthenticatorData.create(
sha256(self.app_id),
flags=AuthenticatorData.FLAG.USER_PRESENT, counter=0)
sha256(self.app_id), flags=AuthenticatorData.FLAG.USER_PRESENT, counter=0
)
signature = self.priv_key.sign(
authenticator_data + client_data.hash,
ec.ECDSA(hashes.SHA256())
authenticator_data + client_data.hash, ec.ECDSA(hashes.SHA256())
)
return authenticator_data, signature