pySim/transport add support for T=1 protocol and fix APDU/TPDU layer conflicts

ETSI TS 102 221, section 7.3 specifies that UICCs (and eUICCs) may support two
different transport protocols: T=0 or T=1 or both. The spec also says that the
terminal must support both protocols.

This patch adds the necessary functionality to support the T=1 protocol
alongside the T=0 protocol. However, this also means that we have to sharpen
the lines between APDUs and TPDUs.

As this patch also touches the low level interface to readers it was also
manually tested with a classic serial reader. Calypso and AT command readers
were not tested.

Change-Id: I8b56d7804a2b4c392f43f8540e0b6e70001a8970
Related: OS#6367
This commit is contained in:
Philipp Maier
2024-10-28 17:28:43 +01:00
parent f951c56449
commit 852eff54df
25 changed files with 606 additions and 144 deletions

View File

@@ -88,7 +88,7 @@ if __name__ == '__main__':
scc.sel_ctrl = "0004" scc.sel_ctrl = "0004"
# Testing for Classic SIM or UICC # Testing for Classic SIM or UICC
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00") (res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
if sw == '6e00': if sw == '6e00':
# Just a Classic SIM # Just a Classic SIM
scc.cla_byte = "a0" scc.cla_byte = "a0"

View File

@@ -114,6 +114,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.conserve_write = True self.conserve_write = True
self.json_pretty_print = True self.json_pretty_print = True
self.apdu_trace = False self.apdu_trace = False
self.apdu_strict = False
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self, self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
onchange_cb=self._onchange_numeric_path)) onchange_cb=self._onchange_numeric_path))
@@ -122,6 +123,9 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self)) self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self, self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
onchange_cb=self._onchange_apdu_trace)) onchange_cb=self._onchange_apdu_trace))
self.add_settable(Settable2Compat('apdu_strict', bool,
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
onchange_cb=self._onchange_apdu_strict))
self.equip(card, rs) self.equip(card, rs)
def equip(self, card, rs): def equip(self, card, rs):
@@ -198,6 +202,13 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
else: else:
self.card._scc._tp.apdu_tracer = None self.card._scc._tp.apdu_tracer = None
def _onchange_apdu_strict(self, param_name, old, new):
if self.card:
if new == True:
self.card._scc._tp.apdu_strict = True
else:
self.card._scc._tp.apdu_strict = False
class Cmd2ApduTracer(ApduTracer): class Cmd2ApduTracer(ApduTracer):
def __init__(self, cmd2_app): def __init__(self, cmd2_app):
self.cmd2 = cmd2_app self.cmd2 = cmd2_app

View File

@@ -55,7 +55,7 @@ class DummySimLink(LinkBase):
def __str__(self): def __str__(self):
return "dummy" return "dummy"
def _send_apdu_raw(self, pdu): def _send_apdu(self, pdu):
#print("DummySimLink-apdu: %s" % pdu) #print("DummySimLink-apdu: %s" % pdu)
return [], '9000' return [], '9000'

View File

@@ -142,8 +142,9 @@ class SimCardCommands:
Tuple of (decoded_data, sw) Tuple of (decoded_data, sw)
""" """
cmd = cmd_constr.build(cmd_data) if cmd_data else '' cmd = cmd_constr.build(cmd_data) if cmd_data else ''
p3 = i2h([len(cmd)]) lc = i2h([len(cmd)]) if cmd_data else ''
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)]) le = '00' if resp_constr else ''
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan) (data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
if data: if data:
# filter the resulting dict to avoid '_io' members inside # filter the resulting dict to avoid '_io' members inside
@@ -247,7 +248,7 @@ class SimCardCommands:
if not isinstance(dir_list, list): if not isinstance(dir_list, list):
dir_list = [dir_list] dir_list = [dir_list]
for i in dir_list: for i in dir_list:
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i) data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i + "00")
rv.append((data, sw)) rv.append((data, sw))
if sw != '9000': if sw != '9000':
return rv return rv
@@ -277,11 +278,11 @@ class SimCardCommands:
fid : file identifier as hex string fid : file identifier as hex string
""" """
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid) return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
def select_parent_df(self) -> ResTuple: def select_parent_df(self) -> ResTuple:
"""Execute SELECT to switch to the parent DF """ """Execute SELECT to switch to the parent DF """
return self.send_apdu_checksw(self.cla_byte + "a4030400") return self.send_apdu_checksw(self.cla_byte + "a40304")
def select_adf(self, aid: Hexstr) -> ResTuple: def select_adf(self, aid: Hexstr) -> ResTuple:
"""Execute SELECT a given Applicaiton ADF. """Execute SELECT a given Applicaiton ADF.
@@ -291,7 +292,7 @@ class SimCardCommands:
""" """
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:] aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid) return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple: def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
"""Execute READD BINARY. """Execute READD BINARY.
@@ -494,9 +495,9 @@ class SimCardCommands:
# TS 102 221 Section 11.3.1 low-level helper # TS 102 221 Section 11.3.1 low-level helper
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple: def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
if first: if first:
pdu = '80cb008001%02x' % (tag) pdu = '80cb008001%02x00' % (tag)
else: else:
pdu = '80cb000000' pdu = '80cb0000'
return self.send_apdu_checksw(pdu) return self.send_apdu_checksw(pdu)
def retrieve_data(self, ef: Path, tag: int) -> ResTuple: def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
@@ -569,7 +570,7 @@ class SimCardCommands:
if len(rand) != 32: if len(rand) != 32:
raise ValueError('Invalid rand') raise ValueError('Invalid rand')
self.select_path(['3f00', '7f20']) self.select_path(['3f00', '7f20'])
return self.send_apdu_checksw('a088000010' + rand, sw='9000') return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple: def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
"""Execute AUTHENTICATE (USIM/ISIM). """Execute AUTHENTICATE (USIM/ISIM).
@@ -602,7 +603,7 @@ class SimCardCommands:
def status(self) -> ResTuple: def status(self) -> ResTuple:
"""Execute a STATUS command as per TS 102 221 Section 11.1.2.""" """Execute a STATUS command as per TS 102 221 Section 11.1.2."""
return self.send_apdu_checksw('80F2000000') return self.send_apdu_checksw('80F20000')
def deactivate_file(self) -> ResTuple: def deactivate_file(self) -> ResTuple:
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14.""" """Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
@@ -651,7 +652,7 @@ class SimCardCommands:
p1 = 0x80 p1 = 0x80
else: else:
p1 = 0x00 p1 = 0x00
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr) pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
return self.send_apdu_checksw(pdu) return self.send_apdu_checksw(pdu)
def reset_card(self) -> Hexstr: def reset_card(self) -> Hexstr:

View File

