v1.9.3: extended-channel CLA classes + defined odd INS in the acceptance gates
This commit is contained in:
1 parent
3bf5cee222
commit
9f42a88c01
6 files changed
+376
-112
No files matched your search
@@ -160,14 +160,29 @@ The edge-read path had two bugs:
|
|||||||
|
|
||||||
#### CLA validation note
|
#### CLA validation note
|
||||||
|
|
||||||
All example traces (`test_7`, `test_8`, `xiaomi`, `samsung`)
|
`plausible_cla()` (v1.9.3) accepts the higher logical channels:
|
||||||
contain only CLA `0x00` (interindustry) and `0x8x` (USIM/CAT, mostly `0x80`
|
|
||||||
with occasional `0x81` for logical channel 1).
|
- interindustry `0x00`-`0x1F` (channels 0-3 in b2b1) and `0x40`-`0x5F`
|
||||||
The decoder must **not** mandate specific CLA values — other CLA values
|
(`01xx xxxx`: channels 4-19 in b4..b1, no CLA-level SM; `0x50`-`0x5F`
|
||||||
(e.g. `0x04`, `0x08`, `0x84`, `0xA0`, `0xB0`) are valid per ISO 7816-4 / 3GPP
|
are the legacy chaining variants);
|
||||||
and must be accepted. However, during test runs against the example traces,
|
- the UICC extended classes `'CX'`/`'EX'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`,
|
||||||
any CLA value outside `0x00-0x0F` and `0x80-0x8F` is an error flag indicating
|
channels 4-19, SM in b6 -- TS 102 221 Table 10.4a);
|
||||||
the decoder mis-framed the byte stream.
|
- `0x80`/`0x84`/`0xA0`/`0xB0` (0xB0 has no pinned-spec basis; kept for
|
||||||
|
backward compatibility).
|
||||||
|
|
||||||
|
Rejected: `0x20`-`0x3F` (ISO-reserved); `0x60`-`0x6F`/`0x70`-`0x7F` (the
|
||||||
|
SM-indicating extended classes -- `0x60`-`0x6F` overlaps printable ASCII
|
||||||
|
and the text byte `0x61` started a bogus frame on xiaomi_mi_a1_coldboot
|
||||||
|
when it was accepted); `0x81`-`0x8F`/`0xA1`-`0xAF` (the `'8X'`/`'AX'`
|
||||||
|
standard-channel variants for channels 0-3 -- the FCP byte `0xA5` did the
|
||||||
|
same); `0x90`-`0x9F`, `0xD0`-`0xDF`, `0xF0`-`0xFF`. They are spec-coded
|
||||||
|
but cost frame-start precision; add any of them back when a real trace
|
||||||
|
requires it.
|
||||||
|
During test runs such a CLA is an error flag indicating the decoder
|
||||||
|
mis-framed the byte stream. The example traces carry `0x00`
|
||||||
|
(interindustry), `0x01` (logical channel 1 -- the SELECT-by-AID after
|
||||||
|
MANAGE CHANNEL opens it) and `0x80` (USIM/CAT); no trace uses channels 2-3
|
||||||
|
or the higher channels yet.
|
||||||
|
|
||||||
#### ISO 7816 APDU validation (enforced from v1.1.7)
|
#### ISO 7816 APDU validation (enforced from v1.1.7)
|
||||||
|
|
||||||
@@ -179,10 +194,18 @@ Validation is purely structural (no content scanning of command/response
|
|||||||
data), so the false-positive rate is very low. The checks are:
|
data), so the false-positive rate is very low. The checks are:
|
||||||
|
|
||||||
1. Length is at least 7 bytes (header + SW) and at most `MAX_TPDU_LEN` (271).
|
1. Length is at least 7 bytes (header + SW) and at most `MAX_TPDU_LEN` (271).
|
||||||
2. CLA byte is in the recognized set (`0x00`, `0x04`, `0x08`, `0x80`,
|
2. CLA byte is a structurally valid ISO 7816-4 / UICC class — see
|
||||||
`0x84`, `0xA0`, `0xB0`).
|
`plausible_cla()` (v1.9.3): `0x00`-`0x1F`, `0x40`-`0x5F` (logical
|
||||||
|
channels 4-19), `0xC0`-`0xCF`/`0xE0`-`0xEF` (`'CX'`/`'EX'`), plus
|
||||||
|
`0x80`/`0x84`/`0xA0`/`0xB0`.
|
||||||
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
||||||
collision), and the least-significant bit is `0`.
|
collision). Odd INS codes are accepted only as the defined alternate
|
||||||
|
variants (ISO 7816-4 Table 4 pairs: `'0F'`, `'21'`, `'87'`, `'A1'`,
|
||||||
|
`'B1'`, `'B3'`, `'C3'`, `'CB'`, `'D1'`, `'D7'`, `'DB'`, `'DD'`;
|
||||||
|
TS 102 221 AUTHENTICATE `'89'`). Other odd values stay rejected: a
|
||||||
|
mis-framed boundary exposes data bytes, and the text run `49 4D`
|
||||||
|
("IM") started a bogus frame on xiaomi_phone_sample when every odd INS
|
||||||
|
was accepted.
|
||||||
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
||||||
`0x60` is explicitly invalid: it is the T=0 NULL procedure byte
|
`0x60` is explicitly invalid: it is the T=0 NULL procedure byte
|
||||||
(ISO 7816-3 §10.3.3 Table 11 marks SW1 as `'6X'` **except `'60'`**;
|
(ISO 7816-3 §10.3.3 Table 11 marks SW1 as `'6X'` **except `'60'`**;
|
||||||
@@ -309,6 +332,62 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
|||||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||||
|
|
||||||
|
### v1.9.3: higher logical channels in the CLA gate + defined odd INS codes
|
||||||
|
|
||||||
|
A trace with higher logical channels (4-19) or an odd-INS command could not
|
||||||
|
decode: the acceptance gates knew neither. Both are now spec-derived, with
|
||||||
|
the empirical precision of the mis-frame filter preserved.
|
||||||
|
|
||||||
|
- **Higher logical channels (4-19).** TS 102 221 §10.1.1 Table 10.4a codes
|
||||||
|
them `'01x0 xxxx'` (`0x40`-`0x4F` without SM, `0x60`-`0x6F` with SM in b6)
|
||||||
|
and `'11x0 xxxx'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`, the `'CX'`/`'EX'`
|
||||||
|
classes) -- ISO 7816-4 §5.1.1 Table 3 is the same with b5 = chaining.
|
||||||
|
`plausible_cla()` now accepts the `'CX'`/`'EX'` classes alongside the
|
||||||
|
previous `0x40`-`0x5F`, so extended-channel commands (e.g. STATUS on
|
||||||
|
channel 4 = `0xC0`) and higher channels in the no-SM `'4X'` form decode.
|
||||||
|
Rejected after testing: `0x60`-`0x6F`/`0x70`-`0x7F` (the SM-indicating
|
||||||
|
interindustry classes) and the `'8X'`/`'AX'` channel variants
|
||||||
|
`0x81`-`0x8F`/`0xA1`-`0xAF`. They are spec-coded, but they cost
|
||||||
|
frame-start precision on the mis-framed traces: `0x60`-`0x6F` overlaps
|
||||||
|
printable ASCII, and the menu-text byte `0x61` ('a') started a bogus
|
||||||
|
frame on xiaomi_mi_a1_coldboot whose P3 swallowed the following exchanges
|
||||||
|
(a CONCAT regression); the FCP byte `0xA5` did the same for `'AX'`.
|
||||||
|
Previously such stray bytes were discarded as "suspicious CLA" and the
|
||||||
|
decoder resynced; the wider classes turned them into headers. Add the
|
||||||
|
classes back when a real trace needs them.
|
||||||
|
- **Odd INS.** The gates rejected every odd INS code, so TS 102 221
|
||||||
|
AUTHENTICATE `'89'` (chunked authentication data) and the ISO 7816-4
|
||||||
|
Table 4 alternate variants mis-framed. `plausible_ins()` now accepts
|
||||||
|
exactly the 13 defined odd codes (`ODD_INS`: `0F 21 87 89 A1 B1 B3 C3
|
||||||
|
CB D1 D7 DB DD`) and still rejects the rest: accepting every odd value
|
||||||
|
let the text run `49 4D` ("IM") start a bogus frame on
|
||||||
|
xiaomi_phone_sample. The header check calls the same `plausible_ins()`
|
||||||
|
now (the duplicate inline condition is gone) and the stale ISO `§12.1.1`
|
||||||
|
citation was corrected to `§5.1.2`.
|
||||||
|
- **Idle-noise gate.** The 6+ set-bits discard runs before the CLA check,
|
||||||
|
so it would swallow the legitimately bit-dense extended classes
|
||||||
|
(`0xCF`/`0xEB`-`0xEF`) and kept `0x5F` dead (`plausible_cla()` accepted
|
||||||
|
it, the gate always discarded it). The gate is now
|
||||||
|
`is_idle_noise_byte()`: only bytes no spec allows are idle noise; valid
|
||||||
|
CLAs take the normal header path.
|
||||||
|
- **CONCAT detector unchanged by design.** `EMBEDDED_CLA` no longer
|
||||||
|
derives from `plausible_cla()`; it is pinned to the empirically tuned 36
|
||||||
|
values (`0x00`-`0x1F` + `0x80`/`0x84`/`0xA0`/`0xB0`) in both `pd.py` and
|
||||||
|
`tools/vs_reader.py`, with a membership-pin test -- the CONCAT scan runs
|
||||||
|
across payload bytes where the extended classes false-hit (the Alcor
|
||||||
|
`"40 70 70 61 15"` run).
|
||||||
|
- **Tests.** CLA accept/reject vectors (including the two regressing bytes
|
||||||
|
`0x61`/`0xA5` and the reserved ranges), extended-channel
|
||||||
|
`validate_t0_apdu`/`is_desynced` vectors, the odd-INS vectors, the
|
||||||
|
idle-noise vectors and the EMBEDDED_CLA pin (75 -> 82 tests);
|
||||||
|
`tools/vs_reader.py` mirrors the predicate, `ODD_INS` and the pinned set.
|
||||||
|
|
||||||
|
Validation: all 10 traces identical to the v1.9.2 baseline (APDU counts,
|
||||||
|
CHKSUM/CONCAT/BAD_FCS/suspicious-CLA counts, `RESULT: OK` everywhere);
|
||||||
|
`compare_baseline.sh` reports IDENTICAL. The xiaomi CONCAT regressions the
|
||||||
|
wider sets produced were found by this loop and narrowed away before the
|
||||||
|
baseline was regenerated (version + test listing only). VERSION -> 1.9.3.
|
||||||
|
|
||||||
### v1.9.2: PPS CLK upshift + exact CLK-edge projection (new reader support)
|
### v1.9.2: PPS CLK upshift + exact CLK-edge projection (new reader support)
|
||||||
|
|
||||||
Two timing bugs surfaced by `examples/alcor_reader_sample.sr`, a PC/SC reader
|
Two timing bugs surfaced by `examples/alcor_reader_sample.sr`, a PC/SC reader
|
||||||
@@ -503,8 +582,9 @@ the source**, not just gated. All 10 traces report 0 CONCAT:
|
|||||||
| S21p_coldboot_new_cable_sim2 | 375 | 0 |
|
| S21p_coldboot_new_cable_sim2 | 375 | 0 |
|
||||||
|
|
||||||
1. **One-byte re-frame on invalid INS.** When a 5-byte header window passes
|
1. **One-byte re-frame on invalid INS.** When a 5-byte header window passes
|
||||||
`plausible_cla()` but the INS is unusable (`0x00`, `0x6x`/`0x9x`, or odd
|
`plausible_cla()` but the INS is unusable (`0x00`, `0x6x`/`0x9x`, or an
|
||||||
LSB — ISO 7816-4 §12.1.1), the window is almost always shifted by one
|
odd code outside the defined variants -- see the v1.9.3 note), the
|
||||||
|
window is almost always shifted by one
|
||||||
stray leading byte (e.g. a leftover SW2 read after a PACKETEND). The old
|
stray leading byte (e.g. a leftover SW2 read after a PACKETEND). The old
|
||||||
code silently dropped all 5 bytes plus resync (`_resync = True`), and when
|
code silently dropped all 5 bytes plus resync (`_resync = True`), and when
|
||||||
`resync_idle()` gave up on a busy burst the next byte became a bogus CLA
|
`resync_idle()` gave up on a busy burst the next byte became a bogus CLA
|
||||||
@@ -668,37 +748,51 @@ window may be filtering real start bits.
|
|||||||
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
||||||
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
||||||
|
|
||||||
##### CLA (Class) Byte — Interindustry Structure
|
##### CLA (Class) Byte — Coding and Logical Channels
|
||||||
|
|
||||||
```
|
Accepted (v1.9.3):
|
||||||
Bit: 8 7 6 5 4 3 2 1
|
|
||||||
b8 b7 b6 b5 SM SM ch ch
|
|
||||||
```
|
|
||||||
|
|
||||||
- Bits 8–7: `00` or `01` for interindustry; `1x` reserved for proprietary
|
- `000x xxxx` (`0x00`-`0x1F`): first interindustry — SM in b4b3, chaining in
|
||||||
- Bit 6: `0` for interindustry; `1` for proprietary structural formats
|
b5, logical channel **0–3** in b2b1.
|
||||||
- Bits 4–3: Secure Messaging indicator (`00` = no SM)
|
- `01xx xxxx`: further interindustry — logical channel **4–19**
|
||||||
- Bits 2–1: Logical channel (0–3 for interindustry)
|
(b4..b1 = channel − 4). Accepted: `0x40`-`0x4F` plus the legacy
|
||||||
|
`0x50`-`0x5F` chaining variants (no CLA-level SM).
|
||||||
|
- UICC extended classes `'CX'`/`'EX'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`):
|
||||||
|
channels 4–19 in b4..b1, SM in b6.
|
||||||
|
- Legacy `0x80`/`0x84`/`0xA0`/`0xB0`.
|
||||||
|
|
||||||
Valid interindustry CLA digits: `0x`, `1x`, `4x`, `5x`.
|
Rejected: `0x20`-`0x3F` (ISO-reserved); `0x60`-`0x6F`/`0x70`-`0x7F` (SM in
|
||||||
|
b6 — `0x60`-`0x6F` overlaps printable ASCII, and text bytes started bogus
|
||||||
|
frames on the mis-framed phone traces); the `'8X'`/`'AX'` standard-channel
|
||||||
|
variants `0x81`-`0x8F`/`0xA1`-`0xAF` (channels 0–3; same trade-off);
|
||||||
|
`0x90`-`0x9F`, `0xD0`-`0xDF`, `0xF0`-`0xFF`.
|
||||||
|
|
||||||
Examples: `0x00` (standard), `0x0C` (secure messaging).
|
Examples: `0x00` (standard), `0x01` (channel 1), `0x44` (channel 8),
|
||||||
|
`0x4F` (channel 19), `0xC0` (STATUS on channel 4, `'CX'` class),
|
||||||
|
`0x0C` (secure messaging).
|
||||||
|
|
||||||
##### INS (Instruction) Byte — Valid Ranges
|
##### INS (Instruction) Byte — Valid Ranges
|
||||||
|
|
||||||
- Low nibble cannot be `X0` if high nibble is even
|
- `0x00` is reserved/invalid.
|
||||||
- Low nibble cannot be `XF` under any circumstances
|
- `0x6x`/`0x9x` collide with status words and are invalid.
|
||||||
- `0x6x` and `0x7x` ranges are invalid (reserved for SW1 / protocol bytes)
|
- Odd INS codes are valid only as the defined alternate variants
|
||||||
|
(`0F` ERASE BINARY, `21` VERIFY, `87` GENERAL AUTHENTICATE,
|
||||||
|
`A1` SEARCH BINARY, `B1` READ BINARY, `B3` READ RECORD, `C3` ENVELOPE,
|
||||||
|
`CB` GET DATA, `D1` WRITE BINARY, `D7` UPDATE BINARY, `DB` PUT DATA,
|
||||||
|
`DD` UPDATE RECORD; TS 102 221 AUTHENTICATE `89`).
|
||||||
|
|
||||||
Standard interindustry INS values: `A4` (SELECT), `B0` (READ BINARY),
|
Common INS values: `A4` (SELECT), `B0`/`B1` (READ BINARY),
|
||||||
`D6` (UPDATE BINARY), `B2` (READ RECORD), `E2` (APPEND RECORD),
|
`D6`/`D7` (UPDATE BINARY), `B2`/`B3` (READ RECORD), `E2` (APPEND RECORD),
|
||||||
`20` (VERIFY), `82` (EXTERNAL AUTHENTICATE), `88` (INTERNAL AUTHENTICATE).
|
`20`/`21` (VERIFY), `82` (EXTERNAL AUTHENTICATE), `88`/`89` (AUTHENTICATE),
|
||||||
|
`C2`/`C3` (ENVELOPE: CAT), `C0` (GET RESPONSE).
|
||||||
|
|
||||||
##### Quick Validation Checklist
|
##### Quick Validation Checklist
|
||||||
|
|
||||||
1. CLA high nibble is `0`, `1`, `4`, or `5` (for interindustry)
|
1. CLA is a valid class: `0x00`–`0x1F`/`0x40`–`0x5F` (interindustry, channels
|
||||||
2. INS low nibble is not `F`
|
0–3/4–19) or `0xC0`–`0xCF`/`0xE0`–`0xEF` (UICC extended) or
|
||||||
3. INS is outside `0x6x`–`0x7x` range
|
`0x80`/`0x84`/`0xA0`/`0xB0`
|
||||||
|
2. INS is not `0x00`
|
||||||
|
3. INS is outside the `0x6x`/`0x9x` status-word ranges
|
||||||
|
|
||||||
**Current smoke-test status:**
|
**Current smoke-test status:**
|
||||||
|
|
||||||
@@ -753,7 +847,7 @@ append the measured CLK frequency when known
|
|||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.2'`.
|
The decoder version is defined in `pd.py` as `VERSION = '1.9.3'`.
|
||||||
The version is printed to the log on decoder startup.
|
The version is printed to the log on decoder startup.
|
||||||
|
|
||||||
### Testing after decoder changes
|
### Testing after decoder changes
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ filtered from stderr.
|
|||||||
- Desynced TPDUs (mis-framed exchanges) are kept but flagged with
|
- Desynced TPDUs (mis-framed exchanges) are kept but flagged with
|
||||||
`GSMTAP_FLAG_BAD_FCS` in the GSMTAP header `res` byte — visible in
|
`GSMTAP_FLAG_BAD_FCS` in the GSMTAP header `res` byte — visible in
|
||||||
Wireshark as a BAD-FCS warning.
|
Wireshark as a BAD-FCS warning.
|
||||||
- A T=0 header with a structurally invalid INS (ISO 7816-4 §12.1.1:
|
- A T=0 header with a structurally invalid INS (ISO 7816-4 §5.1.2:
|
||||||
`0x00`, `0x6x`/`0x9x`, or odd LSB) is almost always a one-byte shift from
|
`0x00` or `0x6x`/`0x9x`) is almost always a one-byte shift from
|
||||||
a stray leading byte (e.g. a leftover SW2 read after a PACKETEND).
|
a stray leading byte (e.g. a leftover SW2 read after a PACKETEND).
|
||||||
Instead of silently dropping the whole 5-byte window (which let the next
|
Instead of silently dropping the whole 5-byte window (which let the next
|
||||||
byte become a bogus CLA and swallow the remaining dialog into one CONCAT
|
byte become a bogus CLA and swallow the remaining dialog into one CONCAT
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
|||||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||||
GSMTAP_FLAG_BAD_FCS)
|
GSMTAP_FLAG_BAD_FCS)
|
||||||
|
|
||||||
VERSION = '1.9.2'
|
VERSION = '1.9.3'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -120,37 +120,67 @@ def plausible_sw(sw1):
|
|||||||
|
|
||||||
|
|
||||||
def plausible_cla(cla):
|
def plausible_cla(cla):
|
||||||
'''Check if CLA byte is a structurally valid ISO 7816 class.
|
'''Check if CLA byte is a structurally valid ISO 7816-4 / UICC class.
|
||||||
|
|
||||||
Per ISO 7816-4, the CLA byte is laid out as:
|
Accepted:
|
||||||
|
|
||||||
b8 b7 b6 : interindustry / proprietary class
|
0x00-0x1F first interindustry: logical channels 0-3 in b2b1,
|
||||||
bits 4-3 : secure messaging indicator
|
secure messaging in b4b3, chaining in b5
|
||||||
bits 2-1 : logical channel number (0-3 for interindustry)
|
[ISO 7816-4 §5.1.1 Table 2].
|
||||||
|
0x40-0x5F further interindustry: logical channels 4-19 (b4..b1
|
||||||
|
encode the channel number minus four), with the legacy
|
||||||
|
chaining variants [Table 3; TS 102 221 Table 10.4a].
|
||||||
|
0x80/0x84/0xA0/0xB0 SIM/UICC classes (0xB0 has no pinned-spec
|
||||||
|
basis; kept for backward compatibility).
|
||||||
|
0xC0-0xCF/0xE0-0xEF the UICC extended classes 'CX'/'EX': logical
|
||||||
|
channels 4-19 in b4..b1, secure messaging in b6
|
||||||
|
[TS 102 221 Table 10.4a].
|
||||||
|
|
||||||
Interindustry CLA high nibble is 0x0/0x1/0x4/0x5 (i.e. CLA & 0xF0 in
|
Not accepted, deliberately:
|
||||||
(0x00, 0x10, 0x40, 0x50)); this covers 0x00 (basic), 0x01-0x03
|
|
||||||
(logical channels 1-3, e.g. the command that follows a MANAGE CHANNEL
|
|
||||||
that opened channel 1), 0x0C (secure messaging), chaining variants,
|
|
||||||
and the structural/SM classes 0x4x/0x5x.
|
|
||||||
|
|
||||||
b8 = 1 marks proprietary structural formats; only the classes actually
|
0x20-0x3F reserved for future use by ISO/IEC.
|
||||||
used by SIM/UICC CAT stacks (3GPP TS 102 221 / TS 51.011) are accepted.
|
0x60-0x6F extended channels 4-19 with secure messaging in b6
|
||||||
|
('6X'): the class overlaps printable ASCII ('a'-'o'),
|
||||||
|
and stray menu-text bytes started bogus frames on the
|
||||||
|
mis-framed phone traces (0x61 on xiaomi_mi_a1_coldboot).
|
||||||
|
Extended channels without CLA-level SM ('4X') and the
|
||||||
|
'CX'/'EX' classes are accepted instead.
|
||||||
|
0x70-0x7F chaining + secure messaging (not used by UICC: b5=0).
|
||||||
|
0x81-0x8F/0xA1-0xAF the '8X'/'AX' standard-channel variants
|
||||||
|
(channels 0-3): no trace uses them, and the same
|
||||||
|
precision trade-off applies (the FCP byte 0xA5 on
|
||||||
|
xiaomi_mi_a1_coldboot).
|
||||||
|
0x90-0x9F, 0xD0-0xDF, 0xF0-0xFF not SIM/UICC classes.
|
||||||
|
|
||||||
Anything else means the decoder likely mis-framed the byte stream and
|
Anything else means the decoder likely mis-framed the byte stream and
|
||||||
should attempt to resync.'''
|
should attempt to resync.'''
|
||||||
if not (cla & 0x80):
|
if not (cla & 0x80):
|
||||||
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
||||||
return cla in (0x80, 0x84, 0xA0, 0xB0)
|
return (cla & 0xF0) in (0xC0, 0xE0) or cla in (0x80, 0x84, 0xA0, 0xB0)
|
||||||
|
|
||||||
|
|
||||||
|
# Odd INS codes defined by the pinned specs, i.e. the alternate variants of
|
||||||
|
# ISO 7816-4 Table 4 pairs (READ BINARY 'B1' carries the offset in a data
|
||||||
|
# object, ENVELOPE 'C3', UPDATE RECORD 'DD', ...) plus TS 102 221
|
||||||
|
# AUTHENTICATE '89' (chunked authentication data). Odd values outside this
|
||||||
|
# set stay rejected: no command uses them, and a mis-framed boundary often
|
||||||
|
# lands on a data byte -- the text run "I M" (0x49 0x4D) started a bogus
|
||||||
|
# frame on xiaomi_phone_sample when every odd INS was accepted.
|
||||||
|
ODD_INS = frozenset((
|
||||||
|
0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def plausible_ins(ins):
|
def plausible_ins(ins):
|
||||||
'''Check if INS byte is a structurally valid T=0 instruction.
|
'''Check if INS byte is a structurally valid T=0 instruction.
|
||||||
|
|
||||||
Per ISO 7816-4 §12.1.1 / TS 102 221:
|
Per ISO 7816-4 §5.1.2 / TS 102 221 §10.1.2:
|
||||||
- 0x00 is reserved/invalid.
|
- 0x00 is reserved/invalid.
|
||||||
- MSB nibble 0x6/0x9 collides with status words.
|
- MSB nibble 0x6/0x9 collides with status words.
|
||||||
- LSB must be 0 (standard interindustry commands).
|
- Odd INS codes are accepted only as the defined alternate command
|
||||||
|
variants (Table 4 pairs such as READ BINARY 'B1'; TS 102 221
|
||||||
|
AUTHENTICATE '89').
|
||||||
|
|
||||||
Used together with plausible_cla() to distinguish a real command header
|
Used together with plausible_cla() to distinguish a real command header
|
||||||
from noise while hunting for an ATR.'''
|
from noise while hunting for an ATR.'''
|
||||||
@@ -158,11 +188,23 @@ def plausible_ins(ins):
|
|||||||
return False
|
return False
|
||||||
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
||||||
return False
|
return False
|
||||||
if (ins & 0x01) != 0:
|
if (ins & 0x01) != 0 and ins not in ODD_INS:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_idle_noise_byte(first_byte):
|
||||||
|
'''Check if a byte looks like an idle-line artifact rather than a
|
||||||
|
command class byte.
|
||||||
|
|
||||||
|
Idle-line noise is an all-1s byte with a few dropped bits, so 6 or
|
||||||
|
more set bits. A spec-valid CLA can be bit-dense as well (extended
|
||||||
|
logical channels: 0x5F, 0xCF, 0xEB-0xEF), so plausibility wins over
|
||||||
|
density: such a byte takes the normal CLA/header path instead of the
|
||||||
|
noise resync.'''
|
||||||
|
return bin(first_byte).count('1') >= 6 and not plausible_cla(first_byte)
|
||||||
|
|
||||||
|
|
||||||
def validate_t0_apdu(packet):
|
def validate_t0_apdu(packet):
|
||||||
'''Return (valid, reason) for a reassembled T=0 APDU.
|
'''Return (valid, reason) for a reassembled T=0 APDU.
|
||||||
|
|
||||||
@@ -217,15 +259,15 @@ def is_desynced(packet):
|
|||||||
# data region, followed by its P3 data bytes and an interior status word.
|
# data region, followed by its P3 data bytes and an interior status word.
|
||||||
# These constants bound detector precision (empirically tuned on all 10
|
# These constants bound detector precision (empirically tuned on all 10
|
||||||
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
||||||
# A swallowed header's CLA is a real command class byte: interindustry
|
# This CLA set is deliberately NARROWER than plausible_cla(): the CONCAT
|
||||||
# classes including the logical-channel numbers 0x01-0x03. The structural
|
# scan runs across every payload byte, where the extended-channel classes
|
||||||
# 0x4x/0x5x forms that plausible_cla() tolerates for resync robustness are
|
# (0x40-0x5F, 0xC0-0xEF) false-hit -- the Alcor secured-packet payload
|
||||||
# excluded here -- no CAT traffic uses them, and accepting them makes payload
|
# "40 70 70 61 15" + an SW-like byte 21 later. The command classes that
|
||||||
# bytes false-hit (Alcor secured-packet payload: "40 70 70 61 15" + an
|
# really occur on these wires stay covered: interindustry 0x00-0x1F
|
||||||
# SW-like byte 21 later).
|
# (including logical channels 1-3) and the SIM/UICC classes
|
||||||
|
# 0x80/0x84/0xA0/0xB0.
|
||||||
EMBEDDED_CLA = frozenset(
|
EMBEDDED_CLA = frozenset(
|
||||||
cla for cla in range(256)
|
list(range(0x00, 0x20)) + [0x80, 0x84, 0xA0, 0xB0])
|
||||||
if plausible_cla(cla) and (cla & 0xF0) not in (0x40, 0x50))
|
|
||||||
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
||||||
EMBEDDED_INS = frozenset((
|
EMBEDDED_INS = frozenset((
|
||||||
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
||||||
@@ -2255,19 +2297,22 @@ class Decoder(srd.Decoder):
|
|||||||
elif (firstByte == 0x3b) and not self._atr_parsed: # Probably ATR (only if no ATR yet)
|
elif (firstByte == 0x3b) and not self._atr_parsed: # Probably ATR (only if no ATR yet)
|
||||||
self.handle_atr(self.wait({'skip': 0}))
|
self.handle_atr(self.wait({'skip': 0}))
|
||||||
return True
|
return True
|
||||||
elif bin(firstByte).count('1') >= 6:
|
elif is_idle_noise_byte(firstByte):
|
||||||
# Idle-line noise: byte has 6+ bits set (all-1s with
|
# Idle-line noise: an all-1s byte with a few dropped bits.
|
||||||
# 0-2 bit errors). Re-sync instead of mis-parsing as
|
# Re-sync instead of mis-parsing as a T=0 header (which
|
||||||
# a T=0 header (which would pack idle noise into
|
# would pack idle noise into garbage APDUs). Spec-valid
|
||||||
# garbage APDUs).
|
# high-density CLAs (0x5F/0xCF/0xEB-0xEF: extended logical
|
||||||
|
# channels) are not noise and fall through to the
|
||||||
|
# CLA/header path below.
|
||||||
self._note_discard(firstByte, 'idle-line noise (6+ bits set)', idle=True)
|
self._note_discard(firstByte, 'idle-line noise (6+ bits set)', idle=True)
|
||||||
self._consume_first()
|
self._consume_first()
|
||||||
self._resync = True
|
self._resync = True
|
||||||
return True
|
return True
|
||||||
elif not plausible_cla(firstByte):
|
elif not plausible_cla(firstByte):
|
||||||
# Non-standard CLA: byte is not a recognized ISO 7816
|
# CLA byte is not a recognized class (no ISO 7816-4 /
|
||||||
# class. Likely mis-framed data — attempt to resync.
|
# TS 102 221 coding). Likely mis-framed data — attempt
|
||||||
self._note_discard(firstByte, 'suspicious CLA (not ISO 7816-4)')
|
# to resync.
|
||||||
|
self._note_discard(firstByte, 'suspicious CLA (not a recognized class)')
|
||||||
self._consume_first()
|
self._consume_first()
|
||||||
self.log("Suspicious CLA 0x{:02x}, attempting resync".format(firstByte))
|
self.log("Suspicious CLA 0x{:02x}, attempting resync".format(firstByte))
|
||||||
self._resync = True
|
self._resync = True
|
||||||
@@ -2319,15 +2364,12 @@ class Decoder(srd.Decoder):
|
|||||||
["all-zero T=0 header (stuck low?)"]])
|
["all-zero T=0 header (stuck low?)"]])
|
||||||
self._resync = True
|
self._resync = True
|
||||||
return True
|
return True
|
||||||
# T=0 header validation per ISO 7816-4 §12.1.1.
|
# T=0 header validation per ISO 7816-4 §5.1.2: the same
|
||||||
# Rejects noise headers that slip past the CLA filter.
|
# INS rule as plausible_ins() (0x00 and 0x6x/0x9x are
|
||||||
# - INS=0x00 is reserved/invalid.
|
# invalid; odd codes only as the defined alternate
|
||||||
# - MSB nibble 0x6/0x9 conflicts with status words.
|
# variants). A window that fails it is almost always
|
||||||
# - LSB must be 0 (all standard interindustry commands).
|
# shifted by one stray leading byte.
|
||||||
if (bIns == 0x00
|
if not plausible_ins(bIns):
|
||||||
or (bIns & 0xF0) == 0x60
|
|
||||||
or (bIns & 0xF0) == 0x90
|
|
||||||
or (bIns & 0x01) != 0):
|
|
||||||
# Byte-slip re-frame: the whole 5-byte window is a valid
|
# Byte-slip re-frame: the whole 5-byte window is a valid
|
||||||
# size but the INS is unusable, so the window is
|
# size but the INS is unusable, so the window is
|
||||||
# probably shifted by one (a stray leading byte from the
|
# probably shifted by one (a stray leading byte from the
|
||||||
|
|||||||
+21
-14
@@ -1,7 +1,7 @@
|
|||||||
=== Baseline captured 2026-09-27T23:45:58+03:00 ===
|
=== Baseline captured 2026-10-17T23:18:56+03:00 ===
|
||||||
|
|
||||||
=== test_8 ===
|
=== test_8 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('etu recovered (samples):', 1237)
|
('etu recovered (samples):', 1237)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== test_7 ===
|
=== test_7 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('etu recovered (samples):', 1236)
|
('etu recovered (samples):', 1236)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_phone_sample ===
|
=== xiaomi_phone_sample ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('etu recovered (samples):', 53)
|
('etu recovered (samples):', 53)
|
||||||
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_mi_a1_coldboot ===
|
=== xiaomi_mi_a1_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== 4gmodem_coldboot ===
|
=== 4gmodem_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== alcor_reader_sample ===
|
=== alcor_reader_sample ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable ===
|
=== samsung_A55_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable ===
|
=== samsung_S21p_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.3 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -354,9 +354,11 @@ test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_
|
|||||||
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
||||||
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... ok
|
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... ok
|
||||||
test_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pulse_start_edge_preserved) ... ok
|
test_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pulse_start_edge_preserved) ... ok
|
||||||
|
test_idle_noise_byte (__main__.TestDesyncHelpers.test_idle_noise_byte) ... ok
|
||||||
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
||||||
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
||||||
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
||||||
|
test_is_desynced_extended_channel_valid (__main__.TestDesyncHelpers.test_is_desynced_extended_channel_valid) ... ok
|
||||||
test_is_desynced_null_sw1_is_invalid (__main__.TestDesyncHelpers.test_is_desynced_null_sw1_is_invalid) ... ok
|
test_is_desynced_null_sw1_is_invalid (__main__.TestDesyncHelpers.test_is_desynced_null_sw1_is_invalid) ... ok
|
||||||
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... ok
|
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... ok
|
||||||
test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
||||||
@@ -374,6 +376,7 @@ test_clean_secured_packet_payload_alcor (__main__.TestEmbeddedExchanges.test_cle
|
|||||||
test_clean_select_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok
|
test_clean_select_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok
|
||||||
test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... ok
|
test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... ok
|
||||||
test_concat_packets_are_not_is_desynced (__main__.TestEmbeddedExchanges.test_concat_packets_are_not_is_desynced) ... ok
|
test_concat_packets_are_not_is_desynced (__main__.TestEmbeddedExchanges.test_concat_packets_are_not_is_desynced) ... ok
|
||||||
|
test_embedded_cla_membership_pinned (__main__.TestEmbeddedExchanges.test_embedded_cla_membership_pinned) ... ok
|
||||||
test_samsung_record_burst_concat (__main__.TestEmbeddedExchanges.test_samsung_record_burst_concat) ... ok
|
test_samsung_record_burst_concat (__main__.TestEmbeddedExchanges.test_samsung_record_burst_concat) ... ok
|
||||||
test_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
test_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
||||||
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
||||||
@@ -400,8 +403,9 @@ test_repeated_same_sample_is_bounded_stall (__main__.TestStallCheck.test_repeate
|
|||||||
test_untracked_level_wait_never_stalls (__main__.TestStallCheck.test_untracked_level_wait_never_stalls) ... ok
|
test_untracked_level_wait_never_stalls (__main__.TestStallCheck.test_untracked_level_wait_never_stalls) ... ok
|
||||||
test_untracked_resets_stall_window (__main__.TestStallCheck.test_untracked_resets_stall_window) ... ok
|
test_untracked_resets_stall_window (__main__.TestStallCheck.test_untracked_resets_stall_window) ... ok
|
||||||
test_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
test_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
||||||
test_invalid_cla_proprietary_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_proprietary_unseen) ... ok
|
test_invalid_cla_reserved_and_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_reserved_and_unseen) ... ok
|
||||||
test_invalid_ins_lsb_odd (__main__.TestValidateT0Apdu.test_invalid_ins_lsb_odd) ... ok
|
test_invalid_cla_untraced_variants (__main__.TestValidateT0Apdu.test_invalid_cla_untraced_variants) ... ok
|
||||||
|
test_invalid_ins_odd_undefined (__main__.TestValidateT0Apdu.test_invalid_ins_odd_undefined) ... ok
|
||||||
test_invalid_ins_sw_collision (__main__.TestValidateT0Apdu.test_invalid_ins_sw_collision) ... ok
|
test_invalid_ins_sw_collision (__main__.TestValidateT0Apdu.test_invalid_ins_sw_collision) ... ok
|
||||||
test_invalid_ins_zero (__main__.TestValidateT0Apdu.test_invalid_ins_zero) ... ok
|
test_invalid_ins_zero (__main__.TestValidateT0Apdu.test_invalid_ins_zero) ... ok
|
||||||
test_invalid_oversized (__main__.TestValidateT0Apdu.test_invalid_oversized) ... ok
|
test_invalid_oversized (__main__.TestValidateT0Apdu.test_invalid_oversized) ... ok
|
||||||
@@ -411,10 +415,13 @@ test_valid_case1_direct_sw (__main__.TestValidateT0Apdu.test_valid_case1_direct_
|
|||||||
test_valid_case2_with_response (__main__.TestValidateT0Apdu.test_valid_case2_with_response) ... ok
|
test_valid_case2_with_response (__main__.TestValidateT0Apdu.test_valid_case2_with_response) ... ok
|
||||||
test_valid_case3_with_command_data (__main__.TestValidateT0Apdu.test_valid_case3_with_command_data) ... ok
|
test_valid_case3_with_command_data (__main__.TestValidateT0Apdu.test_valid_case3_with_command_data) ... ok
|
||||||
test_valid_cla_channel1_select_after_manage_channel (__main__.TestValidateT0Apdu.test_valid_cla_channel1_select_after_manage_channel) ... ok
|
test_valid_cla_channel1_select_after_manage_channel (__main__.TestValidateT0Apdu.test_valid_cla_channel1_select_after_manage_channel) ... ok
|
||||||
|
test_valid_cla_extended_channel_exchanges (__main__.TestValidateT0Apdu.test_valid_cla_extended_channel_exchanges) ... ok
|
||||||
test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla_interindustry_classes) ... ok
|
test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla_interindustry_classes) ... ok
|
||||||
|
test_valid_cla_uicc_classes (__main__.TestValidateT0Apdu.test_valid_cla_uicc_classes) ... ok
|
||||||
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
||||||
|
test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_variants) ... ok
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 75 tests in 0.024s
|
Ran 82 tests in 0.023s
|
||||||
|
|
||||||
OK
|
OK
|
||||||
+112
-11
@@ -98,6 +98,32 @@ class TestDesyncHelpers(unittest.TestCase):
|
|||||||
self.assertFalse(self.pd.is_desynced(
|
self.assertFalse(self.pd.is_desynced(
|
||||||
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
||||||
|
|
||||||
|
def test_is_desynced_extended_channel_valid(self):
|
||||||
|
# v1.9.3: higher logical channels and the UICC extended classes
|
||||||
|
# are clean traffic, not desync. 0x44 = SELECT on channel 8,
|
||||||
|
# 0x4F = SELECT on channel 19, 0xC0 = STATUS on channel 4
|
||||||
|
# ('CX'), 0xE2 = READ BINARY on channel 6 with SM ('EX').
|
||||||
|
for h in ('44a40004009000', '4fa40004009000',
|
||||||
|
'c0f20000009000', 'e2b000000212349000'):
|
||||||
|
with self.subTest(h=h):
|
||||||
|
self.assertFalse(self.pd.is_desynced(bytes.fromhex(h)))
|
||||||
|
|
||||||
|
def test_idle_noise_byte(self):
|
||||||
|
# All-1s artifacts with dropped bits are idle noise (including
|
||||||
|
# 0x6F/0x7F/0xAF, which are not valid classes) ...
|
||||||
|
for b in (0x6f, 0x7f, 0xaf, 0xbf, 0xf7, 0xfc, 0xfd, 0xfe):
|
||||||
|
with self.subTest(b=b):
|
||||||
|
self.assertTrue(self.pd.is_idle_noise_byte(b))
|
||||||
|
# ... but spec-valid, bit-dense CLAs are command bytes: extended
|
||||||
|
# logical channels 0x5F/0xCF/0xEB-0xEF must not be swallowed by
|
||||||
|
# the noise resync.
|
||||||
|
for b in (0x5f, 0xcf, 0xeb, 0xee, 0xef):
|
||||||
|
with self.subTest(b=b):
|
||||||
|
self.assertFalse(self.pd.is_idle_noise_byte(b))
|
||||||
|
# A sparse invalid class is not "idle noise" either (it takes the
|
||||||
|
# suspicious-CLA path).
|
||||||
|
self.assertFalse(self.pd.is_idle_noise_byte(0x20))
|
||||||
|
|
||||||
def test_is_desynced_null_sw1_is_invalid(self):
|
def test_is_desynced_null_sw1_is_invalid(self):
|
||||||
# Regression (v1.9.1): the T=0 NULL byte 0x60 is not SW1. The
|
# Regression (v1.9.1): the T=0 NULL byte 0x60 is not SW1. The
|
||||||
# wire "NULL 0x60 + real SW1 0x61" was mis-framed as the status
|
# wire "NULL 0x60 + real SW1 0x61" was mis-framed as the status
|
||||||
@@ -158,14 +184,39 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
self._valid(b'\x00\xd6\x00\x00\x04\x11\x22\x33\x44\x90\x00')
|
self._valid(b'\x00\xd6\x00\x00\x04\x11\x22\x33\x44\x90\x00')
|
||||||
|
|
||||||
def test_valid_cla_interindustry_classes(self):
|
def test_valid_cla_interindustry_classes(self):
|
||||||
# Interindustry CLA digits 0x/1x/4x/5x are structurally valid
|
# Interindustry CLA (ISO 7816-4 §5.1.1): 000x xxxx 0x00-0x1F
|
||||||
# (ISO 7816-4): includes logical channels 0x01-0x03, secure
|
# (channels 0-3 in b2b1, SM 0x0C, chaining 0x10-0x1F) and
|
||||||
# messaging 0x0C, chaining 0x10, and the SM/structural 0x4x/0x5x.
|
# 01xx xxxx 0x40-0x5F (channels 4-19 without CLA-level SM:
|
||||||
for cla in (0x01, 0x02, 0x03, 0x0C, 0x10, 0x14, 0x44, 0x50, 0x54):
|
# b4..b1 = channel-4). 0x20-0x3F is reserved by ISO; the
|
||||||
|
# SM-indicating 0x60-0x6F/0x70-0x7F stay rejected -- see
|
||||||
|
# test_invalid_cla_untraced_variants.
|
||||||
|
for cla in (0x01, 0x02, 0x03, 0x0C, 0x10, 0x14,
|
||||||
|
0x40, 0x44, 0x4F, 0x50, 0x54):
|
||||||
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
with self.subTest(cla=cla):
|
with self.subTest(cla=cla):
|
||||||
self._valid(pkt)
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_valid_cla_uicc_classes(self):
|
||||||
|
# UICC extended classes (TS 102 221 §10.1.1 Table 10.4a): 'CX'/'EX'
|
||||||
|
# (0xC0-0xCF/0xE0-0xEF) carry the extended channels 4-19 in b4..b1
|
||||||
|
# (SM in b6). The legacy SIM/UICC values and 0xB0 (no pinned-spec
|
||||||
|
# basis, kept for compatibility) are unchanged.
|
||||||
|
for cla in (0x80, 0x84, 0xA0, 0xB0,
|
||||||
|
0xC0, 0xC4, 0xCF, 0xE0, 0xE4, 0xEF):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_valid_cla_extended_channel_exchanges(self):
|
||||||
|
# Real higher-channel shapes from TS 102 221 Tables 10.4a/10.5:
|
||||||
|
# SELECT on channel 8 ('4X'), SELECT on channel 19 ('4X'),
|
||||||
|
# STATUS on channel 4 ('CX'), READ BINARY on channel 6 with SM
|
||||||
|
# ('EX' -- the UICC extended class accepts b6 = SM).
|
||||||
|
self._valid(b'\x44\xa4\x00\x04\x00\x90\x00')
|
||||||
|
self._valid(b'\x4f\xa4\x00\x04\x00\x90\x00')
|
||||||
|
self._valid(b'\xc0\xf2\x00\x00\x00\x90\x00')
|
||||||
|
self._valid(b'\xe2\xb0\x00\x00\x02\x12\x34\x90\x00')
|
||||||
|
|
||||||
def test_valid_cla_channel1_select_after_manage_channel(self):
|
def test_valid_cla_channel1_select_after_manage_channel(self):
|
||||||
# The command that follows MANAGE CHANNEL (00 70 00 00 01) opening
|
# The command that follows MANAGE CHANNEL (00 70 00 00 01) opening
|
||||||
# logical channel 1 legitimately uses CLA 0x01. This exact type
|
# logical channel 1 legitimately uses CLA 0x01. This exact type
|
||||||
@@ -174,9 +225,28 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
b'\xa0\x00\x00\x00\x63\x50\x4b\x43\x53\x2d\x31\x35'
|
b'\xa0\x00\x00\x00\x63\x50\x4b\x43\x53\x2d\x31\x35'
|
||||||
b'\x6a\x82')
|
b'\x6a\x82')
|
||||||
|
|
||||||
def test_invalid_cla_proprietary_unseen(self):
|
def test_invalid_cla_reserved_and_unseen(self):
|
||||||
# 0xC0 is proprietary but not a class these cards emit; stays invalid
|
# 0x20-0x3F (001x xxxx) is reserved by ISO 7816-4; 0x90-0x9F,
|
||||||
self._invalid(b'\xc0\xa4\x00\x04\x00\x90\x00', 'invalid CLA')
|
# 0xD0-0xDF and 0xF0-0xFF are not SIM/UICC classes ('11x0' needs
|
||||||
|
# b5 = 0). All stay invalid.
|
||||||
|
for cla in (0x20, 0x2F, 0x3F, 0x90, 0x9F, 0xD0, 0xDF, 0xF0, 0xFE):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._invalid(pkt, 'invalid CLA')
|
||||||
|
|
||||||
|
def test_invalid_cla_untraced_variants(self):
|
||||||
|
# Accepted-set precision (v1.9.3): the higher-channel codings that
|
||||||
|
# cost frame-start precision stay rejected. 0x60-0x6F/0x70-0x7F
|
||||||
|
# are the SM-indicating extended classes -- 0x60-0x6F overlaps
|
||||||
|
# printable ASCII ('a'-'o'), and the text byte 0x61 started a bogus
|
||||||
|
# frame on xiaomi_mi_a1_coldboot; 0x81-0x8F/0xA1-0xAF are the
|
||||||
|
# '8X'/'AX' standard-channel variants (channels 0-3), where the
|
||||||
|
# FCP byte 0xA5 did the same.
|
||||||
|
for cla in (0x60, 0x61, 0x6F, 0x70, 0x73, 0x7F,
|
||||||
|
0x81, 0x85, 0x8F, 0xA1, 0xA5, 0xAF):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._invalid(pkt, 'invalid CLA')
|
||||||
|
|
||||||
def test_invalid_too_short(self):
|
def test_invalid_too_short(self):
|
||||||
self._invalid(b'\x00\xa4\x00\x04\x00\x90', 'too short')
|
self._invalid(b'\x00\xa4\x00\x04\x00\x90', 'too short')
|
||||||
@@ -187,11 +257,31 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
def test_invalid_ins_zero(self):
|
def test_invalid_ins_zero(self):
|
||||||
self._invalid(b'\x00\x00\x00\x04\x00\x90\x00', 'invalid INS')
|
self._invalid(b'\x00\x00\x00\x04\x00\x90\x00', 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_ins_lsb_odd(self):
|
def test_valid_ins_odd_variants(self):
|
||||||
self._invalid(b'\x00\xa5\x00\x04\x00\x90\x00', 'invalid INS')
|
# Odd INS codes are accepted as the defined alternate variants:
|
||||||
|
# ISO 7816-4 Table 4 pairs (READ BINARY 'B1', ENVELOPE 'C3',
|
||||||
|
# UPDATE RECORD 'DD', ...) and TS 102 221 AUTHENTICATE '89'.
|
||||||
|
for ins in (0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xD1, 0xD7, 0xDB, 0xDD):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x00, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_invalid_ins_odd_undefined(self):
|
||||||
|
# Odd INS values with no defined command stay rejected: a
|
||||||
|
# mis-framed boundary exposes data bytes, and the text run
|
||||||
|
# "I M" (0x49 0x4D) started a bogus frame on xiaomi_phone_sample
|
||||||
|
# when every odd INS was accepted.
|
||||||
|
for ins in (0x03, 0x4D, 0x4F, 0xA5, 0xD3):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x00, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._invalid(pkt, 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_ins_sw_collision(self):
|
def test_invalid_ins_sw_collision(self):
|
||||||
self._invalid(b'\x00\x64\x00\x04\x00\x90\x00', 'invalid INS')
|
for ins in (0x64, 0x9F):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._invalid(pkt, 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_sw1(self):
|
def test_invalid_sw1(self):
|
||||||
self._invalid(b'\x00\xa4\x00\x04\x00\xff\x00', 'invalid SW1')
|
self._invalid(b'\x00\xa4\x00\x04\x00\xff\x00', 'invalid SW1')
|
||||||
@@ -406,13 +496,24 @@ class TestEmbeddedExchanges(unittest.TestCase):
|
|||||||
# alcor_reader[46] (n=87): GET RESPONSE whose 80-byte payload holds a
|
# alcor_reader[46] (n=87): GET RESPONSE whose 80-byte payload holds a
|
||||||
# secured-packet run "40 70 70 61 15" 21 bytes before an SW-like byte;
|
# secured-packet run "40 70 70 61 15" 21 bytes before an SW-like byte;
|
||||||
# the reader follows with 00 c0 00 00 19 -- a clean single exchange.
|
# the reader follows with 00 c0 00 00 19 -- a clean single exchange.
|
||||||
# The structural 0x4x CLA must not turn payload bytes into a hit.
|
# The extended-channel 0x4x CLA must not turn payload bytes into a
|
||||||
|
# hit: the CONCAT detector is deliberately blind to those classes.
|
||||||
h = ('80c2000050d14e82028381060291978b4440048111227ff64070706115'
|
h = ('80c2000050d14e82028381060291978b4440048111227ff64070706115'
|
||||||
'3500350270000030151601252500000018ed15e8476126aa9cacbbe8f4'
|
'3500350270000030151601252500000018ed15e8476126aa9cacbbe8f4'
|
||||||
'735f962016e3c6e30a26af20c02bad2a34bdf49f152e263bc7390a6119')
|
'735f962016e3c6e30a26af20c02bad2a34bdf49f152e263bc7390a6119')
|
||||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||||
self.assertEqual(self._hits(h), [])
|
self.assertEqual(self._hits(h), [])
|
||||||
|
|
||||||
|
def test_embedded_cla_membership_pinned(self):
|
||||||
|
# CONCAT precision (v1.9.3): the detector's CLA set is deliberately
|
||||||
|
# NARROWER than plausible_cla() -- it must not follow the widened
|
||||||
|
# acceptance into the extended-channel classes (0x40-0x7F,
|
||||||
|
# 0xC0-0xEF) that false-hit on payload bytes (the Alcor vector
|
||||||
|
# above). Pin the exact membership.
|
||||||
|
expected = frozenset(list(range(0x00, 0x20)) +
|
||||||
|
[0x80, 0x84, 0xA0, 0xB0])
|
||||||
|
self.assertEqual(self.pd.EMBEDDED_CLA, expected)
|
||||||
|
|
||||||
def test_a55_u1_concat(self):
|
def test_a55_u1_concat(self):
|
||||||
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
|
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
|
||||||
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
|
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
|
||||||
|
|||||||
+29
-9
@@ -44,14 +44,18 @@ CAT_INS = {
|
|||||||
# GSMTAP_FLAG_BAD_FCS from gsmtap_stream.py
|
# GSMTAP_FLAG_BAD_FCS from gsmtap_stream.py
|
||||||
GSMTAP_FLAG_BAD_FCS = 0x01
|
GSMTAP_FLAG_BAD_FCS = 0x01
|
||||||
|
|
||||||
# Valid CLA values for ISO 7816 / 3GPP. Mirror of plausible_cla in pd.py:
|
# Valid CLA values for ISO 7816-4 / TS 102 221. Mirror of plausible_cla in
|
||||||
# interindustry structures (high nibble 0x0/0x1/0x4/0x5), which covers the
|
# pd.py: interindustry 0x00-0x1F (channels 0-3) and 0x40-0x5F (channels
|
||||||
# logical-channel classes 0x01-0x03 (e.g. the command after MANAGE CHANNEL
|
# 4-19 without CLA-level SM), the UICC extended classes 'CX'/'EX'
|
||||||
# opens channel 1), plus the proprietary SIM/UICC CAT classes.
|
# (0xC0-0xCF/0xE0-0xEF, channels 4-19), plus 0x80/0x84/0xA0/0xB0 (0xB0 has
|
||||||
|
# no pinned-spec basis; kept for backward compatibility). 0x60-0x6F
|
||||||
|
# ('6X': SM; overlaps printable ASCII 'a'-'o'), 0x70-0x7F and the '8X'/'AX'
|
||||||
|
# standard-channel variants stay rejected: stray data bytes with those
|
||||||
|
# classes started bogus frames on the mis-framed phone traces.
|
||||||
def _plausible_cla(cla):
|
def _plausible_cla(cla):
|
||||||
if not (cla & 0x80):
|
if not (cla & 0x80):
|
||||||
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
||||||
return cla in (0x80, 0x84, 0xA0, 0xB0)
|
return (cla & 0xF0) in (0xC0, 0xE0) or cla in (0x80, 0x84, 0xA0, 0xB0)
|
||||||
|
|
||||||
|
|
||||||
VALID_CLA = frozenset(cla for cla in range(256) if _plausible_cla(cla))
|
VALID_CLA = frozenset(cla for cla in range(256) if _plausible_cla(cla))
|
||||||
@@ -65,10 +69,12 @@ MAX_TPDU_LEN = 271
|
|||||||
# data region, followed by its P3 data bytes and an interior status word.
|
# data region, followed by its P3 data bytes and an interior status word.
|
||||||
# These constants bound detector precision (empirically tuned on all 10
|
# These constants bound detector precision (empirically tuned on all 10
|
||||||
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
||||||
# Mirror of pd.py: real command classes only -- the structural 0x4x/0x5x
|
# Pinned in both pd.py and here -- deliberately narrower than VALID_CLA:
|
||||||
# forms that _plausible_cla() tolerates only add payload false hits.
|
# the extended-channel classes false-hit on payload bytes (the Alcor
|
||||||
|
# "40 70 70 61 15" secured-packet run). Only the command classes that
|
||||||
|
# really occur on these wires stay covered.
|
||||||
EMBEDDED_CLA = frozenset(
|
EMBEDDED_CLA = frozenset(
|
||||||
cla for cla in VALID_CLA if (cla & 0xF0) not in (0x40, 0x50))
|
list(range(0x00, 0x20)) + [0x80, 0x84, 0xA0, 0xB0])
|
||||||
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
||||||
EMBEDDED_INS = frozenset((
|
EMBEDDED_INS = frozenset((
|
||||||
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
||||||
@@ -134,12 +140,26 @@ def _plausible_sw(sw1):
|
|||||||
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||||
|
|
||||||
|
|
||||||
|
# Odd INS codes defined by the pinned specs: the alternate variants of the
|
||||||
|
# ISO 7816-4 Table 4 pairs plus TS 102 221 AUTHENTICATE '89'. Other odd
|
||||||
|
# values stay rejected (no command uses them; mis-framed boundary data
|
||||||
|
# bytes with odd INS started bogus frames on the phone traces).
|
||||||
|
ODD_INS = frozenset((
|
||||||
|
0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def _plausible_ins(ins):
|
def _plausible_ins(ins):
|
||||||
|
# Mirror of plausible_ins in pd.py: 0x00 is invalid and 0x6x/0x9x
|
||||||
|
# collide with status words; odd INS values are accepted only as the
|
||||||
|
# defined alternate command variants (READ BINARY 'B1', AUTHENTICATE
|
||||||
|
# '89', ...).
|
||||||
if ins == 0x00:
|
if ins == 0x00:
|
||||||
return False
|
return False
|
||||||
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
||||||
return False
|
return False
|
||||||
if (ins & 0x01) != 0:
|
if (ins & 0x01) != 0 and ins not in ODD_INS:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user