fix: SCP80 RC/CPL parity, file-life-cycle labels, EF.ARR and PS template decoders (v3.5.1)

Three fixes found while filling the ETSI/3GPP registry gaps against the
SIMalliance Stepping Stones R7:

- SCP80 builder/verification parity (TS 31.115 Table 1 NOTE / 4.2 / 4.3):
  the CPL is now transmitted whenever the packet is ciphered or carries
  RC/CC/DS - it is part of their input - and whenever the packet needs SMS
  concatenation; a single unprotected SM keeps pySim's CHL-first form.
  Before, the JS dropped the CPL for every unciphered packet while the
  server reference re-added it, so "Verify vs pySim" reported a false
  MISMATCH for every unciphered RC/CC packet (SPI1 01/02/0A/12/1A...).
  All ten offered SPI1 values now match the server reference byte-for-byte.
- RC (SPI1 b2b1 = 01) was offered but not built: the JS now computes CRC-32
  (TS 102 225 5.1.3.2, pySim zlib.crc32 parity) over the same CPL frame as
  the CC; the packet no longer silently omits the 4-byte RC field.
- Server: _build_secured_packet/_ota_reference add the CPL to a concatenated
  unprotected packet too (Table 1 NOTE / 4.3).

- fcpLifeCycle: unlisted values with b8 clear are RFU, b8 set is proprietary
  (Table 11.7b); previously all unmatched values were labelled proprietary.
- EF.ARR decoder now decodes the expanded format (AM_DO/SC_DO per ISO 7816-4
  5.4.3.2 + TS 102 221 9.2.7): operation bit masks, INCREASE/RESIZE AM_DO
  0x84, OR/AND/NOT templates, PIN key references with usage qualifiers.
- FCP 'C6' PS template DO decoded (PS_DO bitmap + key references + usage
  qualifiers, TS 102 221 11.1.1.4.10/9.5.2) with a shared key-reference map.

