Wednesday, April 22, 2020

SCARD_ATTR_CHANNEL_ID and USB devices

Sometimes you need a way to identify which USB device is the smart reader reported by PC/SC.
That was the request of Stephan Guilloux in issue 68 and Pull Request 69.

SCARD_ATTR_CHANNEL_ID

The WinSCard API already provides something like that with the SCARD_ATTR_CHANNEL_ID attribute.

The Microsoft documentation says:
SCARD_ATTR_CHANNEL_ID
DWORD encoded as 0xDDDDCCCC, where DDDD = data channel type and CCCC = channel number:
  • The following encodings are defined for DDDD:
  • 0x01 serial I/O; CCCC is a port number.
  • 0x02 parallel I/O; CCCC is a port number.
  • 0x04 PS/2 keyboard port; CCCC is zero.
  • 0x08 SCSI; CCCC is SCSI ID number.
  • 0x10 IDE; CCCC is device number.
  • 0x20 USB; CCCC is device number.
  • 0xFy vendor-defined interface with y in the range zero through 15; CCCC is vendor defined.
So for a USB device the DDDD value is 0x0020 and we have 2 bytes for the CCCC value.
We decided to use the 2 bytes to encode the bus number in the most significant byte (MSB) and the device address in the least significant byte (LSB).

Implementation

I added the support of this new attribute in the CCID driver version 1.4.32.

I also added support in the PCSC Unitary Test SCardGetAttrib.py to check the code is working correctly.

Example (Low Level Python API)


#! /usr/bin/env python

from smartcard.scard import *
from smartcard.pcsc.PCSCExceptions import *
from struct import unpack

hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
if hresult != SCARD_S_SUCCESS:
    raise EstablishContextException(hresult)

hresult, readers = SCardListReaders(hcontext, [])
if hresult != SCARD_S_SUCCESS:
    raise ListReadersException(hresult)

for reader in readers:
    hresult, hcard, dwActiveProtocol = SCardConnect(hcontext, reader,
        SCARD_SHARE_DIRECT, SCARD_PROTOCOL_ANY)
    if hresult != SCARD_S_SUCCESS:
        raise BaseSCardException(hresult)

    print("reader:", reader)
    hresult, attrib = SCardGetAttrib(hcard, SCARD_ATTR_CHANNEL_ID)
    if hresult != SCARD_S_SUCCESS:
        print(SCardGetErrorMessage(hresult))
    else:
        print(attrib)
        # get the DWORD value
        DDDDCCCC = unpack("i", bytearray(attrib))[0]
        DDDD = DDDDCCCC >> 16
        if DDDD == 0x0020:
            bus = (DDDDCCCC & 0xFF00) >> 8
            addr = DDDDCCCC & 0xFF
            print(" USB: bus: {}, addr: {}".format(bus, addr))

    hresult = SCardDisconnect(hcard, SCARD_LEAVE_CARD)
    if hresult != SCARD_S_SUCCESS:
        raise BaseSCardException(hresult)

hresult = SCardReleaseContext(hcontext)
if hresult != SCARD_S_SUCCESS:
    raise ReleaseContextException(hresult)

The important part of the code is the line:
        DDDDCCCC = unpack("i", bytearray(attrib))[0]
As documented by Microsoft, the SCARD_ATTR_CHANNEL_ID attribute does not return a buffer of bytes but a DWORD.
The difference is that the order of the 4 bytes of the DWORD is important and depends on the CPU architecture. The code must take into account the endianess of the CPU. Since the Python program does not know if the CPU is little or big endian we must use unpack() to let the CPU decode the 4 bytes as an integer using its internal endianess.

If you have a better solution please share it.

Output

$ python3 SCardGetAttrib.py 
reader: Gemalto PC Twin Reader 00 00
[8, 1, 32, 0]
 USB: bus: 1, addr: 8
reader: Cherry KC 1000 SC Z [KC 1000 SC Z] 01 00
[5, 1, 32, 0]
 USB: bus: 1, addr: 5

Example (high level Python API)

We can also use the higher level Python API.
The code is much shorter but may be more complex to understand and translate into a PC/SC wrapper in another language.

#! /usr/bin/env python

from smartcard.System import readers
from smartcard.scard import (SCARD_SHARE_DIRECT, SCARD_ATTR_CHANNEL_ID)
from struct import unpack

for reader in readers():
    print("reader:", reader)
    card_connection = reader.createConnection()
    card_connection.connect(mode=SCARD_SHARE_DIRECT)

    attrib = card_connection.getAttrib(SCARD_ATTR_CHANNEL_ID)
    print(attrib)
    DDDDCCCC = unpack("i", bytearray(attrib))[0]
    DDDD = DDDDCCCC >> 16
    if DDDD == 0x0020:
        bus = (DDDDCCCC & 0xFF00) >> 8
        addr = DDDDCCCC & 0xFF
        print(" USB: bus: {}, addr: {}".format(bus, addr))

Output

Of course the output is the same.
$ python3 getAttrib.py 
reader: Gemalto PC Twin Reader 00 00
[8, 1, 32, 0]
 USB: bus: 1, addr: 8
reader: Cherry KC 1000 SC Z [KC 1000 SC Z] 01 00
[5, 1, 32, 0]
 USB: bus: 1, addr: 5

Use case

The use case of this new feature is be able to make a bijective relation between a PC/SC smart card reader and a USB device connected to the host.

The information returned by SCARD_ATTR_CHANNEL_ID is also returned by the lsusb command (on GNU/Linux).
$ lsusb 
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 006: ID 1bcf:0005 Sunplus Innovation Technology Inc. Optical Mouse
Bus 001 Device 005: ID 046a:00a4 Cherry GmbH 
Bus 001 Device 008: ID 08e6:3437 Gemalto (was Gemplus) GemPC Twin SmartCard Reader
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

You can see that the PC/SC reader "Gemalto PC Twin Reader 00 00" is at "USB: bus: 1, addr: 8" for PC/SC and at "Bus 001 Device 008:" for lsusb.

You can also ask lsusb to list only this specific device using:
$ lsusb -s 1:8
Bus 001 Device 008: ID 08e6:3437 Gemalto (was Gemplus) GemPC Twin SmartCard Reader

You may have noticed that the device "Bus 001 Device 005" is the PC/SC reader "Cherry KC 1000 SC Z [KC 1000 SC Z] 01 00" and it is also my keyboard. It is a Cherry KC 1000 SC Z.

More details

You can also get more information about the USB reader at the PC/SC level using CM_IOCTL_GET_FEATURE_REQUEST to get PCSCv2_PART10_PROPERTY_wIdVendor and PCSCv2_PART10_PROPERTY_wIdProduct values.
See "Identifying a reader model (part 2)".

Conclusion

Not everybody will need to use this new feature.
But it was easy to implement and it has no side effect. So why not?