Hoernchen has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/43552?usp=email )
Change subject: GP: only send a GET STATUS tag list to cards that support it ......................................................................
GP: only send a GET STATUS tag list to cards that support it
get_status() has appended a hardcoded '5c054f9f70c5cc' to the command data field. Of the data objects in GP CS v2.3.1 Table 11-35 only the AID search tag 4F is mandatory, the tag list is optional and not supported in v2.1.1, where Section 9.4.2.3 defines the data field as the search qualifier. Cards implementing that revision can reject anything else with 6A80 as per v2.1.1 Table 9-26.
A sysmocom SJA5 does that. Its data field must be one 4F TLV, the value is free, but nothing may precede or follow it. So every subset returned nothing at all...
There is no need to guess: v2.1.1/v2.3.1 Section 7.4.1.3 Card Recognition Data is "shall be present" and contains the GP version on selected SD. Query it once, and send the tag list to cards that announce v2.2 or later. SJA5 reports 2.1.1, sysmoEUICC reports 2.2.
Two more problems with the old list: - A tag list is an inclusion list, old list omits tag 84, so it suppressed the Executable Module AIDs - It asks for tag C5 for Executable Load Files, which "may" be answered with an error status.
Fix this by constricting or omitting the tag list depending on reported GP version.
Change-Id: I74cd2bd47617d616bede6453397f544cde5abcb7 --- M pySim/global_platform/__init__.py M tests/unittests/test_globalplatform.py 2 files changed, 205 insertions(+), 11 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/52/43552/1
diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py index 80772fe..4a41d2f 100644 --- a/pySim/global_platform/__init__.py +++ b/pySim/global_platform/__init__.py @@ -37,6 +37,9 @@ from pySim.profile import CardProfile from pySim.ota import SimFileAccessAndToolkitAppSpecParams from pySim.javacard import CapFile +from pySim.log import PySimLogger + +log = PySimLogger.get(__name__)
# GPCS Table 11-48 Load Parameter Tags class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6): @@ -532,6 +535,63 @@ ExecutableModuleAID, AssociatedSecurityDomainAID]): pass
+# GP CS v2.3.1 Table 11-36/11-37 possible data objects requested/returned from GET STATUS for each registry entry. +# Applications and Executable Load Files have _different_ sets, so a tag list requesting them has +# to match the subset because 11.4.2.3 warns that asking for a data object an entry does not have +# "may" be answered with an error status. +GetStatusTagListIEs = { + # Table 11-36 GP Application Data + 'isd': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter, + ExecutableLoadFileAID, AssociatedSecurityDomainAID], + 'applications': [ApplicationAID, LifeCycleState, Privileges, ImplicitSelectionParameter, + ExecutableLoadFileAID, AssociatedSecurityDomainAID], + # Table 11-37 GP Executable Load File Data. 84 only for the subset that asks for the modules (Note 2)! + 'files': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber, + AssociatedSecurityDomainAID], + 'files_and_modules': [ApplicationAID, LifeCycleState, ExecutableLoadFileVersionNumber, + ExecutableModuleAID, AssociatedSecurityDomainAID], +} + +def get_status_tag_list(subset: str) -> bytes: + """Encode the GET STATUS tag list for the given status subset""" + tags = b''.join([bertlv_encode_tag(ie.tag) for ie in GetStatusTagListIEs[subset]]) + return b'\x5c' + bertlv_encode_len(len(tags)) + tags + +# GP CS v2.3.1 Appendix H.2 / Table H-1 +# oid prefix {iso(1) member-body(2) country-USA(840) globalPlatform(114283)} + card management type 2 +# afterwards GP version. +OID_GP_CARD_MGMT_TYPE = h2b('2a864886fc6b02') + +def _find_tlv_value(decoded, key: str): + """depth first search for the nested decoded TLV_IE dict/list""" + if isinstance(decoded, dict): + for k, v in decoded.items(): + if k == key: + return v + found = _find_tlv_value(v, key) + if found is not None: + return found + elif isinstance(decoded, list): + for item in decoded: + found = _find_tlv_value(item, key) + if found is not None: + return found + return None + +def decode_gp_version(card_data: bytes) -> Optional[Tuple[int, ...]]: + """GP version from Card Data returned by GET DATA, like (2, 1, 1) or (2, 2). + None if cm type OID is absent/unknown""" + cd = CardData() + cd.from_tlv(card_data) + ctv = _find_tlv_value(cd.to_dict(), 'card_management_type_and_version') + oid = _find_tlv_value(ctv, 'object_identifier') if ctv is not None else None + if oid is None: + return None + oid = h2b(oid) if isinstance(oid, str) else bytes(oid) + if not oid.startswith(OID_GP_CARD_MGMT_TYPE): + return None + return tuple(oid[len(OID_GP_CARD_MGMT_TYPE):]) + # Application Dedicated File of a Security Domain class ADF_SD(CardADF): StoreData = BitStruct('last_block'/Flag, @@ -733,19 +793,62 @@ for grd in grd_list: self._cmd.poutput_json(grd.to_dict())
+ def gp_version(self) -> Optional[Tuple[int, ...]]: + """GP version the selected SD reports in its Card Recognition + Data, e.g. (2, 1, 1). Card Recognition Data "shall be present" v2.1.1/v2.3.1 section 7.4.1.3, + so this must succeed no matter the GP version. None if card did not answer GET DATA / OID unknown. + Cached, it cannot change during a session.""" + if not hasattr(self, '_gp_version'): + self._gp_version = None + try: + data, _sw = self._cmd.lchan.scc.get_data(cla=0x80, tag=CardData.tag) + self._gp_version = decode_gp_version(h2b(data)) + except (SwMatchError, ValueError) as e: + log.warning("Could not determine GlobalPlatform version: %s", e) + return self._gp_version + def get_status(self, subset:str, aid_search_qualifier:Hexstr = '') -> List[GpRegistryRelatedData]: - subset_hex = b2h(build_construct(StatusSubset, subset)) aid = ApplicationAID(decoded=aid_search_qualifier) - cmd_data = aid.to_tlv() + h2b('5c054f9f70c5cc') + # GPC CardSpec v2.3.1 Table 11-35 says only the AID search tag is mandatory, tag list is + # Optional and not present in the older v2.1.1, where section 9.4.2.3 defines the data + # field as the search qualifier. + # Cards like the sja5 implementing that old GP version reject anything else with 6A80 + # from v2.1.1 Table 9-26 so only send a tag list to a card that announces v2.2 or later. + # + # Not sending one is not a problem on older cards, the tag list only gives us data beyond + # what 11.4.3.1 gives us anyway, for example the associated SD AID which matters on an eUICC + # where entries belong to different SD. + version = self.gp_version() + if version is not None and version >= (2, 2): + try: + return self._get_status(subset, aid.to_tlv() + get_status_tag_list(subset)) + except SwMatchError as e: + # Retry if v2.2 or later but rejected the tag list anyway. + # 6A80 and 6A88 are the error conditions GET STATUS defines in table 11-39. + # Retrying beats not ending up with a list again... + if e.sw_actual not in ('6a80', '6a88'): + raise + log.warning("Card reports GlobalPlatform %s but answered %s to the GET STATUS tag list; " + "retrying with the default search", + '.'.join(str(v) for v in version), e.sw_actual) + return self._get_status(subset, aid.to_tlv(), empty_on_6a88=True) + + def _get_status(self, subset:str, cmd_data:bytes, + empty_on_6a88: bool = False) -> List[GpRegistryRelatedData]: + subset_hex = b2h(build_construct(StatusSubset, subset)) p2 = 0x02 # TLV format according to 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 + # Table 11-39 "Referenced data not found". After collecting all pages this can + # only mean "nothing more matches" -> listing is complete. On the first page + # it is ambiguous, empty result or bad command data field, so leave that to get_status() + # which knows if a tag list was sent. + if grd_list or empty_on_6a88: + return grd_list + raise SwMatchError(sw, '9000/6310') if sw not in ['9000', '6310']: # Never return a silently truncated registry raise SwMatchError(sw, '9000/6310') diff --git a/tests/unittests/test_globalplatform.py b/tests/unittests/test_globalplatform.py index 2dbec70..1116023 100644 --- a/tests/unittests/test_globalplatform.py +++ b/tests/unittests/test_globalplatform.py @@ -712,14 +712,26 @@ for wrapped in scc.wrapped: self.assertLessEqual(wrapped[4], 255)
+# Real Card Data (GET DATA '66'), as returned by sja5 + euicc +CARD_DATA_V211 = ('6631732f06072a864886fc6b01600c060a2a864886fc6b0202010163090607' + '2a864886fc6b03640b06092a864886fc6b040215') +CARD_DATA_V22 = ('663b733906072a864886fc6b01600b06092a864886fc6b020202630906072a86' + '4886fc6b03640b06092a864886fc6b040370640b06092a864886fc6b04810400') +
class _FakeScc: """mock lchan.scc: replays scripted (data, sw) pairs + records the APDUs sent."""
- def __init__(self, responses): + def __init__(self, responses, card_data=CARD_DATA_V211): self._responses = list(responses) + self._card_data = card_data self.sent = []
+ def get_data(self, cla, tag): + if self._card_data is None: + raise SwMatchError('6a88', '9000') + return self._card_data, '9000' + def send_apdu(self, apdu): self.sent.append(apdu.lower()) if not self._responses: @@ -727,6 +739,28 @@ return self._responses.pop(0)
+class GpVersion_Test(unittest.TestCase): + """GP version from Card Recognition Data, which v2.1.1/v2.3.1 section 7.4.1.3 + require to be present. The OID under tag 60 is {globalPlatform 2 v...}.""" + + def test_decode_real_cards(self): + self.assertEqual(decode_gp_version(h2b(CARD_DATA_V211)), (2, 1, 1)) + self.assertEqual(decode_gp_version(h2b(CARD_DATA_V22)), (2, 2)) + + def test_unknown_oid_is_none(self): + self.assertIsNone(decode_gp_version(h2b('66097307060512345678'))) + + def test_tag_lists_follow_the_spec_tables(self): + """table 11-36 applications, table 11-37 for load files""" + self.assertEqual(b2h(get_status_tag_list('isd')), '5c074f9f70c5cfc4cc') + self.assertEqual(b2h(get_status_tag_list('applications')), '5c074f9f70c5cfc4cc') + self.assertEqual(b2h(get_status_tag_list('files')), '5c054f9f70cecc') + self.assertEqual(b2h(get_status_tag_list('files_and_modules')), '5c064f9f70ce84cc') + # C5 never load files, 84 never applications + self.assertNotIn('c5', b2h(get_status_tag_list('files'))) + self.assertNotIn('84', b2h(get_status_tag_list('applications'))[4:]) + + class GetStatus_Pagination_Test(unittest.TestCase): """GP CS v2.3 section 11.4.3.1 GET STATUS pagination test
@@ -737,8 +771,8 @@ ENTRY_1 = 'e3074f05a000000151' ENTRY_2 = 'e3074f05a000000152'
- def _sd(self, responses): - scc = _FakeScc(responses) + def _sd(self, responses, card_data=CARD_DATA_V211): + scc = _FakeScc(responses, card_data) 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}) @@ -750,15 +784,15 @@ 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(scc.sent, ['80f24002024f0000']) 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(scc.sent, ['80f24002024f0000', + '80f24003024f0000']) self.assertEqual(self._aids(grd_list), ['a000000151', 'a000000152'])
def test_three_pages_keep_p2_next_occurrence(self): @@ -772,6 +806,44 @@ sd, _scc = self._sd([('', '6a88')]) self.assertEqual(sd.get_status('applications'), [])
+ def test_v211_card_gets_no_tag_list(self): + """v2.1.1 section 9.4.2.3 has no tag list,not send a tag list""" + sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V211) + sd.get_status('applications') + self.assertEqual(scc.sent, ['80f24002024f0000']) + self.assertNotIn('5c', scc.sent[0][8:]) + + def test_v22_card_gets_a_tag_list(self): + sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + sd.get_status('applications') + self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00']) + + def test_unknown_version_gets_no_tag_list(self): + """If the card will not say, assume the conservative form that works everywhere.""" + sd, scc = self._sd([(self.ENTRY_1, '9000')], card_data=None) + sd.get_status('applications') + self.assertEqual(scc.sent, ['80f24002024f0000']) + + def test_v22_card_rejecting_tag_list_falls_back(self): + """card announcing v2.2+ that still answers 6A80 to the tag list.""" + sd, scc = self._sd([('', '6a80'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + grd_list = sd.get_status('applications') + self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00', + '80f24002024f0000']) + self.assertEqual(self._aids(grd_list), ['a000000151']) + + def test_aid_search_qualifier(self): + sd, scc = self._sd([(self.ENTRY_1, '9000')]) + sd.get_status('applications', 'a000000087') + self.assertEqual(scc.sent, ['80f24002074f05a00000008700']) + + def test_6a80_is_reported_on_a_v211_card(self): + """no tag list -> 6A80 is error""" + sd, _scc = self._sd([('', '6a80')], card_data=CARD_DATA_V211) + with self.assertRaises(SwMatchError) as ctx: + sd.get_status('applications') + self.assertEqual(ctx.exception.sw_actual, '6a80') + def test_unexpected_sw_is_not_silently_truncated(self): """partial is not complete result""" sd, _scc = self._sd([(self.ENTRY_1, '6310'), ('', '6982')]) @@ -779,7 +851,26 @@ sd.get_status('applications') self.assertEqual(ctx.exception.sw_actual, '6982')
+ def test_v22_card_answering_6a88_to_the_tag_list_falls_back(self): + """6A88 is the other GET STATUS error condition of table 11-39, section 11.4.2.3 + says we may get get an error status. 6A88 to the tag-list attempt should be retried + without it or we get nothing""" + sd, scc = self._sd([('', '6a88'), (self.ENTRY_1, '9000')], card_data=CARD_DATA_V22) + grd_list = sd.get_status('applications') + self.assertEqual(scc.sent, ['80f240020b4f005c074f9f70c5cfc4cc00', + '80f24002024f0000']) + self.assertEqual(self._aids(grd_list), ['a000000151'])
+ def test_v22_card_with_a_genuinely_empty_subset(self): + """...and when the retry answers 6A88, the list really is empty.""" + sd, scc = self._sd([('', '6a88'), ('', '6a88')], card_data=CARD_DATA_V22) + self.assertEqual(sd.get_status('applications'), []) + self.assertEqual(len(scc.sent), 2) + + def test_6a88_after_a_page_keeps_that_page(self): + """6A88 is "no more matches" after we have data, we're done""" + sd, _scc = self._sd([(self.ENTRY_1, '6310'), ('', '6a88')], card_data=CARD_DATA_V22) + self.assertEqual(self._aids(sd.get_status('applications')), ['a000000151'])
if __name__ == "__main__": unittest.main()