Skip to content

nerves-hub/nerves_key

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NervesKey

CircleCI Hex version

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:

  1. Provision blank ATECC508A/608A devices - this includes private key generation
  2. Storage for serial number and one-time calibration data (useful if primary storage is on a removable MicroSD card)
  3. Support for Microchip's compressed X.509 certificate format to work with Microchip's C libraries
  4. Support for signing device certificates so that devices can be included in a PKI
  5. Support for storing a small amount of run-time configuration in unused data EEPROM slots
  6. 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

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:

  1. A signer certificate and its private certificate (in other contexts, this is called a certificate authority)
  2. A serial number for your device
  3. 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:

  1. Recovering from expiration or loss of the original signer key
  2. Experimentation
  3. 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