Skip to content

Commit 852eff5

Browse files
committed
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
1 parent f951c56 commit 852eff5

25 files changed

+605
-143
lines changed

pySim-read.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def select_app(adf: str, card: SimCard):
8888
scc.sel_ctrl = "0004"
8989

9090
# Testing for Classic SIM or UICC
91-
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00")
91+
(res, sw) = sl.send_apdu(scc.cla_byte + "a4" + scc.sel_ctrl + "02" + "3f00" + "00")
9292
if sw == '6e00':
9393
# Just a Classic SIM
9494
scc.cla_byte = "a0"

pySim-shell.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def __init__(self, card, rs, sl, ch, script=None):
114114
self.conserve_write = True
115115
self.json_pretty_print = True
116116
self.apdu_trace = False
117+
self.apdu_strict = False
117118

118119
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
119120
onchange_cb=self._onchange_numeric_path))
@@ -122,6 +123,9 @@ def __init__(self, card, rs, sl, ch, script=None):
122123
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
123124
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
124125
onchange_cb=self._onchange_apdu_trace))
126+
self.add_settable(Settable2Compat('apdu_strict', bool,
127+
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
128+
onchange_cb=self._onchange_apdu_strict))
125129
self.equip(card, rs)
126130

127131
def equip(self, card, rs):
@@ -198,6 +202,13 @@ def _onchange_apdu_trace(self, param_name, old, new):
198202
else:
199203
self.card._scc._tp.apdu_tracer = None
200204

205+
def _onchange_apdu_strict(self, param_name, old, new):
206+
if self.card:
207+
if new == True:
208+
self.card._scc._tp.apdu_strict = True
209+
else:
210+
self.card._scc._tp.apdu_strict = False
211+
201212
class Cmd2ApduTracer(ApduTracer):
202213
def __init__(self, cmd2_app):
203214
self.cmd2 = cmd2_app

pySim-trace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def __init__(self, debug: bool = False, **kwargs):
5555
def __str__(self):
5656
return "dummy"
5757

58-
def _send_apdu_raw(self, pdu):
58+
def _send_apdu(self, pdu):
5959
#print("DummySimLink-apdu: %s" % pdu)
6060
return [], '9000'
6161

pySim/commands.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ def send_apdu_constr(self, cla: Hexstr, ins: Hexstr, p1: Hexstr, p2: Hexstr, cmd
142142
Tuple of (decoded_data, sw)
143143
"""
144144
cmd = cmd_constr.build(cmd_data) if cmd_data else ''
145-
p3 = i2h([len(cmd)])
146-
pdu = ''.join([cla, ins, p1, p2, p3, b2h(cmd)])
145+
lc = i2h([len(cmd)]) if cmd_data else ''
146+
le = '00' if resp_constr else ''
147+
pdu = ''.join([cla, ins, p1, p2, lc, b2h(cmd), le])
147148
(data, sw) = self.send_apdu(pdu, apply_lchan = apply_lchan)
148149
if data:
149150
# filter the resulting dict to avoid '_io' members inside
@@ -247,7 +248,7 @@ def try_select_path(self, dir_list: List[Hexstr]) -> List[ResTuple]:
247248
if not isinstance(dir_list, list):
248249
dir_list = [dir_list]
249250
for i in dir_list:
250-
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i)
251+
data, sw = self.send_apdu(self.cla_byte + "a4" + self.sel_ctrl + "02" + i + "00")
251252
rv.append((data, sw))
252253
if sw != '9000':
253254
return rv
@@ -277,11 +278,11 @@ def select_file(self, fid: Hexstr) -> ResTuple:
277278
fid : file identifier as hex string
278279
"""
279280

280-
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid)
281+
return self.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid + "00")
281282

282283
def select_parent_df(self) -> ResTuple:
283284
"""Execute SELECT to switch to the parent DF """
284-
return self.send_apdu_checksw(self.cla_byte + "a4030400")
285+
return self.send_apdu_checksw(self.cla_byte + "a40304")
285286

286287
def select_adf(self, aid: Hexstr) -> ResTuple:
287288
"""Execute SELECT a given Applicaiton ADF.
@@ -291,7 +292,7 @@ def select_adf(self, aid: Hexstr) -> ResTuple:
291292
"""
292293

293294
aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:]
294-
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid)
295+
return self.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid + "00")
295296

