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:
@@ -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
@@ -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 (Уровень безопасности)
|
||||||
|
|||||||
@@ -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–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 §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 §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 §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 §5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
|
||||||
|
<li><strong>Redundancy Check (RC)</strong> — CRC-32 по тому же кадру заголовка, что и CC (TS 102 225 §5.1.3.2; ключ не нужен, SPI1 b2b1 = <code class="font-mono text-sm">01</code>)</li>
|
||||||
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5 b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 §5.1.2/§5.1.3.1</li>
|
<li>AES требует счётчик с защитой от повтора: биты SPI1 b5 b4 должны быть <code class="font-mono text-sm">10</code> (счётчик больше) или <code class="font-mono text-sm">11</code> (счётчик +1) согласно TS 102 225 §5.1.2/§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 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Разбор R-APDU»</strong> (Remote APDU → «Разбор»), а успешный 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 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Разбор R-APDU»</strong> (Remote APDU → «Разбор»), а успешный 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
@@ -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–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 §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 §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 §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 §5.1.3.1, KID <code class="font-mono text-sm">x2</code>)</li>
|
||||||
|
<li><strong>Redundancy Check (RC)</strong> — CRC-32 over the same header frame as the CC (TS 102 225 §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 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 §5.1.2/§5.1.3.1</li>
|
<li>AES requires a replay-protected counter: SPI1 bits b5 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 §5.1.2/§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 “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” 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 §4.3) — 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’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’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 “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A “Send to Card” 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’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 §4.3) — 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’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’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
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v252';
|
const CACHE = 'simple-v253';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user