Awesome
NervesKey
The NervesKey is a configured ATECC508A or ATECC608A Crypto Authentication chip that's used for authenticating devices with NervesHub and other cloud services. At a high level, it is a simple hardware security module (HSM) that protects one private key by requiring all operations on that key to occur inside chip. The project provides access to the chip from Elixir and makes configuration decisions to make working with the device easier. It has the following features:
- Provision blank ATECC508A/608A devices - this includes private key generation
- Storage for serial number and one-time calibration data (useful if primary storage is on a removable MicroSD card)
- Support for Microchip's compressed X.509 certificate format to work with Microchip's C libraries
- Support for signing device certificates so that devices can be included in a PKI
- Support for storing a small amount of run-time configuration in unused data EEPROM slots
- Support auxillary device/signer certificate storage to support pre-production experimentation without needing to lock down certificates
It cannot be stressed enough that the NervesKey library locks down the ATECC508A/608A during the provisioning process. This is a feature and is required for normal operation, but if you're getting started, make sure that you have a few extra parts just in case you make a mistake.
See NervesHub documentation for end-user NervesKey and NervesHub documentation.
See hw/hw.md for hardware information or go to Tindie/NervesKey for a prebuilt-one. There are a few options for purchase as breakout boards with STEMMA QT / Qwiic 4-Pin JST SH connectors
- Adafruit ATECC608 - https://www.adafruit.com/product/4314
- Sparkfun ATECC508A - https://www.sparkfun.com/products/15573
- Sparkfun ATECC608A - https://www.sparkfun.com/products/15838
Since many of us had a hard time buying ATECC parts, but had a much easier time buying ATECC608B Trust and Go parts, this library supports these. The Trust and Go parts come provisioned. You can still get their serial number and device certificates, though.
Installation
The package can be installed by adding nerves_key
to your list of dependencies in mix.exs
:
def deps do
[
{:nerves_key, "~> 1.1"}
]
end
The docs can be found at https://hexdocs.pm/nerves_key.
General use
NervesKeys need to be provisioned before they can be used. That's a one-time step that could already have been done for you. If not, see subsequent sections for how that works.
To use any of the NervesKey APIs, you will need a "transport" to communicate with the ATECC508A/608A that's doing all of the work. Currently the only supported transport is I2C. The following line would be run on your Nerves device (like a Raspberry Pi, BeagleBone or your own custom hardware):
iex> {:ok, i2c} = ATECC508A.Transport.I2C.init([])
Check if your NervesKey has been provisioned:
iex> NervesKey.provisioned?(i2c)
true
If you get false
, go to the provisioning sections. If you received an error,
check that the NervesKey has a good connection to your hardware. If you have a
custom board, you may need to pass parameters to
ATECC508A.Transport.I2C.init/1
to set the correct I2C bus.
NervesKeys are provisioned with serial numbers. In production, these can be of your choosing.
iex> NervesKey.manufacturer_sn(i2c)
"ABC12345"
Of course, the more interesting part of the NervesKeys are its storage of device private keys and their certificates. For the common case, it stores two X.509 certificates: one for the device and one for the certificate that signed the device certificate. The signer certificate is usually uploaded to the servers that the device will connect to so that it can authenticate the device. Here's how to get both of the certificates:
iex> NervesKey.device_cert(i2c)
{:OTPCertificate, ...}
# Put this in a convenient form:
iex> X509.Certificate.to_pem(v()) |> IO.puts
-----BEGIN CERTIFICATE-----
stuff
stuff
stuff
-----END CERTIFICATE-----
iex> NervesKey.signer_cert(i2c)
{:OTPCertificate, ...}
The next step is to tell Erlang's SSL library that you want to use the NervesKey when connecting to the server. For that, you'll need nerves_key_pkcs11 which is included as a dependency of this library. This code is somewhat tedious but hopefully the following code fragment will help:
{:ok, engine} = NervesKey.PKCS11.load_engine()
{:ok, i2c} = ATECC508A.Transport.I2C.init([])
signer_cert = X509.Certificate.to_der(NervesKey.signer_cert(i2c))
cert = X509.Certificate.to_der(NervesKey.device_cert(i2c))
key = NervesKey.PKCS11.private_key(engine, {:i2c, 1})
cacerts = [signer_cert] ++ Keyword.get(opts, :trusted_certs, [])
Tortoise.Supervisor.start_child(
server: {
Tortoise.Transport.SSL,
verify: :verify_peer,
host: Keyword.get(opts, :host),
cert: cert,
key: key,
cacerts: cacerts,
versions: [:"tlsv1.2"],
})
Connecting to Google Cloud Platform IoT Core
While the prior Tortoise
code snippet works well with services like AWS, Google Cloud Platform (GCP) requires that a device must create a JWT token in order to connect to the broker. NervesKey provides the NervesKey.sign_digest
function to assist with the JWT signature portion of Google's documentation. The following code fragment can help with creating the JWT that is required as the MQTT password:
@doc """
Generate a JSON Web Token used to sign into the Google Cloud Platform
IoT Core MQTT broker.
"""
@spec generate_jwt() :: String.t()
def generate_jwt do
jwt_expiration_in_hours = 4
iat = System.os_time(:second)
exp = iat + jwt_expiration_in_hours * 60 * 60
aud = Application.get_env(:__my_project__, :gcp_project_id)
header = %{"alg" => "ES256", "typ" => "JWT"}
payload = %{"iat" => iat, "exp" => exp, "aud" => aud}
sign(header, payload)
end
@doc """
Sign a JSON Web Token.
"""
@spec sign(header :: map, payload :: map) :: String.t()
def sign(header, payload) do
{:ok, transport} = ATECC508A.Transport.I2C.init([])
encoded_header =
header
|> Jason.encode!()
|> Base.url_encode64(padding: false)
encoded_payload =
payload
|> Jason.encode!()
|> Base.url_encode64(padding: false)
digest = :crypto.hash(:sha256, "#{encoded_header}.#{encoded_payload}")
{:ok, signature} = NervesKey.sign_digest(transport, digest)
encoded_signature = Base.url_encode64(signature, padding: false)
"#{encoded_header}.#{encoded_payload}.#{encoded_signature}"
end
This fragment can allow for the dependency injection of sign
if the firmware needs to run both with and without the ATECC crypto chip (i.e. on the target and on the host). JOSE.JWT.sign
can be used for a host implementation.
Preparing for provisioning
The ATECC508A/608A in the NervesKey needs to be provisioned before it can be used. Before you can do that, you'll need the following:
- A signer certificate and its private certificate (in other contexts, this is called a certificate authority)
- A serial number for your device
- A name for the device
The signer certificate and serial number are very important. After the provisioning process, they are locked down and cannot be changed without replacing the ATECC508A/608A. The device name is purely informational unless you choose to use it in your software.
NervesKeys support an auxillary set of certificates that identify the device. These are writable after the provisioning process. Since they're writable, they can be provisioned and updated at any time. As such, they're not programmed in the first-time provisioning process.
Signer certificates
Part of the provisioning process creates an X.509 certificate for the NervesKey that can be used to authenticate TLS connections. This certificate is signed by a "signer certificate". You will eventually need to upload the signer certificate to NervesHub or AWS IoT or wherever you would like to authenticate devices.
Due to memory limitations, the ATECC508A/608A has a way to compress X.509 certificates on chip. See ATECC Compressed Certificate Definition. To comply with the limitations of compressible certificates, NervesKey provides a mix task to create them:
$ mix nerves_key.signer create nerveskey_prod_signer1
Created signer cert, nerveskey_prod_signer1.cert and private key, nerveskey_prod_signer1.key.
Please store nerveskey_prod_signer1.key in a safe place.
nerveskey_prod_signer1.cert is ready to be uploaded to the servers that need
to authenticate devices signed by the private key.
There is no magic in the compressible certificates. They're just limited in what
they can contain. You can inspect them with openssl x509 -in nerveskey_prod_signer1.cert -text
.
Check with your IoT service on how the signer certificate is used. If it's only used for first-time device registration, then the signer certificate may not need a long expiration time. You may also be interested in creating more than one signer certificate if you have more than one manufacturing facility.
Manufacturer serial numbers
Be aware that there are a lot of things called serial numbers. In an attempt to minimize confusion, we'll refer to the serial number that identifies the device to humans and other machines as the "manufacturer serial number". This string (it need not be a number) is commonly printed on a label on a device. It may be embedded in a barcode. Other serial numbers exist - the ATECC508A/608A has a 9 byte one and X.509 certificates have ones. Those serial numbers have guarantees on uniqueness. It is up to the device manufacturer to make sure that the "manufacturer serial number" is unique. People generally want to do this for their own sanity.
The NervesKey saves the manufacturing serial number in the one-time programmable memory on the ATECC508A/608A and also in the device's X.509 certificate. The device's X.509 certificate is signed, so cloud servers can trust the manufacturer serial number.
At this point, you're the manufacturer. Decide how you'd like your serial numbers to look. Whatever you pick, it must fit in 48-bytes. Representing the serial number is ASCII is commonly done. If you don't want to deal with this, do what we do (Base32-encode the ATECC508A/608A's globally unique identifier):
iex> {:ok, i2c} = ATECC508A.Transport.I2C.init([])
{:ok, {ATECC508A.Transport.I2C, {#Reference<0.879310498.269090821.27261>, 96}}}
iex> NervesKey.default_info(i2c)
%NervesKey.ProvisioningInfo{
board_name: "NervesKey",
manufacturer_sn: "AER245UNQOY4T3Q"
}
Provisioning
Now that you have a signer certificate, the signer's private key, and a manufacturer serial number, you can provision a NervesKey or the ATECC508A/608A acting as a NervesKey in your device. Usually there's some custom manufacturing support software that performs this step. We'll provision at the iex prompt.
Use sftp
to copy the signer certificate and private key to your device. We'll
put them /tmp
so that they disappear on reboot:
$ sftp nerves.local
Connected to nerves.local.
sftp> cd /tmp
sftp> put nerveskey_prod_signer1.*
Uploading nerveskey_prod_signer1.cert to /tmp/nerveskey_prod_signer1.cert
nerveskey_prod_signer1.cert 100% 636 78.3KB/s 00:00
Uploading nerveskey_prod_signer1.key to /tmp/nerveskey_prod_signer1.key
nerveskey_prod_signer1.key 100% 228 78.3KB/s 00:00
sftp> exit
Next, go to the IEx prompt on the device and run the following:
# Customize these or use `NervesKey.default_info/1` for defaults
cert_name="nerveskey_prod_signer1"
manufacturer_sn = "N1234"
board_name = "NervesKey"
# These lines should be copy/paste
signer_cert = File.read!("/tmp/#{cert_name}.cert") |> X509.Certificate.from_pem!;true
signer_key = File.read!("/tmp/#{cert_name}.key") |> X509.PrivateKey.from_pem!();true
{:ok, i2c} = ATECC508A.Transport.I2C.init([])
provision_info = %NervesKey.ProvisioningInfo{manufacturer_sn: manufacturer_sn, board_name: board_name}
# Double-check what you typed above before running this
NervesKey.provision(i2c, provision_info, signer_cert, signer_key)
If the last line returns :ok
after about 2 seconds, then celebrate. You
successfully programmed a NervesKey. You can't program it again. If you try,
you'll get an error.
Provisioning an auxiliary certificate
If a situation arises where the originally provisioned certificate can't be used, it's possible to store a second certificate on the device. This second certificate uses the same private key as the first certificate. (It is assumed that the algorithmic and physical protections on the first private key are sufficient that storing two different private keys doesn't add value.) Use cases include:
- Recovering from expiration or loss of the original signer key
- Experimentation
- Fixing errors in the original certificates
The auxiliary certificate is stored in writable memory on the ATECC508A/608A.
The NervesKey must be provisioned before the auxiliary certificate can be written. Assuming that's been done, copy the signer certificate and private key to your device similar to what you did before. Then run the following at the IEx prompt:
# Customize these
cert_name="nerveskey_prod_signer1"
# These lines should be copy/paste
signer_cert = File.read!("/tmp/#{cert_name}.cert") |> X509.Certificate.from_pem!;true
signer_key = File.read!("/tmp/#{cert_name}.key") |> X509.PrivateKey.from_pem!();true
{:ok, i2c} = ATECC508A.Transport.I2C.init([])
NervesKey.provision_aux_certificates(i2c, signer_cert, signer_key)
Debugging without an ATECC508A/608A
Before hardware is available or if you're debugging connections to a service
(like AWS IoT) and having no luck, it can be useful to manually generate
device certificates. The nerves_key.device
helper can be used for this and
does not require a NervesKey at all. Certificates generated using this helper
will look like ones stored on the NervesKey except for the important feature of
the private key part being private.
Here's an example:
mix nerves_key.device create <serial number> --signer-cert ca.cert --signer-key ca.key
Just like the signer certs, you can inspect the generated certs with openssl x509
. Services that work with these certificates should work with real
NervesKeys.
Settings
The NervesKey has bytes left over for storing a few settings. The
NervesKey.put_settings/2
and NervesKey.get_settings/1
APIs let you store and
retrieve a map. Since the storage is limited and relatively slow, this is
intended for settings that rarely change or may be tightly coupled with
certificates already being stored in the NervesKey.
Internally, NervesKey
calls :erlang.term_to_binary
to convert the map to raw
bytes and then it spreads it across ATECC508A slots for storage. This means that
the keys used in the map take up space too.
Support
If you run into problems, please help us improve this project by filing an issue.
ATECC508A configuration
This section describes the ATECC508A/608A configuration used for the NervesKey. This information isn't needed for using the library.
See Table 2-5 in the ATECC508A data sheet for documentation on the configuration zone. This software expects the following configuration to be programmed (unspecified bytes are either not programmable or kept as their defaults):
Bytes | Name | Value | Description |
---|---|---|---|
14 | I2C_Enable | 01 | I2C mode |
16 | I2C_Address | C0 | I2C address of the module (default) |
18 | OTPmode | AA | OTP is in read-only mode |
19 | ChipMode | 00 | Default mode |
20-51 | SlotConfig | N/A | See the next table |
92-95 | X509Format | 00..00 | Unused |
96-127 | KeyConfig | N/A | See next table |
The slots are programmed as follows. This definition is organized to be similar to the Microchip Standard TLS Configuration to minimize changes to other software. Unused slots are configured so that applications can use them as they would an EEPROM.
Slot | Description | SlotConfig | KeyConfig | Primary properties |
---|---|---|---|---|
0 | Device private key | 87 20 | 33 00 | Private key, read only; lockable |
1 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable |
2 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable |
3 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable |
4 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable |
5 | Settings (Part 3) | 0F 0F | 1C 00 | Clear read/write; not lockable |
6 | Settings (Part 2) | 0F 0F | 1C 00 | Clear read/write; not lockable |
7 | Settings (Part 1) | 0F 0F | 1C 00 | Clear read/write; not lockable |
8 | Settings (Part 0) | 0F 0F | 3C 00 | Clear read/write; lockable |
9 | Aux device certificate | 0F 0F | 3C 00 | Clear read/write; lockable |
10 | Device certificate | 0F 2F | 3C 00 | Clear read only; lockable |
11 | Signer public key | 0F 2F | 30 00 | P256; Clear read only; lockable |
12 | Signer certificate | 0F 2F | 3C 00 | Clear read only; lockable |
13 | Signer serial number + | 0F 2F | 3C 00 | Clear read only; lockable |
14 | Aux signer public key | 0F 0F | 3C 00 | Clear read/write; lockable |
15 | Aux signer certificate | 0F 0F | 3C 00 | Clear read/write; lockable |
- The signer serial number slot is currently unused since the signer's cert is computed from the public key
The ATECC508A includes a 64 byte OTP (one-time programmable) memory. It has the following layout:
Bytes | Name | Contents |
---|---|---|
0-3 | Magic | 4e 72 76 73 |
4 | Flags_MSB | 0 |
5 | Flags_LSB | 0 = 16 byte serial number, 1 = 32 byte serial number |
6-15 | Board name | 10 byte name for the board in ASCII (set unused bytes to 0) |
16-31 | Mfg serial number | Serial number in ASCII (set unused bytes to 0) |
32-63 | Serial# or User | If Flags == 1, then the rest of the serial number |