Skip to content

Commit 0c5a2a7

Browse files
committed
Add APIs for getting and putting a map
1 parent bea27ad commit 0c5a2a7

File tree

2 files changed

+121
-20
lines changed

2 files changed

+121
-20
lines changed

README.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ device easier. It has the following features:
1414
1. Provision blank ATECC508A/608A devices - this includes private key generation
1515
2. Storage for serial number and one-time calibration data (useful if primary
1616
storage is on a removable MicroSD card)
17-
3. Support for Microchip's compressed X.509 certificate format for interop with
18-
C libraries
17+
3. Support for Microchip's compressed X.509 certificate format to work with
18+
Microchip's C libraries
1919
4. Support for signing device certificates so that devices can be included in a
2020
PKI
2121
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!();
293293
NervesKey.provision_aux_certificates(i2c, signer_cert, signer_key)
294294
```
295295

296+
## Settings
297+
298+
The NervesKey has bytes left over for storing a few settings. The
299+
`NervesKey.put_settings/2` and `NervesKey.get_settings/1` APIs let you store and
300+
retrieve a map. Since the storage is limited and relatively slow, this is
301+
intended for settings that rarely change or may be tightly coupled with
302+
certificates already being stored in the NervesKey.
303+
304+
Internally, `NervesKey` calls `:erlang.term_to_binary` to convert the map to raw
305+
bytes and then it spreads it across ATECC508A slots for storage. This means that
306+
the keys used in the map take up space too.
307+
296308
## Support
297309

298310
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
323335
software. Unused slots are configured so that applications can use them as they
324336
would an EEPROM.
325337

326-
Slot | Description | SlotConfig | KeyConfig | Primary properties
327-
-----|-----------------------------------|------------|-----------|-------------------
328-
0 | Device private key | 87 20 | 33 00 | Private key, read only; lockable
329-
1 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
330-
2 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
331-
3 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
332-
4 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
333-
5 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
334-
6 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
335-
7 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
336-
8 | Unused | 0F 0F | 3C 00 | Clear read/write; lockable
337-
9 | Aux device certificate | 0F 0F | 3C 00 | Clear read/write; lockable
338-
10 | Device certificate | 0F 2F | 3C 00 | Clear read only; lockable
339-
11 | Signer public key | 0F 2F | 30 00 | P256; Clear read only; lockable
340-
12 | Signer certificate | 0F 2F | 3C 00 | Clear read only; lockable
341-
13 | Signer serial number + | 0F 2F | 3C 00 | Clear read only; lockable
342-
14 | Aux signer public key | 0F 0F | 3C 00 | Clear read/write; lockable
343-
15 | Aux signer certificate | 0F 0F | 3C 00 | Clear read/write; lockable
338+
Slot | Description | SlotConfig | KeyConfig | Primary properties
339+
-----|--------------------------|------------|-----------|-------------------
340+
0 | Device private key | 87 20 | 33 00 | Private key, read only; lockable
341+
1 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
342+
2 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
343+
3 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
344+
4 | Unused | 0F 0F | 1C 00 | Clear read/write; not lockable
345+
5 | Settings (Part 3) | 0F 0F | 1C 00 | Clear read/write; not lockable
346+
6 | Settings (Part 2) | 0F 0F | 1C 00 | Clear read/write; not lockable
347+
7 | Settings (Part 1) | 0F 0F | 1C 00 | Clear read/write; not lockable
348+
8 | Settings (Part 0) | 0F 0F | 3C 00 | Clear read/write; lockable
349+
9 | Aux device certificate | 0F 0F | 3C 00 | Clear read/write; lockable
350+
10 | Device certificate | 0F 2F | 3C 00 | Clear read only; lockable
351+
11 | Signer public key | 0F 2F | 30 00 | P256; Clear read only; lockable
352+
12 | Signer certificate | 0F 2F | 3C 00 | Clear read only; lockable
353+
13 | Signer serial number + | 0F 2F | 3C 00 | Clear read only; lockable
354+
14 | Aux signer public key | 0F 0F | 3C 00 | Clear read/write; lockable
355+
15 | Aux signer certificate | 0F 0F | 3C 00 | Clear read/write; lockable
344356

345357
+ The signer serial number slot is currently unused since the signer's cert is
346358
computed from the public key

lib/nerves_key.ex

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ defmodule NervesKey do
77
alias NervesKey.{Config, OTP, Data, ProvisioningInfo}
88

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

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

237+
@doc """
238+
Return the settings block as a binary
239+
"""
240+
@spec get_raw_settings(ATECC508A.Transport.t()) :: {:ok, binary()} | {:error, atom()}
241+
def get_raw_settings(transport) do
242+
all_reads = Enum.map(@settings_slots, &ATECC508A.DataZone.read(transport, &1))
243+
244+
case Enum.find(all_reads, fn {result, _} -> result != :ok end) do
245+
nil ->
246+
raw = Enum.map_join(all_reads, fn {:ok, contents} -> contents end)
247+
{:ok, raw}
248+
249+
error ->
250+
error
251+
end
252+
end
253+
254+
@doc """
255+
Return all of the setting stored in the NervesKey as a map
256+
"""
257+
@spec get_settings(ATECC508A.Transport.t()) :: {:ok, map()} | {:error, atom()}
258+
def get_settings(transport) do
259+
with {:ok, raw_settings} <- get_raw_settings(transport) do
260+
try do
261+
settings = :erlang.binary_to_term(raw_settings, [:safe])
262+
{:ok, settings}
263+
catch
264+
_, _ -> {:error, :corrupt}
265+
end
266+
end
267+
end
268+
269+
@doc """
270+
Store settings on the NervesKey
271+
272+
This overwrites all of the settings that are currently on the key and should
273+
be used with care since there's no protection against a race condition with
274+
other NervesKey users.
275+
"""
276+
@spec put_settings(ATECC508A.Transport.t(), map()) :: :ok
277+
def put_settings(transport, settings) when is_map(settings) do
278+
raw_settings = :erlang.term_to_binary(settings)
279+
put_raw_settings(transport, raw_settings)
280+
end
281+
282+
@doc """
283+
Store raw settings on the Nerves Key
284+
285+
This overwrites all of the settings and should be used with care since there's
286+
no protection against race conditions with other users of this API.
287+
"""
288+
@spec put_raw_settings(ATECC508A.Transport.t(), binary()) :: :ok
289+
def put_raw_settings(transport, raw_settings) when is_binary(raw_settings) do
290+
if byte_size(raw_settings) > @settings_max_length do
291+
raise "Settings are too large and won't fit in the NervesKey. The max raw size is #{
292+
@settings_max_length
293+
}."
294+
end
295+
296+
padded_settings = pad(raw_settings, @settings_max_length)
297+
slots = break_into_slots(padded_settings, @settings_slots)
298+
299+
Enum.each(slots, fn {slot, data} -> ATECC508A.DataZone.write(transport, slot, data) end)
300+
end
301+
302+
defp pad(bin, len) when byte_size(bin) < len do
303+
to_pad = 8 * (len - byte_size(bin))
304+
<<bin::binary, 0::size(to_pad)>>
305+
end
306+
307+
defp pad(bin, _len), do: bin
308+
309+
defp break_into_slots(bin, slots) do
310+
break_into_slots(bin, slots, [])
311+
|> Enum.reverse()
312+
end
313+
314+
defp break_into_slots(<<>>, [], result), do: result
315+
316+
defp break_into_slots(bin, [slot | rest], result) do
317+
slot_len = ATECC508A.DataZone.slot_size(slot)
318+
{contents, next} = :erlang.split_binary(bin, slot_len)
319+
break_into_slots(next, rest, [{slot, contents} | result])
320+
end
321+
233322
# Configure an ATECC508A or ATECC608A as a NervesKey.
234323
#
235324
# This is called from `provision/4`. It can be called multiple

0 commit comments

Comments
 (0)