mirror of https://github.com/Yubico/python-fido2
Reformat with Black.
This commit is contained in:
parent
09db42e08e
commit
226b0021f1
|
@ -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/.*'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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!")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
368
fido2/client.py
368
fido2/client.py
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
434
fido2/ctap2.py
434
fido2/ctap2.py
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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 :],
|
||||
)
|
||||
|
|
17
fido2/hid.py
17
fido2/hid.py
|
@ -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
|
||||
|
|
39
fido2/nfc.py
39
fido2/nfc.py
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
217
fido2/server.py
217
fido2/server.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
universal = 1
|
||||
|
||||
[flake8]
|
||||
max-line-length = 80
|
||||
max-line-length = 88
|
||||
ignore = E203, W503
|
||||
|
|
77
setup.py
77
setup.py
|
@ -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
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)],
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue