Skip to content

fix(android): CCID response and command frame chaining - #380

Open
AdamVe wants to merge 6 commits into
mainfrom
adamve/fix/ccid-chaining
Open

AdamVe wants to merge 6 commits into
mainfrom
adamve/fix/ccid-chaining

Conversation

@AdamVe

@AdamVe AdamVe commented Sep 22, 2026

Copy link
Copy Markdown
Member

Reads the CCID class descriptor to discover the reader's buffer limit, reassembles multi-frame responses via bChainParameter (CCID 1.10 §6.2.1), and splits command APDUs that exceed dwMaxCCIDMessageLength across frames using wLevelParameter chaining; TPDU-only readers are detected and rejected with a clear error.

Fixes silent data corruption on contactless readers such as the HID OMNIKEY 5022-CL, where large responses were split across frames (returning truncated data) and large command APDUs overflowed the reader's 520-byte buffer (corrupting the card's view of the command from byte 495 onward).

Unit tests cover all bChainParameter values, empty-chunk rejection, the reassembly cap, and frame-boundary cases; ExternalCcidReaderTests round-trips payloads up to 2048 bytes through an OMNIKEY 5022-CL with a YubiKey 5 NFC in its field; SmokeDeviceTests (29 tests, YubiKey direct-attached) confirmed no regression on the normal USB path.

happyfuncode and others added 6 commits September 21, 2026 16:39
CCID 1.10 §6.2.1 lets RDR_to_PC_DataBlock signal that the response APDU
is split across multiple frames via the bChainParameter byte at offset 9
of the header. UsbSmartCardConnection.sendAndReceive() previously read
the first frame, silently truncated via Math.min(received, dwLength),
and returned. For YubiKeys the response always fits in one frame so the
bug never surfaced. For contactless USB CCID readers (HID OMNIKEY
5022-CL etc.) reading anything bigger than ~500 B — e.g. a PIV
CHUID or facial image — the response came back mid-byte.

Fix: parse bChainParameter into a per-connection field; if it's 0x01
(first) or 0x03 (middle), pull each subsequent chunk via a follow-up
PC_to_RDR_XfrBlock with wLevelParameter = 0x0010 (CCID §6.1.4) and an
empty data field until the reader reports 0x00 (complete) or 0x02
(last). Concatenate the chunks before returning.

Adds testChainedResponse exercising a three-frame chained response.
Cap a reassembled chained response at the largest response APDU ISO
7816-4 can express, reject an empty continuation chunk rather than
looping on it, and extract responseIsChained() so the two continuation
states are named in one place.

Adds five cases: two-frame chain, last-without-first, unchained, empty
continuation chunk, and both sides of the length cap.
The default USB filter matches the Yubico vendor id, so no test could
reach a third-party smart card reader and the chaining paths in
UsbSmartCardConnection had no hardware coverage. CcidReaderFilter
widens it to any device exposing a CCID interface; YubiKeys are still
admitted unchanged.

ExternalCcidReaderTests echoes payloads up to 2048 bytes off a YubiKey
in the reader's field and compares byte for byte, since a reader that
drops a chunk still answers SW 9000. It fails rather than skips when
the reader is absent, so it cannot report green untested.
Read dwFeatures and dwMaxCCIDMessageLength from the reader's CCID
class descriptor.

A reader offering only the TPDU exchange level expects the host to
frame T=1 blocks itself, which this class does not do, so it is now
refused at open instead of being sent APDU-level XfrBlocks.
The host never split a command APDU exceeding the reader's
dwMaxCCIDMessageLength, so the OMNIKEY 5022-CL (520 bytes) dropped
the overflow and the card echoed stale buffer under SW 9000.

Split it across XfrBlock messages chained via wLevelParameter and
require an acknowledgement for each non-final chunk.
Heading stays "Next version" until the release version is decided.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants