python-fido2/examples/acr122usam.py

313 lines
10 KiB
Python

# Copyright (c) 2019 Yubico AB
# Copyright (c) 2019 Oleg Moiseenko
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Sample work with reader:
ACR-122U-SAM or touchatag
drivers and manual link:
www.acs.com.hk/en/driver/100/acr122u-nfc-reader-with-sam-slot-proprietary/
"""
import time
from fido2.utils import sha256
from fido2.ctap1 import CTAP1
from smartcard.Exceptions import CardConnectionException
from fido2.pcsc import CtapPcscDevice
class Acr122uSamPcscDevice(CtapPcscDevice):
def __init__(self, connection, name):
self.ats = b""
self.vparity = False
self.max_block_len = 29
try:
super().__init__(connection, name)
except (CardConnectionException, ValueError):
pass
except Exception as e:
print(e.__class__)
# setup reader
if not self.set_auto_iso14443_4_activation():
raise Exception("Set automatic iso-14443-4 activation error")
if not self.set_default_retry_timeout():
raise Exception("Set default retry timeout error")
self.ats = self.get_ats()
if self.ats == b"":
raise Exception("No card in field")
self._select()
def apdu_plain(self, apdu, protocol=None):
"""Exchange data with reader.
:param apdu: byte string. data to exchange with card
:param protocol: protocol to exchange with card. usually set by default
:return: byte string. response from card
"""
# print('>> %s' % b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(list(iter(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
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)
# override base method
# commands in PN 532 User manual (UM0701-02)
# page 178. 7.4.5 DEP chaining mechanism
# page 136. 7.3.9 InCommunicateThru
# chaining ISO 14443-4:2001
# page 20. 7.5.2 Chaining
def apdu_exchange(self, apdu, protocol=None):
all_response = b""
alen = 0
while True:
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)
# 7.3.9 InCommunicateThru
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
alen += len(vapdu)
if not chaining:
break
if len(resp) > 3:
if resp[3] & 0x10 == 0:
return resp[4:-2], resp[-2], resp[-1]
else:
if resp[3] != 0xF2:
all_response = resp[4:]
else:
return b"", 0x90, 0x00
while True:
if len(resp) > 3 and resp[3] == 0xF2:
# WTX
answer = resp[3:5]
else:
# ACK
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)
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
response_chaining = len(resp) > 3 and resp[3] & 0x10 != 0
# if I block
if len(resp) > 3 and resp[3] & 0xE0 == 0x00:
all_response += resp[4:]
if not response_chaining:
break
return all_response[:-2], resp[-2], resp[-1]
def get_ats(self, verbose=False):
self.field_reset()
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])
uid_len = resp[7]
if verbose:
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()))
self.vparity = False
return self.ats
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")
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")
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")
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")
return False
return True
def field_reset(self):
self.led_control(True, False)
result = self.field_control(False)
time.sleep(0.2)
result |= self.field_control(True)
self.led_control()
return result
def reader_version(self):
"""
Get reader's version from reader
:return: string. Reader's version
"""
try:
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")
return str_result
except Exception as e:
print("Get version error:", e)
return "n/a"
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
:param green: boolean. green let on
:param blink_count: int. if needs to blink value > 0. blinks count
:param red_end_blink: boolean.
state of red led at the end of blinking
:param green_end_blink: boolean.
state of green led at the end of blinking
:return:
"""
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
)
else:
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"
)
self.apdu_plain(apdu)
except Exception as e:
print("LED control error:", e)
dev = next(Acr122uSamPcscDevice.list_devices())
print("CONNECT: %s" % dev)
print("version: %s" % dev.reader_version())
print("atr: %s" % 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)
# dev._select()
dev.led_control(False, True, 0)
chal = sha256(b"AAA")
appid = sha256(b"BBB")
ctap1 = CTAP1(dev)
print("ctap1 version:", ctap1.get_version())
reg = ctap1.register(chal, appid)
print("u2f register:", reg)
reg.verify(appid, chal)
print("Register message verify OK")
auth = ctap1.authenticate(chal, appid, reg.key_handle)
print("u2f authenticate: ", auth)
res = auth.verify(appid, chal, reg.public_key)
print("Authenticate message verify OK")
dev.led_control()