docs: roadmap index + custom-14a-command plans + backfill plan docs

- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs.
- Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C
  SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay),
  produced from a multi-agent design workflow.
- Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode,
  live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md.
- Sync CLAUDE.md package structure with the committed transponder models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 12:58:48 -07:00
parent a24f0f803f
commit 615d4a6115
12 changed files with 4673 additions and 0 deletions

780
docs/NTAG5_SECURITY.md Normal file
View File

@@ -0,0 +1,780 @@
# NTAG5 Link (NTP5332) Security Configuration Reference
This document captures key technical details for implementing ISO 29167-10 AES-128 authentication on NTAG5 Link chips.
## Configuration Memory Map
### Device Security Configuration (DEV_SEC_CONFIG)
**Address:** Block 0x3F (NFC) / 0x103F (I2C)
| Byte | Name | Description |
| ---- | --------------- | --------------------------- |
| 0 | DEV_SEC_CONFIG | Security mode and lock bits |
| 1 | SRAM_CONF_PROT | SRAM and config protection |
| 2 | PP_AREA_1 (LSB) | Protection pointer area 1 |
| 3 | PP_AREA_1 (MSB) | Protection pointer area 1 |
**DEV_SEC_CONFIG Bit Definition:**
| Bits | Name | Value | Description |
| ---- | ------------- | ----- | ------------------------ |
| 7-5 | Security Lock | 010b | Locked (cannot modify) |
| | | 101b | Writable (default) |
| 4 | RFU | 00b | Reserved |
| 3-0 | NFC_SEC_MODE | 0010b | AES mode (NTP5332 only) |
| | | 0101b | Plain password (default) |
**Example Values:**
- `0xA5` = Writable + Plain password mode (factory default)
- `0xA2` = Writable + AES mode
### NFC Global Crypto Header (NFC_GCH)
**Address:** Block 0x0C (NFC) / 0x100C (I2C), **Byte 1** (other bytes are RFU)
**What it controls:** NFC_GCH defines the status/access enforcement for passwords (in password mode), keys, protection pointer & conditions, key headers, key privileges, crypto configuration header, and EAS/AFI protection.
**IMPORTANT:** NFC_GCH is **one-way programmable** (lower → higher values only) and **irreversible**.
**Allowed Values (AES Mode):** All other values are invalid.
| Value | Status | Description |
| ----- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0x81 | Deactivated (default) | Protection pointer/conditions not activated; user memory R/W possible without prior mutual auth; PP/PPC can be modified via PROTECT PAGE; keys & privileges are readable/writable via READ/WRITE CONFIG according to each key's KH status; EAS/AFI protection not activated |
| 0x87 | Deactivated & privileges locked | Same as 0x81, but Key Privileges are locked (cannot be modified). EAS/AFI still not protected |
| 0xC1 | Access right activated | Protection pointer/conditions enabled; user-memory R/W protection enforced per configuration; keys/privileges still readable/writable according to KH status; EAS/AFI protection enabled |
| 0xC7 | Access right activated & privileges locked | Same as 0xC1, but Key Privileges are locked |
| 0xE7 | Activated (final) | Protection enabled; all Key Headers/Privileges/Keys are locked (cannot be modified); EAS/AFI protection enabled |
**Allowed Values (Plain Password Mode):**
| Value | Status | Description |
| ----- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| 0x81 | Writable (default) | Passwords readable/writable via READ/WRITE CONFIG |
| 0xE7 | Locked | Passwords not readable/writable via READ/WRITE CONFIG (note: separate LOCK PASSWORD needed to lock passwords permanently) |
### NFC Crypto Configuration Header (NFC_CCH)
**Address:** Block 0x0D (NFC) / 0x100D (I2C), **Byte 1** (other bytes are RFU)
**What it controls:** NFC_CCH controls whether the NFC Authentication Limit can be changed freely or only after authentication. This is the "global knob" for Authentication Limit behavior (not general access-right enforcement).
**Allowed Values:** All other values are invalid.
| Value | Status | Description |
| ----- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| 0x81 | Unlocked (default) | Authentication limit can be modified freely |
| 0xE7 | Locked | Authentication limit can only be modified after mutual auth with a key that has the Crypto Config privilege (bit 5 of NFC_KPx) |
### NFC Key Headers (NFC_KHx)
**Addresses:** Key headers live in blocks 0x10/0x12/0x14/0x16, specifically in **Byte 1** of each block (other bytes are RFU).
| Key | Block (NFC) | Block (I2C) | Byte Position |
| ------- | ----------- | ----------- | ------------- |
| NFC_KH0 | 0x10 | 0x1010 | Byte 1 |
| NFC_KH1 | 0x12 | 0x1012 | Byte 1 |
| NFC_KH2 | 0x14 | 0x1014 | Byte 1 |
| NFC_KH3 | 0x16 | 0x1016 | Byte 1 |
**Key Header Values:**
| Value | Status | Description |
| ----- | -------------------- | ------------------------------------------------------------- |
| 0x81 | Not active (default) | Key can be read/written but CANNOT be used for authentication |
| 0xE7 | Active and locked | Key is active and locked - can be used for authentication |
| 0xFF | Disabled | Key slot is disabled (NXP recommends disabling unused keys) |
**IMPORTANT:**
- Keys with header 0x81 cannot be used for authentication
- The header must be set to 0xE7 to activate the key
- Once 0xE7 is set, the key value cannot be modified
- Key headers are **one-way** (lower→higher values only) and **irreversible**
### NFC Key Privileges (NFC_KPx)
**Addresses:** Key privileges live in blocks 0x11/0x13/0x15/0x17, specifically in **Byte 0** of each block (other bytes are RFU).
| Key | Block (NFC) | Block (I2C) | Byte Position |
| ------- | ----------- | ----------- | ------------- |
| NFC_KP0 | 0x11 | 0x1011 | Byte 0 |
| NFC_KP1 | 0x13 | 0x1013 | Byte 0 |
| NFC_KP2 | 0x15 | 0x1015 | Byte 0 |
| NFC_KP3 | 0x17 | 0x1017 | Byte 0 |
**NFC_KPx Bit Meanings (AES mode):**
Each NFC_KPx is a 1-byte bitmask. Bit = 1 grants that capability after successful mutual authentication using KeyID = x.
| Bit | Name | Privilege Granted |
| --- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 7 | Restricted AREA_1 Write | Write access to restricted user memory AREA_1 |
| 6 | Restricted AREA_1 Read | Read access to restricted user memory AREA_1 |
| 5 | Crypto Config | Preset of Authentication Limit |
| 4 | EAS/AFI | Access to write-alike EAS/AFI commands: PROTECT EAS/AFI, SET EAS, RESET EAS, LOCK EAS, WRITE EAS ID, WRITE AFI, LOCK AFI |
| 3 | Destroy | Access to DESTROY functionality |
| 2 | Privacy | Enable/disable PRIVACY mode |
| 1 | Write | Write access to the protected user memory area |
| 0 | Read | Read access to the protected user memory area |
**Common Privilege Values:**
- `0x03` = Read + Write access to protected memory
- `0x07` = Read + Write + Privacy control
- `0xFF` = All privileges enabled
### AES Key Storage
**Key Addresses (per NTP53x2 datasheet page 16):**
| Key | Blocks (NFC) | Blocks (I2C) | Size |
| ---- | ------------ | ------------- | ------------------- |
| KEY0 | 0x20-0x23 | 0x1020-0x1023 | 16 bytes (4 blocks) |
| KEY1 | 0x24-0x27 | 0x1024-0x1027 | 16 bytes |
| KEY2 | 0x28-0x2B | 0x1028-0x102B | 16 bytes |
| KEY3 | 0x2C-0x2F | 0x102C-0x102F | 16 bytes |
**NOTE:** Addresses 0x1E-0x1F are RFU (reserved), NOT key storage!
**Default Key Value:** Unknown - likely not all zeros. Factory default may vary.
**Key Write Requirements:**
- Must enable AES mode (DEV_SEC_CONFIG = 0xA2) before writing keys
- Keys can be written and read back while header is 0x81 (not active)
- Once a key header is set to 0xE7, the key storage becomes unreadable
- Always verify key values by reading back before activating headers
## Safe Programming Order
This order prevents locking yourself out when provisioning AES mode on an NTP5332.
### Step 1: Keep NFC_GCH at 0x81 (Deactivated)
While NFC_GCH = 0x81:
- Protection settings aren't enforced yet
- Protection pointer/conditions can be modified via PROTECT PAGE without prior mutual auth
- Keys and privileges are freely readable/writable
**WARNING:** NFC_GCH only programs one-way (lower→higher) and is irreversible. Don't jump to C1/C7/E7 until you've tested.
### Step 2: Set Up Memory Protection Layout
Configure these while GCH is still 0x81:
- Protection Pointer Address
- Protection Pointer Condition
- Any EAS/AFI protection conditions you plan to use
### Step 3: Write Key Privileges and Keys
While each NFC_KHx is still 0x81 (Not active):
- Write NFC_KPx (Key Privileges) to define what each key can authorize
- Write the 128-bit key values to storage blocks (0x20-0x2F)
- Key and privileges are readable/writable via READ/WRITE CONFIG
- Key is not active for authentication yet
**Key Storage Addresses (per datasheet page 16):**
- KEY0: blocks 0x20-0x23
- KEY1: blocks 0x24-0x27
- KEY2: blocks 0x28-0x2B
- KEY3: blocks 0x2C-0x2F
### Step 4: Activate/Lock Only the Keys You Need
For each key you intend to use:
- Set NFC_KHx → 0xE7 (Active and locked)
- Key becomes usable for authentication
- Key material can no longer be read/written via READ/WRITE CONFIG
For unused keys (NXP recommendation):
- Set NFC_KHx → 0xFF (Disabled)
- Prevents unused key slots from being exploited
**WARNING:** Key headers are one-way (lower→higher) and irreversible.
### Step 5: Enable Enforcement
Set NFC_GCH → 0xC1 (Access right activated):
- Read/write protection becomes enforced according to your settings
- EAS/AFI protection becomes enforced
- Keys can still be modified if their headers are still 0x81
### Step 6: Test Everything
Verify your protection configuration:
- Try protected reads/writes WITHOUT mutual auth (should be denied)
- Mutual-auth with each KeyID and confirm only the privileges in that key's KPx work
- Test all access patterns you need
### Step 7: (Optional) Freeze Configuration
Only after thorough testing:
- NFC_GCH → 0xC7: Locks privileges, keeps enforcement, keys still modifiable per their KH status
- NFC_GCH → 0xE7: "Finalize" state - enforcement on, all key headers/privileges/keys locked forever
### Step 8: (Optional) Lock Authentication Limit
Only when you're sure about auth-limit behavior:
- Set NFC_CCH → 0xE7 to lock the authentication-limit setting
- After this, auth limit can only be modified with a key that has Crypto Config privilege (bit 5)
### Footgun to Avoid
**Authentication Limit:** If you enable the Authentication Limit feature (block 0x0E), hitting the terminal failure count causes **permanent, irreversible lockout** of that key slot. Set a reasonable limit or leave at 0 (unlimited) during development.
## Legacy Key Provisioning (Simple Version)
For simple use cases (single key, no protection pointer):
1. **Ensure AES Mode is Enabled**
- Read DEV_SEC_CONFIG at 0x3F
- If bits 3-0 != 0010b, write new value with AES mode enabled
- Power cycle the chip (remove and replace on reader)
2. **Write Key Value**
- Write 16 bytes to KEY0 blocks 0x20-0x23
- Key can only be written when NFC_KH0 is 0x81
3. **Activate Key Header**
- Write 0xE7 to block 0x10 byte 1
- This is IRREVERSIBLE - key is locked after this
4. **(Optional) Activate NFC_GCH**
- Write 0xC1 or 0xE7 to enable protection pointer enforcement
- 0xE7 locks all crypto configuration permanently
## Authentication Protocol
### COMPLETE MAM AUTHENTICATION (VERIFIED WORKING 2026-02-13)
**NTAG5 Link uses UNDOCUMENTED AuthMethod values for MAM:**
| Value | Method | Result |
| ----- | --------------- | -------------------------------------------------- |
| 0x00 | TAM1 (Tag Auth) | SUCCESS - 18 byte response |
| 0x02 | **MAM1** | SUCCESS - 24 byte response |
| 0x06 | **MAM2** | SUCCESS - empty response (authentication complete) |
| 0x80 | MAM1 (per ISO) | Error 0x0F (NOT SUPPORTED) |
| 0x90 | MAM2 (per ISO) | Error 0x0F (NOT SUPPORTED) |
**CRITICAL BYTE ORDERING RULES:**
All data to/from the chip must be byte-reversed:
- IChallenge sent to chip: **REVERSED**
- TChallenge_high from response: **REVERSED**
- IResponse sent to chip: **REVERSED**
### MAM1 (AuthMethod = 0x02)
**Command Format:**
```
FLAGS(0x22) | CMD(0x35) | UID(8) | CSI(0x00) | AuthMethod(0x02) | KeyID | IChallenge[::-1]
```
- IChallenge: 10 bytes, must be **REVERSED** before sending
**Response Structure (24 bytes):**
| Offset | Length | Content |
| ------ | ------ | ---------------------------------------------- |
| 0 | 1 | Flags: 0x04 |
| 1 | 1 | Header: 0xA7 |
| 2-7 | 6 | TChallenge[79:32] (plaintext, needs reversal!) |
| 8-23 | 16 | Encrypted block |
**Decryption Procedure:**
1. Extract TChallenge_high_raw = bytes 2-7 (6 bytes)
2. Extract encrypted block = bytes 8-23 (16 bytes)
3. **REVERSE** encrypted block, then AES-ECB decrypt
4. Parse decrypted: `C_MAM1(2) || TChallenge[31:0](4) || IChallenge(10)`
5. **REVERSE** TChallenge_high_raw to get actual TChallenge_high
6. TChallenge[79:0] = TChallenge_high + TChallenge_low
**Verified Constants:**
- C_MAM1 = 0xDA83
- IChallenge in decrypted = original IChallenge (chip de-reverses what it receives)
### MAM2 (AuthMethod = 0x06)
**Command Format (NO KeyID!):**
```
FLAGS(0x22) | CMD(0x35) | UID(8) | CSI(0x00) | AuthMethod(0x06) | IResponse[::-1]
```
- IResponse: 16 bytes, must be **REVERSED** before sending
**IResponse Computation:**
```python
# Build plaintext (16 bytes)
C_MAM2_PURPOSE = bytes([0xDA, 0x80]) # C_MAM2[11:0] || Purpose[3:0]
IChallenge_31_0 = original_ichallenge[6:10] # Last 4 bytes of ORIGINAL
TChallenge_79_0 = tc_high_reversed + tc_low # From MAM1 response
plaintext = C_MAM2_PURPOSE + IChallenge_31_0 + TChallenge_79_0
# Compute IResponse
iresponse_to_send = AES_DEC(key, plaintext)[::-1] # Decrypt then REVERSE
```
**Expected Response:** Empty (just flags/header, no data) = SUCCESS
### Complete MAM Example (Python)
```python
from Crypto.Cipher import AES
def perform_mam(hcard, proto, uid_lsb, key, key_id, ichallenge):
cipher = AES.new(key, AES.MODE_ECB)
# MAM1: Send IChallenge REVERSED
mam1_cmd = bytes([0x22, 0x35]) + uid_lsb + bytes([0x00, 0x02, key_id]) + ichallenge[::-1]
mam1_resp = send_command(hcard, proto, mam1_cmd)
# Parse MAM1 response
tc_high_raw = mam1_resp[2:8] # Needs reversal!
encrypted = mam1_resp[8:24]
decrypted = cipher.decrypt(encrypted[::-1])
assert decrypted[0:2] == bytes([0xDA, 0x83]) # C_MAM1
tc_high = tc_high_raw[::-1] # REVERSE!
tc_low = decrypted[2:6]
tc_full = tc_high + tc_low
# MAM2: Build IResponse
C_MAM2_PURPOSE = bytes([0xDA, 0x80])
ich_31_0 = ichallenge[6:10] # Original, not reversed
plaintext = C_MAM2_PURPOSE + ich_31_0 + tc_full
iresponse = cipher.decrypt(plaintext)[::-1] # Decrypt then REVERSE
# MAM2: No KeyID in command!
mam2_cmd = bytes([0x22, 0x35]) + uid_lsb + bytes([0x00, 0x06]) + iresponse
mam2_resp = send_command(hcard, proto, mam2_cmd)
return mam2_resp is empty or success
```
**Verified Working (2026-02-13):** All 4 key slots (KEY0-KEY3) authenticate successfully using this protocol.
**Standard MAM1/MAM2 (0x80/0x90) fail** with Error 0x0F despite the chip's GET NXP SYSTEM INFORMATION reporting MUTUAL AUTH = 1. Use AuthMethod 0x02/0x06 instead.
---
### ISO 29167-10 Standard MAM (AuthMethod 0x80/0x90) - NOT SUPPORTED
**NOTE:** The standard ISO 29167-10 AuthMethod values (0x80 for MAM1, 0x90 for MAM2) are **NOT supported** by NTAG5 Link hardware, despite documentation suggesting otherwise.
Use the undocumented AuthMethod values instead:
- **AuthMethod 0x02** for MAM1
- **AuthMethod 0x06** for MAM2 (no KeyID in command)
See "Complete MAM Authentication" section above for the working protocol.
## TAM1 Authentication (Tag Authentication Method 1)
TAM1 is a one-way authentication where the reader verifies the tag's identity. It uses the CHALLENGE (0x39) and READBUFFER (0x3A) commands instead of the full mutual authentication.
### TAM1 Protocol Flow
1. **Reader sends CHALLENGE** - Contains KeyID and random IChallenge
2. **Tag computes AES encryption** - No response sent
3. **Reader sends READBUFFER** - Requests the crypto result
4. **Tag returns TResponse** - 16-byte encrypted block
5. **Reader decrypts and verifies** - Checks C_TAM1 constant and IChallenge
### TAM1 Message Format
**CHALLENGE Command (0x39):**
```
Flags | 0x39 | CSI | AuthMethod/RFU | KeyID | IChallenge (10 bytes)
```
- Flags: 0x02 (high data rate, NOT addressed)
- CSI: 0x00 (AES-128 Crypto Suite Identifier)
- AuthMethod/RFU: 0x00 (TAM1 = 00b, CustomData = 0, RFU = 00000b)
- KeyID: 0x00-0x03 (KEY0-KEY3)
- IChallenge: 10 bytes random challenge
**READBUFFER Command (0x3A):**
```
Flags | 0x3A
```
- Flags: 0x02 (high data rate, NOT addressed)
**TResponse Format (16 bytes):**
```
AES-ECB-ENC(Key, C_TAM1 || TRnd || IChallenge)
```
- C_TAM1: 0x96C5 (2 bytes) - constant per ISO 29167-10
- TRnd: 4 bytes random from tag
- IChallenge: 10 bytes echoed from CHALLENGE command
### TAM1 Verification
To verify TAM1:
1. **Reverse the TResponse byte order** (ACR1552 returns data in LSB-first order)
2. Decrypt TResponse with the expected key using AES-128-ECB
3. Check bytes 0-1 equal 0x96C5 (C_TAM1 constant)
4. **Reverse the echoed IChallenge** (bytes 6-15) before comparing
5. Check reversed bytes 6-15 equal the IChallenge you sent
6. If both match, the tag is authentic
**CRITICAL: ACR1552 Byte Ordering**
The ACR1552 returns TResponse in reversed byte order due to ISO 15693's LSB-first transmission.
Both the entire TResponse and the echoed IChallenge within the decrypted data must be reversed.
```python
# Example verification (Python)
tresponse_raw = resp[1:17] # Skip flags byte
tresponse = tresponse_raw[::-1] # Reverse entire TResponse
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(tresponse)
c_tam1 = decrypted[0:2] # Should be 96C5
trnd = decrypted[2:6] # 4 bytes random from tag
echoed_raw = decrypted[6:16] # Echoed IChallenge (reversed)
echoed = echoed_raw[::-1] # Reverse to get original order
if c_tam1 == bytes([0x96, 0xC5]) and echoed == ichallenge:
# Authentication successful
```
### ACR1552 Complete APDU Format for TAM1
The ACR1552 uses pseudo-APDUs with TLV wrapping for transparent mode communication.
**PCSC APDU Structure:**
```
FF C2 00 01 Lc [TLV Data] 00
```
- CLA: 0xFF (pseudo-APDU)
- INS: 0xC2 (transparent exchange)
- P1: 0x00
- P2: 0x01 (exchange function)
- Lc: Length of TLV data
- Le: 0x00
**TLV Data Structure:**
```
5F46 04 [timeout_4bytes] Timeout in microseconds (big-endian)
FF6E 03 03 01 0F FWTI (Frame Waiting Time Integer)
95 xx [iso15693_command] ISO 15693 command payload
```
**Response TLV Structure:**
```
C0 03 [status_3bytes] Error/status TLV (SW1 SW2 at bytes 1-2)
92 01 xx Framing TLV
96 02 xx xx Status TLV
97 xx [iso15693_response] Response data TLV (includes flags byte)
```
- SW = 0x9000: Success
- SW = 0x6401: Execution error (no response from ICC)
### Complete APDU Examples (Working TAM1 Flow)
**Example 1: GET SYSTEM INFO (required before CHALLENGE)**
```
TX: ffc20001115f4604000f4240ff6e0303010f9502022b00
├─ FF C2 00 01 11 PCSC header (Lc=17)
├─ 5F46 04 000F4240 Timeout = 1,000,000 µs (1 second)
├─ FF6E 03 030100F FWTI
├─ 95 02 022B ISO15693: Flags=02, Cmd=2B (GET SYSTEM INFO)
└─ 00 Le
RX: c00300900092010096020000970f000f02e88c59580104e00000ff0301
├─ C0 03 009000 Status OK (SW=9000)
├─ 92 01 00 Framing OK
├─ 96 02 0000 Status OK
└─ 97 0F 00... Response: Flags=00 + System Info (14 bytes)
```
**Example 2: CHALLENGE (KeyID=3, IChallenge=33ccc451ad4d9a6e9cd6)**
```
TX: ffc200011e5f4604000f4240ff6e0303010f950f023900000333ccc451ad4d9a6e9cd600
├─ FF C2 00 01 1E PCSC header (Lc=30)
├─ 5F46 04 000F4240 Timeout = 1 second
├─ FF6E 03 03010F FWTI
├─ 95 0F 02 39 00 00 03 33ccc451ad4d9a6e9cd6
│ ├─ 02 Flags (high data rate)
│ ├─ 39 CHALLENGE command
│ ├─ 00 CSI (AES-128)
│ ├─ 00 AuthMethod=TAM1
│ ├─ 03 KeyID=3
│ └─ 33ccc451ad4d9a6e9cd6 IChallenge (10 bytes)
└─ 00 Le
RX: c003036401
└─ C0 03 036401 Status: SW=6401 (no response from ICC)
This is EXPECTED - CHALLENGE has no response
```
**Example 3: READBUFFER**
```
TX: ffc20001115f4604000f4240ff6e0303010f9502023a00
├─ FF C2 00 01 11 PCSC header (Lc=17)
├─ 5F46 04 000F4240 Timeout = 1 second
├─ FF6E 03 03010F FWTI
├─ 95 02 02 3A ISO15693: Flags=02, Cmd=3A (READBUFFER)
└─ 00 Le
RX: c00300900092010096020000971102238333363a3142b26328d1bf618362e4
├─ C0 03 009000 Status OK
├─ 92 01 00 Framing OK
├─ 96 02 0000 Status OK
└─ 97 11 02 238333363a3142b26328d1bf618362e4
├─ 02 Flags (success, no error)
└─ 238333...e4 TResponse (16 bytes)
```
### Critical Timing Requirement
**IMPORTANT:** The chip requires time to compute the AES encryption after receiving CHALLENGE.
| Scenario | READBUFFER Result |
| ----------------------------------------------------- | ------------------------- |
| READBUFFER sent immediately after CHALLENGE | Often fails (no response) |
| GET SYSTEM INFO sent between CHALLENGE and READBUFFER | Works (implicit delay) |
| 1 second delay between CHALLENGE and READBUFFER | Works reliably |
**Recommended approach:** Either:
1. Send any command (e.g., GET SYSTEM INFO) after CHALLENGE before READBUFFER, OR
2. Add a minimum 50-100ms delay after CHALLENGE before sending READBUFFER
The crypto calculation typically takes ~4-8ms, but reader/driver overhead can cause timing issues if READBUFFER is sent too quickly.
### READBUFFER Failure Example (Insufficient Delay)
When CHALLENGE is sent immediately after connection with no prior commands and READBUFFER follows too quickly:
```
TX: ffc200011e5f4604000f4240ff6e0303010f950f0239000003[IChallenge]00
RX: c003036401 (SW=6401, expected - no response for CHALLENGE)
TX: ffc20001115f4604000f4240ff6e0303010f9502023a00
RX: c003036401 (SW=6401, FAILURE - chip not ready)
```
The second `c003036401` response indicates "no response from ICC" - the chip hasn't finished computing the crypto result yet. Adding a delay or sending an intermediate command resolves this.
### TAM1 vs State Machine
Per the datasheet, CHALLENGE can only be executed in READY state with non-addressed mode. However, testing shows:
- CHALLENGE works even after GET SYSTEM INFO (which puts chip in SELECTED state)
- The critical factor is the **timing delay**, not the chip state
- Sending GET SYSTEM INFO first provides the necessary delay for crypto completion
## Reader-Specific Notes
### ACS ACR1552
- Uses transparent mode with TLV-wrapped commands
- Supports ISO15693 natively
- Switch protocol command: `8F 02 02 03` (ISO15693 Layer 3)
- Timeout TLV: `5F46` with 4-byte timeout in microseconds
- Commands work correctly for basic operations
- CHALLENGE returns SW=6401 (no response from ICC) - this is expected
- READBUFFER returns SW=9000 with 17-byte response (flags + 16-byte TResponse)
- Authentication returns error 0x0F when key is not properly configured
**CRITICAL: Byte Order Reversal Required**
- TResponse data is returned in LSB-first (reversed) byte order
- Must reverse entire TResponse before AES decryption
- Echoed IChallenge in decrypted data is also reversed
- See TAM1 Verification section for correct processing order
- AUTHENTICATE (0x35) command returns "no ICC response" - use CHALLENGE/READBUFFER instead
### HID Omnikey 5022 CL
- Uses transceive APDU: `FF 68 0E 03`
- 10-byte header: TxRxFlags | ValidBits | Timeout(4) | RFU(4)
- TxRxFlags: 0x05 for ISO15693 (CRC TX + CRC RX enabled)
- Response format: STS(1) | RxB(1) | Card_Response(n) | SW(2)
- Direct transceive works WITHOUT entering transparent session first
- ISO15693 transparent session may not work reliably - use direct transceive
**AUTHENTICATE (0x35) for TAM1 - CONFIRMED WORKING EXAMPLE:**
TAM1 command format (AuthMethod = 0x00):
```
Flags(22) | Cmd(35) | UID(8) | CSI(00) | AuthMethod(00) | KeyID | IChallenge(10)
```
**Verified Example (2026-02-13):**
Input parameters:
- Chip UID: E0040158F08BE802 (MSB) / 02E88BF0580104E0 (LSB)
- KEY0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- IChallenge: 112233445566778899AA
Command sent to reader:
```
FF680E03210500004C4B4000000000223502E88BF0580104E0000000112233445566778899AA00
```
| Bytes | Value | Meaning |
| -------------------- | ---------- | -------------------- |
| FF680E03 | Header | Omnikey transceive |
| 21 | Lc=33 | Data length |
| 05 | TxRxFlags | ISO15693 CRC |
| 00 | ValidBits | Whole bytes |
| 004C4B40 | Timeout | 5 seconds |
| 00000000 | RFU | Reserved |
| 22 | Flags | Addressed, high rate |
| 35 | Cmd | AUTHENTICATE |
| 02E88BF0580104E0 | UID | LSB order |
| 00 | CSI | AES-128 |
| 00 | AuthMethod | TAM1 |
| 00 | KeyID | KEY0 |
| 112233445566778899AA | IChallenge | 10 bytes |
| 00 | Le | Response length |
Response received:
```
000006A7F2E114AFC63A0F6A7170A4F1692DD29F9000
```
| Bytes | Value | Meaning |
| -------------------------------- | --------- | ------------------ |
| 0000 | STS+RxB | Transceive OK |
| 06 | Flags | Success |
| A7 | Header | Barker code (skip) |
| F2E114AFC63A0F6A7170A4F1692DD29F | TResponse | 16 bytes encrypted |
| 9000 | SW | Success |
**Decryption process:**
1. Extract TResponse: `F2E114AFC63A0F6A7170A4F1692DD29F`
2. REVERSE bytes: `9FD22D69F1A470716A0F3AC6AF14E1F2`
3. AES-ECB decrypt with KEY0: `96C51B6F50BBAA998877665544332211`
4. Parse result:
- C_TAM1: `96C5` (correct constant)
- TRnd: `1B6F50BB` (tag random)
- Echoed: `AA998877665544332211` (IChallenge reversed)
**VERIFICATION: SUCCESS** - C_TAM1=96C5, IChallenge matches (reversed)
```python
# Omnikey 5022 TAM1 verification (AuthMethod=0x00)
card_resp = response[2:-2] # Strip STS, RxB, and SW
data_after_flags = card_resp[1:] # Skip flags byte (0x06)
tresponse = data_after_flags[1:17] # Skip header byte (0xA7), take 16 bytes
tresponse = tresponse[::-1] # Reverse byte order
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(tresponse)
c_tam1 = decrypted[0:2] # Should be 96C5
trnd = decrypted[2:6] # 4-byte tag random
echoed = decrypted[6:16] # REVERSED - compare against ichallenge[::-1]
if c_tam1 == bytes([0x96, 0xC5]) and echoed == ichallenge[::-1]:
# TAM1 authentication successful
```
## Error Codes
### ISO15693 Error Codes (from flag byte with error bit set)
| Code | Description |
| ---- | ---------------------- |
| 0x01 | Command not supported |
| 0x02 | Command not recognized |
| 0x03 | Option not supported |
| 0x0F | Unknown error |
| 0x10 | Block not available |
| 0x11 | Block already locked |
| 0x12 | Block locked |
| 0x13 | Programming failed |
| 0x14 | Locking failed |
**Error 0x0F during authentication typically means:**
- Key header not activated (still 0x81 instead of 0xE7)
- Wrong key slot used
- Chip not in AES mode
- Malformed MAM1 message
## READ CONFIG Command Requirements
**IMPORTANT:** READ CONFIG (0xC0) must use **unaddressed mode** (flags=0x02).
**Correct Format:**
```
Flags | Cmd | MfgCode | BlockAddr | NumBlocks
0x02 | 0xC0 | 0x04 | addr | 0x00
```
Example: `bytes([0x02, 0xC0, 0x04, 0x3F, 0x00])` to read DEV_SEC_CONFIG
**Wrong Format (does not work):**
```
Flags | Cmd | MfgCode | UID (8 bytes) | BlockAddr
0x22 | 0xC0 | 0x04 | uid_lsb | addr
```
Using addressed mode (flags=0x22 with UID) returns no response from the chip.
## Common Issues
1. **"Unknown error" (0x0F) on AUTHENTICATE**
- Check DEV_SEC_CONFIG is in AES mode (bits 3-0 = 0010b)
- Check Key Header is 0xE7 (active and locked)
- Power cycle chip after mode changes
2. **Key Header reads as 0x00**
- Incorrect address used - Key headers are at 0x10, 0x12, 0x14, 0x16
- Block 0x0E is NFC_AUTH_LIMIT, not KEY0 header
3. **Cannot write key after activation**
- Once key header is 0xE7, key is permanently locked
- Must set key value BEFORE activating header
4. **Mode change doesn't take effect**
- Configuration changes require power cycle (POR)
- Remove card from reader field and place again
5. **Keys must be written in AES mode**
- Enable AES mode (DEV_SEC_CONFIG = 0xA2) before writing key values
- Power cycle after enabling AES mode, then write keys
- Keys written while in password mode may not work correctly for authentication
6. **TAM1/MAM1 authentication fails despite correct key**
- Key values verified by read-back but authentication still fails
- This is a known issue under investigation with NXP (open support ticket)
- Ensure chip is in AES mode before writing keys
## Troubleshooting Checklist
Before attempting authentication:
1. **Verify AES Mode:**
```
Read config block 0x3F
DEV_SEC_CONFIG byte 0, bits 3-0 should be 0010b
Example: 0xA2 = AES mode enabled, writable
```
2. **Verify Key Header is Active:**
```
Read config block 0x10 (KEY0), 0x12 (KEY1), 0x14 (KEY2), or 0x16 (KEY3)
Byte 1 should be 0xE7 for the key you want to use
0x81 = not active (cannot authenticate)
```
3. **Verify Key Value is Known:**
```
If key header was 0x81 before you configured it:
- Was AES mode enabled BEFORE writing key values?
- Did read-back verification match what you wrote?
- After activating header to 0xE7, key blocks should return errors (protected)
```
4. **Check Auth Failure Counter:**
```
Read config block 0x0E
NFC_AUTH_LIMIT (bits 9-0) sets max failures before lockout
If counter reaches limit, key slot is permanently blocked
```
5. **Test with Correct Command Format:**
```
AUTHENTICATE: 0x22 0x35 <UID_LSB_8bytes> 0x00 0x80 <key_slot> <challenge_10bytes>
- FLAGS = 0x22 (high data rate, addressed mode) - REQUIRED
- CSI = 0x00 (AES-128)
- MAM1_Header = 0x80
```