@@ -332,7 +332,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]: def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22. """Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
Only single-block store supported for now.""" Only single-block store supported for now."""
capdu = '80E29100%02x%s' % (len(tx_do)//2, tx_do) capdu = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
return scc.send_apdu_checksw(capdu, exp_sw) return scc.send_apdu_checksw(capdu, exp_sw)
@staticmethod @staticmethod

View File

@@ -580,7 +580,7 @@ class ADF_SD(CardADF):
{'last_block': len(remainder) == 0, 'encryption': encryption, {'last_block': len(remainder) == 0, 'encryption': encryption,
'structure': structure, 'response': response_permitted}) 'structure': structure, 'response': response_permitted})
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk)) hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk)) data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
block_nr += 1 block_nr += 1
response += data response += data
return data return data
@@ -646,7 +646,7 @@ class ADF_SD(CardADF):
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details.""" See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict) key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data)) hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data)) data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
return data return data
get_status_parser = argparse.ArgumentParser() get_status_parser = argparse.ArgumentParser()
@@ -671,7 +671,7 @@ class ADF_SD(CardADF):
grd_list = [] grd_list = []
while True: while True:
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data)) hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data)) data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data) + "00")
remainder = h2b(data) remainder = h2b(data)
while len(remainder): while len(remainder):
# tlv sequence, each element is one GpRegistryRelatedData() # tlv sequence, each element is one GpRegistryRelatedData()
@@ -752,7 +752,7 @@ class ADF_SD(CardADF):
self.install(p1, 0x00, b2h(ifi_bytes)) self.install(p1, 0x00, b2h(ifi_bytes))
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple: def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data) cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex) return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
del_cc_parser = argparse.ArgumentParser() del_cc_parser = argparse.ArgumentParser()

View File

