SCARD_CTL_CODE(3601): USB path of the reader

In version 1.7.1 of my CCID driver I added a new service with the Control Code SCARD_CTL_CODE(3601).

Problem

Some people use two (or more) readers. And each reader plays a specific role in their application. It is therefore important to identify each one.

While it is often possible to use the PC/SC reader name (see What is in a PC/SC reader name?), if you have two identical readers with no serial numbers, it becomes impossible to distinguish between them.

Solution

A USB device is connected to a USB bus with a specific topology. The lsusb --tree command, for example, can display the USB bus topology. The output looks like this:

$ lsusb -t
/:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/16p, 480M
    |__ Port 003: Dev 039, If 0, Class=Chip/SmartCard, Driver=usbfs, 12M

The smart card reader is connected to USB Port 003 on Bus 001. These numbers are fixed and linked to the computer's physical USB port. Disconnecting and reconnecting the reader to the same USB port will result in the same topology.

On GNU/Linux, the device number will change. For example, if I disconnect and reconnect the device, the reader's device number (Dev) will change from 039 to 040.

SCARD_CTL_CODE(3601)

This code is specific to my CCID driver. It comes after SCARD_CTL_CODE(3600) that is also specific/proprietary and used for MEP (see CCID driver and Multiple Enabled Profiles (MEP)).

Ideally, we would use a code defined by the PC/SC Workgroup but this group is essentially defunct and Microsoft is no longer a member. Microsoft stopped following the PC/SC Workgroup Specification a long time ago. Therefore, even if a name is defined by the PC/SC Workgroup, it will not be available on Windows.

API documentation

Use SCardControl() to send the control code SCARD_CTL_CODE(3601) to the reader. This code will be intercepted by the CCID driver. On return, you get a NUL-terminated string in the format <bus>-<port[.port[.port]]>:<config>.<interface> (e.g., 1-1.3.2:1.0) as found in /sys/bus/usb/devices/.

Source code

This example code is also included in the CCID driver's source code, in the examples/get_usb_path.py file.

The code also displays the SCARD_ATTR_CHANNEL_ID of the reader (see SCARD_ATTR_CHANNEL_ID and USB devices). This is an official PC/SC feature. It can be used when topology information is obtained from another source.

#! /usr/bin/env python3

"""
get_usb_path.py: get USB path of readers
Copyright (C) 2026  Ludovic Rousseau
"""

#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License along
#   with this program; if not, see <http://www.gnu.org/licenses/>.

import struct
import smartcard
from smartcard.pcsc.PCSCPart10 import SCARD_CTL_CODE
from smartcard.scard import (
    SCARD_E_NOT_TRANSACTED,
    SCARD_E_INVALID_PARAMETER,
    SCARD_ATTR_CHANNEL_ID,
    SCARD_SHARE_DIRECT,
    SCARD_LEAVE_CARD,
)
from smartcard.util import toASCIIString


def get_usb_path(reader):
    """
    Display USB topology
    """
    print("Using:", reader)
    card_connection = reader.createConnection()
    card_connection.connect(mode=SCARD_SHARE_DIRECT, disposition=SCARD_LEAVE_CARD)

    # special control code
    ioctl_get_usb_path = SCARD_CTL_CODE(3601)
    try:
        res = card_connection.control(ioctl_get_usb_path)
    except smartcard.Exceptions.SmartcardException as ex:
        # SCARD_E_NOT_TRANSACTED returned by pcsc-lite
        # SCARD_E_INVALID_PARAMETER retruned by macOS
        if ex.hresult in [SCARD_E_NOT_TRANSACTED, SCARD_E_INVALID_PARAMETER]:
            print("Your driver does not (yet) support SCARD_CTL_CODE(3601)")
            return
        raise
    print("USB path:", toASCIIString(res))

    # get Channel ID
    attrib = card_connection.getAttrib(SCARD_ATTR_CHANNEL_ID)
    ddddcccc = struct.unpack("i", bytearray(attrib))[0]
    dddd = ddddcccc >> 16
    if dddd == 0x0020:
        bus = (ddddcccc & 0xFF00) >> 8
        addr = ddddcccc & 0xFF
        print(f" USB: bus: {bus}, addr: {addr}")
    print()


def main():
    """
    main
    """
    # for all the available readers
    for reader in smartcard.System.readers():
        get_usb_path(reader)


if __name__ == "__main__":
    main()

Output

The reader is connected directly to the computer

$ ./get_usb_path.py
Using: Gemalto PC Twin Reader (70D7E2EE) 01 00
USB path: 1-3:1.0
 USB: bus: 1, addr: 39

$ lsusb -t
/:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/16p, 480M
    |__ Port 003: Dev 039, If 0, Class=Chip/SmartCard, Driver=usbfs, 12M

The USB path is 1-3:1.0.

This corresponds to bus 1, port 3, configuration 1 and interface 0. You can ignore the configuration and interface details for non-composite USB devices.

Connected to another USB port of the computer

$ ./get_usb_path.py
Using: Gemalto PC Twin Reader (70D7E2EE) 01 00
USB path: 1-6:1.0
 USB: bus: 1, addr: 40

$ lsusb -t
/:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/16p, 480M
    |__ Port 006: Dev 040, If 0, Class=Chip/SmartCard, Driver=usbfs, 12M

Note:

  • the Port changed from 003 to 006

  • the Dev changed from 039 to 040

Connected to a hub

$ ./get_usb_path.py
Using: Gemalto PC Twin Reader (70D7E2EE) 01 00
USB path: 1-3.1:1.0
 USB: bus: 1, addr: 42

$ lsusb -t
/:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/16p, 480M
    |__ Port 003: Dev 041, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 001: Dev 042, If 0, Class=Chip/SmartCard, Driver=usbfs, 12M

Note:

  • Port 003 is now used by the USB hub (Class=Hub)

  • the reader uses Port 001 of the hub

  • the USB path has one extra level 1-3.1:1.0

Connected to another port of the hub

$ ./get_usb_path.py
Using: Gemalto PC Twin Reader (70D7E2EE) 01 00
USB path: 1-3.4:1.0
 USB: bus: 1, addr: 43

$ lsusb -t
/:  Bus 001.Port 001: Dev 001, Class=root_hub, Driver=xhci_hcd/16p, 480M
    |__ Port 003: Dev 041, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 004: Dev 043, If 0, Class=Chip/SmartCard, Driver=usbfs, 12M

Note:

  • the reader is connected to Port 004 of the hub

  • the hub is still connected to Port 003

Conclusion

Not everyone will need or use this feature. However, it can be very important if you have a large number of readers.

Many Thanks to Diego de los Santos for the idea and implementation.