Merge pull request #34 from cindersocket/feat-testing
Some checks failed
FAP: Build, test, and lint / ufbt: Build for release branch (push) Failing after 1m11s

Improve CCID and T=1 conformance against specifications
This commit is contained in:
Eric Betts
2026-03-08 19:52:04 -07:00
committed by GitHub
15 changed files with 1141 additions and 212 deletions

View File

@@ -15,10 +15,14 @@ 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_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

16
ccid.c
View File

@@ -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) {

94
ccid_logic.c Normal file
View File

@@ -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;
}

50
ccid_logic.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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);

View File

@@ -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,31 +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_IFS = 0x01,
} SeaderT1Constant;
typedef struct {
uint8_t ifsc;
uint8_t nad;
uint8_t send_pcb;
uint8_t recv_pcb;
BitBuffer* tx_buffer;
size_t tx_buffer_offset;
BitBuffer* rx_buffer;
} SeaderT1State;
struct SeaderUartBridge {
uint8_t rx_buf[SEADER_UART_RX_BUF_SIZE];
uint8_t tx_buf[SEADER_UART_RX_BUF_SIZE];

View File

@@ -0,0 +1,233 @@
#include <stdint.h>
#include "ccid_logic.h"
#include "munit.h"
static MunitResult test_sequence_advance_wraps(const MunitParameter params[], void* fixture) {
(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);
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;
/* 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));
return MUNIT_OK;
}
static MunitResult test_data_in_scratchpad(const MunitParameter params[], void* fixture) {
(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));
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;
/* 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));
return MUNIT_OK;
}
static MunitResult test_status_decode_ok(const MunitParameter params[], void* fixture) {
(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);
return MUNIT_OK;
}
static MunitResult test_status_decode_failed(const MunitParameter params[], void* fixture) {
(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);
return MUNIT_OK;
}
static MunitResult test_status_decode_time_extension(const MunitParameter params[], void* fixture) {
(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(
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;
/* 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;
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;
/* 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;
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;
/* 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));
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;
/* 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(
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,
};

View File

@@ -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;

View File

@@ -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_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", 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},
};
MunitSuite main_suite = {

View File

@@ -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;

View File

@@ -0,0 +1,292 @@
#include <string.h>
#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;
/* 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);
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_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);
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;
/* 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);
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;
/* 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);
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;
/* 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);
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;
/* 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);
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;
/* 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);
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;
/* 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);
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(g_t1_host_test_state.last_frame[1], ==, 0x00);
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;
/* 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);
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/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_protocol_suite = {
"",
test_t1_regression_cases,
NULL,
1,
MUNIT_SUITE_OPTION_NONE,
};

View File

@@ -9,6 +9,8 @@
#include <furi.h>
#include <furi_hal.h>
#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;

308
t_1.c
View File

@@ -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;
}

10
t_1.h
View File

@@ -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);

200
t_1_logic.c Normal file
View File

@@ -0,0 +1,200 @@
#include "t_1_logic.h"
#include "lrc.h"
#include <string.h>
#ifdef SEADER_HOST_TEST
#include "lib/host_tests/bit_buffer.h"
#else
#include <lib/toolbox/bit_buffer.h>
#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;
}

73
t_1_logic.h Normal file
View File

@@ -0,0 +1,73 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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);