From dcdb1a3529071ac936f0eebd1771adf6d36713ad Mon Sep 17 00:00:00 2001 From: Siddharth Chandrasekaran Date: Sat, 11 Jul 2026 23:48:43 +0200 Subject: [PATCH] Add Transparent Reader Support (TRS) TRS tunnels raw smart-card APDUs from the CP, through a PD acting as a transparent pipe, to a card in the reader (OSDP CMD_XWR/REPLY_XRD, capability SMART_CARD_SUPPORT). The control panel drives the card directly, so a compromised reader cannot forge a credential. The CP runs the session as a library-driven state machine: it negotiates transparent mode, streams the app's APDUs, and tears the session down on osdp_cp_trs_stop(). Apps submit OSDP_CMD_XWR commands and receive card responses as OSDP_EVENT_TRS events. The PD answers each APDU either synchronously, in the command callback, or deferred: it ACKs "working" and delivers the response on a later poll once a slow card responds. Deferral keeps a shared multi-drop bus non-blocking, so each PD's session makes progress independently. Gated behind OPT_BUILD_OSDP_TRS (default off); wired into both build systems and covered by a CP<->PD APDU round-trip unit test. Co-developed-by: Aaron Tulino (Aaronjamt) Signed-off-by: Siddharth Chandrasekaran --- .github/workflows/reusable-ci.yml | 7 +- CMakeLists.txt | 1 + configure.sh | 11 + include/osdp.h | 219 ++++++++ library.json | 1 + python/setup.py | 11 + src/CMakeLists.txt | 10 + src/osdp_common.c | 2 +- src/osdp_common.h | 67 +++ src/osdp_cp.c | 344 ++++++++++++- src/osdp_pd.c | 68 +++ src/osdp_trs.c | 807 +++++++++++++++++++++++++++++ src/osdp_trs.h | 90 ++++ tests/unit-tests/CMakeLists.txt | 1 + tests/unit-tests/test-trs.c | 809 ++++++++++++++++++++++++++++++ tests/unit-tests/test.c | 17 + tests/unit-tests/test.h | 2 + 17 files changed, 2444 insertions(+), 23 deletions(-) create mode 100644 src/osdp_trs.c create mode 100644 src/osdp_trs.h create mode 100644 tests/unit-tests/test-trs.c diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 6ebbc4ba..2f868463 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -66,12 +66,17 @@ jobs: Test: runs-on: ubuntu-latest + strategy: + matrix: + # TRS is off by default; run the suite both ways so the transparent + # reader sources and their tests are built and exercised too. + trs: [ OFF, ON ] steps: - uses: actions/checkout@v7 with: submodules: recursive - name: Configure - run: cmake -DCMAKE_BUILD_TYPE=Debug . + run: cmake -DCMAKE_BUILD_TYPE=Debug -DOPT_BUILD_OSDP_TRS=${{ matrix.trs }} . - name: Run unit-tests run: cmake --build . --parallel 7 --target check diff --git a/CMakeLists.txt b/CMakeLists.txt index fa04457a..08648be0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ option(OPT_OSDP_STATIC "Build without dynamic memory allocation" OFF) option(OPT_OSDP_LIB_ONLY "Only build the library" OFF) option(OPT_BUILD_BARE_METAL "Build library for bare metal targets" OFF) option(OPT_USE_32BIT_TICK_T "Use uint32_t tick_t on bare-metal targets" OFF) +option(OPT_BUILD_OSDP_TRS "Enable Transparent Reader Support" OFF) set(OPT_OSDP_CRYPTO_BACKEND "auto" CACHE STRING "Crypto backend selection: auto, openssl, mbedtls, or tinyaes") set_property(CACHE OPT_OSDP_CRYPTO_BACKEND PROPERTY STRINGS diff --git a/configure.sh b/configure.sh index b67a45ec..12019dd7 100755 --- a/configure.sh +++ b/configure.sh @@ -28,6 +28,7 @@ usage() { --lib-only Only build the library --bare-metal Enable bare-metal build paths --use-32bit-tick-t Use uint32_t tick_t (requires --bare-metal) + --enable-trs Enable Transparent Reader Support (TRS) --cross-compile PREFIX Use to pass a compiler prefix --prefix PATH Install path prefix (default: /usr) --build-dir Build output directory (default: ./build) @@ -57,6 +58,7 @@ while [ $# -gt 0 ]; do --lib-only) LIB_ONLY=1;; --bare-metal) BARE_METAL=1;; --use-32bit-tick-t) USE_32BIT_TICK_T=1;; + --enable-trs) ENABLE_TRS=1;; --build-dir) BUILD_DIR=$2; shift;; -d|--debug) DEBUG=1;; -f|--force) FORCE=1;; @@ -133,6 +135,10 @@ if [[ ! -z "${USE_32BIT_TICK_T}" ]]; then CCFLAGS+=" -DUSE_32BIT_TICK_T" fi +if [[ ! -z "${ENABLE_TRS}" ]]; then + CCFLAGS+=" -DOPT_BUILD_OSDP_TRS" +fi + ## Repo meta data echo "Extracting source code meta information" PROJECT_VERSION=$(perl -ne 'print if s/^project\(libosdp VERSION ([0-9.]+)\)$/\1/' CMakeLists.txt) @@ -214,6 +220,10 @@ fi TARGETS="cp_app pd_app" +if [[ ! -z "${ENABLE_TRS}" ]]; then + LIBOSDP_SOURCES+=" src/osdp_trs.c" +fi + TEST_SOURCES="tests/unit-tests/test.c" TEST_SOURCES+=" tests/unit-tests/test-cp-phy.c" TEST_SOURCES+=" tests/unit-tests/test-pd-phy.c" @@ -227,6 +237,7 @@ TEST_SOURCES+=" tests/unit-tests/test-sc.c" TEST_SOURCES+=" tests/unit-tests/test-sc-sia-vectors.c" TEST_SOURCES+=" tests/unit-tests/test-notifications.c" TEST_SOURCES+=" tests/unit-tests/test-codec-fuzz.c" +TEST_SOURCES+=" tests/unit-tests/test-trs.c" TEST_SOURCES+=" ${LIBOSDP_SOURCES} ${UTILS_SOURCES}" if [[ ! -z "${LIB_ONLY}" ]]; then diff --git a/include/osdp.h b/include/osdp.h index 673f9ce2..6ef77ccc 100644 --- a/include/osdp.h +++ b/include/osdp.h @@ -789,6 +789,16 @@ enum osdp_notification_type { * local abort, or a negative status reported by the PD). */ OSDP_NOTIFICATION_FILE_TX_DONE, + /** + * TRS card session state change (CP only). + * + * arg0: status -- see enum osdp_trs_session_status_e + * + * Fires when the reader enters transparent mode (OPENED), and once more + * when the session ends, either because a STOP was honoured (CLOSED) or + * because it could not be established or was cut short (FAILED). + */ + OSDP_NOTIFICATION_TRS_STATUS, }; /** @@ -831,9 +841,215 @@ enum osdp_cmd_e { OSDP_CMD_STATUS, /**< Status report command */ OSDP_CMD_COMSET_DONE, /**< Comset completed; Alias for OSDP_CMD_COMSET */ OSDP_CMD_NOTIFICATION,/**< LibOSDP notification (PD mode, synthesized) */ + OSDP_CMD_XWR, /**< Transparent mode command */ OSDP_CMD_SENTINEL /**< Max command value */ }; +/** + * @brief Transparent Reader Support (TRS) commands a CP application can issue to + * a smart card in the reader. Set in @c struct osdp_trs_cmd::command and + * submitted as an @c OSDP_CMD_XWR command. + * + * A card session is the band of commands between an @c OSDP_TRS_CMD_START and + * the matching @c OSDP_TRS_CMD_STOP, in submission order: + * + * START, SEND_APDU, SEND_APDU, ..., STOP + * + * The library owns the wire handshakes the band implies (it negotiates + * transparent mode on START, and disconnects the card and restores the reader + * on STOP) and reports the session's progress as @c OSDP_NOTIFICATION_TRS_STATUS + * events. The band is enforced when a command is submitted, so misuse is an + * error from osdp_cp_submit_command() and never a command that runs and then + * has to be taken back: inside a band only TRS commands are accepted (anything + * else would interrupt the card transaction), and a card command is refused + * outside one (it would reach a reader that is not in transparent mode). + * + * To abandon a session and the APDUs still queued for it, flush the command + * queue (osdp_cp_flush_commands()) and then submit @c OSDP_TRS_CMD_STOP; the + * flushed APDUs are reported to the app individually. If the session ends on its + * own before its STOP is reached -- the reader rejects transparent mode, say -- + * the APDUs left in the band are completed @c OSDP_COMPLETION_FLUSHED and an + * @c OSDP_TRS_SESSION_FAILED notification says why. + */ +enum osdp_trs_cmd_e { + OSDP_TRS_CMD_START = 1, /**< Open a card session */ + OSDP_TRS_CMD_STOP, /**< Close the card session opened by START */ + OSDP_TRS_CMD_SEND_APDU, /**< Send a C-APDU to the card */ + OSDP_TRS_CMD_ENTER_PIN, /**< EMV PIN entry */ + OSDP_TRS_CMD_CARD_SCAN, /**< Scan for a card in the field */ +}; + +/** + * @brief Life-cycle of a TRS card session, reported as @a arg0 of an + * @c OSDP_NOTIFICATION_TRS_STATUS notification. + */ +enum osdp_trs_session_status_e { + OSDP_TRS_SESSION_OPENED = 0, /**< Reader is in transparent mode; APDUs flow */ + OSDP_TRS_SESSION_CLOSED, /**< STOP honoured; reader restored */ + OSDP_TRS_SESSION_FAILED, /**< PD refused transparent mode, or the + * session was aborted by a link error */ +}; + +/** @brief Max APDU length carried in a TRS command or reply */ +#define OSDP_TRS_APDU_MAX_LEN 64 +/** @brief Max CSN length carried in a TRS card-info reply */ +#define OSDP_TRS_CSN_MAX_LEN 32 +/** @brief Max protocol-data length carried in a TRS card-info reply */ +#define OSDP_TRS_PROTOCOL_DATA_MAX_LEN 64 + +struct osdp_trs_apdu { + uint16_t length; /**< APDU length in bytes */ + uint8_t data[OSDP_TRS_APDU_MAX_LEN]; /**< APDU bytes */ +}; + +/** @brief Encoding of the PIN digits the reader inserts into the C-APDU */ +enum osdp_trs_pin_format_e { + OSDP_TRS_PIN_FORMAT_BINARY = 1, /**< PIN digits as binary values */ + OSDP_TRS_PIN_FORMAT_BCD, /**< PIN digits as packed BCD */ + OSDP_TRS_PIN_FORMAT_ASCII, /**< PIN digits as ASCII characters */ +}; + +/** + * @brief Conditions that end PIN entry; OR them into + * @c osdp_trs_pin_entry::complete_on. + */ +enum osdp_trs_pin_complete_e { + OSDP_TRS_PIN_COMPLETE_ON_MAX_DIGITS = 1 << 0, /**< max_digits entered */ + OSDP_TRS_PIN_COMPLETE_ON_KEY = 1 << 1, /**< Validation (enter) key pressed */ + OSDP_TRS_PIN_COMPLETE_ON_TIMEOUT = 1 << 2, /**< Entry timed out */ +}; + +/** + * @brief TRS secure PIN entry request: the reader prompts the user for their + * PIN, inserts it into @a apdu as described by the layout fields below, and + * sends the result to the card. + * + * APDU positions are expressed in bits from the start of the APDU payload. + * Not every position is expressible on the wire: it must be byte-aligned (up + * to 120 bits) or fall within the first 15 bits; anything else fails the + * command submission. + */ +struct osdp_trs_pin_entry { + uint8_t timeout_initial; /**< First-digit timeout in seconds (0 = reader default) */ + uint8_t timeout_digit; /**< Per-digit timeout in seconds after the first key */ + + /** + * The PIN block: the region of the C-APDU where the reader formats + * and inserts the entered PIN. + */ + struct { + enum osdp_trs_pin_format_e format; /**< PIN digit encoding */ + bool right_justify; /**< Right-justify the PIN within the block + * (default: left-justified) */ + uint16_t offset_bits; /**< Block position in the APDU payload, in bits */ + uint8_t size_bytes; /**< Block size in bytes, after justification + * and formatting, as defined by the card + * scheme (8 for EMV/ISO 9564 PIN blocks) */ + } pin_block; + + /* + * Optional slot in the C-APDU where the reader records how many PIN + * digits the user entered; the app cannot pre-fill it because only + * the reader knows the entered length. + */ + struct { + uint8_t size_bits; /**< Slot size in bits (0 = APDU has no such slot) */ + uint16_t offset_bits; /**< Slot position in the APDU payload, in bits */ + } pin_length_field; + + uint8_t min_digits; /**< Minimum PIN length, in digits */ + uint8_t max_digits; /**< Maximum PIN length, in digits */ + uint32_t complete_on; /**< When PIN entry ends: OR of + * enum osdp_trs_pin_complete_e conditions */ + + uint8_t num_messages; /**< Number of display messages */ + uint16_t language_id; /**< Display language identifier */ + uint8_t msg_index; /**< Index of the message to display */ + uint8_t teo_prologue[3]; /**< T=1 protocol prologue */ + struct osdp_trs_apdu apdu; /**< C-APDU to send after PIN entry */ +}; + +struct osdp_trs_cmd { + enum osdp_trs_cmd_e command; /**< Which TRS command; selects the union */ + union { + struct osdp_trs_apdu apdu; /**< For OSDP_TRS_CMD_SEND_APDU */ + struct osdp_trs_pin_entry pin_entry; + }; +}; + +/** @brief Smart-card communication protocol reported in a TRS card-info reply */ +enum osdp_trs_card_protocol_e { + OSDP_TRS_CARD_PROTOCOL_CONTACT = 1, /**< ISO 7816 contact (T=0/T=1) */ + OSDP_TRS_CARD_PROTOCOL_CONTACTLESS, /**< ISO 14443 A/B contactless */ +}; + +struct osdp_trs_card_info { + uint8_t reader; /**< Reader number (0 = first, 1 = second) */ + enum osdp_trs_card_protocol_e protocol; /**< Card communication protocol */ + uint8_t csn_len; /**< Length of @a csn in bytes */ + uint8_t csn[OSDP_TRS_CSN_MAX_LEN]; /**< Card serial number */ + uint8_t protocol_data_len; /**< Length of @a protocol_data in bytes */ + /** ATR (contact) or ATS/ATQB (contactless) */ + uint8_t protocol_data[OSDP_TRS_PROTOCOL_DATA_MAX_LEN]; +}; + +/** @brief Smart-card presence (and interface) reported in a TRS card-present reply */ +enum osdp_trs_card_status_e { + OSDP_TRS_CARD_NOT_PRESENT = 1, /**< No card detected */ + OSDP_TRS_CARD_PRESENT, /**< Card present; interface not specified */ + OSDP_TRS_CARD_PRESENT_CONTACTLESS, /**< Card present on the contactless (ISO 14443) interface */ + OSDP_TRS_CARD_PRESENT_CONTACT, /**< Card present on the contact (ISO 7816) interface */ +}; + +struct osdp_trs_card_present { + uint8_t reader; /**< Reader number (0 = first, 1 = second) */ + enum osdp_trs_card_status_e status; /**< Smart-card presence status */ +}; + +struct osdp_trs_card_data { + uint8_t reader; /**< Reader number (0 = first, 1 = second) */ + uint8_t status; /**< Result of the APDU exchange as reported by the reader + * (reader-defined; not standardized by OSDP) */ + struct osdp_trs_apdu apdu; /**< R-APDU returned by the card */ +}; + +struct osdp_trs_pin_complete { + uint8_t reader; /**< Reader number (0 = first, 1 = second) */ + uint8_t status; /**< Result of the secure PIN entry sequence as reported by the + * reader (reader-defined; not standardized by OSDP) */ + uint8_t tries; /**< Number of PIN-entry attempts */ +}; + +struct osdp_trs_error { + uint8_t code; /**< Error/NAK condition from the reader or card + * (reader-defined; not standardized by OSDP) */ +}; + +/** + * @brief Transparent Reader Support (TRS) replies delivered to a CP application + * as an @c OSDP_EVENT_TRS event, or submitted by a PD application (via + * @c osdp_pd_submit_event) in answer to a TRS command. The @a reply field + * selects the active union member. + */ +enum osdp_trs_reply_e { + OSDP_TRS_REPLY_CARD_INFO = 1, /**< A card entered the field (CSN, protocol) */ + OSDP_TRS_REPLY_CARD_PRESENT, /**< Card-present status for a reader */ + OSDP_TRS_REPLY_CARD_DATA, /**< R-APDU returned by the card */ + OSDP_TRS_REPLY_PIN_COMPLETE, /**< PIN entry completed */ + OSDP_TRS_REPLY_ERROR, /**< Transparent-mode error / NAK from reader */ +}; + +struct osdp_trs_reply { + enum osdp_trs_reply_e reply; /**< Which TRS reply; selects the union */ + union { + struct osdp_trs_card_info card_info; + struct osdp_trs_card_present card_present; + struct osdp_trs_card_data card_data; + struct osdp_trs_pin_complete pin_complete; + struct osdp_trs_error error; + }; +}; + /** * @brief When set (`struct osdp_cmd::flags`), the command is sent out with the * OSDP packet broadcast flag to the PD. @@ -875,6 +1091,7 @@ struct osdp_cmd { struct osdp_cmd_file_tx file_tx; /**< File transfer command structure */ struct osdp_status_report status; /**< Status report command structure */ struct osdp_notification notif; /**< LibOSDP notification (PD mode) */ + struct osdp_trs_cmd trs; /**< Transparent mode command structure */ }; }; @@ -978,6 +1195,7 @@ enum osdp_event_type { OSDP_EVENT_MFGREP, /**< Manufacturer specific reply event */ OSDP_EVENT_STATUS, /**< Status event */ OSDP_EVENT_NOTIFICATION, /**< LibOSDP notification event */ + OSDP_EVENT_TRS, /**< Transparent mode response event */ OSDP_EVENT_SENTINEL /**< Max event value */ }; @@ -995,6 +1213,7 @@ struct osdp_event { struct osdp_event_mfgrep mfgrep; /**< Manufacturer specific response event struture */ struct osdp_status_report status; /**< Status report event structure */ struct osdp_notification notif; /**< LibOSDP notification (CP mode) */ + struct osdp_trs_reply trs; /**< Transparent mode reply event structure */ }; }; diff --git a/library.json b/library.json index 14016901..eab77cb0 100644 --- a/library.json +++ b/library.json @@ -14,6 +14,7 @@ "srcFilter": [ "+<**/*.c>", "-", + "-", "-", "-", "+<../utils/src/disjoint_set.c>", diff --git a/python/setup.py b/python/setup.py index 917ef695..59580198 100644 --- a/python/setup.py +++ b/python/setup.py @@ -144,6 +144,7 @@ def try_vendor_sources(src_dir, src_files, vendor_dir): "src/osdp_common.h", "src/osdp_file.h", "src/osdp_metrics.h", + "src/osdp_trs.h", "src/crypto/tinyaes_src.h", ] @@ -168,6 +169,9 @@ def try_vendor_sources(src_dir, src_files, vendor_dir): "src/osdp_diag.h", "utils/include/utils/pcap_gen.h", "utils/src/pcap_gen.c", + + # Optional when TRS is enabled + "src/osdp_trs.c", ] # LICENSE lives at the repo root; vendor a copy so wheel/sdist builds @@ -176,6 +180,8 @@ def try_vendor_sources(src_dir, src_files, vendor_dir): definitions = [ "OPT_OSDP_PACKET_TRACE", + # osdp_sys exposes no TRS bindings yet; keep TRS out of the extension. + # "OPT_BUILD_OSDP_TRS", # "OPT_OSDP_DATA_TRACE", # "OPT_OSDP_SKIP_MARK_BYTE", ] @@ -195,6 +201,11 @@ def try_vendor_sources(src_dir, src_files, vendor_dir): "utils/src/pcap_gen.c", ] +if "OPT_BUILD_OSDP_TRS" in definitions: + source_files += [ + "src/osdp_trs.c", + ] + source_files = add_prefix_to_path(source_files, "vendor") include_dirs = [ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c75501c7..22bedccd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,6 +36,10 @@ if (OPT_OSDP_STATIC) list(APPEND LIB_OSDP_DEFINITIONS "-DOPT_OSDP_STATIC=1") endif() +if (OPT_BUILD_OSDP_TRS) + list(APPEND LIB_OSDP_DEFINITIONS "-DOPT_BUILD_OSDP_TRS") +endif() + # Crypto backend selection driven by OPT_OSDP_CRYPTO_BACKEND: # auto - probe openssl, then mbedtls, else fall back to bundled tinyaes # openssl - require OpenSSL (hard-fail if missing) @@ -93,6 +97,12 @@ if (OPT_OSDP_PACKET_TRACE OR OPT_OSDP_DATA_TRACE) ) endif() +if (OPT_BUILD_OSDP_TRS) + list(APPEND LIB_OSDP_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/osdp_trs.c + ) +endif() + list(APPEND LIB_OSDP_INCLUDE_DIRS ${PROJECT_BINARY_DIR}/include ) diff --git a/src/osdp_common.c b/src/osdp_common.c index 2d09be19..4fbad31c 100644 --- a/src/osdp_common.c +++ b/src/osdp_common.c @@ -404,7 +404,7 @@ void osdp_get_status_mask(const osdp_t *ctx, uint8_t *bitmask) *mask = 0; } pd = osdp_to_pd(ctx, i); - if (pd->state == OSDP_CP_STATE_ONLINE) { + if (cp_is_online(pd)) { *mask |= 1 << pos; } } diff --git a/src/osdp_common.h b/src/osdp_common.h index 96fe2589..1970c6b3 100644 --- a/src/osdp_common.h +++ b/src/osdp_common.h @@ -318,6 +318,8 @@ static inline __noreturn void die() #define PD_FLAG_PKT_BROADCAST BIT(13) /* this packet was addressed to 0x7F */ #define PD_FLAG_CP_USE_CRC BIT(14) /* CP uses CRC-16 instead of checksum */ #define PD_FLAG_ONLINE BIT(15) /* PD mode: CP link is active */ +#define PD_FLAG_TRS_CAPABLE BIT(16) /* PD reported a smart card reader */ +#define PD_FLAG_TRS_ACTIVE BIT(17) /* a TRS card session is in progress */ /* PD Init flags */ #define PD_FLAG_ENFORCE_SECURE BIT(24) /* See: OSDP_FLAG_ENFORCE_SECURE */ @@ -334,6 +336,11 @@ static inline __noreturn void die() #define CP_REQ_DISABLE 0x00000008 #define CP_REQ_ENABLE 0x00000010 +#define TRS_MODE_00 0x00 +#define TRS_MODE_01 0x01 + +#define TRS_DISABLE_CARD_INFO_REPORT 0x00 + enum osdp_cp_phy_state_e { OSDP_CP_PHY_STATE_IDLE, OSDP_CP_PHY_STATE_SEND_CMD, @@ -355,9 +362,35 @@ enum osdp_cp_state_e { OSDP_CP_STATE_PROBE, OSDP_CP_STATE_OFFLINE, OSDP_CP_STATE_DISABLED, + OSDP_CP_STATE_TRS_SETUP, + OSDP_CP_STATE_TRS_RUN, OSDP_CP_STATE_SENTINEL }; +#ifdef OPT_BUILD_OSDP_TRS +enum trs_state_e { + TRS_STATE_SET_MODE, /* CP negotiating TRS mode 01 with PD */ + TRS_STATE_XMIT, /* session up; draining app APDU commands */ + TRS_STATE_DISCONNECT_CARD, /* terminating the current card session */ + TRS_STATE_TEARDOWN, /* CP restoring TRS mode 00 (transparent off) */ + TRS_STATE_DONE, /* session finished; return to ONLINE */ +}; + +struct osdp_trs { + enum trs_state_e state; /* current TRS session sub-state */ + uint8_t mode; /* negotiated TRS mode (TRS_MODE_00/01) */ + bool failed; /* session did not open, or was cut short by an error */ + /* + * Tracks the tail of cmd_queue, not the session: set when a START is + * queued and cleared when its STOP is. A command is inside the band iff + * this is set when it is submitted; pd->state cannot answer that, as it + * trails the tail by everything still queued behind it. + */ + bool band_open; + bool stop_pending; /* this session's STOP has not been dequeued yet */ +}; +#endif + enum osdp_pkt_errors_e { OSDP_ERR_PKT_NONE = 0, /** @@ -513,6 +546,9 @@ struct osdp_pd { struct osdp_secure_channel sc; /* Secure Channel session context */ struct osdp_file *file; /* File transfer context */ struct osdp_metrics metrics; /* link/protocol health counters */ +#ifdef OPT_BUILD_OSDP_TRS + struct osdp_trs trs; /* TRS mode context (embedded) */ +#endif /* PD command callback to app with opaque arg pointer as passed by app */ void *command_callback_arg; @@ -548,6 +584,12 @@ struct osdp { void osdp_keyset_complete(struct osdp_pd *pd); +#ifdef OPT_BUILD_OSDP_TRS +/* --- from osdp_trs.c --- */ +enum osdp_cp_state_e osdp_trs_state_update(struct osdp_pd *pd); +enum osdp_cp_state_e osdp_trs_state_update_err(struct osdp_pd *pd); +#endif + /* --- from osdp_phy.c --- */ int osdp_phy_packet_init(struct osdp_pd *p, uint8_t *buf, int max_len); int osdp_phy_check_packet(struct osdp_pd *pd); @@ -793,6 +835,31 @@ static inline void pd_set_offline(struct osdp_pd *pd) static inline void make_request(struct osdp_pd *pd, uint32_t req) { pd->request |= req; } +static inline bool trs_capable(struct osdp_pd *pd) +{ + return ISSET_FLAG(pd, PD_FLAG_TRS_CAPABLE); +} + +static inline bool trs_active(struct osdp_pd *pd) +{ + return ISSET_FLAG(pd, PD_FLAG_TRS_ACTIVE); +} + +/* + * A TRS session runs on top of an established link: the PD is still online, + * just busy with a card. Use this over a bare `state == ONLINE` test wherever + * the question is "is this PD up and serving the app?". + */ +static inline bool cp_is_online(struct osdp_pd *pd) +{ +#ifdef OPT_BUILD_OSDP_TRS + if (pd->state == OSDP_CP_STATE_TRS_SETUP || + pd->state == OSDP_CP_STATE_TRS_RUN) { + return true; + } +#endif + return pd->state == OSDP_CP_STATE_ONLINE; +} static inline bool check_request(struct osdp_pd *pd, uint32_t req) { if (pd->request & req) { diff --git a/src/osdp_cp.c b/src/osdp_cp.c index 15d411c6..32fa764c 100644 --- a/src/osdp_cp.c +++ b/src/osdp_cp.c @@ -10,6 +10,14 @@ #include "osdp_file.h" #include "osdp_diag.h" #include "osdp_metrics.h" +#include "osdp_trs.h" + +/* + * Pseudo command id returned by a command selector to ask state_update() for a + * state change instead of a wire command. Kept clear of the OSDP command code + * range (a byte) so it can never be mistaken for one. + */ +#define CP_CMD_TRS_BAND_OPEN 0x100 #define CMD_POLL_LEN 1 #define CMD_LSTAT_LEN 1 @@ -55,8 +63,7 @@ enum osdp_cp_error_e { OSDP_CP_ERR_APP = -8, /* Application layer error */ }; -static void cp_dispatch_event(struct osdp_pd *pd, - const struct osdp_event *event) +static void cp_dispatch_event(struct osdp_pd *pd, const struct osdp_event *event) { struct osdp *ctx = pd_to_osdp(pd); @@ -95,6 +102,14 @@ static int cp_cmd_dequeue(struct osdp_pd *pd, const struct osdp_cmd **cmd) return 0; } +#ifdef OPT_BUILD_OSDP_TRS +/* TRS session markers are consumed by the library; they never go on the wire */ +static bool cp_cmd_is_trs(const struct osdp_cmd *cmd, enum osdp_trs_cmd_e which) +{ + return cmd->id == OSDP_CMD_XWR && cmd->trs.command == which; +} +#endif + static inline void cp_complete_cmd(struct osdp_pd *pd, const struct osdp_cmd *cmd, enum osdp_completion_status status) @@ -291,6 +306,20 @@ static int cp_build_command(struct osdp_pd *pd, const struct osdp_cmd *active_cm buf[len++] = pd->cmd_id; len += ret; break; + case CMD_XWR: + buf[len++] = pd->cmd_id; + if (cmd) { + /* app-issued transparent command (send-APDU / PIN / scan) */ + ret = osdp_trs_cmd_build(pd, cmd, buf + len, max_len - len); + } else { + /* library-driven mode-set / terminate for this session */ + ret = osdp_trs_local_cmd_build(pd, buf + len, max_len - len); + } + if (ret <= 0) { + return OSDP_CP_ERR_GENERIC; + } + len += ret; + break; case CMD_KEYSET: if (!sc_is_active(pd)) { LOG_ERR("Cannot perform KEYSET without SC!"); @@ -448,6 +477,15 @@ static int cp_decode_response(struct osdp_pd *pd, uint8_t *buf, int len) CLEAR_FLAG(pd, PD_FLAG_CP_USE_CRC); } } + + /* Check for Transparent Reader Support */ + t = OSDP_PD_CAP_SMART_CARD_SUPPORT; + if (pd->cap[t].compliance_level & 0x01) { + SET_FLAG(pd, PD_FLAG_TRS_CAPABLE); + } else { + CLEAR_FLAG(pd, PD_FLAG_TRS_CAPABLE); + } + ret = OSDP_CP_ERR_NONE; break; case REPLY_OSTATR: @@ -585,6 +623,20 @@ static int cp_decode_response(struct osdp_pd *pd, uint8_t *buf, int len) case REPLY_FTSTAT: ret = osdp_file_cmd_stat_decode(pd, buf + pos, len); break; + case REPLY_XRD: + if (!trs_active(pd)) { + LOG_WRN("Unsolicited REPLY_XRD outside a TRS session"); + return OSDP_CP_ERR_UNKNOWN; + } + ret = osdp_trs_reply_decode(pd, buf + pos, len, &event); + if (ret < 0) { + break; + } + if (ret == OSDP_TRS_REPLY_ACTION_DISPATCH) { + cp_dispatch_event(pd, &event); + } + ret = OSDP_CP_ERR_NONE; + break; case REPLY_CCRYPT: if (sc_is_active(pd) || pd->cmd_id != CMD_CHLNG) { LOG_EM("Out of order REPLY_CCRYPT; has PD gone rogue?"); @@ -736,6 +788,7 @@ static int cp_translate_cmd(struct osdp_pd *pd, const struct osdp_cmd *cmd) case OSDP_CMD_TEXT: return CMD_TEXT; case OSDP_CMD_COMSET: return CMD_COMSET; case OSDP_CMD_MFG: return CMD_MFG; + case OSDP_CMD_XWR: return CMD_XWR; case OSDP_CMD_STATUS: switch (cmd->status.type) { case OSDP_STATUS_REPORT_INPUT: return CMD_ISTAT; @@ -930,29 +983,95 @@ static const char *state_get_name(enum osdp_cp_state_e state) case OSDP_CP_STATE_ONLINE: return "Online"; case OSDP_CP_STATE_OFFLINE: return "Offline"; case OSDP_CP_STATE_DISABLED: return "Disabled"; + case OSDP_CP_STATE_TRS_SETUP: + return "TRS-Setup"; + case OSDP_CP_STATE_TRS_RUN: + return "TRS-Run"; default: BUG(); } } +/* + * Set a dequeued command up as the active command and return the wire cmd_id to + * send for it, or -1 if it could not be translated (then it is completed FAILED). + */ +static int cp_setup_active_cmd(struct osdp_pd *pd, const struct osdp_cmd *cmd) +{ + int ret; + + ret = cp_translate_cmd(pd, cmd); + if (cmd->flags & OSDP_CMD_FLAG_BROADCAST) { + SET_FLAG(pd, PD_FLAG_PKT_BROADCAST); + } + if (ret < 0) { + cp_complete_cmd(pd, cmd, OSDP_COMPLETION_FAILED); + pd->active_cmd = NULL; + } else { + pd->active_cmd = cmd; + } + cp_cmd_free(pd, cmd); + return ret; +} + +#ifdef OPT_BUILD_OSDP_TRS +/* Consume a command the library handles itself; nothing goes on the wire */ +static void cp_discard_cmd(struct osdp_pd *pd, const struct osdp_cmd *cmd, + enum osdp_completion_status status) +{ + cp_complete_cmd(pd, cmd, status); + cp_cmd_free(pd, cmd); + pd->active_cmd = NULL; +} + +/* + * A session that died before reaching its STOP leaves the rest of its band + * queued behind it with no session left to run in. Drop it, up to and including + * the STOP that would have closed it, so that whatever the app queued after the + * band runs as it would have. + */ +static void cp_flush_trs_band(struct osdp_pd *pd) +{ + const struct osdp_cmd *cmd; + bool stop_seen = false; + + while (!stop_seen && cp_cmd_dequeue(pd, &cmd) == 0) { + stop_seen = cp_cmd_is_trs(cmd, OSDP_TRS_CMD_STOP); + cp_complete_cmd(pd, cmd, OSDP_COMPLETION_FLUSHED); + cp_cmd_free(pd, cmd); + } + if (!stop_seen) { + /* Ran to the end of the queue: the STOP was never submitted, so + * the tail is inside the band we just dropped. */ + pd->trs.band_open = false; + } +} +#endif + +/* Poll only as often as OSDP_PD_POLL_TIMEOUT_MS allows; -1 means "not yet" */ +static int cp_get_poll_command(struct osdp_pd *pd) +{ + if (osdp_millis_since(pd->tstamp) > OSDP_PD_POLL_TIMEOUT_MS) { + pd->tstamp = osdp_millis_now(); + return CMD_POLL; + } + + return -1; +} + static int cp_get_online_command(struct osdp_pd *pd) { const struct osdp_cmd *cmd; int ret; if (cp_cmd_dequeue(pd, &cmd) == 0) { - ret = cp_translate_cmd(pd, cmd); - if (cmd->flags & OSDP_CMD_FLAG_BROADCAST) { - SET_FLAG(pd, PD_FLAG_PKT_BROADCAST); +#ifdef OPT_BUILD_OSDP_TRS + if (cp_cmd_is_trs(cmd, OSDP_TRS_CMD_START)) { + cp_discard_cmd(pd, cmd, OSDP_COMPLETION_OK); + return CP_CMD_TRS_BAND_OPEN; } - if (ret < 0) { - cp_complete_cmd(pd, cmd, OSDP_COMPLETION_FAILED); - pd->active_cmd = NULL; - } else { - pd->active_cmd = cmd; - } - cp_cmd_free(pd, cmd); - return ret; +#endif + return cp_setup_active_cmd(pd, cmd); } ret = osdp_file_tx_get_command(pd); @@ -960,13 +1079,46 @@ static int cp_get_online_command(struct osdp_pd *pd) return ret; } - if (osdp_millis_since(pd->tstamp) > OSDP_PD_POLL_TIMEOUT_MS) { - pd->tstamp = osdp_millis_now(); - return CMD_POLL; - } + return cp_get_poll_command(pd); +} - return -1; +#ifdef OPT_BUILD_OSDP_TRS +/* + * Command selector for a TRS session (the band of commands between a START and + * its STOP). Mode negotiation and teardown are library-generated straight from + * pd->trs.state (active_cmd == NULL, so cp_build_command serializes them via + * osdp_trs_local_cmd_build); in XMIT the band's card commands are drained in + * order, and CMD_POLL keeps the link warm between them. + * + * Everything the queue can hand us here is a card command or the STOP that ends + * the band; cp_band_admit() turned away anything else when it was submitted. + */ +static int cp_get_trs_command(struct osdp_pd *pd) +{ + const struct osdp_cmd *cmd; + + switch (pd->trs.state) { + case TRS_STATE_XMIT: + if (cp_cmd_dequeue(pd, &cmd)) { + return cp_get_poll_command(pd); + } + if (cp_cmd_is_trs(cmd, OSDP_TRS_CMD_STOP)) { + cp_discard_cmd(pd, cmd, OSDP_COMPLETION_OK); + pd->trs.stop_pending = false; + pd->trs.state = TRS_STATE_DISCONNECT_CARD; + return CMD_XWR; + } + return cp_setup_active_cmd(pd, cmd); + case TRS_STATE_SET_MODE: + case TRS_STATE_DISCONNECT_CARD: + case TRS_STATE_TEARDOWN: + pd->active_cmd = NULL; + return CMD_XWR; + default: + return cp_get_poll_command(pd); + } } +#endif static void notify_pd_status(struct osdp_pd *pd, bool is_online) { @@ -984,6 +1136,25 @@ static void notify_pd_status(struct osdp_pd *pd, bool is_online) osdp_metrics_report(pd, OSDP_METRIC_EVENT); } +#ifdef OPT_BUILD_OSDP_TRS +static void notify_trs_status(struct osdp_pd *pd, + enum osdp_trs_session_status_e status) +{ + struct osdp *ctx = pd_to_osdp(pd); + struct osdp_event evt; + + if (!ctx->event_callback || !is_notifications_enabled(pd)) { + return; + } + + evt.type = OSDP_EVENT_NOTIFICATION; + evt.notif.type = OSDP_NOTIFICATION_TRS_STATUS; + evt.notif.arg0 = status; + ctx->event_callback(ctx->event_callback_arg, pd->idx, &evt); + osdp_metrics_report(pd, OSDP_METRIC_EVENT); +} +#endif + static void notify_sc_status(struct osdp_pd *pd) { struct osdp *ctx = pd_to_osdp(pd); @@ -1034,7 +1205,7 @@ static void cp_keyset_complete(struct osdp_pd *pd) } sc_deactivate(pd); notify_sc_status(pd); - if (pd->state == OSDP_CP_STATE_ONLINE) { + if (cp_is_online(pd)) { make_request(pd, CP_REQ_RESTART_SC); LOG_INF("SCBK set; restarting SC to verify new SCBK"); } @@ -1069,7 +1240,8 @@ static bool cp_check_online_response(struct osdp_pd *pd) pd->reply_id == REPLY_RSTATR || pd->reply_id == REPLY_MFGREP || pd->reply_id == REPLY_RAW || - pd->reply_id == REPLY_KEYPAD) { + pd->reply_id == REPLY_KEYPAD || + (pd->reply_id == REPLY_XRD && trs_active(pd))) { return true; } return is_ignore_unsolicited_messages(pd); @@ -1087,6 +1259,7 @@ static bool cp_check_online_response(struct osdp_pd *pd) case CMD_OSTAT: return pd->reply_id == REPLY_OSTATR; case CMD_OUT: return pd->reply_id == REPLY_OSTATR; case CMD_RSTAT: return pd->reply_id == REPLY_RSTATR; + case CMD_XWR: return pd->reply_id == REPLY_XRD; default: LOG_ERR("Unexpected respose: CMD: %s(%02x) REPLY: %s(%02x)", osdp_cmd_name(pd->cmd_id), pd->cmd_id, @@ -1106,6 +1279,11 @@ static inline int state_get_cmd(struct osdp_pd *pd) case OSDP_CP_STATE_SC_SCRYPT: return CMD_SCRYPT; case OSDP_CP_STATE_SET_SCBK: return CMD_KEYSET; case OSDP_CP_STATE_ONLINE: return cp_get_online_command(pd); +#ifdef OPT_BUILD_OSDP_TRS + case OSDP_CP_STATE_TRS_SETUP: + case OSDP_CP_STATE_TRS_RUN: + return cp_get_trs_command(pd); +#endif default: return -1; } } @@ -1121,6 +1299,11 @@ static inline bool state_check_reply(struct osdp_pd *pd) case OSDP_CP_STATE_SC_SCRYPT: return pd->reply_id == REPLY_RMAC_I; case OSDP_CP_STATE_SET_SCBK: return pd->reply_id == REPLY_ACK; case OSDP_CP_STATE_ONLINE: return cp_check_online_response(pd); +#ifdef OPT_BUILD_OSDP_TRS + case OSDP_CP_STATE_TRS_SETUP: + case OSDP_CP_STATE_TRS_RUN: + return cp_check_online_response(pd); +#endif default: return false; } } @@ -1171,6 +1354,11 @@ static enum osdp_cp_state_e get_next_ok_state(struct osdp_pd *pd) return OSDP_CP_STATE_OFFLINE; case OSDP_CP_STATE_DISABLED: return OSDP_CP_STATE_DISABLED; +#ifdef OPT_BUILD_OSDP_TRS + case OSDP_CP_STATE_TRS_SETUP: + case OSDP_CP_STATE_TRS_RUN: + return osdp_trs_state_update(pd); +#endif default: BUG(); } } @@ -1224,6 +1412,11 @@ static enum osdp_cp_state_e get_next_err_state(struct osdp_pd *pd) return OSDP_CP_STATE_OFFLINE; case OSDP_CP_STATE_DISABLED: return OSDP_CP_STATE_DISABLED; +#ifdef OPT_BUILD_OSDP_TRS + case OSDP_CP_STATE_TRS_SETUP: + case OSDP_CP_STATE_TRS_RUN: + return osdp_trs_state_update_err(pd); +#endif default: BUG(); } } @@ -1242,6 +1435,23 @@ static void cp_state_change(struct osdp_pd *pd, enum osdp_cp_state_e next) osdp_phy_state_reset(pd, true); break; case OSDP_CP_STATE_ONLINE: + CLEAR_FLAG(pd, PD_FLAG_TRS_ACTIVE); +#ifdef OPT_BUILD_OSDP_TRS + /* End of a TRS session: the PD never left online, so report the + * session outcome instead of a spurious online transition. */ + if (cur == OSDP_CP_STATE_TRS_SETUP || + cur == OSDP_CP_STATE_TRS_RUN) { + LOG_INF("TRS session %s", + pd->trs.failed ? "failed" : "closed"); + if (pd->trs.stop_pending) { + cp_flush_trs_band(pd); + } + notify_trs_status(pd, pd->trs.failed ? + OSDP_TRS_SESSION_FAILED : + OSDP_TRS_SESSION_CLOSED); + break; + } +#endif LOG_INF("Online; %s SC", sc_is_active(pd) ? "With" : "Without"); notify_pd_status(pd, true); break; @@ -1268,6 +1478,21 @@ static void cp_state_change(struct osdp_pd *pd, enum osdp_cp_state_e next) osdp_phy_state_reset(pd, true); LOG_INF("PD disabled; going offline until re-enabled"); break; +#ifdef OPT_BUILD_OSDP_TRS + case OSDP_CP_STATE_TRS_SETUP: + pd->trs.state = TRS_STATE_SET_MODE; + pd->trs.mode = TRS_MODE_00; /* until the PD accepts mode 01 */ + pd->trs.failed = false; + pd->trs.stop_pending = true; + SET_FLAG(pd, PD_FLAG_TRS_ACTIVE); + break; + case OSDP_CP_STATE_TRS_RUN: + if (cur == OSDP_CP_STATE_TRS_SETUP) { + LOG_INF("TRS session opened"); + notify_trs_status(pd, OSDP_TRS_SESSION_OPENED); + } + break; +#endif default: break; } @@ -1343,6 +1568,14 @@ static int state_update(struct osdp_pd *pd) switch (pd->phy_state) { case OSDP_CP_PHY_STATE_IDLE: pd->cmd_id = state_get_cmd(pd); +#ifdef OPT_BUILD_OSDP_TRS + if (pd->cmd_id == CP_CMD_TRS_BAND_OPEN) { + /* A START was consumed: open the session and let the TRS + * state machine pick its first wire command (mode-set) */ + cp_state_change(pd, OSDP_CP_STATE_TRS_SETUP); + pd->cmd_id = state_get_cmd(pd); + } +#endif if (pd->cmd_id > 0 && cp_phy_kick(pd)) { return OSDP_CP_ERR_DEFER; } @@ -1398,6 +1631,48 @@ static int state_update(struct osdp_pd *pd) return OSDP_CP_ERR_DEFER; } +#ifdef OPT_BUILD_OSDP_TRS +/* + * Decide whether a command may join the queue, and move the band with it. The + * band boundary is a position in the queue, so it is decided here -- at the tail + * the command is about to be appended to -- and never at dequeue: by the time a + * command is dequeued, the app has long since been told it was accepted. + */ +static int cp_band_admit(struct osdp_pd *pd, const struct osdp_cmd *cmd) +{ + if (cp_cmd_is_trs(cmd, OSDP_TRS_CMD_START)) { + if (pd->trs.band_open) { + LOG_ERR("A TRS session is already open; STOP it first"); + return -1; + } + pd->trs.band_open = true; + return 0; + } + if (cp_cmd_is_trs(cmd, OSDP_TRS_CMD_STOP)) { + if (!pd->trs.band_open) { + LOG_ERR("TRS stop without a session to close"); + return -1; + } + pd->trs.band_open = false; + return 0; + } + if (cmd->id == OSDP_CMD_XWR && !pd->trs.band_open) { + LOG_ERR("TRS command outside a session; queue a START first"); + return -1; + } + if (cmd->id != OSDP_CMD_XWR && pd->trs.band_open) { + /* + * Sending it would interrupt the card transaction; queuing it + * would stall the APDUs behind it. Neither is ours to choose. + */ + LOG_ERR("Cannot queue %s inside a TRS session", + osdp_cmd_name(cp_translate_cmd(pd, cmd))); + return -1; + } + return 0; +} +#endif + static int cp_submit_command(struct osdp_pd *pd, const struct osdp_cmd *cmd) { const uint32_t all_flags = ( @@ -1409,7 +1684,7 @@ static int cp_submit_command(struct osdp_pd *pd, const struct osdp_cmd *cmd) return -1; } - if (pd->state != OSDP_CP_STATE_ONLINE) { + if (!cp_is_online(pd)) { LOG_ERR("PD is not online"); return -1; } @@ -1441,6 +1716,24 @@ static int cp_submit_command(struct osdp_pd *pd, const struct osdp_cmd *cmd) return -1; } + if (cmd->id == OSDP_CMD_XWR) { +#ifdef OPT_BUILD_OSDP_TRS + if (!trs_capable(pd)) { + LOG_ERR("PD has no smart-card support; dropping XWR"); + return -1; + } +#else + LOG_ERR("TRS support not enabled in this build"); + return -1; +#endif + } + +#ifdef OPT_BUILD_OSDP_TRS + if (cp_band_admit(pd, cmd)) { + return -1; + } +#endif + return cp_cmd_enqueue(pd, cmd); } @@ -1765,6 +2058,15 @@ int osdp_cp_flush_commands(osdp_t *ctx, int pd_idx) cp_cmd_free(pd, cmd); count++; } +#ifdef OPT_BUILD_OSDP_TRS + /* + * The band markers went out with the queue, so the tail is now wherever + * the state machine is: still inside a session that only a STOP can + * close, or outside one. This is what makes flush-then-STOP an abort. + */ + pd->trs.band_open = ISSET_FLAG(pd, PD_FLAG_TRS_ACTIVE) && + pd->trs.stop_pending; +#endif return count; } diff --git a/src/osdp_pd.c b/src/osdp_pd.c index 67df4e88..adb394f7 100644 --- a/src/osdp_pd.c +++ b/src/osdp_pd.c @@ -8,6 +8,7 @@ #include "osdp_file.h" #include "osdp_diag.h" #include "osdp_metrics.h" +#include "osdp_trs.h" #ifndef OPT_OSDP_STATIC #include @@ -107,6 +108,16 @@ static int pd_event_dequeue(struct osdp_pd *pd, const struct osdp_event **event) return 0; } +static int pd_event_peek(struct osdp_pd *pd, const struct osdp_event **event) +{ + queue_node_t *node; + + if (queue_peek_first(&pd->event_queue, &node)) + return -1; + *event = CONTAINER_OF(node, struct osdp_event, _node); + return 0; +} + static inline void pd_complete_event(struct osdp_pd *pd, const struct osdp_event *event, enum osdp_completion_status status) @@ -163,6 +174,12 @@ static int pd_translate_event(struct osdp_pd *pd, const struct osdp_event *event case OSDP_EVENT_MFGREP: reply_code = REPLY_MFGREP; break; + case OSDP_EVENT_TRS: + /* Deferred TRS reply: a card-data/APDU response the app + * submitted after its CMD_XWR callback returned. Delivered as an + * unsolicited REPLY_XRD on this poll. */ + reply_code = REPLY_XRD; + break; default: LOG_ERR("Unknown event type %d", event->type); BUG(); @@ -730,6 +747,48 @@ static int pd_decode_command(struct osdp_pd *pd, uint8_t *buf, int len) break; } break; + case CMD_XWR: { + const struct osdp_event *trs_event; + int trs_ret; + + trs_ret = osdp_trs_cmd_decode(pd, &cmd, buf + pos, len); + if (trs_ret < 0) { + break; /* NAK */ + } + if (trs_ret == OSDP_TRS_DECODE_ACK) { + /* library-handled command (mode set / card terminate): + * acknowledge per spec Table 36/42, no app payload */ + pd->reply_id = REPLY_ACK; + ret = OSDP_PD_ERR_NONE; + break; + } + if (trs_ret == OSDP_TRS_DECODE_MODE_REPORT) { + /* mode-read (Table 39): reply with the current mode */ + pd->reply_id = REPLY_XRD; + ret = OSDP_PD_ERR_NONE; + break; + } + /* OSDP_TRS_DECODE_TO_APP: mode-1 command; hand it to the app */ + if (!do_command_callback(pd, &cmd)) { + ret = OSDP_PD_ERR_REPLY; + break; + } + /* Only consume the app's TRS answer; an older non-TRS event at + * the queue head must ride out on a poll, not be eaten here. */ + if (pd_event_peek(pd, &trs_event) == 0 && + trs_event->type == OSDP_EVENT_TRS) { + /* app answered synchronously: reply now (fast card) */ + pd_event_dequeue(pd, &trs_event); + pd->active_event = trs_event; + pd->reply_id = REPLY_XRD; + } else { + /* app deferred: ACK ("working"). The R-APDU rides out as + * a REPLY_XRD on a later poll once the app submits it. */ + pd->reply_id = REPLY_ACK; + } + ret = OSDP_PD_ERR_NONE; + break; + } case CMD_KEYSET: if (len != CMD_KEYSET_DATA_LEN) { break; @@ -1013,6 +1072,15 @@ static int pd_build_reply(struct osdp_pd *pd, uint8_t *buf, int max_len) len += ret; ret = OSDP_PD_ERR_NONE; break; + case REPLY_XRD: + buf[len++] = pd->reply_id; + ret = osdp_trs_reply_build(pd, buf + len, max_len - len); + if (ret <= 0) { + break; + } + len += ret; + ret = OSDP_PD_ERR_NONE; + break; case REPLY_CCRYPT: if (smb == NULL) { break; diff --git a/src/osdp_trs.c b/src/osdp_trs.c new file mode 100644 index 00000000..71c35a4f --- /dev/null +++ b/src/osdp_trs.c @@ -0,0 +1,807 @@ +/* + * Copyright (c) 2022 Siddharth Chandrasekaran + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file OSDP Transparent Reader Support (TRS) + * + * TRS tunnels raw smart-card APDUs from the CP, through a PD acting as a + * transparent pipe, to a contact/contactless card in the reader. It rides on + * the reserved OSDP CMD_XWR (0xA1) / REPLY_XRD (0xB1) command-reply pair. + * + * Every TRS payload begins with a 2-byte header: `mode` (0 = mode handshake, + * 1 = card session) followed by a per-mode command/reply `code`. Mode-0 + * commands (mode-get / mode-set) drive the session lifecycle and are handled + * internally by the library. Mode-1 commands (send-APDU, enter-PIN, card-scan, + * terminate) carry application payloads: + * + * CP --CMD_XWR(mode-set 01)--> PD (osdp_trs_local_cmd_build / _cmd_decode) + * CP <--REPLY_ACK-------------- PD (mode accepted) + * CP --CMD_XWR(send-apdu)-----> PD -> app callback + * CP <--REPLY_XRD(card-data)---- PD <- app osdp_pd_submit_event() + * ... CP delivers OSDP_EVENT_TRS to its app for each REPLY_XRD ... + */ + +#include "osdp_trs.h" + +#define TO_TRS(pd) (&(pd)->trs) + +/* Wire encoding of a transparent-mode command/reply: (mode << 8) | code */ +#define MODE_CODE(mode, pcmnd) (uint16_t)(((mode) & 0xff) << 8u | ((pcmnd) & 0xff)) + +#define CMD_MODE_GET MODE_CODE(0, 1) +#define CMD_MODE_SET MODE_CODE(0, 2) +#define CMD_SEND_APDU MODE_CODE(1, 1) +#define CMD_TERMINATE MODE_CODE(1, 2) +#define CMD_ENTER_PIN MODE_CODE(1, 3) +#define CMD_CARD_SCAN MODE_CODE(1, 4) + +#define REPLY_ERROR_MODE0 MODE_CODE(0, 0) +#define REPLY_CURRENT_MODE MODE_CODE(0, 1) +#define REPLY_CARD_INFO_REPORT MODE_CODE(0, 2) +#define REPLY_ERROR_MODE1 MODE_CODE(1, 0) +#define REPLY_CARD_PRESENT MODE_CODE(1, 1) +#define REPLY_CARD_DATA MODE_CODE(1, 2) +#define REPLY_PIN_ENTRY_COMPLETE MODE_CODE(1, 3) + +/* On-the-wire card-protocol codes (mapped to enum osdp_trs_card_protocol_e) */ +#define TRS_WIRE_PROTOCOL_CONTACT_T0T1 0x00 +#define TRS_WIRE_PROTOCOL_14443AB 0x01 + +/* On-the-wire card-present status codes (mapped to enum osdp_trs_card_status_e) */ +#define TRS_WIRE_CARD_NOT_PRESENT 0x00 +#define TRS_WIRE_CARD_PRESENT_UNSPECIFIED 0x01 +#define TRS_WIRE_CARD_PRESENT_CONTACTLESS 0x02 +#define TRS_WIRE_CARD_PRESENT_CONTACT 0x03 + +static int trs_card_status_from_wire(uint8_t wire, + enum osdp_trs_card_status_e *status) +{ + switch (wire) { + case TRS_WIRE_CARD_NOT_PRESENT: + *status = OSDP_TRS_CARD_NOT_PRESENT; + return 0; + case TRS_WIRE_CARD_PRESENT_UNSPECIFIED: + *status = OSDP_TRS_CARD_PRESENT; + return 0; + case TRS_WIRE_CARD_PRESENT_CONTACTLESS: + *status = OSDP_TRS_CARD_PRESENT_CONTACTLESS; + return 0; + case TRS_WIRE_CARD_PRESENT_CONTACT: + *status = OSDP_TRS_CARD_PRESENT_CONTACT; + return 0; + default: + return -1; + } +} + +static int trs_card_status_to_wire(enum osdp_trs_card_status_e status, + uint8_t *wire) +{ + switch (status) { + case OSDP_TRS_CARD_NOT_PRESENT: + *wire = TRS_WIRE_CARD_NOT_PRESENT; + return 0; + case OSDP_TRS_CARD_PRESENT: + *wire = TRS_WIRE_CARD_PRESENT_UNSPECIFIED; + return 0; + case OSDP_TRS_CARD_PRESENT_CONTACTLESS: + *wire = TRS_WIRE_CARD_PRESENT_CONTACTLESS; + return 0; + case OSDP_TRS_CARD_PRESENT_CONTACT: + *wire = TRS_WIRE_CARD_PRESENT_CONTACT; + return 0; + default: + return -1; + } +} + +/* On-the-wire PIN-format codes in bmFormatString bits 1-0 (mapped to + * enum osdp_trs_pin_format_e); 0x03 is reserved */ +#define TRS_WIRE_PIN_FORMAT_BINARY 0x00 +#define TRS_WIRE_PIN_FORMAT_BCD 0x01 +#define TRS_WIRE_PIN_FORMAT_ASCII 0x02 + +static int trs_pin_format_from_wire(uint8_t wire, + enum osdp_trs_pin_format_e *format) +{ + switch (wire) { + case TRS_WIRE_PIN_FORMAT_BINARY: + *format = OSDP_TRS_PIN_FORMAT_BINARY; + return 0; + case TRS_WIRE_PIN_FORMAT_BCD: + *format = OSDP_TRS_PIN_FORMAT_BCD; + return 0; + case TRS_WIRE_PIN_FORMAT_ASCII: + *format = OSDP_TRS_PIN_FORMAT_ASCII; + return 0; + default: + return -1; + } +} + +static int trs_pin_format_to_wire(enum osdp_trs_pin_format_e format, + uint8_t *wire) +{ + switch (format) { + case OSDP_TRS_PIN_FORMAT_BINARY: + *wire = TRS_WIRE_PIN_FORMAT_BINARY; + return 0; + case OSDP_TRS_PIN_FORMAT_BCD: + *wire = TRS_WIRE_PIN_FORMAT_BCD; + return 0; + case OSDP_TRS_PIN_FORMAT_ASCII: + *wire = TRS_WIRE_PIN_FORMAT_ASCII; + return 0; + default: + return -1; + } +} + +/* + * The wire carries APDU positions as a 4-bit offset plus a bits/bytes unit + * flag; the API expresses them plainly in bits. A position is representable + * only if it is byte-aligned (up to 15 bytes) or fits the 4-bit offset as-is + * (up to 15 bits). + */ +static int trs_pin_pos_to_wire(uint16_t pos_bits, uint8_t *offset, + bool *unit_bytes) +{ + if (pos_bits % 8 == 0 && pos_bits / 8 <= 15) { + *offset = (uint8_t)(pos_bits / 8); + *unit_bytes = true; + return 0; + } + if (pos_bits <= 15) { + *offset = (uint8_t)pos_bits; + *unit_bytes = false; + return 0; + } + return -1; +} + +static int trs_pin_pos_from_wire(uint8_t offset, bool unit_bytes) +{ + return offset * (unit_bytes ? 8 : 1); +} + +/* Validate the (mode, code) pair against the transparent-mode command space */ +static bool trs_mode_code_valid(uint8_t mode, uint8_t code) +{ + return !(code == 0 || (mode != 0 && mode != 1) || + (mode == 0 && code > 2) || (mode == 1 && code > 4)); +} + +/* --- CP: build outgoing CMD_XWR from an app command --- */ + +/* Map an app-facing TRS command to its wire (mode << 8 | code) encoding */ +static int trs_cmd_to_wire(enum osdp_trs_cmd_e command, uint16_t *mode_code) +{ + switch (command) { + case OSDP_TRS_CMD_SEND_APDU: *mode_code = CMD_SEND_APDU; return 0; + case OSDP_TRS_CMD_ENTER_PIN: *mode_code = CMD_ENTER_PIN; return 0; + case OSDP_TRS_CMD_CARD_SCAN: *mode_code = CMD_CARD_SCAN; return 0; + default: return -1; + } +} + +int osdp_trs_cmd_build(struct osdp_pd *pd, const struct osdp_cmd *cmd, + uint8_t *buf, int max_len) +{ + const struct osdp_trs_cmd *c = &cmd->trs; + int len = 0; + uint32_t apdu_length; + uint16_t mode_code; + uint8_t byte; + + ARG_UNUSED(pd); + + if (trs_cmd_to_wire(c->command, &mode_code)) { + return -1; + } + + /* App commands are all mode-1: 2-byte header + a reader byte */ + if (max_len < 3) { + return -1; + } + bwrite_u16_be(mode_code, buf, &len); + buf[len++] = 0; /* reader (always 0) */ + + switch (c->command) { + case OSDP_TRS_CMD_SEND_APDU: + apdu_length = c->apdu.length; + if (apdu_length > sizeof(c->apdu.data) || + apdu_length > (uint32_t)(max_len - len)) { + LOG_ERR("TRS: APDU length invalid! need/have: %d/%d", + (int)apdu_length, (max_len - len)); + return -1; + } + memcpy(buf + len, c->apdu.data, apdu_length); + len += apdu_length; + break; + case OSDP_TRS_CMD_ENTER_PIN: { + const struct osdp_trs_pin_entry *pe = &c->pin_entry; + bool unit_bytes; + uint8_t offset; + + if (max_len - len < 17 || + trs_pin_format_to_wire(pe->pin_block.format, &byte)) { + return -1; + } + buf[len++] = pe->timeout_initial; + buf[len++] = pe->timeout_digit; + /* bmFormatString: [7]=offset unit is bytes, [6:3]=PIN offset, + * [2]=right justify, [1:0]=PIN format */ + if (trs_pin_pos_to_wire(pe->pin_block.offset_bits, &offset, + &unit_bytes)) { + LOG_ERR("TRS: PIN offset %u not wire-representable", + pe->pin_block.offset_bits); + return -1; + } + byte |= pe->pin_block.right_justify ? BIT(2) : 0; + byte |= (uint8_t)(offset << 3); + byte |= unit_bytes ? BIT(7) : 0; + buf[len++] = byte; + /* bmPINBlockString: [7:4]=PIN-length field size (bits), + * [3:0]=PIN block size (bytes). Both are 4-bit wire nibbles; + * reject an out-of-range value rather than masking it down to + * something the reader would silently act on. */ + if (pe->pin_length_field.size_bits > 0x0f || + pe->pin_block.size_bytes > 0x0f) { + LOG_ERR("TRS: PIN length/block size not wire-representable (%u/%u)", + pe->pin_length_field.size_bits, + pe->pin_block.size_bytes); + return -1; + } + buf[len++] = (uint8_t)(pe->pin_length_field.size_bits << 4 | + pe->pin_block.size_bytes); + /* bmPINLengthFormat: [4]=offset unit is bytes, + * [3:0]=PIN-length field offset */ + if (trs_pin_pos_to_wire(pe->pin_length_field.offset_bits, + &offset, &unit_bytes)) { + LOG_ERR("TRS: PIN-length offset %u not wire-representable", + pe->pin_length_field.offset_bits); + return -1; + } + byte = offset; + byte |= unit_bytes ? BIT(4) : 0; + buf[len++] = byte; + buf[len++] = pe->min_digits; + buf[len++] = pe->max_digits; + + /* bEntryValidationCondition: + * [0]=max digits reached, + * [1]=validation key pressed + * [2]=timeout */ + byte = 0; + byte |= (pe->complete_on & OSDP_TRS_PIN_COMPLETE_ON_MAX_DIGITS) ? BIT(0) : 0; + byte |= (pe->complete_on & OSDP_TRS_PIN_COMPLETE_ON_KEY) ? BIT(1) : 0; + byte |= (pe->complete_on & OSDP_TRS_PIN_COMPLETE_ON_TIMEOUT) ? BIT(2) : 0; + buf[len++] = byte; + + buf[len++] = pe->num_messages; + bwrite_u16_be(pe->language_id, buf, &len); + buf[len++] = pe->msg_index; + buf[len++] = pe->teo_prologue[0]; + buf[len++] = pe->teo_prologue[1]; + buf[len++] = pe->teo_prologue[2]; + + apdu_length = pe->apdu.length; + /* Validate before emitting: the length field is written ahead + * of the payload, so a rejected APDU must not leave a stale + * length behind it. */ + if (apdu_length > sizeof(pe->apdu.data) || + apdu_length > (uint32_t)(max_len - len - 2)) { + LOG_ERR("TRS: PIN APDU length invalid! need/have: %d/%d", + (int)apdu_length, (max_len - len - 2)); + return -1; + } + bwrite_u16_be((uint16_t)apdu_length, buf, &len); + memcpy(buf + len, pe->apdu.data, apdu_length); + len += apdu_length; + break; + } + case OSDP_TRS_CMD_CARD_SCAN: + break; + default: + return -1; + } + return len; +} + +/* --- CP: decode incoming REPLY_XRD into an app event --- */ + +/* + * Decode a REPLY_XRD payload into *event. Returns <0 on error, else one of + * enum osdp_trs_reply_action_e telling osdp_cp.c whether the reply carried + * anything for the app: the mode handshake is the library's own business, but + * card data, card status and reader errors are dispatched as OSDP_EVENT_TRS. + */ +int osdp_trs_reply_decode(struct osdp_pd *pd, uint8_t *buf, int len, + struct osdp_event *event) +{ + uint8_t card_protocol, card_status, csn_len, prot_data_len; + uint16_t mode_code; + int pos = 0, data_len, apdu_len; + bool dispatch = true; + + if (len < 2) { + return -1; + } + + memset(event, 0, sizeof(*event)); + event->type = OSDP_EVENT_TRS; + + mode_code = bread_u16_be(buf, &pos); + data_len = len - pos; + + switch (mode_code) { + case REPLY_CURRENT_MODE: + if (data_len < 1) { + return -1; + } + /* internal handshake reply: record mode, don't notify app */ + pd->trs.mode = buf[pos++]; + dispatch = false; + break; + case REPLY_CARD_INFO_REPORT: + event->trs.reply = OSDP_TRS_REPLY_CARD_INFO; + if (data_len < 4) { + return -1; + } + event->trs.card_info.reader = buf[pos++]; + card_protocol = buf[pos++]; + if (card_protocol == TRS_WIRE_PROTOCOL_CONTACT_T0T1) { + event->trs.card_info.protocol = + OSDP_TRS_CARD_PROTOCOL_CONTACT; + } else if (card_protocol == TRS_WIRE_PROTOCOL_14443AB) { + event->trs.card_info.protocol = + OSDP_TRS_CARD_PROTOCOL_CONTACTLESS; + } else { + LOG_ERR("TRS: unsupported card protocol %02x", + card_protocol); + return -1; + } + csn_len = buf[pos++]; + prot_data_len = buf[pos++]; + if (csn_len > OSDP_TRS_CSN_MAX_LEN || + prot_data_len > OSDP_TRS_PROTOCOL_DATA_MAX_LEN) { + LOG_ERR("TRS: CSN/protocol-data too large (%d/%d)", + csn_len, prot_data_len); + return -1; + } + if (data_len < 4 + csn_len + prot_data_len) { + LOG_ERR("TRS: truncated card-info report"); + return -1; + } + event->trs.card_info.csn_len = csn_len; + event->trs.card_info.protocol_data_len = prot_data_len; + memcpy(event->trs.card_info.csn, buf + pos, csn_len); + pos += csn_len; + memcpy(event->trs.card_info.protocol_data, buf + pos, + prot_data_len); + pos += prot_data_len; + break; + case REPLY_CARD_PRESENT: + event->trs.reply = OSDP_TRS_REPLY_CARD_PRESENT; + if (data_len < 2) { + return -1; + } + event->trs.card_present.reader = buf[pos++]; + card_status = buf[pos++]; + if (trs_card_status_from_wire(card_status, + &event->trs.card_present.status)) { + LOG_ERR("TRS: reserved card-present status %02x", + card_status); + return -1; + } + break; + case REPLY_ERROR_MODE0: + case REPLY_ERROR_MODE1: + event->trs.reply = OSDP_TRS_REPLY_ERROR; + if (data_len < 1) { + return -1; + } + event->trs.error.code = buf[pos++]; + break; + case REPLY_CARD_DATA: + event->trs.reply = OSDP_TRS_REPLY_CARD_DATA; + if (data_len < 2) { + return -1; + } + event->trs.card_data.reader = buf[pos++]; + event->trs.card_data.status = buf[pos++]; + apdu_len = len - pos; + if (apdu_len > (int)sizeof(event->trs.card_data.apdu.data)) { + LOG_ERR("TRS: R-APDU too large (%d); rejecting reply", + apdu_len); + return -1; + } + event->trs.card_data.apdu.length = apdu_len; + memcpy(event->trs.card_data.apdu.data, buf + pos, apdu_len); + pos += apdu_len; + break; + case REPLY_PIN_ENTRY_COMPLETE: + event->trs.reply = OSDP_TRS_REPLY_PIN_COMPLETE; + if (data_len < 3) { + return -1; + } + event->trs.pin_complete.reader = buf[pos++]; + event->trs.pin_complete.status = buf[pos++]; + event->trs.pin_complete.tries = buf[pos++]; + break; + default: + LOG_ERR("TRS: unknown reply mode/code %04x", mode_code); + return -1; + } + + return dispatch ? OSDP_TRS_REPLY_ACTION_DISPATCH : + OSDP_TRS_REPLY_ACTION_NONE; +} + +/* --- PD: build outgoing REPLY_XRD --- */ + +/* Map an app-facing TRS reply to its wire (mode << 8 | code) encoding */ +static uint16_t trs_reply_to_wire(enum osdp_trs_reply_e reply) +{ + switch (reply) { + case OSDP_TRS_REPLY_CARD_INFO: return REPLY_CARD_INFO_REPORT; + case OSDP_TRS_REPLY_CARD_PRESENT: return REPLY_CARD_PRESENT; + case OSDP_TRS_REPLY_CARD_DATA: return REPLY_CARD_DATA; + case OSDP_TRS_REPLY_PIN_COMPLETE: return REPLY_PIN_ENTRY_COMPLETE; + case OSDP_TRS_REPLY_ERROR: return REPLY_ERROR_MODE1; + default: return 0; + } +} + +int osdp_trs_reply_build(struct osdp_pd *pd, uint8_t *buf, int max_len) +{ + int len = 0, csn_len, prot_data_len, apdu_len; + const struct osdp_event *event = pd->active_event; + const struct osdp_trs_reply *reply; + uint16_t mode_code; + uint8_t card_status; + + if (event == NULL || event->type != OSDP_EVENT_TRS) { + /* No app payload: answer with the current-mode report (Table 71), + * which carries both the mode code and its config byte. */ + if (max_len < 4) { + return -1; + } + bwrite_u16_be(REPLY_CURRENT_MODE, buf, &len); + buf[len++] = pd->trs.mode; + buf[len++] = TRS_DISABLE_CARD_INFO_REPORT; /* mode config */ + return len; + } + + reply = &event->trs; + mode_code = trs_reply_to_wire(reply->reply); + if (mode_code == 0 || max_len < 2) { + return -1; + } + bwrite_u16_be(mode_code, buf, &len); + + switch (reply->reply) { + case OSDP_TRS_REPLY_CARD_INFO: + csn_len = reply->card_info.csn_len; + prot_data_len = reply->card_info.protocol_data_len; + if (csn_len > OSDP_TRS_CSN_MAX_LEN) { + csn_len = OSDP_TRS_CSN_MAX_LEN; + } + if (prot_data_len > OSDP_TRS_PROTOCOL_DATA_MAX_LEN) { + prot_data_len = OSDP_TRS_PROTOCOL_DATA_MAX_LEN; + } + if (max_len - len < 4 + csn_len + prot_data_len) { + return -1; + } + buf[len++] = reply->card_info.reader; + buf[len++] = (reply->card_info.protocol == + OSDP_TRS_CARD_PROTOCOL_CONTACTLESS) ? + TRS_WIRE_PROTOCOL_14443AB : + TRS_WIRE_PROTOCOL_CONTACT_T0T1; + buf[len++] = (uint8_t)csn_len; + buf[len++] = (uint8_t)prot_data_len; + memcpy(buf + len, reply->card_info.csn, csn_len); + len += csn_len; + memcpy(buf + len, reply->card_info.protocol_data, + prot_data_len); + len += prot_data_len; + break; + case OSDP_TRS_REPLY_CARD_PRESENT: + if (max_len - len < 2 || + trs_card_status_to_wire(reply->card_present.status, + &card_status)) { + return -1; + } + buf[len++] = reply->card_present.reader; + buf[len++] = card_status; + break; + case OSDP_TRS_REPLY_ERROR: + if (max_len - len < 1) { + return -1; + } + buf[len++] = reply->error.code; + break; + case OSDP_TRS_REPLY_CARD_DATA: + apdu_len = reply->card_data.apdu.length; + if (apdu_len > (int)sizeof(reply->card_data.apdu.data)) { + LOG_ERR("TRS: R-APDU length invalid (%d)", apdu_len); + return -1; + } + if (max_len - len < 2 + apdu_len) { + return -1; + } + buf[len++] = reply->card_data.reader; + buf[len++] = reply->card_data.status; + memcpy(buf + len, reply->card_data.apdu.data, apdu_len); + len += apdu_len; + break; + case OSDP_TRS_REPLY_PIN_COMPLETE: + if (max_len - len < 3) { + return -1; + } + buf[len++] = reply->pin_complete.reader; + buf[len++] = reply->pin_complete.status; + buf[len++] = reply->pin_complete.tries; + break; + default: + return -1; + } + return len; +} + +/* --- PD: decode incoming CMD_XWR --- + * + * Returns <0 on error (NAK), else one of enum osdp_trs_decode_e telling the + * caller how to answer: ACK a library-handled command (mode set / card + * terminate), send an XRD mode report (mode read), or deliver the decoded + * card-session command to the app. + */ +int osdp_trs_cmd_decode(struct osdp_pd *pd, struct osdp_cmd *cmd, uint8_t *buf, + int len) +{ + int pos = 0, apdu_length; + uint8_t mode, code, new_mode; + uint16_t mode_code; + + if (len < 2) { + return -1; + } + mode_code = bread_u16_be(buf, &pos); + mode = BYTE_1(mode_code); + code = BYTE_0(mode_code); + + if (!trs_mode_code_valid(mode, code)) { + return -1; + } + + /* mode-0 (handshake) commands are handled by the library itself */ + if (mode == 0) { + if (mode_code == CMD_MODE_SET) { + if (len - pos < 2) { + return -1; + } + new_mode = buf[pos++]; + /* config byte (buf[pos]) currently unused */ + if (new_mode != TRS_MODE_00 && new_mode != TRS_MODE_01) { + LOG_ERR("TRS: unsupported mode %02x requested", + new_mode); + return -1; + } + pd->trs.mode = new_mode; + return OSDP_TRS_DECODE_ACK; /* Table 36: Mode-Set is ACK'd */ + } + /* CMD_MODE_GET (Table 39): answer with the current-mode report */ + return OSDP_TRS_DECODE_MODE_REPORT; + } + + /* Card-session commands are only meaningful once the CP has put us in + * transparent mode; refuse them otherwise. */ + if (pd->trs.mode != TRS_MODE_01) { + LOG_ERR("TRS: mode-1 command received in mode %02x", + pd->trs.mode); + return -1; + } + + /* Card-session teardown is library-driven; ACK it without the app */ + if (mode_code == CMD_TERMINATE) { + return OSDP_TRS_DECODE_ACK; + } + + /* mode-1 commands carry a reader byte and are delivered to the app */ + if (len - pos < 1) { + return -1; + } + pos++; /* reader -- always 0 */ + + cmd->id = OSDP_CMD_XWR; + + switch (mode_code) { + case CMD_SEND_APDU: + cmd->trs.command = OSDP_TRS_CMD_SEND_APDU; + apdu_length = len - pos; + if (apdu_length > (int)sizeof(cmd->trs.apdu.data)) { + LOG_ERR("TRS: C-APDU too large (%d); NAK-ing command", + apdu_length); + return -1; + } + cmd->trs.apdu.length = apdu_length; + memcpy(cmd->trs.apdu.data, buf + pos, apdu_length); + pos += apdu_length; + break; + case CMD_ENTER_PIN: { + struct osdp_trs_pin_entry *pe = &cmd->trs.pin_entry; + uint8_t byte; + + cmd->trs.command = OSDP_TRS_CMD_ENTER_PIN; + if (len - pos < 17) { + return -1; + } + pe->timeout_initial = buf[pos++]; + pe->timeout_digit = buf[pos++]; + /* bmFormatString: [7]=offset unit is bytes, [6:3]=PIN offset, + * [2]=right justify, [1:0]=PIN format */ + byte = buf[pos++]; + if (trs_pin_format_from_wire(byte & 0x03, &pe->pin_block.format)) { + LOG_ERR("TRS: reserved PIN format in %02x", byte); + return -1; + } + pe->pin_block.right_justify = byte & BIT(2); + pe->pin_block.offset_bits = + trs_pin_pos_from_wire((byte >> 3) & 0x0f, byte & BIT(7)); + /* bmPINBlockString: [7:4]=PIN-length field size (bits), + * [3:0]=PIN block size (bytes) */ + byte = buf[pos++]; + pe->pin_length_field.size_bits = (byte >> 4) & 0x0f; + pe->pin_block.size_bytes = byte & 0x0f; + /* bmPINLengthFormat: [4]=offset unit is bytes, + * [3:0]=PIN-length field offset */ + byte = buf[pos++]; + pe->pin_length_field.offset_bits = + trs_pin_pos_from_wire(byte & 0x0f, byte & BIT(4)); + pe->min_digits = buf[pos++]; + pe->max_digits = buf[pos++]; + /* bEntryValidationCondition: [0]=max digits reached, + * [1]=validation key pressed, [2]=timeout */ + byte = buf[pos++]; + pe->complete_on = 0; + pe->complete_on |= (byte & BIT(0)) ? + OSDP_TRS_PIN_COMPLETE_ON_MAX_DIGITS : 0; + pe->complete_on |= (byte & BIT(1)) ? + OSDP_TRS_PIN_COMPLETE_ON_KEY : 0; + pe->complete_on |= (byte & BIT(2)) ? + OSDP_TRS_PIN_COMPLETE_ON_TIMEOUT : 0; + pe->num_messages = buf[pos++]; + pe->language_id = bread_u16_be(buf, &pos); + pe->msg_index = buf[pos++]; + pe->teo_prologue[0] = buf[pos++]; + pe->teo_prologue[1] = buf[pos++]; + pe->teo_prologue[2] = buf[pos++]; + apdu_length = bread_u16_be(buf, &pos); + pe->apdu.length = apdu_length; + if (apdu_length > (int)sizeof(pe->apdu.data) || + apdu_length > (len - pos)) { + LOG_ERR("TRS: PIN APDU length invalid! need/have: %d/%d", + apdu_length, (len - pos)); + return -1; + } + memcpy(pe->apdu.data, buf + pos, apdu_length); + pos += apdu_length; + break; + } + case CMD_CARD_SCAN: + cmd->trs.command = OSDP_TRS_CMD_CARD_SCAN; + /* no additional payload */ + break; + default: + return -1; + } + return OSDP_TRS_DECODE_TO_APP; +} + +/* --- CP: library-driven session state machine --- */ + +/* + * Serialize the library-driven CMD_XWR for the current TRS session state: the + * transparent-mode handshake (mode-set) and card-session teardown (terminate) + * steps the CP sequences on its own. These never pass through a public + * struct osdp_trs_cmd, so their wire form is emitted straight into the buffer. + */ +int osdp_trs_local_cmd_build(struct osdp_pd *pd, uint8_t *buf, int max_len) +{ + int len = 0; + + switch (pd->trs.state) { + case TRS_STATE_SET_MODE: + case TRS_STATE_TEARDOWN: + if (max_len < 4) { + return -1; + } + bwrite_u16_be(CMD_MODE_SET, buf, &len); + buf[len++] = (pd->trs.state == TRS_STATE_SET_MODE) ? + TRS_MODE_01 : TRS_MODE_00; + buf[len++] = TRS_DISABLE_CARD_INFO_REPORT; + break; + case TRS_STATE_DISCONNECT_CARD: + if (max_len < 3) { + return -1; + } + bwrite_u16_be(CMD_TERMINATE, buf, &len); + buf[len++] = 0; /* reader (always 0) */ + break; + default: + return -1; + } + return len; +} + +/* + * Advance the TRS sub-state after the PD accepted a command, and return the next + * CP role state. The XMIT->DISCONNECT transition is driven by a stop request in + * cp_get_trs_command(); here we only handle post-reply advances. + */ +enum osdp_cp_state_e osdp_trs_state_update(struct osdp_pd *pd) +{ + switch (pd->trs.state) { + case TRS_STATE_SET_MODE: + /* + * A PD that entered transparent mode ACKs the mode-set; one that + * declined answers with a mode report naming the mode it stayed + * in (recorded by osdp_trs_reply_decode). Streaming APDUs to a + * PD that is not in mode 01 would just draw errors, so give up + * on the session instead. + */ + if (pd->reply_id == REPLY_ACK) { + pd->trs.mode = TRS_MODE_01; + } + if (pd->trs.mode != TRS_MODE_01) { + LOG_ERR("TRS: PD refused transparent mode; aborting"); + pd->trs.state = TRS_STATE_DONE; + pd->trs.failed = true; + return OSDP_CP_STATE_ONLINE; + } + pd->trs.state = TRS_STATE_XMIT; + return OSDP_CP_STATE_TRS_RUN; + case TRS_STATE_XMIT: + return OSDP_CP_STATE_TRS_RUN; + case TRS_STATE_DISCONNECT_CARD: + pd->trs.state = TRS_STATE_TEARDOWN; + return OSDP_CP_STATE_TRS_RUN; + case TRS_STATE_TEARDOWN: + pd->trs.state = TRS_STATE_DONE; + __fallthrough; + case TRS_STATE_DONE: + default: + return OSDP_CP_STATE_ONLINE; + } +} + +/* + * A TRS command failed (NAK, or no usable reply). If the PD already entered + * transparent mode, it must not be left there -- a reader in mode 01 stops + * reporting ordinary card reads -- so make one attempt to restore mode 00 + * before letting go of the session. If that attempt is what failed, the PD is + * past helping; hand it back to the online path, which takes it offline if it + * stays unresponsive. + */ +enum osdp_cp_state_e osdp_trs_state_update_err(struct osdp_pd *pd) +{ + pd->trs.failed = true; + + switch (pd->trs.state) { + case TRS_STATE_XMIT: + case TRS_STATE_DISCONNECT_CARD: + LOG_WRN("TRS: session error; restoring transparent mode off"); + pd->trs.state = TRS_STATE_TEARDOWN; + return OSDP_CP_STATE_TRS_RUN; + case TRS_STATE_TEARDOWN: + LOG_ERR("TRS: failed to restore transparent mode off"); + __fallthrough; + case TRS_STATE_SET_MODE: /* mode never took effect; nothing to undo */ + default: + pd->trs.state = TRS_STATE_DONE; + return OSDP_CP_STATE_ONLINE; + } +} diff --git a/src/osdp_trs.h b/src/osdp_trs.h new file mode 100644 index 00000000..742c5dcc --- /dev/null +++ b/src/osdp_trs.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Siddharth Chandrasekaran + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _OSDP_TRS_H_ +#define _OSDP_TRS_H_ + +#include "osdp_common.h" + +/* + * Reply action returned by osdp_trs_cmd_decode() (a negative return means NAK). + * Tells osdp_pd.c how to answer a CMD_XWR the library handled internally. + */ +enum osdp_trs_decode_e { + OSDP_TRS_DECODE_ACK = 0, /* library handled it; answer osdp_ACK */ + OSDP_TRS_DECODE_MODE_REPORT, /* answer osdp_XRD current-mode report */ + OSDP_TRS_DECODE_TO_APP, /* deliver the decoded command to the app */ +}; + +/* + * Action returned by osdp_trs_reply_decode() (a negative return means the reply + * was malformed). Tells osdp_cp.c what to do with the decoded event. + */ +enum osdp_trs_reply_action_e { + OSDP_TRS_REPLY_ACTION_NONE = 0, /* mode handshake; nothing for the app */ + OSDP_TRS_REPLY_ACTION_DISPATCH, /* deliver the decoded event to the app */ +}; + +#ifdef OPT_BUILD_OSDP_TRS + +int osdp_trs_cmd_build(struct osdp_pd *pd, const struct osdp_cmd *cmd, + uint8_t *buf, int max_len); +int osdp_trs_local_cmd_build(struct osdp_pd *pd, uint8_t *buf, int max_len); +int osdp_trs_reply_decode(struct osdp_pd *pd, uint8_t *buf, int len, + struct osdp_event *event); +int osdp_trs_reply_build(struct osdp_pd *pd, uint8_t *buf, int max_len); +int osdp_trs_cmd_decode(struct osdp_pd *pd, struct osdp_cmd *cmd, uint8_t *buf, + int len); + +#else /* OPT_BUILD_OSDP_TRS */ + +static inline int osdp_trs_cmd_build(struct osdp_pd *pd, + const struct osdp_cmd *cmd, uint8_t *buf, + int max_len) +{ + ARG_UNUSED(pd); + ARG_UNUSED(cmd); + ARG_UNUSED(buf); + ARG_UNUSED(max_len); + return -1; +} +static inline int osdp_trs_local_cmd_build(struct osdp_pd *pd, uint8_t *buf, + int max_len) +{ + ARG_UNUSED(pd); + ARG_UNUSED(buf); + ARG_UNUSED(max_len); + return -1; +} +static inline int osdp_trs_reply_decode(struct osdp_pd *pd, uint8_t *buf, + int len, struct osdp_event *event) +{ + ARG_UNUSED(pd); + ARG_UNUSED(buf); + ARG_UNUSED(len); + ARG_UNUSED(event); + return -1; +} +static inline int osdp_trs_reply_build(struct osdp_pd *pd, uint8_t *buf, int max_len) +{ + ARG_UNUSED(pd); + ARG_UNUSED(buf); + ARG_UNUSED(max_len); + return -1; +} +static inline int osdp_trs_cmd_decode(struct osdp_pd *pd, struct osdp_cmd *cmd, + uint8_t *buf, int len) +{ + ARG_UNUSED(pd); + ARG_UNUSED(cmd); + ARG_UNUSED(buf); + ARG_UNUSED(len); + return -1; +} + +#endif /* OPT_BUILD_OSDP_TRS */ + +#endif /* _OSDP_TRS_H_ */ \ No newline at end of file diff --git a/tests/unit-tests/CMakeLists.txt b/tests/unit-tests/CMakeLists.txt index 6816522a..15b27c23 100644 --- a/tests/unit-tests/CMakeLists.txt +++ b/tests/unit-tests/CMakeLists.txt @@ -33,6 +33,7 @@ list(APPEND OSDP_UNIT_TEST_SRC test-sc.c test-sc-sia-vectors.c test-pd-zc.c + test-trs.c ) add_executable(${OSDP_UNIT_TEST} EXCLUDE_FROM_ALL ${OSDP_UNIT_TEST_SRC}) diff --git a/tests/unit-tests/test-trs.c b/tests/unit-tests/test-trs.c new file mode 100644 index 00000000..1d535150 --- /dev/null +++ b/tests/unit-tests/test-trs.c @@ -0,0 +1,809 @@ +/* + * Copyright (c) 2022-2026 Siddharth Chandrasekaran + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "osdp_common.h" +#include "osdp_trs.h" +#include "test.h" + +#ifdef OPT_BUILD_OSDP_TRS + +/* + * Drives a full library-managed TRS session over the mock channel: + * CP submits OSDP_CMD_XWR(send-apdu) -> library runs mode-set handshake -> + * PD receives the APDU and answers with an OSDP_EVENT_TRS(card-data) -> + * CP delivers OSDP_EVENT_TRS to its app -> CP asks for an orderly stop. + */ + +/* APDU the CP sends and the PD echoes back inside the card-data reply */ +static const uint8_t g_req_apdu[] = { 0x00, 0xA4, 0x04, 0x00, 0x0E }; +static const uint8_t g_rsp_apdu[] = { 0x6F, 0x1A, 0x84, 0x0E, 0x90, 0x00 }; + +struct test_trs_ctx { + osdp_t *cp_ctx; + osdp_t *pd_ctx; + int cp_runner; + int pd_runner; + + /* CP side: last OSDP_EVENT_TRS seen */ + bool event_seen; + struct osdp_trs_reply last_reply; + + /* PD side: last CMD_XWR delivered to the app */ + bool cmd_seen; + struct osdp_trs_cmd last_cmd; + + /* When set, the PD app does NOT answer the send-APDU in the callback; + * the test submits the R-APDU later to exercise the deferred path. */ + bool defer; + + /* PD reply event; app-owned, must outlive the command callback since + * osdp_pd_submit_event() stores it by reference until it is sent. */ + struct osdp_event resp_event; + + /* CP side: last OSDP_NOTIFICATION_TRS_STATUS seen */ + bool status_seen; + enum osdp_trs_session_status_e last_status; + + /* CP side: last command completion seen */ + bool completion_seen; + enum osdp_completion_status last_completion; +}; + +static struct test_trs_ctx g_trs = { 0 }; + +/* Build the card-data R-APDU reply into the app-owned resp_event */ +static void trs_fill_response(struct test_trs_ctx *ctx) +{ + struct osdp_event *resp = &ctx->resp_event; + + memset(resp, 0, sizeof(*resp)); + resp->type = OSDP_EVENT_TRS; + resp->trs.reply = OSDP_TRS_REPLY_CARD_DATA; + resp->trs.card_data.reader = 0; + resp->trs.card_data.status = 0; + resp->trs.card_data.apdu.length = sizeof(g_rsp_apdu); + memcpy(resp->trs.card_data.apdu.data, g_rsp_apdu, sizeof(g_rsp_apdu)); +} + +static int trs_cp_event_callback(void *arg, int pd, struct osdp_event *ev) +{ + ARG_UNUSED(pd); + struct test_trs_ctx *ctx = arg; + + if (ev->type == OSDP_EVENT_TRS) { + ctx->event_seen = true; + memcpy(&ctx->last_reply, &ev->trs, sizeof(ctx->last_reply)); + } + if (ev->type == OSDP_EVENT_NOTIFICATION && + ev->notif.type == OSDP_NOTIFICATION_TRS_STATUS) { + ctx->status_seen = true; + ctx->last_status = ev->notif.arg0; + } + return 0; +} + +static void trs_cp_completion_callback(void *arg, int pd, + const struct osdp_cmd *cmd, + enum osdp_completion_status status) +{ + struct test_trs_ctx *ctx = arg; + + ARG_UNUSED(pd); + if (cmd->id == OSDP_CMD_XWR) { + ctx->completion_seen = true; + ctx->last_completion = status; + } +} + +/* + * Submit a bare TRS marker/command (START, STOP, ...) to the CP. The command is + * queued by reference, so @a cmd is the caller's to keep alive until the command + * completes -- callers here block on the session notification that follows it. + */ +static int submit_trs_cmd(struct osdp_cmd *cmd, enum osdp_trs_cmd_e command) +{ + *cmd = (struct osdp_cmd){ + .id = OSDP_CMD_XWR, + .trs = { .command = command }, + }; + + return osdp_cp_submit_command(g_trs.cp_ctx, 0, cmd); +} + +static bool wait_for_trs_status(enum osdp_trs_session_status_e want, + int timeout_sec) +{ + int rc = 0; + + while (rc++ < timeout_sec) { + if (g_trs.status_seen && g_trs.last_status == want) { + return true; + } + usleep(1000 * 1000); + } + return false; +} + +static int trs_pd_command_callback(void *arg, struct osdp_cmd *cmd) +{ + struct test_trs_ctx *ctx = arg; + + if (cmd->id != OSDP_CMD_XWR) { + return 0; + } + + ctx->cmd_seen = true; + memcpy(&ctx->last_cmd, &cmd->trs, sizeof(ctx->last_cmd)); + + /* Fast card: answer the send-APDU synchronously so the R-APDU rides + * back in the immediate REPLY_XRD. In deferred mode the app submits the + * response later (see test_trs_deferred_apdu), so the PD replies ACK + * ("working") and the R-APDU is delivered on a subsequent poll. */ + if (cmd->trs.command == OSDP_TRS_CMD_SEND_APDU && !ctx->defer) { + trs_fill_response(ctx); + osdp_pd_submit_event(ctx->pd_ctx, &ctx->resp_event); + } + return 0; +} + +static int setup_test_environment(struct test *t) +{ + printf(SUB_1 "setting up OSDP devices\n"); + + if (test_setup_devices_ext(t, &g_trs.cp_ctx, &g_trs.pd_ctx, + OSDP_FLAG_ENABLE_NOTIFICATION, 0)) { + printf(SUB_1 "Failed to setup devices!\n"); + return -1; + } + + osdp_cp_set_event_callback(g_trs.cp_ctx, trs_cp_event_callback, &g_trs); + osdp_cp_set_command_completion_callback(g_trs.cp_ctx, + trs_cp_completion_callback, + &g_trs); + osdp_pd_set_command_callback(g_trs.pd_ctx, trs_pd_command_callback, + &g_trs); + + printf(SUB_1 "starting async runners\n"); + g_trs.cp_runner = async_runner_start(g_trs.cp_ctx, osdp_cp_refresh); + g_trs.pd_runner = async_runner_start(g_trs.pd_ctx, osdp_pd_refresh); + if (g_trs.cp_runner < 0 || g_trs.pd_runner < 0) { + printf(SUB_1 "Failed to create CP/PD runners\n"); + return -1; + } + + if (!test_wait_for_online(g_trs.cp_ctx, 0, 10)) { + printf(SUB_1 "PD failed to come online\n"); + return -1; + } + return 0; +} + +static void teardown_test_environment() +{ + printf(SUB_1 "tearing down test environment\n"); + async_runner_stop(g_trs.cp_runner); + async_runner_stop(g_trs.pd_runner); + osdp_cp_teardown(g_trs.cp_ctx); + osdp_pd_teardown(g_trs.pd_ctx); + memset(&g_trs, 0, sizeof(g_trs)); +} + +static bool wait_for_trs_event(int timeout_sec) +{ + int rc = 0; + while (rc++ < timeout_sec) { + if (g_trs.event_seen) { + return true; + } + usleep(1000 * 1000); + } + return false; +} + +/* A card command with no START must be turned away at submit, not queued */ +static bool test_trs_apdu_outside_band() +{ + printf(SUB_2 "testing TRS APDU outside a session\n"); + + struct osdp_cmd cmd = { + .id = OSDP_CMD_XWR, + .trs = { + .command = OSDP_TRS_CMD_SEND_APDU, + .apdu = { .length = sizeof(g_req_apdu) }, + }, + }; + memcpy(cmd.trs.apdu.data, g_req_apdu, sizeof(g_req_apdu)); + + if (osdp_cp_submit_command(g_trs.cp_ctx, 0, &cmd) == 0) { + printf(SUB_2 "sessionless APDU must be rejected\n"); + return false; + } + /* So must a STOP that closes nothing */ + struct osdp_cmd stop; + + if (submit_trs_cmd(&stop, OSDP_TRS_CMD_STOP) == 0) { + printf(SUB_2 "sessionless TRS stop must be rejected\n"); + return false; + } + return true; +} + +/* Inside a band, a non-TRS command is refused: it would cut the card session */ +static bool test_trs_non_trs_cmd_in_band() +{ + printf(SUB_2 "testing non-TRS command inside a session\n"); + + struct osdp_cmd cmd = { + .id = OSDP_CMD_BUZZER, + .buzzer = { .reader = 0, .control_code = 1, .on_count = 10, + .off_count = 10, .rep_count = 1 }, + }; + + if (osdp_cp_submit_command(g_trs.cp_ctx, 0, &cmd) == 0) { + printf(SUB_2 "in-band buzzer command must be rejected\n"); + return false; + } + /* A nested START is refused too */ + struct osdp_cmd start; + + if (submit_trs_cmd(&start, OSDP_TRS_CMD_START) == 0) { + printf(SUB_2 "nested TRS start must be rejected\n"); + return false; + } + return true; +} + +/* START opens the band: the library negotiates transparent mode and says so */ +static bool test_trs_session_start() +{ + printf(SUB_2 "testing TRS session start\n"); + + struct osdp_cmd cmd; + + g_trs.status_seen = false; + + if (submit_trs_cmd(&cmd, OSDP_TRS_CMD_START)) { + printf(SUB_2 "Failed to submit TRS start\n"); + return false; + } + if (!wait_for_trs_status(OSDP_TRS_SESSION_OPENED, 10)) { + printf(SUB_2 "TRS session-opened notification not received\n"); + return false; + } + return true; +} + +static bool test_trs_apdu_exchange() +{ + printf(SUB_2 "testing TRS APDU exchange (synchronous)\n"); + + g_trs.event_seen = false; + g_trs.cmd_seen = false; + g_trs.defer = false; + + /* Stack storage must outlive the submission; wait_for_trs_event() + * below blocks here until the whole exchange completes. */ + struct osdp_cmd cmd = { + .id = OSDP_CMD_XWR, + .trs = { + .command = OSDP_TRS_CMD_SEND_APDU, + .apdu = { + .length = sizeof(g_req_apdu), + }, + }, + }; + memcpy(cmd.trs.apdu.data, g_req_apdu, sizeof(g_req_apdu)); + + if (osdp_cp_submit_command(g_trs.cp_ctx, 0, &cmd)) { + printf(SUB_2 "Failed to submit TRS command\n"); + return false; + } + + if (!wait_for_trs_event(10)) { + printf(SUB_2 "TRS event not received\n"); + return false; + } + + /* PD must have received the exact APDU the CP sent */ + if (!g_trs.cmd_seen || + g_trs.last_cmd.command != OSDP_TRS_CMD_SEND_APDU || + g_trs.last_cmd.apdu.length != (int)sizeof(g_req_apdu) || + memcmp(g_trs.last_cmd.apdu.data, g_req_apdu, + sizeof(g_req_apdu)) != 0) { + printf(SUB_2 "PD did not receive the request APDU intact\n"); + return false; + } + + /* CP must have received the card-data reply the PD sent */ + if (g_trs.last_reply.reply != OSDP_TRS_REPLY_CARD_DATA || + g_trs.last_reply.card_data.apdu.length != (int)sizeof(g_rsp_apdu) || + memcmp(g_trs.last_reply.card_data.apdu.data, g_rsp_apdu, + sizeof(g_rsp_apdu)) != 0) { + printf(SUB_2 "CP did not receive the response APDU intact\n"); + return false; + } + + return true; +} + +static bool test_trs_deferred_apdu() +{ + int rc; + + printf(SUB_2 "testing TRS APDU exchange (deferred / slow card)\n"); + + /* The session is already active (TRS_RUN) from the synchronous test; + * submit a second C-APDU that the PD app answers late. */ + g_trs.event_seen = false; + g_trs.cmd_seen = false; + g_trs.defer = true; + + struct osdp_cmd cmd = { + .id = OSDP_CMD_XWR, + .trs = { + .command = OSDP_TRS_CMD_SEND_APDU, + .apdu = { + .length = sizeof(g_req_apdu), + }, + }, + }; + memcpy(cmd.trs.apdu.data, g_req_apdu, sizeof(g_req_apdu)); + + if (osdp_cp_submit_command(g_trs.cp_ctx, 0, &cmd)) { + printf(SUB_2 "Failed to submit deferred TRS command\n"); + return false; + } + + /* Wait until the PD has received the C-APDU and replied ACK ("working"). + * The PD app deliberately did not answer, so no event must arrive yet. */ + rc = 0; + while (rc++ < 8 && !g_trs.cmd_seen) { + usleep(1000 * 1000); + } + if (!g_trs.cmd_seen) { + printf(SUB_2 "PD never received the deferred C-APDU\n"); + g_trs.defer = false; + return false; + } + if (g_trs.event_seen) { + printf(SUB_2 "R-APDU arrived before the deferred submit\n"); + g_trs.defer = false; + return false; + } + + /* Simulate the card finally responding: the app submits the R-APDU now. + * The CP is still polling, so it rides out on the next poll as XRD. */ + trs_fill_response(&g_trs); + if (osdp_pd_submit_event(g_trs.pd_ctx, &g_trs.resp_event)) { + printf(SUB_2 "Failed to submit deferred R-APDU\n"); + g_trs.defer = false; + return false; + } + + if (!wait_for_trs_event(10)) { + printf(SUB_2 "Deferred TRS event not received\n"); + g_trs.defer = false; + return false; + } + + g_trs.defer = false; + + /* The deferred R-APDU must round-trip intact */ + if (g_trs.last_reply.reply != OSDP_TRS_REPLY_CARD_DATA || + g_trs.last_reply.card_data.apdu.length != (int)sizeof(g_rsp_apdu) || + memcmp(g_trs.last_reply.card_data.apdu.data, g_rsp_apdu, + sizeof(g_rsp_apdu)) != 0) { + printf(SUB_2 "Deferred response APDU mismatch\n"); + return false; + } + + return true; +} + +/* --- wire-level unit tests: assert exact on-wire bytes vs SIA OSDP 2.2 --- */ + +/* + * A bare PD for codec tests. The codec logs through pd_to_osdp(), so it needs + * an osdp context even when no device is set up; `mode` seeds the transparent + * mode the PD believes it is in. + */ +static void trs_wire_pd_init(struct osdp *ctx, struct osdp_pd *pd, uint8_t mode) +{ + memset(ctx, 0, sizeof(*ctx)); + memset(pd, 0, sizeof(*pd)); + pd->osdp_ctx = ctx; + pd->trs.mode = mode; +} + +/* Table 71: Mode Setting Report must carry both Mode code and Mode config */ +static bool test_trs_wire_mode_report(void) +{ + struct osdp ctx; + struct osdp_pd pd; + uint8_t buf[8]; + int len; + + printf(SUB_2 "testing TRS wire: mode-setting-report layout\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_01); + pd.active_event = NULL; /* handshake reply, no app payload */ + + len = osdp_trs_reply_build(&pd, buf, sizeof(buf)); + if (len != 4 || buf[0] != 0x00 || buf[1] != 0x01 || + buf[2] != TRS_MODE_01 || buf[3] != 0x00) { + printf(SUB_2 "want [00 01 mode 00] len 4; got len %d\n", len); + return false; + } + return true; +} + +/* Table 75: Card Present reply must carry both Reader and Status */ +static bool test_trs_wire_card_present_build(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_event ev; + uint8_t buf[8]; + int len; + + printf(SUB_2 "testing TRS wire: card-present layout\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_01); + memset(&ev, 0, sizeof(ev)); + ev.type = OSDP_EVENT_TRS; + ev.trs.reply = OSDP_TRS_REPLY_CARD_PRESENT; + ev.trs.card_present.reader = 0; + ev.trs.card_present.status = OSDP_TRS_CARD_PRESENT_CONTACT; + pd.active_event = &ev; + + len = osdp_trs_reply_build(&pd, buf, sizeof(buf)); + if (len != 4 || buf[0] != 0x01 || buf[1] != 0x01 || buf[2] != 0x00 || + buf[3] != 0x03) { + printf(SUB_2 "card-present want len 4; got %d\n", len); + return false; + } + return true; +} + +/* Table 74: Mode-1 Error reply (osdp_PR01ERROR) = [01 00 error-code] */ +static bool test_trs_wire_error_build(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_event ev; + uint8_t buf[8]; + int len; + + printf(SUB_2 "testing TRS wire: error-reply layout\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_01); + memset(&ev, 0, sizeof(ev)); + ev.type = OSDP_EVENT_TRS; + ev.trs.reply = OSDP_TRS_REPLY_ERROR; + ev.trs.error.code = 0x6A; + pd.active_event = &ev; + + len = osdp_trs_reply_build(&pd, buf, sizeof(buf)); + if (len != 3 || buf[0] != 0x01 || buf[1] != 0x00 || buf[2] != 0x6A) { + printf(SUB_2 "want [01 00 6A] len 3; got len %d\n", len); + return false; + } + return true; +} + +/* + * Table 36/39/42: Mode-Set and Terminate are ACK'd; Mode-Read (Table 39) is + * answered with an XRD mode report; card-session commands go to the app. + */ +static bool test_trs_wire_cmd_decode_actions(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_cmd cmd; + uint8_t set_mode[] = { 0x00, 0x02, TRS_MODE_01, 0x00 }; + uint8_t read_mode[] = { 0x00, 0x01 }; + uint8_t terminate[] = { 0x01, 0x02, 0x00 }; + uint8_t send_apdu[] = { 0x01, 0x01, 0x00, 0x00, 0xA4 }; + int r; + + printf(SUB_2 "testing TRS wire: command decode reply-actions\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_00); + + r = osdp_trs_cmd_decode(&pd, &cmd, set_mode, sizeof(set_mode)); + if (r != OSDP_TRS_DECODE_ACK) { + printf(SUB_2 "mode-set must be ACK'd; got %d\n", r); + return false; + } + r = osdp_trs_cmd_decode(&pd, &cmd, read_mode, sizeof(read_mode)); + if (r != OSDP_TRS_DECODE_MODE_REPORT) { + printf(SUB_2 "mode-read want mode report; got %d\n", r); + return false; + } + r = osdp_trs_cmd_decode(&pd, &cmd, terminate, sizeof(terminate)); + if (r != OSDP_TRS_DECODE_ACK) { + printf(SUB_2 "terminate must be ACK'd; got %d\n", r); + return false; + } + r = osdp_trs_cmd_decode(&pd, &cmd, send_apdu, sizeof(send_apdu)); + if (r != OSDP_TRS_DECODE_TO_APP) { + printf(SUB_2 "send-apdu must be delivered to app; got %d\n", r); + return false; + } + return true; +} + +/* + * The PD must refuse a mode it cannot honour, and must not act on card-session + * commands until the CP has actually put it in transparent mode. + */ +static bool test_trs_wire_mode_gating(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_cmd cmd; + uint8_t bad_mode[] = { 0x00, 0x02, 0x05, 0x00 }; + uint8_t send_apdu[] = { 0x01, 0x01, 0x00, 0x00, 0xA4 }; + uint8_t set_mode[] = { 0x00, 0x02, TRS_MODE_01, 0x00 }; + int r; + + printf(SUB_2 "testing TRS wire: transparent-mode gating\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_00); + + /* An unsupported mode must be NAK'd, not stored and ACK'd */ + r = osdp_trs_cmd_decode(&pd, &cmd, bad_mode, sizeof(bad_mode)); + if (r >= 0 || pd.trs.mode != TRS_MODE_00) { + printf(SUB_2 "mode 0x05 must be refused; got %d mode %02x\n", r, + pd.trs.mode); + return false; + } + + /* Card-session commands are invalid while transparent mode is off */ + r = osdp_trs_cmd_decode(&pd, &cmd, send_apdu, sizeof(send_apdu)); + if (r >= 0) { + printf(SUB_2 "send-apdu in mode 00 must be refused; got %d\n", r); + return false; + } + + /* ... and valid once the CP negotiates mode 01 */ + r = osdp_trs_cmd_decode(&pd, &cmd, set_mode, sizeof(set_mode)); + if (r != OSDP_TRS_DECODE_ACK || pd.trs.mode != TRS_MODE_01) { + printf(SUB_2 "mode-set 01 must be ACK'd; got %d\n", r); + return false; + } + r = osdp_trs_cmd_decode(&pd, &cmd, send_apdu, sizeof(send_apdu)); + if (r != OSDP_TRS_DECODE_TO_APP) { + printf(SUB_2 "send-apdu in mode 01 must reach app; got %d\n", r); + return false; + } + return true; +} + +/* Oversized APDUs must be rejected on decode, never silently truncated */ +static bool test_trs_wire_oversized_apdu_reject(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_cmd cmd; + struct osdp_event ev; + uint8_t buf[128]; + int r; + + printf(SUB_2 "testing TRS wire: oversized APDU rejection\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_01); + memset(buf, 0xA5, sizeof(buf)); + + /* CP side: REPLY_XRD card-data carrying an R-APDU 6 bytes too long */ + buf[0] = 0x01; /* mode 1 */ + buf[1] = 0x02; /* card data */ + buf[2] = 0x00; /* reader */ + buf[3] = 0x00; /* status */ + r = osdp_trs_reply_decode(&pd, buf, 4 + OSDP_TRS_APDU_MAX_LEN + 6, &ev); + if (r >= 0) { + printf(SUB_2 "oversized R-APDU must be rejected; got %d\n", r); + return false; + } + + /* PD side: CMD_XWR send-APDU carrying a C-APDU 6 bytes too long */ + buf[1] = 0x01; /* send-apdu */ + r = osdp_trs_cmd_decode(&pd, &cmd, buf, 3 + OSDP_TRS_APDU_MAX_LEN + 6); + if (r >= 0) { + printf(SUB_2 "oversized C-APDU must be NAK'd; got %d\n", r); + return false; + } + + /* ... while an exactly max-size C-APDU still goes through */ + r = osdp_trs_cmd_decode(&pd, &cmd, buf, 3 + OSDP_TRS_APDU_MAX_LEN); + if (r != OSDP_TRS_DECODE_TO_APP || + cmd.trs.apdu.length != OSDP_TRS_APDU_MAX_LEN) { + printf(SUB_2 "max-size C-APDU must be accepted; got %d\n", r); + return false; + } + return true; +} + +/* Table 43: PIN-entry packed bytes must survive a CP-build -> PD-decode trip */ +static bool test_trs_wire_pin_entry_roundtrip(void) +{ + struct osdp ctx; + struct osdp_pd pd; + struct osdp_cmd in, out; + struct osdp_trs_pin_entry *pe = &in.trs.pin_entry; + uint8_t buf[64]; + int len, r; + + printf(SUB_2 "testing TRS wire: PIN-entry round-trip\n"); + trs_wire_pd_init(&ctx, &pd, TRS_MODE_01); + memset(&in, 0, sizeof(in)); + memset(&out, 0, sizeof(out)); + + in.id = OSDP_CMD_XWR; + in.trs.command = OSDP_TRS_CMD_ENTER_PIN; + pe->timeout_initial = 30; + pe->timeout_digit = 10; + pe->pin_block.format = OSDP_TRS_PIN_FORMAT_ASCII; + pe->pin_block.right_justify = true; + pe->pin_block.offset_bits = 40; /* byte-aligned: goes out in byte units */ + pe->pin_block.size_bytes = 8; + pe->pin_length_field.size_bits = 4; + pe->pin_length_field.offset_bits = 3; /* sub-byte: goes out in bit units */ + pe->min_digits = 4; + pe->max_digits = 12; + pe->complete_on = OSDP_TRS_PIN_COMPLETE_ON_MAX_DIGITS | + OSDP_TRS_PIN_COMPLETE_ON_KEY; + pe->num_messages = 1; + pe->language_id = 0x0409; + pe->msg_index = 0; + pe->apdu.length = sizeof(g_req_apdu); + memcpy(pe->apdu.data, g_req_apdu, sizeof(g_req_apdu)); + + /* header(2) + reader(1) + fixed fields(15) + apdu len(2) + apdu(5) */ + len = osdp_trs_cmd_build(&pd, &in, buf, sizeof(buf)); + if (len != 25) { + printf(SUB_2 "PIN-entry build: want len 25; got %d\n", len); + return false; + } + r = osdp_trs_cmd_decode(&pd, &out, buf, len); + if (r != OSDP_TRS_DECODE_TO_APP || + out.trs.command != OSDP_TRS_CMD_ENTER_PIN) { + printf(SUB_2 "PIN-entry decode failed: %d\n", r); + return false; + } + if (memcmp(pe, &out.trs.pin_entry, sizeof(*pe)) != 0) { + printf(SUB_2 "PIN-entry did not round-trip intact\n"); + return false; + } + return true; +} + +/* End-to-end: PD reports a card-present status; CP app must receive it */ +static bool test_trs_card_present() +{ + printf(SUB_2 "testing TRS card-present notification\n"); + + g_trs.event_seen = false; + memset(&g_trs.resp_event, 0, sizeof(g_trs.resp_event)); + g_trs.resp_event.type = OSDP_EVENT_TRS; + g_trs.resp_event.trs.reply = OSDP_TRS_REPLY_CARD_PRESENT; + g_trs.resp_event.trs.card_present.reader = 0; + g_trs.resp_event.trs.card_present.status = OSDP_TRS_CARD_PRESENT_CONTACT; + + if (osdp_pd_submit_event(g_trs.pd_ctx, &g_trs.resp_event)) { + printf(SUB_2 "Failed to submit card-present event\n"); + return false; + } + if (!wait_for_trs_event(10)) { + printf(SUB_2 "card-present event not received\n"); + return false; + } + if (g_trs.last_reply.reply != OSDP_TRS_REPLY_CARD_PRESENT || + g_trs.last_reply.card_present.reader != 0 || + g_trs.last_reply.card_present.status != + OSDP_TRS_CARD_PRESENT_CONTACT) { + printf(SUB_2 "card-present reader/status not intact\n"); + return false; + } + return true; +} + +/* End-to-end: PD reports a transparent-mode error; CP app must receive it */ +static bool test_trs_error_reply() +{ + printf(SUB_2 "testing TRS error reply\n"); + + g_trs.event_seen = false; + memset(&g_trs.resp_event, 0, sizeof(g_trs.resp_event)); + g_trs.resp_event.type = OSDP_EVENT_TRS; + g_trs.resp_event.trs.reply = OSDP_TRS_REPLY_ERROR; + g_trs.resp_event.trs.error.code = 0x6A; + + if (osdp_pd_submit_event(g_trs.pd_ctx, &g_trs.resp_event)) { + printf(SUB_2 "Failed to submit error event\n"); + return false; + } + if (!wait_for_trs_event(10)) { + printf(SUB_2 "error event not received\n"); + return false; + } + if (g_trs.last_reply.reply != OSDP_TRS_REPLY_ERROR || + g_trs.last_reply.error.code != 0x6A) { + printf(SUB_2 "error code not intact\n"); + return false; + } + return true; +} + +static bool test_trs_session_stop() +{ + printf(SUB_2 "testing TRS session stop\n"); + + struct osdp_cmd cmd; + + g_trs.status_seen = false; + + if (submit_trs_cmd(&cmd, OSDP_TRS_CMD_STOP)) { + printf(SUB_2 "Failed to submit TRS stop\n"); + return false; + } + if (!wait_for_trs_status(OSDP_TRS_SESSION_CLOSED, 10)) { + printf(SUB_2 "TRS session-closed notification not received\n"); + return false; + } + + /* After teardown the PD is back to ordinary online operation */ + if (!test_wait_for_online(g_trs.cp_ctx, 0, 10)) { + printf(SUB_2 "PD did not return online after TRS stop\n"); + return false; + } + + return true; +} + +void run_trs_tests(struct test *t) +{ + bool result = true; + + printf("\nBegin TRS Tests\n"); + + /* wire-level unit tests need no devices; run them first */ + printf(SUB_1 "running TRS wire-encoding tests\n"); + result &= test_trs_wire_mode_report(); + result &= test_trs_wire_card_present_build(); + result &= test_trs_wire_error_build(); + result &= test_trs_wire_cmd_decode_actions(); + result &= test_trs_wire_mode_gating(); + result &= test_trs_wire_oversized_apdu_reject(); + result &= test_trs_wire_pin_entry_roundtrip(); + + if (setup_test_environment(t) != 0) { + printf(SUB_1 "Failed to setup test environment\n"); + TEST_REPORT(t, false); + return; + } + + printf(SUB_1 "running TRS tests\n"); + result &= test_trs_apdu_outside_band(); + result &= test_trs_session_start(); + result &= test_trs_non_trs_cmd_in_band(); + result &= test_trs_apdu_exchange(); + result &= test_trs_deferred_apdu(); + result &= test_trs_card_present(); + result &= test_trs_error_reply(); + result &= test_trs_session_stop(); + + teardown_test_environment(); + + printf(SUB_1 "TRS tests %s\n", result ? "succeeded" : "failed"); + TEST_REPORT(t, result); +} + +#else /* OPT_BUILD_OSDP_TRS */ + +/* TRS-only suite: compiles to a no-op when TRS support is not built in, so the + * file can sit in the shared source list without a link dependency. */ +void run_trs_tests(struct test *t) +{ + ARG_UNUSED(t); +} + +#endif /* OPT_BUILD_OSDP_TRS */ diff --git a/tests/unit-tests/test.c b/tests/unit-tests/test.c index c7934415..0f7457a8 100644 --- a/tests/unit-tests/test.c +++ b/tests/unit-tests/test.c @@ -797,6 +797,7 @@ int test_setup_devices_ext(struct test *t, osdp_t **cp, osdp_t **pd, { OSDP_PD_CAP_OUTPUT_CONTROL, 1, 4 }, { OSDP_PD_CAP_READER_TEXT_OUTPUT, 1, 1 }, { OSDP_PD_CAP_CONTACT_STATUS_MONITORING, 1, 8 }, + { OSDP_PD_CAP_SMART_CARD_SUPPORT, 1, 1 }, { -1, -1, -1 } }; @@ -841,6 +842,21 @@ int test_setup_devices(struct test *t, osdp_t **cp, osdp_t **pd) return test_setup_devices_ext(t, cp, pd, 0, 0); } +bool test_wait_for_online(osdp_t *cp_ctx, int pd_idx, int timeout_sec) +{ + uint8_t status = 0; + int rc = 0; + + while (rc++ < timeout_sec) { + osdp_get_status_mask(cp_ctx, &status); + if (status & (1 << pd_idx)) { + return true; + } + usleep(1000 * 1000); + } + return false; +} + void test_start(struct test *t, int log_level) { memset(t, 0, sizeof(*t)); @@ -1024,6 +1040,7 @@ int main(int argc, char *argv[]) { "codec_fuzz", run_codec_fuzz_tests }, { "sc", run_sc_tests }, { "vectors", run_vector_tests }, + { "trs", run_trs_tests }, }; ARG_UNUSED(argc); diff --git a/tests/unit-tests/test.h b/tests/unit-tests/test.h index 526adc8c..b23c87cd 100644 --- a/tests/unit-tests/test.h +++ b/tests/unit-tests/test.h @@ -118,6 +118,7 @@ void test_suite_end(struct test *t); int test_setup_devices(struct test *t, osdp_t **cp, osdp_t **pd); int test_setup_devices_ext(struct test *t, osdp_t **cp, osdp_t **pd, uint32_t cp_flags, uint32_t pd_flags); +bool test_wait_for_online(osdp_t *cp_ctx, int pd_idx, int timeout_sec); int async_runner_start(osdp_t *ctx, void (*fn)(osdp_t *)); int async_runner_stop(int runner); int async_cp_runner_start(osdp_t *cp_ctx); @@ -143,6 +144,7 @@ void run_async_fuzz_tests(struct test *t); void run_codec_fuzz_tests(struct test *t); void run_sc_tests(struct test *t); void run_vector_tests(struct test *t); +void run_trs_tests(struct test *t); /* no-op unless OPT_BUILD_OSDP_TRS */ void run_pd_zc_tests(struct test *t); /* no-op unless OPT_OSDP_RX_ZERO_COPY */ #define printf(...) test_printf(__VA_ARGS__)