oidc-pcsc plugin


This plugin provides a sample implementation for Smartcard/NFC-token authentication based on pcsc-lite.


Relies on use-space pcscd resource manager to read/write NFC scard/token.

  • Libraries (with xxx-devel when building from sources)
    • pcsc-lite
    • pcsc-*ccid
    • afb-libafb
  • Blacklist default NFC kernel module
    • sudo cp $SOURCES/libs/pcscd-client/test/nfc-blacklist.conf /etc/modprobe.d
    • rmmod nfc and dependencies (or reboot)
    • systemctl enable pcscd.service
  • Run pcscd manager
    • foreground: sudo /usr/sbin/pcscd -f
    • background: systemctl start pcscd.service

Supported readers/cards

The code was tested with Mifare classic tokens but pcsc-lite supports most of CCID compliant tokens. Nevertheless note that each scard/token has it own flavor or API and data organization which may require config/code customization.



The simplest way to test your reader/token is to use pcsc-client with a custom config.json. Note that pcsc-client should be available for major Linux distributions.

 ~/$SOURCES/sec-gate-oidc/build> ./package/bin/pcsc-client --config=../src/idps-plugins/oidc-pcsc/test/simple-pcsc.json --group=0 --async --verbose
 -- Waiting: 1s events for reader=ACS ACR122U PICC Interface 00 00 (ctrl-C to quit)
 -- async: reader=ACS ACR122U PICC Interface 00 00 status=0x5e0012
 -- event: reader=ACS ACR122U PICC Interface 00 00 removed (waiting for new card)
 Ctrl-C received
 On Signal Exit


Json configuration is organized in sections:

  • reader: a subset of reader full name
  • keys: defined keys used when a command requires authentication
  • cmds: your commands list
  • verbose: level of verbosity when not passed from API with –verbose


It is a subset of reader name. When multiple readers respond to the subset first reader found is used. When no reader name is provided, oidc-pcsc uses first available reader. As a result if your have only one reader you do not have to know its name.

    // "ACR122U" for "ACS ACR122U PICC Interface 00 00"
    "reader": "ACR122U",


