# Stateful Response Table Firmware Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add a stateful response table engine to PM3 firmware for custom/vendor 15693 commands, with access control in the native handler and shared AES-CMAC crypto. **Architecture:** New `sim_table.c/h` provides table storage, lookup with group-based state machine, and action execution (static response, EML read/write, AES-CMAC). The existing `SimTagIso15693()` native handler gains per-block access control and falls through to the table for unrecognized commands. Shared `sim_crypto.c/h` wraps existing `ulaes_cmac()`. **Tech Stack:** C (ARM7 firmware), existing PM3 build system, existing `cmac_calc.c` for AES **Working directory:** `/home/work/proxmark3/` on branch `sim-relay` **Build command:** `make armsrc/obj/fullimage.elf` (firmware only, no client) **Important:** All commits use `git commit --no-gpg-sign`. Firmware has no unit test framework — we verify by successful compilation. Python-side tests come later in a separate plan. --- ### Task 1: Command IDs **Files:** - Modify: `/home/work/proxmark3/include/pm3_cmd.h` **Step 1: Add table command IDs** After the `CMD_HF_SEOS_SIMULATE` line (~930), add: ```c // Sim response table management #define CMD_SIM_TABLE_UPLOAD 0x0950 // Host→FW: upload table (chunked) #define CMD_SIM_TABLE_CLEAR 0x0951 // Host→FW: clear table #define CMD_SIM_TABLE_UPDATE 0x0952 // Host→FW: add/modify single entry ``` **Step 2: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 3: Commit** ```bash cd /home/work/proxmark3 git add include/pm3_cmd.h git commit --no-gpg-sign -m "feat: add CMD_SIM_TABLE_* command IDs" ``` --- ### Task 2: iso15_tag_t extensions **Files:** - Modify: `/home/work/proxmark3/include/iso15.h` **Step 1: Add new fields to iso15_tag_t** Add these fields before the closing `} PACKED iso15_tag_t;` (after `trace_enabled`): ```c // Stateful table support uint8_t random_uid; // if nonzero, inventory returns random UID uint8_t auth_state; // bitmask: authenticated passwords // bit 0: read, bit 1: write, bit 2: privacy, bit 3: destroy uint8_t lock_mode[ISO15693_TAG_MAX_PAGES]; // per-block access control: // 0x00 = open // 0x01 = permanently locked // 0x02 = read needs auth (check auth_state bit 0) // 0x04 = write needs auth (check auth_state bit 1) // 0x06 = read+write need auth ``` **Step 2: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 3: Commit** ```bash cd /home/work/proxmark3 git add include/iso15.h git commit --no-gpg-sign -m "feat: extend iso15_tag_t with auth_state, lock_mode, random_uid" ``` --- ### Task 3: sim_crypto module **Files:** - Create: `/home/work/proxmark3/armsrc/sim_crypto.h` - Create: `/home/work/proxmark3/armsrc/sim_crypto.c` - Modify: `/home/work/proxmark3/armsrc/Makefile` **Step 1: Create sim_crypto.h** ```c //----------------------------------------------------------------------------- // Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // See LICENSE.txt for the text of the license. //----------------------------------------------------------------------------- // Shared crypto primitives for card simulation //----------------------------------------------------------------------------- #ifndef __SIM_CRYPTO_H #define __SIM_CRYPTO_H #include "common.h" // AES-CMAC: compute MAC over msg using 16-byte key // mac_out receives up to mac_len bytes of the 16-byte MAC // Returns true on success bool sim_aes_cmac(const uint8_t key[16], const uint8_t *msg, uint8_t msg_len, uint8_t *mac_out, uint8_t mac_len); #endif // __SIM_CRYPTO_H ``` **Step 2: Create sim_crypto.c** ```c //----------------------------------------------------------------------------- // Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // See LICENSE.txt for the text of the license. //----------------------------------------------------------------------------- // Shared crypto primitives for card simulation //----------------------------------------------------------------------------- #include "sim_crypto.h" #include "cmac_calc.h" #include bool sim_aes_cmac(const uint8_t key[16], const uint8_t *msg, uint8_t msg_len, uint8_t *mac_out, uint8_t mac_len) { if (mac_len > 16) mac_len = 16; uint8_t full_mac[16]; ulaes_cmac(key, 16, msg, msg_len, full_mac); memcpy(mac_out, full_mac, mac_len); return true; } ``` **Step 3: Add to Makefile** In `armsrc/Makefile`, find the `SRC_ASM` or `SRC_FIRM` source list (where `cmac_calc.c` appears). Add `sim_crypto.c \` on the line after `cmac_calc.c \`. **Step 4: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 5: Commit** ```bash cd /home/work/proxmark3 git add armsrc/sim_crypto.h armsrc/sim_crypto.c armsrc/Makefile git commit --no-gpg-sign -m "feat: add sim_crypto module (AES-CMAC wrapper)" ``` --- ### Task 4: sim_table header **Files:** - Create: `/home/work/proxmark3/armsrc/sim_table.h` **Step 1: Create sim_table.h** ```c //----------------------------------------------------------------------------- // Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // See LICENSE.txt for the text of the license. //----------------------------------------------------------------------------- // Stateful response table for card simulation //----------------------------------------------------------------------------- #ifndef __SIM_TABLE_H #define __SIM_TABLE_H #include "common.h" #include "iso15.h" #define SIM_TABLE_MAX_MATCH 32 #define SIM_TABLE_MAX_RESPONSE 64 #define SIM_TABLE_MAX_ENTRIES 300 // EML action types #define SIM_EML_NONE 0 // static response only #define SIM_EML_READ 1 // read from EML into response #define SIM_EML_WRITE 2 // write command data to EML #define SIM_EML_AES_CMAC 3 // compute AES-CMAC, key from EML, challenge from cmd // Match modes #define SIM_MATCH_EXACT 0 #define SIM_MATCH_PREFIX 1 // Entry flags #define SIM_FLAG_APPEND_CRC 0x01 #define SIM_FLAG_CONSUME 0x02 // one-shot: disable entry after use typedef struct { // Match uint8_t match[SIM_TABLE_MAX_MATCH]; uint8_t match_len; uint8_t match_mode; // SIM_MATCH_EXACT or SIM_MATCH_PREFIX // Response uint8_t response[SIM_TABLE_MAX_RESPONSE]; uint8_t response_len; uint8_t response_flags; // SIM_FLAG_APPEND_CRC // EML actions uint8_t eml_action; // SIM_EML_* uint16_t eml_offset; // EML byte offset for read/write/key uint8_t eml_len; // bytes to read/write / MAC output length uint8_t eml_resp_insert; // position in response to insert data (read/CMAC) uint8_t cmd_data_offset; // position in command for write data / challenge uint8_t cmd_data_len; // length of data to write / challenge length // State uint8_t group; // group this entry belongs to (0 = always active) uint32_t activate_groups; // bitmask: groups to activate on match uint32_t deactivate_groups; // bitmask: groups to deactivate on match uint8_t set_auth; // auth_state bits to SET on match uint8_t clear_auth; // auth_state bits to CLEAR on match // Flags uint8_t flags; // SIM_FLAG_CONSUME etc. uint8_t _pad[1]; // alignment } PACKED sim_table_entry_t; // ~128 bytes // Initialize table in BigBuf with initial active group mask void sim_table_init(uint32_t initial_groups); // Clear all entries, reset state void sim_table_clear(void); // Add entry from serialized data (128 bytes) // Returns true if entry was added bool sim_table_add(const uint8_t *data, uint16_t len); // Current entry count uint16_t sim_table_count(void); // Find first matching entry for command // Returns NULL if no match sim_table_entry_t *sim_table_lookup(const uint8_t *cmd, uint8_t cmd_len); // Execute matched entry: build response, apply state changes // tag pointer needed for EML access and auth_state updates void sim_table_execute(sim_table_entry_t *e, const uint8_t *cmd, uint8_t cmd_len, uint8_t *resp_buf, uint16_t *resp_len, iso15_tag_t *tag); // Get current active groups mask uint32_t sim_table_get_active_groups(void); #endif // __SIM_TABLE_H ``` **Step 2: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds (header only, no .c yet) **Step 3: Commit** ```bash cd /home/work/proxmark3 git add armsrc/sim_table.h git commit --no-gpg-sign -m "feat: add sim_table.h — stateful response table header" ``` --- ### Task 5: sim_table implementation **Files:** - Create: `/home/work/proxmark3/armsrc/sim_table.c` - Modify: `/home/work/proxmark3/armsrc/Makefile` **Step 1: Create sim_table.c** ```c //----------------------------------------------------------------------------- // Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // See LICENSE.txt for the text of the license. //----------------------------------------------------------------------------- // Stateful response table for card simulation //----------------------------------------------------------------------------- #include "sim_table.h" #include "sim_crypto.h" #include "BigBuf.h" #include "dbprint.h" #include static struct { sim_table_entry_t *entries; uint16_t count; uint16_t max_entries; uint32_t active_groups; } g_table; void sim_table_init(uint32_t initial_groups) { // Allocate table from BigBuf // We use a dedicated region at the end of BigBuf uint32_t table_size = SIM_TABLE_MAX_ENTRIES * sizeof(sim_table_entry_t); g_table.entries = (sim_table_entry_t *)BigBuf_calloc(table_size); g_table.count = 0; g_table.max_entries = SIM_TABLE_MAX_ENTRIES; g_table.active_groups = initial_groups; if (g_dbglevel >= DBG_INFO) Dbprintf("sim_table_init: %d entries max, initial groups=0x%08X", g_table.max_entries, initial_groups); } void sim_table_clear(void) { if (g_table.entries) { memset(g_table.entries, 0, g_table.count * sizeof(sim_table_entry_t)); } g_table.count = 0; g_table.active_groups = 0; } bool sim_table_add(const uint8_t *data, uint16_t len) { if (g_table.entries == NULL) return false; if (g_table.count >= g_table.max_entries) return false; if (len < sizeof(sim_table_entry_t)) return false; memcpy(&g_table.entries[g_table.count], data, sizeof(sim_table_entry_t)); g_table.count++; return true; } uint16_t sim_table_count(void) { return g_table.count; } uint32_t sim_table_get_active_groups(void) { return g_table.active_groups; } sim_table_entry_t *sim_table_lookup(const uint8_t *cmd, uint8_t cmd_len) { for (uint16_t i = 0; i < g_table.count; i++) { sim_table_entry_t *e = &g_table.entries[i]; // Skip empty entries (consumed one-shots) if (e->match_len == 0) continue; // Skip if entry's group is not active (group 0 = always active) if (e->group != 0 && !(g_table.active_groups & (1u << e->group))) { continue; } // Match bool matched = false; if (e->match_mode == SIM_MATCH_EXACT) { if (cmd_len == e->match_len && memcmp(cmd, e->match, e->match_len) == 0) { matched = true; } } else if (e->match_mode == SIM_MATCH_PREFIX) { if (cmd_len >= e->match_len && memcmp(cmd, e->match, e->match_len) == 0) { matched = true; } } if (!matched) continue; // Match found — apply state changes if (e->activate_groups) g_table.active_groups |= e->activate_groups; if (e->deactivate_groups) g_table.active_groups &= ~e->deactivate_groups; // Consume if one-shot if (e->flags & SIM_FLAG_CONSUME) { e->match_len = 0; // mark as empty } return e; } return NULL; } void sim_table_execute(sim_table_entry_t *e, const uint8_t *cmd, uint8_t cmd_len, uint8_t *resp_buf, uint16_t *resp_len, iso15_tag_t *tag) { // Apply auth state changes if (e->set_auth) tag->auth_state |= e->set_auth; if (e->clear_auth) tag->auth_state &= ~e->clear_auth; // Start with static response memcpy(resp_buf, e->response, e->response_len); *resp_len = e->response_len; switch (e->eml_action) { case SIM_EML_NONE: break; case SIM_EML_READ: { // Read from tag EML data into response uint16_t off = e->eml_offset; uint8_t len = e->eml_len; uint8_t ins = e->eml_resp_insert; if (off + len <= sizeof(tag->data)) { memcpy(resp_buf + ins, tag->data + off, len); if (ins + len > *resp_len) *resp_len = ins + len; } break; } case SIM_EML_WRITE: { // Write command data to tag EML uint8_t src_off = e->cmd_data_offset; uint8_t len = e->eml_len; uint16_t dst_off = e->eml_offset; if (src_off + len <= cmd_len && dst_off + len <= sizeof(tag->data)) { memcpy(tag->data + dst_off, cmd + src_off, len); } break; } case SIM_EML_AES_CMAC: { // Key from EML, challenge from command, MAC into response uint8_t key[16]; uint16_t key_off = e->eml_offset; if (key_off + 16 > sizeof(tag->data)) break; memcpy(key, tag->data + key_off, 16); uint8_t challenge_off = e->cmd_data_offset; uint8_t challenge_len = e->cmd_data_len; if (challenge_off + challenge_len > cmd_len) break; uint8_t mac_len = e->eml_len; uint8_t mac_ins = e->eml_resp_insert; sim_aes_cmac(key, cmd + challenge_off, challenge_len, resp_buf + mac_ins, mac_len); if (mac_ins + mac_len > *resp_len) *resp_len = mac_ins + mac_len; break; } } } ``` **Step 2: Add to Makefile** Add `sim_table.c \` after the `sim_crypto.c \` line added in Task 3. **Step 3: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 4: Commit** ```bash cd /home/work/proxmark3 git add armsrc/sim_table.c armsrc/Makefile git commit --no-gpg-sign -m "feat: add sim_table.c — stateful response table engine" ``` --- ### Task 6: Command dispatch in appmain.c **Files:** - Modify: `/home/work/proxmark3/armsrc/appmain.c` **Step 1: Add include** Near the top of `appmain.c`, with the other includes, add: ```c #include "sim_table.h" ``` **Step 2: Add dispatch cases** Find the `CMD_HF_SEOS_SIMULATE` case (around line 1571). After it, add: ```c case CMD_SIM_TABLE_UPLOAD: { // First chunk: init table, then add entries uint32_t initial_groups = 0xFFFFFFFF; // all groups active by default if (packet->length >= 4) { // First 4 bytes = initial_groups mask memcpy(&initial_groups, packet->data.asBytes, 4); } sim_table_init(initial_groups); // Remaining bytes = entries uint16_t data_off = 4; while (data_off + sizeof(sim_table_entry_t) <= packet->length) { sim_table_add(packet->data.asBytes + data_off, sizeof(sim_table_entry_t)); data_off += sizeof(sim_table_entry_t); } reply_ng(CMD_SIM_TABLE_UPLOAD, PM3_SUCCESS, NULL, 0); break; } case CMD_SIM_TABLE_CLEAR: { sim_table_clear(); reply_ng(CMD_SIM_TABLE_CLEAR, PM3_SUCCESS, NULL, 0); break; } case CMD_SIM_TABLE_UPDATE: { // Add single entry (or chunk of entries) uint16_t off = 0; while (off + sizeof(sim_table_entry_t) <= packet->length) { sim_table_add(packet->data.asBytes + off, sizeof(sim_table_entry_t)); off += sizeof(sim_table_entry_t); } reply_ng(CMD_SIM_TABLE_UPDATE, PM3_SUCCESS, NULL, 0); break; } ``` **Step 3: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 4: Commit** ```bash cd /home/work/proxmark3 git add armsrc/appmain.c git commit --no-gpg-sign -m "feat: dispatch CMD_SIM_TABLE_* in appmain" ``` --- ### Task 7: Native handler access control + random UID **Files:** - Modify: `/home/work/proxmark3/armsrc/iso15693.c` **Step 1: Add includes** Near the top of `iso15693.c`, add: ```c #include "sim_table.h" ``` **Step 2: Initialize new fields in SimTagIso15693** In `SimTagIso15693()`, after the existing field initializations (around line 2193, after `tag->trace_enabled`), add: ```c tag->random_uid = 0; tag->auth_state = 0; memset(tag->lock_mode, 0, sizeof(tag->lock_mode)); ``` **Step 3: Add random UID to inventory response** Find the inventory response block (around line 2412-2416): ```c // No error: Answer recv[0] = ISO15_NOERROR; recv[1] = tag->dsfid; memcpy(&recv[2], tag->uid, 8); recvLen = 10; ``` Replace with: ```c // No error: Answer recv[0] = ISO15_NOERROR; recv[1] = tag->dsfid; if (tag->random_uid) { // Privacy mode: return random UID for (int i = 0; i < 8; i++) recv[2 + i] = prand() & 0xFF; } else { memcpy(&recv[2], tag->uid, 8); } recvLen = 10; ``` Also do the same for the second inventory path (around line 2450-2453 in the `switch(cmd[1])` case `ISO15693_INVENTORY`): ```c case ISO15693_INVENTORY: if (g_dbglevel >= DBG_DEBUG) Dbprintf("Inventory cmd"); recv[0] = ISO15_NOERROR; recv[1] = tag->dsfid; if (tag->random_uid) { for (int i = 0; i < 8; i++) recv[2 + i] = prand() & 0xFF; } else { memcpy(&recv[2], tag->uid, 8); } recvLen = 10; break; ``` **Step 4: Add access control to READBLOCK** Find the `ISO15693_READBLOCK` case (around line 2459). After the `pageNum` extraction and before the existing bounds check, add access control: ```c case ISO15693_READBLOCK: if (g_dbglevel >= DBG_DEBUG) Dbprintf("ReadBlock cmd"); pageNum = cmd[cmdCpt++]; // Access control check if (pageNum < ISO15693_TAG_MAX_PAGES) { uint8_t lm = tag->lock_mode[pageNum]; if (lm == 0x01) { error = ISO15_ERROR_BLOCK_LOCKED; break; } if ((lm & 0x02) && !(tag->auth_state & 0x01)) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; break; } } if (pageNum >= tag->pagesCount) error = ISO15_ERROR_BLOCK_UNAVAILABLE; else { // ... existing read logic unchanged ... ``` **Step 5: Add access control to WRITEBLOCK** Find the `ISO15693_WRITEBLOCK` case (around line 2476). After `pageNum` extraction, add: ```c case ISO15693_WRITEBLOCK: if (g_dbglevel >= DBG_DEBUG) Dbprintf("WriteBlock cmd"); pageNum = cmd[cmdCpt++]; // Access control check if (pageNum < ISO15693_TAG_MAX_PAGES) { uint8_t lm = tag->lock_mode[pageNum]; if (lm == 0x01) { error = ISO15_ERROR_BLOCK_LOCKED; break; } if ((lm & 0x04) && !(tag->auth_state & 0x02)) { error = ISO15_ERROR_BLOCK_UNAVAILABLE; break; } } if (pageNum >= tag->pagesCount) error = ISO15_ERROR_BLOCK_UNAVAILABLE; else { // ... existing write logic unchanged ... ``` **Step 6: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 7: Commit** ```bash cd /home/work/proxmark3 git add armsrc/iso15693.c git commit --no-gpg-sign -m "feat: add access control + random UID to 15693 sim handler" ``` --- ### Task 8: Table fallback in default case **Files:** - Modify: `/home/work/proxmark3/armsrc/iso15693.c` **Step 1: Replace default case with table lookup + relay fallback** Find the `default:` case in the command switch (around line 2629). Replace the entire case body: ```c default: // --- Table lookup for custom/vendor commands --- if (sim_table_count() > 0) { sim_table_entry_t *entry = sim_table_lookup(cmd, cmd_len); if (entry) { sim_table_execute(entry, cmd, cmd_len, recv, &recvLen, tag); if (g_dbglevel >= DBG_DEBUG) Dbprintf("Table match for CMD 0x%02X, resp %d bytes", cmd[1], recvLen); break; } } // No table match — fall through to relay if (tag->relay_resp_ready && tag->relay_cmd_len == cmd_len && memcmp(tag->relay_cmd, cmd, cmd_len) == 0) { memcpy(recv, tag->relay_resp, tag->relay_resp_len); recvLen = tag->relay_resp_len; tag->relay_resp_ready = false; if (g_dbglevel >= DBG_DEBUG) Dbprintf("Serving cached relay response for CMD 0x%02X", cmd[1]); } else { memcpy(tag->relay_cmd, cmd, MIN(cmd_len, sizeof(tag->relay_cmd))); tag->relay_cmd_len = cmd_len; tag->relay_pending = true; tag->relay_resp_ready = false; reply_ng(CMD_HF_ISO15693_SIM_RELAY, PM3_SUCCESS, cmd, cmd_len); if (g_dbglevel >= DBG_DEBUG) Dbprintf("Relayed CMD 0x%02X to host (%d bytes)", cmd[1], cmd_len); recvLen = 0; } break; ``` **Step 2: Verify build** Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` Expected: Build succeeds **Step 3: Commit** ```bash cd /home/work/proxmark3 git add armsrc/iso15693.c git commit --no-gpg-sign -m "feat: table lookup before relay fallback in 15693 sim default case" ``` --- ### Task 9: Verify full build + flash size **Step 1: Clean build** Run: `cd /home/work/proxmark3 && make clean && make -j` Expected: Full build succeeds (client + firmware) **Step 2: Check firmware size** Run: `cd /home/work/proxmark3 && arm-none-eabi-size armsrc/obj/fullimage.elf` Expected: text section fits in 512KB flash. Note the size for comparison with upstream. **Step 3: Commit (if any Makefile adjustments needed)** If build revealed issues, fix and commit. Otherwise, this task is just verification.