- 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>
33 KiB
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 memory0x07= Read + Write + Privacy control0xFF= 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):
-
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)
-
Write Key Value
- Write 16 bytes to KEY0 blocks 0x20-0x23
- Key can only be written when NFC_KH0 is 0x81
-
Activate Key Header
- Write 0xE7 to block 0x10 byte 1
- This is IRREVERSIBLE - key is locked after this
-
(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:
- Extract TChallenge_high_raw = bytes 2-7 (6 bytes)
- Extract encrypted block = bytes 8-23 (16 bytes)
- REVERSE encrypted block, then AES-ECB decrypt
- Parse decrypted:
C_MAM1(2) || TChallenge[31:0](4) || IChallenge(10) - REVERSE TChallenge_high_raw to get actual TChallenge_high
- 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:
# 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)
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
- Reader sends CHALLENGE - Contains KeyID and random IChallenge
- Tag computes AES encryption - No response sent
- Reader sends READBUFFER - Requests the crypto result
- Tag returns TResponse - 16-byte encrypted block
- 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:
- Reverse the TResponse byte order (ACR1552 returns data in LSB-first order)
- Decrypt TResponse with the expected key using AES-128-ECB
- Check bytes 0-1 equal 0x96C5 (C_TAM1 constant)
- Reverse the echoed IChallenge (bytes 6-15) before comparing
- Check reversed bytes 6-15 equal the IChallenge you sent
- 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.
# 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:
- Send any command (e.g., GET SYSTEM INFO) after CHALLENGE before READBUFFER, OR
- 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:
5F46with 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:
- Extract TResponse:
F2E114AFC63A0F6A7170A4F1692DD29F - REVERSE bytes:
9FD22D69F1A470716A0F3AC6AF14E1F2 - AES-ECB decrypt with KEY0:
96C51B6F50BBAA998877665544332211 - Parse result:
- C_TAM1:
96C5(correct constant) - TRnd:
1B6F50BB(tag random) - Echoed:
AA998877665544332211(IChallenge reversed)
- C_TAM1:
VERIFICATION: SUCCESS - C_TAM1=96C5, IChallenge matches (reversed)
# 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
-
"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
-
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
-
Cannot write key after activation
- Once key header is 0xE7, key is permanently locked
- Must set key value BEFORE activating header
-
Mode change doesn't take effect
- Configuration changes require power cycle (POR)
- Remove card from reader field and place again
-
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
-
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:
-
Verify AES Mode:
Read config block 0x3F DEV_SEC_CONFIG byte 0, bits 3-0 should be 0010b Example: 0xA2 = AES mode enabled, writable -
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) -
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) -
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 -
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