Skip to content
Permalink
Browse files

Add APIs for getting and putting a map

  • Loading branch information...
fhunleth committed Feb 22, 2019
1 parent bea27ad commit 0c5a2a72f63f9ba7ca1f31233b5ffa92a7ccc23c
Showing with 121 additions and 20 deletions.
  1. +32 −20 README.md
  2. +89 −0 lib/nerves_key.ex
@@ -14,8 +14,8 @@ 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 for interop with
C libraries
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
@@ -293,6 +293,18 @@ signer_key = File.read!("/tmp/#{cert_name}.key") |> X509.PrivateKey.from_pem!();
NervesKey.provision_aux_certificates(i2c, signer_cert, signer_key)
```

## 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
@@ -323,24 +335,24 @@ 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 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
6 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
7 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
8 | Unused | 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
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
@@ -7,6 +7,10 @@ defmodule NervesKey do
alias NervesKey.{Config, OTP, Data, ProvisioningInfo}

@build_year DateTime.utc_now().year
@settings_slots [8, 7, 6, 5]
@settings_max_length Enum.reduce(@settings_slots, 0, fn slot, acc ->
acc + ATECC508A.DataZone.slot_size(slot)
end)

@typedoc "Which device/signer certificate pair to use"
@type certificate_pair() :: :primary | :aux
@@ -230,6 +234,91 @@ defmodule NervesKey do
%ProvisioningInfo{manufacturer_sn: Base.encode32(sn, padding: false), board_name: "NervesKey"}
end

@doc """
Return the settings block as a binary
"""
@spec get_raw_settings(ATECC508A.Transport.t()) :: {:ok, binary()} | {:error, atom()}
def get_raw_settings(transport) do
all_reads = Enum.map(@settings_slots, &ATECC508A.DataZone.read(transport, &1))

case Enum.find(all_reads, fn {result, _} -> result != :ok end) do
nil ->
raw = Enum.map_join(all_reads, fn {:ok, contents} -> contents end)
{:ok, raw}

error ->
error
end
end

@doc """
Return all of the setting stored in the NervesKey as a map
"""
@spec get_settings(ATECC508A.Transport.t()) :: {:ok, map()} | {:error, atom()}
def get_settings(transport) do
with {:ok, raw_settings} <- get_raw_settings(transport) do
try do
settings = :erlang.binary_to_term(raw_settings, [:safe])
{:ok, settings}
catch
_, _ -> {:error, :corrupt}
end
end
end

@doc """
Store settings on the NervesKey
This overwrites all of the settings that are currently on the key and should
be used with care since there's no protection against a race condition with
other NervesKey users.
"""
@spec put_settings(ATECC508A.Transport.t(), map()) :: :ok
def put_settings(transport, settings) when is_map(settings) do
raw_settings = :erlang.term_to_binary(settings)
put_raw_settings(transport, raw_settings)
end

@doc """
Store raw settings on the Nerves Key
This overwrites all of the settings and should be used with care since there's
no protection against race conditions with other users of this API.
"""
@spec put_raw_settings(ATECC508A.Transport.t(), binary()) :: :ok
def put_raw_settings(transport, raw_settings) when is_binary(raw_settings) do
if byte_size(raw_settings) > @settings_max_length do
raise "Settings are too large and won't fit in the NervesKey. The max raw size is #{
@settings_max_length
}."
end

padded_settings = pad(raw_settings, @settings_max_length)
slots = break_into_slots(padded_settings, @settings_slots)

Enum.each(slots, fn {slot, data} -> ATECC508A.DataZone.write(transport, slot, data) end)
end

defp pad(bin, len) when byte_size(bin) < len do
to_pad = 8 * (len - byte_size(bin))
<<bin::binary, 0::size(to_pad)>>
end

defp pad(bin, _len), do: bin

defp break_into_slots(bin, slots) do
break_into_slots(bin, slots, [])
|> Enum.reverse()
end

defp break_into_slots(<<>>, [], result), do: result

defp break_into_slots(bin, [slot | rest], result) do
slot_len = ATECC508A.DataZone.slot_size(slot)
{contents, next} = :erlang.split_binary(bin, slot_len)
break_into_slots(next, rest, [{slot, contents} | result])
end

# Configure an ATECC508A or ATECC608A as a NervesKey.
#
# This is called from `provision/4`. It can be called multiple

0 comments on commit 0c5a2a7

Please sign in to comment.
You can’t perform that action at this time.