NFC fun with Python

10 minute read

I’ve recently been looking into using an NFC reader/writer pad at work to make configuring some devices we have much quicker and easier, as the standard process for these devices is an Android phone and a lot of manual data entry. Checking the device using a mobile is great, but configuring them this way is not scalable.


The hardware

I did some research and found we could get an NFC pad and write some Python code using nfcpy to automate much of the labour, reducing the time taken and decreasing the likelihood of human error. Some quick searches on eBay showed that the ACS ACR122U was a reasonably cost-effective option, so I ordered a couple of units for testing.1

The ACR122U hardware

When they arrived, I unpacked one and powered it up — so far, so good. Linux seems to have a basic driver for it that makes it beep when you bring a card near (at least that’s all I could get it to do). It’s also a fairly powerful reader — mine was able to communicate with test tags up to 50mm away, including through 25mm particleboard.

The next step is getting the reader working with Python.

Using the reader with nfcpy

At this point, I immediately encountered problems. The most recent version of nfcpy does not support Python 3. In particular, there a number of places where it treats str and bytes types the same, which is no longer the case. My options were to move back to Python 2, port the module to Python 3 or abandon the idea entirely — I decided to try and port the library to Python 3. I’ve spent a fair amount of time on it so it can work for my device; feel free to grab the code from my fork, but be aware it might not work for you out-of-the-box.

Part way through this process, I also discovered that nfcpy has this to say of the ACR122U:

It can not be overstated that the ACR122U is not a good choice for nfcpy.

So, yeah. The lesson for me here is do (better) research before I commit to a plan. I hope you’ll learn from my mistake.

Linux device access issues

While porting the code, I also hit upon the issue of the device being locked out, so Python couldn’t use it. Some research led me to running python -m nfc, which helpfully reports that it’s a kernel module causing the problems:

** found usb:072f:2200 at usb:001:008 but it's already used
-- scan sysfs entry at '/sys/bus/usb/devices/1-1:1.0/'
-- the device is used by the 'pn533_usb' kernel driver
-- this kernel driver belongs to the linux nfc subsystem
-- you can remove it to free the device for this session
   sudo modprobe -r pn533_usb
-- and blacklist the driver to prevent loading next time
   sudo sh -c 'echo blacklist pn533_usb >> /etc/modprobe.d/blacklist-nfc.conf'

As indicated, a temporary fix is running sudo modprobe -r pn533_usb; or for a permanent solution blacklist the module:

$ sudo sh -c 'echo blacklist pn533_usb >> /etc/modprobe.d/blacklist-nfc.conf'

Wireshark USB analysis

Part way through migrating the module to Python 3, I started encountering issues where the nfc-poll command on Linux would work fine, but Python was indicating that the NFC chip wasn’t responding with enough data. This seemed a little bit suspect to me, so I decided to crack out Wireshark to do some USB sniffing and find out where the differences were.

Linux permissions

Wireshark can capture data from USB devices using the various usbmon adapters. Unfortunately the permissions on these are by default pretty locked down on Linux, so some searching led me to this helpful SuperUser post. The temporary hack to fix this involves granting everyone read and write access to the file: sudo chmod o=rw /dev/usbmon1.

Tip: If you’re missing /dev/usbmon* files, chances are you haven’t loaded the kernel module — run sudo modprobe usbmon. Following the instructions below will take care of this for you automatically from this point onwards.

A more robust solution is to create a usbmon group and tell the udev daemon to grant appropriate access. These instructions add you to the newly-created group; you’ll need to logout for the group changes to take effect (or you can try your luck with these instructions).

$ sudo addgroup usbmon
$ sudo usermod -a -G usbmon $USER
$ echo 'SUBSYSTEM=="usbmon", MODE="640", GROUP="usbmon"' | sudo tee /etc/udev/rules.d/99-usbmon.rules
$ echo 'SUBSYSTEM=="usb", RUN+="/sbin/modprobe usbmon"' | sudo tee /etc/udev/rules.d/99-usbmon.rules
$ sudo udevadm trigger

Opening a USB capture

You might have a number of usbmon devices that Wireshark can attach to. To work out which one you need, run lsusb and look for your device. The Bus number (001) means that we will use usbmon1.

Bus 001 Device 020: ID 072f:2200 Advanced Card Systems, Ltd ACR122U

Depending on what other devices are on the bus you’re capturing on, you could be flooded by traffic pretty quickly. You can reduce what is shown using a display filter of usbccid (to jump straight to the display filter box in Wireshark, press Ctrl+/).

Dissectors and filtering

Wireshark should now be capturing USB traffic and looking for CCID packets to display. Chances are, there might not be anything there right now. We can get some messages to appear in Wireshark by running the nfc-poll command in a terminal; it will look for your NFC card reader and interrogate it. To get some informative output, put an NFC card on the top of the reader before you run the command. Here’s output from when I ran it:

nfc-poll uses libnfc 1.7.1
NFC reader: ACS / ACR122U PICC Interface opened
NFC device will poll during 30000 ms (20 pollings of 300 ms for 5 modulations)
ISO/IEC 14443A (106 kbps) target:
    ATQA (SENS_RES): 00  04  
       UID (NFCID1): fc  09  2e  06  
      SAK (SEL_RES): 08  
nfc_initiator_target_is_present: Target Released
Waiting for card removing...done.

Once Wireshark has captured some USB traffic, you can take a look through it to try and spot any issues. Unfortunately, it doesn’t tell us much about the content of the messages by default. We can tell Wireshark that we know the messages are from an ACR122U device by changing the decoding.

If you’ve got rows with USBCCID in the Protocol column, just right click one of them and select ‘Decode As…’ in the context menu. A dialog will open with a table with one row. Change the (none) in the ‘Current’ column to ACR 122, then click OK:

Selecting the ACR 122 dissector in Wireshark

On the other hand, if you don’t have any USBCCID rows we can still get the same result. Just right click one of the USB rows and pick ‘Decode As…’ in the context menu. In the USB product row, change the (none) in the ‘Current’ column to USBCCID. Then click the plus button and pick USB CCID Payload in the dropdown in the first column. Then you’ll be able to select ACR 122 like above:

Selecting the USB CCID and ACR 122 dissectors in Wireshark

Wireshark will now be able to work out exactly what commands are being sent to the NFC reader and chipset — you can check the ‘Protocol’ column in the packet list. I’d recommend taking a couple of minutes to click through the packet list and get an idea of what’s going on.

An assortment of ACR 122 packets recorded in Wireshark

Fixing the bug

After capturing known-good traffic from nfc-poll and comparing it with the output from Python, I could immediately see where the messages diverged:

A packet being examined in Wireshark A second packet being examined in Wireshark
The bad packet on the left, with the working packet on the right

The Python packet wasn’t being constructed properly. Some clever hunches with the debugger and breakpoints revealed the source of the error: my attempts at migrating the Python 2 code to Python 3. The original source includes this line:

# brty is type int, initiator_data is type bytearray
data = chr(1) + chr(brty) + initiator_data

Which is no longer valid Python (you can’t concatenate str and bytearray objects). I’d migrated it to look like this:

data = bytes(1) + bytes(brty) + initiator_data

Once I realised there was a problem here, I decided to actually read the documentation, which told me exactly what I should have known all along:

In addition to the literal forms, bytes objects can be created in a number of other ways:

  • A zero-filled bytes object of a specified length: bytes(10)
  • From an iterable of integers: bytes(range(20))

So where I was trying to create a bytes object of length 1 containing the value brty, I was instead creating a bytes object brty elements long with each element set to zero (the same bug is present with bytes(1)). The easy fix is to make the arguments into iterables: lists or sets. The correct code now looks like this:

data = bytes([1, brty]) + initiator_data

Next steps

Success, right? Not really. The Python 3 port is still incomplete at time of writing and there’s a number of fixes to be made, but progress is definitely underway and it looks like this device is capable of doing what I need it to (but not much more). Once I have the code working for my intended application, I might buy a more capable reader for some further experimentation.

Next week I hope to finish the project for work and provide some code for you that demonstrates how to read and write smartcards.

  1. Image sourced from Advanced Card Systems