Hardware-Bound Device Keys
Each device authenticates to Nexigon Hub with a certificate and its private key. By default, Nexigon Agent stores the private key in a file. Anyone who copies the device’s storage, for example by cloning its SD card, also copies the key.
This guide shows how to bind the key to the device’s hardware instead. A TPM, a PKCS#11 token, or device firmware then holds the key and signs with it on request, so the key is not stored in a file. A device booted from a copy of the storage then cannot authenticate as the original device. Software on the running device can still have the hardware sign, so an attacker who controls the device can still authenticate as it.
How It Works
Nexigon Agent uses a hardware-bound key through a signer script: an executable
that you provide and that performs operations with the key. The agent runs the
script to read the public key, to sign a new certificate when none exists at
ssl-cert, and to sign each TLS handshake with Nexigon Hub. Unlike with a key
file, the agent does not generate the key. You create it in the hardware once,
as described below.
The script adapts the hardware’s tools to the protocol that the agent expects. This guide provides scripts for TPM 2.0, for PKCS#11 tokens, and for the Raspberry Pi firmware. For other hardware, write a script that follows the signer script protocol.
Set Up the Key
Install the signer script in your device image at
/usr/libexec/nexigon/nexigon-signer and make it executable. Create the key
once on each device, for example during manufacturing or in a first-boot
service that runs before the agent.
TPM 2.0
A TPM 2.0 can create an ECDSA P-256 key that cannot be exported from it. A key that the TPM creates is transient at first: the TPM forgets it when it resets, for example on a reboot. To keep the key, you have the TPM store it in its non-volatile memory under a persistent handle, a 32-bit address that software then uses to refer to the key.
The
TCG handle registry
reserves parts of the handle space, for example for the TPM’s endorsement key.
For keys like the one below, which you create in the owner hierarchy, it marks
the range from 0x81008000 to 0x8100FFFF as available. The examples use
0x81008000. List the handles in use with tpm2_getcap handles-persistent and
pick a free one in that range. If tpm2_readpublic -c 0x81008000 succeeds, the
handle already holds a key, for example one created earlier.
The following commands use
tpm2-tools to create the key
and persist it:
tpm2_createprimary -C o -g sha256 -G ecc256 -c primary.ctx
tpm2_create -C primary.ctx -G ecc256:ecdsa-sha256 \
-a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign" \
-u key.pub -r key.priv
tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx
tpm2_evictcontrol -C o -c key.ctx 0x81008000
rm primary.ctx key.pub key.priv key.ctx
A TPM creates every key under a parent key. The first command creates a primary key as the parent in the owner hierarchy, the hierarchy that the device’s owner controls rather than the device’s firmware. The second command creates the signing key under it with these attributes:
fixedtpmandfixedparentprevent the key from ever being exported.sensitivedataoriginmakes the TPM generate the private key itself instead of accepting one from outside.sign, withoutdecrypt, makes the key a signing-only key.userwithauthwith the default empty password lets any process with access to the TPM use the key without a password.
The third and fourth commands load the signing key and persist it on its own.
The last command deletes the files, which are not needed afterward, nor is the
parent key. If your TPM has an owner password, pass it to tpm2_createprimary
and tpm2_evictcontrol with -P.
The following signer script uses the key at 0x81008000:
#!/bin/sh
set -eu
export TPM2TOOLS_TCTI="device:/dev/tpmrm0"
key=0x81008000
case "${1-}" in
public-key) exec tpm2_readpublic -Q -c "$key" -f der -o /dev/stdout ;;
sign) exec tpm2_sign -Q -c "$key" -g sha256 -f plain -o /dev/stdout /dev/stdin ;;
*) echo "unsupported operation: ${1-}" >&2; exit 1 ;;
esacTPM2TOOLS_TCTI makes the tools connect to the kernel’s TPM resource manager.
If your system runs the tpm2-abrmd daemon instead, remove that line. The
-f der and -f plain options make the tools write the DER-encoded public key
and signature that the agent expects. -Q keeps tpm2_readpublic from printing
a description of the key to stdout as well.
The script runs as the agent’s user. That user needs access to /dev/tpmrm0.
PKCS#11 Tokens
PKCS#11 is a standard interface to hardware that holds keys, such as smart
cards, USB security keys, and hardware security modules. PKCS#11 calls such
hardware a token. The token’s vendor provides a PKCS#11 module, a shared
library through which software uses the token. The pkcs11-tool command from
OpenSC works with any such module. Add
OpenSC and your token’s module to your device image.
A removable token, such as a USB security key, binds the key to the token rather than to the device. Anyone who has the token and a copy of the device’s storage can authenticate as the device.
The examples use the module /path/to/vendor-pkcs11.so, a token with the label
My Token, and a key with the ID 01. Replace them with your module, the label
that pkcs11-tool --module /path/to/vendor-pkcs11.so --list-slots shows for
your token, and an unused ID. An ID is a hex-encoded byte string, so 01 and
1 are different IDs. If the following command succeeds, the token already
holds a key with that ID:
pkcs11-tool --module /path/to/vendor-pkcs11.so --token-label "My Token" \
--read-object --type pubkey --id 01 --output-file /dev/null
Otherwise, create the key. The token must already have a user PIN, set by its
vendor or with pkcs11-tool --init-token and --init-pin. The following
command asks for the user PIN. In a manufacturing or first-boot script, pass
the PIN with --pin env:PKCS11_PIN instead, as the signer script below does:
pkcs11-tool --module /path/to/vendor-pkcs11.so --token-label "My Token" \
--login --keypairgen --key-type EC:prime256v1 --usage-sign \
--id 01 --label nexigon-device-key
--key-type EC:prime256v1 selects an ECDSA key on the curve P-256, which
OpenSSL calls prime256v1. --usage-sign allows the key to sign. The token
stores the private key and the public key as separate objects with the same ID,
through which the signer script finds them. The label is a name for people.
Check that the command’s output lists the private key with
Access: sensitive, always sensitive, never extractable, local, which shows
that the token generated the key and does not let it out.
Some tokens create keys only with their vendor’s tools. With those, create an ECDSA P-256 key as the vendor describes and use its ID in the signer script below.
Signing requires the user PIN. The following signer script reads it from
/etc/nexigon/agent/pkcs11-pin. Create that file with the PIN as its only
content and make it readable only by the users that run the signer script,
typically only the agent’s user. The script passes the PIN to pkcs11-tool
through an environment variable, because other processes can read
command-line arguments. The PIN file is on the device’s storage, so the PIN
prevents use of the token on its own but not together with a copy of the
storage.
With a wrong PIN, every signing attempt fails. The agent retries failed TLS handshakes, and a service manager typically restarts an agent that failed to start, so the wrong PIN reaches the token repeatedly. Many tokens lock the user PIN after a few wrong attempts. Check the script before you deploy it:
#!/bin/sh
set -eu
module=/path/to/vendor-pkcs11.so
token="My Token"
id=01
case "${1-}" in
public-key)
exec pkcs11-tool --module "$module" --token-label "$token" \
--read-object --type pubkey --id "$id" --output-file /dev/stdout
;;
sign)
PKCS11_PIN="$(cat /etc/nexigon/agent/pkcs11-pin)"
export PKCS11_PIN
exec pkcs11-tool --module "$module" --token-label "$token" \
--login --pin env:PKCS11_PIN --id "$id" \
--sign --mechanism ECDSA-SHA256 --signature-format openssl \
--input-file /dev/stdin --output-file /dev/stdout
;;
*) echo "unsupported operation: ${1-}" >&2; exit 1 ;;
esac--read-object --type pubkey writes the DER-encoded public key that the agent
expects. With --mechanism ECDSA-SHA256, the token hashes the message with
SHA-256 and signs the hash, and --signature-format openssl makes
pkcs11-tool write the signature as DER instead of concatenated r and s
values.
Some tokens support only the ECDSA mechanism, which signs a hash that the
caller computes. The following command lists the mechanisms of your token:
pkcs11-tool --module /path/to/vendor-pkcs11.so --token-label "My Token" \
--list-mechanisms
For a token without ECDSA-SHA256, replace the exec pkcs11-tool command of
the sign operation with a pipeline that hashes the message with OpenSSL
first. This variant also needs the openssl command in your device image:
openssl dgst -sha256 -binary | pkcs11-tool --module "$module" \
--token-label "$token" --login --pin env:PKCS11_PIN --id "$id" \
--sign --mechanism ECDSA --signature-format openssl \
--input-file /dev/stdin --output-file /dev/stdout
The script runs as the agent’s user. That user needs access to the token. What that requires depends on the module, for example access to the USB device of a security key.
Raspberry Pi Firmware
The firmware of Raspberry Pi models with a device-unique private key in
one-time programmable (OTP) memory, such as the Raspberry Pi 4 and 5, provides
a cryptography service for that ECDSA P-256 key. The
rpi-fw-crypto
tool from Raspberry Pi uses this service. Raspberry Pi OS ships the tool.
Key 1 is the device-unique OTP key. If rpi-fw-crypto pubkey --key-id 1
succeeds, the device already has the key. Otherwise, generate it. Generation
writes OTP memory and cannot be undone:
rpi-fw-crypto genkey --key-id 1 --alg ec
The following signer script uses key 1. The pubkey command writes the
DER-encoded public key, and the sign command hashes the message with SHA-256
and writes a DER-encoded signature, as the agent expects:
#!/bin/sh
set -eu
case "${1-}" in
public-key) exec rpi-fw-crypto pubkey --key-id 1 ;;
sign) exec rpi-fw-crypto sign --in /dev/stdin --key-id 1 --alg ec ;;
*) echo "unsupported operation: ${1-}" >&2; exit 1 ;;
esacThe script runs as the agent’s user. That user needs access to the firmware
through /dev/vcio_crypto or /dev/vcio.
Until the key is locked against reading, software on the device can read it
with rpi-fw-crypto privkey. Set lock_device_private_key=1 in config.txt,
so that the firmware locks the key at every boot while keeping signing
available. Without secure boot,
anyone who can modify the boot partition can remove this setting, because the
firmware authenticates config.txt only with secure boot enabled. Even with the
lock, software with root privileges can read the OTP memory directly. On a
Raspberry Pi, binding the key to the firmware therefore protects against copies
of the storage but not against root access to the running device.
Check the Signer Script
A broken signer script keeps the agent from starting. Before you deploy a script, sign a test message on the device and verify the signature with OpenSSL. Run the commands as the user that runs Nexigon Agent, so that missing access to the hardware shows up as well:
signer=/usr/libexec/nexigon/nexigon-signer
echo test > /tmp/message
"$signer" public-key > /tmp/public-key.der
"$signer" sign < /tmp/message > /tmp/signature.der
openssl dgst -sha256 -verify /tmp/public-key.der -keyform DER \
-signature /tmp/signature.der /tmp/message
OpenSSL prints Verified OK for a working script. If your own script uses a
P-384 key, use -sha384 instead of -sha256.
Configure Nexigon Agent
Add the [signer] section to the agent’s configuration in place of
ssl-key. All other settings stay the
same. For example, for a device that obtains its credentials through
provisioning:
fingerprint-script = "/usr/libexec/nexigon/nexigon-device-fingerprint"
[signer]
script = "/usr/libexec/nexigon/nexigon-signer"When the agent starts without a certificate at ssl-cert, it has the signer
sign a new one. Nexigon Hub treats the certificate like any other new
certificate, so it is accepted automatically or
pending depending on
the deployment token. See
Signer Configuration for all
options and for how the agent handles signer failures.
Switch an Enrolled Device
You can switch a device that already authenticates with a key file to a hardware-bound key. The device fingerprint does not depend on the key, so the device keeps its device ID and history in Nexigon Hub.
To switch:
- Set up the key and the signer script, and check the script.
- In the configuration, add
[signer], removessl-keyif it is set, and setssl-certto a new path, for example/etc/nexigon/agent/ssl/signer-cert.pem. The existing certificate contains the old public key, so the agent would fail to start with it. With the old files left in place, the agent can connect with the old configuration again, for example after a rollback. - Restart the agent, which creates the new certificate.
- Accept the new certificate in Nexigon Hub. Then revoke the old certificate
and delete the old certificate and key file, by default
/etc/nexigon/agent/ssl/cert.pemand/etc/nexigon/agent/ssl/key.pem. Until you revoke the old certificate, anyone with a copy of the old key file can authenticate as the device.
Nexigon Hub does not accept the device’s new certificate automatically, even with an Auto Accept deployment token, because the device already has a certificate. The device cannot connect until you accept it.