@@ -24,7 +24,7 @@ from construct import Struct, Bytes, Int8ub, Int16ub, Const
from construct import Optional as COptional from construct import Optional as COptional
from osmocom.utils import b2h from osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
from pySim.utils import parse_command_apdu
from pySim.secure_channel import SecureChannel from pySim.secure_channel import SecureChannel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -248,7 +248,7 @@ class SCP02(SCP):
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes: def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
"""Generate INITIALIZE UPDATE APDU.""" """Generate INITIALIZE UPDATE APDU."""
self.host_challenge = host_challenge self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge + b'\x00'
def parse_init_update_resp(self, resp_bin: bytes): def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALZIE UPDATE.""" """Parse response to INITIALZIE UPDATE."""
@@ -280,9 +280,13 @@ class SCP02(SCP):
if not self.do_cmac: if not self.do_cmac:
return apdu return apdu
lc = len(apdu) - 5 (case, lc, le, data) = parse_command_apdu(apdu)
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc) # TODO: add support for extended length fields.
assert lc <= 256
assert le <= 256
lc &= 0xFF
le &= 0xFF
# CLA without log. channel can be 80 or 00 only # CLA without log. channel can be 80 or 00 only
cla = apdu[0] cla = apdu[0]
@@ -298,16 +302,25 @@ class SCP02(SCP):
# CMAC on modified APDU # CMAC on modified APDU
mlc = lc + 8 mlc = lc + 8
clac = cla | CLA_SM clac = cla | CLA_SM
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:]) mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
if self.do_cenc: if self.do_cenc:
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8) k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
data = k.encrypt(pad80(apdu[5:], 8)) data = k.encrypt(pad80(data, 8))
lc = len(data) lc = len(data)
else:
data = apdu[5:]
lc += 8 lc += 8
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
# Since we attach a signature, we will always send some data. This means that if the APDU is of case #4
# or case #2, we must attach an additional Le byte to signal that we expect a response. It is technically
# legal to use 0x00 (=256) as Le byte, even when the caller has specified a different value in the original
# APDU. This is due to the fact that Le always describes the maximum expected length of the response
# (see also ISO/IEC 7816-4, section 5.1). In addition to that, it should also important that depending on
# the configuration of the SCP, the response may also contain a signature that makes the response larger
# than specified in the Le field of the original APDU.
if case == 4 or case == 2:
apdu += b'\x00'
return apdu return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes: def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
@@ -454,7 +467,7 @@ class SCP03(SCP):
if len(host_challenge) != self.s_mode: if len(host_challenge) != self.s_mode:
raise ValueError('Host Challenge must be %u bytes long' % self.s_mode) raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
self.host_challenge = host_challenge self.host_challenge = host_challenge
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge + b'\x00'
def parse_init_update_resp(self, resp_bin: bytes): def parse_init_update_resp(self, resp_bin: bytes):
"""Parse response to INITIALIZE UPDATE.""" """Parse response to INITIALIZE UPDATE."""
@@ -489,12 +502,16 @@ class SCP03(SCP):
ins = apdu[1] ins = apdu[1]
p1 = apdu[2] p1 = apdu[2]
p2 = apdu[3] p2 = apdu[3]
lc = apdu[4] (case, lc, le, cmd_data) = parse_command_apdu(apdu)
assert lc == len(apdu) - 5
cmd_data = apdu[5:] # TODO: add support for extended length fields.
assert lc <= 256
assert le <= 256
lc &= 0xFF
le &= 0xFF
if self.do_cenc and not skip_cenc: if self.do_cenc and not skip_cenc:
if lc == 0: if case <= 2:
# No encryption shall be applied to a command where there is no command data field. In this # No encryption shall be applied to a command where there is no command data field. In this
# case, the encryption counter shall still be incremented # case, the encryption counter shall still be incremented
self.sk.block_nr += 1 self.sk.block_nr += 1
@@ -519,6 +536,11 @@ class SCP03(SCP):
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
cmac = self.sk.calc_cmac(apdu) cmac = self.sk.calc_cmac(apdu)
apdu += cmac[:self.s_mode] apdu += cmac[:self.s_mode]
# See comment in SCP03._wrap_cmd_apdu()
if case == 4 or case == 2:
apdu += b'\x00'
return apdu return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes: def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:

View File

@@ -11,7 +11,7 @@ from construct import Construct
from osmocom.utils import b2h, h2b, i2h, Hexstr from osmocom.utils import b2h, h2b, i2h, Hexstr
from pySim.exceptions import * from pySim.exceptions import *
from pySim.utils import SwHexstr, SwMatchstr, ResTuple, sw_match from pySim.utils import SwHexstr, SwMatchstr, ResTuple, sw_match, parse_command_apdu
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
# #
@@ -90,14 +90,16 @@ class LinkBase(abc.ABC):
self.sw_interpreter = sw_interpreter self.sw_interpreter = sw_interpreter
self.apdu_tracer = apdu_tracer self.apdu_tracer = apdu_tracer
self.proactive_handler = proactive_handler self.proactive_handler = proactive_handler
self.apdu_strict = False
@abc.abstractmethod @abc.abstractmethod
def __str__(self) -> str: def __str__(self) -> str:
"""Implementation specific method for printing an information to identify the device.""" """Implementation specific method for printing an information to identify the device."""
@abc.abstractmethod @abc.abstractmethod
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def _send_apdu(self, apdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the PDU.""" """Implementation specific method for sending the APDU. This method must accept APDUs as defined in
ISO/IEC 7816-3, section 12.1 """
def set_sw_interpreter(self, interp): def set_sw_interpreter(self, interp):
"""Set an (optional) status word interpreter.""" """Set an (optional) status word interpreter."""
@@ -134,61 +136,51 @@ class LinkBase(abc.ABC):
self.apdu_tracer.trace_reset() self.apdu_tracer.trace_reset()
return self._reset_card() return self._reset_card()
def send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def send_apdu(self, apdu: Hexstr) -> ResTuple:
"""Sends an APDU with minimal processing """Sends an APDU with minimal processing
Args: Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00") apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
Returns: Returns:
tuple(data, sw), where tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF") data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000") sw : string (in hex) of status word (ex. "9000")
""" """
# To make sure that no invalid APDUs can be passed further down into the transport layer, we parse the APDU.
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
if self.apdu_tracer: if self.apdu_tracer:
self.apdu_tracer.trace_command(pdu) self.apdu_tracer.trace_command(apdu)
(data, sw) = self._send_apdu_raw(pdu)
# Handover APDU to concrete transport layer implementation
(data, sw) = self._send_apdu(apdu)
if self.apdu_tracer: if self.apdu_tracer:
self.apdu_tracer.trace_response(pdu, sw, data) self.apdu_tracer.trace_response(apdu, sw, data)
# The APDU case (See aso ISO/IEC 7816-3, table 12) dictates if we should receive a response or not. If we
# receive a response in an APDU case that does not allow the reception of a respnse we print a warning to
# make the user/caller aware of the problem. Since the transaction is over at this point and data was received
# we count it as a successful transaction anyway, even though the spec was violated. The problem is most likely
# caused by a missing Le field in the APDU. This is an error that the caller/user should correct to avoid
# problems at some later point when a different transport protocol or transport layer implementation is used.
# All APDUs passed to this function must comply to ISO/IEC 7816-3, section 12.
if len(data) > 0 and (case == 3 or case == 1):
exeption_str = 'received unexpected response data, incorrect APDU-case ' + \
'(%d, should be %d, missing Le field?)!' % (case, case + 1)
if self.apdu_strict:
raise ValueError(exeption_str)
else:
print('Warning: %s' % exeption_str)
return (data, sw) return (data, sw)
def send_apdu(self, pdu: Hexstr) -> ResTuple: def send_apdu_checksw(self, apdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and auto fetch response data
Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00")
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
prev_pdu = pdu
data, sw = self.send_apdu_raw(pdu)
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
prev_pdu = pdu_gr
d, sw = self.send_apdu_raw(pdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
pdu_gr = prev_pdu[0:8] + sw[2:4]
data, sw = self.send_apdu_raw(pdu_gr)
return data, sw
def send_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
"""Sends an APDU and check returned SW """Sends an APDU and check returned SW
Args: Args:
pdu : string of hexadecimal characters (ex. "A0A40000023F00") apdu : string of hexadecimal characters (ex. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12.1)
sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain
digits using a '?' to add some ambiguity if needed. digits using a '?' to add some ambiguity if needed.
Returns: Returns:
@@ -196,7 +188,7 @@ class LinkBase(abc.ABC):
data : string (in hex) of returned data (ex. "074F4EFFFF") data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000") sw : string (in hex) of status word (ex. "9000")
""" """
rv = self.send_apdu(pdu) rv = self.send_apdu(apdu)
last_sw = rv[1] last_sw = rv[1]
while sw == '9000' and sw_match(last_sw, '91xx'): while sw == '9000' and sw_match(last_sw, '91xx'):
@@ -247,6 +239,89 @@ class LinkBase(abc.ABC):
raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter) raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter)
return rv return rv
class LinkBaseTpdu(LinkBase):
# Use the T=0 TPDU format by default as this is the most commonly used transport protocol.
protocol = 0
def set_tpdu_format(self, protocol: int):
"""Set TPDU format. Each transport protocol has its specific TPDU format. This method allows the
concrete transport layer implementation to set the TPDU format it expects. (This method must not be
called by higher layers. Switching the TPDU format does not switch the transport protocol that the
reader uses on the wire)
Args:
protocol : number of the transport protocol used. (0 => T=0, 1 => T=1)
"""
self.protocol = protocol
@abc.abstractmethod
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
"""Implementation specific method for sending the resulting TPDU. This method must accept TPDUs as defined in
ETSI TS 102 221, section 7.3.1 and 7.3.2, depending on the protocol selected. """
def _send_apdu(self, apdu: Hexstr) -> ResTuple:
"""Transforms APDU into a TPDU and sends it. The response TPDU is returned as APDU back to the caller.
Args:
apdu : string of hexadecimal characters (eg. "A0A40000023F00", must comply to ISO/IEC 7816-3, section 12)
Returns:
tuple(data, sw), where
data : string (in hex) of returned data (ex. "074F4EFFFF")
sw : string (in hex) of status word (ex. "9000")
"""
if self.protocol == 0:
return self.__send_apdu_T0(apdu)
elif self.protocol == 1:
return self.__send_apdu_transparent(apdu)
raise ValueError('unspported protocol selected (T=%d)' % self.protocol)
def __send_apdu_T0(self, apdu: Hexstr) -> ResTuple:
# Transform the given APDU to the T=0 TPDU format and send it. Automatically fetch the response (case #4 APDUs)
# (see also ETSI TS 102 221, section 7.3.1.1)
# Transform APDU to T=0 TPDU (see also ETSI TS 102 221, section 7.3.1)
(case, _lc, _le, _data) = parse_command_apdu(h2b(apdu))
if case == 1:
# Attach an Le field to all case #1 APDUs (see also ETSI TS 102 221, section 7.3.1.1.1)
tpdu = apdu + '00'
elif case == 4:
# Remove the Le field from all case #4 APDUs (see also ETSI TS 102 221, section 7.3.1.1.4)
tpdu = apdu[:-2]
else:
tpdu = apdu
prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu)
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if sw is not None:
while (sw[0:2] in ['9f', '61', '62', '63']):
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
prev_tpdu = tpdu_gr
d, sw = self.send_tpdu(tpdu_gr)
data += d
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr)
return data, sw
def __send_apdu_transparent(self, apdu: Hexstr) -> ResTuple:
# In cases where the TPDU format is the same as the APDU format, we may pass the given APDU through without modification
# (This is the case for T=1, see also ETSI TS 102 221, section 7.3.2.0.)
return self.send_tpdu(apdu)
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser): def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
"""Add all reader related arguments to the given argparse.Argumentparser instance.""" """Add all reader related arguments to the given argparse.Argumentparser instance."""
from pySim.transport.serial import SerialSimLink from pySim.transport.serial import SerialSimLink

View File

