Production setup with NervesKey

Set up certificate-based (mTLS) device auth with NervesKey hardware: choose a method, provision the chip, and ship signed firmware.

Written By Josh Kalderimis

Last updated About 2 months ago

This tutorial sets up a production-grade device using NervesKey hardware (a Microchip ATECC608) for certificate-based authentication (mTLS) against NervesHub.

It's more involved than the shared-secret Quickstart, but not a huge undertaking. The same steps apply to other HSMs that expose an OpenSSL engine, and largely to the file-based LocalCertKey method if your hardware has no secure element.

Prototyping? An ATECC608 breakout is available from Adafruit and others.

Choose an authentication method

NervesHub supports two certificate-based approaches. Pick one before you start.

Device Certificate (recommended)

JITP (Just-In-Time Provisioning)

How

You upload each device's certificate to NervesHub ahead of time. An explicit allow-list.

Devices with a certificate signed by a trusted Signer CA are auto-registered on first connect.

Signer CA upload

Strongly recommended

Required

Control

Highest β€” nothing connects unless you added it

Lower β€” anything with a valid signed cert can join

Risk if Signer key leaks

Contained (device must already exist)

Higher (new devices self-provision)

Use when

Normal production manufacturing

Complex key infrastructure where pre-registration isn't possible

The rest of this guide notes where the two methods differ.

Prerequisites

  • A working Nerves setup.

  • A target device with an ATECC608 on I2C, and SSH access to it.

  • The nh CLI installed and authenticated. See the CLI reference:

    nh config set uri "https://manage.nervescloud.com/"
    
    nh user auth

This guide uses NervesCloud and a Raspberry Pi 4. Substitute your own host and target.

1. Create the project

mix nerves.new my_project

cd my_project

export MIX_TARGET=rpi4

2. Add dependencies

In mix.exs:

{:nerves_hub_link, "~> 2.7"},
{:nerves_key, "~> 1.2"}
mix deps.get

With nerves_key present, NervesHubLink reads the certificate and key from the chip automatically (default: :primary pair, I2C bus 1).

3. Configure the device

In config/target.exs, point at the device endpoint (for NervesCloud, devices.nervescloud.com):

config :nerves_hub_link,
  host: "devices.nervescloud.com",
  remote_iex: true

Stop the client connecting during dev and test:

# config/dev.exs and config/test.exs
config :nerves_hub_link, connect: false

Make sure the device has network access (Ethernet works out of the box; otherwise configure Wi-Fi via vintage_net).

4. Build and burn

mix firmware
mix burn

Insert the SD card, power on, and confirm you can reach the device:

ssh nerves.local

5. Provision the NervesKey

Handle the Signer key with care. The Signer certificate's private key is what makes devices provably yours. Store it in real secret management, limit access, and keep backups. Devices you provision are permanently linked to it.

Create a Signer certificate and key (matches the ATECC compressed-certificate format):

mix nerves_key.signer create my_board_prod_signer_1

This produces my_board_prod_signer_1.cert (public, low-sensitivity) and my_board_prod_signer_1.key (secret).

Upload both to the device, then provision the chip from an IEx session on the device:

sftp nerves.local        # put the .cert and .key into /tmp
ssh nerves.local         # open IEx
cert_name = "my_board_prod_signer_1"
manufacturer_sn = "MB000001"
board_name = "my_board"

signer_cert = 
  File.read!("/tmp/#{cert_name}.cert") 
  |> X509.Certificate.from_pem!()

signer_key = 
  File.read!("/tmp/#{cert_name}.key")
  |> X509.PrivateKey.from_pem!()

{:ok, i2c} = ATECC508A.Transport.I2C.init([])

info = %NervesKey.ProvisioningInfo{
  manufacturer_sn: manufacturer_sn, 
  board_name: board_name
}

# Double-check the values above β€” provisioning is permanent
NervesKey.provision(i2c, info, signer_cert, signer_key)

Provisioning burns permanent data into the chip. Practice on spare ATECC parts first β€” a mistake can ruin the chip.

Read back the device certificate and save it locally as MB000001.cert:

{:ok, i2c} = ATECC508A.Transport.I2C.init([])

true = NervesKey.provisioned?(i2c)

NervesKey.device_cert(i2c)
|> X509.Certificate.to_pem()
|> IO.puts()

6. Create the product and upload the Signer CA

  1. In the web UI, create a product (using your project name, my_project, is convenient).

  2. Register the Signer certificate as a CA on the organization:

    nh cacert register my_board_prod_signer_1.cert

JITP only: in the CA settings, enable Just-In-Time Provisioning and select your product from the dropdown.

7. Register the device

Device Certificate method β€” create the device and attach its certificate:

nh device create  # enter MB000001 when prompted

nh device cert import MB000001 MB000001.cert

JITP method β€” skip this step. The device is created automatically on first connect.

For a manufactured batch, script this from a CSV of provisioned devices.

8. Confirm the device connected

Give it a moment; the device should appear in your product's Devices list. If not, SSH in and check RingLogger.next, or force a reconnect with NervesHubLink.reconnect().

9. Create firmware signing keys

Firmware must be signed. Create a key (you'll be prompted for a password):

nh key create my-key

This uploads the public key to your organization and stores the password-protected private key locally. Guard this key β€” it's what authorizes new firmware on your devices. Store it alongside the Signer key in secret management.

10. Build, sign, and publish firmware

Bump the version in mix.exs, then:

export FW_PATH="./_build/${MIX_TARGET}_dev/nerves/images/my_project.fw"

mix firmware

nh firmware sign "$FW_PATH" --key my-key

nh firmware publish "$FW_PATH"

You can also sign/upload from the web UI under Firmware.

11. Create a deployment

Get the firmware UUID with fwup if you need it:

fwup -i "$FW_PATH" -m --metadata-key meta-uuid

Create the deployment group and activate it:

nh deployment create --name "My deployment" \
  --firmware "UUID" --version "" --tag "main"

nh deployment update "My deployment" state on

12. Confirm the update

The device is added to the deployment automatically (if it wasn't already in one) and is offered the new firmware. To avoid waiting, hit Reconnect on the device page.

Sending more updates

Later releases are a short loop:

mix firmware

nh firmware sign "$FW_PATH" --key my-key

nh firmware publish "$FW_PATH" --deploy "My deployment"

Wrap it in a script or CI job when you're ready.

Related