fix: UICC file-access parameters use tag 81, not 82 (v3.6.14)

TS 102 226 8.2.1.3.2.2 defines the EA inner tags as 80 (toolkit params),
C3 (toolkit DAP), 81 (UICC Access Application specific parameters) and 82
(UICC *Administrative* Access) - the plain-text spec extract had shifted the
table columns, so the access list was emitted under 82.  With the list under
82 the card rejected the ADF.USIM entry with 6A80 and the applet never got
its uicc.access.FileView rights (live 2026-09-28).

- stkParamsBuild emits the access list under 81 (the structure is unchanged:
  the admin field is coded the same way per 8.2.1.3.2.2.4).
- tests updated (81 04 / 81 0F / 81 17 expectations).

641 frontend / 496 Python green; version 3.6.14; sw simple-v287.
This commit is contained in:
2026-09-28 03:07:40 +03:00
parent 138760f75c
commit 7c683fcb0a
5 changed files with 22 additions and 21 deletions
+9 -8
View File
@@ -1682,7 +1682,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.13'; const SIMPLE_VERSION = '3.6.14';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -2548,7 +2548,7 @@ function ramResetGrants() {
// value cannot be coded: each TAR is 3 bytes (6 hex digits), and menu item // value cannot be coded: each TAR is 3 bytes (6 hex digits), and menu item
// identifiers 128..255 are reserved for the toolkit framework, so only // identifiers 128..255 are reserved for the toolkit framework, so only
// 01..7F may be requested (TS 102 226 8.2.1.3.2.3). In EA mode `v.fsAccess` // 01..7F may be requested (TS 102 226 8.2.1.3.2.3). In EA mode `v.fsAccess`
// adds the UICC file-access parameters (tag '82') and `v.adfAccess` extends // adds the UICC file-access parameters (tag '81') and `v.adfAccess` extends
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path // them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
// grants access via the CA Access Domain field instead. // grants access via the CA Access Domain field instead.
function stkParamsBuild(v) { function stkParamsBuild(v) {
@@ -2600,21 +2600,22 @@ function stkParamsBuild(v) {
services.toString(16).padStart(2, '0'); services.toString(16).padStart(2, '0');
let eaValue = '80' + berLenStr(tkPayload.length / 2) + tkPayload; let eaValue = '80' + berLenStr(tkPayload.length / 2) + tkPayload;
if (v.fsAccess) { if (v.fsAccess) {
// UICC Access Application specific parameters (TS 102 226 // UICC Access Application specific parameters (tag '81', TS 102 226
// 8.2.1.3.2.2.2): every entry ends with the "Length of Access Domain // 8.2.1.3.2.2/8.2.1.3.2.2.2; '82' is the *Administrative* Access
// DAP" byte (00 = no DAP): // field): every entry ends with the "Length of Access Domain DAP"
// byte (00 = no DAP):
// [file system AID length 00 = shared FS][AD length 01] // [file system AID length 00 = shared FS][AD length 01]
// [ADP 00 = full access][DAP length 00] // [ADP 00 = full access][DAP length 00]
// [ADF AID length][ADF AID][AD length 01][ADP 00][DAP length 00] // [ADF AID length][ADF AID][AD length 01][ADP 00][DAP length 00]
// (without the DAP length byte the card rejected the ADF entry with // (the ADF AID must be 5..16 bytes; under '82' the card rejected the
// 6A80; the ADF AID must be 5..16 bytes.) // ADF entry with 6A80 - the applet never got its FileView rights.)
let acc = '000100' + '00'; let acc = '000100' + '00';
if (v.adfAccess) { if (v.adfAccess) {
const adf = (v.adfAid || 'A0000000871002').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); const adf = (v.adfAid || 'A0000000871002').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (adf.length < 10 || adf.length > 32) return null; if (adf.length < 10 || adf.length > 32) return null;
acc += (adf.length / 2).toString(16).padStart(2, '0').toUpperCase() + adf + '0100' + '00'; acc += (adf.length / 2).toString(16).padStart(2, '0').toUpperCase() + adf + '0100' + '00';
} }
eaValue += '82' + berLenStr(acc.length / 2) + acc; eaValue += '81' + berLenStr(acc.length / 2) + acc;
} }
return 'EA' + berLenStr(eaValue.length / 2) + eaValue; return 'EA' + berLenStr(eaValue.length / 2) + eaValue;
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v286'; const CACHE = 'simple-v287';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+10 -10
View File
@@ -150,24 +150,24 @@ test('the RAM form fields build the live install parameters end to end', () => {
}); });
test('UICC file-access parameters (82) are appended in EA mode', () => { test('UICC file-access parameters (82) are appended in EA mode', () => {
// TS 102 226 8.2.1.3.2.2.2: every entry ends with the "Length of Access // TS 102 226 8.2.1.3.2.2/8.2.1.3.2.2.2: the file-access list is the
// Domain DAP" byte (00 = no DAP): // tag '81' field ('82' is the *Administrative* Access field); every entry
// ends with the "Length of Access Domain DAP" byte (00 = no DAP):
// [FS AID len 00 = shared FS][AD len 01][ADP 00 = full][DAP len 00] // [FS AID len 00 = shared FS][AD len 01][ADP 00 = full][DAP len 00]
// [ADF AID len][ADF AID][AD len 01][ADP 00][DAP len 00] // [ADF AID len][ADF AID][AD len 01][ADP 00][DAP len 00]
// The SIM path grants the same rights via the CA Access Domain field; the // The SIM path grants the same rights via the CA Access Domain field; the
// ADF entry is an extension of the file-system entry. A missing DAP // ADF entry is an extension of the file-system entry. Under '82' the card
// length byte made the card reject the ADF entry with 6A80 (live // rejected the ADF entry with 6A80 (live 2026-09-28).
// 2026-09-28).
const base = vals({ channels: '1', msl: '12', tar: 'AF4D01' }); const base = vals({ channels: '1', msl: '12', tar: 'AF4D01' });
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { fsAccess: true })), assert.strictEqual(stkParamsBuild(Object.assign({}, base, { fsAccess: true })),
'EA15800D000000000102011203AF4D0100820400010000'); 'EA15800D000000000102011203AF4D0100810400010000');
assert.strictEqual( assert.strictEqual(
stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true })), stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true })),
'EA20800D000000000102011203AF4D0100820F0001000007A0000000871002010000'); 'EA20800D000000000102011203AF4D0100810F0001000007A0000000871002010000');
assert.strictEqual( assert.strictEqual(
stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true, stkParamsBuild(Object.assign({}, base, { fsAccess: true, adfAccess: true,
adfAid: 'A0000000871002FF33FFFF89010101' })), adfAid: 'A0000000871002FF33FFFF89010101' })),
'EA28800D000000000102011203AF4D01008217000100000FA0000000871002FF33FFFF89010101010000'); 'EA28800D000000000102011203AF4D01008117000100000FA0000000871002FF33FFFF89010101010000');
assert.strictEqual(stkParamsBuild(base), 'EA0F800D000000000102011203AF4D0100'); assert.strictEqual(stkParamsBuild(base), 'EA0F800D000000000102011203AF4D0100');
assert.strictEqual(stkParamsBuild(Object.assign({}, base, { adfAccess: true })), assert.strictEqual(stkParamsBuild(Object.assign({}, base, { adfAccess: true })),
'EA0F800D000000000102011203AF4D0100'); 'EA0F800D000000000102011203AF4D0100');
@@ -182,12 +182,12 @@ test('the RAM form emits full file access when the checkbox is ticked', () => {
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12', fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true }); 'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true });
assert.strictEqual(buildRcToolkitParams(), assert.strictEqual(buildRcToolkitParams(),
'EA15800D000000000102011203AF4D0100820400010000'); 'EA15800D000000000102011203AF4D0100810400010000');
fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12', fakeForm({ 'rc-toolkit-enable': true, 'rc-tk-mode': 'ea', 'rc-tk-msl': '12',
'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true, 'rc-tk-tar': 'AF4D01', 'rc-tk-channels': '1', 'rc-tk-fsaccess': true,
'rc-tk-adfaccess': true }); 'rc-tk-adfaccess': true });
assert.strictEqual(buildRcToolkitParams(), assert.strictEqual(buildRcToolkitParams(),
'EA20800D000000000102011203AF4D0100820F0001000007A0000000871002010000'); 'EA20800D000000000102011203AF4D0100810F0001000007A0000000871002010000');
}); });
test('updateStkParamsHex refreshes the field and clears the manual flag', () => { test('updateStkParamsHex refreshes the field and clears the manual flag', () => {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.6.13" version = "3.6.14"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.6.13' VERSION = '3.6.14'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE