dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/38657?usp=email )
Change subject: pySim/transport add support for T=1 protocol and fix APDU/TPDU layer conflicts ......................................................................
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.
Change-Id: I8b56d7804a2b4c392f43f8540e0b6e70001a8970 --- M pySim-read.py M pySim-shell.py M pySim-trace.py M pySim/commands.py M pySim/euicc.py M pySim/global_platform/__init__.py M pySim/global_platform/scp.py M pySim/transport/__init__.py M pySim/transport/calypso.py M pySim/transport/modem_atcmd.py M pySim/transport/pcsc.py M pySim/transport/serial.py M pySim/utils.py A tests/pySim-shell_test/apdu/__init__.py A tests/pySim-shell_test/apdu/test.py A tests/pySim-shell_test/apdu/test_apdu.script A tests/pySim-shell_test/apdu/test_apdu_legacy.script A tests/pySim-shell_test/apdu/test_apdu_legacy_scp02.template A tests/pySim-shell_test/apdu/test_apdu_legacy_scp03.template A tests/pySim-shell_test/apdu/test_apdu_scp02.template A tests/pySim-shell_test/apdu/test_apdu_scp03.template M tests/pySim-shell_test/config.yaml M tests/pySim-shell_test/lchan/test.ok M tests/unittests/test_globalplatform.py M tests/unittests/test_utils.py 25 files changed, 604 insertions(+), 144 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/57/38657/1
diff --git a/pySim-read.py b/pySim-read.py index 4ede6dd..91e17ae 100755 --- a/pySim-read.py +++ b/pySim-read.py @@ -88,7 +88,7 @@ scc.sel_ctrl = "0004"
# 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': # Just a Classic SIM scc.cla_byte = "a0" diff --git a/pySim-shell.py b/pySim-shell.py index b2de874..08f00f9 100755 --- a/pySim-shell.py +++ b/pySim-shell.py @@ -114,6 +114,7 @@ self.conserve_write = True self.json_pretty_print = True self.apdu_trace = False + self.apdu_strict = False
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self, onchange_cb=self._onchange_numeric_path)) @@ -122,6 +123,9 @@ 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, 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)
def equip(self, card, rs): @@ -198,6 +202,13 @@ else: 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): def __init__(self, cmd2_app): self.cmd2 = cmd2_app diff --git a/pySim-trace.py b/pySim-trace.py index 46d4c40..a45fcfc 100755 --- a/pySim-trace.py +++ b/pySim-trace.py @@ -55,7 +55,7 @@ def __str__(self): return "dummy"
- def _send_apdu_raw(self, pdu): + def _send_apdu(self, pdu): #print("DummySimLink-apdu: %s" % pdu) return [], '9000'
diff --git a/pySim/commands.py b/pySim/commands.py index 0173f70..af9f247 100644 --- a/pySim/commands.py +++ b/pySim/commands.py @@ -142,8 +142,9 @@ Tuple of (decoded_data, sw) """ cmd = cmd_constr.build(cmd_data) if cmd_data else '' - p3 = i2h([len(cmd)]) - pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)]) + lc = i2h([len(cmd)]) if cmd_data else '' + 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) if data: # filter the resulting dict to avoid '_io' members inside @@ -247,7 +248,7 @@ if not isinstance(dir_list, list): dir_list = [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)) if sw != '9000': return rv @@ -277,11 +278,11 @@ 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: """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: """Execute SELECT a given Applicaiton ADF. @@ -291,7 +292,7 @@ """
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: """Execute READD BINARY. @@ -494,9 +495,9 @@ # TS 102 221 Section 11.3.1 low-level helper def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple: if first: - pdu = '80cb008001%02x' % (tag) + pdu = '80cb008001%02x00' % (tag) else: - pdu = '80cb000000' + pdu = '80cb0000' return self.send_apdu_checksw(pdu)
def retrieve_data(self, ef: Path, tag: int) -> ResTuple: @@ -569,7 +570,7 @@ if len(rand) != 32: raise ValueError('Invalid rand') 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: """Execute AUTHENTICATE (USIM/ISIM). @@ -602,7 +603,7 @@
def status(self) -> ResTuple: """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: """Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14.""" @@ -651,7 +652,7 @@ p1 = 0x80 else: 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)
def reset_card(self) -> Hexstr: diff --git a/pySim/euicc.py b/pySim/euicc.py index 069a5c7..e9321d6 100644 --- a/pySim/euicc.py +++ b/pySim/euicc.py @@ -332,7 +332,7 @@ 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. 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)
@staticmethod diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py index 53e13e8..fddab4e 100644 --- a/pySim/global_platform/__init__.py +++ b/pySim/global_platform/__init__.py @@ -580,7 +580,7 @@ {'last_block': len(remainder) == 0, 'encryption': encryption, 'structure': structure, 'response': response_permitted}) 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 response += data return data @@ -646,7 +646,7 @@ 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) 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
get_status_parser = argparse.ArgumentParser() @@ -671,7 +671,7 @@ grd_list = [] while True: 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) while len(remainder): # tlv sequence, each element is one GpRegistryRelatedData() @@ -752,7 +752,7 @@ self.install(p1, 0x00, b2h(ifi_bytes))
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)
del_cc_parser = argparse.ArgumentParser() diff --git a/pySim/global_platform/scp.py b/pySim/global_platform/scp.py index ebdd880..ead4c26 100644 --- a/pySim/global_platform/scp.py +++ b/pySim/global_platform/scp.py @@ -24,7 +24,7 @@ from construct import Optional as COptional from osmocom.utils import b2h from osmocom.tlv import bertlv_parse_len, bertlv_encode_len - +from pySim.utils import parse_command_apdu from pySim.secure_channel import SecureChannel
logger = logging.getLogger(__name__) @@ -248,7 +248,7 @@ def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes: """Generate INITIALIZE UPDATE APDU.""" 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): """Parse response to INITIALZIE UPDATE.""" @@ -277,9 +277,13 @@ """Wrap Command APDU for SCP02: calculate MAC and encrypt.""" logger.debug("wrap_cmd_apdu(%s)", b2h(apdu)) if self.do_cmac: - lc = len(apdu) - 5 - 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) + (case, lc, le, data) = parse_command_apdu(apdu) + + # 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 = apdu[0] @@ -295,17 +299,25 @@ # CMAC on modified APDU mlc = lc + 8 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: 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) - else: - data = apdu[5:]
lc += 8 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
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes: @@ -452,7 +464,7 @@ if len(host_challenge) != self.s_mode: raise ValueError('Host Challenge must be %u bytes long' % self.s_mode) 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): """Parse response to INITIALIZE UPDATE.""" @@ -484,12 +496,16 @@ ins = apdu[1] p1 = apdu[2] p2 = apdu[3] - lc = apdu[4] - assert lc == len(apdu) - 5 - cmd_data = apdu[5:] + (case, lc, le, cmd_data) = parse_command_apdu(apdu) + + # 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 lc == 0: + if case <= 2: # 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 self.sk.block_nr += 1 @@ -515,6 +531,10 @@ cmac = self.sk.calc_cmac(apdu) apdu += cmac[:self.s_mode]
+ # See comment in SCP03._wrap_cmd_apdu() + if case == 4 or case == 2: + apdu += b'\x00' + return apdu
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes: diff --git a/pySim/transport/__init__.py b/pySim/transport/__init__.py index 9482528..5743978 100644 --- a/pySim/transport/__init__.py +++ b/pySim/transport/__init__.py @@ -11,7 +11,7 @@ from osmocom.utils import b2h, h2b, i2h, Hexstr
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
# @@ -90,14 +90,16 @@ self.sw_interpreter = sw_interpreter self.apdu_tracer = apdu_tracer self.proactive_handler = proactive_handler + self.apdu_strict = False
@abc.abstractmethod def __str__(self) -> str: """Implementation specific method for printing an information to identify the device."""
@abc.abstractmethod - def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: - """Implementation specific method for sending the PDU.""" + def _send_apdu(self, apdu: Hexstr) -> ResTuple: + """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): """Set an (optional) status word interpreter.""" @@ -134,61 +136,51 @@ self.apdu_tracer.trace_reset() 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
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: tuple(data, sw), where data : string (in hex) of returned data (ex. "074F4EFFFF") 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: - self.apdu_tracer.trace_command(pdu) - (data, sw) = self._send_apdu_raw(pdu) + self.apdu_tracer.trace_command(apdu) + + # Handover APDU to concrete transport layer implementation + (data, sw) = self._send_apdu(apdu) + 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)
- def send_apdu(self, pdu: Hexstr) -> 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: + def send_apdu_checksw(self, apdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple: """Sends an APDU and check returned SW
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 digits using a '?' to add some ambiguity if needed. Returns: @@ -196,7 +188,7 @@ data : string (in hex) of returned data (ex. "074F4EFFFF") sw : string (in hex) of status word (ex. "9000") """ - rv = self.send_apdu(pdu) + rv = self.send_apdu(apdu) last_sw = rv[1]
while sw == '9000' and sw_match(last_sw, '91xx'): @@ -247,6 +239,89 @@ raise SwMatchError(rv[1], sw.lower(), self.sw_interpreter) return rv
+ +class TpduAdaptor(): + + # 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_T1(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_T1(self, apdu: Hexstr) -> ResTuple: + # The T=1 TPDU format is the same as the APDU format, so we may pass the given APDU through without modification + # (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): """Add all reader related arguments to the given argparse.Argumentparser instance.""" from pySim.transport.serial import SerialSimLink diff --git a/pySim/transport/calypso.py b/pySim/transport/calypso.py index 9462939..0e2b7b9 100644 --- a/pySim/transport/calypso.py +++ b/pySim/transport/calypso.py @@ -24,7 +24,7 @@ from typing import Optional from osmocom.utils import h2b, b2h, Hexstr
-from pySim.transport import LinkBase +from pySim.transport import LinkBase, TpduAdaptor from pySim.exceptions import ReaderError, ProtocolError from pySim.utils import ResTuple
@@ -70,12 +70,12 @@ L1CTL_SIM_REQ = 0x16 L1CTL_SIM_CONF = 0x17
- def __init__(self, pdu): + def __init__(self, tpdu): super().__init__(self.L1CTL_SIM_REQ) - self.data += pdu + self.data += tpdu
-class CalypsoSimLink(LinkBase): +class CalypsoSimLink(TpduAdaptor, LinkBase): """Transport Link for Calypso based phones.""" name = 'Calypso-based (OsmocomBB) reader'
@@ -129,10 +129,10 @@ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False): pass # Nothing to do really ...
- def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: + def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
- # Request FULL reset - req_msg = L1CTLMessageSIM(h2b(pdu)) + # Request sending of TPDU + req_msg = L1CTLMessageSIM(h2b(tpdu)) self.sock.send(req_msg.gen_msg())
# Read message length first diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py index 8970f7d..21879a3 100644 --- a/pySim/transport/modem_atcmd.py +++ b/pySim/transport/modem_atcmd.py @@ -25,14 +25,14 @@ from osmocom.utils import Hexstr
from pySim.utils import ResTuple -from pySim.transport import LinkBase +from pySim.transport import LinkBase, TpduAdaptor from pySim.exceptions import ReaderError, ProtocolError
# HACK: if somebody needs to debug this thing # log.root.setLevel(log.DEBUG)
-class ModemATCommandLink(LinkBase): +class ModemATCommandLink(TpduAdaptor, LinkBase): """Transport Link for 3GPP TS 27.007 compliant modems.""" name = "modem for Generic SIM Access (3GPP TS 27.007)"
@@ -145,12 +145,12 @@ def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False): 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] - pdu = pdu.upper() + tpdu = tpdu.upper()
# 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)
# Send AT+CSIM command to the modem @@ -164,14 +164,14 @@ # Make sure that the response has format: b'+CSIM: %d,"%s"' try: 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: raise ReaderError('Failed to parse response from modem: %s' % rsp) from exc
# TODO: make sure we have at least SW - data = rsp_pdu[:-4].decode().lower() - sw = rsp_pdu[-4:].decode().lower() - log.debug('Command response: %s, %s', data, sw) + data = rsp_tpdu[:-4].decode().lower() + sw = rsp_tpdu[-4:].decode().lower() + log.debug('Command response: %s, %s', data, sw) return data, sw
def __str__(self) -> str: diff --git a/pySim/transport/pcsc.py b/pySim/transport/pcsc.py index 207cf6c..3ad9919 100644 --- a/pySim/transport/pcsc.py +++ b/pySim/transport/pcsc.py @@ -30,11 +30,11 @@ from osmocom.utils import h2i, i2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError, ReaderError -from pySim.transport import LinkBase +from pySim.transport import LinkBase, TpduAdaptor from pySim.utils import ResTuple
-class PcscSimLink(LinkBase): +class PcscSimLink(TpduAdaptor, LinkBase): """ pySim: PCSC reader transport link.""" name = 'PC/SC'
@@ -84,8 +84,19 @@ # is disconnected self.disconnect()
- # Explicitly select T=0 communication protocol - self._con.connect(CardConnection.T0_protocol) + # Make card connection and select a suitable communication 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: raise ProtocolError() from exc except NoCardException as exc: @@ -102,12 +113,8 @@ self.connect() return 1
- def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: - - apdu = h2i(pdu) - - data, sw1, sw2 = self._con.transmit(apdu) - + def send_tpdu(self, tpdu: Hexstr) -> ResTuple: + data, sw1, sw2 = self._con.transmit(h2i(tpdu)) sw = [sw1, sw2]
# Return value diff --git a/pySim/transport/serial.py b/pySim/transport/serial.py index 04b4ab7..7b58774 100644 --- a/pySim/transport/serial.py +++ b/pySim/transport/serial.py @@ -24,11 +24,11 @@ from osmocom.utils import h2b, b2h, Hexstr
from pySim.exceptions import NoCardError, ProtocolError -from pySim.transport import LinkBase +from pySim.transport import LinkBase, TpduAdaptor from pySim.utils import ResTuple
-class SerialSimLink(LinkBase): +class SerialSimLink(TpduAdaptor, LinkBase): """ pySim: Transport Link for serial (RS232) based readers included with simcard""" name = 'Serial'
@@ -187,13 +187,13 @@ def _rx_byte(self): return self._sl.read()
- def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple: + def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
- pdu = h2b(pdu) - data_len = pdu[4] # P3 + tpdu = h2b(tpdu) + data_len = tpdu[4] # 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 # - INS: Command acked -> go ahead @@ -201,7 +201,7 @@ # - SW1: The card can apparently proceed ... while True: b = self._rx_byte() - if ord(b) == pdu[1]: + if ord(b) == tpdu[1]: break if b != '\x60': # Ok, it 'could' be SW1 @@ -214,12 +214,12 @@ raise ProtocolError()
# Send data (if any) - if len(pdu) > 5: - self._tx_string(pdu[5:]) + if len(tpdu) > 5: + self._tx_string(tpdu[5:])
# Receive data (including SW !) - # length = [P3 - tx_data (=len(pdu)-len(hdr)) + 2 (SW1//2) ] - to_recv = data_len - len(pdu) + 5 + 2 + # length = [P3 - tx_data (=len(tpdu)-len(hdr)) + 2 (SW1//2) ] + to_recv = data_len - len(tpdu) + 5 + 2
data = bytes(0) while len(data) < to_recv: diff --git a/pySim/utils.py b/pySim/utils.py index c706b34..48a9998 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -539,6 +539,52 @@ 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): """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 diff --git a/tests/pySim-shell_test/apdu/__init__.py b/tests/pySim-shell_test/apdu/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/pySim-shell_test/apdu/__init__.py diff --git a/tests/pySim-shell_test/apdu/test.py b/tests/pySim-shell_test/apdu/test.py new file mode 100644 index 0000000..de79ae3 --- /dev/null +++ b/tests/pySim-shell_test/apdu/test.py @@ -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() diff --git a/tests/pySim-shell_test/apdu/test_apdu.script b/tests/pySim-shell_test/apdu/test_apdu.script new file mode 100644 index 0000000..a949b2d --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu.script @@ -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]+$' \ No newline at end of file diff --git a/tests/pySim-shell_test/apdu/test_apdu_legacy.script b/tests/pySim-shell_test/apdu/test_apdu_legacy.script new file mode 100644 index 0000000..c3dfb1b --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu_legacy.script @@ -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]+$' \ No newline at end of file diff --git a/tests/pySim-shell_test/apdu/test_apdu_legacy_scp02.template b/tests/pySim-shell_test/apdu/test_apdu_legacy_scp02.template new file mode 100644 index 0000000..51497ed --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu_legacy_scp02.template @@ -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 \ No newline at end of file diff --git a/tests/pySim-shell_test/apdu/test_apdu_legacy_scp03.template b/tests/pySim-shell_test/apdu/test_apdu_legacy_scp03.template new file mode 100644 index 0000000..b41620b --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu_legacy_scp03.template @@ -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 \ No newline at end of file diff --git a/tests/pySim-shell_test/apdu/test_apdu_scp02.template b/tests/pySim-shell_test/apdu/test_apdu_scp02.template new file mode 100644 index 0000000..fbe6c07 --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu_scp02.template @@ -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 \ No newline at end of file diff --git a/tests/pySim-shell_test/apdu/test_apdu_scp03.template b/tests/pySim-shell_test/apdu/test_apdu_scp03.template new file mode 100644 index 0000000..09b2639 --- /dev/null +++ b/tests/pySim-shell_test/apdu/test_apdu_scp03.template @@ -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 \ No newline at end of file diff --git a/tests/pySim-shell_test/config.yaml b/tests/pySim-shell_test/config.yaml index 705d1b4..dac22fd 100644 --- a/tests/pySim-shell_test/config.yaml +++ b/tests/pySim-shell_test/config.yaml @@ -1,6 +1,6 @@ regenerate: False keepfiles: False -print_content: False +print_content: True cards: - name : "sysmoISIM-SJA5-S17" atr : "3B9F96801F878031E073FE211B674A357530350265F8" diff --git a/tests/pySim-shell_test/lchan/test.ok b/tests/pySim-shell_test/lchan/test.ok index 29882cd..2201cb5 100644 --- a/tests/pySim-shell_test/lchan/test.ok +++ b/tests/pySim-shell_test/lchan/test.ok @@ -1,8 +1,8 @@ --> 0070000100 +-> 00700001 <- 9000: --> 0070000200 +-> 00700002 <- 9000: --> 0070000300 +-> 00700003 <- 9000: currently selected file: MF/DF.TELECOM/EF.MSISDN (3f00/7f10/6f40) currently selected file: MF/ADF.USIM/EF.IMSI (3f00/a0000000871002/6f07) @@ -12,9 +12,9 @@ "response_all_ref_ar_do": null } ] --> 0070800100 +-> 00708001 <- 9000: --> 0070800200 +-> 00708002 <- 9000: --> 0070800300 +-> 00708003 <- 9000: diff --git a/tests/unittests/test_globalplatform.py b/tests/unittests/test_globalplatform.py index c8fdda5..597d51d 100644 --- a/tests/unittests/test_globalplatform.py +++ b/tests/unittests/test_globalplatform.py @@ -36,14 +36,14 @@
def test_mutual_auth_success(self): 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) ext_auth_cmd = self.scp02.gen_ext_auth_apdu() self.assertEqual(b2h(ext_auth_cmd).upper(), '8482010010BA6961667737C5BCEBECE14C7D6A4376')
def test_mutual_auth_fail_card_cryptogram(self): 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[-1:] = b'\xff' with self.assertRaises(ValueError): @@ -61,15 +61,32 @@ ext_auth_cmd = self.scp02.gen_ext_auth_apdu()
def test_mac_command(self): - wrapped = self.scp02.wrap_cmd_apdu(h2b('80f28002024f00')) - self.assertEqual(b2h(wrapped).upper(), '84F280020A4F00B21AAFA3EB2D1672') + # Case #1: No command data field, No response data field present + 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: """some kind of 'abstract base class' for a unittest.UnitTest, implementing common functionality for all of our SCP03 test caseses.""" - get_eid_cmd_plain = h2b('80E2910006BF3E035C015A') + get_eid_cmd_plain = h2b('80E2910006BF3E035C015A00') 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 init_upd_cmd = b'' @@ -81,7 +98,7 @@
@property def host_challenge(self) -> bytes: - return self.init_upd_cmd[5:] + return self.init_upd_cmd[5:-1]
@property def kvn(self) -> int: @@ -128,6 +145,21 @@ # pylint: disable=no-member 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 KEYSET_AES128 = GpCardKeyset(0x30, h2b('000102030405060708090a0b0c0d0e0f'), h2b('101112131415161718191a1b1c1d1e1f'), h2b('202122232425262728292a2b2c2d2e2f')) @@ -139,75 +171,111 @@
class SCP03_Test_AES128_11(SCP03_Test, unittest.TestCase): keyset = KEYSET_AES128 - init_upd_cmd = h2b('8050300008b13e5f938fc108c4') + init_upd_cmd = h2b('8050300008b13e5f938fc108c400') init_upd_rsp = h2b('000000000000000000003003703eb51047495b249f66c484c1d2ef1948000002') ext_auth_cmd = h2b('84821100107d5f5826a993ebc89eea24957fa0b3ce') - get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a28297') + get_eid_cmd = h2b('84e291000ebf3e035c015a558d036518a2829700') 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): keyset = KEYSET_AES128 - init_upd_cmd = h2b('80503000088e1552d0513c60f3') + init_upd_cmd = h2b('80503000088e1552d0513c60f300') init_upd_rsp = h2b('0000000000000000000030037030760cd2c47c1dd395065fe5ead8a9d7000001') ext_auth_cmd = h2b('8482030010fd4721a14d9b07003c451d2f8ae6bb21') - get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c') + get_eid_cmd = h2b('84e2910018ca9c00f6713d79bc8baa642bdff51c3f6a4082d3bd9ad26c00') 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): keyset = KEYSET_AES128 - init_upd_cmd = h2b('8050300008fdf38259a1e0de44') + init_upd_cmd = h2b('8050300008fdf38259a1e0de4400') init_upd_rsp = h2b('000000000000000000003003703b1aca81e821f219081cdc01c26b372d000003') ext_auth_cmd = h2b('84823300108c36f96bcc00724a4e13ad591d7da3f0') - get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f') + get_eid_cmd = h2b('84e2910018267a85dfe4a98fca6fb0527e0dfecce4914e40401433c87f00') 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): keyset = KEYSET_AES192 - init_upd_cmd = h2b('80503100087396430b768b085b') + init_upd_cmd = h2b('80503100087396430b768b085b00') init_upd_rsp = h2b('000000000000000000003103708cfc23522ffdbf1e5df5542cac8fd866000003') ext_auth_cmd = h2b('84821100102145ed30b146f5db252fb7e624cec244') - get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d143944') + get_eid_cmd = h2b('84e291000ebf3e035c015aff42cf801d14394400') 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): keyset = KEYSET_AES192 - init_upd_cmd = h2b('805031000869c65da8202bf19f') + init_upd_cmd = h2b('805031000869c65da8202bf19f00') init_upd_rsp = h2b('00000000000000000000310370b570a67be38446717729d6dd3d2ec5b1000001') ext_auth_cmd = h2b('848203001065df4f1a356a887905466516d9e5b7c1') - get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d') + get_eid_cmd = h2b('84e2910018d2c6fb477c5d4afe4fd4d21f17eff10d3578ec1774a12a2d00') 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): keyset = KEYSET_AES192 - init_upd_cmd = h2b('80503100089b3f2eef0e8c9374') + init_upd_cmd = h2b('80503100089b3f2eef0e8c937400') init_upd_rsp = h2b('00000000000000000000310370f6bb305a15bae1a68f79fb08212fbed7000002') ext_auth_cmd = h2b('84823300109100bc22d58b45b86a26365ce39ff3cf') - get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de589') + get_eid_cmd = h2b('84e29100188f7f946c84f70d17994bc6e8791251bb1bb1bf02cf8de58900') 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): keyset = KEYSET_AES256 - init_upd_cmd = h2b('805032000811666d57866c6f54') + init_upd_cmd = h2b('805032000811666d57866c6f5400') init_upd_rsp = h2b('0000000000000000000032037053ea8847efa7674e41498a4d66cf0dee000003') ext_auth_cmd = h2b('84821100102f2ad190eff2fafc4908996d1cebd310') - get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d') + get_eid_cmd = h2b('84e291000ebf3e035c015af4b680372542b59d00') 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): keyset = KEYSET_AES256 - init_upd_cmd = h2b('8050320008c6066990fc426e1d') + init_upd_cmd = h2b('8050320008c6066990fc426e1d00') init_upd_rsp = h2b('000000000000000000003203708682cd81bbd8919f2de3f2664581f118000001') ext_auth_cmd = h2b('848203001077c493b632edadaf865a1e64acc07ce9') - get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe44') + get_eid_cmd = h2b('84e29100183ddaa60594963befaada3525b492ede23c2ab2c1ce3afe4400') 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): keyset = KEYSET_AES256 - init_upd_cmd = h2b('805032000897b2055fe58599fd') + init_upd_cmd = h2b('805032000897b2055fe58599fd00') init_upd_rsp = h2b('00000000000000000000320370a8439a22cedf045fa9f1903b2834f26e000002') ext_auth_cmd = h2b('8482330010508a0fd959d2e547c6b33154a6be2057') - get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea') + get_eid_cmd = h2b('84e29100187a5ef717eaf1e135ae92fe54429d0e465decda65f5fe5aea00') 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: # - for S8 and S16 mode diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py index 30a01d2..04ceba4 100755 --- a/tests/unittests/test_utils.py +++ b/tests/unittests/test_utils.py @@ -4,6 +4,7 @@ from pySim import utils from pySim.legacy import utils as legacy_utils 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 from pySim.ts_102_221 import AM_DO_EF, AM_DO_DF, SC_DO @@ -189,5 +190,19 @@ # 18 digits; we expect luhn check digit to be added 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__": unittest.main()