Keys are only needed when your commands require authentication. This is typically the case when using scard/token data for authentication.

    "keys": [
        {"uid":"dfltA", "idx": 0, "value":["0xFF","0xFF","0xFF","0xFF","0xFF","0xFF"]},
        {"uid":"key-a", "idx": 0, "value":["0x01","0x02","0x03","0x04","0x05","0x06"]},
        {"uid":"key-b", "idx": 1, "value":["0x0A","0x0B","0x0C","0x0D","0x0E","0x0F"]}
  • key-A -> idx:0
  • key-B -> idx:1
  • value: ASCII or Hexa key value.

Mifare-Classic support two keys A/B where both should have 6 bytes. Default keys on new cards is 0xFFFFFF for both keys. When a command does not specify a key default keysA is used for both read and write operation. Default should work with any new card.


Each scard model has a private physical organization (page, sector, blocs, …) as well as it own authentication and API capabilities. As said before oidc-pcsc was tested with Mifare-Classic, if you need to support a different card model you may have to tweak configuration and code. Note that commands are stored in order and pcsc-client execute then from config order.

    "cmds": [
        {"uid":"cmd-1" , "group": 0, "action":"read", "blk": 1, "len": 16},
        {"uid":"cmd-2" , "group": 0, "action":"read", "blk": 3, "len": 16},
        {"uid":"cmd-n" , "group":x, "action":xxxx, "blk":xx, "len":xx},

Each command should have:

  • uid: [mandatory] information used to identify command within your config
  • group: [optional] used to class command in config. pscsc-test command use –group=xx to only execute command from a given group. (default:0)
  • action: [mandatory]
    • read: read one/multiple blocs
    • write read one/multiple blocs
    • trailer: write access control bit and authentication keys for a given sector.
  • sec: [optional] With Mifare/classic sector is map to 4 blocks also (sec:1,block:1) is equivalent to (block:5). Some token as NFC/type-2 requires a sector index. (default:0)
  • blk: [mandatory] block index for read and write commands.
  • len: [mandatory/read, optional/write] specify amount of data to read. With write action, ‘len’ is the maximum of data written, any remaining input is silently ignored. Warning: it is the application responsibility to provide a buffer big enough to hold read data.
  • value: [mandatory for write/trailer] provides information to write on the scard. The information may by provided in hexa or ascii form. Warning: depending on token/scard model writable size diverge. Mifare only supports 0x10,0x20,x30 value length. Last bloc written with trailer command is reserved for access control bits/keys.


Trailer is a specialized version of write command used to simplify access control bit/keys writing.

{"uid":"set-acls", "group": 2, "action":"trailer", "blk": 27, "key":"dfltA", "trailer": {"keyA": "key-a","keyB":"key-b","acls":["0xF0","0xF7","0x80","0x00"]}},

WARNING: trailer command can brick you scard/token. Writing a wrong ACL/keys will kill concerned block. Double, triple check your command on a single block/card before provisioning a set of cards.

  • group: [recommended] while not mandatory ‘trailer’ commands are usually isolated in a private group, as they change access control and usually cannot be executed twice.
  • blk: [mandatory] with Myfare/classic block index should match the last bloc of a given sector/page with blocIdx modulo 4 == 3.
  • key: [optional] authentication key. When not provided trailer command uses default (0xFFFFFF) which should work with any new Mifare/classic card. After changing ACLs authentication key depends on your config.
  • key-a/b: [mandatory] The new access control key. Key-a/b should match a predefined key-uid from your ‘keys’ config section. Key-a/b are written with your acls bloc and will be required for further action on concerned blocs.
  • acls: [mandatory] The acls control bits of you trailer block. WARNING invalid acls data will brick all blocks from targeted page/sector. Check further note to create a valid acls blocs.

ACLs control bits

Trailer access control is composed of 4 bytes.

  • The 3 first bytes store the access control as such. Acls are stored in both normal and inverted bit values.
  • Last byte is free and available as user-data.
  • *Any error in acls data will brick the 4 blocs of concerned page**

Acls are too complex/risky for normal human: use http://calc.gmss.ru/Mifare1k

Here after default configuration for a new scard.

    // ---
    // blk-0:   (C10 C20 C30)= 000 (|C10|C20|C30)= 111 (transport config)
    // blk-1:   (C11 C21 C31)= 000 (|C11|C21|C31)= 111
    // blk-2:   (C12 C22 C32)= 000 (|C12|C22|C32)= 111
    // trailer: (C13 C23 C33)= 001 (|C13|C23|C33)= 110 (transport config)
    // ---
    // Byte-6 |C23|C22|C21|C20 0xFF 1111-1111  |C13|C12|C11|C10
    // Byte-7  C13,C12,C11,C10 0x07 0000-0111  |C33|C32|C31|C30
    // Byte-8  C33,C32,C31,C30 0x80 1000-0000   C23,C22,C21,C20
    // ----

For further information: check NXP Mifare chaper 8.7.2 MF1S70YYX

Sample full config

Note: the ‘set-acls’ command (group=2) should work for a new card. But after 1st usage do not forget that ‘key-b’ will be required to write concerned trailer bloc.

    "info" : "Basic PCSC test for Mifare Classic",
    "reader": "ACR122U",
    "keys": [
        {"uid":"dfltA", "idx": 0, "value":["0xFF","0xFF","0xFF","0xFF","0xFF","0xFF"]},
        {"uid":"key-a", "idx": 0, "value":["0x01","0x02","0x03","0x04","0x05","0x06"]},
        {"uid":"key-b", "idx": 1, "value":["0x0A","0x0B","0x0C","0x0D","0x0E","0x0F"]}
    "cmds": [
        // group :0 public access (default keys)
        {"uid":"public-read" , "group": 0, "action":"read", "blk": 1, "len": 16},
        {"uid":"public-write", "group": 0, "action":"write","blk": 2, "data": "abcdefghijklmnop"},
        {"uid":"public-read" , "group": 0, "action":"read", "blk": 2, "len": 16},

        // group:2 write acls on a new key
        {"uid":"set-acls"   , "group": 2, "action":"trailer", "blk": 27, "key":"dfltA", "trailer": {"keyA": "key-a","keyB":"key-b","acls":["0xF0","0xF7","0x80","0x00"]}},

        // group:1 read/write key after acls was written
        {"uid":"keyB-write"  , "group": 1, "action":"write","blk": 26, "key":"key-b", "data": "abcdefghijklmnop"},
        {"uid":"keyA-read"   , "group": 1, "action":"read","blk": 26, "key":"key-a", "len": 16},

OIDC-pcsc C/APIs

Config APIs

High level API, hopefully match most application requirements.

 #include <pcsc-config.h>
 pcscConfigT *pcscParseConfig (json_object *configJ, const int verbosity);
 pcscCmdT *pcscCmdByUid (pcscConfigT *config, const char *cmdUid);
 int pcscExecOneCmd(pcscHandleT *handle, const pcscCmdT *cmd, u_int8_t *data);
  • pcscParseConfig: parse a config.json as defined in previous chapters.
  • pcscCmdByUid: find a command from its ‘uid’ and return command handle
  • pcscExecOneCmd: execute a command from its handle

Pcsc APIs

Connecting to pcsc reader

 #include <pcsc-glue.h>
 pcscHandleT *pcscConnect (const char uid, const char *readerName);
 int pcscDisconnect (pcscHandleT *handle);
 int pcscSetOpt (pcscHandleT *handle, pcscOptsE opt, ulong value);
 const char* pcscErrorMsg (pcscHandleT *handle);
  • pcscConnect: connect to a given reader: “readername” should be a subset of full reader name. When NULL first reader available is used. “uid” is a free human-readable string used to track debug messages.
  • pcscDisconnect: close and free reader connection.
  • pcscSetOpt: pcsc handle is opaque and options require a setter (
  • pcscErrorMsg: last command error message

Connecting to scard/token in synchronous or asynchronous mode

 #include <pcsc-glue.h>
 int pcscReaderCheck (pcscHandleT *handle, int ticks);
 ulong pcscMonitorReader (pcscHandleT *handle, pcscStatusCbT callback, void *ctx);
 int pcscMonitorWait (pcscHandleT *handle, pcscMonitorActionE action);
 void* pcscGetCtx (pcscHandleT *handle);

 typedef int (*pcscStatusCbT) (pcscHandleT *handle, ulong state);
 u_int64_t pcscGetCardUuid (pcscHandleT *handle);
  • pcscReaderCheck: in synchronous mode wait xx ticks for reader to be ready. Default ticks is 60s, and can be changed with timeout option.
  • pcscMonitorReader: start monitoring thread and register callback and context. Unfortunately pcsc-lite does not support asynchronous operation and application should register a dedicated thread to run pcsc operations in background.
  • pcscMonitorWait: wait for monitor thread to finish. Action=PCSC_MONITOR_WAIT|PCSC_MONITOR_CANCEL
  • pcscGetCtx: return handle context provided by pcscMonitorReader.
  • pcscGetCardUuid: check scard ATR and return UUID. If card is not supported this returns an error.
  • pcscStatusCbT monitoring callback signature register by pcscMonitorReader. This callback is called each time reader status changes. Typically when a scard is inserted/removed. As callback gets pcsc handle it can run any commands. Check main-pcsc.c for sample.

Reading/Writing to scard/token

Low level commands, most users may prefer to rather pcscExecOneCmd.

 #include <pcsc-glue.h>
 const pcscKeyT *pcscNewKey (const char *uid, u_int8_t *value, size_t len);
 int pcsWriteBlock (pcscHandleT *handle, const char *uid, u_int8_t secIdx, u_int8_t blkIdx, u_int8_t *dataBuf, ulong dataLen, const pcscKeyT *key);
 int pcscReadBlock (pcscHandleT *handle, const char *uid, u_int8_t secIdx, u_int8_t blkIdx, u_int8_t *data, ulong *dlen, const pcscKeyT *key);
 int pcsWriteTrailer (pcscHandleT *handle, const char *uid, u_int8_t secIdx, u_int8_t blkIdx, const pcscKeyT *key, const pcscTrailerT *trailer);
  • pcscNewKey: create a new key.
    • value: uint8 array, buffer should remain valid after api call.
    • len: buffer len, if len=0 then strlen(value) is used.
  • pcsWriteBlock: write bloc on scard/token.
    • uuid: is used only for debug purpose.
    • secIdx: sector index. Use with NFC-type2 but not with MiFare
    • blkIdx: block index. Note that with Mifare/classic sector/page is equivalent to blocIdx/4.
    • dataBut: the buffer to write
    • dataLen: the length to write. Depending on your scard/token length as contrains. With Mifare/classic you should write full block len=0x10,0x20,x30 and should not break page/sector boundary. Do not forget that you may write by block, but then authentication is by sector/page.
    • key: key handle to be used for operation authentication.
  • pcscReadBlock: read bloc on scard/token.
    • uuid: is used only for debug purposes.
    • secIdx: sector index. Use with NFC-type2 but not with MiFare
    • blkIdx: block index.
    • dataBut: buffer address where to place result
    • dataLen: input dataBuf size, return amount of effective data read.
    • key: key handle to be used for operation authentication.
  • pcsWriteTrailer: write a bloc on scard/token.
    • uuid: is used only for debug purposes.
    • secIdx: sector index. Use with NFC-type2 but not with MiFare
    • blkIdx: block index. Block index should match last bloc of a given page/sector.
    • key: key handle to be used for operation authentication.
    • trailer: trailer handle as created from pcscNewKey api.