|
/* |
|
* ifd-nfc.c — PC/SC IFD Handler for Linux kernel NFC subsystem |
|
* |
|
* This is a custom PC/SC IFD (Interface Device) handler that bridges the |
|
* Linux kernel NFC subsystem to pcscd, enabling AusweisApp (and any PC/SC |
|
* application) to use a phone's built-in NFC controller for contactless |
|
* smartcard operations (ISO-DEP / ISO 14443-4). |
|
* |
|
* No existing solution for this exists anywhere — libnfc/ifdnfc only |
|
* support USB NFC readers, not the kernel's NFC subsystem. This handler |
|
* talks directly to the kernel via: |
|
* - AF_NETLINK + NETLINK_GENERIC (NFC generic netlink, family "nfc"): |
|
* control plane for DEV_UP, START_POLL, STOP_POLL, GET_TARGET, etc. |
|
* - AF_NFC + SOCK_SEQPACKET + NFC_SOCKPROTO_RAW: |
|
* data plane for ISO-DEP APDU exchange with the card. |
|
* |
|
* Architecture: |
|
* [AusweisApp] → [libpcsclite] → [pcscd] → [ifd-nfc.so] → [kernel NFC] |
|
* ↕ |
|
* [NFC controller] |
|
* (e.g. PN553) |
|
* ↕ |
|
* [NFC card/eID] |
|
* |
|
* Key design decisions and workarounds: |
|
* |
|
* 1. NCI auto-discovery: When we close an AF_NFC socket, the NCI controller |
|
* auto-enters discovery mode (kernel sets dev->polling=true). Sending |
|
* STOP_POLL is futile — the NCI notification handler immediately re-enables |
|
* polling. We treat START_POLL EBUSY as "kernel is already polling" and |
|
* simply wait for NFC_EVENT_TARGETS_FOUND. |
|
* |
|
* 2. DEV_DOWN avoidance: Sending NFC_CMD_DEV_DOWN during normal operation |
|
* can crash the NFC chip (NCI CORE_RESET to a hung controller → i2c |
|
* timeout → ETIMEDOUT, requires module reload). We NEVER send DEV_DOWN |
|
* except during final CloseChannel shutdown. |
|
* |
|
* 3. ISO-DEP PCB byte: AF_NFC raw socket prepends a 1-byte ISO-DEP PCB |
|
* byte (typically 0x00) to every response. We strip it to get the raw |
|
* APDU response. |
|
* |
|
* 4. Pseudo-APDU interception: AusweisApp sends CLA=FF pseudo-APDUs |
|
* (TR-03119 PACE reader features). These must NOT be forwarded to the |
|
* NFC card — we return 6E00 (CLA not supported) locally. |
|
* |
|
* 5. Event-driven card detection: Uses TAG_IFD_POLLING_THREAD_WITH_TIMEOUT |
|
* to provide pcscd with a blocking function for card insert/remove events, |
|
* replacing the default 400ms IFDHICCPresence polling. |
|
* |
|
* 6. Connection staleness: NFC connections go stale after ~8-10 seconds of |
|
* inactivity (NCI idle timeout). TransmitToICC detects this and performs |
|
* a clean reconnect cycle. |
|
* |
|
* 7. AusweisApp reader detection: AusweisApp on Linux shows the reader as |
|
* "available" only when a card is present (same as Android NFC). The |
|
* reader is always registered in pcscd's reader list, but AusweisApp's |
|
* UI treats contactless readers as card-presence-dependent. |
|
* |
|
* Tested with: |
|
* - Device: SHIFT6mq (SDM845), PostmarketOS, PN553 NFC controller |
|
* - AusweisApp 2.5.1 on Linux (Qt 6.11.0) |
|
* - German eID card (ISO 14443-4 Type A, PACE authentication) |
|
* - pcscd from pcsc-lite (Alpine package) |
|
* |
|
* Build: |
|
* gcc -shared -fPIC -Wall -Wextra -O2 -o ifd-nfc.so ifd-nfc.c \ |
|
* $(pkg-config --cflags --libs libnl-genl-3.0) \ |
|
* $(pkg-config --cflags libpcsclite) -lpthread |
|
* |
|
* Install: see install-nfc-pcsc.sh |
|
*/ |
|
|
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <unistd.h> |
|
#include <errno.h> |
|
#include <signal.h> |
|
#include <poll.h> |
|
#include <pthread.h> |
|
#include <sys/socket.h> |
|
#include <linux/nfc.h> |
|
#include <netlink/netlink.h> |
|
#include <netlink/genl/genl.h> |
|
#include <netlink/genl/ctrl.h> |
|
|
|
#include <PCSC/ifdhandler.h> |
|
|
|
#define LOG(fmt, ...) fprintf(stderr, "ifd-nfc: " fmt "\n", ##__VA_ARGS__) |
|
|
|
#define NFC_DEV_IDX 0 |
|
|
|
#define NFC_ALL_PROTOS (NFC_PROTO_JEWEL_MASK | NFC_PROTO_MIFARE_MASK | \ |
|
NFC_PROTO_FELICA_MASK | NFC_PROTO_ISO14443_MASK | \ |
|
NFC_PROTO_NFC_DEP_MASK | NFC_PROTO_ISO14443_B_MASK) |
|
|
|
static const unsigned char DEFAULT_ATR[] = { |
|
0x3B, 0x80, 0x80, 0x01, 0x01 |
|
}; |
|
#define DEFAULT_ATR_LEN sizeof(DEFAULT_ATR) |
|
|
|
static struct { |
|
int initialized; |
|
struct nl_sock *cmd_sk; |
|
struct nl_sock *evt_sk; |
|
int nfc_family; |
|
int events_group; |
|
int evt_fd; |
|
|
|
int poll_active; |
|
int card_present; |
|
int card_connected; |
|
int raw_fd; |
|
uint32_t target_idx; |
|
uint32_t target_proto; |
|
|
|
/* Set when WE close the raw_fd (vs kernel closing it). |
|
* Prevents TARGET_LOST from clearing card_present. */ |
|
int self_disconnect; |
|
|
|
unsigned char atr[MAX_ATR_SIZE]; |
|
DWORD atr_len; |
|
|
|
char nfc_name[64]; /* NFC chip name from sysfs (e.g. "pn553") */ |
|
|
|
pthread_mutex_t lock; |
|
} ctx = { |
|
.initialized = 0, |
|
.raw_fd = -1, |
|
.target_idx = (uint32_t)-1, |
|
.nfc_name = "unknown", |
|
.lock = PTHREAD_MUTEX_INITIALIZER, |
|
}; |
|
|
|
/* --- Netlink helpers --- */ |
|
|
|
static const char *nfc_cmd_name(int cmd) { |
|
switch (cmd) { |
|
case NFC_CMD_DEV_UP: return "DEV_UP"; |
|
case NFC_CMD_DEV_DOWN: return "DEV_DOWN"; |
|
case NFC_CMD_START_POLL: return "START_POLL"; |
|
case NFC_CMD_STOP_POLL: return "STOP_POLL"; |
|
case NFC_CMD_GET_TARGET: return "GET_TARGET"; |
|
default: return "UNKNOWN"; |
|
} |
|
} |
|
|
|
static int ack_cb(struct nl_msg *m, void *a) { (void)m; *(int *)a = 0; return NL_STOP; } |
|
static int err_cb(struct sockaddr_nl *n, struct nlmsgerr *e, void *a) { |
|
(void)n; |
|
struct genlmsghdr *gh = (struct genlmsghdr *) |
|
((char *)NLMSG_DATA(&e->msg) + 0); |
|
int resp_cmd = (e->msg.nlmsg_len >= NLMSG_HDRLEN + GENL_HDRLEN) |
|
? gh->cmd : -1; |
|
LOG("netlink error: %d (%s) for %s (seq=%u)", |
|
e->error, strerror(-e->error), |
|
resp_cmd >= 0 ? nfc_cmd_name(resp_cmd) : "?", |
|
e->msg.nlmsg_seq); |
|
*(int *)a = e->error; |
|
return NL_STOP; |
|
} |
|
|
|
static int nfc_cmd(int cmd, int dev_idx, int extra_attr, uint32_t extra_val) { |
|
struct nl_msg *msg = nlmsg_alloc(); |
|
if (!msg) return -ENOMEM; |
|
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, ctx.nfc_family, 0, |
|
NLM_F_ACK, cmd, NFC_GENL_VERSION); |
|
nla_put_u32(msg, NFC_ATTR_DEVICE_INDEX, dev_idx); |
|
if (extra_attr >= 0) nla_put_u32(msg, extra_attr, extra_val); |
|
int ret = 1; |
|
struct nl_cb *cb = nl_cb_alloc(NL_CB_DEFAULT); |
|
if (!cb) { nlmsg_free(msg); return -ENOMEM; } |
|
nl_cb_err(cb, NL_CB_CUSTOM, err_cb, &ret); |
|
nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_cb, &ret); |
|
nl_send_auto(ctx.cmd_sk, msg); |
|
/* Receive with bounded attempts to prevent blocking forever. |
|
* With SO_RCVTIMEO=3s, each iteration waits at most 3 seconds. */ |
|
for (int i = 0; ret > 0 && i < 5; i++) { |
|
int r = nl_recvmsgs(ctx.cmd_sk, cb); |
|
if (r < 0 && r != -NLE_AGAIN) { ret = r; break; } |
|
} |
|
if (ret > 0) { |
|
LOG("nfc_cmd(%d): no ACK received (timeout)", cmd); |
|
ret = -ETIMEDOUT; |
|
} |
|
nl_cb_put(cb); |
|
nlmsg_free(msg); |
|
return ret; |
|
} |
|
|
|
/* --- NFC operations --- */ |
|
|
|
static int nfc_check_events(void); |
|
|
|
static int nfc_start_poll(void) { |
|
if (ctx.poll_active) return 0; |
|
|
|
/* DEV_UP: bring the device up. Only truly needed once; after that |
|
* EALREADY, EPERM, and EBUSY are all non-fatal — the device IS up, |
|
* just in some intermediate state. Proceed to START_POLL anyway. */ |
|
int ret = nfc_cmd(NFC_CMD_DEV_UP, NFC_DEV_IDX, -1, 0); |
|
if (ret < 0 && ret != -EALREADY && ret != -EPERM && ret != -EBUSY) { |
|
LOG("DEV_UP failed: %s (%d)", strerror(-ret), ret); |
|
return ret; |
|
} |
|
|
|
struct nl_msg *msg = nlmsg_alloc(); |
|
if (!msg) return -ENOMEM; |
|
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, ctx.nfc_family, 0, |
|
NLM_F_ACK, NFC_CMD_START_POLL, NFC_GENL_VERSION); |
|
nla_put_u32(msg, NFC_ATTR_DEVICE_INDEX, NFC_DEV_IDX); |
|
nla_put_u32(msg, NFC_ATTR_IM_PROTOCOLS, NFC_ALL_PROTOS); |
|
nla_put_u32(msg, NFC_ATTR_PROTOCOLS, NFC_ALL_PROTOS); |
|
ret = 1; |
|
struct nl_cb *cb = nl_cb_alloc(NL_CB_DEFAULT); |
|
if (!cb) { nlmsg_free(msg); return -ENOMEM; } |
|
nl_cb_err(cb, NL_CB_CUSTOM, err_cb, &ret); |
|
nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_cb, &ret); |
|
nl_send_auto(ctx.cmd_sk, msg); |
|
for (int i = 0; ret > 0 && i < 5; i++) { |
|
int r = nl_recvmsgs(ctx.cmd_sk, cb); |
|
if (r < 0 && r != -NLE_AGAIN) { ret = r; break; } |
|
} |
|
nl_cb_put(cb); |
|
nlmsg_free(msg); |
|
if (ret > 0) { |
|
LOG("START_POLL: no ACK received (timeout)"); |
|
return -ETIMEDOUT; |
|
} |
|
if (ret == -EBUSY) { |
|
/* EBUSY: the kernel is already polling (likely NCI auto-discovery |
|
* after target deactivation). The NCI controller re-enters |
|
* discovery mode on its own after we close an AF_NFC socket. |
|
* Don't fight it — just mark polling as active and wait for |
|
* TARGETS_FOUND. Sending STOP_POLL is futile here because the |
|
* NCI notification handler immediately re-enables polling. */ |
|
LOG("START_POLL: EBUSY, kernel is already polling (NCI auto-discovery)"); |
|
ctx.poll_active = 1; |
|
return 0; |
|
} |
|
if (ret < 0) { |
|
LOG("START_POLL failed: %s (%d)", strerror(-ret), ret); |
|
return ret; |
|
} |
|
ctx.poll_active = 1; |
|
LOG("NFC poll started"); |
|
return 0; |
|
} |
|
|
|
/* Try to start polling. nfc_start_poll now treats EBUSY as success |
|
* (kernel auto-polling), so retries are only for transient errors. */ |
|
static int nfc_start_poll_retry(void) { |
|
for (int attempt = 0; attempt < 5; attempt++) { |
|
int ret = nfc_start_poll(); |
|
if (ret == 0) return 0; |
|
LOG("START_POLL attempt %d/5 failed (%d), retrying...", attempt + 1, ret); |
|
usleep(300000); |
|
nfc_check_events(); |
|
} |
|
LOG("START_POLL: gave up after 5 retries"); |
|
return -1; |
|
} |
|
|
|
/* nfc_stop_poll removed — after socket close, the NCI controller |
|
* auto-enters discovery mode and re-sets dev->polling=true. |
|
* Sending STOP_POLL is futile and causes race conditions. |
|
* If we need to truly stop the NFC device, use DEV_DOWN (not |
|
* recommended during normal operation). */ |
|
|
|
/* Process pending NFC events (non-blocking). |
|
* Returns: 1 if TARGETS_FOUND, 0 otherwise. */ |
|
static int nfc_check_events(void) { |
|
int found = 0; |
|
struct pollfd pfd = { .fd = ctx.evt_fd, .events = POLLIN }; |
|
while (poll(&pfd, 1, 0) > 0) { |
|
unsigned char buf[4096]; |
|
int len = recv(ctx.evt_fd, buf, sizeof(buf), MSG_DONTWAIT); |
|
if (len <= 0) break; |
|
struct nlmsghdr *nlh = (struct nlmsghdr *)buf; |
|
if (nlh->nlmsg_type != (uint16_t)ctx.nfc_family) continue; |
|
struct genlmsghdr *gh = (struct genlmsghdr *)NLMSG_DATA(nlh); |
|
if (gh->cmd == NFC_EVENT_TARGETS_FOUND) { |
|
LOG("Event: target found"); |
|
ctx.card_present = 1; |
|
ctx.poll_active = 0; |
|
found = 1; |
|
} else if (gh->cmd == NFC_EVENT_TARGET_LOST) { |
|
if (ctx.self_disconnect) { |
|
LOG("Event: target lost (self-initiated, card still present)"); |
|
ctx.self_disconnect = 0; |
|
} else { |
|
LOG("Event: target lost (card removed)"); |
|
ctx.card_present = 0; |
|
} |
|
ctx.card_connected = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
if (ctx.raw_fd >= 0) { close(ctx.raw_fd); ctx.raw_fd = -1; } |
|
} else { |
|
LOG("Event: cmd=%d (ignored)", gh->cmd); |
|
} |
|
} |
|
return found; |
|
} |
|
|
|
static int nfc_get_target(void) { |
|
struct nl_msg *msg = nlmsg_alloc(); |
|
genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, ctx.nfc_family, 0, |
|
NLM_F_DUMP, NFC_CMD_GET_TARGET, NFC_GENL_VERSION); |
|
nla_put_u32(msg, NFC_ATTR_DEVICE_INDEX, NFC_DEV_IDX); |
|
nl_send_auto(ctx.cmd_sk, msg); |
|
nlmsg_free(msg); |
|
|
|
int cmd_fd = nl_socket_get_fd(ctx.cmd_sk); |
|
unsigned char buf[4096]; |
|
int got_target = 0; |
|
|
|
struct timeval tv = { .tv_sec = 2, .tv_usec = 0 }; |
|
setsockopt(cmd_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
|
|
|
for (;;) { |
|
int len = recv(cmd_fd, buf, sizeof(buf), 0); |
|
if (len <= 0) break; |
|
struct nlmsghdr *nlh = (struct nlmsghdr *)buf; |
|
if (nlh->nlmsg_type == NLMSG_DONE) break; |
|
if (nlh->nlmsg_type == NLMSG_ERROR) break; |
|
if (nlh->nlmsg_type == (uint16_t)ctx.nfc_family) { |
|
struct nlattr *a = (struct nlattr *)((char *)NLMSG_DATA(nlh) + GENL_HDRLEN); |
|
int rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; |
|
while (rem > (int)NLA_HDRLEN) { |
|
if (a->nla_type == NFC_ATTR_TARGET_INDEX) { |
|
ctx.target_idx = *(uint32_t *)((char *)a + NLA_HDRLEN); |
|
got_target = 1; |
|
} |
|
if (a->nla_type == NFC_ATTR_PROTOCOLS) { |
|
uint32_t p = *(uint32_t *)((char *)a + NLA_HDRLEN); |
|
if (p & NFC_PROTO_ISO14443_MASK) ctx.target_proto = NFC_PROTO_ISO14443; |
|
else if (p & NFC_PROTO_ISO14443_B_MASK) ctx.target_proto = NFC_PROTO_ISO14443_B; |
|
} |
|
int al = NLA_ALIGN(a->nla_len); |
|
rem -= al; |
|
a = (struct nlattr *)((char *)a + al); |
|
} |
|
} |
|
} |
|
|
|
/* Restore the 3-second timeout used by nfc_cmd(). |
|
* We temporarily used 2s for GET_TARGET's dump recv loop above. */ |
|
tv.tv_sec = 3; tv.tv_usec = 0; |
|
setsockopt(cmd_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
|
|
|
/* Drain any remaining messages */ |
|
struct pollfd pfd2 = { .fd = cmd_fd, .events = POLLIN }; |
|
while (poll(&pfd2, 1, 100) > 0) recv(cmd_fd, buf, sizeof(buf), 0); |
|
|
|
if (got_target) { |
|
LOG("Target: idx=%u proto=%u", ctx.target_idx, ctx.target_proto); |
|
return 0; |
|
} |
|
LOG("No target in GET_TARGET response"); |
|
return -1; |
|
} |
|
|
|
static int nfc_connect_target(void) { |
|
if (ctx.raw_fd >= 0) return 0; |
|
ctx.raw_fd = socket(AF_NFC, SOCK_SEQPACKET, NFC_SOCKPROTO_RAW); |
|
if (ctx.raw_fd < 0) { |
|
LOG("AF_NFC socket: %s", strerror(errno)); |
|
return -1; |
|
} |
|
struct sockaddr_nfc sa = { |
|
.sa_family = AF_NFC, .dev_idx = NFC_DEV_IDX, |
|
.target_idx = ctx.target_idx, .nfc_protocol = ctx.target_proto, |
|
}; |
|
if (connect(ctx.raw_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { |
|
LOG("NFC connect: %s (idx=%u proto=%u)", strerror(errno), |
|
ctx.target_idx, ctx.target_proto); |
|
close(ctx.raw_fd); ctx.raw_fd = -1; return -1; |
|
} |
|
ctx.card_connected = 1; |
|
LOG("NFC connected (idx=%u)", ctx.target_idx); |
|
return 0; |
|
} |
|
|
|
/* Disconnect the raw socket. If 'self' is true, we initiated this |
|
* (not a card removal), so TARGET_LOST should not clear card_present. */ |
|
static void nfc_disconnect_ex(int self) { |
|
if (ctx.raw_fd >= 0) { |
|
if (self) ctx.self_disconnect = 1; |
|
close(ctx.raw_fd); |
|
ctx.raw_fd = -1; |
|
} |
|
ctx.card_connected = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
} |
|
|
|
/* Full disconnect: mark card as absent, reset internal state. |
|
* Does NOT send STOP_POLL — after socket close, the NCI controller |
|
* auto-enters discovery mode (dev->polling=true), and sending STOP_POLL |
|
* would be immediately undone by the NCI notification handler. */ |
|
static void nfc_disconnect_full(void) { |
|
if (ctx.raw_fd >= 0) { close(ctx.raw_fd); ctx.raw_fd = -1; } |
|
ctx.card_connected = 0; |
|
ctx.card_present = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
ctx.self_disconnect = 0; |
|
ctx.poll_active = 0; |
|
LOG("NFC fully disconnected (card gone)"); |
|
} |
|
|
|
/* Fully connect to an NFC card: poll → find target → connect. |
|
* Handles recovery from stale connections. */ |
|
static int nfc_full_connect(void) { |
|
if (ctx.card_connected && ctx.raw_fd >= 0) |
|
return 0; |
|
|
|
/* Clean up any stale state */ |
|
if (ctx.raw_fd >= 0) { close(ctx.raw_fd); ctx.raw_fd = -1; } |
|
ctx.card_connected = 0; |
|
|
|
/* Drain any pending events */ |
|
nfc_check_events(); |
|
|
|
if (ctx.card_present && ctx.target_idx != (uint32_t)-1) { |
|
/* Card present with known target — try direct connect */ |
|
LOG("full_connect: card present (idx=%u), connecting directly", |
|
ctx.target_idx); |
|
if (nfc_connect_target() == 0) { |
|
LOG("full_connect: success (direct)"); |
|
return 0; |
|
} |
|
/* Connect failed (EINVAL = stale target). Clear state for re-poll. */ |
|
LOG("full_connect: direct connect failed, clearing for re-poll"); |
|
ctx.card_present = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
} |
|
|
|
if (ctx.card_present && ctx.target_idx == (uint32_t)-1) { |
|
/* Card was found by event but we don't have target info yet. |
|
* Try GET_TARGET first before re-polling. */ |
|
LOG("full_connect: card present, trying GET_TARGET"); |
|
if (nfc_get_target() == 0 && nfc_connect_target() == 0) { |
|
LOG("full_connect: success (via GET_TARGET)"); |
|
return 0; |
|
} |
|
/* Also stale — clear for re-poll. */ |
|
LOG("full_connect: GET_TARGET/connect failed, clearing for re-poll"); |
|
ctx.card_present = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
} |
|
|
|
/* Need to poll for card discovery. |
|
* After socket close, the NCI auto-enters discovery mode. |
|
* nfc_start_poll will either start our own poll or recognize |
|
* the kernel's auto-poll (EBUSY → poll_active=1). */ |
|
int poll_ret = nfc_start_poll_retry(); |
|
if (poll_ret < 0 && !ctx.card_present) { |
|
LOG("full_connect: poll failed and no card present"); |
|
return -1; |
|
} |
|
|
|
/* Wait for card detection. The NCI controller needs time to |
|
* deactivate the old target and find the card again. */ |
|
if (!ctx.card_present) { |
|
LOG("full_connect: waiting for card (up to 8s)..."); |
|
for (int i = 0; i < 80; i++) { |
|
if (nfc_check_events()) break; |
|
usleep(100000); |
|
} |
|
} |
|
|
|
if (!ctx.card_present) { |
|
LOG("full_connect: no card found"); |
|
return -1; |
|
} |
|
|
|
if (nfc_get_target() < 0) return -1; |
|
if (nfc_connect_target() < 0) return -1; |
|
LOG("full_connect: success"); |
|
return 0; |
|
} |
|
|
|
/* === Polling thread function for pcscd === */ |
|
|
|
/* pcscd calls this blocking function from its event handler thread. |
|
* It replaces the 400ms IFDHICCPresence polling with event-driven |
|
* card detection. Returns IFD_SUCCESS when a card event (insert/remove) |
|
* is detected. Returns non-SUCCESS on timeout. |
|
* |
|
* IMPORTANT: This function must be cancellable via pthread_cancel |
|
* (TAG_IFD_POLLING_THREAD_KILLABLE = 1). poll() is a cancellation point. */ |
|
static RESPONSECODE ifd_card_event(DWORD Lun, int timeout) { |
|
(void)Lun; |
|
|
|
pthread_mutex_lock(&ctx.lock); |
|
if (!ctx.initialized) { |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_ICC_NOT_PRESENT; |
|
} |
|
|
|
/* Check for already-pending events before blocking */ |
|
int prev_present = ctx.card_present; |
|
int prev_connected = ctx.card_connected; |
|
nfc_check_events(); |
|
if (ctx.card_present != prev_present || ctx.card_connected != prev_connected) { |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
/* Ensure polling is active when no card is present */ |
|
if (!ctx.card_present && !ctx.poll_active) { |
|
nfc_start_poll(); |
|
} |
|
|
|
int evt_fd = ctx.evt_fd; |
|
pthread_mutex_unlock(&ctx.lock); |
|
|
|
/* Block on event fd — no mutex held, so TransmitToICC can |
|
* proceed on the pcscd application thread. poll() is a |
|
* pthread cancellation point as required by pcscd. */ |
|
struct pollfd pfd = { .fd = evt_fd, .events = POLLIN }; |
|
int ret = poll(&pfd, 1, timeout); |
|
|
|
if (ret <= 0) { |
|
/* Timeout or error — no card event. |
|
* On timeout, also try to ensure poll is active. */ |
|
if (ret == 0) { |
|
pthread_mutex_lock(&ctx.lock); |
|
if (!ctx.card_present && !ctx.poll_active) { |
|
nfc_start_poll(); |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
} |
|
return IFD_ICC_NOT_PRESENT; |
|
} |
|
|
|
/* Event available — process it */ |
|
pthread_mutex_lock(&ctx.lock); |
|
nfc_check_events(); |
|
|
|
/* Restart polling after card removal so we detect the next card */ |
|
if (!ctx.card_present && !ctx.poll_active) { |
|
nfc_start_poll(); |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
|
|
return IFD_SUCCESS; |
|
} |
|
|
|
/* === IFD Handler API === */ |
|
|
|
RESPONSECODE IFDHCreateChannelByName(DWORD Lun, LPSTR DeviceName) { |
|
LOG("CreateChannel: Lun=%lu dev=%s", (unsigned long)Lun, |
|
DeviceName ? DeviceName : "(null)"); |
|
pthread_mutex_lock(&ctx.lock); |
|
if (ctx.initialized) { |
|
LOG("CreateChannel: already initialized"); |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
ctx.cmd_sk = nl_socket_alloc(); |
|
if (!ctx.cmd_sk || genl_connect(ctx.cmd_sk) < 0) { |
|
LOG("CreateChannel: cmd_sk alloc/connect failed"); |
|
goto fail; |
|
} |
|
ctx.evt_sk = nl_socket_alloc(); |
|
if (!ctx.evt_sk || genl_connect(ctx.evt_sk) < 0) { |
|
LOG("CreateChannel: evt_sk alloc/connect failed"); |
|
goto fail; |
|
} |
|
|
|
ctx.nfc_family = genl_ctrl_resolve(ctx.cmd_sk, NFC_GENL_NAME); |
|
if (ctx.nfc_family < 0) { |
|
LOG("CreateChannel: no NFC genl family (is NFC kernel module loaded?)"); |
|
goto fail; |
|
} |
|
ctx.events_group = genl_ctrl_resolve_grp(ctx.cmd_sk, NFC_GENL_NAME, |
|
NFC_GENL_MCAST_EVENT_NAME); |
|
if (ctx.events_group < 0) { |
|
LOG("CreateChannel: no NFC events group"); |
|
goto fail; |
|
} |
|
|
|
nl_socket_add_membership(ctx.evt_sk, ctx.events_group); |
|
nl_socket_disable_seq_check(ctx.evt_sk); |
|
ctx.evt_fd = nl_socket_get_fd(ctx.evt_sk); |
|
|
|
/* Set recv timeout on cmd socket AFTER genl_ctrl operations complete. |
|
* This prevents nl_recvmsgs from blocking forever on NFC commands |
|
* while not interfering with libnl's internal ctrl operations. */ |
|
{ |
|
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 }; |
|
setsockopt(nl_socket_get_fd(ctx.cmd_sk), SOL_SOCKET, SO_RCVTIMEO, |
|
&tv, sizeof(tv)); |
|
} |
|
|
|
memcpy(ctx.atr, DEFAULT_ATR, DEFAULT_ATR_LEN); |
|
ctx.atr_len = DEFAULT_ATR_LEN; |
|
ctx.target_idx = (uint32_t)-1; |
|
ctx.self_disconnect = 0; |
|
ctx.initialized = 1; |
|
|
|
/* Read NFC chip name from sysfs for identification */ |
|
FILE *f = fopen("/sys/class/nfc/nfc0/device/name", "r"); |
|
if (f) { |
|
if (fgets(ctx.nfc_name, sizeof(ctx.nfc_name), f)) { |
|
/* Strip trailing newline */ |
|
char *nl = strchr(ctx.nfc_name, '\n'); |
|
if (nl) *nl = '\0'; |
|
} |
|
fclose(f); |
|
} |
|
|
|
/* Ignore SIGPIPE: send() on a closed NFC socket must return an error, |
|
* not kill the pcscd process. */ |
|
signal(SIGPIPE, SIG_IGN); |
|
|
|
nfc_start_poll(); |
|
|
|
pthread_mutex_unlock(&ctx.lock); |
|
LOG("Initialized: %s (family=%d events=%d)", ctx.nfc_name, ctx.nfc_family, ctx.events_group); |
|
return IFD_SUCCESS; |
|
fail: |
|
if (ctx.evt_sk) { nl_socket_free(ctx.evt_sk); ctx.evt_sk = NULL; } |
|
if (ctx.cmd_sk) { nl_socket_free(ctx.cmd_sk); ctx.cmd_sk = NULL; } |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_COMMUNICATION_ERROR; |
|
} |
|
|
|
RESPONSECODE IFDHCreateChannel(DWORD Lun, DWORD Channel) { |
|
(void)Channel; |
|
return IFDHCreateChannelByName(Lun, NULL); |
|
} |
|
|
|
RESPONSECODE IFDHCloseChannel(DWORD Lun) { |
|
(void)Lun; |
|
LOG("CloseChannel"); |
|
pthread_mutex_lock(&ctx.lock); |
|
if (ctx.raw_fd >= 0) { close(ctx.raw_fd); ctx.raw_fd = -1; } |
|
ctx.card_connected = 0; |
|
ctx.card_present = 0; |
|
ctx.target_idx = (uint32_t)-1; |
|
/* Full shutdown: STOP_POLL + DEV_DOWN to cleanly release the NFC device */ |
|
nfc_cmd(NFC_CMD_STOP_POLL, NFC_DEV_IDX, -1, 0); |
|
nfc_cmd(NFC_CMD_DEV_DOWN, NFC_DEV_IDX, -1, 0); |
|
ctx.poll_active = 0; |
|
if (ctx.evt_sk) { nl_socket_free(ctx.evt_sk); ctx.evt_sk = NULL; } |
|
if (ctx.cmd_sk) { nl_socket_free(ctx.cmd_sk); ctx.cmd_sk = NULL; } |
|
ctx.initialized = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
RESPONSECODE IFDHGetCapabilities(DWORD Lun, DWORD Tag, PDWORD Length, PUCHAR Value) { |
|
(void)Lun; |
|
switch (Tag) { |
|
case TAG_IFD_ATR: |
|
if (*Length < ctx.atr_len) return IFD_ERROR_INSUFFICIENT_BUFFER; |
|
memcpy(Value, ctx.atr, ctx.atr_len); |
|
*Length = ctx.atr_len; |
|
return IFD_SUCCESS; |
|
case TAG_IFD_SIMULTANEOUS_ACCESS: |
|
*Value = 1; *Length = 1; return IFD_SUCCESS; |
|
case TAG_IFD_SLOTS_NUMBER: |
|
*Value = 1; *Length = 1; return IFD_SUCCESS; |
|
case TAG_IFD_THREAD_SAFE: |
|
case TAG_IFD_SLOT_THREAD_SAFE: |
|
*Value = 0; *Length = 1; return IFD_SUCCESS; |
|
case TAG_IFD_POLLING_THREAD_WITH_TIMEOUT: |
|
/* Event-driven card detection: pcscd calls our blocking function |
|
* instead of polling IFDHICCPresence every 400ms. */ |
|
if (*Length < sizeof(void *)) return IFD_ERROR_INSUFFICIENT_BUFFER; |
|
*(void **)Value = (void *)ifd_card_event; |
|
*Length = sizeof(void *); |
|
LOG("GetCapabilities: providing polling thread function"); |
|
return IFD_SUCCESS; |
|
case TAG_IFD_POLLING_THREAD_KILLABLE: |
|
/* Our blocking poll uses poll() which is a cancellation point. */ |
|
*Value = 1; *Length = 1; return IFD_SUCCESS; |
|
default: |
|
return IFD_ERROR_TAG; |
|
} |
|
} |
|
|
|
RESPONSECODE IFDHSetCapabilities(DWORD Lun, DWORD Tag, DWORD Length, PUCHAR Value) { |
|
(void)Lun; (void)Tag; (void)Length; (void)Value; |
|
return IFD_NOT_SUPPORTED; |
|
} |
|
|
|
RESPONSECODE IFDHSetProtocolParameters(DWORD Lun, DWORD Protocol, |
|
UCHAR Flags, UCHAR PTS1, UCHAR PTS2, UCHAR PTS3) { |
|
(void)Lun; (void)Protocol; (void)Flags; (void)PTS1; (void)PTS2; (void)PTS3; |
|
return IFD_SUCCESS; |
|
} |
|
|
|
RESPONSECODE IFDHPowerICC(DWORD Lun, DWORD Action, PUCHAR Atr, PDWORD AtrLength) { |
|
(void)Lun; |
|
LOG("PowerICC action=%lu", (unsigned long)Action); |
|
pthread_mutex_lock(&ctx.lock); |
|
|
|
switch (Action) { |
|
case IFD_POWER_UP: |
|
case IFD_RESET: |
|
/* NFC cards don't support power cycling or reset. |
|
* If already connected, just return success. |
|
* Both POWER_UP and RESET behave identically: |
|
* keep the existing connection if live. */ |
|
if (ctx.card_connected && ctx.raw_fd >= 0) { |
|
LOG("PowerICC: already connected (no-op)"); |
|
if (Atr && AtrLength) { |
|
memcpy(Atr, ctx.atr, ctx.atr_len); |
|
*AtrLength = ctx.atr_len; |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
/* Not connected — do full discovery + connect */ |
|
LOG("PowerICC: connecting..."); |
|
if (nfc_full_connect() < 0) { |
|
LOG("PowerICC: connect failed"); |
|
if (AtrLength) *AtrLength = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_ICC_NOT_PRESENT; |
|
} |
|
|
|
LOG("PowerICC: connected"); |
|
if (Atr && AtrLength) { |
|
memcpy(Atr, ctx.atr, ctx.atr_len); |
|
*AtrLength = ctx.atr_len; |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
|
|
case IFD_POWER_DOWN: |
|
/* Keep NFC connection alive. Disconnecting triggers TARGET_LOST |
|
* and a reconnection cycle that is slow and error-prone. */ |
|
LOG("PowerICC: power down (keeping connection)"); |
|
if (AtrLength) *AtrLength = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
|
|
default: |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_NOT_SUPPORTED; |
|
} |
|
} |
|
|
|
/* Internal: send APDU and receive response, stripping ISO-DEP PCB byte. |
|
* Returns number of response bytes (after stripping), or -1 on error. |
|
* Does NOT log APDU content (may contain private data). */ |
|
static ssize_t nfc_transceive(const unsigned char *tx, DWORD tx_len, |
|
unsigned char *rx, DWORD rx_max) { |
|
ssize_t sent = send(ctx.raw_fd, tx, tx_len, MSG_NOSIGNAL); |
|
if (sent < 0 || (size_t)sent != tx_len) { |
|
LOG("transceive: send failed: %s", strerror(errno)); |
|
return -1; |
|
} |
|
|
|
unsigned char tmp[4096]; |
|
DWORD tmp_max = rx_max < sizeof(tmp) ? rx_max + 1 : sizeof(tmp); |
|
|
|
/* Use a timeout so we don't block forever on a dead connection */ |
|
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; |
|
setsockopt(ctx.raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
|
|
|
ssize_t rcvd = recv(ctx.raw_fd, tmp, tmp_max, 0); |
|
|
|
/* Clear timeout */ |
|
tv.tv_sec = 0; tv.tv_usec = 0; |
|
setsockopt(ctx.raw_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
|
|
|
if (rcvd <= 0) { |
|
LOG("transceive: recv failed: %s", rcvd == 0 ? "EOF" : strerror(errno)); |
|
return -1; |
|
} |
|
|
|
/* AF_NFC raw socket prepends an ISO-DEP PCB byte (typically 0x00) |
|
* to every response. Strip it to get the raw APDU response. */ |
|
if (rcvd > 1) { |
|
memcpy(rx, tmp + 1, rcvd - 1); |
|
return rcvd - 1; |
|
} |
|
LOG("transceive: response too short (%zd bytes)", rcvd); |
|
return -1; |
|
} |
|
|
|
RESPONSECODE IFDHTransmitToICC(DWORD Lun, SCARD_IO_HEADER SendPci, |
|
PUCHAR TxBuffer, DWORD TxLength, |
|
PUCHAR RxBuffer, PDWORD RxLength, PSCARD_IO_HEADER RecvPci) { |
|
(void)Lun; (void)SendPci; |
|
pthread_mutex_lock(&ctx.lock); |
|
|
|
if (ctx.raw_fd < 0) { |
|
LOG("Transmit: not connected, trying to connect"); |
|
if (nfc_full_connect() < 0) { |
|
LOG("Transmit: connect failed"); |
|
*RxLength = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_COMMUNICATION_ERROR; |
|
} |
|
} |
|
|
|
/* Intercept pseudo-APDUs (CLA=FF): these are PC/SC reader commands |
|
* (e.g. FF 9A for TR-03119 PACE), not card APDUs. Never forward them |
|
* to the NFC card — the card won't respond or will timeout. */ |
|
if (TxLength >= 1 && TxBuffer[0] == 0xFF) { |
|
LOG("Transmit: pseudo-APDU CLA=FF INS=%02X (handled locally)", |
|
TxLength >= 2 ? TxBuffer[1] : 0); |
|
/* Return 6E00 = "Class not supported" */ |
|
if (*RxLength >= 2) { |
|
RxBuffer[0] = 0x6E; |
|
RxBuffer[1] = 0x00; |
|
*RxLength = 2; |
|
} else { |
|
*RxLength = 0; |
|
} |
|
if (RecvPci) { |
|
RecvPci->Protocol = SendPci.Protocol; |
|
RecvPci->Length = sizeof(SCARD_IO_HEADER); |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
/* Log APDU class/instruction only (no private data) */ |
|
if (TxLength >= 4) { |
|
LOG("Transmit: CLA=%02X INS=%02X P1=%02X P2=%02X len=%lu", |
|
TxBuffer[0], TxBuffer[1], TxBuffer[2], TxBuffer[3], |
|
(unsigned long)TxLength); |
|
} |
|
|
|
ssize_t rcvd = nfc_transceive(TxBuffer, TxLength, RxBuffer, *RxLength); |
|
if (rcvd < 0) { |
|
/* Transceive failed — connection is stale (NCI idle timeout). |
|
* Close socket without self_disconnect so TARGET_LOST clears |
|
* card_present, forcing a clean re-poll cycle. */ |
|
LOG("Transmit: failed, attempting clean reconnect"); |
|
nfc_disconnect_ex(0); |
|
usleep(500000); |
|
nfc_check_events(); |
|
if (nfc_full_connect() < 0) { |
|
LOG("Transmit: reconnect failed"); |
|
nfc_disconnect_full(); |
|
*RxLength = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_COMMUNICATION_ERROR; |
|
} |
|
rcvd = nfc_transceive(TxBuffer, TxLength, RxBuffer, *RxLength); |
|
if (rcvd < 0) { |
|
LOG("Transmit: retry failed after reconnect"); |
|
nfc_disconnect_full(); |
|
*RxLength = 0; |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_COMMUNICATION_ERROR; |
|
} |
|
LOG("Transmit: retry succeeded after reconnect"); |
|
} |
|
|
|
/* Log only SW bytes (status, not private data) */ |
|
if (rcvd >= 2) { |
|
LOG("Transmit: SW=%02X%02X (%zd bytes)", |
|
RxBuffer[rcvd - 2], RxBuffer[rcvd - 1], rcvd); |
|
} |
|
|
|
*RxLength = rcvd; |
|
if (RecvPci) { |
|
RecvPci->Protocol = SendPci.Protocol; |
|
RecvPci->Length = sizeof(SCARD_IO_HEADER); |
|
} |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
RESPONSECODE IFDHICCPresence(DWORD Lun) { |
|
(void)Lun; |
|
pthread_mutex_lock(&ctx.lock); |
|
if (!ctx.initialized) { |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_ICC_NOT_PRESENT; |
|
} |
|
|
|
/* Process any pending NFC events */ |
|
nfc_check_events(); |
|
|
|
if (ctx.card_connected && ctx.raw_fd >= 0) { |
|
/* Verify socket is still alive */ |
|
struct pollfd pfd = { .fd = ctx.raw_fd, .events = POLLERR | POLLHUP }; |
|
if (poll(&pfd, 1, 0) > 0 && (pfd.revents & (POLLERR | POLLHUP))) { |
|
LOG("ICCPresence: socket error, disconnecting"); |
|
nfc_disconnect_ex(1); |
|
usleep(100000); |
|
nfc_check_events(); |
|
} else { |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
} |
|
|
|
if (ctx.card_present) { |
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_SUCCESS; |
|
} |
|
|
|
/* No card — ensure polling is active so we detect new cards. |
|
* This is a fallback; primary card detection is via the |
|
* polling thread (ifd_card_event). */ |
|
if (!ctx.poll_active) { |
|
nfc_start_poll(); |
|
} |
|
|
|
pthread_mutex_unlock(&ctx.lock); |
|
return IFD_ICC_NOT_PRESENT; |
|
} |
|
|
|
RESPONSECODE IFDHControl(DWORD Lun, DWORD dwControlCode, |
|
PUCHAR TxBuffer, DWORD TxLength, |
|
PUCHAR RxBuffer, DWORD RxLength, LPDWORD pdwBytesReturned) { |
|
(void)Lun; (void)dwControlCode; (void)TxBuffer; (void)TxLength; |
|
(void)RxBuffer; (void)RxLength; |
|
LOG("Control: code=0x%lX (not supported)", (unsigned long)dwControlCode); |
|
*pdwBytesReturned = 0; |
|
return IFD_ERROR_NOT_SUPPORTED; |
|
} |