v3.26.0 review: the keyless-TAR path
An MSL 00 TAR without a default keyset is a legitimate keyless entry, but _resolve_operation_keyset left the request's KIc/KID empty and the RAM / APDU builds failed with "invalid literal for int() with base 16: ''". The resolution now fills the spec's no-security 00/00 KIc/KID form for a preset entry without a keyset, and refuses a TAR outside the preset with a hint (the old silent '25' default was arbitrary). The PWA's TAR change resets the form the same way - a stale previous TAR's KIc/KID would otherwise travel explicitly and build a non-keyless packet. Also: the dead _preset_tar_keyset helper removed; tests for both keyless paths. 892 frontend + 886 Python; sw cache simple-v435.
This commit is contained in:
@@ -5075,6 +5075,10 @@ function spTarChanged() {
|
|||||||
if (entry) {
|
if (entry) {
|
||||||
const ks = entry.keyset ? spKeysetFor(preset, entry.keyset) : null;
|
const ks = entry.keyset ? spKeysetFor(preset, entry.keyset) : null;
|
||||||
if (ks) spKeysetApply(ks);
|
if (ks) spKeysetApply(ks);
|
||||||
|
// A TAR without a default keyset (MSL 00) gets the spec's no-security
|
||||||
|
// values - a stale previous TAR's KIc/KID would otherwise travel
|
||||||
|
// explicitly and build a non-keyless packet (v3.26.0 review).
|
||||||
|
else spKeysetApply({ kic: '00', kid: '00', kicKey: '', kidKey: '' });
|
||||||
if (entry.msl) spSpi1Seed(entry.msl);
|
if (entry.msl) spSpi1Seed(entry.msl);
|
||||||
}
|
}
|
||||||
spInvalidate();
|
spInvalidate();
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v434';
|
const CACHE = 'simple-v435';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ let code = '';
|
|||||||
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||||
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
|
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
|
||||||
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml',
|
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml',
|
||||||
'cardsTarList', 'cardsTarEntry', 'cardsRoleTar']) {
|
'cardsTarList', 'cardsTarEntry', 'cardsRoleTar',
|
||||||
|
'spTarChanged', 'spKeysetApply', 'updateSpKic', 'updateSpKid']) {
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
code += 'function t(s){return s;}\n';
|
code += 'function t(s){return s;}\n';
|
||||||
@@ -180,6 +181,43 @@ test('the RAM and TAR views rebuild their keyset lists when their preset is know
|
|||||||
assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }"));
|
assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('spTarChanged applies the TAR keyset and clears it for a keyless TAR', () => {
|
||||||
|
// v3.26.0 review: a TAR without a default keyset (MSL 00) must reset the
|
||||||
|
// form to the no-security '00/00' form - a stale previous TAR's KIc/KID
|
||||||
|
// would otherwise travel explicitly and build a non-keyless packet
|
||||||
|
globalThis.updateKeyPlaceholder = () => {};
|
||||||
|
globalThis.spInvalidate = () => {};
|
||||||
|
globalThis.updateSp = () => {};
|
||||||
|
globalThis.spCntrPlaceholder = () => {};
|
||||||
|
const els = {
|
||||||
|
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
|
||||||
|
'sp-card-sel': { value: '0' },
|
||||||
|
'sp-kic-idx': { value: '' }, 'sp-kic-alg': { value: '' }, 'sp-kic-hex': { value: '' },
|
||||||
|
'sp-kid-idx': { value: '' }, 'sp-kid-alg': { value: '' }, 'sp-kid-hex': { value: '' },
|
||||||
|
'sp-kic-key': { value: '' }, 'sp-kid-key': { value: '' }, 'sp-cntr': { value: 'X' },
|
||||||
|
};
|
||||||
|
globalThis.document = { getElementById: id => els[id] || null };
|
||||||
|
globalThis.cards = [{ name: 'EP', keysets: [
|
||||||
|
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }],
|
||||||
|
tars: [
|
||||||
|
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
|
||||||
|
{ tar: 'B00011', msl: '00' }] }];
|
||||||
|
// a keyset TAR applies its KIc/KID and keys (the counter override clears)
|
||||||
|
els['sp-tar'].value = '000000';
|
||||||
|
spTarChanged();
|
||||||
|
assert.strictEqual(els['sp-kic-hex'].value, '15');
|
||||||
|
assert.strictEqual(els['sp-kid-hex'].value, '15');
|
||||||
|
assert.strictEqual(els['sp-kic-key'].value, 'AA');
|
||||||
|
assert.strictEqual(els['sp-cntr'].value, '');
|
||||||
|
// a keyless TAR (MSL 00, no default keyset) resets to the no-security form
|
||||||
|
els['sp-kic-key'].value = 'AA';
|
||||||
|
els['sp-tar'].value = 'B00011';
|
||||||
|
spTarChanged();
|
||||||
|
assert.strictEqual(els['sp-kic-hex'].value, '00');
|
||||||
|
assert.strictEqual(els['sp-kid-hex'].value, '00');
|
||||||
|
assert.strictEqual(els['sp-kic-key'].value, '');
|
||||||
|
});
|
||||||
|
|
||||||
test('the TAR selector waits for a preset and lists its entries', () => {
|
test('the TAR selector waits for a preset and lists its entries', () => {
|
||||||
const els = {
|
const els = {
|
||||||
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
|
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
|
||||||
|
|||||||
@@ -1700,15 +1700,6 @@ def _preset_keyset_check(server, preset_id, kic, kid):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _preset_tar_keyset(server, preset_id, tar):
|
|
||||||
"""The default keyset number of a TAR in the named preset (v3.26.0), or
|
|
||||||
None (no preset, no such TAR entry, or an MSL 00 entry without one)."""
|
|
||||||
preset = _preset_by_id(server, preset_id)
|
|
||||||
if preset is None:
|
|
||||||
return None
|
|
||||||
return presets.tar_keyset(preset, tar)
|
|
||||||
|
|
||||||
|
|
||||||
def _preset_keyset_by_kvn(preset, kvn):
|
def _preset_keyset_by_kvn(preset, kvn):
|
||||||
"""The preset's keyset dict with number `kvn`, or None."""
|
"""The preset's keyset dict with number `kvn`, or None."""
|
||||||
return next((ks for ks in _preset_keysets(preset)
|
return next((ks for ks in _preset_keysets(preset)
|
||||||
@@ -1737,6 +1728,18 @@ def _resolve_operation_keyset(server, body, tar):
|
|||||||
if msl and msl != '00':
|
if msl and msl != '00':
|
||||||
return None, ('TAR %s needs a default keyset (its MSL %s requires '
|
return None, ('TAR %s needs a default keyset (its MSL %s requires '
|
||||||
'security) - set it in the Cards tab' % (tar, msl))
|
'security) - set it in the Cards tab' % (tar, msl))
|
||||||
|
if presets.tar_entry(preset, tar) is None:
|
||||||
|
# A TAR outside the preset: the caller must name its keyset - the
|
||||||
|
# old silent '25' default was arbitrary (v3.26.0 review).
|
||||||
|
return None, ('TAR %s is not in the preset - add it in the Cards '
|
||||||
|
'tab or pass the keyset (KIc/KID) explicitly' % tar)
|
||||||
|
# An MSL 00 entry without a default keyset: the spec's no-security
|
||||||
|
# values (A.2) - the packet is built keyless. A keyset set on an
|
||||||
|
# MSL 00 entry was handled above (it supplies its KIc/KID).
|
||||||
|
body['kic'] = '00'
|
||||||
|
body['kid'] = '00'
|
||||||
|
body['kicKey'] = ''
|
||||||
|
body['kidKey'] = ''
|
||||||
return None, None
|
return None, None
|
||||||
keyset = _preset_keyset_by_kvn(preset, kvn)
|
keyset = _preset_keyset_by_kvn(preset, kvn)
|
||||||
if keyset is None:
|
if keyset is None:
|
||||||
|
|||||||
@@ -1181,6 +1181,51 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(status, 400, resp)
|
self.assertEqual(status, 400, resp)
|
||||||
self.assertIn('needs a default keyset', resp['error'])
|
self.assertIn('needs a default keyset', resp['error'])
|
||||||
|
|
||||||
|
def test_a_keyless_tar_builds_the_no_security_form(self):
|
||||||
|
# v3.26.0 review: an MSL 00 entry without a default keyset is a
|
||||||
|
# legitimate keyless TAR - the operation sends the spec's '00/00'
|
||||||
|
# KIc/KID form instead of failing on an empty one, and a TAR outside
|
||||||
|
# the preset is refused with a hint
|
||||||
|
p = self.store.add(self._preset(iccid='8970119000004600098',
|
||||||
|
tars=[{'role': 'isd', 'tar': '000000', 'msl': '00'},
|
||||||
|
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'keyset': 1},
|
||||||
|
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'keyset': 1}],
|
||||||
|
keysets=[self._keyset(), self._keyset(kic='29', kid='29')]))
|
||||||
|
built = []
|
||||||
|
patches = [
|
||||||
|
mock.patch.object(self.srv, '_build_secured_packet',
|
||||||
|
side_effect=lambda *a, **k: (built.append(a) or ('00' * 21, {}))),
|
||||||
|
mock.patch.object(self.srv, '_send_secured_packet', lambda *a, **k: {
|
||||||
|
'success': True, 'sw': '9000', 'bytes': 21, 'segments': 1,
|
||||||
|
'response_data': ''}),
|
||||||
|
mock.patch.object(self.srv, '_decode_por', lambda *a, **k: None),
|
||||||
|
]
|
||||||
|
self.server.app = object()
|
||||||
|
for patch in patches:
|
||||||
|
patch.start()
|
||||||
|
try:
|
||||||
|
status, resp = self._post('/api/send-ota', {
|
||||||
|
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id']})
|
||||||
|
finally:
|
||||||
|
for patch in reversed(patches):
|
||||||
|
patch.stop()
|
||||||
|
self.assertEqual(status, 200, resp)
|
||||||
|
args = built[0]
|
||||||
|
self.assertEqual((args[0], args[2], args[3]), ('00', '00', '00'))
|
||||||
|
# a TAR outside the preset without explicit KIc/KID is refused: the
|
||||||
|
# SPI1 resolution names it first, and with an explicit SPI1 the
|
||||||
|
# keyset hint follows
|
||||||
|
status, resp = self._post('/api/send-ota', {
|
||||||
|
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id'],
|
||||||
|
'tar': 'B00099'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('B00099', resp['error'])
|
||||||
|
status, resp = self._post('/api/send-ota', {
|
||||||
|
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id'],
|
||||||
|
'tar': 'B00099', 'spi1': '16'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('is not in the preset', resp['error'])
|
||||||
|
|
||||||
def test_a_ram_install_refuses_an_undefined_key_version(self):
|
def test_a_ram_install_refuses_an_undefined_key_version(self):
|
||||||
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
p = self.store.add(self._preset(iccid='8970119000004600098'))
|
||||||
status, resp = self._post('/api/ram-install', {
|
status, resp = self._post('/api/ram-install', {
|
||||||
|
|||||||
Reference in New Issue
Block a user