@@ -24,7 +24,7 @@ import argparse
from typing import Optional from typing import Optional
from osmocom.utils import h2b, b2h, Hexstr from osmocom.utils import h2b, b2h, Hexstr
from pySim.transport import LinkBase from pySim.transport import LinkBaseTpdu
from pySim.exceptions import ReaderError, ProtocolError from pySim.exceptions import ReaderError, ProtocolError
from pySim.utils import ResTuple from pySim.utils import ResTuple
@@ -70,12 +70,12 @@ class L1CTLMessageSIM(L1CTLMessage):
L1CTL_SIM_REQ = 0x16 L1CTL_SIM_REQ = 0x16
L1CTL_SIM_CONF = 0x17 L1CTL_SIM_CONF = 0x17
def __init__(self, pdu): def __init__(self, tpdu):
super().__init__(self.L1CTL_SIM_REQ) super().__init__(self.L1CTL_SIM_REQ)
self.data += pdu self.data += tpdu
class CalypsoSimLink(LinkBase): class CalypsoSimLink(LinkBaseTpdu):
"""Transport Link for Calypso based phones.""" """Transport Link for Calypso based phones."""
name = 'Calypso-based (OsmocomBB) reader' name = 'Calypso-based (OsmocomBB) reader'
@@ -129,10 +129,10 @@ class CalypsoSimLink(LinkBase):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False): def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ... pass # Nothing to do really ...
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
# Request FULL reset # Request sending of TPDU
req_msg = L1CTLMessageSIM(h2b(pdu)) req_msg = L1CTLMessageSIM(h2b(tpdu))
self.sock.send(req_msg.gen_msg()) self.sock.send(req_msg.gen_msg())
# Read message length first # Read message length first

View File

@@ -25,14 +25,14 @@ import serial
from osmocom.utils import Hexstr from osmocom.utils import Hexstr
from pySim.utils import ResTuple from pySim.utils import ResTuple
from pySim.transport import LinkBase from pySim.transport import LinkBaseTpdu
from pySim.exceptions import ReaderError, ProtocolError from pySim.exceptions import ReaderError, ProtocolError
# HACK: if somebody needs to debug this thing # HACK: if somebody needs to debug this thing
# log.root.setLevel(log.DEBUG) # log.root.setLevel(log.DEBUG)
class ModemATCommandLink(LinkBase): class ModemATCommandLink(LinkBaseTpdu):
"""Transport Link for 3GPP TS 27.007 compliant modems.""" """Transport Link for 3GPP TS 27.007 compliant modems."""
name = "modem for Generic SIM Access (3GPP TS 27.007)" name = "modem for Generic SIM Access (3GPP TS 27.007)"
@@ -145,12 +145,12 @@ class ModemATCommandLink(LinkBase):
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False): def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
pass # Nothing to do really ... pass # Nothing to do really ...
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
# Make sure pdu has upper case hex digits [A-F] # Make sure pdu has upper case hex digits [A-F]
pdu = pdu.upper() tpdu = tpdu.upper()
# Prepare the command as described in 8.17 # Prepare the command as described in 8.17
cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu) cmd = 'AT+CSIM=%d,\"%s\"' % (len(tpdu), tpdu)
log.debug('Sending command: %s', cmd) log.debug('Sending command: %s', cmd)
# Send AT+CSIM command to the modem # Send AT+CSIM command to the modem
@@ -164,14 +164,14 @@ class ModemATCommandLink(LinkBase):
# Make sure that the response has format: b'+CSIM: %d,\"%s\"' # Make sure that the response has format: b'+CSIM: %d,\"%s\"'
try: try:
result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp) result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
(_rsp_pdu_len, rsp_pdu) = result.groups() (_rsp_tpdu_len, rsp_tpdu) = result.groups()
except Exception as exc: except Exception as exc:
raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
# TODO: make sure we have at least SW # TODO: make sure we have at least SW
data = rsp_pdu[:-4].decode().lower() data = rsp_tpdu[:-4].decode().lower()
sw = rsp_pdu[-4:].decode().lower() sw = rsp_tpdu[-4:].decode().lower()
log.debug('Command response: %s, %s', data, sw) log.debug('Command response: %s, %s', data, sw)
return data, sw return data, sw
def __str__(self) -> str: def __str__(self) -> str:

View File

@@ -30,11 +30,11 @@ from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnect
from osmocom.utils import h2i, i2h, Hexstr from osmocom.utils import h2i, i2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError, ReaderError from pySim.exceptions import NoCardError, ProtocolError, ReaderError
from pySim.transport import LinkBase from pySim.transport import LinkBaseTpdu
from pySim.utils import ResTuple from pySim.utils import ResTuple
class PcscSimLink(LinkBase): class PcscSimLink(LinkBaseTpdu):
""" pySim: PCSC reader transport link.""" """ pySim: PCSC reader transport link."""
name = 'PC/SC' name = 'PC/SC'
@@ -84,8 +84,19 @@ class PcscSimLink(LinkBase):
# is disconnected # is disconnected
self.disconnect() self.disconnect()
# Explicitly select T=0 communication protocol # Make card connection and select a suitable communication protocol
self._con.connect(CardConnection.T0_protocol) self._con.connect()
supported_protocols = self._con.getProtocol();
self.disconnect()
if (supported_protocols & CardConnection.T0_protocol):
protocol = CardConnection.T0_protocol
self.set_tpdu_format(0)
elif (supported_protocols & CardConnection.T1_protocol):
protocol = CardConnection.T1_protocol
self.set_tpdu_format(1)
else:
raise ReaderError('Unsupported card protocol')
self._con.connect(protocol)
except CardConnectionException as exc: except CardConnectionException as exc:
raise ProtocolError() from exc raise ProtocolError() from exc
except NoCardException as exc: except NoCardException as exc:
@@ -102,12 +113,8 @@ class PcscSimLink(LinkBase):
self.connect() self.connect()
return 1 return 1
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
data, sw1, sw2 = self._con.transmit(h2i(tpdu))
apdu = h2i(pdu)
data, sw1, sw2 = self._con.transmit(apdu)
sw = [sw1, sw2] sw = [sw1, sw2]
# Return value # Return value

View File

