Files
pm3py/docs/NTAG5_SECURITY.md
michael 615d4a6115 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>
2026-07-05 12:58:48 -07:00

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 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:

# 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

  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.

# 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)

# 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