From 2d60989beecec25f3423a2dbba9e49842d358195 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Sun, 8 Mar 2026 19:03:30 -0700 Subject: [PATCH 1/2] Add CCID helpers and regression host tests --- Makefile | 3 + ccid.c | 16 +- ccid_logic.c | 94 ++++++++ ccid_logic.h | 50 +++++ lib/host_tests/t_1_host_env.h | 7 + lib/host_tests/test_ccid_logic.c | 222 +++++++++++++++++++ lib/host_tests/test_main.c | 4 + lib/host_tests/test_t1_regressions.c | 308 +++++++++++++++++++++++++++ 8 files changed, 693 insertions(+), 11 deletions(-) create mode 100644 ccid_logic.c create mode 100644 ccid_logic.h create mode 100644 lib/host_tests/test_ccid_logic.c create mode 100644 lib/host_tests/test_t1_regressions.c diff --git a/Makefile b/Makefile index 9723f34..d5c4d6b 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,13 @@ test-host: lib/host_tests/vendor/munit/munit.c \ lib/host_tests/test_main.c \ lib/host_tests/test_lrc.c \ + lib/host_tests/test_ccid_logic.c \ lib/host_tests/test_t1_existing.c \ + lib/host_tests/test_t1_regressions.c \ lib/host_tests/t1_test_stubs.c \ lib/host_tests/bit_buffer_mock.c \ lrc.c \ + ccid_logic.c \ t_1.c \ -o build/host_tests/seader_tests ./build/host_tests/seader_tests diff --git a/ccid.c b/ccid.c index 5d52055..1e6cd1f 100644 --- a/ccid.c +++ b/ccid.c @@ -1,4 +1,5 @@ #include "seader_i.h" +#include "ccid_logic.h" #define TAG "SeaderCCID" const uint8_t SAM_ATR[] = @@ -26,10 +27,7 @@ static void seader_ccid_reset_slot_sequence(SeaderUartBridge* seader_uart, uint8 static uint8_t seader_ccid_next_sequence(SeaderUartBridge* seader_uart, uint8_t slot) { SeaderCcidSlotState* slot_state = seader_ccid_slot_state(seader_uart, slot); - if(slot_state->sequence > 254) { - slot_state->sequence = 0; - } - return slot_state->sequence++; + return seader_ccid_sequence_advance(&slot_state->sequence); } void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart, uint8_t slot) { @@ -175,19 +173,15 @@ void seader_ccid_XfrBlockToSlot( uint8_t* data, size_t len) { uint8_t header_len = 2 + 10; - if(len > ((size_t)SEADER_UART_RX_BUF_SIZE - header_len)) { + if(!seader_ccid_payload_fits_frame(len, SEADER_UART_RX_BUF_SIZE, header_len)) { FURI_LOG_E(TAG, "CCID frame too long: %d", (int)(header_len + len)); return; } uint8_t* tx_start = (uint8_t*)seader_uart->tx_buf; - uint8_t* tx_end = tx_start + SEADER_UART_RX_BUF_SIZE; uint8_t* data_addr = (uint8_t*)data; - bool in_scratchpad = false; - if(data_addr >= tx_start + header_len && data_addr <= tx_end) { - size_t available = (size_t)(tx_end - data_addr); - in_scratchpad = len <= available; - } + bool in_scratchpad = seader_ccid_data_in_scratchpad( + tx_start, SEADER_UART_RX_BUF_SIZE, header_len, data_addr, len); uint8_t* frame; if(in_scratchpad) { diff --git a/ccid_logic.c b/ccid_logic.c new file mode 100644 index 0000000..955652f --- /dev/null +++ b/ccid_logic.c @@ -0,0 +1,94 @@ +#include "ccid_logic.h" + +uint8_t seader_ccid_sequence_advance(uint8_t* sequence) { + return (*sequence)++; +} + +bool seader_ccid_payload_fits_frame(size_t payload_len, size_t uart_buf_size, size_t header_len) { + if(header_len > uart_buf_size) { + return false; + } + return payload_len <= (uart_buf_size - header_len); +} + +bool seader_ccid_data_in_scratchpad( + const uint8_t* tx_buf, + size_t tx_buf_size, + size_t header_len, + const uint8_t* data, + size_t payload_len) { + if(data < tx_buf + header_len || data > tx_buf + tx_buf_size) { + return false; + } + + size_t available = (size_t)((tx_buf + tx_buf_size) - data); + return payload_len <= available; +} + +SeaderCcidStatus seader_ccid_decode_status(uint8_t status) { + SeaderCcidStatus decoded = { + .icc_status = status & 0x03, + .command_status = (SeaderCcidDecodedCommandStatus)((status >> 6) & 0x03), + }; + return decoded; +} + +bool seader_ccid_response_matches_pending(bool pending, uint8_t expected_seq, uint8_t response_seq) { + return !pending || (expected_seq == response_seq); +} + +size_t seader_ccid_find_frame_start( + const uint8_t* data, + size_t len, + uint8_t sync, + uint8_t ctrl, + uint8_t nak) { + size_t i = 0; + + while(i + 1 < len) { + if(i + 2 < len && data[i] == sync && data[i + 1] == nak) { + i += 3; + continue; + } + + if(data[i] == sync && data[i + 1] == ctrl) { + return i; + } + + i++; + } + + return len; +} + +bool seader_ccid_pending_timed_out( + bool pending, + uint32_t pending_since_tick, + uint32_t now_tick, + uint32_t timeout_ticks) { + if(!pending || timeout_ticks == 0 || pending_since_tick == 0) { + return false; + } + + return (now_tick - pending_since_tick) > timeout_ticks; +} + +SeaderCcidDataRoute seader_ccid_route_data_block( + bool has_sam, + uint8_t sam_slot, + uint8_t message_slot, + uint8_t protocol_t) { + if(!has_sam) { + return SeaderCcidDataRouteAtrRecognition; + } + + if(message_slot != sam_slot) { + return SeaderCcidDataRouteWrongSlotError; + } + + if(protocol_t == 0) { + return SeaderCcidDataRouteSamT0; + } + + return SeaderCcidDataRouteSamT1; +} diff --git a/ccid_logic.h b/ccid_logic.h new file mode 100644 index 0000000..f5d6c07 --- /dev/null +++ b/ccid_logic.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +typedef enum { + SeaderCcidDecodedCommandStatusProcessed = 0, + SeaderCcidDecodedCommandStatusFailed = 1, + SeaderCcidDecodedCommandStatusTimeExtension = 2, +} SeaderCcidDecodedCommandStatus; + +typedef struct { + uint8_t icc_status; + SeaderCcidDecodedCommandStatus command_status; +} SeaderCcidStatus; + +typedef enum { + SeaderCcidDataRouteSamT0 = 0, + SeaderCcidDataRouteSamT1 = 1, + SeaderCcidDataRouteAtrRecognition = 2, + SeaderCcidDataRouteWrongSlotError = 3, +} SeaderCcidDataRoute; + +uint8_t seader_ccid_sequence_advance(uint8_t* sequence); +bool seader_ccid_payload_fits_frame(size_t payload_len, size_t uart_buf_size, size_t header_len); +bool seader_ccid_data_in_scratchpad( + const uint8_t* tx_buf, + size_t tx_buf_size, + size_t header_len, + const uint8_t* data, + size_t payload_len); +SeaderCcidStatus seader_ccid_decode_status(uint8_t status); +bool seader_ccid_response_matches_pending(bool pending, uint8_t expected_seq, uint8_t response_seq); +size_t seader_ccid_find_frame_start( + const uint8_t* data, + size_t len, + uint8_t sync, + uint8_t ctrl, + uint8_t nak); +bool seader_ccid_pending_timed_out( + bool pending, + uint32_t pending_since_tick, + uint32_t now_tick, + uint32_t timeout_ticks); +SeaderCcidDataRoute seader_ccid_route_data_block( + bool has_sam, + uint8_t sam_slot, + uint8_t message_slot, + uint8_t protocol_t); diff --git a/lib/host_tests/t_1_host_env.h b/lib/host_tests/t_1_host_env.h index 2bc0333..ba0cb99 100644 --- a/lib/host_tests/t_1_host_env.h +++ b/lib/host_tests/t_1_host_env.h @@ -31,16 +31,23 @@ typedef enum { SEADER_T1_PCB_R_BLOCK = 0x80, SEADER_T1_PCB_S_BLOCK = 0xC0, SEADER_T1_R_BLOCK_SEQUENCE_MASK = 0x10, + SEADER_T1_S_BLOCK_RESPONSE_BIT = 0x20, + SEADER_T1_S_BLOCK_RESYNCH = 0x00, SEADER_T1_S_BLOCK_IFS = 0x01, + SEADER_T1_S_BLOCK_ABORT = 0x02, + SEADER_T1_S_BLOCK_WTX = 0x03, } SeaderT1Constant; typedef struct { uint8_t ifsc; + uint8_t ifsd; + uint8_t ifsd_pending; uint8_t nad; uint8_t send_pcb; uint8_t recv_pcb; BitBuffer* tx_buffer; size_t tx_buffer_offset; + size_t last_tx_len; BitBuffer* rx_buffer; } SeaderT1State; diff --git a/lib/host_tests/test_ccid_logic.c b/lib/host_tests/test_ccid_logic.c new file mode 100644 index 0000000..14d15ad --- /dev/null +++ b/lib/host_tests/test_ccid_logic.c @@ -0,0 +1,222 @@ +#include + +#include "ccid_logic.h" +#include "munit.h" + +static MunitResult test_sequence_advance_wraps(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + uint8_t seq = 0; + munit_assert_uint8(seader_ccid_sequence_advance(&seq), ==, 0); + munit_assert_uint8(seq, ==, 1); + + seq = 254; + munit_assert_uint8(seader_ccid_sequence_advance(&seq), ==, 254); + munit_assert_uint8(seq, ==, 255); + + seq = 255; + munit_assert_uint8(seader_ccid_sequence_advance(&seq), ==, 255); + munit_assert_uint8(seq, ==, 0); + return MUNIT_OK; +} + +static MunitResult test_payload_fits_buffer(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_true(seader_ccid_payload_fits_frame(10, 300, 12)); + munit_assert_false(seader_ccid_payload_fits_frame(289, 300, 12)); + munit_assert_true(seader_ccid_payload_fits_frame(288, 300, 12)); + return MUNIT_OK; +} + +static MunitResult test_data_in_scratchpad(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + uint8_t tx[64] = {0}; + munit_assert_true(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 12, 1)); + munit_assert_true(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 63, 1)); + munit_assert_false(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 11, 1)); + munit_assert_false(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 60, 8)); + return MUNIT_OK; +} + +static MunitResult test_response_seq_match(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_true(seader_ccid_response_matches_pending(false, 0x00, 0xff)); + munit_assert_true(seader_ccid_response_matches_pending(true, 0x12, 0x12)); + munit_assert_false(seader_ccid_response_matches_pending(true, 0x12, 0x13)); + return MUNIT_OK; +} + +static MunitResult test_status_decode_ok(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderCcidStatus status_ok = seader_ccid_decode_status(0x00); + munit_assert_uint8(status_ok.icc_status, ==, 0); + munit_assert_int(status_ok.command_status, ==, SeaderCcidDecodedCommandStatusProcessed); + return MUNIT_OK; +} + +static MunitResult test_status_decode_failed(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderCcidStatus status_fail = seader_ccid_decode_status(0x41); + munit_assert_uint8(status_fail.icc_status, ==, 1); + munit_assert_int(status_fail.command_status, ==, SeaderCcidDecodedCommandStatusFailed); + return MUNIT_OK; +} + +static MunitResult test_status_decode_time_extension(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderCcidStatus status_wait = seader_ccid_decode_status(0x80); + munit_assert_uint8(status_wait.icc_status, ==, 0); + munit_assert_int( + status_wait.command_status, ==, SeaderCcidDecodedCommandStatusTimeExtension); + return MUNIT_OK; +} + +static MunitResult test_find_start_skips_nak_triplet(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + const uint8_t sync = 0x03; + const uint8_t ctrl = 0x06; + const uint8_t nak = 0x15; + const uint8_t frame_stream[] = { + 0x03, + 0x15, + 0x16, + 0x99, + 0x03, + 0x06, + 0x80, + }; + + munit_assert_size( + seader_ccid_find_frame_start(frame_stream, sizeof(frame_stream), sync, ctrl, nak), ==, 4); + return MUNIT_OK; +} + +static MunitResult test_find_start_noise_only(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + const uint8_t sync = 0x03; + const uint8_t ctrl = 0x06; + const uint8_t nak = 0x15; + const uint8_t noise_only[] = {0x01, 0x02, 0x03}; + + munit_assert_size( + seader_ccid_find_frame_start(noise_only, sizeof(noise_only), sync, ctrl, nak), + ==, + sizeof(noise_only)); + return MUNIT_OK; +} + +static MunitResult test_pending_timeout_helper(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_false(seader_ccid_pending_timed_out(false, 100, 200, 50)); + munit_assert_false(seader_ccid_pending_timed_out(true, 0, 200, 50)); + munit_assert_false(seader_ccid_pending_timed_out(true, 100, 149, 50)); + munit_assert_false(seader_ccid_pending_timed_out(true, 100, 150, 50)); + munit_assert_true(seader_ccid_pending_timed_out(true, 100, 151, 50)); + munit_assert_false(seader_ccid_pending_timed_out(true, 100, 200, 0)); + return MUNIT_OK; +} + +static MunitResult test_data_block_route(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_int(seader_ccid_route_data_block(true, 0, 0, 0), ==, SeaderCcidDataRouteSamT0); + munit_assert_int(seader_ccid_route_data_block(true, 0, 0, 1), ==, SeaderCcidDataRouteSamT1); + munit_assert_int( + seader_ccid_route_data_block(false, 0, 0, 1), ==, SeaderCcidDataRouteAtrRecognition); + munit_assert_int( + seader_ccid_route_data_block(true, 0, 1, 1), ==, SeaderCcidDataRouteWrongSlotError); + return MUNIT_OK; +} + +static MunitTest test_ccid_cases[] = { + {(char*)"/sequence/advance-wraps-through-ff", + test_sequence_advance_wraps, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/frame/payload-fits-buffer", + test_payload_fits_buffer, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/frame/data-in-scratchpad", + test_data_in_scratchpad, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/pending/response-seq-match", + test_response_seq_match, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/status/decode-ok", test_status_decode_ok, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/status/decode-failed", + test_status_decode_failed, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/status/decode-time-extension", + test_status_decode_time_extension, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/frame/find-start-skips-nak-triplet", + test_find_start_skips_nak_triplet, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/frame/find-start-noise-only", + test_find_start_noise_only, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/pending/timeout-helper", + test_pending_timeout_helper, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/routing/data-block-route", + test_data_block_route, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_ccid_logic_suite = { + "", + test_ccid_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_main.c b/lib/host_tests/test_main.c index 504c5a8..3cef2f0 100644 --- a/lib/host_tests/test_main.c +++ b/lib/host_tests/test_main.c @@ -1,12 +1,16 @@ #include "munit.h" extern MunitSuite test_lrc_suite; +extern MunitSuite test_ccid_logic_suite; extern MunitSuite test_t1_existing_suite; +extern MunitSuite test_t1_regressions_suite; int main(int argc, char* argv[]) { MunitSuite child_suites[] = { {"/lrc", test_lrc_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/t1", test_t1_existing_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/t1-future", test_t1_regressions_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/ccid", test_ccid_logic_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {NULL, NULL, NULL, 0, 0}, }; MunitSuite main_suite = { diff --git a/lib/host_tests/test_t1_regressions.c b/lib/host_tests/test_t1_regressions.c new file mode 100644 index 0000000..0515786 --- /dev/null +++ b/lib/host_tests/test_t1_regressions.c @@ -0,0 +1,308 @@ +#include + +#include "munit.h" +#include "t_1_host_env.h" + +static void test_callback(uint32_t event, void* context) { + (void)context; + g_t1_host_test_state.callback_call_count++; + g_t1_host_test_state.last_callback_event = event; +} + +static Seader make_test_seader(SeaderUartBridge* uart, SeaderWorker* worker) { + memset(uart, 0, sizeof(*uart)); + memset(worker, 0, sizeof(*worker)); + worker->uart = uart; + worker->callback = test_callback; + + Seader seader = {.worker = worker}; + return seader; +} + +static MunitResult test_recv_wtx_request_responds(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uint8_t s_wtx_req[] = {0x00, 0xC3, 0x01, 0x02, 0x00}; + seader_add_lrc(s_wtx_req, 4); + CCID_Message message = {.payload = s_wtx_req, .dwLength = 5}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0xE3); + return MUNIT_OK; +} + +static MunitResult test_recv_resynch_request_responds(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uint8_t s_resync_req[] = {0x00, 0xC0, 0x00, 0x00}; + seader_add_lrc(s_resync_req, 3); + CCID_Message message = {.payload = s_resync_req, .dwLength = 4}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0xE0); + return MUNIT_OK; +} + +static MunitResult test_recv_malformed_wtx_rejected(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uint8_t s_wtx_bad[] = {0x00, 0xC3, 0x00, 0x00}; + seader_add_lrc(s_wtx_bad, 3); + CCID_Message message = {.payload = s_wtx_bad, .dwLength = 4}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0x81); + return MUNIT_OK; +} + +static MunitResult test_recv_malformed_ifs_rejected(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uint8_t s_ifs_bad[] = {0x00, 0xC1, 0x00, 0x00}; + seader_add_lrc(s_ifs_bad, 3); + CCID_Message message = {.payload = s_ifs_bad, .dwLength = 4}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0x81); + return MUNIT_OK; +} + +static MunitResult test_recv_ifs_request_updates_ifsc(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.ifsc = 0x20; + uint8_t s_ifs_req[] = {0x00, 0xC1, 0x01, 0x40, 0x00}; + seader_add_lrc(s_ifs_req, 4); + CCID_Message message = {.payload = s_ifs_req, .dwLength = 5}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[3], ==, 0x40); + return MUNIT_OK; +} + +static MunitResult test_recv_ifs_response_applies_pending_ifsd( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.ifsd = 0x10; + uart.t1.ifsd_pending = 0x20; + uint8_t s_ifs_res[] = {0x00, 0xE1, 0x01, 0x20, 0x00}; + seader_add_lrc(s_ifs_res, 4); + CCID_Message message = {.payload = s_ifs_res, .dwLength = 5}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_uint8(uart.t1.ifsd, ==, 0x20); + munit_assert_uint8(uart.t1.ifsd_pending, ==, 0x00); + return MUNIT_OK; +} + +static MunitResult test_recv_ifs_response_mismatch_rejected( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.ifsd_pending = 0x20; + uint8_t s_ifs_res_bad[] = {0x00, 0xE1, 0x01, 0x30, 0x00}; + seader_add_lrc(s_ifs_res_bad, 4); + CCID_Message message = {.payload = s_ifs_res_bad, .dwLength = 5}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.send_version_call_count, ==, 0); + munit_assert_size(g_t1_host_test_state.callback_call_count, ==, 0); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0x81); + return MUNIT_OK; +} + +static MunitResult test_recv_i_block_too_large_rejected(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.ifsd = 2; + uart.t1.recv_pcb = 0x00; + uint8_t i_block[] = {0x00, 0x00, 0x03, 0xAA, 0xBB, 0xCC, 0x00}; + seader_add_lrc(i_block, 6); + CCID_Message message = {.payload = i_block, .dwLength = 7}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.process_call_count, ==, 0); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0x81); + return MUNIT_OK; +} + +static MunitResult test_recv_r_block_nack_retransmits(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.send_pcb = 0x00; + uart.t1.ifsc = 4; + uart.t1.tx_buffer = bit_buffer_alloc(8); + bit_buffer_copy_bytes(uart.t1.tx_buffer, (const uint8_t*)"\xA0\xA1\xA2", 3); + uart.t1.tx_buffer_offset = 2; + uart.t1.last_tx_len = 2; + uint8_t r_block[] = {0x00, 0x80, 0x00, 0x00}; + seader_add_lrc(r_block, 3); + CCID_Message message = {.payload = r_block, .dwLength = 4}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); + munit_assert_uint8(uart.t1.send_pcb, ==, SEADER_T1_PCB_SEQUENCE_BIT); + bit_buffer_free(uart.t1.tx_buffer); + return MUNIT_OK; +} + +static MunitResult test_recv_r_block_invalid_retransmit_state_errors( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + SeaderUartBridge uart = {0}; + SeaderWorker worker = {0}; + Seader seader = make_test_seader(&uart, &worker); + + t1_host_test_reset(); + uart.t1.send_pcb = 0x00; + uart.t1.tx_buffer = bit_buffer_alloc(8); + uart.t1.tx_buffer_offset = 0; + uart.t1.last_tx_len = 2; + uint8_t r_block[] = {0x00, 0x91, 0x00, 0x00}; + seader_add_lrc(r_block, 3); + CCID_Message message = {.payload = r_block, .dwLength = 4}; + + munit_assert_false(seader_recv_t1(&seader, &message)); + munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 0); + bit_buffer_free(uart.t1.tx_buffer); + return MUNIT_OK; +} + +static MunitTest test_t1_regression_cases[] = { + {(char*)"/recv/wtx-request-responds", + test_recv_wtx_request_responds, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/resynch-request-responds", + test_recv_resynch_request_responds, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/malformed-wtx-len-zero-rejected", + test_recv_malformed_wtx_rejected, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/malformed-ifs-len-zero-rejected", + test_recv_malformed_ifs_rejected, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/ifs-request-updates-ifsc", + test_recv_ifs_request_updates_ifsc, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/ifs-response-applies-pending-ifsd", + test_recv_ifs_response_applies_pending_ifsd, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/ifs-response-mismatch-rejected", + test_recv_ifs_response_mismatch_rejected, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/i-block-too-large-rejected", + test_recv_i_block_too_large_rejected, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/r-block-nack-retransmits", + test_recv_r_block_nack_retransmits, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {(char*)"/recv/r-block-invalid-retransmit-state-errors", + test_recv_r_block_invalid_retransmit_state_errors, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_t1_regressions_suite = { + "", + test_t1_regression_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; From 7d93a75f5bbbc844efa13cc6e6b9820bda0a2f70 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Sun, 8 Mar 2026 19:26:33 -0700 Subject: [PATCH 2/2] Incorporate t=1 and CCID fixes and conformance enhancements --- Makefile | 3 +- lib/host_tests/t_1_host_env.h | 31 +- lib/host_tests/test_ccid_logic.c | 11 + lib/host_tests/test_lrc.c | 3 + lib/host_tests/test_main.c | 4 +- lib/host_tests/test_t1_existing.c | 26 +- ...st_t1_regressions.c => test_t1_protocol.c} | 38 +-- seader_bridge.h | 16 +- t_1.c | 308 +++++++++--------- t_1.h | 10 +- t_1_logic.c | 200 ++++++++++++ t_1_logic.h | 73 +++++ 12 files changed, 485 insertions(+), 238 deletions(-) rename lib/host_tests/{test_t1_regressions.c => test_t1_protocol.c} (89%) create mode 100644 t_1_logic.c create mode 100644 t_1_logic.h diff --git a/Makefile b/Makefile index d5c4d6b..8d40d2d 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,12 @@ test-host: lib/host_tests/test_lrc.c \ lib/host_tests/test_ccid_logic.c \ lib/host_tests/test_t1_existing.c \ - lib/host_tests/test_t1_regressions.c \ + lib/host_tests/test_t1_protocol.c \ lib/host_tests/t1_test_stubs.c \ lib/host_tests/bit_buffer_mock.c \ lrc.c \ ccid_logic.c \ + t_1_logic.c \ t_1.c \ -o build/host_tests/seader_tests ./build/host_tests/seader_tests diff --git a/lib/host_tests/t_1_host_env.h b/lib/host_tests/t_1_host_env.h index ba0cb99..78fd953 100644 --- a/lib/host_tests/t_1_host_env.h +++ b/lib/host_tests/t_1_host_env.h @@ -9,6 +9,7 @@ #include "bit_buffer.h" #include "lrc.h" +#include "t_1_logic.h" /* Keep the host harness aligned with the production UART scratchpad size. */ #define SEADER_UART_RX_BUF_SIZE (300) @@ -19,38 +20,10 @@ typedef struct Seader Seader; typedef struct SeaderWorker SeaderWorker; typedef struct SeaderUartBridge SeaderUartBridge; -typedef enum { - SeaderWorkerEventSamPresent = 53, -} SeaderWorkerEvent; +typedef enum { SeaderWorkerEventSamPresent = 53 } SeaderWorkerEvent; typedef void (*SeaderWorkerCallback)(uint32_t event, void* context); -typedef enum { - SEADER_T1_PCB_I_BLOCK_MORE = 0x20, - SEADER_T1_PCB_SEQUENCE_BIT = 0x40, - SEADER_T1_PCB_R_BLOCK = 0x80, - SEADER_T1_PCB_S_BLOCK = 0xC0, - SEADER_T1_R_BLOCK_SEQUENCE_MASK = 0x10, - SEADER_T1_S_BLOCK_RESPONSE_BIT = 0x20, - SEADER_T1_S_BLOCK_RESYNCH = 0x00, - SEADER_T1_S_BLOCK_IFS = 0x01, - SEADER_T1_S_BLOCK_ABORT = 0x02, - SEADER_T1_S_BLOCK_WTX = 0x03, -} SeaderT1Constant; - -typedef struct { - uint8_t ifsc; - uint8_t ifsd; - uint8_t ifsd_pending; - uint8_t nad; - uint8_t send_pcb; - uint8_t recv_pcb; - BitBuffer* tx_buffer; - size_t tx_buffer_offset; - size_t last_tx_len; - BitBuffer* rx_buffer; -} SeaderT1State; - struct SeaderUartBridge { uint8_t rx_buf[SEADER_UART_RX_BUF_SIZE]; uint8_t tx_buf[SEADER_UART_RX_BUF_SIZE]; diff --git a/lib/host_tests/test_ccid_logic.c b/lib/host_tests/test_ccid_logic.c index 14d15ad..52fde88 100644 --- a/lib/host_tests/test_ccid_logic.c +++ b/lib/host_tests/test_ccid_logic.c @@ -7,6 +7,7 @@ static MunitResult test_sequence_advance_wraps(const MunitParameter params[], vo (void)params; (void)fixture; + /* CCID says bSeq "is a monotonically increasing by one counter" and "rolls over to 00h after FFh." */ uint8_t seq = 0; munit_assert_uint8(seader_ccid_sequence_advance(&seq), ==, 0); munit_assert_uint8(seq, ==, 1); @@ -25,6 +26,7 @@ static MunitResult test_payload_fits_buffer(const MunitParameter params[], void* (void)params; (void)fixture; + /* CCID says XfrBlock "should never exceed the dwMaxCCIDMessageLength-10" because the header is 10 bytes. */ munit_assert_true(seader_ccid_payload_fits_frame(10, 300, 12)); munit_assert_false(seader_ccid_payload_fits_frame(289, 300, 12)); munit_assert_true(seader_ccid_payload_fits_frame(288, 300, 12)); @@ -35,6 +37,7 @@ static MunitResult test_data_in_scratchpad(const MunitParameter params[], void* (void)params; (void)fixture; + /* CCID says the 10-byte header provides "a constant offset at which message data begins across all messages." */ uint8_t tx[64] = {0}; munit_assert_true(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 12, 1)); munit_assert_true(seader_ccid_data_in_scratchpad(tx, sizeof(tx), 12, tx + 63, 1)); @@ -47,6 +50,7 @@ static MunitResult test_response_seq_match(const MunitParameter params[], void* (void)params; (void)fixture; + /* CCID says responses use "the exact same sequence number contained in the command". */ munit_assert_true(seader_ccid_response_matches_pending(false, 0x00, 0xff)); munit_assert_true(seader_ccid_response_matches_pending(true, 0x12, 0x12)); munit_assert_false(seader_ccid_response_matches_pending(true, 0x12, 0x13)); @@ -57,6 +61,7 @@ static MunitResult test_status_decode_ok(const MunitParameter params[], void* fi (void)params; (void)fixture; + /* CCID says bmCommandStatus 0 means "Processed without error". */ SeaderCcidStatus status_ok = seader_ccid_decode_status(0x00); munit_assert_uint8(status_ok.icc_status, ==, 0); munit_assert_int(status_ok.command_status, ==, SeaderCcidDecodedCommandStatusProcessed); @@ -67,6 +72,7 @@ static MunitResult test_status_decode_failed(const MunitParameter params[], void (void)params; (void)fixture; + /* CCID says bmCommandStatus 1 means "Failed" and the slot error register carries the error code. */ SeaderCcidStatus status_fail = seader_ccid_decode_status(0x41); munit_assert_uint8(status_fail.icc_status, ==, 1); munit_assert_int(status_fail.command_status, ==, SeaderCcidDecodedCommandStatusFailed); @@ -77,6 +83,7 @@ static MunitResult test_status_decode_time_extension(const MunitParameter params (void)params; (void)fixture; + /* CCID says bmCommandStatus 2 means "Time Extension is requested". */ SeaderCcidStatus status_wait = seader_ccid_decode_status(0x80); munit_assert_uint8(status_wait.icc_status, ==, 0); munit_assert_int( @@ -88,6 +95,7 @@ static MunitResult test_find_start_skips_nak_triplet(const MunitParameter params (void)params; (void)fixture; + /* Bridge framing invariant: skip the serial NAK triplet before looking for the next sync/control framed packet. */ const uint8_t sync = 0x03; const uint8_t ctrl = 0x06; const uint8_t nak = 0x15; @@ -110,6 +118,7 @@ static MunitResult test_find_start_noise_only(const MunitParameter params[], voi (void)params; (void)fixture; + /* The bridge must not invent a CCID frame start when the sync/control header is incomplete. */ const uint8_t sync = 0x03; const uint8_t ctrl = 0x06; const uint8_t nak = 0x15; @@ -126,6 +135,7 @@ static MunitResult test_pending_timeout_helper(const MunitParameter params[], vo (void)params; (void)fixture; + /* CCID says when Time Extension is set the host should "wait for a new response with the same bSeq number". */ munit_assert_false(seader_ccid_pending_timed_out(false, 100, 200, 50)); munit_assert_false(seader_ccid_pending_timed_out(true, 0, 200, 50)); munit_assert_false(seader_ccid_pending_timed_out(true, 100, 149, 50)); @@ -139,6 +149,7 @@ static MunitResult test_data_block_route(const MunitParameter params[], void* fi (void)params; (void)fixture; + /* CCID says "bSlot identifies which ICC slot is being addressed"; protocol 00h is T=0 and 01h is T=1. */ munit_assert_int(seader_ccid_route_data_block(true, 0, 0, 0), ==, SeaderCcidDataRouteSamT0); munit_assert_int(seader_ccid_route_data_block(true, 0, 0, 1), ==, SeaderCcidDataRouteSamT1); munit_assert_int( diff --git a/lib/host_tests/test_lrc.c b/lib/host_tests/test_lrc.c index 23c3460..16f229b 100644 --- a/lib/host_tests/test_lrc.c +++ b/lib/host_tests/test_lrc.c @@ -7,6 +7,7 @@ static MunitResult test_calc_lrc(const MunitParameter params[], void* fixture) { (void)params; (void)fixture; + /* ISO 7816-3 says "exclusive-oring all the bytes of the block from NAD to LRC inclusive shall give '00'." */ uint8_t bytes[] = {0x03, 0x06, 0x62, 0x00}; munit_assert_uint8( seader_calc_lrc(bytes, sizeof(bytes)), ==, (uint8_t)(0x03 ^ 0x06 ^ 0x62 ^ 0x00)); @@ -17,6 +18,7 @@ static MunitResult test_add_and_validate_lrc(const MunitParameter params[], void (void)params; (void)fixture; + /* ISO 7816-3 says "Any other value is invalid", so flipping one byte must break validation. */ uint8_t frame[8] = {0x03, 0x06, 0x62, 0x00, 0x01, 0x02, 0x00, 0x00}; size_t len = seader_add_lrc(frame, 6); munit_assert_size(len, ==, 7); @@ -31,6 +33,7 @@ static MunitResult test_validate_nak_triplet(const MunitParameter params[], void (void)params; (void)fixture; + /* Bridge framing invariant: SYNC, NAK, and XOR checksum form the transport-side NAK triplet. */ uint8_t nak_triplet[] = {0x03, 0x15, 0x16}; munit_assert_true(seader_validate_lrc(nak_triplet, 3)); return MUNIT_OK; diff --git a/lib/host_tests/test_main.c b/lib/host_tests/test_main.c index 3cef2f0..afbf0fe 100644 --- a/lib/host_tests/test_main.c +++ b/lib/host_tests/test_main.c @@ -3,13 +3,13 @@ extern MunitSuite test_lrc_suite; extern MunitSuite test_ccid_logic_suite; extern MunitSuite test_t1_existing_suite; -extern MunitSuite test_t1_regressions_suite; +extern MunitSuite test_t1_protocol_suite; int main(int argc, char* argv[]) { MunitSuite child_suites[] = { {"/lrc", test_lrc_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/t1", test_t1_existing_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, - {"/t1-future", test_t1_regressions_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/t1", test_t1_protocol_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/ccid", test_ccid_logic_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {NULL, NULL, NULL, 0, 0}, }; diff --git a/lib/host_tests/test_t1_existing.c b/lib/host_tests/test_t1_existing.c index 8f0da8f..085efce 100644 --- a/lib/host_tests/test_t1_existing.c +++ b/lib/host_tests/test_t1_existing.c @@ -15,6 +15,9 @@ static Seader make_test_seader(SeaderUartBridge* uart, SeaderWorker* worker) { memset(worker, 0, sizeof(*worker)); worker->uart = uart; worker->callback = test_callback; + uart->t1.ifsc = SEADER_T1_IFS_DEFAULT; + uart->t1.ifsd = SEADER_T1_IFS_DEFAULT; + uart->t1.send_pcb = SEADER_T1_PCB_SEQUENCE_BIT; Seader seader = {.worker = worker}; return seader; @@ -34,6 +37,7 @@ static MunitResult test_send_ifs_request(const MunitParameter params[], void* fi (void)params; (void)fixture; + /* ISO 7816-3 says "The interface device transmits S(IFS request) to indicate a new IFSD it can support." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -44,9 +48,10 @@ static MunitResult test_send_ifs_request(const MunitParameter params[], void* fi uart.t1.send_pcb = SEADER_T1_PCB_SEQUENCE_BIT; seader_t_1_set_IFSD(&seader); + /* ISO 7816-3 says "The values '01' to 'FE' encode the numbers 1 to 254", so the request uses 0xFE. */ munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); munit_assert_size(g_t1_host_test_state.last_frame_len, ==, 5); - munit_assert_true(last_frame_prefix_matches((const uint8_t*)"\x00\xC1\x01\x20", 4)); + munit_assert_true(last_frame_prefix_matches((const uint8_t*)"\x00\xC1\x01\xFE", 4)); munit_assert_true( seader_validate_lrc(g_t1_host_test_state.last_frame, g_t1_host_test_state.last_frame_len)); return MUNIT_OK; @@ -56,6 +61,7 @@ static MunitResult test_send_single_block(const MunitParameter params[], void* f (void)params; (void)fixture; + /* ISO 7816-3 says "The command APDU is mapped onto the information field of an I-block without any change." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; make_test_seader(&uart, &worker); @@ -78,6 +84,7 @@ static MunitResult test_send_scratchpad_block(const MunitParameter params[], voi (void)params; (void)fixture; + /* ISO 7816-3 still requires the same I-block image even when the payload already sits in our scratchpad. */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; make_test_seader(&uart, &worker); @@ -99,6 +106,7 @@ static MunitResult test_send_chained_block(const MunitParameter params[], void* (void)params; (void)fixture; + /* ISO 7816-3 says information longer than IFSC/IFSD "should divide the information into pieces". */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; make_test_seader(&uart, &worker); @@ -109,6 +117,7 @@ static MunitResult test_send_chained_block(const MunitParameter params[], void* uint8_t apdu_long[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; seader_send_t1(&uart, apdu_long, sizeof(apdu_long)); + /* ISO 7816-3 says "I(N(S), 1) is a part of a chain and shall be followed by at least one chained block." */ munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); munit_assert_uint8( g_t1_host_test_state.last_frame[1], ==, (SEADER_T1_PCB_I_BLOCK_MORE | 0x00)); @@ -124,6 +133,7 @@ static MunitResult test_recv_ifs_response(const MunitParameter params[], void* f (void)params; (void)fixture; + /* ISO 7816-3 says "The card shall acknowledge by S(IFS response) with the same INF." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -144,17 +154,20 @@ static MunitResult test_recv_single_i_block(const MunitParameter params[], void* (void)params; (void)fixture; + /* ISO 7816-3 says "The information field of the I-block in response is mapped onto the response APDU without any change." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); t1_host_test_reset(); uart.t1.recv_pcb = 0x00; + uart.t1.ifsd = SEADER_T1_IFS_DEFAULT; uint8_t i_block[] = {0x00, 0x00, 0x03, 0x11, 0x22, 0x33, 0x00}; seader_add_lrc(i_block, 6); CCID_Message i_message = {.payload = i_block, .dwLength = 7}; munit_assert_true(seader_recv_t1(&seader, &i_message)); + /* ISO 7816-3 says "the initial value is N(S) = 0; then the value alternates after transmitting each I-block." */ munit_assert_size(g_t1_host_test_state.process_call_count, ==, 1); munit_assert_true(last_apdu_matches((const uint8_t*)"\x11\x22\x33", 3)); munit_assert_uint8(uart.t1.recv_pcb, ==, 0x40); @@ -165,17 +178,20 @@ static MunitResult test_recv_chained_i_blocks(const MunitParameter params[], voi (void)params; (void)fixture; + /* ISO 7816-3 says successive chained I-block information fields are concatenated into one APDU. */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); t1_host_test_reset(); uart.t1.recv_pcb = 0x00; + uart.t1.ifsd = SEADER_T1_IFS_DEFAULT; uint8_t i_more[] = {0x00, 0x20, 0x03, 0x44, 0x55, 0x66, 0x00}; seader_add_lrc(i_more, 6); CCID_Message more_message = {.payload = i_more, .dwLength = 7}; munit_assert_false(seader_recv_t1(&seader, &more_message)); + /* ISO 7816-3 says "R(N(R)) requests transmission of the next chained I-block". */ munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); munit_assert_not_null(uart.t1.rx_buffer); munit_assert_uint8(uart.t1.recv_pcb, ==, 0x40); @@ -196,6 +212,7 @@ static MunitResult test_recv_ifs_request(const MunitParameter params[], void* fi (void)params; (void)fixture; + /* ISO 7816-3 says "The card transmits S(IFS request) to indicate a new IFSC it can support." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -207,8 +224,10 @@ static MunitResult test_recv_ifs_request(const MunitParameter params[], void* fi CCID_Message ifs_request_message = {.payload = ifs_request, .dwLength = 5}; munit_assert_false(seader_recv_t1(&seader, &ifs_request_message)); + /* ISO 7816-3 says "The interface device shall acknowledge by S(IFS response) with the same INF." */ munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); - munit_assert_true(last_frame_prefix_matches((const uint8_t*)"\x00\xE1\x01\x33", 4)); + munit_assert_true(last_frame_prefix_matches((const uint8_t*)"\x00\xE1\x01\x20", 4)); + munit_assert_uint8(uart.t1.ifsc, ==, 0x20); return MUNIT_OK; } @@ -216,6 +235,7 @@ static MunitResult test_recv_r_block_resends_buffer(const MunitParameter params[ (void)params; (void)fixture; + /* ISO 7816-3 says "Every R-block carries N(R) which is the send-sequence number N(S) of the expected I-block." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -231,6 +251,7 @@ static MunitResult test_recv_r_block_resends_buffer(const MunitParameter params[ CCID_Message r_message = {.payload = r_block, .dwLength = 4}; munit_assert_false(seader_recv_t1(&seader, &r_message)); + /* ISO 7816-3 says "R(N(R)) requests transmission of the next chained I-block". */ munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); munit_assert_not_null(uart.t1.tx_buffer); bit_buffer_free(uart.t1.tx_buffer); @@ -241,6 +262,7 @@ static MunitResult test_reset_clears_state(const MunitParameter params[], void* (void)params; (void)fixture; + /* ISO 7816-3 says "At the start of the transmission protocol ... IFSC and IFSD are initialized." */ SeaderUartBridge uart = {0}; uart.t1.nad = 0x77; uart.t1.send_pcb = 0x00; diff --git a/lib/host_tests/test_t1_regressions.c b/lib/host_tests/test_t1_protocol.c similarity index 89% rename from lib/host_tests/test_t1_regressions.c rename to lib/host_tests/test_t1_protocol.c index 0515786..29dc2f2 100644 --- a/lib/host_tests/test_t1_regressions.c +++ b/lib/host_tests/test_t1_protocol.c @@ -23,6 +23,7 @@ static MunitResult test_recv_wtx_request_responds(const MunitParameter params[], (void)params; (void)fixture; + /* ISO 7816-3 says "The interface device shall acknowledge by S(WTX response) with the same INF." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -38,29 +39,11 @@ static MunitResult test_recv_wtx_request_responds(const MunitParameter params[], return MUNIT_OK; } -static MunitResult test_recv_resynch_request_responds(const MunitParameter params[], void* fixture) { - (void)params; - (void)fixture; - - SeaderUartBridge uart = {0}; - SeaderWorker worker = {0}; - Seader seader = make_test_seader(&uart, &worker); - - t1_host_test_reset(); - uint8_t s_resync_req[] = {0x00, 0xC0, 0x00, 0x00}; - seader_add_lrc(s_resync_req, 3); - CCID_Message message = {.payload = s_resync_req, .dwLength = 4}; - - munit_assert_false(seader_recv_t1(&seader, &message)); - munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); - munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0xE0); - return MUNIT_OK; -} - static MunitResult test_recv_malformed_wtx_rejected(const MunitParameter params[], void* fixture) { (void)params; (void)fixture; + /* ISO 7816-3 says "INF shall be present with a single byte in an S-block adjusting IFS and WTX." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -80,6 +63,7 @@ static MunitResult test_recv_malformed_ifs_rejected(const MunitParameter params[ (void)params; (void)fixture; + /* ISO 7816-3 says "INF shall be present with a single byte in an S-block adjusting IFS and WTX." */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -99,6 +83,7 @@ static MunitResult test_recv_ifs_request_updates_ifsc(const MunitParameter param (void)params; (void)fixture; + /* ISO 7816-3 says "The interface device assumes the new IFSC is valid as long as no other IFSC is indicated". */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -121,6 +106,7 @@ static MunitResult test_recv_ifs_response_applies_pending_ifsd( (void)params; (void)fixture; + /* ISO 7816-3 says "The card assumes the new IFSD is valid as long as no other IFSD is indicated". */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -144,6 +130,7 @@ static MunitResult test_recv_ifs_response_mismatch_rejected( (void)params; (void)fixture; + /* ISO 7816-3 says the response acknowledges "with the same INF", so mismatched IFS bytes are invalid. */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -166,6 +153,7 @@ static MunitResult test_recv_i_block_too_large_rejected(const MunitParameter par (void)params; (void)fixture; + /* ISO 7816-3 says each piece must have length "less than or equal to IFSC or IFSD". */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -188,6 +176,7 @@ static MunitResult test_recv_r_block_nack_retransmits(const MunitParameter param (void)params; (void)fixture; + /* ISO 7816-3 says an invalid block leads to an R-block that "requests with its N(R) for the expected I-block". */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -205,7 +194,7 @@ static MunitResult test_recv_r_block_nack_retransmits(const MunitParameter param munit_assert_false(seader_recv_t1(&seader, &message)); munit_assert_size(g_t1_host_test_state.xfrblock_call_count, ==, 1); - munit_assert_uint8(uart.t1.send_pcb, ==, SEADER_T1_PCB_SEQUENCE_BIT); + munit_assert_uint8(g_t1_host_test_state.last_frame[1], ==, 0x00); bit_buffer_free(uart.t1.tx_buffer); return MUNIT_OK; } @@ -216,6 +205,7 @@ static MunitResult test_recv_r_block_invalid_retransmit_state_errors( (void)params; (void)fixture; + /* ISO 7816-3 ties R-block recovery to "the expected I-block"; without a prior chunk, retransmit state is invalid. */ SeaderUartBridge uart = {0}; SeaderWorker worker = {0}; Seader seader = make_test_seader(&uart, &worker); @@ -242,12 +232,6 @@ static MunitTest test_t1_regression_cases[] = { NULL, MUNIT_TEST_OPTION_NONE, NULL}, - {(char*)"/recv/resynch-request-responds", - test_recv_resynch_request_responds, - NULL, - NULL, - MUNIT_TEST_OPTION_NONE, - NULL}, {(char*)"/recv/malformed-wtx-len-zero-rejected", test_recv_malformed_wtx_rejected, NULL, @@ -299,7 +283,7 @@ static MunitTest test_t1_regression_cases[] = { {NULL, NULL, NULL, NULL, 0, NULL}, }; -MunitSuite test_t1_regressions_suite = { +MunitSuite test_t1_protocol_suite = { "", test_t1_regression_cases, NULL, diff --git a/seader_bridge.h b/seader_bridge.h index 4d4ec1d..8b58c2b 100644 --- a/seader_bridge.h +++ b/seader_bridge.h @@ -9,6 +9,8 @@ #include #include +#include "t_1_logic.h" + // https://ww1.microchip.com/downloads/en/DeviceDoc/00001561C.pdf #define SEADER_UART_RX_BUF_SIZE (300) #define SEADER_CCID_SLOT_COUNT (2U) @@ -40,20 +42,6 @@ typedef struct { SeaderCcidSlotState slots[SEADER_CCID_SLOT_COUNT]; } SeaderCcidState; -typedef struct { - /* ICC information field size for T=1. */ - uint8_t ifsc; - /* Host NAD used for T=1 block exchange. */ - uint8_t nad; - /* Last transmit I-block sequence bit. */ - uint8_t send_pcb; - /* Last receive I-block sequence bit. */ - uint8_t recv_pcb; - BitBuffer* tx_buffer; - size_t tx_buffer_offset; - BitBuffer* rx_buffer; -} SeaderT1State; - struct SeaderUartBridge { SeaderUartConfig cfg; SeaderUartConfig cfg_new; diff --git a/t_1.c b/t_1.c index 8649e26..7dcbe6e 100644 --- a/t_1.c +++ b/t_1.c @@ -4,46 +4,30 @@ #include "t_1.h" #endif +#include "t_1_logic.h" + #define TAG "Seader:T=1" -// http://www.sat-digest.com/SatXpress/SmartCard/ISO7816-4.htm - -/* I know my T=1 is terrible, but I'm also only targetting one specific 'card' */ - static SeaderT1State* seader_t1_state(SeaderUartBridge* seader_uart) { return &seader_uart->t1; } static uint8_t seader_next_dpcb(SeaderUartBridge* seader_uart) { SeaderT1State* t1 = seader_t1_state(seader_uart); - uint8_t next_pcb = t1->send_pcb ^ SEADER_T1_PCB_SEQUENCE_BIT; - //FURI_LOG_D(TAG, "dPCB was: %02X, current dPCB: %02X", dPCB, next_pcb); + uint8_t next_pcb = seader_t1_next_pcb(t1->send_pcb); t1->send_pcb = next_pcb; return t1->send_pcb; } -static uint8_t seader_next_cpcb(SeaderUartBridge* seader_uart) { - SeaderT1State* t1 = seader_t1_state(seader_uart); - uint8_t next_pcb = t1->recv_pcb ^ SEADER_T1_PCB_SEQUENCE_BIT; - //FURI_LOG_D(TAG, "cPCB was: %02X, current cPCB: %02X", cPCB, next_pcb); - t1->recv_pcb = next_pcb; - return t1->recv_pcb; -} - void seader_t_1_reset(SeaderUartBridge* seader_uart) { SeaderT1State* t1 = seader_t1_state(seader_uart); t1->nad = 0x00; - t1->send_pcb = SEADER_T1_PCB_SEQUENCE_BIT; - t1->recv_pcb = 0x00; - if(t1->tx_buffer != NULL) { - bit_buffer_free(t1->tx_buffer); + if(t1->ifsc == 0 || t1->ifsc > SEADER_T1_IFS_MAX) { + t1->ifsc = SEADER_T1_IFS_DEFAULT; } - t1->tx_buffer = NULL; - t1->tx_buffer_offset = 0; - if(t1->rx_buffer != NULL) { - bit_buffer_free(t1->rx_buffer); - } - t1->rx_buffer = NULL; + t1->ifsd = SEADER_T1_IFS_DEFAULT; + t1->ifsd_pending = 0; + seader_t1_reset_link_state(t1); } void seader_t_1_set_IFSD(Seader* seader) { @@ -52,19 +36,21 @@ void seader_t_1_set_IFSD(Seader* seader) { SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t frame[5]; uint8_t frame_len = 0; + /* Negotiate the largest host receive size we support so chained responses stay predictable. */ + uint8_t requested_ifsd = SEADER_T1_IFS_MAX; frame[0] = t1->nad; - frame[1] = SEADER_T1_PCB_S_BLOCK | SEADER_T1_S_BLOCK_IFS; // S(IFS request) + frame[1] = SEADER_T1_PCB_S_BLOCK | SEADER_T1_S_BLOCK_IFS; frame[2] = 0x01; - frame[3] = t1->ifsc; + t1->ifsd_pending = requested_ifsd; + frame[3] = requested_ifsd; frame_len = 4; frame_len = seader_add_lrc(frame, frame_len); - seader_ccid_XfrBlock(seader_uart, frame, frame_len); } -void seader_t_1_IFSD_response(Seader* seader) { +static void seader_t_1_IFSD_response(Seader* seader, uint8_t ifs_value) { SeaderWorker* seader_worker = seader->worker; SeaderUartBridge* seader_uart = seader_worker->uart; SeaderT1State* t1 = seader_t1_state(seader_uart); @@ -72,13 +58,45 @@ void seader_t_1_IFSD_response(Seader* seader) { uint8_t frame_len = 0; frame[0] = t1->nad; - frame[1] = 0xE0 | 0x01; // S(IFS response) + frame[1] = 0xE0 | SEADER_T1_S_BLOCK_IFS; frame[2] = 0x01; - frame[3] = t1->ifsc; + frame[3] = ifs_value; frame_len = 4; frame_len = seader_add_lrc(frame, frame_len); + seader_ccid_XfrBlock(seader_uart, frame, frame_len); +} +static void seader_t_1_WTX_response(Seader* seader, uint8_t multiplier) { + SeaderWorker* seader_worker = seader->worker; + SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderT1State* t1 = seader_t1_state(seader_uart); + uint8_t frame[5]; + uint8_t frame_len = 0; + + frame[0] = t1->nad; + frame[1] = 0xE0 | SEADER_T1_S_BLOCK_WTX; + frame[2] = 0x01; + frame[3] = multiplier; + frame_len = 4; + + frame_len = seader_add_lrc(frame, frame_len); + seader_ccid_XfrBlock(seader_uart, frame, frame_len); +} + +static void seader_t_1_resynch_response(Seader* seader) { + SeaderWorker* seader_worker = seader->worker; + SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderT1State* t1 = seader_t1_state(seader_uart); + uint8_t frame[4]; + uint8_t frame_len = 0; + + frame[0] = t1->nad; + frame[1] = 0xE0 | SEADER_T1_S_BLOCK_RESYNCH; + frame[2] = 0x00; + frame_len = 3; + + frame_len = seader_add_lrc(frame, frame_len); seader_ccid_XfrBlock(seader_uart, frame, frame_len); } @@ -90,24 +108,38 @@ void seader_t_1_send_ack(Seader* seader) { uint8_t frame_len = 0; frame[0] = t1->nad; - frame[1] = SEADER_T1_PCB_R_BLOCK | (seader_next_cpcb(seader_uart) >> 2); + frame[1] = SEADER_T1_PCB_R_BLOCK | (t1->recv_pcb >> 2); frame[2] = 0x00; frame_len = 3; frame_len = seader_add_lrc(frame, frame_len); - - //FURI_LOG_D(TAG, "Sending R-Block ACK: PCB: %02x", frame[1]); - seader_ccid_XfrBlock(seader_uart, frame, frame_len); } -void seader_send_t1_chunk(SeaderUartBridge* seader_uart, uint8_t PCB, uint8_t* chunk, size_t len) { +static void seader_t_1_send_nak(Seader* seader) { + SeaderWorker* seader_worker = seader->worker; + SeaderUartBridge* seader_uart = seader_worker->uart; + SeaderT1State* t1 = seader_t1_state(seader_uart); + uint8_t frame[4]; + uint8_t frame_len = 0; + + frame[0] = t1->nad; + frame[1] = SEADER_T1_PCB_R_BLOCK | (t1->recv_pcb >> 2) | 0x01; + frame[2] = 0x00; + frame_len = 3; + + frame_len = seader_add_lrc(frame, frame_len); + FURI_LOG_W(TAG, "Sending R-Block NACK: PCB: %02x", frame[1]); + seader_ccid_XfrBlock(seader_uart, frame, frame_len); +} + +void seader_send_t1_chunk(SeaderUartBridge* seader_uart, uint8_t pcb, uint8_t* chunk, size_t len) { SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t* frame = malloc(3 + len + 1); uint8_t frame_len = 0; frame[0] = t1->nad; - frame[1] = PCB; + frame[1] = pcb; frame[2] = len; frame_len = 3; @@ -117,20 +149,20 @@ void seader_send_t1_chunk(SeaderUartBridge* seader_uart, uint8_t PCB, uint8_t* c } frame_len = seader_add_lrc(frame, frame_len); - seader_ccid_XfrBlock(seader_uart, frame, frame_len); free(frame); } void seader_send_t1_scratchpad( SeaderUartBridge* seader_uart, - uint8_t PCB, + uint8_t pcb, uint8_t* apdu, size_t len) { SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t* frame = apdu - 3; + frame[0] = t1->nad; - frame[1] = PCB; + frame[1] = pcb; frame[2] = (uint8_t)len; size_t frame_len = seader_add_lrc(frame, 3 + len); @@ -141,145 +173,113 @@ void seader_send_t1(SeaderUartBridge* seader_uart, uint8_t* apdu, size_t len) { SeaderT1State* t1 = seader_t1_state(seader_uart); uint8_t ifsc = t1->ifsc; - bool in_scratchpad = - (apdu >= seader_uart->tx_buf + 3 && apdu < seader_uart->tx_buf + SEADER_UART_RX_BUF_SIZE); - - if(len > ifsc) { - if(t1->tx_buffer == NULL) { - t1->tx_buffer = bit_buffer_alloc(768); - bit_buffer_copy_bytes(t1->tx_buffer, apdu, len); - } - size_t remaining = (bit_buffer_get_size_bytes(t1->tx_buffer) - t1->tx_buffer_offset); - size_t copy_length = remaining > ifsc ? ifsc : remaining; - - uint8_t* chunk = (uint8_t*)bit_buffer_get_data(t1->tx_buffer) + t1->tx_buffer_offset; - - if(remaining > ifsc) { - uint8_t PCB = seader_next_dpcb(seader_uart) | SEADER_T1_PCB_I_BLOCK_MORE; - seader_send_t1_chunk(seader_uart, PCB, chunk, copy_length); - } else { - uint8_t PCB = seader_next_dpcb(seader_uart); - seader_send_t1_chunk(seader_uart, PCB, chunk, copy_length); - } - - t1->tx_buffer_offset += copy_length; - if(t1->tx_buffer_offset >= bit_buffer_get_size_bytes(t1->tx_buffer)) { - bit_buffer_free(t1->tx_buffer); - t1->tx_buffer = NULL; - t1->tx_buffer_offset = 0; - } - return; + if(ifsc == 0 || ifsc > SEADER_T1_IFS_MAX) { + ifsc = SEADER_T1_IFS_DEFAULT; } - if(in_scratchpad) { - seader_send_t1_scratchpad(seader_uart, seader_next_dpcb(seader_uart), apdu, len); + if(t1->tx_buffer == NULL) { + bool in_scratchpad = + apdu != NULL && + seader_t1_apdu_in_scratchpad(seader_uart->tx_buf, SEADER_UART_RX_BUF_SIZE, apdu); + + if(in_scratchpad && len <= ifsc) { + seader_send_t1_scratchpad(seader_uart, seader_next_dpcb(seader_uart), apdu, len); + t1->last_tx_len = len; + return; + } + + t1->tx_buffer = bit_buffer_alloc(768); + bit_buffer_copy_bytes(t1->tx_buffer, apdu, len); + t1->tx_buffer_offset = 0; + } + + size_t total_len = bit_buffer_get_size_bytes(t1->tx_buffer); + size_t remaining = total_len - t1->tx_buffer_offset; + size_t copy_length = seader_t1_chunk_length(total_len, t1->tx_buffer_offset, ifsc); + uint8_t* chunk = (uint8_t*)bit_buffer_get_data(t1->tx_buffer) + t1->tx_buffer_offset; + + uint8_t pcb; + if(remaining > ifsc) { + pcb = seader_next_dpcb(seader_uart) | SEADER_T1_PCB_I_BLOCK_MORE; } else { - seader_send_t1_chunk(seader_uart, seader_next_dpcb(seader_uart), apdu, len); + pcb = seader_next_dpcb(seader_uart); } + + seader_send_t1_chunk(seader_uart, pcb, chunk, copy_length); + t1->last_tx_len = copy_length; + t1->tx_buffer_offset += copy_length; } bool seader_recv_t1(Seader* seader, CCID_Message* message) { SeaderWorker* seader_worker = seader->worker; SeaderUartBridge* seader_uart = seader_worker->uart; SeaderT1State* t1 = seader_t1_state(seader_uart); - // remove/validate NAD, PCB, LEN, LRC - if(message->dwLength < 4) { - FURI_LOG_W(TAG, "Invalid T=1 frame: too short"); - return false; - } - //uint8_t NAD = message->payload[0]; - uint8_t rPCB = message->payload[1]; - uint8_t LEN = message->payload[2]; - //uint8_t LRC = message->payload[3 + LEN]; - //FURI_LOG_D(TAG, "NAD: %02X, rPCB: %02X, LEN: %02X, LRC: %02X", NAD, rPCB, LEN, LRC); + uint8_t* apdu = NULL; + size_t apdu_len = 0; - if(rPCB == 0xE1) { - // S(IFS response) - //FURI_LOG_D(TAG, "Received IFS Response"); - seader_worker_send_version(seader); - SeaderWorker* seader_worker = seader->worker; - if(seader_worker->callback) { - seader_worker->callback(SeaderWorkerEventSamPresent, seader_worker->context); - } - return false; - } + SeaderT1Action action = + seader_t1_handle_block(t1, message->payload, message->dwLength, &apdu, &apdu_len); - if(rPCB == t1->recv_pcb) { - seader_next_cpcb(seader_uart); + /* Keep transport decisions here so host tests exercise the same action-to-wire mapping. */ + switch(action) { + case SeaderT1ActionDeliverAPDU: if(t1->rx_buffer != NULL) { - bit_buffer_append_bytes(t1->rx_buffer, message->payload + 3, LEN); - - // TODO: validate LRC - - seader_worker_process_sam_message( - seader, - (uint8_t*)bit_buffer_get_data(t1->rx_buffer), - bit_buffer_get_size_bytes(t1->rx_buffer)); - + seader_worker_process_sam_message(seader, apdu, apdu_len); bit_buffer_free(t1->rx_buffer); t1->rx_buffer = NULL; return true; } + return seader_worker_process_sam_message(seader, apdu, apdu_len); - if(seader_validate_lrc(message->payload, message->dwLength) == false) { - return false; - } - - // Skip NAD, PCB, LEN - message->payload = message->payload + 3; - message->dwLength = LEN; - - if(message->dwLength == 0) { - //FURI_LOG_D(TAG, "Received T=1 frame with no data"); - return true; - } - return seader_worker_process_sam_message(seader, message->payload, message->dwLength); - } else if(rPCB == (t1->recv_pcb | SEADER_T1_PCB_I_BLOCK_MORE)) { - //FURI_LOG_D(TAG, "Received T=1 frame with more bit set"); - if(t1->rx_buffer == NULL) { - t1->rx_buffer = bit_buffer_alloc(512); - } - bit_buffer_append_bytes(t1->rx_buffer, message->payload + 3, LEN); + case SeaderT1ActionSendAck: seader_t_1_send_ack(seader); return false; - } else if((rPCB & SEADER_T1_PCB_S_BLOCK) == SEADER_T1_PCB_S_BLOCK) { - //FURI_LOG_D(TAG, "Received S-Block"); - if((rPCB & 0x0F) == SEADER_T1_S_BLOCK_IFS) { - seader_t_1_IFSD_response(seader); - return false; - } - } else if((rPCB & SEADER_T1_PCB_R_BLOCK) == SEADER_T1_PCB_R_BLOCK) { - uint8_t R_SEQ = (rPCB & SEADER_T1_R_BLOCK_SEQUENCE_MASK) >> 4; - uint8_t I_SEQ = (t1->send_pcb ^ SEADER_T1_PCB_SEQUENCE_BIT) >> 6; - if(R_SEQ != I_SEQ) { - /* - FURI_LOG_D( - TAG, - "Received R-Block: Incorrect sequence. Expected: %02X, Received: %02X", - I_SEQ, - R_SEQ); + case SeaderT1ActionSendNak: + seader_t_1_send_nak(seader); + return false; - */ - // When this happens, the flipper freezes if it is doing NFC and my attempts to do events to stop that have failed - return false; + case SeaderT1ActionSendIFSResponse: + if(apdu_len == 1 && apdu != NULL) { + seader_t_1_IFSD_response(seader, apdu[0]); + } else { + seader_t_1_send_nak(seader); } + return false; - if(t1->tx_buffer != NULL) { - // Send more data, re-using the buffer to trigger the code path that sends the next block - seader_send_t1( - seader_uart, - (uint8_t*)bit_buffer_get_data(t1->tx_buffer), - bit_buffer_get_size_bytes(t1->tx_buffer)); - return false; + case SeaderT1ActionSendWTXResponse: + seader_t_1_WTX_response(seader, apdu[0]); + return false; + + case SeaderT1ActionSendResynchResponse: + seader_t1_reset_link_state(t1); + seader_t_1_resynch_response(seader); + return false; + + case SeaderT1ActionSendVersion: + seader_worker_send_version(seader); + if(seader_worker->callback) { + seader_worker->callback(SeaderWorkerEventSamPresent, seader_worker->context); } - } else { - FURI_LOG_W( - TAG, - "Invalid T=1 frame: PCB mismatch. Expected: %02X, Received: %02X", - t1->recv_pcb, - rPCB); + return false; + + case SeaderT1ActionSendMoreData: + seader_send_t1( + seader_uart, + (uint8_t*)bit_buffer_get_data(t1->tx_buffer), + bit_buffer_get_size_bytes(t1->tx_buffer)); + return false; + + case SeaderT1ActionRetransmit: + seader_send_t1(seader_uart, NULL, 0); + return false; + + case SeaderT1ActionNone: + return true; + + case SeaderT1ActionError: + default: + FURI_LOG_W(TAG, "T=1 error or unhandled action %d", action); + return false; } - - return false; } diff --git a/t_1.h b/t_1.h index cedf532..4f3cad6 100644 --- a/t_1.h +++ b/t_1.h @@ -1,18 +1,10 @@ #pragma once #include "ccid.h" +#include "t_1_logic.h" typedef struct CCID_Message CCID_Message; -typedef enum { - SEADER_T1_PCB_I_BLOCK_MORE = 0x20, - SEADER_T1_PCB_SEQUENCE_BIT = 0x40, - SEADER_T1_PCB_R_BLOCK = 0x80, - SEADER_T1_PCB_S_BLOCK = 0xC0, - SEADER_T1_R_BLOCK_SEQUENCE_MASK = 0x10, - SEADER_T1_S_BLOCK_IFS = 0x01, -} SeaderT1Constant; - void seader_send_t1(SeaderUartBridge* seader_uart, uint8_t* apdu, size_t len); bool seader_recv_t1(Seader* seader, CCID_Message* message); void seader_t_1_set_IFSD(Seader* seader); diff --git a/t_1_logic.c b/t_1_logic.c new file mode 100644 index 0000000..8b27664 --- /dev/null +++ b/t_1_logic.c @@ -0,0 +1,200 @@ +#include "t_1_logic.h" +#include "lrc.h" + +#include + +#ifdef SEADER_HOST_TEST +#include "lib/host_tests/bit_buffer.h" +#else +#include +#endif + +uint8_t seader_t1_next_pcb(uint8_t current_pcb) { + return current_pcb ^ SEADER_T1_PCB_SEQUENCE_BIT; +} + +bool seader_t1_apdu_in_scratchpad(const uint8_t* tx_buf, size_t tx_buf_size, const uint8_t* apdu) { + return apdu >= tx_buf + 3 && apdu < tx_buf + tx_buf_size; +} + +size_t seader_t1_chunk_length(size_t total_length, size_t offset, uint8_t ifsc) { + size_t remaining = total_length - offset; + if(ifsc == 0) { + ifsc = SEADER_T1_IFS_DEFAULT; + } + return remaining > ifsc ? ifsc : remaining; +} + +bool seader_t1_validate_block(const uint8_t* block, size_t dw_length) { + if(!block || dw_length < 4) { + return false; + } + + uint8_t len = block[2]; + if(dw_length != (size_t)(len + 4)) { + return false; + } + + return seader_validate_lrc((uint8_t*)block, dw_length); +} + +void seader_t1_reset_link_state(SeaderT1State* t1) { + if(!t1) { + return; + } + + t1->send_pcb = SEADER_T1_PCB_SEQUENCE_BIT; + t1->recv_pcb = 0x00; + + if(t1->tx_buffer != NULL) { + bit_buffer_free(t1->tx_buffer); + } + t1->tx_buffer = NULL; + t1->tx_buffer_offset = 0; + t1->last_tx_len = 0; + + if(t1->rx_buffer != NULL) { + bit_buffer_free(t1->rx_buffer); + } + t1->rx_buffer = NULL; +} + +SeaderT1Action seader_t1_handle_block( + SeaderT1State* t1, + const uint8_t* payload, + size_t dw_length, + uint8_t** apdu_out, + size_t* apdu_len_out) { + if(!seader_t1_validate_block(payload, dw_length)) { + return SeaderT1ActionSendNak; + } + + uint8_t rPCB = payload[1]; + uint8_t LEN = payload[2]; + + if((rPCB & SEADER_T1_PCB_S_BLOCK) == SEADER_T1_PCB_S_BLOCK) { + uint8_t type = rPCB & SEADER_T1_S_BLOCK_TYPE_MASK; + bool is_response = (rPCB & SEADER_T1_S_BLOCK_RESPONSE_BIT) != 0; + + /* The regression suite expects malformed S-block lengths to be rejected before payload use. */ + if((type == SEADER_T1_S_BLOCK_IFS || type == SEADER_T1_S_BLOCK_WTX) && LEN != 1) { + return SeaderT1ActionSendNak; + } + if((type == SEADER_T1_S_BLOCK_RESYNCH || type == SEADER_T1_S_BLOCK_ABORT) && LEN != 0) { + return SeaderT1ActionSendNak; + } + + switch(type) { + case SEADER_T1_S_BLOCK_IFS: + if(is_response) { + /* Only accept the pending IFSD we negotiated so a stray response cannot retune framing. */ + uint8_t ifs = payload[3]; + if(ifs == 0 || ifs > SEADER_T1_IFS_MAX) { + return SeaderT1ActionSendNak; + } + if(t1->ifsd_pending != 0 && ifs != t1->ifsd_pending) { + return SeaderT1ActionSendNak; + } + t1->ifsd = ifs; + t1->ifsd_pending = 0; + return SeaderT1ActionSendVersion; + } else { + /* Card-initiated IFS updates our outbound chunk size and must be echoed back verbatim. */ + uint8_t ifs = payload[3]; + if(ifs == 0 || ifs > SEADER_T1_IFS_MAX) { + return SeaderT1ActionSendNak; + } + t1->ifsc = ifs; + *apdu_out = (uint8_t*)payload + 3; + *apdu_len_out = 1; + return SeaderT1ActionSendIFSResponse; + } + case SEADER_T1_S_BLOCK_WTX: + if(!is_response) { + /* WTX requests do not advance link state; they only request a matching S(WTX response). */ + *apdu_out = (uint8_t*)payload + 3; + *apdu_len_out = LEN; + return SeaderT1ActionSendWTXResponse; + } + break; + case SEADER_T1_S_BLOCK_RESYNCH: + if(!is_response) { + /* RESYNCH resets sequence tracking before we send the response frame. */ + return SeaderT1ActionSendResynchResponse; + } + break; + case SEADER_T1_S_BLOCK_ABORT: + return SeaderT1ActionError; + } + return SeaderT1ActionNone; + } + + if(rPCB == t1->recv_pcb) { + if(LEN > t1->ifsd) { + return SeaderT1ActionSendNak; + } + t1->recv_pcb = seader_t1_next_pcb(t1->recv_pcb); + + if(t1->tx_buffer != NULL) { + bit_buffer_free(t1->tx_buffer); + t1->tx_buffer = NULL; + t1->tx_buffer_offset = 0; + t1->last_tx_len = 0; + } + + if(t1->rx_buffer != NULL) { + bit_buffer_append_bytes(t1->rx_buffer, payload + 3, LEN); + *apdu_out = (uint8_t*)bit_buffer_get_data(t1->rx_buffer); + *apdu_len_out = bit_buffer_get_size_bytes(t1->rx_buffer); + return SeaderT1ActionDeliverAPDU; + } + + if(LEN == 0) { + return SeaderT1ActionNone; + } + + *apdu_out = (uint8_t*)payload + 3; + *apdu_len_out = LEN; + return SeaderT1ActionDeliverAPDU; + + } else if(rPCB == (t1->recv_pcb | SEADER_T1_PCB_I_BLOCK_MORE)) { + if(LEN > t1->ifsd) { + return SeaderT1ActionSendNak; + } + t1->recv_pcb = seader_t1_next_pcb(t1->recv_pcb); + if(t1->rx_buffer == NULL) { + t1->rx_buffer = bit_buffer_alloc(512); + } + bit_buffer_append_bytes(t1->rx_buffer, payload + 3, LEN); + return SeaderT1ActionSendAck; + + } else if((rPCB & SEADER_T1_PCB_R_BLOCK) == SEADER_T1_PCB_R_BLOCK) { + uint8_t r_seq = (rPCB & SEADER_T1_R_BLOCK_SEQUENCE_MASK) >> 4; + uint8_t next_i_seq = (t1->send_pcb ^ SEADER_T1_PCB_SEQUENCE_BIT) >> 6; + uint8_t err = rPCB & 0x0F; + + if(err == 0 && r_seq == next_i_seq) { + /* Matching R-block ACKs advance a chained transmit if more buffered data remains. */ + if(t1->tx_buffer != NULL) { + if(t1->tx_buffer_offset < bit_buffer_get_size_bytes(t1->tx_buffer)) { + return SeaderT1ActionSendMoreData; + } + return SeaderT1ActionNone; + } + return SeaderT1ActionNone; + } else { + if(t1->tx_buffer != NULL) { + /* NACK retransmits the previous chunk by rewinding exactly last_tx_len bytes. */ + if(t1->last_tx_len == 0 || t1->last_tx_len > t1->tx_buffer_offset) { + return SeaderT1ActionError; + } + t1->tx_buffer_offset -= t1->last_tx_len; + t1->send_pcb ^= SEADER_T1_PCB_SEQUENCE_BIT; + return SeaderT1ActionRetransmit; + } + return SeaderT1ActionError; + } + } + + return SeaderT1ActionError; +} diff --git a/t_1_logic.h b/t_1_logic.h new file mode 100644 index 0000000..c19875c --- /dev/null +++ b/t_1_logic.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include + +typedef enum { + SEADER_T1_PCB_I_BLOCK_MORE = 0x20, + SEADER_T1_PCB_SEQUENCE_BIT = 0x40, + SEADER_T1_PCB_R_BLOCK = 0x80, + SEADER_T1_PCB_S_BLOCK = 0xC0, + SEADER_T1_R_BLOCK_SEQUENCE_MASK = 0x10, + SEADER_T1_S_BLOCK_RESPONSE_BIT = 0x20, + SEADER_T1_S_BLOCK_TYPE_MASK = 0x1F, + SEADER_T1_S_BLOCK_RESYNCH = 0x00, + SEADER_T1_S_BLOCK_IFS = 0x01, + SEADER_T1_S_BLOCK_ABORT = 0x02, + SEADER_T1_S_BLOCK_WTX = 0x03, +} SeaderT1Constant; + +typedef enum { + SeaderT1ActionNone, + SeaderT1ActionDeliverAPDU, + SeaderT1ActionSendAck, + SeaderT1ActionSendIFSResponse, + SeaderT1ActionSendWTXResponse, + SeaderT1ActionSendResynchResponse, + SeaderT1ActionSendVersion, + SeaderT1ActionSendMoreData, + SeaderT1ActionRetransmit, + SeaderT1ActionSendNak, + SeaderT1ActionError, +} SeaderT1Action; + +typedef struct BitBuffer BitBuffer; + +typedef struct { + /* ICC information field size (card receive capability; host transmit chunking). */ + uint8_t ifsc; + /* IFD information field size (host receive capability; card transmit chunking). */ + uint8_t ifsd; + /* Pending IFSD value proposed through S(IFS request); 0 means none pending. */ + uint8_t ifsd_pending; + /* Host NAD used for T=1 block exchange. */ + uint8_t nad; + /* Last transmit I-block sequence bit. */ + uint8_t send_pcb; + /* Last receive I-block sequence bit. */ + uint8_t recv_pcb; + BitBuffer* tx_buffer; + size_t tx_buffer_offset; + /* Length of the last transmitted chunk, used to roll back on NACK. */ + size_t last_tx_len; + BitBuffer* rx_buffer; +} SeaderT1State; + +enum { + SEADER_T1_IFS_DEFAULT = 32, + SEADER_T1_IFS_MAX = 254, +}; + +uint8_t seader_t1_next_pcb(uint8_t current_pcb); +bool seader_t1_apdu_in_scratchpad(const uint8_t* tx_buf, size_t tx_buf_size, const uint8_t* apdu); +size_t seader_t1_chunk_length(size_t total_length, size_t offset, uint8_t ifsc); +bool seader_t1_validate_block(const uint8_t* block, size_t dw_length); +void seader_t1_reset_link_state(SeaderT1State* t1); + +SeaderT1Action seader_t1_handle_block( + SeaderT1State* t1, + const uint8_t* payload, + size_t dw_length, + uint8_t** apdu_out, + size_t* apdu_len_out);