@@ -24,11 +24,11 @@ import serial
from osmocom.utils import h2b, b2h, Hexstr from osmocom.utils import h2b, b2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError from pySim.exceptions import NoCardError, ProtocolError
from pySim.transport import LinkBase from pySim.transport import LinkBaseTpdu
from pySim.utils import ResTuple from pySim.utils import ResTuple
class SerialSimLink(LinkBase): class SerialSimLink(LinkBaseTpdu):
""" pySim: Transport Link for serial (RS232) based readers included with simcard""" """ pySim: Transport Link for serial (RS232) based readers included with simcard"""
name = 'Serial' name = 'Serial'
@@ -187,13 +187,13 @@ class SerialSimLink(LinkBase):
def _rx_byte(self): def _rx_byte(self):
return self._sl.read() return self._sl.read()
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
pdu = h2b(pdu) tpdu = h2b(tpdu)
data_len = pdu[4] # P3 data_len = tpdu[4] # P3
# Send first CLASS,INS,P1,P2,P3 # Send first CLASS,INS,P1,P2,P3
self._tx_string(pdu[0:5]) self._tx_string(tpdu[0:5])
# Wait ack which can be # Wait ack which can be
# - INS: Command acked -> go ahead # - INS: Command acked -> go ahead
@@ -201,7 +201,7 @@ class SerialSimLink(LinkBase):
# - SW1: The card can apparently proceed ... # - SW1: The card can apparently proceed ...
while True: while True:
b = self._rx_byte() b = self._rx_byte()
if ord(b) == pdu[1]: if ord(b) == tpdu[1]:
break break
if b != '\x60': if b != '\x60':
# Ok, it 'could' be SW1 # Ok, it 'could' be SW1
@@ -214,12 +214,12 @@ class SerialSimLink(LinkBase):
raise ProtocolError() raise ProtocolError()
# Send data (if any) # Send data (if any)
if len(pdu) > 5: if len(tpdu) > 5:
self._tx_string(pdu[5:]) self._tx_string(tpdu[5:])
# Receive data (including SW !) # Receive data (including SW !)
# length = [P3 - tx_data (=len(pdu)-len(hdr)) + 2 (SW1//2) ] # length = [P3 - tx_data (=len(tpdu)-len(hdr)) + 2 (SW1//2) ]
to_recv = data_len - len(pdu) + 5 + 2 to_recv = data_len - len(tpdu) + 5 + 2
data = bytes(0) data = bytes(0)
while len(data) < to_recv: while len(data) < to_recv:

View File

@@ -539,6 +539,52 @@ def boxed_heading_str(heading, width=80):
return res return res
def parse_command_apdu(apdu: bytes) -> int:
"""Parse a given command APDU and return case (see also ISO/IEC 7816-3, Table 12 and Figure 26),
lc, le and the data field.
Args:
apdu : hexstring that contains the command APDU
Returns:
tuple containing case, lc and le values of the APDU (case, lc, le, data)
"""
if len(apdu) == 4:
# Case #1, No command data field, no response data field
lc = 0
le = 0
data = b''
return (1, lc, le, data)
elif len(apdu) == 5:
# Case #2, No command data field, response data field present
lc = 0
le = apdu[4]
if le == 0:
le = 256
data = b''
return (2, lc, le, data)
elif len(apdu) > 5:
lc = apdu[4];
if lc == 0:
lc = 256
data = apdu[5:lc+5]
if len(apdu) == 5 + lc:
# Case #3, Command data field present, no response data field
le = 0
return (3, lc, le, data)
elif len(apdu) == 5 + lc + 1:
# Case #4, Command data field present, no response data field
le = apdu[5 + lc]
if le == 0:
le = 256
return (4, lc, le, data)
else:
raise ValueError('invalid APDU (%s), Lc=0x%02x (%d) does not match the length (%d) of the data field'
% (b2h(apdu), lc, lc, len(apdu[5:])))
else:
raise ValueError('invalid APDU (%s), too short!' % b2h(apdu))
class DataObject(abc.ABC): class DataObject(abc.ABC):
"""A DataObject (DO) in the sense of ISO 7816-4. Contrary to 'normal' TLVs where one """A DataObject (DO) in the sense of ISO 7816-4. Contrary to 'normal' TLVs where one
simply has any number of different TLVs that may occur in any order at any point, ISO 7816 simply has any number of different TLVs that may occur in any order at any point, ISO 7816

View File

View File

@@ -0,0 +1,70 @@
# Testsuite for pySim-shell.py
#
# (C) 2024 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import os
from utils import *
class test_case(UnittestUtils):
def test_apdu_legacy(self):
cardname = 'sysmoISIM-SJA5-S17'
self.runPySimShell(cardname, "test_apdu_legacy.script", no_exceptions = True)
def test_apdu_legacy_scp02(self):
cardname = 'sysmoISIM-SJA5-S17'
self.equipTemplate("test_apdu_legacy_scp02.script", SEC_LEVEL = 3)
self.runPySimShell(cardname, "test_apdu_legacy_scp02.script", no_exceptions = True, add_csv = True)
self.equipTemplate("test_apdu_legacy_scp02.script", SEC_LEVEL = 1)
self.runPySimShell(cardname, "test_apdu_legacy_scp02.script", no_exceptions = True, add_csv = True)
def test_apdu_legacy_scp03(self):
cardname = 'sysmoEUICC1-C2T'
self.equipTemplate("test_apdu_legacy_scp03.script", SEC_LEVEL = 3)
self.runPySimShell(cardname, "test_apdu_legacy_scp03.script", no_exceptions = True, add_csv = True)
self.equipTemplate("test_apdu_legacy_scp03.script", SEC_LEVEL = 1)
self.runPySimShell(cardname, "test_apdu_legacy_scp03.script", no_exceptions = True, add_csv = True)
def test_apdu(self):
cardname = 'sysmoISIM-SJA5-S17'
self.runPySimShell(cardname, "test_apdu.script", no_exceptions = True)
def test_apdu_legacy_scp02(self):
cardname = 'sysmoISIM-SJA5-S17'
self.equipTemplate("test_apdu_scp02.script", SEC_LEVEL = 3)
self.runPySimShell(cardname, "test_apdu_scp02.script", no_exceptions = True, add_csv = True)
self.equipTemplate("test_apdu_scp02.script", SEC_LEVEL = 1)
self.runPySimShell(cardname, "test_apdu_scp02.script", no_exceptions = True, add_csv = True)
def test_apdu_legacy_scp03(self):
cardname = 'sysmoEUICC1-C2T'
self.equipTemplate("test_apdu_scp03.script", SEC_LEVEL = 3)
self.runPySimShell(cardname, "test_apdu_scp03.script", no_exceptions = True, add_csv = True)
self.equipTemplate("test_apdu_scp03.script", SEC_LEVEL = 1)
self.runPySimShell(cardname, "test_apdu_scp03.script", no_exceptions = True, add_csv = True)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,20 @@
set debug true
set echo true
set apdu_trace true
set apdu_strict true
# Case #1: (open channel #1)
# No command data field, No response data field present
apdu 00700001 --expect-sw 9000 --expect-response-regex '^$'
# Case #2: (status)
# No command data field, Response data field present
apdu 80F2000000 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$'
# Case #3: (terminal capability)
# Command data field present, No response data field
apdu 80AA000005a903830180 --expect-sw 9000 --expect-response-regex '^$'
# Case #4: (select MF)
# Command data field present, Response data field present
apdu 00a40004023f0000 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$'

View File

