PC/SC & CCID: Smart Card Reader Programming Guide
1. PC/SC Architecture Overview
PC/SC (Personal Computer / Smart Card) is the industry-standard API for accessing smart card readers from desktop applications. It was originally defined by the PC/SC Workgroup (Microsoft, Bull, Gemplus, Schlumberger, HP, etc.) and is now maintained as part of the ISO/IEC 7816 ecosystem.
Layered Architecture
+---------------------------+ | Application (your code) | +---------------------------+ | WinSCard.dll / libpcsclite | +---------------------------+ | PC/SC Resource Manager | <-- SCardSvr (Windows) / pcscd (Linux) +---------------------------+ | Reader Driver (.inf/.sys) | <-- IFD Handler (Interface Device) +---------------------------+ | USB / Serial / PCI | +---------------------------+ | Smart Card Reader HW | +---------------------------+ | Smart Card (ICC) | +---------------------------+
Key Components
- Resource Manager (WinSCard.dll / libpcsclite.so) — Mediates access to readers and cards. Handles reader enumeration, card insertion/removal events, and exclusive access arbitration.
- IFD Handler (Interface Device Handler) — Vendor-specific driver that translates PC/SC API calls into reader-specific commands. Most readers use the generic CCID driver.
- Service/daemon:
SCardSvr(Windows service) /pcscd(Linux daemon)
2. WinSCard API: The Core Functions
The WinSCard API (winscard.dll) is the Windows implementation of PC/SC. The complete lifecycle:
// 1. Establish context — initializes the PC/SC subsystem
SCardEstablishContext(
SCARD_SCOPE_USER, // or SCARD_SCOPE_SYSTEM for service-level
NULL, // reserved
NULL, // reserved
&hContext // [out] context handle
);
// 2. List readers — get available reader names
SCardListReaders(
hContext,
NULL, // all reader groups
mszReaders, // [out] multi-string buffer
&dwReadersLen // [in/out] buffer size
);
// mszReaders contains null-separated reader names, double-null terminated
// e.g. "ACS ACR122U 0\0Broadcom Corp Contacted 0\0\0"
// 3. Connect to card — establishes APDU transport
SCardConnect(
hContext,
szReader, // reader name from ListReaders
SCARD_SHARE_SHARED, // exclusive vs shared access
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, // preferred protocols
&hCard, // [out] card handle
&dwActiveProtocol // [out] negotiated protocol (T=0 or T=1)
);
// 4. Transmit APDU — send command, receive response
SCardTransmit(
hCard,
pioSendPci, // SCARD_PCI_T0 or SCARD_PCI_T1
pbSendBuffer, // APDU command bytes
cbSendLength, // command length
pioRecvPci, // NULL (optional, for receiving protocol info)
pbRecvBuffer, // [out] APDU response buffer
&cbRecvLength // [in/out] max receive size → actual received
);
// 5. Disconnect
SCardDisconnect(hCard, SCARD_LEAVE_CARD); // or SCARD_EJECT_CARD, SCARD_RESET_CARD
// 6. Release context
SCardReleaseContext(hContext);
Complete Win32 C Example
#include <winscard.h>
#include <stdio.h>
#pragma comment(lib, "winscard.lib")
int main() {
SCARDCONTEXT hContext;
SCARDHANDLE hCard;
DWORD dwActiveProtocol, dwReadersLen, dwRecvLen;
LONG lRet;
// 1. Establish context
lRet = SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, &hContext);
if (lRet != SCARD_S_SUCCESS) { printf("EstablishContext failed: 0x%08X\n", lRet); return 1; }
// 2. List readers (get buffer size first)
dwReadersLen = 0;
lRet = SCardListReaders(hContext, NULL, NULL, &dwReadersLen);
if (lRet != SCARD_S_SUCCESS) { printf("No readers found\n"); return 1; }
char *mszReaders = malloc(dwReadersLen);
lRet = SCardListReaders(hContext, NULL, mszReaders, &dwReadersLen);
// 3. Connect to first reader
char *reader = mszReaders;
printf("Reader: %s\n", reader);
lRet = SCardConnect(hContext, reader, SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
&hCard, &dwActiveProtocol);
if (lRet != SCARD_S_SUCCESS) { printf("Connect failed: 0x%08X\n", lRet); return 1; }
printf("Protocol: %s\n", dwActiveProtocol == SCARD_PROTOCOL_T0 ? "T=0" : "T=1");
// 4. Transmit SELECT APDU
BYTE cmd[] = { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x00 };
BYTE resp[258];
dwRecvLen = sizeof(resp);
lRet = SCardTransmit(hCard, SCARD_PCI_T1, cmd, sizeof(cmd),
NULL, resp, &dwRecvLen);
if (lRet == SCARD_S_SUCCESS) {
printf("Response (%lu bytes): ", dwRecvLen);
for (DWORD i = 0; i < dwRecvLen; i++) printf("%02X ", resp[i]);
printf("\nSW1 SW2: %02X %02X\n", resp[dwRecvLen-2], resp[dwRecvLen-1]);
}
// 5-6. Cleanup
SCardDisconnect(hCard, SCARD_LEAVE_CARD);
SCardReleaseContext(hContext);
free(mszReaders);
return 0;
}
SCardControl — Reader-Specific Commands
SCardControl sends commands directly to the reader driver (bypassing the card). Common uses:
- IOCTL_CCID_ESCAPE — Send raw CCID escape command (e.g., get reader firmware version)
- SCARD_CTL_CODE(3500) — Transparent APDU exchange (no T=0/T=1 framing)
- IOCTL_SMARTCARD_POWER — Power on/off the card slot
- IOCTL_SMARTCARD_GET_ATTRIBUTE — Get reader attributes (vendor name, firmware version)
3. pcsc-lite on Linux & macOS
pcsc-lite is the POSIX implementation of PC/SC, maintained by the Muscle project. Its API is nearly identical to WinSCard but follows POSIX conventions.
Installation
- Debian/Ubuntu:
apt install libpcsclite-dev pcscd pcsc-tools - RHEL/Fedora:
dnf install pcsc-lite-devel pcsc-lite - macOS: Built-in (Apple PC/SC framework).
#include <PCSC/winscard.h> - Daemon:
sudo systemctl start pcscd/sudo pcscd -f -d(foreground debug)
Key Differences from Windows
- Header:
#include <winscard.h>(same API names, different header) - Linking:
-lpcsclite(not-lwinscard) - Return type:
LONGworks the same; error codes are identical (SCARD_S_SUCCESS,SCARD_E_TIMEOUT) - macOS specific: Use
#include <PCSC/winscard.h>and link with-framework PCSC - Reader names: Linux uses descriptive names like
"ACS ACR122U PICC Interface 00 00" - Debugging: Set
PCSCLITE_DEBUG=3environment variable for verbose logging
pcsc_scan — Quick Reader/Card Test
The pcsc_scan utility shows all connected readers and card events in real time:
$ pcsc_scan PC/SC device scanner Using reader plug'n play mechanism Scanning present readers... 0: ACS ACR122U PICC Interface 00 00 Fri Jul 28 09:15:00 2026 Reader 0: ACS ACR122U PICC Interface 00 00 Event number: 0 Card state: Card inserted, ATR: 3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A
4. CCID USB Protocol: Under the Hood
CCID (Chip/Smart Card Interface Device) is the USB device class specification that standardizes how smart card readers appear on the USB bus. 95%+ of modern smart card readers use the CCID USB class — this means one generic OS driver (usbccid.sys on Windows, libccid on Linux) works with thousands of reader models.
USB Descriptors
A CCID reader identifies itself via USB class codes:
- Class: 0x0B (Chip Card / Smart Card)
- Subclass: 0x00
- Protocol: 0x00 (CCID)
CCID Bulk Transfer Protocol
+--------+-----------+--------+------------+---------+ | bType | dwLength | bSlot | bSeq | Data... | | (1 B) | (4 B LE) | (1 B) | (1 B) | (var) | +--------+-----------+--------+------------+---------+ bMessageType: 0x61 = PC_to_RDR_IccPowerOn — power on card, get ATR 0x62 = PC_to_RDR_IccPowerOff — power off card 0x63 = PC_to_RDR_GetSlotStatus — check card present/absent 0x65 = PC_to_RDR_XfrBlock — send APDU, receive response 0x6B = PC_to_RDR_Escape — vendor-specific command Response messages use 0x80-0x83 (RDR_to_PC_*)
Key CCID Command: PC_to_RDR_XfrBlock
This is the CCID equivalent of SCardTransmit. The host sends an APDU as part of the CCID message, and the reader returns the card's response:
// Host → Reader (XfrBlock, APDU: SELECT command) 65 00 00 00 00 00 00 0D 00 00 00 00 A4 04 00 07 A0 00 00 00 04 10 10 00 │ │ │ │ │ │ │ │ │ │ │ │ └─ bBWI (block wait) └─ APDU data (13 bytes) │ │ │ │ └─ wLevelParameter │ │ │ └─ bSeq (sequence number) │ │ └─ bSlot (0 = first slot) │ └─ dwLength (20 bytes total = 10 header + 13 APDU) └─ bMessageType = 0x65 (XfrBlock) // Reader → Host (response) 80 00 00 00 00 00 00 02 00 00 00 90 00 │ │ │ │ │ │ │ │ │ │ │ │ └─ CCID status └─ APDU response (SW1 SW2 = 90 00) │ │ │ │ └─ bChainParameter │ │ │ └─ bSeq │ │ └─ bSlot │ └─ dwLength (12 bytes header + 2 data) └─ bMessageType = 0x80 (RDR_to_PC_DataBlock)
CCID on Linux: libccid
The libccid driver translates pcsc-lite API calls into CCID USB bulk transfers. Reader quirks (timing, special commands) are handled through an Info.plist configuration file at /usr/lib/pcsc/drivers/ifd-ccid.bundle/Contents/Info.plist.
sudo usbmon (Linux) or Wireshark USB capture filter usb.bDescriptorType == 0x21 to inspect raw CCID bulk transfers. On Windows, use USBPcap + Wireshark.
5. ATR Parsing: Answer to Reset Decoded
The ATR (Answer to Reset) is the first data the card sends after power-on. It identifies the card's communication parameters, protocol capabilities, and optionally historical bytes that encode the card's application capabilities. We also provide an interactive ATR Decoder Tool for parsing ATR hex in-browser.
ATR Structure
ATR = TS + T0 + [TA1 TB1 TC1 TD1] + [TA2 TB2 TC2 TD2] + ... + [Historical Bytes] + [TCK] TS (1 byte): Initial character 0x3B = Direct convention (L/H = 1/0) 0x23 = Inverse convention (L/H = 0/1) T0 (1 byte): Bits 7-4: Y1 — indicates presence of TA1..TD1 Bits 3-0: K — number of Historical Bytes (0-15) TAi (format character): Clock rate, bit rate adjustment TBi (format character): Programming voltage, I/O parameters TCi (format character): Extra guard time TDi (format character): Bits 7-4: Y(i+1) — indicates next interface bytes Bits 3-0: T — protocol type (0=T=0, 1=T=1, 14=Type 14)
Example ATR Decoding
ATR: 3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A TS = 0x3B → Direct convention T0 = 0x8F → Y1=0x8 (TA1+TD1 present), K=0xF (15 historical bytes) TA1 = 0x80 → Fi/f=372/4 (clock rate conversion) TD1 = 0x01 → Y2=0x0 (no more TA-TD), T=1 protocol Historical bytes (15): 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 TCK = 0x6A → XOR checksum Interpretation: - ISO 7816-3 compliant, T=1 protocol - Card supports Vcc=5V, I=50mA - Historical bytes indicate a GlobalPlatform card (80 4F 0C = GP tag)
6. T=0 vs T=1: Transmission Protocols
| T=0 | T=1 | |
|---|---|---|
| Type | Byte-oriented (asynchronous half-duplex) | Block-oriented (asynchronous half-duplex) |
| Error detection | Parity bit only (no LRC/CRC) | LRC or CRC per block |
| Procedure byte | ACK/NULL/SW1 after each byte | N/A — blocks are acknowledged |
| APDU case 2/4 Le | GET RESPONSE command if Le > returned data | Native — Le in APDU header works directly |
| Chain commands | Not supported natively | Block chaining (M-bit) |
| Typical use | Legacy GSM SIM cards, older banking cards | EMV payment cards, Java Card, eUICC/eSIM |
| Throughput | Slower (~9600-38400 bps) | Faster (up to 600+ kbps with PPS) |
PC/SC Protocol Handling
PC/SC abstracts protocol differences: SCardTransmit handles T=0 GET RESPONSE and T=1 chaining transparently for Case 2/4 APDUs. The application simply sends the APDU and receives the full response. However, in T=0 mode, some cards require the application to handle 61 XX (data available) and 6C XX (wrong Le) status words manually.
7. Python + pyscard Examples
pyscard is the most popular Python wrapper for PC/SC. Install: pip install pyscard.
from smartcard.System import readers
from smartcard.util import toHexString
# 1. List readers
r = readers()
print("Available readers:", [str(rdr) for rdr in r])
if not r:
print("No readers found")
exit()
connection = r[0].createConnection()
connection.connect()
# 2. Send SELECT APDU
apdu = [0x00, 0xA4, 0x04, 0x00, 0x07,
0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x00]
response, sw1, sw2 = connection.transmit(apdu)
print(f"Response: {toHexString(response)}")
print(f"Status: SW1={sw1:02X} SW2={sw2:02X}")
# 3. Non-blocking card event monitoring
from smartcard.CardMonitoring import CardMonitor, CardObserver
class MyObserver(CardObserver):
def update(self, observable, actions):
(added_cards, removed_cards) = actions
for card in added_cards:
print(f"Card inserted: {card.atr}")
for card in removed_cards:
print("Card removed")
monitor = CardMonitor()
observer = MyObserver()
monitor.addObserver(observer)
# Keep running...
import time
time.sleep(30)
monitor.deleteObserver(observer)
pyscard + GlobalPlatform Example
# SELECT ISD (Card Manager AID)
select_isd = [0x00, 0xA4, 0x04, 0x00, 0x08,
0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00]
resp, sw1, sw2 = connection.transmit(select_isd)
print(f"ISD Selected: SW={sw1:02X}{sw2:02X}")
# INITIALIZE UPDATE (GP SCP02)
init_update = [0x80, 0x50, 0x00, 0x00, 0x08,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
resp, sw1, sw2 = connection.transmit(init_update)
# Parse: Key Diversification Data, Key Info, Card Challenge
print(f"Init Update response: {toHexString(resp)}")
8. Java + javax.smartcardio Examples
import javax.smartcardio.*;
import java.util.List;
public class SmartCardDemo {
public static void main(String[] args) throws Exception {
TerminalFactory factory = TerminalFactory.getDefault();
List<CardTerminal> terminals = factory.terminals().list();
System.out.println("Readers: " + terminals.size());
CardTerminal terminal = terminals.get(0);
System.out.println("Using: " + terminal.getName());
// Wait for card (5 seconds timeout)
if (!terminal.waitForCardPresent(5000)) {
System.out.println("No card detected");
return;
}
Card card = terminal.connect("T=1"); // or "*" for any protocol
CardChannel channel = card.getBasicChannel();
// SELECT APDU
byte[] apdu = {
0x00, (byte)0xA4, 0x04, 0x00, 0x07,
(byte)0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x00
};
CommandAPDU cmd = new CommandAPDU(apdu);
ResponseAPDU resp = channel.transmit(cmd);
System.out.printf("SW: %04X (%s)%n", resp.getSW(),
resp.getSW() == 0x9000 ? "Success" : "Error");
System.out.println("Data: " + bytesToHex(resp.getData()));
card.disconnect(false);
}
}
9. Practical Patterns: APDU Send/Receive
Pattern 1: SELECT + GET RESPONSE (T=0 compatible)
// Send SELECT
resp = transmit([00 A4 04 00 07 A0 00 00 00 04 10 10 00])
if SW == 61 XX:
// Card has XX bytes of data available — send GET RESPONSE
resp = transmit([00 C0 00 00 XX])
// XX = SW2 from previous response
Pattern 2: Extended APDU with Large Payload
// Sending 512 bytes of data (Case 4 Extended APDU) CLA = 00 INS = D6 P1 = 00 P2 = 00 Lc = 00 02 00 (3-byte extended Lc, value = 512) DATA = [512 bytes...] Le = 00 00 (2-byte extended Le, max response) APDU = CLA INS P1 P2 00 02 00 DATA... 00 00
Pattern 3: Card Presence Monitor Loop
SCARD_READERSTATE state;
state.szReader = readerName;
state.dwCurrentState = SCARD_STATE_UNAWARE;
while (running) {
SCardGetStatusChange(hContext, INFINITE, &state, 1);
if (state.dwEventState & SCARD_STATE_PRESENT) {
if (!(state.dwCurrentState & SCARD_STATE_PRESENT)) {
// Card just inserted
OnCardInserted(readerName, state.rgbAtr, state.cbAtr & 0xFF);
}
}
state.dwCurrentState = state.dwEventState;
}
Summary
PC/SC provides a stable, cross-platform API for smart card reader access that has been the industry standard for decades. Understanding the full stack — from the high-level SCardTransmit API down to CCID USB bulk transfers — enables robust smart card application development on Windows, Linux, macOS, and embedded systems.
Key takeaways:
- PC/SC is the universal smart card reader API — same WinSCard API on Windows, pcsc-lite on Linux/macOS
- CCID is the USB device class that 95%+ of readers use — one generic driver works for thousands of models
- SCardTransmit is the core function — send an APDU byte array, get a response byte array back
- T=0 requires GET RESPONSE handling for Case 2/4 APDUs; T=1 handles it natively
- ATR parsing reveals the card's protocol, voltage, and application capabilities in the first bytes after power-on
- pyscard (Python) and javax.smartcardio (Java) provide first-class PC/SC wrappers