91c642a646
The chip endpoint returned pySim's flattened EuiccInfo dict, whose classes are incomplete: the capability fields are raw GreedyBytes, extCardResource is raw bytes and several SGP.22 TLVs are missing from the class, so cards showed 'unknown_ber_tlv_ie_99' and raw hex instead of decoded values. - request the EUICCInfo1/2 and configured-address TLVs raw and decode them in esim.py per SGP.22 v2.6 5.7.8, cross-checked against lpac's es10c_ex.c: extended card resource, UICC/RSP capability bit lists (first octet = unused bits, MSB-first), CI PKI lists, category (both the implicit 0x8B and explicit 0xAB tag encodings), forbidden profile policy rules (0x99), ppVersion (0x04), sasAcreditationNumber (0x0C) and the optional certification data object / TRE fields; undecoded TLVs stay in raw_tlvs instead of being dropped. - add the ES10b GetRat rules authorisation table (PPR ids, allowed operators, consent flag) to the chip response. - PWA: label every new field, map nested labels per path component (the old code only matched whole keys), group the view into EUICCInfo1 / EUICCInfo2 / Addresses / RAT sections, render arrays of objects with index labels and translate the labels (RU). - tests: decoders against a real card's values (077F3E1F80, 0490, 0640, 81010082040006B32C83022646, the RAT fixture) and the frontend label mapping; sw.js simple-v229.
314 lines
14 KiB
Python
314 lines
14 KiB
Python
# coding=utf-8
|
|
"""Tests for the local eSIM/LPA operations (ES10a/b/c) in esim.py.
|
|
|
|
The ES10 transport (``CardApplicationISDR.store_data_tlv``/``get_eid``) is
|
|
monkeypatched with canned responses, so the mapping to the JSON API and the
|
|
ISD-R selection/restore logic are tested without hardware.
|
|
"""
|
|
|
|
import inspect
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
|
|
from pySim.euicc import (
|
|
AID_ISD_R, CardApplicationISDR, DisableProfileResp, DisableResult,
|
|
EnableProfileResp, EnableResult,
|
|
Iccid, IsdpAid, ListNotificationResp, NotificationAddress,
|
|
NotificationMetadata, NotificationMetadataList, ProfileClass, ProfileInfo,
|
|
ProfileInfoListResp, ProfileInfoSeq, ProfileMgmtOperation, ProfileNickname,
|
|
ProfileOwner, ProfileOwnerPLMN, ProfileState, SeqNumber,
|
|
)
|
|
from pySim.utils import h2b
|
|
|
|
from pysim_simple_server import esim
|
|
|
|
|
|
def _tlv(tag, value):
|
|
"""Encode one BER-TLV with a single-byte length (test fixtures only)."""
|
|
tag_hex = '%02X' % tag if tag <= 0xFF else '%04X' % tag
|
|
return '%s%02X%s' % (tag_hex, len(value) // 2, value)
|
|
|
|
|
|
class FakeLchan:
|
|
def __init__(self):
|
|
self.scc = SimpleNamespace(name='lchan-scc')
|
|
self.selected = None
|
|
|
|
def select_file(self, app):
|
|
self.selected = app
|
|
|
|
|
|
def make_app(profile='Consumer eUICC (SGP.22)', isdr=True):
|
|
lchan = FakeLchan()
|
|
apps = {}
|
|
if isdr:
|
|
apps[AID_ISD_R.lower()] = SimpleNamespace(name='ISD-R')
|
|
rs = SimpleNamespace(profile=profile,
|
|
mf=SimpleNamespace(applications=apps),
|
|
lchan=[lchan],
|
|
resets=0)
|
|
|
|
def soft_reset():
|
|
rs.resets += 1
|
|
|
|
rs.soft_reset = soft_reset
|
|
return SimpleNamespace(rs=rs), lchan
|
|
|
|
|
|
def profile_info(iccid, aid, state=None, nickname=None, cls=None, owner=None):
|
|
children = [Iccid(decoded=iccid), IsdpAid(decoded=bytes.fromhex(aid))]
|
|
if state:
|
|
children.append(ProfileState(decoded=state))
|
|
if nickname:
|
|
children.append(ProfileNickname(decoded=nickname))
|
|
if cls:
|
|
children.append(ProfileClass(decoded=cls))
|
|
if owner:
|
|
children.append(ProfileOwner(children=[ProfileOwnerPLMN(decoded=owner)]))
|
|
return ProfileInfo(children=children)
|
|
|
|
|
|
class EsimTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self._orig_store = CardApplicationISDR.store_data_tlv
|
|
self._orig_eid = CardApplicationISDR.get_eid
|
|
self.calls = []
|
|
|
|
def tearDown(self):
|
|
CardApplicationISDR.store_data_tlv = self._orig_store
|
|
CardApplicationISDR.get_eid = self._orig_eid
|
|
|
|
def patch(self, responses):
|
|
"""Queue canned responses for store_data_tlv (Exception entries raise)."""
|
|
def fake(scc, cmd_do, resp_cls, exp_sw='9000'):
|
|
self.calls.append(cmd_do)
|
|
r = responses.pop(0)
|
|
if isinstance(r, Exception):
|
|
raise r
|
|
return r
|
|
CardApplicationISDR.store_data_tlv = staticmethod(fake)
|
|
|
|
def patch_eid(self, eid):
|
|
CardApplicationISDR.get_eid = staticmethod(lambda scc: eid)
|
|
|
|
def test_is_euicc(self):
|
|
app, _ = make_app()
|
|
self.assertTrue(esim.is_euicc(app))
|
|
other, _ = make_app(profile='UICC')
|
|
self.assertFalse(esim.is_euicc(other))
|
|
|
|
def test_profiles_maps_metadata(self):
|
|
app, lchan = make_app()
|
|
resp = ProfileInfoListResp(children=[ProfileInfoSeq(children=[
|
|
profile_info('8970119000004002667', 'A0000005591010FFFFFFFF8900000100',
|
|
state='enabled', nickname='Work', cls='operational',
|
|
owner='250-99'),
|
|
profile_info('8970119000004002668', 'A0000005591010FFFFFFFF8900000200',
|
|
state='disabled', cls='test'),
|
|
])])
|
|
self.patch([resp])
|
|
out = esim.profiles(app)
|
|
self.assertIsNone(out['error'])
|
|
self.assertEqual(len(out['profiles']), 2)
|
|
first = out['profiles'][0]
|
|
self.assertEqual(first['iccid'], '8970119000004002667')
|
|
self.assertEqual(first['isdp_aid'], 'A0000005591010FFFFFFFF8900000100')
|
|
self.assertEqual(first['state'], 'enabled')
|
|
self.assertEqual(first['nickname'], 'Work')
|
|
self.assertEqual(first['class'], 'operational')
|
|
self.assertEqual(first['owner'], '250-99')
|
|
self.assertEqual(out['profiles'][1]['state'], 'disabled')
|
|
# ISD-R was selected and the previous selection restored
|
|
self.assertEqual(lchan.selected.name, 'ISD-R')
|
|
self.assertEqual(app.rs.resets, 1)
|
|
# the request asks for every ProfileInfo tag
|
|
self.assertIn('9F70', self.calls[0].to_tlv().hex().upper())
|
|
|
|
def test_profiles_error_result(self):
|
|
app, _ = make_app()
|
|
resp = ProfileInfoListResp()
|
|
resp.from_tlv(h2b('BF2D0381017F'))
|
|
self.patch([resp])
|
|
out = esim.profiles(app)
|
|
self.assertEqual(out['profiles'], [])
|
|
self.assertEqual(out['error'], 'undefined error')
|
|
|
|
def test_notifications_maps_operations(self):
|
|
app, _ = make_app()
|
|
meta = NotificationMetadata(children=[
|
|
SeqNumber(decoded=3),
|
|
ProfileMgmtOperation(decoded={'install': False, 'enable': True,
|
|
'disable': False, 'delete': False}),
|
|
NotificationAddress(decoded='smdp.example.org'),
|
|
Iccid(decoded='8970119000004002667'),
|
|
])
|
|
self.patch([ListNotificationResp(
|
|
children=[NotificationMetadataList(children=[meta])])])
|
|
out = esim.notifications(app)
|
|
self.assertIsNone(out['error'])
|
|
self.assertEqual(out['notifications'], [{
|
|
'seq_number': 3, 'operations': ['enable'],
|
|
'address': 'smdp.example.org', 'iccid': '8970119000004002667',
|
|
}])
|
|
|
|
def test_chip_info_collects_parts_and_errors(self):
|
|
app, _ = make_app()
|
|
self.patch_eid('89049032000000000000000000000001')
|
|
info1 = _tlv(0xBF20, _tlv(0x82, '010203'))
|
|
addresses = _tlv(0xBF3C, _tlv(0x80, '736D64702E6578616D706C652E6F7267')
|
|
+ _tlv(0x81, ''))
|
|
rat = _tlv(0xBF43, _tlv(0xA0, _tlv(0x30, _tlv(0x80, '0460'))))
|
|
self.patch([info1, RuntimeError('no info2'), addresses, rat])
|
|
out = esim.chip_info(app)
|
|
self.assertEqual(out['eid'], '89049032000000000000000000000001')
|
|
self.assertEqual(out['info1'], {
|
|
'svn': '1.2.3', 'euicc_ci_pki_list_for_verification': [],
|
|
'euicc_ci_pki_list_for_signing': []})
|
|
self.assertIsNone(out['info2'])
|
|
self.assertIn('info2', out['errors'])
|
|
self.assertEqual(out['addresses'], {'default_dp_address': 'smdp.example.org',
|
|
'root_ds_address': ''})
|
|
self.assertEqual(out['rat'], [{'ppr_ids': ['ppr1', 'ppr2'],
|
|
'allowed_operators': [], 'ppr_flags': []}])
|
|
|
|
def test_set_profile_state_enable_uses_iccid_and_refresh(self):
|
|
app, _ = make_app()
|
|
self.patch([EnableProfileResp(children=[EnableResult(decoded='ok')])])
|
|
out = esim.set_profile_state(app, 'enable', iccid='8970119000004002667')
|
|
self.assertTrue(out['ok'])
|
|
self.assertEqual(out['result'], 'ok')
|
|
tlv = self.calls[0].to_tlv().hex().upper()
|
|
self.assertIn('5A0A980711090000042066F7', tlv) # ProfileIdentifier/ICCID
|
|
self.assertTrue(tlv.endswith('810101')) # RefreshFlag = 1
|
|
|
|
def test_set_profile_state_maps_cat_busy(self):
|
|
app, _ = make_app()
|
|
self.patch([DisableProfileResp(children=[DisableResult(decoded='catBusy')])])
|
|
out = esim.set_profile_state(app, 'disable', iccid='8970119000004002667')
|
|
self.assertFalse(out['ok'])
|
|
self.assertEqual(out['result'], 'catBusy')
|
|
self.assertIn('busy', out['message'])
|
|
|
|
def test_set_profile_state_validation(self):
|
|
app, _ = make_app()
|
|
with self.assertRaises(esim.EsimError):
|
|
esim.set_profile_state(app, 'delete', iccid='8970119000004002667')
|
|
with self.assertRaises(esim.EsimError):
|
|
esim.set_profile_state(app, 'enable')
|
|
with self.assertRaises(esim.EsimError):
|
|
esim.set_profile_state(app, 'enable', iccid='abc')
|
|
|
|
def test_select_isdr_requires_an_euicc(self):
|
|
app, _ = make_app(isdr=False)
|
|
with self.assertRaises(esim.EsimError):
|
|
esim.chip_info(app)
|
|
|
|
|
|
SKI = '81370F5125D0B1D408D4C3B232E6D25E795BEBFB'
|
|
|
|
|
|
class EsimInfoDecodeTests(unittest.TestCase):
|
|
"""EUICCInfo1/2 and RAT decoders against a real consumer eUICC's values."""
|
|
|
|
def test_bit_string_decodes_unused_bits_msb_first(self):
|
|
self.assertEqual(
|
|
esim._decode_bit_string(bytes.fromhex('077F3E1F80'),
|
|
esim.UICC_CAPABILITY_BITS),
|
|
['usimSupport', 'isimSupport', 'csimSupport', 'akaMilenage',
|
|
'akaCave', 'akaTuak128', 'akaTuak256', 'gbaAuthenUsim',
|
|
'gbaAuthenISim', 'mbmsAuthenUsim', 'eapClient', 'javacard',
|
|
'berTlvFileSupport', 'dfLinkSupport', 'catTp', 'getIdentity',
|
|
'profile-a-x25519', 'profile-b-p256'])
|
|
self.assertEqual(
|
|
esim._decode_bit_string(bytes.fromhex('0490'),
|
|
esim.RSP_CAPABILITY_BITS),
|
|
['additionalProfile', 'testProfileSupport'])
|
|
self.assertEqual(
|
|
esim._decode_bit_string(bytes.fromhex('0640'), esim.PPR_ID_BITS),
|
|
['ppr1'])
|
|
self.assertEqual(esim._decode_bit_string(b'', esim.PPR_ID_BITS), [])
|
|
|
|
def test_info2_decodes_every_field(self):
|
|
ski = _tlv(0x04, SKI)
|
|
raw = _tlv(0xBF22, ''.join((
|
|
_tlv(0x81, '020301'), _tlv(0x82, '020202'), _tlv(0x83, '040200'),
|
|
_tlv(0x84, '81010082040006B32C83022646'),
|
|
_tlv(0x85, '077F3E1F80'), _tlv(0x86, '090200'),
|
|
_tlv(0x87, '020300'), _tlv(0x88, '0490'),
|
|
_tlv(0xA9, ski), _tlv(0xAA, ski), _tlv(0x8B, '00'),
|
|
_tlv(0x99, '0640'), _tlv(0x04, '010000'),
|
|
_tlv(0x0C, '45442D5A492D55502D30383236'),
|
|
)))
|
|
out = esim._decode_info2(raw)
|
|
self.assertEqual(out['profile_version'], '2.3.1')
|
|
self.assertEqual(out['svn'], '2.2.2')
|
|
self.assertEqual(out['euicc_firmware_ver'], '4.2.0')
|
|
self.assertEqual(out['ext_card_resource'], {
|
|
'installed_application': 0, 'free_non_volatile_memory': 439084,
|
|
'free_volatile_memory': 9798})
|
|
self.assertEqual(out['uicc_capability'][:3],
|
|
['usimSupport', 'isimSupport', 'csimSupport'])
|
|
self.assertEqual(out['ts102241_version'], '9.2.0')
|
|
self.assertEqual(out['globalplatform_version'], '2.3.0')
|
|
self.assertEqual(out['rsp_capability'],
|
|
['additionalProfile', 'testProfileSupport'])
|
|
self.assertEqual(out['euicc_ci_pki_list_for_verification'], [SKI])
|
|
self.assertEqual(out['euicc_ci_pki_list_for_signing'], [SKI])
|
|
self.assertEqual(out['euicc_category'], 'other')
|
|
self.assertEqual(out['forbidden_profile_policy_rules'], ['ppr1'])
|
|
self.assertEqual(out['pp_version'], '1.0.0')
|
|
self.assertEqual(out['ss_acreditation_number'], 'ED-ZI-UP-0826')
|
|
self.assertNotIn('raw_tlvs', out)
|
|
|
|
def test_info2_accepts_both_category_tags_and_keeps_unknown_tlvs(self):
|
|
out = esim._decode_info2(_tlv(0xBF22, _tlv(0xAB, '02') + _tlv(0xE0, 'AABB')))
|
|
self.assertEqual(out['euicc_category'], 'mediumEuicc')
|
|
self.assertEqual(out['raw_tlvs'], {'E0': 'AABB'})
|
|
|
|
def test_info2_decodes_certification_data_object(self):
|
|
out = esim._decode_info2(_tlv(0xBF22, _tlv(0xAC, _tlv(0x80, '504C')
|
|
+ _tlv(0x81, '68747470733A2F2F642E6578616D706C65'))))
|
|
self.assertEqual(out['certification_data_object'],
|
|
{'platform_label': 'PL',
|
|
'discovery_base_url': 'https://d.example'})
|
|
|
|
def test_info1_decodes_svn_and_ski_lists(self):
|
|
out = esim._decode_info1(_tlv(0xBF20, _tlv(0x82, '020202')
|
|
+ _tlv(0xA9, _tlv(0x04, SKI))))
|
|
self.assertEqual(out, {'svn': '2.2.2',
|
|
'euicc_ci_pki_list_for_verification': [SKI],
|
|
'euicc_ci_pki_list_for_signing': []})
|
|
|
|
def test_rat_decodes_rules(self):
|
|
raw = _tlv(0xBF43, _tlv(0xA0, _tlv(0x30,
|
|
_tlv(0x80, '0460')
|
|
+ _tlv(0xA1, _tlv(0x30, _tlv(0x80, 'EEEEEE')))
|
|
+ _tlv(0x82, '0180'))))
|
|
self.assertEqual(esim._decode_rat(raw), [{
|
|
'ppr_ids': ['ppr1', 'ppr2'],
|
|
'allowed_operators': [{'plmn': 'EEEEEE', 'gid1': None, 'gid2': None}],
|
|
'ppr_flags': ['consentRequired']}])
|
|
|
|
def test_addresses_decode(self):
|
|
out = esim._decode_addresses(_tlv(0xBF3C, _tlv(
|
|
0x81, '74657374726F6F74736D64732E67736D612E636F6D')))
|
|
self.assertIsNone(out['default_dp_address'])
|
|
self.assertEqual(out['root_ds_address'], 'testrootsmds.gsma.com')
|
|
|
|
|
|
class EsimRoutingTests(unittest.TestCase):
|
|
def test_esim_routes_are_in_the_right_http_handlers(self):
|
|
from pysim_simple_server import server
|
|
get_src = inspect.getsource(server.PysimHandler._do_GET)
|
|
post_src = inspect.getsource(server.PysimHandler._do_POST)
|
|
for route in ('/api/esim/chip', '/api/esim/profiles',
|
|
'/api/esim/notifications'):
|
|
self.assertIn("self.path == '%s'" % route, get_src, route)
|
|
self.assertNotIn("self.path == '%s'" % route, post_src, route)
|
|
self.assertIn("self.path == '/api/esim/profile'", post_src)
|
|
self.assertNotIn("self.path == '/api/esim/profile'", get_src)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|