@@ -0,0 +1,21 @@
set debug true
set echo true
set apdu_trace true
# Case #1: (open channel #1)
# No command data field, No response data field present
# (in ISO/IEC 7816-3 format, this APDU would lack the 0x00 at the end)
apdu 0070000100 --expect-sw 9000 --expect-response-regex '^$'
# Case #2: (status)
# No command data field, Response data field present
apdu 80F2000000 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$'
# Case #3: (terminal capability)
# Command data field present, No response data field
apdu 80AA000005a903830180 --expect-sw 9000 --expect-response-regex '^$'
# Case #4: (select MF)
# Command data field present, Response data field present
# (in ISO/IEC 7816-3 format, this APDU would have an additional 0x00 at the end)
apdu 00a40004023f00 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$'

View File

@@ -0,0 +1,27 @@
set debug true
set echo true
set apdu_trace true
# Establish secure channel:
select ADF.ISD
establish_scp02 --key-provider-suffix 1 --key-ver 112 --security-level $SEC_LEVEL
# Case #1: (get status with no data field to mimic a case #1 APDU)
# No command data field, No response data field present
# (in ISO/IEC 7816-3 format, this APDU would lack the 0x00 at the end)
apdu 80F2200200 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #2: (get data)
# No command data field, Response data field present
apdu 80ca006600 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
# Case #3: (get status with wrong parameters to mimic a case #3 APDU)
# Command data field present, No response data field
apdu 80F220020a4f0212345c054f9f70c5 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #4: (initialize update, to mimic a case #4 APDU, this will unfortunately kill the session but we are done anyway)
# Command data field present, Response data field present
# (in ISO/IEC 7816-3 format, this APDU would have an additional 0x00 at the end)
apdu 805000000855baa7eca1cd629e --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
release_scp

View File

@@ -0,0 +1,27 @@
set debug true
set echo true
set apdu_trace true
# Establish secure channel:
select ADF.ISD-R
establish_scp03 --key-provider-suffix 1 --key-ver 50 --security-level $SEC_LEVEL
# Case #1: (get status with no data field to mimic a case #1 APDU)
# No command data field, No response data field present
# (in ISO/IEC 7816-3 format, this APDU would lack the 0x00 at the end)
apdu 80F2200200 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #2: (get data)
# No command data field, Response data field present
apdu 80ca006600 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
# Case #3: (get status with wrong parameters to mimic a case #3 APDU)
# Command data field present, No response data field
apdu 80F220020a4f0212345c054f9f70c5 --expect-sw 6a88 --expect-response-regex '^$$'
# Case #4: (get eid)
# Command data field present, Response data field present
# (in ISO/IEC 7816-3 format, this APDU would have an additional 0x00 at the end)
apdu 80E2910006bf3e035c015a --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
release_scp

View File

@@ -0,0 +1,26 @@
set debug true
set echo true
set apdu_trace true
set apdu_strict true
# Establish secure channel:
select ADF.ISD
establish_scp02 --key-provider-suffix 1 --key-ver 112 --security-level $SEC_LEVEL
# Case #1: (get status with no data field to mimic a case #1 APDU)
# No command data field, No response data field present
apdu 80F22002 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #2: (get data)
# No command data field, Response data field present
apdu 80ca006600 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
# Case #3: (get status with wrong parameters to mimic a case #3 APDU)
# Command data field present, No response data field
apdu 80F220020a4f0212345c054f9f70c5 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #4: (initialize update, to mimic a case #4 APDU, this will unfortunately kill the session but we are done anyway)
# Command data field present, Response data field present
apdu 805000000855baa7eca1cd629e00 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
release_scp

View File

@@ -0,0 +1,26 @@
set debug true
set echo true
set apdu_trace true
set apdu_strict true
# Establish secure channel:
select ADF.ISD-R
establish_scp03 --key-provider-suffix 1 --key-ver 50 --security-level $SEC_LEVEL
# Case #1: (get status with no data field to mimic a case #1 APDU)
# No command data field, No response data field present
apdu 80F22002 --expect-sw 6a80 --expect-response-regex '^$$'
# Case #2: (get data)
# No command data field, Response data field present
apdu 80ca006600 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
# Case #3: (get status with wrong parameters to mimic a case #3 APDU)
# Command data field present, No response data field
apdu 80F220020a4f0212345c054f9f70c5 --expect-sw 6a88 --expect-response-regex '^$$'
# Case #4: (get eid)
# Command data field present, Response data field present
apdu 80E2910006bf3e035c015a00 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$$'
release_scp

View File

@@ -1,6 +1,6 @@
regenerate: False regenerate: False
keepfiles: False keepfiles: False
print_content: False print_content: True
cards: cards:
- name : "sysmoISIM-SJA5-S17" - name : "sysmoISIM-SJA5-S17"
atr : "3B9F96801F878031E073FE211B674A357530350265F8" atr : "3B9F96801F878031E073FE211B674A357530350265F8"

View File

@@ -1,8 +1,8 @@
-> 0070000100 -> 00700001
<- 9000: <- 9000:
-> 0070000200 -> 00700002
<- 9000: <- 9000:
-> 0070000300 -> 00700003
<- 9000: <- 9000:
currently selected file: MF/DF.TELECOM/EF.MSISDN (3f00/7f10/6f40) currently selected file: MF/DF.TELECOM/EF.MSISDN (3f00/7f10/6f40)
currently selected file: MF/ADF.USIM/EF.IMSI (3f00/a0000000871002/6f07) currently selected file: MF/ADF.USIM/EF.IMSI (3f00/a0000000871002/6f07)
@@ -12,9 +12,9 @@ currently selected file: MF/ADF.ISIM/EF.AD (3f00/a0000000871004/6fad)
"response_all_ref_ar_do": null "response_all_ref_ar_do": null
} }
] ]
-> 0070800100 -> 00708001
<- 9000: <- 9000:
-> 0070800200 -> 00708002
<- 9000: <- 9000:
-> 0070800300 -> 00708003
<- 9000: <- 9000:

View File

