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.
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
nhCLI 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=rpi42. Add dependencies
In mix.exs:
{:nerves_hub_link, "~> 2.7"},
{:nerves_key, "~> 1.2"}mix deps.getWith 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: trueStop the client connecting during dev and test:
# config/dev.exs and config/test.exs
config :nerves_hub_link, connect: falseMake 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 burnInsert the SD card, power on, and confirm you can reach the device:
ssh nerves.local5. 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_1This 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 IExcert_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
In the web UI, create a product (using your project name,
my_project, is convenient).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.certJITP 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-keyThis 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-uuidCreate the deployment group and activate it:
nh deployment create --name "My deployment" \
--firmware "UUID" --version "" --tag "main"
nh deployment update "My deployment" state on12. 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.