296297
def read_binary(self, ef: Path, length: int = None, offset: int = 0) -> ResTuple:
297298
"""Execute READD BINARY.
@@ -494,9 +495,9 @@ def binary_size(self, ef: Path) -> int:
494495
# TS 102 221 Section 11.3.1 low-level helper
495496
def _retrieve_data(self, tag: int, first: bool = True) -> ResTuple:
496497
if first:
497-
pdu = '80cb008001%02x' % (tag)
498+
pdu = '80cb008001%02x00' % (tag)
498499
else:
499-
pdu = '80cb000000'
500+
pdu = '80cb0000'
500501
return self.send_apdu_checksw(pdu)
501502

502503
def retrieve_data(self, ef: Path, tag: int) -> ResTuple:
@@ -569,7 +570,7 @@ def run_gsm(self, rand: Hexstr) -> ResTuple:
569570
if len(rand) != 32:
570571
raise ValueError('Invalid rand')
571572
self.select_path(['3f00', '7f20'])
572-
return self.send_apdu_checksw('a088000010' + rand, sw='9000')
573+
return self.send_apdu_checksw('a088000010' + rand + '00', sw='9000')
573574

574575
def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTuple:
575576
"""Execute AUTHENTICATE (USIM/ISIM).
@@ -602,7 +603,7 @@ def authenticate(self, rand: Hexstr, autn: Hexstr, context: str = '3g') -> ResTu
602603

603604
def status(self) -> ResTuple:
604605
"""Execute a STATUS command as per TS 102 221 Section 11.1.2."""
605-
return self.send_apdu_checksw('80F2000000')
606+
return self.send_apdu_checksw('80F20000')
606607

607608
def deactivate_file(self) -> ResTuple:
608609
"""Execute DECATIVATE FILE command as per TS 102 221 Section 11.1.14."""
@@ -651,7 +652,7 @@ def manage_channel(self, mode: str = 'open', lchan_nr: int =0) -> ResTuple:
651652
p1 = 0x80
652653
else:
653654
p1 = 0x00
654-
pdu = self.cla_byte + '70%02x%02x00' % (p1, lchan_nr)
655+
pdu = self.cla_byte + '70%02x%02x' % (p1, lchan_nr)
655656
return self.send_apdu_checksw(pdu)
656657

657658
def reset_card(self) -> Hexstr:

pySim/euicc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def __init__(self):
332332
def store_data(scc: SimCardCommands, tx_do: Hexstr, exp_sw: SwMatchstr ="9000") -> Tuple[Hexstr, SwHexstr]:
333333
"""Perform STORE DATA according to Table 47+48 in Section 5.7.2 of SGP.22.
334334
Only single-block store supported for now."""
335-
capdu = '80E29100%02x%s' % (len(tx_do)//2, tx_do)
335+
capdu = '80E29100%02x%s00' % (len(tx_do)//2, tx_do)
336336
return scc.send_apdu_checksw(capdu, exp_sw)
337337

338338
@staticmethod

pySim/global_platform/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none
580580
{'last_block': len(remainder) == 0, 'encryption': encryption,
581581
'structure': structure, 'response': response_permitted})
582582
hdr = "80E2%02x%02x%02x" % (p1b[0], block_nr, len(chunk))
583-
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk))
583+
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
584584
block_nr += 1
585585
response += data
586586
return data
@@ -646,7 +646,7 @@ def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
646646
See GlobalPlatform CardSpecification v2.3 Section 11.8 for details."""
647647
key_data = kvn.to_bytes(1, 'big') + build_construct(ADF_SD.AddlShellCommands.KeyDataBasic, key_dict)
648648
hdr = "80D8%02x%02x%02x" % (old_kvn, kid, len(key_data))
649-
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data))
649+
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(key_data) + "00")
650650
return data
651651

652652
get_status_parser = argparse.ArgumentParser()
@@ -671,7 +671,7 @@ def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpReg
671671
grd_list = []
672672
while True:
673673
hdr = "80F2%s%02x%02x" % (subset_hex, p2, len(cmd_data))
674-
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data))
674+
data, sw = self._cmd.lchan.scc.send_apdu(hdr + b2h(cmd_data) + "00")
675675
remainder = h2b(data)
676676
while len(remainder):
677677
# tlv sequence, each element is one GpRegistryRelatedData()
@@ -752,7 +752,7 @@ def do_install_for_install(self, opts):
752752
self.install(p1, 0x00, b2h(ifi_bytes))
753753

754754
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
755-
cmd_hex = "80E6%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
755+
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
756756
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
757757

758758
del_cc_parser = argparse.ArgumentParser()

pySim/global_platform/scp.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from construct import Optional as COptional
2525
from osmocom.utils import b2h
2626
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
27-
27+
from pySim.utils import parse_command_apdu
2828
from pySim.secure_channel import SecureChannel
2929

3030
logger = logging.getLogger(__name__)
@@ -248,7 +248,7 @@ def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
248248
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
249249
"""Generate INITIALIZE UPDATE APDU."""
250250
self.host_challenge = host_challenge
251-
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge
251+
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, 8]) + self.host_challenge + b'\x00'
252252

253253
def parse_init_update_resp(self, resp_bin: bytes):
254254
"""Parse response to INITIALZIE UPDATE."""
@@ -280,9 +280,13 @@ def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
280280
if not self.do_cmac:
281281
return apdu
282282

283-
lc = len(apdu) - 5
284-
assert len(apdu) >= 5, "Wrong APDU length: %d" % len(apdu)
285-
assert len(apdu) == 5 or apdu[4] == lc, "Lc differs from length of data: %d vs %d" % (apdu[4], lc)
283+
(case, lc, le, data) = parse_command_apdu(apdu)
284+
285+
# TODO: add support for extended length fields.
286+
assert lc <= 256
287+
assert le <= 256
288+
lc &= 0xFF
289+
le &= 0xFF
286290

287291
# CLA without log. channel can be 80 or 00 only
288292
cla = apdu[0]
@@ -298,16 +302,25 @@ def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
298302
# CMAC on modified APDU
299303
mlc = lc + 8
300304
clac = cla | CLA_SM
301-
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + apdu[5:])
305+
mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data)
302306
if self.do_cenc:
303307
k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8)
304-
data = k.encrypt(pad80(apdu[5:], 8))
308+
data = k.encrypt(pad80(data, 8))
305309
lc = len(data)
306-
else:
307-
data = apdu[5:]
308310

309311
lc += 8
310312
apdu = bytes([self._cla(True, b8)]) + apdu[1:4] + bytes([lc]) + data + mac
313+
314+
# Since we attach a signature, we will always send some data. This means that if the APDU is of case #4
315+
# or case #2, we must attach an additional Le byte to signal that we expect a response. It is technically
316+
# legal to use 0x00 (=256) as Le byte, even when the caller has specified a different value in the original
317+
# APDU. This is due to the fact that Le always describes the maximum expected length of the response
318+
# (see also ISO/IEC 7816-4, section 5.1). In addition to that, it should also important that depending on
319+
# the configuration of the SCP, the response may also contain a signature that makes the response larger
320+
# than specified in the Le field of the original APDU.
321+
if case == 4 or case == 2:
322+
apdu += b'\x00'
323+
311324
return apdu
312325

313326
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:
@@ -454,7 +467,7 @@ def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
454467
if len(host_challenge) != self.s_mode:
455468
raise ValueError('Host Challenge must be %u bytes long' % self.s_mode)
456469
self.host_challenge = host_challenge
457-
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge
470+
return bytes([self._cla(), INS_INIT_UPDATE, self.card_keys.kvn, 0, len(host_challenge)]) + host_challenge + b'\x00'
458471

459472
def parse_init_update_resp(self, resp_bin: bytes):
460473
"""Parse response to INITIALIZE UPDATE."""
@@ -489,12 +502,16 @@ def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
489502
ins = apdu[1]
490503
p1 = apdu[2]
491504
p2 = apdu[3]
492-
lc = apdu[4]
493-
assert lc == len(apdu) - 5
494-
cmd_data = apdu[5:]
505+
(case, lc, le, cmd_data) = parse_command_apdu(apdu)
506+
507+
# TODO: add support for extended length fields.
508+
assert lc <= 256
509+
assert le <= 256
510+
lc &= 0xFF
511+
le &= 0xFF
495512

496513
if self.do_cenc and not skip_cenc:
497-
if lc == 0:
514+
if case <= 2:
498515
# No encryption shall be applied to a command where there is no command data field. In this
499516
# case, the encryption counter shall still be incremented
500517
self.sk.block_nr += 1
@@ -519,6 +536,11 @@ def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
519536
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
520537
cmac = self.sk.calc_cmac(apdu)
521538
apdu += cmac[:self.s_mode]
539+
540+
# See comment in SCP03._wrap_cmd_apdu()
541+
if case == 4 or case == 2:
542+
apdu += b'\x00'
543+
522544
return apdu
523545

524546
def unwrap_rsp_apdu(self, sw: bytes, rsp_apdu: bytes) -> bytes:

0 commit comments

Comments
 (0)