From 8fd32e1956f4caa1d2417d02e20befa902ae16cb Mon Sep 17 00:00:00 2001 From: iabdalkader Date: Tue, 24 Sep 2024 15:48:15 +0200 Subject: [PATCH] lib/se05x: Add ISO7816, APDU and SE05x package. Signed-off-by: iabdalkader --- lib/se05x/examples/sign_verify.py | 40 +++ lib/se05x/examples/write_read_verify.py | 94 ++++++ lib/se05x/manifest.py | 6 + lib/se05x/se05x/__init__.py | 22 ++ lib/se05x/se05x/iso7816.py | 389 ++++++++++++++++++++++++ lib/se05x/se05x/se05x.py | 253 +++++++++++++++ 6 files changed, 804 insertions(+) create mode 100644 lib/se05x/examples/sign_verify.py create mode 100644 lib/se05x/examples/write_read_verify.py create mode 100644 lib/se05x/manifest.py create mode 100644 lib/se05x/se05x/__init__.py create mode 100644 lib/se05x/se05x/iso7816.py create mode 100644 lib/se05x/se05x/se05x.py diff --git a/lib/se05x/examples/sign_verify.py b/lib/se05x/examples/sign_verify.py new file mode 100644 index 0000000..df277f2 --- /dev/null +++ b/lib/se05x/examples/sign_verify.py @@ -0,0 +1,40 @@ +import logging +import se05x +from micropython import const + +TC_R = "\033[91m" +TC_G = "\033[92m" +TC_B = "\033[94m" +TC_RST = "\033[0m" +KEY_ID = const(0x10) + +if __name__ == "__main__": + logging.basicConfig( + datefmt="%H:%M:%S", + format="%(asctime)s.%(msecs)03d %(message)s", + level=logging.INFO + ) + + # Create and initialize SE05x device. + se = se05x.SE05X() + + # Print applet version. + major, minor, patch = se.version() + logging.info(f"{TC_G}Applet Version: {major}.{minor}.{patch}{TC_RST}") + + # Delete key object if it exists. + if se.exists(KEY_ID): + se.delete(KEY_ID) + + # Generate EC key pair. + se.write(KEY_ID, se05x.EC_KEY, curve=se05x.EC_CURVE_NIST_P256) + ec_pub_key = se.read(KEY_ID) + logging.info(f"{TC_B}Public Key: " + "".join("%02X" % b for b in ec_pub_key) + TC_RST) + + # Sign and verify hash. + data = bytes(range(32)) + sign = se.sign(KEY_ID, data) + logging.info(f"{TC_B}EC signature: " + "".join("%02X" % b for b in sign) + TC_RST) + if not se.verify(KEY_ID, data, sign): + logging.info(f"{TC_R}EC signature verified failed!{TC_RST}") + logging.info(f"{TC_G}EC signature verified successfully!{TC_RST}") diff --git a/lib/se05x/examples/write_read_verify.py b/lib/se05x/examples/write_read_verify.py new file mode 100644 index 0000000..266a371 --- /dev/null +++ b/lib/se05x/examples/write_read_verify.py @@ -0,0 +1,94 @@ +import logging +from micropython import const +import se05x + +TC_R = "\033[91m" +TC_G = "\033[92m" +TC_B = "\033[94m" +TC_RST = "\033[0m" +KEY_ID = const(0x10) +CERT_ID = const(0x11) + +PRIVATE_KEY = const( + b"\xd0\xa2\x15\x85\x55\xf2\x13\xa5\x3a\xae\xd8\xe1\x22\xd6\x0f\xd3" + b"\x94\x15\x3c\xa5\x56\x65\xf2\x38\xc8\xf1\xed\x3f\xe9\x29\xde\xb0" +) + +PUBLIC_KEY = const( + b"\x04\xb0\x86\x11\x63\xf3\x8e\xb6\x64\xc5\x46\xd8\xc6\x7f\x17\xbf" + b"\xbc\x68\x24\xf0\x07\x68\x37\xa9\x26\xc2\xbd\x2d\x48\xf8\xd6\x85" + b"\x6e\xa9\x61\xf3\x88\x1a\x98\x5f\xd8\x50\x53\x32\x46\x7f\xe4\x24" + b"\x4a\x94\x1f\x87\xc8\x53\xa4\x91\x2a\x09\x3f\x72\xdf\x44\xb6\x87" + b"\x03" +) + +CERT = const( + b"\x2a\x40\x42\x1e\xe9\x36\x80\xbb\xb5\xb0\xc3\x76\xed\x7f\xca\xf8" + b"\xf3\x12\xeb\x67\xee\xc1\x2f\x7e\xb3\x1b\x48\x36\x6d\x16\xba\xa3" + b"\x38\x29\x5b\x22\x52\xf9\x97\x2f\xc9\xbb\x67\x2b\xc3\xe0\x0a\x57" + b"\xbe\x64\x12\x0a\x62\xc4\xe7\xa6\xfe\xfc\xae\xee\x39\x84\xb9\x50" + b"\x9f\x6d\x36\x87\xc0\xf6\x21\xb4\xb7\xa9\xe9\x82\x11\x0b\x9a\x62" + b"\x04\x9f\x4f\x93\xcb\x33\xf3\x84\x95\x1e\x5f\xe8\x38\x93\xc4\xcb" + b"\xe4\xf5\xd3\x61\x4d\x99\x14\xb2\xfb\xbe\xa5\xfe\xe8\x40\x09\xdd" + b"\xc0\x89\xac\x27\x27\x2e\x52\x3b\x2f\x49\x09\xa5\xd3\x24\x3d\xb1" + b"\x31\xee\x2f\xa6\x74\xb6\xb9\x1a\x4e\xd3\x48\x73\xff\x2e\x07\xc1" + b"\x67\xf0\x75\x62\xd5\x7e\x80\x01\xe5\x24\xb4\x75\x37\x75\xf6\xfe" + b"\x4e\x6c\xb2\xfc\xd1\xb4\x2c\x80\x72\x63\x50\x8d\x21\x86\xd7\x8b" + b"\xf2\x75\xc2\xea\x49\x23\x95\x73\x7b\x91\x28\x69\x46\xd2\x00\x11" + b"\x6c\x51\x65\xb4\xf9\x2b\x42\xe6\xeb\x8c\xa1\xd3\xb1\xec\xf1\x47" + b"\x07\xe2\x24\x3d\x8c\xa5\xc3\x4f\xf5\xfd\x67\xb1\x45\x36\x6d\x30" + b"\x0e\x89\x7d\x96\xe8\x3b\xeb\xae\x38\x1b\x07\xbd\xb4\xa8\xc1\x62" + b"\xee\xa2\x78\x9f\xc5\x8f\xc0\x8b\x21\xdc\x55\x8e\xf2\x14\x3e\x40" + b"\x33\xfc\x11\xf8\xc5\x2d\xc8\x0e\x73\x88\x23\xc3\xd2\xf2\xde\xb5" + b"\x69\x0c\xa4\xb5\xb4\x64\xd5\x0f\xbd\x2a\x18\x35\x08\x73\xaa\xc5" + b"\x32\x5a\x8f\xbe\x31\x9f\xda\x8d\x09\x5e\xbe\xf0\x9f\x45\xbe\x5d" + b"\x5b\x26\x57\xa8\x7a\xcd\x63\xa9\x1a\x27\x49\x0a\x4e\x45\x32\x14" + b"\x00\xbb\x88\xea\x5a\x46\x88\x59\x79\x5b\x01\xc4\xd4\x0a\x08\x55" + b"\x20\x58\xba\xb4\x74\xfd\x40\x6b\xf6\x56\x55\xbd\xca\x56\x25\xdf" + b"\xb8\xbb\x9a\xb7\x76\xb1\xcb\x1c\x81\xfd\x0f\xde\x87\x6a\xcf\x1d" + b"\x77\xa4\xa0\x6a\xbe\xf7\xd0\x60\x7d\xba\xb5\x1c\xf0\x62\x4d\xab" + b"\x62\xf9\x36\xf4\x95\x76\x90\x79\xc0\x47\xfc\x3e\x32\xc7\x60\x3a" + b"\x31\xf4\xff\x86\xfc\x67\xb3\xa8\x32\x39\xce\x7c" +) + + +if __name__ == "__main__": + logging.basicConfig( + datefmt="%H:%M:%S", + format="%(asctime)s.%(msecs)03d %(message)s", + level=logging.INFO + ) + + # Create and initialize SE05x device. + se = se05x.SE05X() + + # Print applet version. + major, minor, patch = se.version() + logging.info(f"{TC_G}Applet Version: {major}.{minor}.{patch}{TC_RST}") + + # Delete key object if it exists. + if se.exists(KEY_ID): + se.delete(KEY_ID) + + # Write public/private key pair and verify. + se.write(KEY_ID, se05x.EC_KEY, key=(PRIVATE_KEY, PUBLIC_KEY), curve=se05x.EC_CURVE_NIST_P256) + + # Read back the public key part and verify it. + ec_pub_key = se.read(KEY_ID) + if PUBLIC_KEY != ec_pub_key: + logging.info(f"{TC_R}Key verification failed!{TC_RST}") + logging.info(f"{TC_G}Key verified successfully!{TC_RST}") + logging.info(f"{TC_B}Public Key: " + "".join("%02X" % b for b in ec_pub_key) + TC_RST) + + # Delete certificate object if it exists. + if se.exists(CERT_ID): + se.delete(CERT_ID) + + # Write binary certificate object. + se.write(CERT_ID, se05x.BINARY, binary=CERT) + logging.info(f"{TC_G}Binary certificate written successfully!{TC_RST}") + + # Read back the certificate and verify it. + if CERT != se.read(CERT_ID, 412): + logging.info(f"{TC_R}Certificate verification failed!{TC_RST}") + logging.info(f"{TC_G}Certificate verified successfully!{TC_RST}") diff --git a/lib/se05x/manifest.py b/lib/se05x/manifest.py new file mode 100644 index 0000000..0e14a35 --- /dev/null +++ b/lib/se05x/manifest.py @@ -0,0 +1,6 @@ +metadata( + description="ISO7816 standard implementation and SE05X driver for MicroPython.", + version="0.0.1", +) + +package("se05x") diff --git a/lib/se05x/se05x/__init__.py b/lib/se05x/se05x/__init__.py new file mode 100644 index 0000000..12457b9 --- /dev/null +++ b/lib/se05x/se05x/__init__.py @@ -0,0 +1,22 @@ +from .se05x import SE05X # noqa +from .iso7816 import SmartCard # noqa +from micropython import const + +# Secure Object Types. +EC_KEY = const(0x01) +AES_KEY = const(0x03) +DES_KEY = const(0x04) +HMAC_KEY = const(0x05) +BINARY = const(0x06) +USERID = const(0x07) +CURVE = const(0x0B) +SIGNATURE = const(0x0C) +MAC = const(0x0D) +CIPHER = const(0x0E) + +# Supported EC curves. +EC_CURVE_NIST_P192 = const(0x01) +EC_CURVE_NIST_P224 = const(0x02) +EC_CURVE_NIST_P256 = const(0x03) +EC_CURVE_NIST_P384 = const(0x04) +EC_CURVE_NIST_P521 = const(0x05) diff --git a/lib/se05x/se05x/iso7816.py b/lib/se05x/se05x/iso7816.py new file mode 100644 index 0000000..98c87fb --- /dev/null +++ b/lib/se05x/se05x/iso7816.py @@ -0,0 +1,389 @@ +# This file is part of the se05x package. +# Copyright (c) 2024 Arduino SA +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# An implementation of ISO7816-3/4 standards. +import struct +import logging +from micropython import const +from time import sleep + +# PCB bits +_NAD_OFFSET = const(0) +_PCB_OFFSET = const(1) +_LEN_OFFSET = const(2) +_INF_OFFSET = const(3) + +# Blocks +_I_BLOCK = const(0x00) +_I_BLOCK_N = const(6) +_I_BLOCK_M = const(5) + +_S_BLOCK = const(0xC0) +_S_BLOCK_REQ = const(0x3F) + +_R_BLOCK = const(0x80) +_R_BLOCK_N = const(4) +_R_BLOCK_ERR = const(0x03) + +# S-Block request/response +_RESYNC_REQ = const(0x00) +_IFS_REQ = const(0x01) +_ABORT_REQ = const(0x02) +_WTX_REQ = const(0x03) +_END_SESSION_REQ = const(0x05) +_CHIP_RESET_REQ = const(0x06) +_GET_ATR_REQ = const(0x07) +_SOFT_RESET_REQ = const(0x0F) + +_CLA_ISO7816 = const(0x00) +_INS_GP_SELECT = const(0xA4) + +_STATE_IBLK = const(0) +_STATE_SBLK = const(1) +_STATE_SEND = const(2) +_STATE_RECV = const(3) +_STATE_WAIT = const(4) +_STATE_WWTX = const(5) +_STATE_DONE = const(6) + + +def log_enabled(level): + return logging.getLogger().isEnabledFor(level) + + +class SmartCard: + def __init__(self, bus, nad, aid): + self.seq = 0 + self.bus = bus + self.sad = nad & 0xF0 + self.dad = nad & 0x0F + self.aid = aid + self.atr = None + + # TODO: Optimize these + self.w_buf = memoryview(bytearray(512)) + self.r_buf = memoryview(bytearray(512)) + self.s_buf = memoryview(bytearray(32)) + self.apdu_buf = memoryview(bytearray(512)) + + self.block_name = { + _I_BLOCK: "I-Block", + _R_BLOCK: "R-Block", + _S_BLOCK: "S-Block" + } + self.state_name = { + _STATE_IBLK: "IBLK", + _STATE_SBLK: "SBLK", + _STATE_SEND: "SEND", + _STATE_RECV: "RECV", + _STATE_WAIT: "WAIT", + _STATE_WWTX: "WWTX", + _STATE_DONE: "DONE", + } + self.apdu_status = { + 0x6700: "Wrong length", + 0x6985: "Conditions not satisfied", + 0x6982: "Security status not satisfied", + 0x6A80: "Wrong data", + 0x6984: "Data invalid", + 0x6986: "Command not allowed", + 0x6A82: "File not found", + 0x6A84: "File full", + 0x6D00: "Invalid or not supported instruction code.", + } + + def _block_type(self, pcb): + return _I_BLOCK if pcb & 0x80 == 0 else pcb & 0xC0 + + def _block_size(self, buf): + # NAD, PCB, LEN, INF[LEN], CRC[2] + return 3 + buf[_LEN_OFFSET] + 2 + + def _block_crc16(self, prologue, data, poly=0x8408, crc=0xFFFF): + # Calculate prologue checksum + for i in prologue: + crc ^= i + for bit in range(8): + crc = (crc >> 1) ^ poly if crc & 0x1 else crc >> 1 + + # Calculate data checksum + for i in data: + crc ^= i + for bit in range(8): + crc = (crc >> 1) ^ poly if crc & 0x1 else crc >> 1 + crc ^= 0xFFFF + return ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF) + + def _block_write(self, buf, delay=0): + size = self._block_size(buf) + self.bus.write(buf[0:size]) + + def _block_print(self, txrx, *args): + if len(args) == 1: + buf = args[0] + nad, pcb, bsize = buf[_NAD_OFFSET:_LEN_OFFSET + 1] + crc = buf[_LEN_OFFSET + bsize + 1] << 8 | buf[_LEN_OFFSET + bsize + 2] + else: + nad, pcb, bsize, crc, buf = args + btype = self._block_type(pcb) + bname = self.block_name[btype] + boffs = _INF_OFFSET if len(args) == 1 else 0 + seq = (pcb >> _I_BLOCK_N) & 1 if btype == _I_BLOCK else (pcb >> _R_BLOCK_N) & 1 + if log_enabled(logging.DEBUG): + logging.debug( + f"{'Tx' if txrx else 'Rx'}: {bname} NAD: 0x{nad:X} " + f"PCB: 0x{pcb:X} LEN: {bsize} SEQ: {seq} CRC: 0x{crc:X}" + ) + buf_hex = "".join(f"{b:02X}" for b in buf[boffs:boffs + bsize]) + logging.debug(f"RAW: {nad:02X}{pcb:02X}{bsize:02X}{buf_hex}{crc:04X}") + + def _block_new(self, buf, btype, **kwargs): + data = kwargs.get("data", None) + bsize = 0 if data is None else len(data) + buf[_NAD_OFFSET] = self.sad | self.dad + buf[_PCB_OFFSET] = btype + buf[_LEN_OFFSET] = bsize + if btype == _S_BLOCK: + buf[_PCB_OFFSET] |= kwargs["request"] + elif btype == _I_BLOCK: + buf[_PCB_OFFSET] |= self.seq << _I_BLOCK_N + buf[_PCB_OFFSET] |= kwargs["chained"] << _I_BLOCK_M + elif btype == _R_BLOCK: + buf[_PCB_OFFSET] |= self.seq << _R_BLOCK_N + buf[_PCB_OFFSET] |= kwargs["error"] & _R_BLOCK_ERR + if bsize: + buf[_INF_OFFSET:_INF_OFFSET + bsize] = data + # Calculate and set CRC + crc = self._block_crc16(buf[0:_INF_OFFSET], buf[_INF_OFFSET:_INF_OFFSET + bsize]) + buf[_LEN_OFFSET + bsize + 1] = (crc >> 8) & 0xFF + buf[_LEN_OFFSET + bsize + 2] = (crc >> 0) & 0xFF + # Toggle I-Block sequence + if btype == _I_BLOCK: + self.seq = self.seq ^ 1 + return buf + + def _send_block(self, btype, arg, retry=25, backoff=1.2): + r_offs = 0 + w_offs = 0 + retry_delay = 1 / 1000 + next_state = _STATE_SBLK if btype == _S_BLOCK else _STATE_IBLK + + while retry: + if log_enabled(logging.DEBUG): + logging.debug(f"STATE: {self.state_name[next_state]} retry: {retry}") + if next_state == _STATE_SBLK: + next_state = _STATE_SEND + prev_state = _STATE_RECV + block = self._block_new(self.w_buf, _S_BLOCK, request=arg) + elif next_state == _STATE_IBLK: + next_state = _STATE_SEND + prev_state = _STATE_RECV + remain = len(arg) - w_offs + bsize = min(remain, self.atr["IFSC"]) + chained = int(remain > self.atr["IFSC"]) + block = self._block_new(self.w_buf, _I_BLOCK, chained=chained, data=arg[w_offs:w_offs + bsize]) + w_offs += bsize + elif next_state == _STATE_SEND: + try: + self._block_write(block) + next_state = prev_state + if log_enabled(logging.DEBUG): + self._block_print(True, block) + except Exception: + retry -= 1 + elif next_state == _STATE_RECV: + try: + # Read NAD, PCB, LEN, information (if present) and CRC. + nad, pcb, bsize = self.bus.read(self.s_buf[0:3]) + if bsize: + self.bus.read(self.r_buf[r_offs:r_offs + bsize]) + crc = int.from_bytes(self.bus.read(self.s_buf[3:5]), "big") + except Exception: + retry -= 1 + next_state = _STATE_WAIT + prev_state = _STATE_RECV + continue + + # Check NAD and CRC. + exp = self._block_crc16(self.s_buf[0:3], self.r_buf[r_offs:r_offs + bsize]) + if (nad != (self.sad >> 4) | (self.dad << 4) or crc != exp): + retry -= 1 + next_state = _STATE_SEND + prev_state = _STATE_RECV + block = self._block_new(self.s_buf, _R_BLOCK, error=1) + continue + + if log_enabled(logging.DEBUG): + self._block_print(False, nad, pcb, bsize, crc, self.r_buf[r_offs:r_offs + bsize]) + + # Process block. + btype = self._block_type(pcb) + if btype == _R_BLOCK: + # Retransmit last block if error, or continue block chain. + next_state = _STATE_SEND if pcb & _R_BLOCK_ERR else _STATE_IBLK + prev_state = _STATE_RECV + continue + + if btype == _I_BLOCK: + # Acknowledge I-Block in block chain with R(N(R)). + if pcb & (1 << _I_BLOCK_M): + next_state = _STATE_SEND + prev_state = _STATE_RECV + block = self._block_new(self.s_buf, _R_BLOCK, error=0) + else: + next_state = _STATE_DONE + # Add current I-Block INF size (could be 0). + r_offs += bsize + continue + + if btype == _S_BLOCK: + if pcb & _S_BLOCK_REQ == _RESYNC_REQ: + # Respond to a resync request. + self.seq = 0 + next_state = _STATE_SEND + prev_state = _STATE_RECV + block = self._block_new(self.s_buf, _S_BLOCK, request=_RESYNC_REQ & 0x20) + elif pcb & _S_BLOCK_REQ == _WTX_REQ: + # Respond to a WTX request. + next_state = _STATE_SEND + prev_state = _STATE_WWTX + wtx_delay = self.r_buf[r_offs] * self.atr["BWT"] / 1000 + block = self._block_new(self.s_buf, _S_BLOCK, request=_WTX_REQ & 0x20) + else: + # Add current S-Block INF size (could be 0). + r_offs += bsize + next_state = _STATE_DONE + continue + elif next_state == _STATE_WWTX: + sleep(wtx_delay) + next_state = _STATE_RECV + elif next_state == _STATE_WAIT: + sleep(retry_delay) + retry_delay *= backoff + next_state = prev_state + elif next_state == _STATE_DONE: + return self.r_buf[0:r_offs] + + if retry == 0: + raise RuntimeError("_send_block failed") + + def send_apdu(self, cla, ins, p1, p2, data=None, le=0): + size = 4 + self.apdu_buf[0] = cla + self.apdu_buf[1] = ins + self.apdu_buf[2] = p1 + self.apdu_buf[3] = p2 + if data is not None: + size = len(data) + self.apdu_buf[4] = size + self.apdu_buf[5:5 + size] = data + self.apdu_buf[5 + size] = 0x00 + size += 5 + + # Send APDU in I-Block + resp = self._send_block(_I_BLOCK, self.apdu_buf[0:size]) + + # Check response TPDU status + status = int.from_bytes(resp[-2:], "big") + if status != 0x9000: + raise RuntimeError("APDU Error: " + self.apdu_status.get(status, f"Unknown 0x{status:X}")) + + # Return data bytes, if any, or the status. + if len(resp) == 2: + return status + return resp[2 + (0 if resp[1] <= 0x7F else resp[1] & 0x0F):-2] + + def reset(self): + self.seq = 0 + atr_raw = self._send_block(_S_BLOCK, _SOFT_RESET_REQ) + if self.atr is None: + self.atr = self._parse_atrs(atr_raw) + if log_enabled(logging.INFO): + self._dump_atrs(self.atr) + # Select applet + self.send_apdu(_CLA_ISO7816, _INS_GP_SELECT, 0x04, 0x00, self.aid, le=True) + + def resync(self): + self._send_block(_S_BLOCK, _RESYNC_REQ) + self.seq = 0 + + def _parse_atrs(self, atr_bytes): + atr = {} + # PVER - 1 byte + atr["PVER"] = atr_bytes[0] + # VID - 5 bytes + atr["VID"] = atr_bytes[1:6].hex().upper() + + # Length of DLLP - 1 byte + dllp_length = atr_bytes[6] + atr["DLLP_LENGTH"] = dllp_length + + # DLLP - Variable length (Decode using struct) + dllp = atr_bytes[7:7 + dllp_length] + atr["DLLP"] = dllp.hex().upper() + if dllp_length >= 4: + atr["BWT"], atr["IFSC"] = struct.unpack(">HH", dllp[:4]) + + # PLID - 1 byte + atr["PLID"] = atr_bytes[7 + dllp_length] + # Length of PLP - 1 byte + plp_length = atr_bytes[8 + dllp_length] + atr["PLP_LENGTH"] = plp_length + + # PLP - Variable length (Decode using struct) + plp = atr_bytes[9 + dllp_length:9 + dllp_length + plp_length] + atr["PLP"] = plp.hex().upper() + if plp_length >= 2: + atr["MCF"] = struct.unpack(">H", plp[:2])[0] + if plp_length >= 3: + atr["CONFIGURATION"] = plp[2] + if plp_length >= 4: + atr["MPOT"] = plp[3] + if plp_length >= 6: + atr["SEGT"], atr["WUT"] = struct.unpack(">HH", plp[4:8]) + + # Length of HB - 1 byte + hb_length = atr_bytes[9 + dllp_length + plp_length] + atr["HB_LENGTH"] = hb_length + # HB - Variable length + hb = atr_bytes[10 + dllp_length + plp_length:10 + dllp_length + plp_length + hb_length] + atr["HB"] = hb.hex().upper() + return atr + + def _dump_atrs(self, atr): + logging.info(f"PVER (Protocol Version): {atr['PVER']}") + logging.info(f"VID (Vendor ID): {atr['VID']}") + logging.info(f"Length of DLLP: {atr['DLLP_LENGTH']}") + + if "DLLP" in atr: + logging.info(f"DLLP: {atr['DLLP']}") + + if "BWT" in atr and "IFSC" in atr: + logging.info(f"BWT (Block Waiting Time): {atr['BWT']} ms") + logging.info(f"IFSC (Maximum Information Field Size): {atr['IFSC']} bytes") + + logging.info(f"PLID (Physical Layer ID): {atr['PLID']}") + logging.info(f"Length of PLP: {atr['PLP_LENGTH']}") + + if "PLP" in atr: + logging.info(f"PLP: {atr['PLP']}") + + if "MCF" in atr: + logging.info(f"MCF (Max I2C Clock Frequency): {atr['MCF']} kHz") + + if "CONFIGURATION" in atr: + logging.info(f"Configuration: {atr['CONFIGURATION']:#04x}") + + if "MPOT" in atr: + logging.info(f"MPOT (Minimum Polling Time): {atr['MPOT']} ms") + + if "SEGT" in atr and "WUT" in atr: + logging.info(f"SEGT (Secure Element Guard Time): {atr['SEGT']} µs") + logging.info(f"WUT (Wake-Up Time): {atr['WUT']} µs") + + logging.info(f"Length of HB (Historical Bytes): {atr['HB_LENGTH']}") + if "HB" in atr: + logging.info(f"HB (Historical Bytes): {atr['HB']}") diff --git a/lib/se05x/se05x/se05x.py b/lib/se05x/se05x/se05x.py new file mode 100644 index 0000000..32ac4ef --- /dev/null +++ b/lib/se05x/se05x/se05x.py @@ -0,0 +1,253 @@ +# This file is part of the se05x package. +# Copyright (c) 2024 Arduino SA +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# NXP SE05x EdgeLock device driver. + +import struct +import logging +from time import sleep_ms +from machine import I2C +from machine import Pin +from .iso7816 import SmartCard +from micropython import const + +_RESULT_OK = const(1) +_APPLET_NAD = const(0x5A) +_APPLET_AID = const(b"\xa0\x00\x00\x03\x96\x54\x53\x00" + b"\x00\x00\x01\x03\x00\x00\x00\x00") +_CLA_KSE05X = const(0x80) + +_INS_WRITE = const(0x01) +_INS_READ = const(0x02) +_INS_CRYPTO = const(0x03) +_INS_MGMT = const(0x04) +_INS_PROCESS = const(0x05) +_INS_IMPORT_EXTERNAL = const(0x06) +_INS_TRANSIENT = const(0x80) +_INS_AUTH_OBJECT = const(0x40) +_INS_ATTEST = const(0x20) + +_P1_DEFAULT = const(0x00) +_P1_EC = const(0x01) +_P1_AES = const(0x03) +_P1_DES = const(0x04) +_P1_HMAC = const(0x05) +_P1_BINARY = const(0x06) +_P1_USERID = const(0x07) +_P1_CURVE = const(0x0B) +_P1_SIGNATURE = const(0x0C) +_P1_MAC = const(0x0D) +_P1_CIPHER = const(0x0E) +_P1_KEY_PRIVATE = const(0x40) +_P1_KEY_PUBLIC = const(0x20) + +_P2_DEFAULT = const(0x00) +_P2_GENERATE = const(0x03) +_P2_CREATE = const(0x04) +_P2_SIZE = const(0x07) +_P2_SIGN = const(0x09) +_P2_VERIFY = const(0x0A) +_P2_SESSION_CREATE = const(0x1B) +_P2_SESSION_CLOSE = const(0x1C) +_P2_VERSION = const(0x20) +_P2_LIST = const(0x25) +_P2_EXIST = const(0x27) +_P2_DELETE_OBJECT = const(0x28) +_P2_SESSION_USERID = const(0x2C) +_P2_DH = const(0x0F) +_P2_ENCRYPT_ONESHOT = const(0x37) +_P2_DECRYPT_ONESHOT = const(0x38) +_P2_SCP = const(0x52) +_P2_ONESHOT = const(0x0E) + +_TLV_TAG1 = const(0x41) +_TLV_TAG2 = const(0x42) +_TLV_TAG3 = const(0x43) +_TLV_TAG4 = const(0x44) +_TLV_TAG5 = const(0x45) +_TLV_TAG6 = const(0x46) +_TLV_TAG7 = const(0x47) +_TLV_TAG8 = const(0x48) +_TLV_TAG9 = const(0x49) +_TLV_TAG10 = const(0x4A) +_TLV_TAG11 = const(0x4B) +_TLV_TAG_SESSION_ID = const(0x10) +_TLV_TAG_POLICY = const(0x11) +_TLV_TAG_MAX_ATTEMPTS = const(0x12) +_TLV_TAG_IMPORT_AUTH_DATA = const(0x13) +_TLV_TAG_IMPORT_AUTH_KEY_ID = const(0x14) +_TLV_TAG_POLICY_CHECK = const(0x15) + +_SIG_ECDSA_SHA_1 = const(0x11) +_SIG_ECDSA_SHA_224 = const(0x25) +_SIG_ECDSA_SHA_256 = const(0x21) +_SIG_ECDSA_SHA_384 = const(0x22) +_SIG_ECDSA_SHA_512 = const(0x26) + + +class I2CBus: + def __init__(self, addr, freq): + self.addr = addr + self.bus = None + # Scan the first 3 I2C buses + for i in range(3): + try: + bus = I2C(i, freq=freq) + if self.addr in bus.scan(): + logging.info(f"SE05x detected on bus: {i} addr: 0x{addr:02X}") + self.bus = bus + break + except Exception: + pass + if self.bus is None: + raise RuntimeError("Failed to detect SE05x on I2C bus") + + def read(self, buf): + self.bus.readfrom_into(self.addr, buf) + return buf + + def write(self, buf): + self.bus.writeto(self.addr, buf) + + +class SE05X: + def __init__(self, addr=0x48, freq=400_000, rst=Pin("SE05X_EN", Pin.OUT_PP, Pin.PULL_UP)): + self.rst = rst + self.scard = None + self.reset() + self.bus = I2CBus(addr, freq) + self.scard = SmartCard(self.bus, _APPLET_NAD, _APPLET_AID) + self.scard.reset() + self.ecdsa_algo = { + 160: _SIG_ECDSA_SHA_1, + 224: _SIG_ECDSA_SHA_224, + 256: _SIG_ECDSA_SHA_256, + 384: _SIG_ECDSA_SHA_384, + 512: _SIG_ECDSA_SHA_512, + } + self.tlv_offs = 0 + self.tlv_buf = memoryview(bytearray(254)) + + def _tlv_pack(self, fmt, *args): + struct.pack_into(fmt, self.tlv_buf, self.tlv_offs, *args) + self.tlv_offs += struct.calcsize(fmt) + + def _tlv_flush(self): + mv = self.tlv_buf[0:self.tlv_offs] + self.tlv_offs = 0 + return mv + + def _ecdsa_algo(self, hash_size): + if hash_size * 8 not in self.ecdsa_algo: + raise ValueError("Invalid SHA digest size") + return self.ecdsa_algo[hash_size * 8] + + def reset(self, reset_card=True): + self.rst.low() + sleep_ms(10) + self.rst.high() + sleep_ms(10) + if self.scard is not None: + self.scard.reset() + + def version(self): + resp = self.scard.send_apdu(_CLA_KSE05X, _INS_MGMT, _P1_DEFAULT, _P2_VERSION) + major, minor, patch = struct.unpack(">BBB", resp) + return major, minor, patch + + def read(self, obj_id, size=0): + if not self.exists(obj_id): + raise RuntimeError(f"Object with id 0x{obj_id:X} doesn't exist.") + if size == 0: + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + resp = self.scard.send_apdu(_CLA_KSE05X, _INS_READ, _P1_DEFAULT, _P2_DEFAULT, self._tlv_flush()) + return bytes(resp) + offset = 0 + buf = bytearray() + maxblk = 254 - struct.calcsize("BBIBBHBBH") + while size: + bsize = min(size, maxblk) + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + self._tlv_pack(">BBH", _TLV_TAG2, 0x2, offset) + self._tlv_pack(">BBH", _TLV_TAG3, 0x2, bsize) + resp = self.scard.send_apdu(_CLA_KSE05X, _INS_READ, _P1_DEFAULT, _P2_DEFAULT, self._tlv_flush()) + buf.extend(resp) + offset += bsize + size -= bsize + return bytes(buf) + + def write(self, obj_id, obj_type, **kwargs): + if self.exists(obj_id): + raise RuntimeError(f"Object with id 0x{obj_id:X} already exists.") + + ins = _INS_WRITE | kwargs.get("ins_flags", 0) + if obj_type == _P1_EC: + p1 = _P1_EC + key = kwargs.get("key", (None, None)) + curve_id = kwargs.get("curve", 0x3) + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + self._tlv_pack(">BBB", _TLV_TAG2, 0x1, curve_id) + if key[0] is not None: + p1 |= _P1_KEY_PRIVATE + self._tlv_pack(f"BB{len(key[0])}s", _TLV_TAG3, len(key[0]), key[0]) + if key[1] is not None: + p1 |= _P1_KEY_PUBLIC + self._tlv_pack(f"BB{len(key[1])}s", _TLV_TAG4, len(key[1]), key[1]) + if key[0] is None and key[1] is None: + p1 |= _P1_KEY_PRIVATE | _P1_KEY_PUBLIC + self.scard.send_apdu(_CLA_KSE05X, ins, p1, _P2_DEFAULT, self._tlv_flush()) + elif obj_type == _P1_BINARY: + offset = 0 + binary = kwargs["binary"] + maxblk = 254 - struct.calcsize("BBIBBHBBHBBH") + remain = len(binary) + while remain: + bsize = min(remain, maxblk) + data = binary[offset:offset + bsize] + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + self._tlv_pack(">BBH", _TLV_TAG2, 0x2, offset) + if offset == 0: + self._tlv_pack(">BBH", _TLV_TAG3, 0x2, remain) + self._tlv_pack(f">BBH{bsize}s", _TLV_TAG4, 0x82, bsize, data) + self.scard.send_apdu(_CLA_KSE05X, ins, _P1_BINARY, _P2_DEFAULT, self._tlv_flush()) + offset += bsize + remain -= bsize + + def delete(self, obj_id): + if not self.exists(obj_id): + raise RuntimeError(f"Object with id 0x{obj_id:X} doesn't exist.") + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + self.scard.send_apdu(_CLA_KSE05X, _INS_MGMT, _P1_DEFAULT, _P2_DELETE_OBJECT, self._tlv_flush()) + + def exists(self, obj_id): + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + resp = self.scard.send_apdu( + _CLA_KSE05X, _INS_MGMT, _P1_DEFAULT, _P2_EXIST, self._tlv_flush() + ) + return resp[0] == _RESULT_OK + + def sign(self, obj_id, data): + if not self.exists(obj_id): + raise RuntimeError(f"Object with id 0x{obj_id:X} doesn't exist.") + hash_size = len(data) + hash_algo = self._ecdsa_algo(hash_size) + self._tlv_pack(">BBIBBB", _TLV_TAG1, 0x4, obj_id, _TLV_TAG2, 0x1, hash_algo) + self._tlv_pack(f"BB{hash_size}s", _TLV_TAG3, hash_size, data) + resp = self.scard.send_apdu(_CLA_KSE05X, _INS_CRYPTO, _P1_SIGNATURE, _P2_SIGN, self._tlv_flush()) + return bytes(resp) + + def verify(self, obj_id, data, sign): + if not self.exists(obj_id): + raise RuntimeError(f"Object with id 0x{obj_id:X} doesn't exist.") + hash_size = len(data) + sign_size = len(sign) + hash_algo = self._ecdsa_algo(hash_size) + self._tlv_pack(">BBI", _TLV_TAG1, 0x4, obj_id) + self._tlv_pack(">BBB", _TLV_TAG2, 0x1, hash_algo) + self._tlv_pack(f"BB{hash_size}s", _TLV_TAG3, hash_size, data) + self._tlv_pack(f"BB{sign_size}s", _TLV_TAG5, sign_size, sign) + resp = self.scard.send_apdu(_CLA_KSE05X, _INS_CRYPTO, _P1_SIGNATURE, _P2_VERIFY, self._tlv_flush()) + return resp[0] == _RESULT_OK