mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-09-28 15:06:33 +03:00
transport: stop the T=0 layer from breaking GP 6310
TS 102 221 section 7.3.1.1.4 clause 4b lets the card answer a case #4 command TPDU with a 62xx/63xx warning, upon which the terminal sends a dummy GET RESPONSE to obtain the 61xx that announces the response length. __send_apdu_T0() applies that unconditionally, to the status word that terminates a GET RESPONSE... That is not "redundant", as per GPC v2.3.1 section 11.4.3.2 table 11-38 GP GET STATUS (80 F2) answers 6310 "more data available", meaning reissue the command as get next occurrence(s) with P2 bit 1 set (11.4.2.2 table 11-34), so for example a paginated registry listing exchange looks like this: 84f22000024f00 => 61e4 first match, e4 bytes waiting 84c00000e4 => <page 1> 6310 more matches pending 84c0000000 => 6982 <- unsolicited, card rejects it The card rejects the unsolicited GET RESPONSE with 6982, our SCP02 session breaks, and the next command fail with 6985. The 6310 never reaches ADF_SD.get_status() either, so the pagination loop exits after page 1 and prints a sliently truncated listing. Observed with a SJA5. Fix by turning clause 4b würgaround into what it should be: a one shot reaction to the SW returned for the command TPDU. While at it, stop ADF_SD.get_status() from silently returning a truncated registry: it treated every status word other than 6310 as "nothing more to report". 6A88 is now the explicit empty result and anything else raises, so a partial listing can no longer silently pass. Adds unit tests for all of that so we dont break basic T0 things. Change-Id: I10f8afa8dd5623a49a6a0e7132607b3a1fad2d8c
This commit is contained in:
+4
-3
@@ -38,15 +38,16 @@ class SwMatchError(Exception):
|
||||
"""Raised when an operation specifies an expected SW but the actual SW from
|
||||
the card doesn't match."""
|
||||
|
||||
def __init__(self, sw_actual: str, sw_expected: str, rs=None):
|
||||
def __init__(self, sw_actual: str, sw_expected, rs=None):
|
||||
"""
|
||||
Args:
|
||||
sw_actual : the SW we actually received from the card (4 hex digits)
|
||||
sw_expected : the SW we expected to receive from the card (4 hex digits)
|
||||
sw_expected : the SW we expected to receive from the card (4 hex digits),
|
||||
or a list of acceptable ones
|
||||
rs : interpreter class to convert SW to string
|
||||
"""
|
||||
self.sw_actual = sw_actual
|
||||
self.sw_expected = sw_expected
|
||||
self.sw_expected = '/'.join(sw_expected) if isinstance(sw_expected, (list, tuple)) else sw_expected
|
||||
self.rs = rs
|
||||
|
||||
@property
|
||||
|
||||
@@ -737,22 +737,29 @@ class ADF_SD(CardADF):
|
||||
subset_hex = b2h(build_construct(StatusSubset, subset))
|
||||
aid = ApplicationAID(decoded=aid_search_qualifier)
|
||||
cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc')
|
||||
p2 = 0x02 # TLV format according to Table 11-36
|
||||
p2 = 0x02 # GPC v2.3.1 11.4.2.2 table 11-34, b2: response data structure per table 11-36
|
||||
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) + "00")
|
||||
if sw == '6a88':
|
||||
# "Referenced data not found": nothing (more) matches the requested subset and AID
|
||||
# search qualifier. That is empty, not error?
|
||||
return grd_list
|
||||
if sw not in ['9000', '6310']:
|
||||
# Never return a silently truncated registry
|
||||
raise SwMatchError(sw, ['9000', '6310'])
|
||||
remainder = h2b(data)
|
||||
while len(remainder):
|
||||
# tlv sequence, each element is one GpRegistryRelatedData()
|
||||
grd = GpRegistryRelatedData()
|
||||
_dec, remainder = grd.from_tlv(remainder)
|
||||
grd_list.append(grd)
|
||||
if sw != '6310':
|
||||
if sw == '9000':
|
||||
return grd_list
|
||||
else:
|
||||
p2 |= 0x01
|
||||
return grd_list
|
||||
# 6310 = more data available, table 11-38: reissue as get next occurrence(s), b1 of
|
||||
# table 11-34. Keeps b2 unchanged.
|
||||
p2 |= 0x01
|
||||
|
||||
set_status_parser = argparse.ArgumentParser()
|
||||
set_status_parser.add_argument('scope', choices=list(SetStatusScope.ksymapping.values()),
|
||||
|
||||
@@ -333,6 +333,18 @@ class LinkBaseTpdu(LinkBase):
|
||||
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
|
||||
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
|
||||
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
|
||||
#
|
||||
# The dummy GET RESPONSE of clause 4b (see below) is one shot: it turns a warning SW into the 61xx
|
||||
# that announces the response length. It is only ever a valid reaction to the SW returned for the
|
||||
# _command_ TPDU. Once a response has been fetched there is nothing left to announce, so a warning
|
||||
# SW is the final result of the command and has to be passed on to the caller unmodified.
|
||||
#
|
||||
# This matters because the 62xx/63xx range is not exclusive to ETSI TS 102 221.
|
||||
# GPC v2.3.1 section 11.4.3.2 table 11-38 GP GET STATUS (80 F2) answers
|
||||
# 6310 "more data available", meaning "reissue with P2 bit 1 set" as per section 11.4.2.2 table 11-34
|
||||
# rather than "response data is waiting". Trying a random GET RESPONSE at that point
|
||||
# makes the card answer 6982 and tears down the whole SCP session and following commands fail with 6985.
|
||||
dummy_gr_allowed = not data
|
||||
while True:
|
||||
if sw in ['9000', '9100']:
|
||||
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
|
||||
@@ -345,7 +357,7 @@ class LinkBaseTpdu(LinkBase):
|
||||
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
|
||||
# ISO/IEC 7816-4, Table 5)
|
||||
le_gr = sw[2:4]
|
||||
elif sw[0:2] in ['62', '63']:
|
||||
elif sw[0:2] in ['62', '63'] and dummy_gr_allowed:
|
||||
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
|
||||
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
|
||||
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
|
||||
@@ -360,6 +372,7 @@ class LinkBaseTpdu(LinkBase):
|
||||
data_gr, sw = self.send_tpdu(tpdu_gr)
|
||||
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
|
||||
data += data_gr
|
||||
dummy_gr_allowed = False
|
||||
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]
|
||||
|
||||
@@ -713,5 +713,73 @@ class Load_ChunkLen_Test(unittest.TestCase):
|
||||
self.assertLessEqual(wrapped[4], 255)
|
||||
|
||||
|
||||
class _FakeScc:
|
||||
"""mock lchan.scc: replays scripted (data, sw) pairs + records the APDUs sent."""
|
||||
|
||||
def __init__(self, responses):
|
||||
self._responses = list(responses)
|
||||
self.sent = []
|
||||
|
||||
def send_apdu(self, apdu):
|
||||
self.sent.append(apdu.lower())
|
||||
if not self._responses:
|
||||
raise AssertionError('get_status sent unexpected APDU: %s' % apdu)
|
||||
return self._responses.pop(0)
|
||||
|
||||
|
||||
class GetStatus_Pagination_Test(unittest.TestCase):
|
||||
"""GPC v2.3.1 section 11.4.3.2 table 11-38 GET STATUS pagination test
|
||||
|
||||
Card answers 6310 when further matches are pending; command reissued with
|
||||
P2 bit 1 "next occurrence" set. Tied to T=0 handling pySim/transport, which
|
||||
used to swallow that 6310 and replied with GET RESPONSE, so page 2 was never fetched."""
|
||||
|
||||
ENTRY_1 = 'e3074f05a000000151'
|
||||
ENTRY_2 = 'e3074f05a000000152'
|
||||
|
||||
def _sd(self, responses):
|
||||
scc = _FakeScc(responses)
|
||||
cmd = type('_Cmd', (), {'lchan': type('_Lchan', (), {'scc': scc})()})()
|
||||
# cmd2 strikes again, CommandSet exposes _cmd as a read only property, needs shadowing
|
||||
_SD = type('_SD', (ADF_SD.AddlShellCommands,), {'_cmd': cmd})
|
||||
return _SD.__new__(_SD), scc
|
||||
|
||||
def _aids(self, grd_list):
|
||||
return [b2h(grd.to_dict()['gp_registry_related_data'][0]['application_aid']) for grd in grd_list]
|
||||
|
||||
def test_single_page(self):
|
||||
sd, scc = self._sd([(self.ENTRY_1, '9000')])
|
||||
grd_list = sd.get_status('applications')
|
||||
self.assertEqual(scc.sent, ['80f24002094f005c054f9f70c5cc00'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151'])
|
||||
|
||||
def test_two_pages(self):
|
||||
"""6310 -> reissue with P2 bit 1 set -> 9000, both pages in result"""
|
||||
sd, scc = self._sd([(self.ENTRY_1, '6310'), (self.ENTRY_2, '9000')])
|
||||
grd_list = sd.get_status('applications')
|
||||
self.assertEqual(scc.sent, ['80f24002094f005c054f9f70c5cc00',
|
||||
'80f24003094f005c054f9f70c5cc00'])
|
||||
self.assertEqual(self._aids(grd_list), ['a000000151', 'a000000152'])
|
||||
|
||||
def test_three_pages_keep_p2_next_occurrence(self):
|
||||
sd, scc = self._sd([(self.ENTRY_1, '6310'), (self.ENTRY_2, '6310'), (self.ENTRY_1, '9000')])
|
||||
grd_list = sd.get_status('applications')
|
||||
self.assertEqual([a[6:8] for a in scc.sent], ['02', '03', '03'])
|
||||
self.assertEqual(len(grd_list), 3)
|
||||
|
||||
def test_no_match_returns_empty(self):
|
||||
"""6A88 "referenced data not found" is empty result not failure."""
|
||||
sd, _scc = self._sd([('', '6a88')])
|
||||
self.assertEqual(sd.get_status('applications'), [])
|
||||
|
||||
def test_unexpected_sw_is_not_silently_truncated(self):
|
||||
"""partial is not complete result"""
|
||||
sd, _scc = self._sd([(self.ENTRY_1, '6310'), ('', '6982')])
|
||||
with self.assertRaises(SwMatchError) as ctx:
|
||||
sd.get_status('applications')
|
||||
self.assertEqual(ctx.exception.sw_actual, '6982')
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import unittest
|
||||
from osmocom.utils import h2b, b2h
|
||||
from pySim.cat import ProactiveCommand, CommandDetails, DeviceIdentities, Result
|
||||
from pySim.transport import ProactiveHandler
|
||||
from pySim.transport import ProactiveHandler, LinkBaseTpdu
|
||||
|
||||
|
||||
def _send_short_message_pcmd():
|
||||
@@ -73,5 +73,192 @@ class Test_prepare_response(unittest.TestCase):
|
||||
self.assertIn('CommandDetails', str(ctx.exception))
|
||||
|
||||
|
||||
class FakeTpduLink(LinkBaseTpdu):
|
||||
"""mock LinkBaseTpdu that replays a list of (data, sw) responses + records every TPDU that
|
||||
the T=0 state machine sends. Secretly sending more TPDUs than intended is the error,
|
||||
designed to test "unsolicited GET RESPONSE" mishaps"""
|
||||
|
||||
def __init__(self, responses):
|
||||
super().__init__()
|
||||
self._responses = list(responses)
|
||||
self.sent = []
|
||||
|
||||
def send_tpdu(self, tpdu):
|
||||
self.sent.append(tpdu.lower())
|
||||
if not self._responses:
|
||||
raise AssertionError('T=0 layer sent an unpexpected TPDU: %s (total so far: %s)'
|
||||
% (tpdu, self.sent))
|
||||
return self._responses.pop(0)
|
||||
|
||||
def __str__(self):
|
||||
return 'FakeTpduLink'
|
||||
|
||||
def wait_for_card(self, timeout=None, newcardonly=False):
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
pass
|
||||
|
||||
def get_atr(self):
|
||||
return '3b00'
|
||||
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
def _reset_card(self):
|
||||
pass
|
||||
|
||||
|
||||
# GP GET STATUS, wrapped in SCP02 CLA 84, Case #4.
|
||||
GET_STATUS = '84f22002094f005c054f9f70c5cc' + '00'
|
||||
GET_STATUS_TPDU = '84f22002094f005c054f9f70c5cc'
|
||||
|
||||
# generic #4 SELECT by DF name command
|
||||
CASE4 = '00a4040c07a0000000871002' + '00'
|
||||
CASE4_TPDU = '00a4040c07a0000000871002'
|
||||
|
||||
|
||||
class Test_send_apdu_T0(unittest.TestCase):
|
||||
"""regression tests for the T=0 state machine in LinkBaseTpdu.__send_apdu_T0()"""
|
||||
|
||||
def _exchange(self, apdu, responses, strict=True, protocol=0):
|
||||
link = FakeTpduLink(responses)
|
||||
link.apdu_strict = strict
|
||||
link.set_tpdu_format(protocol)
|
||||
data, sw = link._send_apdu(apdu)
|
||||
return link, data, sw
|
||||
|
||||
#### TS 102 221 section 7.3.1.1 TPDU construction
|
||||
|
||||
def test_case1_gets_le_appended(self):
|
||||
link, data, sw = self._exchange('00200001', [('', '9000')])
|
||||
self.assertEqual(link.sent, ['0020000100'])
|
||||
self.assertEqual((data, sw), ('', '9000'))
|
||||
|
||||
def test_case3_passed_through_unmodified(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, _data, sw = self._exchange(apdu, [('', '9000')])
|
||||
self.assertEqual(link.sent, [apdu])
|
||||
self.assertEqual(sw, '9000')
|
||||
|
||||
def test_case4_le_stripped(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '9000'))
|
||||
|
||||
#### TS 102 221 7.3.1.1.4 4a GP GET RESPONSE for 61xx / 9fxx
|
||||
|
||||
def test_61xx_fetches_response(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6103'), ('a1b2c3', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000003'])
|
||||
self.assertEqual((data, sw), ('a1b2c3', '9000'))
|
||||
|
||||
def test_61xx_chained(self):
|
||||
link, data, sw = self._exchange(CASE4,
|
||||
[('', '6102'), ('aabb', '6102'), ('ccdd', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000002', '00c0000002'])
|
||||
self.assertEqual((data, sw), ('aabbccdd', '9000'))
|
||||
|
||||
def test_9fxx_fetches_response(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '9f04'), ('deadbeef', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000004'])
|
||||
self.assertEqual((data, sw), ('deadbeef', '9000'))
|
||||
|
||||
def test_get_response_inherits_cla(self):
|
||||
"""GET RESPONSE must reuse CLA of command"""
|
||||
link, _data, _sw = self._exchange(GET_STATUS, [('', '6102'), ('aabb', '9000')])
|
||||
self.assertEqual(link.sent, [GET_STATUS_TPDU, '84c0000002'])
|
||||
|
||||
def test_9100_terminates(self):
|
||||
"""9100 is final status word, not fetch trigger"""
|
||||
link, data, sw = self._exchange(CASE4, [('', '9100')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '9100'))
|
||||
|
||||
def test_error_sw_terminates(self):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6982')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU])
|
||||
self.assertEqual((data, sw), ('', '6982'))
|
||||
|
||||
def test_no_status_word_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self._exchange(CASE4, [('', None)])
|
||||
|
||||
#### TS 102 221 7.3.1.1.4 4b dummy GET RESPONSE
|
||||
|
||||
def test_clause_4b_warning_before_data_bootstraps(self):
|
||||
"""warning SW returned for the _command_ TPDU triggers dummy GET RESPONSE (Le=00)"""
|
||||
for warn in ('6200', '6281', '62f1', '6300', '63f1'):
|
||||
with self.subTest(sw=warn):
|
||||
link, data, sw = self._exchange(CASE4,
|
||||
[('', warn), ('', '6103'), ('a1b2c3', '9000')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000000', '00c0000003'])
|
||||
self.assertEqual((data, sw), ('a1b2c3', '9000'))
|
||||
|
||||
def test_warning_after_data_terminates(self):
|
||||
"""Once the response has been fetched a warning status word is the final result of the command"""
|
||||
for warn in ('6281', '6283', '63c2', '6300', '62f1', '63f1', '6310'):
|
||||
with self.subTest(sw=warn):
|
||||
link, data, sw = self._exchange(CASE4, [('', '6102'), ('aabb', warn)])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000002'])
|
||||
self.assertEqual((data, sw), ('aabb', warn))
|
||||
|
||||
def test_no_dummy_get_response_when_command_already_returned_data(self):
|
||||
"""warning that arrives together with response data (for example 6282 on a case #2 read) is final, too"""
|
||||
link, data, sw = self._exchange('00b0000004', [('01020304', '6282')], strict=False)
|
||||
self.assertEqual(link.sent, ['00b0000004'])
|
||||
self.assertEqual((data, sw), ('01020304', '6282'))
|
||||
|
||||
def test_repeated_warning_does_not_loop(self):
|
||||
"""warning -> dummy GET RESPONSE -> warning again must terminate"""
|
||||
link, data, sw = self._exchange(CASE4, [('', '6281'), ('', '6281')])
|
||||
self.assertEqual(link.sent, [CASE4_TPDU, '00c0000000'])
|
||||
self.assertEqual((data, sw), ('', '6281'))
|
||||
|
||||
#### fixed GlobalPlatform GET STATUS pagination
|
||||
|
||||
def test_gp_6310_reaches_the_caller(self):
|
||||
"""GET STATUS answers 6310"""
|
||||
link, data, sw = self._exchange(GET_STATUS, [('', '6104'), ('e3024f00', '6310')])
|
||||
self.assertEqual(link.sent, [GET_STATUS_TPDU, '84c0000004'])
|
||||
self.assertEqual((data, sw), ('e3024f00', '6310'))
|
||||
|
||||
def test_gp_get_status_two_pages(self):
|
||||
"""Both GET STATUS pages, page 1 6310, reissued with P2 bit 1 set, page 2 9000."""
|
||||
page1 = self._exchange(GET_STATUS, [('', '6104'), ('e3024f00', '6310')])
|
||||
self.assertEqual(page1[1:], ('e3024f00', '6310'))
|
||||
page2 = self._exchange('84f22003094f005c054f9f70c5cc00',
|
||||
[('', '6104'), ('e3024f01', '9000')])
|
||||
self.assertEqual(page2[0].sent, ['84f22003094f005c054f9f70c5cc', '84c0000004'])
|
||||
self.assertEqual(page2[1:], ('e3024f01', '9000'))
|
||||
|
||||
#### 6cxx and apdu_strict
|
||||
|
||||
def test_6cxx_reissues_command_with_correct_length(self):
|
||||
link, data, sw = self._exchange('00b0000000', [('', '6c04'), ('01020304', '9000')])
|
||||
self.assertEqual(link.sent, ['00b0000000', '00b0000004'])
|
||||
self.assertEqual((data, sw), ('01020304', '9000'))
|
||||
|
||||
def test_strict_mode_does_not_auto_fetch_for_case3(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, data, sw = self._exchange(apdu, [('', '6104')], strict=True)
|
||||
self.assertEqual(link.sent, [apdu])
|
||||
self.assertEqual((data, sw), ('', '6104'))
|
||||
|
||||
def test_non_strict_mode_auto_fetches_for_case3(self):
|
||||
apdu = '00200001081122334455667788'
|
||||
link, data, sw = self._exchange(apdu, [('', '6104'), ('aabbccdd', '9000')], strict=False)
|
||||
self.assertEqual(link.sent, [apdu, '00c0000004'])
|
||||
self.assertEqual((data, sw), ('aabbccdd', '9000'))
|
||||
|
||||
#### T=1 briefly
|
||||
|
||||
def test_t1_is_passed_through(self):
|
||||
"""T=1 has no GET RESPONSE"""
|
||||
link, data, sw = self._exchange(GET_STATUS, [('e3024f00', '6310')], protocol=1)
|
||||
self.assertEqual(link.sent, [GET_STATUS.lower()])
|
||||
self.assertEqual((data, sw), ('e3024f00', '6310'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user