esim: decode EUICCInfo1/2 and the RAT per SGP.22 (no version bump yet)

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.
This commit is contained in:
2026-09-21 23:56:41 +03:00
parent d087632573
commit 91c642a646
10 changed files with 518 additions and 37 deletions
+1 -1
View File
@@ -497,7 +497,7 @@
<h3 id="esim" class="text-lg font-medium mb-2">8.8 eSIM / LPA (локальные операции)</h3>
<p class="text-sm mb-2">Для eUICC (SGP.22/SGP.32) подвкладка <strong>&laquo;eSIM&raquo;</strong> читает чип и управляет установленными профилями через локальный интерфейс ES10 (через pySim, без обращения к SM-DP+):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Чип</strong> — EID, EUICCInfo1/2 (версия профиля, SVN, прошивка, версия GlobalPlatform, возможности RSP, категория, …) и настроенные адреса SM-DP+ по умолчанию / корневого DS.</li>
<li><strong>Чип</strong> — EID, EUICCInfo1/2 со всеми декодированными полями (версия профиля, SVN, прошивка, расширенные ресурсы карты — число установленных приложений и свободная энергонезависимая/энергозависимая память —, битовые списки возможностей UICC и RSP, идентификаторы CI PKI, категория, запрещённые PPR, версия PP, номер аккредитации SS, данные сертификации), настроенные адреса SM-DP+ по умолчанию / корневого DS и <strong>таблица авторизации правил</strong> (ES10b GetRat: идентификаторы PPR, разрешённые операторы, флаг согласия). Неизвестные TLV сохраняются как сырой hex.</li>
<li><strong>Профили</strong> — все профили с состоянием (включён/отключён), ником, оператором, именем, ICCID, ISD-P AID, классом, владельцем и типом значка. Кнопки <strong>&laquo;Включить&raquo;</strong>/<strong>&laquo;Отключить&raquo;</strong> переключают профиль; карта обычно сначала отправляет REFRESH (виден в журнале проактивных команд), а после переключения сессия карты переинициализируется как при equip — ICCID, сетевое состояние и все кэшированные данные перечитываются.</li>
<li><strong>Уведомления</strong> — список ожидающих уведомлений только для чтения (номер последовательности, операция, адрес, ICCID); ничего не обрабатывается и не удаляется.</li>
</ul>
+1 -1
View File
@@ -497,7 +497,7 @@
<h3 id="esim" class="text-lg font-medium mb-2">8.8 eSIM / LPA (local operations)</h3>
<p class="text-sm mb-2">For an eUICC (SGP.22/SGP.32) the <strong>eSIM</strong> pill reads the chip and manages the installed profiles through the local ES10 interface (via pySim, no SM-DP+ contact):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Chip</strong> — EID, EUICCInfo1/2 (profile version, SVN, firmware, GlobalPlatform version, RSP capabilities, category, &hellip;) and the configured default SM-DP+ / root DS addresses.</li>
<li><strong>Chip</strong> — EID, EUICCInfo1/2 with every field decoded (profile version, SVN, firmware, extended card resource — installed applications and free non-volatile/volatile memory —, UICC and RSP capability bit lists, CI PKI identifiers, category, forbidden profile policy rules, PP version, SS accreditation number, certification data), the configured default SM-DP+ / root DS addresses, and the <strong>rules authorisation table</strong> (ES10b GetRat: PPR IDs, allowed operators, consent flag). Unknown TLVs are preserved as raw hex.</li>
<li><strong>Profiles</strong> — every profile with its state (enabled/disabled), nickname, provider, name, ICCID, ISD-P AID, class, owner and icon type. <strong>Enable</strong>/<strong>Disable</strong> switches a profile; the card usually sends REFRESH first (logged in the proactive command log) and after the switch the card session is re-initialized like an equip, so the ICCID, network state and every cached card view are re-read.</li>
<li><strong>Notifications</strong> — read-only list of pending notifications (sequence number, operation, address, ICCID); nothing is processed or removed.</li>
</ul>
+82 -11
View File
@@ -1533,13 +1533,25 @@ let _esimNotifications = [];
const ESIM_CHIP_LABELS = {
eid: 'EID', svn: 'SVN', profile_version: 'Profile version',
euicc_firmware_ver: 'Firmware version', ext_card_resource: 'Card resource',
installed_application: 'Installed applications',
free_non_volatile_memory: 'Free non-volatile memory',
free_volatile_memory: 'Free volatile memory',
uicc_capability: 'UICC capability', ts102241_version: 'TS 102 241 version',
globalplatform_version: 'GlobalPlatform version', rsp_capability: 'RSP capability',
euicc_category: 'Category', pp_version: 'PP version',
ss_acreditation_number: 'SS accreditation number',
euicc_category: 'Category', forbidden_profile_policy_rules: 'Forbidden PPRs',
pp_version: 'PP version', ss_acreditation_number: 'SS accreditation number',
certification_data_object: 'Certification data',
platform_label: 'Platform label', discovery_base_url: 'Discovery base URL',
tre_properties: 'TRE properties', tre_product_reference: 'TRE product reference',
additional_euicc_profile_package_versions: 'Additional profile package versions',
raw_tlvs: 'Raw TLVs',
default_dp_address: 'Default SM-DP+ address', root_ds_address: 'Root DS address',
euicc_ci_pki_list_for_verification: 'CI PKI (verification)',
euicc_ci_pki_list_for_signing: 'CI PKI (signing)',
subject_key_identifier: 'Subject key identifier',
rat: 'Rules authorisation table', ppr_ids: 'PPR IDs',
allowed_operators: 'Allowed operators', ppr_flags: 'PPR flags',
plmn: 'PLMN', gid1: 'GID1', gid2: 'GID2',
iot_specific_info: 'IoT specific info',
};
@@ -1563,25 +1575,32 @@ function esimGroupEid(eid) {
return s.replace(/(.{4})(?=.)/g, '$1 ');
}
// Chip-info value -> [label, value] rows; nested dicts recurse, arrays are
// joined, empty values skipped.
// Chip-info value -> [label, value] rows; nested dicts recurse, arrays of
// scalars are joined, arrays of objects recurse with an index label, empty
// values skipped. Labels are mapped and translated per path component.
function esimFieldRows(obj, prefix) {
const rows = [];
if (obj === null || obj === undefined) return rows;
if (typeof obj !== 'object') {
rows.push([prefix ? esimLabel(prefix) : '', String(obj)]);
rows.push([prefix || '', String(obj)]);
return rows;
}
for (const k of Object.keys(obj)) {
const v = obj[k];
if (v === null || v === undefined || v === '') continue;
const label = prefix ? prefix + ' / ' + k : k;
const label = prefix ? prefix + ' / ' + t(esimLabel(k)) : t(esimLabel(k));
if (Array.isArray(v)) {
rows.push([esimLabel(label), v.map(x => (typeof x === 'object' ? JSON.stringify(x) : String(x))).join(', ')]);
if (v.length && typeof v[0] === 'object') {
v.forEach((item, i) => {
rows.push(...esimFieldRows(item, label + ' ' + (i + 1)));
});
} else {
rows.push([label, v.map(x => String(x)).join(', ')]);
}
} else if (typeof v === 'object') {
rows.push(...esimFieldRows(v, label));
} else {
rows.push([esimLabel(label), String(v)]);
rows.push([label, String(v)]);
}
}
return rows;
@@ -1621,11 +1640,27 @@ function esimRenderChip() {
const c = _esimChip || {};
const rows = [];
if (c.eid) rows.push(['EID', esimGroupEid(c.eid)]);
rows.push(...esimFieldRows(c.info1 || {}, ''));
rows.push(...esimFieldRows(c.info2 || {}, ''));
rows.push(...esimFieldRows(c.addresses || {}, ''));
const sections = [
['EUICCInfo1', c.info1], ['EUICCInfo2', c.info2],
['Addresses', c.addresses], ['Rules authorisation table', c.rat],
];
for (const [name, data] of sections) {
if (!data || (Array.isArray(data) && !data.length)) continue;
rows.push(['§ ' + t(name), '']);
if (Array.isArray(data)) {
data.forEach((item, i) => {
rows.push(...esimFieldRows(item, t('Rule') + ' ' + (i + 1)));
});
} else {
rows.push(...esimFieldRows(data, ''));
}
}
let html = '';
for (const [k, v] of rows) {
if (k.startsWith('§ ')) {
html += '<div class="mt-2 mb-0.5 font-semibold text-gray-600 dark:text-slate-300">' + esc(k.slice(2)) + '</div>';
continue;
}
html += '<div class="flex gap-2 mb-0.5"><span class="w-56 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
'<span class="font-mono break-all">' + esc(v) + '</span></div>';
}
@@ -13573,6 +13608,42 @@ const LANG_RU = {
'Not available': 'Недоступно',
'No profiles': 'Нет профилей',
'No notifications': 'Нет уведомлений',
'EID': 'EID',
'SVN': 'SVN',
'Profile version': 'Версия профиля',
'Firmware version': 'Версия прошивки',
'Card resource': 'Ресурсы карты',
'Installed applications': 'Установленных приложений',
'Free non-volatile memory': 'Свободная энергонезависимая память',
'Free volatile memory': 'Свободная энергозависимая память',
'UICC capability': 'Возможности UICC',
'TS 102 241 version': 'Версия TS 102 241',
'GlobalPlatform version': 'Версия GlobalPlatform',
'RSP capability': 'Возможности RSP',
'Category': 'Категория',
'Forbidden PPRs': 'Запрещённые PPR',
'PP version': 'Версия PP',
'SS accreditation number': 'Номер аккредитации SS',
'Certification data': 'Данные сертификации',
'Platform label': 'Метка платформы',
'Discovery base URL': 'Базовый URL Discovery',
'TRE properties': 'Свойства TRE',
'TRE product reference': 'Ссылка на продукт TRE',
'Additional profile package versions': 'Доп. версии пакетов профилей',
'Raw TLVs': 'Сырые TLV',
'CI PKI (verification)': 'CI PKI (проверка)',
'CI PKI (signing)': 'CI PKI (подпись)',
'Subject key identifier': 'Идентификатор ключа субъекта',
'Rules authorisation table': 'Таблица авторизации правил (RAT)',
'Rule': 'Правило',
'PPR IDs': 'Идентификаторы PPR',
'Allowed operators': 'Разрешённые операторы',
'PPR flags': 'Флаги PPR',
'PLMN': 'PLMN',
'Addresses': 'Адреса',
'Default SM-DP+ address': 'Адрес SM-DP+ по умолчанию',
'Root DS address': 'Адрес корневого DS',
'IoT specific info': 'IoT-специфичная информация',
'Enabled': 'Включён',
'Disabled': 'Отключён',
'Enable': 'Включить',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v228';
const CACHE = 'simple-v229';
const URLS = [
'index.html',
'help.html',
+34 -1
View File
@@ -42,6 +42,9 @@ test('esimGroupEid groups the hex digits in fours', () => {
test('esimLabel maps known chip keys and prettifies the rest', () => {
assert.strictEqual(esimLabel('svn'), 'SVN');
assert.strictEqual(esimLabel('default_dp_address'), 'Default SM-DP+ address');
assert.strictEqual(esimLabel('installed_application'), 'Installed applications');
assert.strictEqual(esimLabel('forbidden_profile_policy_rules'), 'Forbidden PPRs');
assert.strictEqual(esimLabel('allowed_operators'), 'Allowed operators');
assert.strictEqual(esimLabel('some_unknown_key'), 'Some unknown key');
});
@@ -52,12 +55,42 @@ test('esimFieldRows flattens nested values and skips empties', () => {
});
assert.deepStrictEqual(rows, [
['SVN', '1.2.3'],
['Nested / profile version', '2.2'],
['Nested / Profile version', '2.2'],
['List', 'a, b'],
['Zero', '0'],
]);
});
test('esimFieldRows labels nested keys per path component', () => {
const rows = esimFieldRows({
euicc_ci_pki_list_for_verification: { subject_key_identifier: '8137AB' },
ext_card_resource: {
installed_application: 0,
free_non_volatile_memory: 439084,
},
}, '');
assert.deepStrictEqual(rows, [
['CI PKI (verification) / Subject key identifier', '8137AB'],
['Card resource / Installed applications', '0'],
['Card resource / Free non-volatile memory', '439084'],
]);
});
test('esimFieldRows recurses into arrays of objects with index labels', () => {
const rows = esimFieldRows({
rat: [{
ppr_ids: ['ppr1', 'ppr2'],
allowed_operators: [{ plmn: 'EEEEEE', gid1: null, gid2: null }],
ppr_flags: ['consentRequired'],
}],
}, '');
assert.deepStrictEqual(rows, [
['Rules authorisation table 1 / PPR IDs', 'ppr1, ppr2'],
['Rules authorisation table 1 / Allowed operators 1 / PLMN', 'EEEEEE'],
['Rules authorisation table 1 / PPR flags', 'consentRequired'],
]);
});
test('esimResultText localizes result codes and passes errors through', () => {
assert.strictEqual(esimResultText({ result: 'catBusy' }), 'card is busy with a CAT session');
assert.strictEqual(esimResultText({ result: 'ok' }), 'ok');