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

@@ -580,7 +580,7 @@ class ADF_SD(CardADF):
{'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 @@ class ADF_SD(CardADF):
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 @@ class ADF_SD(CardADF):
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 @@ class ADF_SD(CardADF):
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()

View File

@@ -24,7 +24,7 @@ from construct import Struct, Bytes, Int8ub, Int16ub, Const
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 @@ class SCP02(SCP):
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."""
@@ -280,9 +280,13 @@ class SCP02(SCP):
if not self.do_cmac:
return apdu
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]
@@ -298,16 +302,25 @@ class SCP02(SCP):
# 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:
@@ -454,7 +467,7 @@ class SCP03(SCP):
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."""
@@ -489,12 +502,16 @@ class SCP03(SCP):
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
@@ -519,6 +536,11 @@ class SCP03(SCP):
apdu = bytes([mcla, ins, p1, p2, mlc]) + cmd_data
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: