Automating nRF91 TLS credential management via serial port AT commands

I have spent a bit of time recently experimenting with different TLS credential configurations using nRF91-DKs. Eventually I got to the point where I wanted to do things like swap out the certificates on a DK for different types or connect several DKs to my MQTT server using the same firmware but different PSKs. Of course credentials can be managed from firmware but rebuilding the same application every time I wanted to change a credential eventually became tedious so I started looking at managing credentials using AT commands. Long story short, I wrote some Python to make my life easier and here it is in case it's useful for you as well.

Requirements

The module was written using Python 3.5 on Linux and tested using 3.7 on Windows. It's pretty simple so any Python 3x should work but keep in mind that if you are using Python <= 3.5 and you want to use the PSK script below you will want to use pip to install python2-secrets instead of secrets. You'll definitely need Nordic's excellent pynrfjprog module along with pyserial for handling the serial port.

How it works

The Python module is called "at" and is hosted on github. Although it started as a simple AT command parser, as I found more ways to use it I kept adding to it until it eventually became a module plus a command line interface.

The at_client project in the nRF Connect SDK (NCS) exposes an AT command interface over the nRF91-DK's USB CDC interface. As long as the project is compiled with CONFIG_AT_CMD_RESPONSE_MAX_LEN set to something reasonable (e.g. 4096 bytes) you have access to all of the DK's credential management commands by opening a serial port on your PC. I compiled this project from NCS v1.1.0 and included the hex file with the at module for convenience.

Having a prebuilt at_client hex file in one hand is a good start. In the other hand you probably have an NCS project in which you've been working. In my case that has been the mqtt_simple project, modified for my own purposes. Ideally, I'd like to automatically program the at_client hex file to a DK, do some work via the AT command interface, and then finish by replacing the at_client with my firmware.

Example usage

The command line interface help looks like this:

 

  > python cmng.py --help
  usage: cmng [-h] [--sec_tag SECURITY_TAG] [--cred_type CREDENTIAL_TYPE]
              [--passwd PRIVATE_KEY_PASSWD] [-o PATH_TO_OUT_FILE]
              [--content CONTENT | --content_path PATH_TO_CONTENT]
              [-s JLINK_SERIAL_NUMBER] [-x] [--program_app PATH_TO_APP_HEX_FILE]
              [--power_off]
              {list,read,write,delete} SERIAL_PORT_DEVICE
  
  A command line interface for managing nRF91 credentials.
  
  positional arguments:
    {list,read,write,delete}
                          operation
    SERIAL_PORT_DEVICE    serial port device to use for AT commands
  
  optional arguments:
    -h, --help            show this help message and exit
    --sec_tag SECURITY_TAG
                          specify sec_tag [0, 2147483647]
    --cred_type CREDENTIAL_TYPE
                          specify cred_type [0, 5]
    --passwd PRIVATE_KEY_PASSWD
                          specify private key password
    -o PATH_TO_OUT_FILE, --out_file PATH_TO_OUT_FILE
                          write output from read operation to file instead of
                          stdout.
    --content CONTENT     specify content (i.e. key material)
    --content_path PATH_TO_CONTENT
                          read content (i.e. key material) from file
    -s JLINK_SERIAL_NUMBER, --serial_number JLINK_SERIAL_NUMBER
                          serial number of J-Link
    -x, --program_hex     begin by writing prebuilt 'at_client' hex file to
                          device
    --program_app PATH_TO_APP_HEX_FILE
                          program specified hex file to device before finishing
    --power_off           put modem in CFUN_MODE_POWER_OFF if necessary
  
  WARNING: nrf_cloud relies on credentials with sec_tag 16842753.

You'll need to know which serial port the J-Link driver enumerated for the AT command interface. On my Linux machine this tends to be "/dev/ttyACM0". On Windows you can use the Device Manager to narrow it down to three possibilities and brute force it! Let's agree to describe this serial port as SERIAL_PORT for the remainder of this post (a cross-platform way of discovering this from Python is on my TODO list).

Replacing a CA certificate is a good use case because it can be both read and written. The at_client firmware may not be on the DK already so we'll include "-x" to write it:

> git clone https://github.com/inductivekickback/at.git
> cd at
> python cmng.py list SERIAL_PORT -x
[16842753, 0, '0000000000000000000000000000000000000000000000000000000000000000']

Here you can see that my DK currently has exactly one credential and this credential has a sec_tag of 16842753, cred_type of 0 (at.CRED_TYPE_ROOT_CA), and some placeholder content that isn't representative of the actual content. Let's read it to a text file in the current directory called "example_ca_out_file.crt":

> python cmng.py read SERIAL_PORT --sec_tag 16842753 --cred_type 0 -o example_ca_out_file.crt

If you take a peek at "example_ca_out_file.crt" you'll see that it starts with the standard "-----BEGIN CERTIFICATE-----" stuff. Now that it's backed up let's delete the certificate from the DK:

> python cmng.py delete SERIAL_PORT --sec_tag 1234 --cred_type 0
> python cmng.py list SERIAL_PORT
[]

The square brackets are Python notation for an empty list. Certificates are a little unwieldy so I prefer to not copy-and-paste them. We can restore this certificate using its file path and finish by replacing the existing application hex file:

> python cmng.py write SERIAL_PORT --sec_tag 16842753 --cred_type 0 --content_path example_ca_out_file.crt
> python cmng.py list SERIAL_PORT --program_app my_mqtt_simple.hex
[16842753, 0, '0000000000000000000000000000000000000000000000000000000000000000']

Next let's automate some work. My Mosquitto MQTT server makes it easy to provision devices as long as they have a pre-shared key (PSK) and a unique identity. The Mosquitto PSK file has a specific format. Save the following script as "psk_gen.py" in the "at" directory that was created when you cloned the repository:

  """Simple script for generating PSK credentials."""
  import sys
  import os
  import secrets
  
  import at
  
  
  PRESHARED_KEY_LEN_BYTES = 32
  PSK_IDENT_PREFIX = "nrf-"
  PSK_CONFIG_FILE = "psk_file.txt"
  PSK_FILE_FORMAT = "{}:{}{}"
  SEC_TAG = 1234
  
  
  def main(com_port):
      """Create a PSK/PSK identity pair, program it to an nRF91-DK, and save it for later."""
      # Connect to the nRF91-DK and read IMEI.
      soc = at.SoC(com_port)
      imei = soc.get_imei()
  
      # Create an identity based on IMEI.
      psk_ident = "{}{}".format(PSK_IDENT_PREFIX, imei)
  
      # Generate a reasonable preshared key.
      psk = secrets.token_hex(PRESHARED_KEY_LEN_BYTES)
  
      # Write to the nRF91-DK.
      soc.write_credential(SEC_TAG, at.CRED_TYPE_PSK_IDENTITY, psk_ident)
      soc.write_credential(SEC_TAG, at.CRED_TYPE_PSK, psk)
  
      # Record in Mosquitto-stype PSK config file for later.
      with open(PSK_CONFIG_FILE, 'a') as out_file:
          out_file.write(PSK_FILE_FORMAT.format(psk_ident, psk, os.linesep))
  
  
  if __name__ == "__main__":
      main(sys.argv[1])

Every time you run this script it will add a line to a file called "psk_file.txt" that consists of a unique PSK identity that is based on the IMEI of the DK concatenated with a randomly generated PSK:

> python psk_gen.py SERIAL_PORT
> type PSK_FILE.TXT nrf-XXXXXXXXXXXXXXX:YYYYYYYYYYYYYYYYYYYY 

The contents of the "psk_file.txt" can then be copied to the server.

Next steps

The at module is a work in progress. Issues and pull requests are welcome on github!

Parents
  • This is great! I would have had to build something similar myself eventually, so it's great that you shared this. Regarding the method of finding the correct serial port. From PySerial you can use the serial.tools.list_ports method to find all the ports. It identifies them by Vendor ID, VID, PID, Serial Number, and a few other useful nuggets of info. You can then (usually) find the correct one to use based on this info.

Comment
  • This is great! I would have had to build something similar myself eventually, so it's great that you shared this. Regarding the method of finding the correct serial port. From PySerial you can use the serial.tools.list_ports method to find all the ports. It identifies them by Vendor ID, VID, PID, Serial Number, and a few other useful nuggets of info. You can then (usually) find the correct one to use based on this info.

Children