@@ -36,14 +36,14 @@ class SCP02_Auth_Test(unittest.TestCase):
def test_mutual_auth_success(self): def test_mutual_auth_success(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge) init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8') self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F800')
self.scp02.parse_init_update_resp(self.init_update_resp) self.scp02.parse_init_update_resp(self.init_update_resp)
ext_auth_cmd = self.scp02.gen_ext_auth_apdu() ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376') self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376')
def test_mutual_auth_fail_card_cryptogram(self): def test_mutual_auth_fail_card_cryptogram(self):
init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge) init_upd_cmd = self.scp02.gen_init_update_apdu(host_challenge=self.host_challenge)
self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F8') self.assertEqual(b2h(init_upd_cmd).upper(), '805020000840A62C37FA6304F800')
wrong_init_update_resp = self.init_update_resp.copy() wrong_init_update_resp = self.init_update_resp.copy()
wrong_init_update_resp[-1:] = b'\xff' wrong_init_update_resp[-1:] = b'\xff'
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@@ -61,15 +61,32 @@ class SCP02_Test(unittest.TestCase):
ext_auth_cmd = self.scp02.gen_ext_auth_apdu() ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
def test_mac_command(self): def test_mac_command(self):
wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f00')) # Case #1: No command data field, No response data field present
self.assertEqual(b2h(wrapped).upper(), '84F280020A4F00B21AAFA3EB2D1672') wrapped = self.scp02.wrap_cmd_apdu(h2b('80F22002'))
self.assertEqual(b2h(wrapped).upper(), '84F220020814DB34FA4341DCA8')
# Case #2: No command data field, Response data field present
wrapped = self.scp02.wrap_cmd_apdu(h2b('80ca006600'))
self.assertEqual(b2h(wrapped).upper(), '84CA00660855ED7C5FF069512B00')
# Case #3: Command data field present, No response data field
wrapped = self.scp02.wrap_cmd_apdu(h2b('80F220020a4f0212345c054f9f70c5'))
self.assertEqual(b2h(wrapped).upper(), '84F22002124F0212345C054F9F70C58FC1B380C4228AF8')
# Case #4: Command data field present, Response data field present
wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f0000'))
self.assertEqual(b2h(wrapped).upper(), '84F280020A4F003B95F09317DE6A4E00')
class SCP03_Test: class SCP03_Test:
"""some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all """some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all
of our SCP03 test caseses.""" of our SCP03 test caseses."""
get_eid_cmd_plain = h2b('80E2910006BF3E035C015A') get_eid_cmd_plain = h2b('80E2910006BF3E035C015A00')
get_eid_rsp_plain = h2b('bf3e125a1089882119900000000000000000000005') get_eid_rsp_plain = h2b('bf3e125a1089882119900000000000000000000005')
case_1_apdu_plain = h2b('80F22002')
case_2_apdu_plain = h2b('80ca006600')
case_3_apdu_plain = h2b('80F220020a4f0212345c054f9f70c5')
case_4_apdu_plain = h2b('80f28002024f0000')
# must be overridden by derived classes # must be overridden by derived classes
init_upd_cmd = b'' init_upd_cmd = b''
@@ -81,7 +98,7 @@ class SCP03_Test:
@property @property
def host_challenge(self) -> bytes: def host_challenge(self) -> bytes:
return self.init_upd_cmd[5:] return self.init_upd_cmd[5:-1]
@property @property
def kvn(self) -> int: def kvn(self) -> int:
@@ -128,6 +145,21 @@ class SCP03_Test:
# pylint: disable=no-member # pylint: disable=no-member
self.assertEqual(self.get_eid_rsp_plain, self.scp.unwrap_rsp_apdu(h2b('9000'), self.get_eid_rsp)) self.assertEqual(self.get_eid_rsp_plain, self.scp.unwrap_rsp_apdu(h2b('9000'), self.get_eid_rsp))
def test_06_mac_command(self):
# pylint: disable=no-member
# Case #1: No command data field, No response data field present
self.assertEqual(self.case_1_apdu, self.scp.wrap_cmd_apdu(self.case_1_apdu_plain))
# Case #2: No command data field, Response data field present
self.assertEqual(self.case_2_apdu, self.scp.wrap_cmd_apdu(self.case_2_apdu_plain))
# Case #3: Command data field present, No response data field
self.assertEqual(self.case_3_apdu, self.scp.wrap_cmd_apdu(self.case_3_apdu_plain))
# Case #4: Command data field present, Response data field present
self.assertEqual(self.case_4_apdu, self.scp.wrap_cmd_apdu(self.case_4_apdu_plain))
# The SCP03 keysets used for various key lenghs # The SCP03 keysets used for various key lenghs
KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f')) KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f'))
@@ -139,75 +171,111 @@ KEYSET_AES256 = GpCardKeyset(0x32, h2b('000102030405060708090a0b0c0d0e0f00010203
class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase): class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128 keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008b13e5f938fc108c4') init_upd_cmd = h2b('8050300008b13e5f938fc108c400')
init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002') init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002')
ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce') ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce')
get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a28297') get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a2829700')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005971be68992dbbdfa') get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005971be68992dbbdfa')
case_1_apdu = h2b('84f220020863a63f8959827fb2')
case_2_apdu = h2b('84ca006608a0c6a4a74166f7ce00')
case_3_apdu = h2b('84f22002124f0212345c054f9f70c52249b50272656536')
case_4_apdu = h2b('84f280020a4f00e91443f6dce6b8ed00')
class SCP03_Test_AES128_03(SCP03_Test, unittest.TestCase): class SCP03_Test_AES128_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128 keyset = KEYSET_AES128
init_upd_cmd = h2b('80503000088e1552d0513c60f3') init_upd_cmd = h2b('80503000088e1552d0513c60f300')
init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001') init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001')
ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21') ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21')
get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c') get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c00')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005') get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
case_1_apdu = h2b('84f2200208c9811b11f1264cf1')
case_2_apdu = h2b('84ca006608e10ab60b3054798800')
case_3_apdu = h2b('84f22002184e2908bdb48b2315a55482e9e936ca122d6ecfae7d17416e')
case_4_apdu = h2b('84f28002180dd10a6b6193e5340b9e77d32d5a179cd710ac2773aefb2800')
class SCP03_Test_AES128_33(SCP03_Test, unittest.TestCase): class SCP03_Test_AES128_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES128 keyset = KEYSET_AES128
init_upd_cmd = h2b('8050300008fdf38259a1e0de44') init_upd_cmd = h2b('8050300008fdf38259a1e0de4400')
init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003') init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003')
ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0') ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0')
get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f') get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f00')
get_eid_rsp = h2b('f3ba2b1013aa6224f5e1c138d71805c569e5439b47576260b75fc021b25097cb2e68f8a0144975b9') get_eid_rsp = h2b('f3ba2b1013aa6224f5e1c138d71805c569e5439b47576260b75fc021b25097cb2e68f8a0144975b9')
case_1_apdu = h2b('84f2200208ac6a59024bed84cc')
case_2_apdu = h2b('84ca006608409912ad8fb7aed000')
case_3_apdu = h2b('84f22002185f3dafc3ac14c381536a488bf44e06d056df9d74dbd21e5a')
case_4_apdu = h2b('84f280021865165105be3373347d0424d4400af2ac393f569ec779389e00')
class SCP03_Test_AES192_11(SCP03_Test, unittest.TestCase): class SCP03_Test_AES192_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192 keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100087396430b768b085b') init_upd_cmd = h2b('80503100087396430b768b085b00')
init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003') init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003')
ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244') ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244')
get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d143944') get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d14394400')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005162fbd33e04940a9') get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005162fbd33e04940a9')
case_1_apdu = h2b('84f22002084584e4f6784811ee')
case_2_apdu = h2b('84ca006608937776ebe190fa3000')
case_3_apdu = h2b('84f22002124f0212345c054f9f70c59a52bddf3040368c')
case_4_apdu = h2b('84f280020a4f009804b11411f7393d00')
class SCP03_Test_AES192_03(SCP03_Test, unittest.TestCase): class SCP03_Test_AES192_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192 keyset = KEYSET_AES192
init_upd_cmd = h2b('805031000869c65da8202bf19f') init_upd_cmd = h2b('805031000869c65da8202bf19f00')
init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001') init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001')
ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1') ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1')
get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d') get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d00')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005') get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
case_1_apdu = h2b('84f2200208964e188f0b1bb697')
case_2_apdu = h2b('84ca006608f0820035a41d3e1800')
case_3_apdu = h2b('84f220021806b076ed452cd1fa84f77f5c08a146aa77a9286757dea791')
case_4_apdu = h2b('84f2800218d06527e39222dce091fabdb8e9b898417a67a6852d3577db00')
class SCP03_Test_AES192_33(SCP03_Test, unittest.TestCase): class SCP03_Test_AES192_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES192 keyset = KEYSET_AES192
init_upd_cmd = h2b('80503100089b3f2eef0e8c9374') init_upd_cmd = h2b('80503100089b3f2eef0e8c937400')
init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002') init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002')
ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf') ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf')
get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de589') get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de58900')
get_eid_rsp = h2b('c05176c1b6f72aae50c32cbee63b0e95998928fd4dfb2be9f27ffde8c8476f5909b4805cc4039599') get_eid_rsp = h2b('c05176c1b6f72aae50c32cbee63b0e95998928fd4dfb2be9f27ffde8c8476f5909b4805cc4039599')
case_1_apdu = h2b('84f2200208d5d97754b6b3d2ba')
case_2_apdu = h2b('84ca006608516c82b8e30adbeb00')
case_3_apdu = h2b('84f2200218cc247f4761e6944277a4e0d6e32e44025b1e31537e2fc668')
case_4_apdu = h2b('84f2800218ba22b63d509bef5d093b43e5eaed03ed23144ab2d9cb51de00')
class SCP03_Test_AES256_11(SCP03_Test, unittest.TestCase): class SCP03_Test_AES256_11(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256 keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000811666d57866c6f54') init_upd_cmd = h2b('805032000811666d57866c6f5400')
init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003') init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003')
ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310') ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310')
get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d') get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d00')
get_eid_rsp = h2b('bf3e125a10898821199000000000000000000000058012dd7f01f1c4c1') get_eid_rsp = h2b('bf3e125a10898821199000000000000000000000058012dd7f01f1c4c1')
case_1_apdu = h2b('84f2200208d618b7da68d5fe52')
case_2_apdu = h2b('84ca0066088f3e055db23ad5e500')
case_3_apdu = h2b('84f22002124f0212345c054f9f70c5b6e15cc42404915e')
case_4_apdu = h2b('84f280020a4f00aa124aa74afe7f7500')
class SCP03_Test_AES256_03(SCP03_Test, unittest.TestCase): class SCP03_Test_AES256_03(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256 keyset = KEYSET_AES256
init_upd_cmd = h2b('8050320008c6066990fc426e1d') init_upd_cmd = h2b('8050320008c6066990fc426e1d00')
init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001') init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001')
ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9') ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9')
get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe44') get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe4400')
get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005') get_eid_rsp = h2b('bf3e125a1089882119900000000000000000000005')
case_1_apdu = h2b('84f2200208480ddc8e419da38d')
case_2_apdu = h2b('84ca0066083e9d6a6c0b2d732000')
case_3_apdu = h2b('84f22002183ebfef2da8b04af2a85f491f299b76973df76ff08a4031be')
case_4_apdu = h2b('84f2800218783fff80990f5585b1055010ea95094a26e4a8f1ef4b18e100')
class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase): class SCP03_Test_AES256_33(SCP03_Test, unittest.TestCase):
keyset = KEYSET_AES256 keyset = KEYSET_AES256
init_upd_cmd = h2b('805032000897b2055fe58599fd') init_upd_cmd = h2b('805032000897b2055fe58599fd00')
init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002') init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002')
ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057') ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057')
get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea') get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea00')
get_eid_rsp = h2b('ea90dbfa648a67c5eb6abc57f8530b97d0cd5647c5e8732016b55203b078dd2ace7f8bc5d1c1cd99') get_eid_rsp = h2b('ea90dbfa648a67c5eb6abc57f8530b97d0cd5647c5e8732016b55203b078dd2ace7f8bc5d1c1cd99')
case_1_apdu = h2b('84f2200208bcc5c17275545d93')
case_2_apdu = h2b('84ca00660804806aba9d543bb600')
case_3_apdu = h2b('84f2200218717222491556ec81a45f49ce48be33320024801a1c4cb0e0')
case_4_apdu = h2b('84f2800218561f105bccd3a1642904b251ccc1228beb80a82370a8637000')
# FIXME: # FIXME:
# - for S8 and S16 mode # - for S8 and S16 mode