Tests: sp.test.js (CPL/RC vectors + crc32 known answer), ef_decode.test.js
(expanded-format ARR vectors), profiler.test.js (LCSI RFU/proprietary, C6),
test_ota_helpers.py (RC reference, unprotected single-SM vs concatenated).
Help EN/RU and READMEs: CPL size 2 (SMS), RC/CC/DS 4-8, RC bullet, CPL rule.
547 frontend / 397 Python green; version 3.5.1; sw cache simple-v253.
This commit is contained in:
2026-09-24 08:05:07 +03:00
parent 7803d8520d
commit 73ff6a60bd
12 changed files with 271 additions and 51 deletions
+2 -2
View File
@@ -415,7 +415,7 @@ Builds SCP80 secured packets per ETSI TS 102 225.
| Field | Size | Description | | Field | Size | Description |
|---|---|---| |---|---|---|
| CPI | 1 | Command Packet Identifier (`02`) | | CPI | 1 | Command Packet Identifier (`02`) |
| CPL | 1 | Command Packet Length | | CPL | 2 (SMS) | Command Packet Length (CHL..end incl. padding) |
| CHI | 1 | Command Header Identifier (`01`) | | CHI | 1 | Command Header Identifier (`01`) |
| CHL | 1 | Command Header Length | | CHL | 1 | Command Header Length |
| SPI | 2 | Security Parameter Indicator | | SPI | 2 | Security Parameter Indicator |
@@ -424,7 +424,7 @@ Builds SCP80 secured packets per ETSI TS 102 225.
| TAR | 3 | Toolkit Application Reference | | TAR | 3 | Toolkit Application Reference |
| CNTR | 5 | Replay counter | | CNTR | 5 | Replay counter |
| PCNTR | 1 | Padding counter | | PCNTR | 1 | Padding counter |
| RC/CC/DS | 8 | Cryptographic Checksum / MAC | | RC/CC/DS | 4–8 | Redundancy Check (CRC-32) / Cryptographic Checksum (MAC) / Digital Signature |
| Secured Data | variable | Padded APDU (encrypted if required) | | Secured Data | variable | Padded APDU (encrypted if required) |
#### SPI1 (Security Level) #### SPI1 (Security Level)
+2 -2
View File
@@ -390,7 +390,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со
| Поле | Размер | Описание | | Поле | Размер | Описание |
|---|---|---| |---|---|---|
| CPI | 1 | Command Packet Identifier (`02`) | | CPI | 1 | Command Packet Identifier (`02`) |
| CPL | 1 | Command Packet Length | | CPL | 2 (SMS) | Command Packet Length (CHL..конец, включая padding) |
| CHI | 1 | Command Header Identifier (`01`) | | CHI | 1 | Command Header Identifier (`01`) |
| CHL | 1 | Command Header Length | | CHL | 1 | Command Header Length |
| SPI | 2 | Security Parameter Indicator | | SPI | 2 | Security Parameter Indicator |
@@ -399,7 +399,7 @@ CLA = `80` (GlobalPlatform v2.3.1). Удалённое управление со
| TAR | 3 | Toolkit Application Reference | | TAR | 3 | Toolkit Application Reference |
| CNTR | 5 | Счётчик повторов | | CNTR | 5 | Счётчик повторов |
| PCNTR | 1 | Padding counter | | PCNTR | 1 | Padding counter |
| RC/CC/DS | 8 | Контрольная сумма / MAC | | RC/CC/DS | 4–8 | CRC (CRC-32) / контрольная сумма (MAC) / цифровая подпись |
| Secured Data | переменная | APDU (с шифрованием при необходимости) | | Secured Data | переменная | APDU (с шифрованием при необходимости) |
#### SPI1 (Уровень безопасности) #### SPI1 (Уровень безопасности)
+4 -3
View File
@@ -285,7 +285,7 @@
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Размер</th><th class="text-left py-1 px-2">Описание</th></tr></thead> <thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Размер</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody> <tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Length</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">2 (SMS)</td><td class="py-1 px-2">Длина командного пакета (CHL..конец, включая padding)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr>
@@ -294,7 +294,7 @@
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Счётчик повторов</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Счётчик повторов</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">8</td><td class="py-1 px-2">Контрольная сумма / MAC</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">4&ndash;8</td><td class="py-1 px-2">CRC (CRC-32) / контрольная сумма (MAC) / цифровая подпись</td></tr>
<tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">пер.</td><td class="py-1 px-2">APDU (с шифрованием при необходимости)</td></tr> <tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">пер.</td><td class="py-1 px-2">APDU (с шифрованием при необходимости)</td></tr>
</tbody> </tbody>
</table> </table>
@@ -304,10 +304,11 @@
<li><strong>AES-CBC</strong> шифрование (нулевой ICV, дополнение нулями до 16), ключи 16/24/32 байта (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li> <li><strong>AES-CBC</strong> шифрование (нулевой ICV, дополнение нулями до 16), ключи 16/24/32 байта (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Retail MAC</strong> (ISO 9797-1, MAC algorithm 3) для контрольной суммы DES/3DES</li> <li><strong>Retail MAC</strong> (ISO 9797-1, MAC algorithm 3) для контрольной суммы DES/3DES</li>
<li><strong>AES-CMAC</strong> (NIST SP 800-38B, усечённый до 8 октетов) для контрольной суммы AES (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li> <li><strong>AES-CMAC</strong> (NIST SP 800-38B, усечённый до 8 октетов) для контрольной суммы AES (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Redundancy Check (RC)</strong> &mdash; CRC-32 по тому же кадру заголовка, что и CC (TS 102 225 &sect;5.1.3.2; ключ не нужен, SPI1 b2b1 = <code class="font-mono text-sm">01</code>)</li>
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5&nbsp;b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li> <li>AES требует счётчик с защитой от повтора: биты SPI1 b5&nbsp;b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li>
<li>Байт паддинга настраивается (<code class="font-mono text-sm">00</code> по умолчанию или <code class="font-mono text-sm">FF</code>)</li> <li>Байт паддинга настраивается (<code class="font-mono text-sm">00</code> по умолчанию или <code class="font-mono text-sm">FF</code>)</li>
</ul> </ul>
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется <strong>конкатенированной</strong> загрузкой (TS 31.115 &sect;4.3) &mdash; пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Разбор R-APDU&raquo;</strong> (Remote APDU &rarr; &laquo;Разбор&raquo;), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p> <p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется <strong>конкатенированной</strong> загрузкой (TS 31.115 &sect;4.3) &mdash; пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>&laquo;Разбор R-APDU&raquo;</strong> (Remote APDU &rarr; &laquo;Разбор&raquo;), а успешный PoR увеличивает счётчик повторов и очищает пакет.</p>
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3> <h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> предустановки, по умолчанию <code class="font-mono text-sm">000000</code>).</p> <p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Предустановка карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> предустановки, по умолчанию <code class="font-mono text-sm">000000</code>).</p>
+4 -3
View File
@@ -284,7 +284,7 @@
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead> <thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody> <tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Identifier (02)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Packet Length</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CPL</td><td class="py-1 px-2">2 (SMS)</td><td class="py-1 px-2">Command Packet Length (CHL..end incl. padding)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHI</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Identifier (01)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CHL</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Command Header Length</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">SPI</td><td class="py-1 px-2">2</td><td class="py-1 px-2">Security Parameter Indicator</td></tr>
@@ -293,7 +293,7 @@
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">TAR</td><td class="py-1 px-2">3</td><td class="py-1 px-2">Toolkit Application Reference</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Replay counter</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">CNTR</td><td class="py-1 px-2">5</td><td class="py-1 px-2">Replay counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">PCNTR</td><td class="py-1 px-2">1</td><td class="py-1 px-2">Padding counter</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">8</td><td class="py-1 px-2">Cryptographic Checksum / MAC</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2 font-mono">RC/CC/DS</td><td class="py-1 px-2">4&ndash;8</td><td class="py-1 px-2">Redundancy Check (CRC-32) / Cryptographic Checksum (MAC) / Digital Signature</td></tr>
<tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">var.</td><td class="py-1 px-2">Padded APDU (encrypted if required)</td></tr> <tr><td class="py-1 px-2 font-mono">Secured Data</td><td class="py-1 px-2">var.</td><td class="py-1 px-2">Padded APDU (encrypted if required)</td></tr>
</tbody> </tbody>
</table> </table>
@@ -303,10 +303,11 @@
<li><strong>AES-CBC</strong> encryption (zero ICV, zero-padded to 16), 16/24/32-byte keys (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li> <li><strong>AES-CBC</strong> encryption (zero ICV, zero-padded to 16), 16/24/32-byte keys (TS 102 225 &sect;5.1.2, KIc <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Retail MAC</strong> (ISO 9797-1 MAC algorithm 3) for the DES/3DES cryptographic checksum</li> <li><strong>Retail MAC</strong> (ISO 9797-1 MAC algorithm 3) for the DES/3DES cryptographic checksum</li>
<li><strong>AES-CMAC</strong> (NIST SP 800-38B, truncated to 8 octets) for the AES cryptographic checksum (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li> <li><strong>AES-CMAC</strong> (NIST SP 800-38B, truncated to 8 octets) for the AES cryptographic checksum (TS 102 225 &sect;5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
<li><strong>Redundancy Check (RC)</strong> &mdash; CRC-32 over the same header frame as the CC (TS 102 225 &sect;5.1.3.2; needs no key, SPI1 b2b1 = <code class="font-mono text-sm">01</code>)</li>
<li>AES requires a replay-protected counter: SPI1 bits b5&nbsp;b4 must be <code class="font-mono text-sm">10</code> (counter higher) or <code class="font-mono text-sm">11</code> (counter +1) per TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li> <li>AES requires a replay-protected counter: SPI1 bits b5&nbsp;b4 must be <code class="font-mono text-sm">10</code> (counter higher) or <code class="font-mono text-sm">11</code> (counter +1) per TS 102 225 &sect;5.1.2/&sect;5.1.3.1</li>
<li>Padding byte configurable (<code class="font-mono text-sm">00</code> default, or <code class="font-mono text-sm">FF</code>)</li> <li>Padding byte configurable (<code class="font-mono text-sm">00</code> default, or <code class="font-mono text-sm">FF</code>)</li>
</ul> </ul>
<p class="text-sm mb-3">A &ldquo;Verify vs pySim&rdquo; button cross-checks the assembled packet against pySim&rsquo;s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A &ldquo;Send to Card&rdquo; button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 &sect;4.3) &mdash; the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card&rsquo;s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command&rsquo;s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter and clears the packet.</p> <p class="text-sm mb-3">A &ldquo;Verify vs pySim&rdquo; button cross-checks the assembled packet against pySim&rsquo;s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A &ldquo;Send to Card&rdquo; button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim&rsquo;s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 &sect;4.3) &mdash; the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card&rsquo;s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command&rsquo;s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter and clears the packet.</p>
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3> <h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default).</p> <p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default).</p>
+163 -22
View File
@@ -1559,7 +1559,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.5.0'; const SIMPLE_VERSION = '3.5.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -4500,10 +4500,23 @@ function aesCmac(data, keyBytes) {
return mac.subarray(0, 8); return mac.subarray(0, 8);
} }
// Redundancy Check (RC) = CRC-32 (TS 102 225 5.1.3.2): reflected polynomial
// 0xEDB88320, init and final XOR 0xFFFFFFFF, result big-endian - byte-identical
// to pySim's zlib.crc32() usage.
function crc32Bytes(data) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < data.length; i++) {
crc ^= data[i];
for (let k = 0; k < 8; k++) crc = (crc >>> 1) ^ (0xEDB88320 & -(crc & 1));
}
crc = (crc ^ 0xFFFFFFFF) >>> 0;
return new Uint8Array([(crc >>> 24) & 0xFF, (crc >>> 16) & 0xFF, (crc >>> 8) & 0xFF, crc & 0xFF]);
}
// ===== Secure Packet assembly ===== // ===== Secure Packet assembly =====
// TS 102 225 Table 1/2 + TS 31.115 Table 1 (SMS-PP): // TS 102 225 Table 1/2 + TS 31.115 Table 1 (SMS-PP):
// CPL(2) CHL(1) SPI(2) KIc(1) KID(1) TAR(3) CNTR(5) PCNTR(1) [RC/CC/DS] secured-data(+padding) // CPL(2) CHL(1) SPI(2) KIc(1) KID(1) TAR(3) CNTR(5) PCNTR(1) [RC/CC/DS] secured-data(+padding)
// CPL = octets from CHL to end incl. padding; CHL = 13 + len_sig. // CPL = octets from CHL to end incl. padding; CHL = 13 + len_sig (CC 8, RC 4).
// CPI (0x70) and CHI (null) are NOT part of the SMS packet data. // CPI (0x70) and CHI (null) are NOT part of the SMS packet data.
function genSp() { function genSp() {
_genSpBuild(); _genSpBuild();
@@ -4575,7 +4588,9 @@ function _genSpBuild() {
const kidIsAes = kidAlg === 0x02; const kidIsAes = kidAlg === 0x02;
const ciphering = (spi1 & 0x04) !== 0; const ciphering = (spi1 & 0x04) !== 0;
const hasMac = (spi1 & 0x03) === 0x02; const rcCcDs = spi1 & 0x03; // 00 none / 01 RC / 10 CC / 11 DS (not offered)
const hasRc = rcCcDs === 0x01;
const hasMac = rcCcDs === 0x02;
const resultEl = document.getElementById('sp-result'); const resultEl = document.getElementById('sp-result');
@@ -4624,13 +4639,13 @@ function _genSpBuild() {
const apdu = hexToBytes(apduHex); const apdu = hexToBytes(apduHex);
const macLen = hasMac ? 8 : 0; const sigLen = hasMac ? 8 : (hasRc ? 4 : 0);
// padding aligns CNTR+PCNTR+RC/CC/DS+data to the cipher block size (ciphering only) // padding aligns CNTR+PCNTR+RC/CC/DS+data to the cipher block size (ciphering only)
const cipherBlock = kicIsAes ? 16 : 8; const cipherBlock = kicIsAes ? 16 : 8;
const padCnt = ciphering ? (cipherBlock - ((6 + macLen + apdu.length) % cipherBlock)) % cipherBlock : 0; const padCnt = ciphering ? (cipherBlock - ((6 + sigLen + apdu.length) % cipherBlock)) % cipherBlock : 0;
const chl = 13 + macLen; const chl = 13 + sigLen;
const cpl = 14 + macLen + apdu.length + padCnt; const cpl = 14 + sigLen + apdu.length + padCnt;
const packetLen = 3 + chl + apdu.length + padCnt; const packetLen = 3 + chl + apdu.length + padCnt;
const packet = new Uint8Array(packetLen); const packet = new Uint8Array(packetLen);
@@ -4652,18 +4667,18 @@ function _genSpBuild() {
packet[14] = parseInt(cntrHex.substr(8,2), 16); packet[14] = parseInt(cntrHex.substr(8,2), 16);
packet[15] = padCnt; packet[15] = padCnt;
const macOff = 16; const macOff = 16;
const dataOff = macOff + macLen; const dataOff = macOff + sigLen;
packet.set(apdu, dataOff); packet.set(apdu, dataOff);
for (let i = 0; i < padCnt; i++) packet[dataOff + apdu.length + i] = padByte; for (let i = 0; i < padCnt; i++) packet[dataOff + apdu.length + i] = padByte;
if (hasMac && kidKey) { if (sigLen) {
// CC is computed over CPL+CHL+SPI+KIc+KID+TAR+CNTR+PCNTR+data(+padding), // RC/CC is computed over CPL+CHL+SPI+KIc+KID+TAR+CNTR+PCNTR+data(+padding),
// i.e. everything up to (but excluding) the RC/CC/DS field (TS 31.115 4.2). // i.e. everything up to (but excluding) the RC/CC/DS field (TS 31.115 4.2).
const macInput = new Uint8Array(packet.length - macLen); const sigInput = new Uint8Array(packet.length - sigLen);
macInput.set(packet.subarray(0, macOff)); sigInput.set(packet.subarray(0, macOff));
macInput.set(packet.subarray(dataOff), macOff); sigInput.set(packet.subarray(dataOff), macOff);
const mac = kidIsAes ? aesCmac(macInput, kidKey) : cbcMac(macInput, kidKey); if (hasRc) packet.set(crc32Bytes(sigInput), macOff);
packet.set(mac, macOff); else if (kidKey) packet.set(kidIsAes ? aesCmac(sigInput, kidKey) : cbcMac(sigInput, kidKey), macOff);
} }
if (ciphering && kicKey) { if (ciphering && kicKey) {
@@ -4675,9 +4690,12 @@ function _genSpBuild() {
packet.set(encrypted, 10); packet.set(encrypted, 10);
} }
// pySim encode_cmd parity: the 2-byte CPL prefix is transmitted only when // CPL transmission (TS 31.115 Table 1 NOTE + 4.3): it is transmitted when
// ciphering is applied; unciphered packets start at CHL (v1.9.5). // the packet is ciphered or carries RC/CC/DS - it is part of their input -
resultEl.value = bytesToHex(ciphering ? packet : packet.subarray(2)); // and it is required once the packet needs SMS concatenation. A single
// unprotected SM keeps pySim's CHL-first form ("not absolutely necessary").
const withCpl = ciphering || rcCcDs !== 0 || (packet.length - 2) > SCP80_SINGLE_BYTES;
resultEl.value = bytesToHex(withCpl ? packet : packet.subarray(2));
} }
// ===== BER-TLV ===== // ===== BER-TLV =====
@@ -13017,13 +13035,17 @@ function fcpFileDescriptor(v) {
function fcpLifeCycle(v) { function fcpLifeCycle(v) {
if (v.length !== 2) return v; if (v.length !== 2) return v;
const b = parseInt(v, 16); const b = parseInt(v, 16);
// TS 102 221 Table 11.7b: the state rows all keep b8-b5 clear; b8 set is
// proprietary, any other unlisted value is RFU.
if (b & 0x80) return 'proprietary (' + v + ')';
if (b & 0x70) return 'RFU (' + v + ')';
if (b === 0x00) return 'no information'; if (b === 0x00) return 'no information';
if (b === 0x01) return 'creation'; if (b === 0x01) return 'creation';
if (b === 0x03) return 'initialization'; if (b === 0x03) return 'initialization';
if ((b & 0x0C) === 0x0C) return 'termination'; if ((b & 0x0C) === 0x0C) return 'termination';
if ((b & 0x05) === 0x05) return 'operational, activated'; if ((b & 0x05) === 0x05) return 'operational, activated';
if ((b & 0x05) === 0x04) return 'operational, deactivated'; if ((b & 0x05) === 0x04) return 'operational, deactivated';
return 'proprietary (' + v + ')'; return 'RFU (' + v + ')';
} }
// TS 102 221 11.1.1.4.8: SFI in b8-b4, b3-b1 zero; length 0 = no short id. // TS 102 221 11.1.1.4.8: SFI in b8-b4, b3-b1 zero; length 0 = no short id.
@@ -13033,6 +13055,35 @@ function fcpSfi(v) {
return v; return v;
} }
// TS 102 221 11.1.1.4.10 / 9.5.2: PS template DO 'C6' = PS_DO ('90') + optional
// usage qualifier ('95') + key reference ('83', repeatable). The PS_DO is a
// bitmap over the key references in order, MSB first; a set bit = PIN enabled.
function fcpPsTemplate(v) {
const parsed = fcpParseTlvs(v);
let psDo = null, uq = null;
const pins = [];
for (const t of parsed.tlvs) {
if (t.tag === '90') psDo = t.value;
else if (t.tag === '95') uq = t.value.length ? parseInt(t.value, 16) : 0;
else if (t.tag === '83' && t.value.length) {
const idx = pins.length;
let enabled = false;
if (psDo && psDo.length) {
const byteIdx = idx >> 3, bit = 7 - (idx & 7);
if (byteIdx * 2 + 2 <= psDo.length) enabled = ((parseInt(psDo.substr(byteIdx * 2, 2), 16) >> bit) & 1) === 1;
}
let s = pinKeyRefName(parseInt(t.value, 16)) + ': ' + (enabled ? 'enabled' : 'disabled');
if (uq === null) s += ', always verified';
else if (uq === 0) s += ', verification not used';
else if (uq & 0x08) s += ', verify';
else s += ', usage 0x' + uq.toString(16).toUpperCase().padStart(2, '0');
pins.push(s);
uq = null;
}
}
return pins.length ? pins.join('; ') : (v || '');
}
function fcpDo(key, value) { function fcpDo(key, value) {
if (/^[0-9A-F]+$/.test(value) === false) value = value || ''; if (/^[0-9A-F]+$/.test(value) === false) value = value || '';
switch (key) { switch (key) {
@@ -13053,7 +13104,7 @@ function fcpDo(key, value) {
case 'A0': return { name: 'Security attributes (data objects)', decoded: value }; case 'A0': return { name: 'Security attributes (data objects)', decoded: value };
case 'A5': return { name: 'Proprietary information', decoded: null }; case 'A5': return { name: 'Proprietary information', decoded: null };
case 'AB': return { name: 'Security attributes (expanded)', decoded: value }; case 'AB': return { name: 'Security attributes (expanded)', decoded: value };
case 'C6': return { name: 'PIN status template DO', decoded: null }; case 'C6': return { name: 'PIN status template DO', decoded: fcpPsTemplate(value) };
case 'A5/80': { case 'A5/80': {
const b = fcpInt(value); const b = fcpInt(value);
const parts = [(b & 0x01) ? 'clock stop allowed' : 'clock stop not allowed']; const parts = [(b & 0x01) ? 'clock stop allowed' : 'clock stop not allowed'];
@@ -13910,10 +13961,100 @@ function efDecDir(b) {
return apps.length ? { applications: apps } : { raw: efHex(b) }; return apps.length ? { applications: apps } : { raw: efHex(b) };
} }
// TS 102 221 9.4.1 Table 9.3: PIN key reference names (ordinal + local PINs).
function pinKeyRefName(v) {
const names = {
0x01: 'PIN app1', 0x02: 'PIN app2', 0x03: 'PIN app3', 0x04: 'PIN app4',
0x05: 'PIN app5', 0x06: 'PIN app6', 0x07: 'PIN app7', 0x08: 'PIN app8',
0x0A: 'ADM1', 0x0B: 'ADM2', 0x0C: 'ADM3', 0x0D: 'ADM4', 0x0E: 'ADM5',
0x11: 'universal PIN',
0x81: 'PIN2 app1', 0x82: 'PIN2 app2', 0x83: 'PIN2 app3', 0x84: 'PIN2 app4',
0x85: 'PIN2 app5', 0x86: 'PIN2 app6', 0x87: 'PIN2 app7', 0x88: 'PIN2 app8',
0x8A: 'ADM6', 0x8B: 'ADM7', 0x8C: 'ADM8', 0x8D: 'ADM9', 0x8E: 'ADM10'
};
if (names[v]) return names[v];
return 'key ref 0x' + v.toString(16).toUpperCase().padStart(2, '0');
}
// ISO 7816-4 Tables 16/17: AM byte bit meanings. A record does not store
// whether it protects an EF or a DF, so both mappings are shown.
function efArrAmLabel(am) {
const bits = [
[0x40, 'DELETE (self)'],
[0x20, 'TERMINATE EF / TERMINATE DF'],
[0x10, 'ACTIVATE'],
[0x08, 'DEACTIVATE'],
[0x04, 'WRITE/APPEND (EF) / CREATE FILE DF (DF)'],
[0x02, 'UPDATE/ERASE (EF) / CREATE FILE EF (DF)'],
[0x01, 'READ/SEARCH (EF) / DELETE FILE child (DF)'],
];
if (am & 0x80) {
const low = bits.filter(b => (b[0] & 0x07) && (am & b[0])).map(b => b[1]);
return 'bits 7-4 proprietary' + (low.length ? ' + ' + low.join(' + ') : '');
}
const ops = bits.filter(b => am & b[0]).map(b => b[1]);
return ops.length ? ops.join(' + ') : 'no operations';
}
// One SC_DO (ISO 7816-4 Table 23; TS 102 221 Annex E.3).
function efArrSc(tlv) {
const tag = tlv.tag, v = tlv.value;
if (tag === 0x90) return 'always';
if (tag === 0x97) return 'never';
if (tag === 0x9E) return 'security condition 0x' + efHex(v);
if (tag === 0xA4) {
let ref = null, uq = null;
for (const t of efTlv(v)) {
if (t.tag === 0x83 && t.value.length) ref = t.value[0];
else if (t.tag === 0x95 && t.value.length) uq = t.value[0];
}
let s = ref === null ? 'PIN' : pinKeyRefName(ref);
if (uq === null) s += ' (verify)';
else if (uq === 0x00) s += ' (verification not used)';
else if (uq & 0x08) s += ' (verify)';
else s += ' (usage 0x' + uq.toString(16).toUpperCase().padStart(2, '0') + ')';
return s;
}
if (tag === 0xB4 || tag === 0xB6 || tag === 0xB8) return 'SM CRT 0x' + tag.toString(16).toUpperCase();
if (tag === 0xA0) return '(' + efTlv(v).map(x => efArrSc(x)).join(' OR ') + ')';
if (tag === 0xAF) return '(' + efTlv(v).map(x => efArrSc(x)).join(' AND ') + ')';
if (tag === 0xA7) {
const inner = efTlv(v)[0];
return 'NOT ' + (inner ? efArrSc(inner) : '?');
}
return '0x' + tag.toString(16).toUpperCase().padStart(2, '0') + ' ' + efHex(v);
}
// One AM_DO (ISO 7816-4 Table 21; INCREASE/RESIZE per TS 102 221 11.1.8.1 and
// TS 102 222 6.10). Returns null for SC_DOs.
function efArrAmDo(tlv) {
const tag = tlv.tag, v = tlv.value;
if (tag === 0x80 && v.length) return 'AM 0x' + efHex(v) + ' (' + efArrAmLabel(v[0]) + ')';
if (tag === 0x84) {
const ins = v.length ? v[v.length - 1] : 0;
if (ins === 0x32) return 'INCREASE (AM_DO 0x84, INS 0x32)';
if (ins === 0xD4) return 'RESIZE FILE (AM_DO 0x84, INS 0xD4)';
return 'AM_DO 0x84 (INS 0x' + ins.toString(16).toUpperCase().padStart(2, '0') + ')';
}
if (tag >= 0x81 && tag <= 0x8F) return 'command description 0x' + tag.toString(16).toUpperCase() + ' (' + efHex(v) + ')';
if (tag === 0x9C) return 'proprietary state machine (' + efHex(v) + ')';
return null;
}
// EF_ARR records: AM_DO + SC_DO sequences in the expanded format
// (ISO 7816-4 5.4.3.2; TS 102 221 9.2.7/13.4). SC_DOs after one AM_DO
// without an OR template all have to be fulfilled (AND).
function efDecArr(b) { function efDecArr(b) {
const rules = []; const rules = [];
for (const t of efTlv(b)) rules.push({ tag: '0x' + t.tag.toString(16).toUpperCase().padStart(2, '0'), hex: efHex(t.value) }); for (const t of efTlv(b)) {
return rules.length ? { rules: rules } : { raw: efHex(b) }; const am = efArrAmDo(t);
if (am !== null) { rules.push({ am: am, scs: [] }); continue; }
const sc = efArrSc(t);
if (!rules.length) rules.push({ am: '(no AM_DO)', scs: [] });
rules[rules.length - 1].scs.push(sc);
}
if (!rules.length) return { raw: efHex(b) };
return { rules: rules.map(r => r.am + (r.scs.length ? ': ' + r.scs.join(' AND ') : '')) };
} }
function efDecPnn(b) { function efDecPnn(b) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v252'; const CACHE = 'simple-v253';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+16 -1
View File
@@ -28,6 +28,7 @@ const FNS = [
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc', 'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl', 'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
'efDecExt1', 'efDecSms', 'efDecSume', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes', 'efDecExt1', 'efDecSms', 'efDecSume', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten', 'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml', 'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
@@ -152,8 +153,22 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
assert.strictEqual(dir.applications.length, 1); assert.strictEqual(dir.applications.length, 1);
assert.strictEqual(dir.applications[0].aid, 'A0000000871002FFFFFFFF8907090000'); assert.strictEqual(dir.applications[0].aid, 'A0000000871002FFFFFFFF8907090000');
assert.strictEqual(dir.applications[0].label, 'USim1'); assert.strictEqual(dir.applications[0].label, 'USim1');
// EF.ARR: AM_DO 01 (read) guarded by ADM1 (TS 102 221 9.2.7 / ISO 7816-4 5.4.3.2)
const arr = efDecArr(efBytes('800101a40683010a950108')); const arr = efDecArr(efBytes('800101a40683010a950108'));
assert.deepStrictEqual(arr.rules, [{ tag: '0x80', hex: '01' }, { tag: '0xA4', hex: '83010A950108' }]); assert.deepStrictEqual(arr.rules, ['AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)']);
// Two rules: update needs PIN app1 or app2, read is always
assert.deepStrictEqual(
efDecArr(efBytes('800102a010a406830101950108a4068301029501088001019000')).rules,
['AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))',
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always']);
// INCREASE / RESIZE ride AM_DO 0x84 with the command INS as value
assert.deepStrictEqual(efDecArr(efBytes('8401329000')).rules,
['INCREASE (AM_DO 0x84, INS 0x32): always']);
assert.deepStrictEqual(efDecArr(efBytes('8401D49000')).rules,
['RESIZE FILE (AM_DO 0x84, INS 0xD4): always']);
// b8=1: bits 7-4 are proprietary
assert.deepStrictEqual(efDecArr(efBytes('8001819000')).rules,
['AM 0x81 (bits 7-4 proprietary + READ/SEARCH (EF) / DELETE FILE child (DF)): always']);
const impi = efDecNai(efBytes('803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')); const impi = efDecNai(efBytes('803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267'));
assert.strictEqual(impi.text, '738006000000101@ims.mnc000.mcc738.3gppnetwork.org'); assert.strictEqual(impi.text, '738006000000101@ims.mnc000.mcc738.3gppnetwork.org');
const smsp = efDecSmsp(efBytes('534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9')); const smsp = efDecSmsp(efBytes('534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9'));
+17 -1
View File
@@ -21,7 +21,8 @@ function extractFunc(src, name, asyncFn) {
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
} }
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget']; const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode',
'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget'];
let code = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n'; code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
@@ -694,6 +695,21 @@ test('fcpDecode maps life cycle status per table 11.7b', () => {
assert.strictEqual(lcs('0C'), 'termination'); assert.strictEqual(lcs('0C'), 'termination');
assert.strictEqual(lcs('0D'), 'termination'); assert.strictEqual(lcs('0D'), 'termination');
assert.strictEqual(lcs('80'), 'proprietary (80)'); assert.strictEqual(lcs('80'), 'proprietary (80)');
assert.strictEqual(lcs('8C'), 'proprietary (8C)');
// Table 11.7b: unlisted values with b8 clear are RFU, not proprietary
assert.strictEqual(lcs('02'), 'RFU (02)');
assert.strictEqual(lcs('2C'), 'RFU (2C)');
});
test('fcpDecode decodes the PS template DO (C6) with PIN key references', () => {
// TS 102 221 11.1.1.4.10 / 9.5.2: PS_DO bitmap over the following key refs
const dec = hex => fcpDecode('620BC609' + hex).items.find(it => it.key === 'C6').decoded;
assert.strictEqual(dec('9001C0950108830101'), 'PIN app1: enabled, verify');
assert.strictEqual(fcpPsTemplate('9001C095010883010183010A'),
'PIN app1: enabled, verify; ADM1: enabled, always verified');
assert.strictEqual(fcpPsTemplate('90014083010183010A'),
'PIN app1: disabled, always verified; ADM1: enabled, always verified');
assert.strictEqual(fcpPsTemplate('900100830111'), 'universal PIN: disabled, always verified');
}); });
test('fcpDecode decodes A5 proprietary sub-TLVs', () => { test('fcpDecode decodes A5 proprietary sub-TLVs', () => {
+29 -10
View File
@@ -27,7 +27,7 @@ function extractFunc(src, name) {
} }
const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt', const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt',
'xorBytes', 'zeroPad', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys', 'xorBytes', 'zeroPad', 'crc32Bytes', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys',
'aesCmac', '_genSpBuild', 'genSp', 'spNextCntr', 'scp80SegmentInfo', 'spSizeInfoText', 'aesCmac', '_genSpBuild', 'genSp', 'spNextCntr', 'scp80SegmentInfo', 'spSizeInfoText',
'spShowSizeInfo']; 'spShowSizeInfo'];
let code = ''; let code = '';
@@ -95,16 +95,35 @@ test('ciphered + CC SPI 16/01 (counter_must_be_higher, plaintext PoR)', () => {
'00201516011515B00000E42573469E68A8462A57A505B0E2B1C09C1928C7A182311F'); '00201516011515B00000E42573469E68A8462A57A505B0E2B1C09C1928C7A182311F');
}); });
test('unciphered + CC SPI 02/09', () => { test('unciphered + CC SPI 02/09 carries the CPL', () => {
assert.strictEqual( const out = makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' });
makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' }), assert.strictEqual(out,
'1502091515B0000000000000010085A8CA1A9828B0BB00A40000023F00'); '001D1502091515B0000000000000010085A8CA1A9828B0BB00A40000023F00');
assert.strictEqual(parseInt(out.slice(0, 4), 16), out.length / 2 - 2);
}); });
test('unciphered packet starts at CHL, no CPL prefix (pySim parity)', () => { test('unprotected packet (SPI 00) keeps the pySim CHL-first form', () => {
const out = makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' }); assert.strictEqual(
assert.strictEqual(out.slice(0, 2), '15'); makeRun({ 'sp-spi1': '00', 'sp-spi2-hex': '09' }),
assert.strictEqual(out.length, 58); '0D00091515B0000000000000010000A40000023F00');
});
test('unprotected concatenated packet gains the CPL (TS 31.115 4.3)', () => {
const out = makeRun({ 'sp-spi1': '00', 'sp-spi2-hex': '09', 'sp-apdu': 'A0'.repeat(200) });
assert.strictEqual(out.length / 2, 216);
assert.strictEqual(parseInt(out.slice(0, 4), 16), 214);
assert.strictEqual(out.slice(4, 6), '0D');
assert.strictEqual(scp80SegmentInfo(out).segments, 2);
});
test('RC (SPI 01) computes CRC-32 over the CPL frame', () => {
assert.strictEqual(
makeRun({ 'sp-spi1': '01', 'sp-spi2-hex': '09' }),
'00191101091515B0000000000000010050C942DC00A40000023F00');
});
test('crc32Bytes known answer (TS 102 225 Annex B)', () => {
assert.strictEqual(bytesToHex(crc32Bytes(hexToBytes('0102030405'))), '470B99F4');
}); });
test('sysmocom public reference vector (spi1 04 / spi2 19, cntr=0)', () => { test('sysmocom public reference vector (spi1 04 / spi2 19, cntr=0)', () => {
@@ -174,7 +193,7 @@ test('AES unciphered + CC SPI 12/09 (counter higher)', () => {
'sp-kic-key': KIC_AES, 'sp-kic-key': KIC_AES,
'sp-kid-key': KID_AES, 'sp-kid-key': KID_AES,
}), }),
'1512092222B0001100000000110029826122C7A0B79500A40004023F00'); '001D1512092222B0001100000000110029826122C7A0B79500A40004023F00');
}); });
test('AES ciphered + CC SPI 1E/19 (counter +1)', () => { test('AES ciphered + CC SPI 1E/19 (counter +1)', () => {
+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.5.0" version = "3.5.1"
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.
+10 -5
View File
@@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.5.0' VERSION = '3.5.1'
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
@@ -937,9 +937,12 @@ def _ota_reference(spi1, spi2, kic, kid, tar_hex, cntr_hex, apdu_hex, kic_key_he
otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex) otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex)
spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16)) spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16))
out = OtaDialectSms().encode_cmd(otak, h2b(tar_hex), spi, h2b(apdu_hex)) out = OtaDialectSms().encode_cmd(otak, h2b(tar_hex), spi, h2b(apdu_hex))
if not spi['ciphering'] and spi['rc_cc_ds'] != 'no_rc_cc_ds': if not spi['ciphering'] and (spi['rc_cc_ds'] != 'no_rc_cc_ds'
or len(out) > SCP80_SINGLE_BYTES):
# pySim drops the CPL octets from its unciphered output; re-add them # pySim drops the CPL octets from its unciphered output; re-add them
# (they are included in the RC/CC/DS calculation) per TS 31.115 4.2. # per TS 31.115 4.2 (they are part of the RC/CC/DS input) and per
# Table 1 NOTE / 4.3 (required for concatenation - the CHL-to-end
# range exceeds one SM).
# CPL counts octets from the CHL octet to the last octet of the # CPL counts octets from the CHL octet to the last octet of the
# Secured Data (incl. padding); pySim's unciphered output is exactly # Secured Data (incl. padding); pySim's unciphered output is exactly
# that range, so the CPL value equals its length. # that range, so the CPL value equals its length.
@@ -1031,10 +1034,12 @@ def _build_secured_packet(spi1, spi2, kic, kid, tar_hex, cntr_hex, apdu_hex,
otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex) otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex)
spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16)) spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16))
out = _encode_cmd_unlimited(otak, spi, h2b(tar_hex), h2b(apdu_hex)) out = _encode_cmd_unlimited(otak, spi, h2b(tar_hex), h2b(apdu_hex))
if not spi['ciphering'] and spi['rc_cc_ds'] != 'no_rc_cc_ds': if not spi['ciphering'] and (spi['rc_cc_ds'] != 'no_rc_cc_ds'
or len(out) > SCP80_SINGLE_BYTES):
# CPL counts octets from the CHL octet to the last octet of the # CPL counts octets from the CHL octet to the last octet of the
# Secured Data (incl. padding) - exactly the length of the # Secured Data (incl. padding) - exactly the length of the
# unciphered range. # unciphered range. Added for the RC/CC/DS input and for
# concatenation (TS 31.115 Table 1 NOTE / 4.3).
cpl = len(out) cpl = len(out)
out = cpl.to_bytes(2, 'big') + out out = cpl.to_bytes(2, 'big') + out
return b2h(out), spi return b2h(out), spi
+22
View File
@@ -59,6 +59,8 @@ REFERENCE_VECTORS = {
'00201516011515b00000e42573469e68a8462a57a505b0e2b1c09c1928c7a182311f', '00201516011515b00000e42573469e68a8462a57a505b0e2b1c09c1928c7a182311f',
('02', '09'): ('02', '09'):
'001d1502091515b0000000000000010085a8ca1a9828b0bb00a40000023f00', '001d1502091515b0000000000000010085a8ca1a9828b0bb00a40000023f00',
('01', '09'):
'00191101091515b0000000000000010050c942dc00a40000023f00',
} }
# AES-128 reference vectors (public synthetic keys from pySim test_ota.py). # AES-128 reference vectors (public synthetic keys from pySim test_ota.py).
@@ -153,6 +155,10 @@ class TestOtaReference(unittest.TestCase):
out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K) out, _ = _ota_reference('02', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out, REFERENCE_VECTORS[('02', '09')]) self.assertEqual(out, REFERENCE_VECTORS[('02', '09')])
def test_unciphered_rc_reference(self):
out, _ = _ota_reference('01', '09', '15', '15', 'b00000', '0000000001', APDU, K, K)
self.assertEqual(out, REFERENCE_VECTORS[('01', '09')])
def test_unciphered_cpl_is_0x001d(self): def test_unciphered_cpl_is_0x001d(self):
# Regression: CPL counts octets from the CHL octet to the last octet # Regression: CPL counts octets from the CHL octet to the last octet
# of the secured data (29 here), it must NOT be len(out)-2 (27/0x001b). # of the secured data (29 here), it must NOT be len(out)-2 (27/0x001b).
@@ -229,6 +235,22 @@ class TestSmsConcatenation(unittest.TestCase):
self.assertEqual(_split_secured_packet(b'A' * 140, include_cpi=False), self.assertEqual(_split_secured_packet(b'A' * 140, include_cpi=False),
[b'A' * 140]) [b'A' * 140])
def test_unprotected_single_sm_keeps_the_chl_first_form(self):
# TS 31.115 Table 1 NOTE: the CPL is "not absolutely necessary" in a
# single SM - an unprotected packet keeps pySim's CHL-first form.
out, _ = _build_secured_packet('00', '09', '15', '15', 'b00000',
'0000000001', APDU, K, K)
self.assertEqual(out[:2], '0d')
def test_unprotected_concatenated_packet_gains_the_cpl(self):
# ... but it is required once the packet needs concatenation
# (TS 31.115 Table 1 NOTE / 4.3).
out, _ = _build_secured_packet('00', '09', '15', '15', 'b00000',
'0000000001', 'A0' * 200, K, K)
self.assertEqual(len(out) // 2, 216)
self.assertEqual(out[:4], '00d6') # CPL = 214 = CHL..end
self.assertEqual(int(out[:4], 16), len(out) // 2 - 2)
def test_240_byte_load_block_encodes_and_fits_the_card_buffer(self): def test_240_byte_load_block_encodes_and_fits_the_card_buffer(self):
# The RAM path no longer clamps LOAD blocks to one SMS: a 240-byte # The RAM path no longer clamps LOAD blocks to one SMS: a 240-byte
# block (the GP maximum) becomes a concatenated command. # block (the GP maximum) becomes a concatenated command.