fix: select the ISD-R before the profile switch command

The switch refactor dropped the ISD-R selection that the old
_transceive/_run wrapper performed, so the STORE DATA went to whatever
application was selected (MF after a preceding ES10 call) and the card
answered 6D00 ('instruction not supported') without ever reaching the
switch flow.

- esim.switch_profile() now takes the app, selects the ISD-R before
  sending the raw STORE DATA and restores the selection in a finally,
  like the other ES10 functions.
- tests assert the selection and the restore for the 9000 and 91XX
  paths (the FakeLchan/rs already track both).
This commit is contained in:
2026-09-22 00:34:00 +03:00
parent f87f3e9841
commit f6d62c225c
3 changed files with 40 additions and 22 deletions
+25 -19
View File
@@ -509,13 +509,15 @@ def parse_switch_response(action, data_hex):
'message': _error_text(result)} 'message': _error_text(result)}
def switch_profile(send_apdu, run_chain, action, iccid=None, isdp_aid=None, def switch_profile(app, send_apdu, run_chain, action, iccid=None,
refresh=True): isdp_aid=None, refresh=True):
"""Run an ES10c Enable/DisableProfile switch. """Run an ES10c Enable/DisableProfile switch on the ISD-R.
``send_apdu(apdu_hex) -> (data_hex, sw)`` performs one raw STORE DATA and ``send_apdu(apdu_hex) -> (data_hex, sw)`` performs one raw STORE DATA and
``run_chain(sw91)`` answers the proactive command(s) the card sends ``run_chain(sw91)`` answers the proactive command(s) the card sends
alongside the switch (True when a REFRESH was answered). alongside the switch (True when a REFRESH was answered). The ISD-R is
selected first and the previous selection restored afterwards, like the
other ES10 functions.
With the refresh flag set the ISD-R returns OK *before* the REFRESH With the refresh flag set the ISD-R returns OK *before* the REFRESH
(SGP.22 v2.6 §5.7.16/§5.7.17 step 6) and the switch completes upon the (SGP.22 v2.6 §5.7.16/§5.7.17 step 6) and the switch completes upon the
@@ -523,18 +525,22 @@ def switch_profile(send_apdu, run_chain, action, iccid=None, isdp_aid=None,
OK: the STORE DATA is never retried (the mid-switch card answers 6985 to OK: the STORE DATA is never retried (the mid-switch card answers 6985 to
the retry) and the caller re-initializes the card afterwards.""" the retry) and the caller re-initializes the card afterwards."""
apdu = build_switch_apdu(action, iccid, isdp_aid, refresh) apdu = build_switch_apdu(action, iccid, isdp_aid, refresh)
data, sw = send_apdu(apdu) _select_isdr(app)
if sw == '9000': try:
out = parse_switch_response(action, data) data, sw = send_apdu(apdu)
out['refresh_seen'] = False if sw == '9000':
return out out = parse_switch_response(action, data)
if sw and sw.startswith('91'): out['refresh_seen'] = False
refresh_seen = False return out
try: if sw and sw.startswith('91'):
refresh_seen = bool(run_chain(sw)) refresh_seen = False
except Exception as e: try:
sys.stderr.write('ESIM: REFRESH chain failed: %s\n' % e) refresh_seen = bool(run_chain(sw))
return {'ok': True, 'result': 'ok', 'message': 'ok', except Exception as e:
'refresh_seen': refresh_seen} sys.stderr.write('ESIM: REFRESH chain failed: %s\n' % e)
return {'ok': False, 'result': 'undefinedError', 'sw': sw, return {'ok': True, 'result': 'ok', 'message': 'ok',
'message': 'SW %s' % sw} 'refresh_seen': refresh_seen}
return {'ok': False, 'result': 'undefinedError', 'sw': sw,
'message': 'SW %s' % sw}
finally:
_restore(app)
+1 -1
View File
@@ -3861,7 +3861,7 @@ class PysimHandler(BaseHTTPRequestHandler):
_finish_pending_menu(self.server, self.server.scc) _finish_pending_menu(self.server, self.server.scc)
cursor = _PROACTIVE_ENTRY_ID cursor = _PROACTIVE_ENTRY_ID
resp = esim.switch_profile( resp = esim.switch_profile(
self.server.scc._tp.send_apdu, app, self.server.scc._tp.send_apdu,
lambda sw: _esim_refresh_chain(self.server, sw), lambda sw: _esim_refresh_chain(self.server, sw),
action, iccid=body.get('iccid'), action, iccid=body.get('iccid'),
isdp_aid=body.get('isdp_aid'), isdp_aid=body.get('isdp_aid'),
+14 -2
View File
@@ -239,9 +239,11 @@ class EsimTests(unittest.TestCase):
self.assertEqual(out['result'], 'undefinedError') self.assertEqual(out['result'], 'undefinedError')
def test_switch_profile_parses_ok_response(self): def test_switch_profile_parses_ok_response(self):
app, lchan = make_app()
sent = [] sent = []
resp = EnableProfileResp(children=[EnableResult(decoded='ok')]) resp = EnableProfileResp(children=[EnableResult(decoded='ok')])
out = esim.switch_profile( out = esim.switch_profile(
app,
lambda apdu: sent.append(apdu) or (resp.to_tlv().hex(), '9000'), lambda apdu: sent.append(apdu) or (resp.to_tlv().hex(), '9000'),
lambda sw: self.fail('no chain expected'), lambda sw: self.fail('no chain expected'),
'enable', iccid='8970119000004002667') 'enable', iccid='8970119000004002667')
@@ -249,10 +251,15 @@ class EsimTests(unittest.TestCase):
self.assertEqual(out['result'], 'ok') self.assertEqual(out['result'], 'ok')
self.assertFalse(out['refresh_seen']) self.assertFalse(out['refresh_seen'])
self.assertEqual(len(sent), 1) self.assertEqual(len(sent), 1)
# the ISD-R was selected for the command and the selection restored
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertEqual(app.rs.resets, 1)
def test_switch_profile_treats_91xx_as_ok_and_runs_the_chain(self): def test_switch_profile_treats_91xx_as_ok_and_runs_the_chain(self):
app, lchan = make_app()
chain = [] chain = []
out = esim.switch_profile( out = esim.switch_profile(
app,
lambda apdu: ('', '9111'), lambda apdu: ('', '9111'),
lambda sw: chain.append(sw) or True, lambda sw: chain.append(sw) or True,
'disable', iccid='8970119000004002667') 'disable', iccid='8970119000004002667')
@@ -260,17 +267,22 @@ class EsimTests(unittest.TestCase):
self.assertEqual(out['result'], 'ok') self.assertEqual(out['result'], 'ok')
self.assertTrue(out['refresh_seen']) self.assertTrue(out['refresh_seen'])
self.assertEqual(chain, ['9111']) self.assertEqual(chain, ['9111'])
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertEqual(app.rs.resets, 1)
def test_switch_profile_chain_failure_keeps_the_accepted_switch(self): def test_switch_profile_chain_failure_keeps_the_accepted_switch(self):
app, _ = make_app()
def boom(sw): def boom(sw):
raise RuntimeError('fetch failed') raise RuntimeError('fetch failed')
out = esim.switch_profile(lambda apdu: ('', '910f'), boom, out = esim.switch_profile(app, lambda apdu: ('', '910f'), boom,
'disable', iccid='8970119000004002667') 'disable', iccid='8970119000004002667')
self.assertTrue(out['ok']) self.assertTrue(out['ok'])
self.assertFalse(out['refresh_seen']) self.assertFalse(out['refresh_seen'])
def test_switch_profile_reports_error_sw(self): def test_switch_profile_reports_error_sw(self):
out = esim.switch_profile(lambda apdu: ('', '6985'), app, _ = make_app()
out = esim.switch_profile(app, lambda apdu: ('', '6985'),
lambda sw: self.fail('no chain expected'), lambda sw: self.fail('no chain expected'),
'disable', iccid='8970119000004002667') 'disable', iccid='8970119000004002667')
self.assertFalse(out['ok']) self.assertFalse(out['ok'])