View File

@@ -4,6 +4,7 @@ import unittest
from pySim import utils from pySim import utils
from pySim.legacy import utils as legacy_utils from pySim.legacy import utils as legacy_utils
from pySim.ts_31_102 import EF_SUCI_Calc_Info from pySim.ts_31_102 import EF_SUCI_Calc_Info
from osmocom.utils import h2b
# we don't really want to thest TS 102 221, but the underlying DataObject codebase # we don't really want to thest TS 102 221, but the underlying DataObject codebase
from pySim.ts_102_221 import AM_DO_EF, AM_DO_DF, SC_DO from pySim.ts_102_221 import AM_DO_EF, AM_DO_DF, SC_DO
@@ -189,5 +190,19 @@ class TestLuhn(unittest.TestCase):
# 18 digits; we expect luhn check digit to be added # 18 digits; we expect luhn check digit to be added
self.assertEqual(utils.sanitize_iccid('898821100000053008'), '8988211000000530082') self.assertEqual(utils.sanitize_iccid('898821100000053008'), '8988211000000530082')
class TestUtils(unittest.TestCase):
def test_parse_command_apdu(self):
# Case #1 APDU:
self.assertEqual(utils.parse_command_apdu(h2b('41414141')), (1,0,0,h2b('')))
# Case #2 APDU:
self.assertEqual(utils.parse_command_apdu(h2b('414141410F')), (2,0,15,h2b('')))
self.assertEqual(utils.parse_command_apdu(h2b('4141414100')), (2,0,256,h2b('')))
# Case #3 APDU:
self.assertEqual(utils.parse_command_apdu(h2b('41414141081122334455667788')), (3,8,0,h2b('1122334455667788')))
self.assertEqual(utils.parse_command_apdu(h2b('4141414100' + 256 * '42')), (3,256,0,h2b(256 * '42')))
# Case #4 APDU:
self.assertEqual(utils.parse_command_apdu(h2b('4141414108112233445566778804')), (4,8,4,h2b('1122334455667788')))
self.assertEqual(utils.parse_command_apdu(h2b('4141414100' + 256 * '42' + '00')), (4,256,256,h2b(256 * '42')))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()