defmodule NervesKey do @moduledoc """ This is a high level interface to provisioning and using the NervesKey or any ATECC508A/608A that can be configured similarly. """ alias NervesKey.{Config, OTP, Data, ProvisioningInfo} @build_year DateTime.utc_now().year @settings_slots_nerves_key [8, 7, 6, 5] @settings_slot_trust_and_go 8 @typedoc "Which device/signer certificate pair to use" @type certificate_pair() :: :primary | :aux @typedoc "Which type of device to use" @type device_type() :: :nerves_key | :trust_and_go @doc """ Detect if a NervesKey is available on the transport """ @spec detected?(ATECC508A.Transport.t()) :: boolean() defdelegate detected?(transport), to: ATECC508A.Transport @doc """ Check whether the NervesKey has been provisioned """ @spec provisioned?(ATECC508A.Transport.t()) :: boolean() def provisioned?(transport) do {:ok, config} = ATECC508A.Configuration.read(transport) # If the OTP and data sections are locked, then this chip has been provisioned. config.lock_value == 0 end @doc """ Create a signing key pair This returns a tuple that contains a new signer certificate and private key. It is compatible with the ATECC508A certificate compression. Options: * :years_valid - how many years this key is valid for """ @spec create_signing_key_pair(keyword()) :: {X509.Certificate.t(), X509.PrivateKey.t()} def create_signing_key_pair(opts \\ []) do years_valid = Keyword.get(opts, :years_valid, 1) ATECC508A.Certificate.new_signer(years_valid) end @doc """ Read the manufacturer's serial number """ @spec manufacturer_sn(ATECC508A.Transport.t(), device_type()) :: binary() def manufacturer_sn(transport, type \\ :nerves_key) def manufacturer_sn(transport, :nerves_key) do {:ok, %OTP{manufacturer_sn: serial_number}} = OTP.read(transport) serial_number end def manufacturer_sn(transport, :trust_and_go) do {:ok, config} = ATECC508A.Configuration.read(transport) %ATECC508A.Configuration{serial_number: sn_bytes} = config Base.encode16(sn_bytes) end @doc """ IEEE EUI-48 MAC address that can be used as a unique identifier in LAN networking This is only available on `:trust_and_go` """ def manufacturer_mac(transport, :trust_and_go) do {:ok, <>} = ATECC508A.DataZone.read(transport, 5) eui48 end @doc """ Sign a SHA256 digest """ @spec sign_digest(ATECC508A.Transport.t(), binary()) :: {:ok, binary()} | {:error, atom()} def sign_digest(transport, digest) do private_key_slot_id = 0 ATECC508A.Request.sign_digest(transport, private_key_slot_id, digest) end @doc """ Return ssl_opts for using the NervesKey Pass an engine and optionally which certificate that you'd like to use. """ @spec ssl_opts(ATECC508A.Transport.t(), certificate_pair(), device_type()) :: keyword() def ssl_opts(transport, which \\ :primary, type \\ :nerves_key) do {:ok, engine} = NervesKey.PKCS11.load_engine() cert = NervesKey.device_cert(transport, which, type) |> X509.Certificate.to_der() signer_cert = NervesKey.signer_cert(transport, which, type) |> X509.Certificate.to_der() transport_info = ATECC508A.Transport.info(transport) key = NervesKey.PKCS11.private_key(engine, i2c: i2c_instance(transport_info.bus_name), type: type) cacerts = [signer_cert] [key: key, cert: cert, cacerts: cacerts] end defp i2c_instance(<<"i2c-", instance::binary>>) do String.to_integer(instance) end @doc """ Read the device certificate from the slot The device must be programmed for this to work. Examples: ``` iex> NervesKey.device_cert(transport, :primary, :nerves_key) {:OTPCertificate, ...} iex> NervesKey.device_cert(transport, :primary, :trust_and_go) {:OTPCertificate, ...} ``` """ @spec device_cert(ATECC508A.Transport.t(), certificate_pair(), device_type()) :: X509.Certificate.t() def device_cert(transport, which \\ :primary, type \\ :nerves_key) def device_cert(transport, which, :nerves_key) do {:ok, device_sn} = Config.device_sn(transport) {:ok, device_data} = ATECC508A.DataZone.read(transport, Data.device_cert_slot(which)) {:ok, <>} = ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which)) signer_public_key = ATECC508A.Certificate.raw_to_public_key(signer_public_key_raw) {:ok, %OTP{manufacturer_sn: serial_number}} = OTP.read(transport) {:ok, public_key_raw} = Data.genkey_raw(transport, false) template = ATECC508A.Certificate.NervesKeyTemplate.device(serial_number, signer_public_key) compressed = %ATECC508A.Certificate.Compressed{ data: device_data, device_sn: device_sn, public_key: public_key_raw, template: template, issuer_rdn: X509.RDNSequence.new("/CN=Signer", :otp), subject_rdn: X509.RDNSequence.new("/CN=" <> serial_number, :otp) } ATECC508A.Certificate.decompress(compressed) end def device_cert(transport, which, :trust_and_go) do {:ok, device_sn} = NervesKey.Config.device_sn(transport) {:ok, device_data} = ATECC508A.DataZone.read(transport, NervesKey.Data.device_cert_slot(which)) {:ok, <<_pad1::bytes-4, signer_public_key_raw_x::bytes-32, _pad2::bytes-4, signer_public_key_raw_y::bytes-32>>} = ATECC508A.DataZone.read(transport, NervesKey.Data.signer_pubkey_slot(which)) signer_public_key_raw = signer_public_key_raw_x <> signer_public_key_raw_y {:ok, public_key_raw} = NervesKey.Data.genkey_raw(transport, false) << _compressed_signature::binary-size(64), _compressed_validity::binary-size(3), signer_id::size(16), template_id::size(4), _chain_id::size(4), _serial_number_source::size(4), _format_version::size(4), 0::size(8) >> = device_data aki = :crypto.hash(:sha, <<4>> <> signer_public_key_raw) ski = :crypto.hash(:sha, <<4>> <> public_key_raw) signer_id_hex_str = Integer.to_string(signer_id, 16) eui48 = manufacturer_mac(transport, :trust_and_go) template = ATECC508A.Certificate.TrustAndGoTemplate.device( device_sn, signer_id, template_id, "eui48_#{eui48}", ski, aki ) issuer_rdn = X509.RDNSequence.new( "/O=Microchip Technology Inc/CN=Crypto Authentication Signer #{signer_id_hex_str}", :otp ) subject_rdn = X509.RDNSequence.new( "/O=Microchip Technology Inc/CN=sn" <> Base.encode16(device_sn), :otp ) compressed = %ATECC508A.Certificate.Compressed{ data: device_data, device_sn: device_sn, public_key: public_key_raw, template: template, issuer_rdn: issuer_rdn, subject_rdn: subject_rdn } ATECC508A.Certificate.decompress(compressed) end @doc """ Read the signer certificate from the slot """ @spec signer_cert(ATECC508A.Transport.t(), certificate_pair(), device_type()) :: X509.Certificate.t() def signer_cert(transport, which \\ :primary, type \\ :nerves_key) def signer_cert(transport, which, :nerves_key) do {:ok, signer_data} = ATECC508A.DataZone.read(transport, Data.signer_cert_slot(which)) {:ok, <>} = ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which)) signer_public_key = ATECC508A.Certificate.raw_to_public_key(signer_public_key_raw) template = ATECC508A.Certificate.NervesKeyTemplate.signer(signer_public_key) compressed = %ATECC508A.Certificate.Compressed{ data: signer_data, public_key: signer_public_key_raw, template: template, issuer_rdn: X509.RDNSequence.new("/CN=Signer", :otp), subject_rdn: X509.RDNSequence.new("/CN=Signer", :otp) } ATECC508A.Certificate.decompress(compressed) end def signer_cert(transport, which, :trust_and_go) do {:ok, signer_data} = ATECC508A.DataZone.read(transport, Data.signer_cert_slot(which)) {:ok, <<_pad1::bytes-4, signer_public_key_raw_x::bytes-32, _pad2::bytes-4, signer_public_key_raw_y::bytes-32>>} = ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which)) signer_public_key_raw = signer_public_key_raw_x <> signer_public_key_raw_y << _compressed_signature::binary-size(64), _compressed_validity::binary-size(3), signer_id::size(16), _template_id::size(4), _chain_id::size(4), _serial_number_source::size(4), _format_version::size(4), 0::size(8) >> = signer_data root_public_key_raw = <<189, 84, 230, 109, 227, 135, 84, 132, 0, 107, 83, 174, 21, 128, 213, 10, 160, 105, 231, 138, 223, 85, 120, 216, 92, 226, 213, 77, 213, 184, 48, 41, 107, 255, 221, 110, 111, 114, 86, 251, 217, 158, 241, 161, 22, 177, 29, 51, 173, 73, 16, 58, 161, 133, 135, 57, 220, 250, 228, 55, 225, 157, 99, 78>> aki = :crypto.hash(:sha, <<4>> <> root_public_key_raw) ski = :crypto.hash(:sha, <<4>> <> signer_public_key_raw) template = ATECC508A.Certificate.TrustAndGoTemplate.signer(signer_id, ski, aki) signer_id_hex_str = Integer.to_string(signer_id, 16) issuer_rdn = X509.RDNSequence.new( "/O=Microchip Technology Inc/CN=Crypto Authentication Root CA 002", :otp ) subject_rdn = X509.RDNSequence.new( "/O=Microchip Technology Inc/CN=Crypto Authentication Signer #{signer_id_hex_str}", :otp ) compressed = %ATECC508A.Certificate.Compressed{ data: signer_data, public_key: signer_public_key_raw, template: template, issuer_rdn: issuer_rdn, subject_rdn: subject_rdn } ATECC508A.Certificate.decompress(compressed) end @doc """ Provision a NervesKey in one step. See the README.md for how to use this. This function locks the ATECC508A down, so you'll want to be sure what you pass it is correct. This function does it all. It requires the signer's private key so handle that with care. Alternatively, please consider sending a PR for supporting off-device signatures so that HSMs can be used. """ @spec provision( ATECC508A.Transport.t(), ProvisioningInfo.t(), X509.Certificate.t(), X509.PrivateKey.t() ) :: :ok def provision(transport, info, signer_cert, signer_key) do check_time() :ok = configure(transport) otp_info = OTP.new(info.board_name, info.manufacturer_sn) otp_data = OTP.to_raw(otp_info) :ok = OTP.write(transport, otp_data) {:ok, device_public_key} = Data.genkey(transport) {:ok, device_sn} = Config.device_sn(transport) device_cert = ATECC508A.Certificate.new_device( device_public_key, device_sn, info.manufacturer_sn, signer_cert, signer_key ) slot_data = Data.slot_data(device_sn, device_cert, signer_cert) :ok = Data.write_slots(transport, slot_data) # This is the point of no return!! # Lock the data and OTP zones :ok = Data.lock(transport, otp_data, slot_data) # Lock the slot that contains the private key to prevent calls to GenKey # from changing it. See datasheet for how GenKey doesn't check the zone # lock. :ok = ATECC508A.Request.lock_slot(transport, 0) end @doc """ Provision the auxiliary device/signer certificates on a NervesKey. This function creates and saves the auxiliary certificates. These are only needed if the ones written by `provision/4` are not usable. They are not used unless explicitly requested. See the README.md for details. You may call this function multiple times after the ATECC508A has been provisioned. """ @spec provision_aux_certificates( ATECC508A.Transport.t(), X509.Certificate.t(), X509.PrivateKey.t(), device_type() ) :: :ok def provision_aux_certificates(transport, signer_cert, signer_key, type \\ :nerves_key) do check_time() manufacturer_sn = if type == :nerves_key do manufacturer_sn(transport) else manufacturer_mac(transport, :trust_and_go) end {:ok, device_public_key} = Data.genkey(transport, false) {:ok, device_sn} = Config.device_sn(transport) device_cert = ATECC508A.Certificate.new_device( device_public_key, device_sn, manufacturer_sn, signer_cert, signer_key ) Data.write_aux_certs(transport, device_sn, device_cert, signer_cert) end @doc """ Clear out the auxiliary certificates This function overwrites the auxiliary certificate slots with """ @spec clear_aux_certificates(ATECC508A.Transport.t()) :: :ok def clear_aux_certificates(transport) do Data.clear_aux_certs(transport) end @doc """ Check whether the auxiliary certificates were programmed """ @spec has_aux_certificates?(ATECC508A.Transport.t()) :: boolean() def has_aux_certificates?(transport) do slot = Data.device_cert_slot(:aux) slot_size = ATECC508A.DataZone.slot_size(slot) {:ok, slot_contents} = ATECC508A.DataZone.read(transport, slot) slot_contents != <<0::size(slot_size)-unit(8)>> end @doc """ Return default provisioning info for a NervesKey This function is used for pre-programmed NervesKey devices. The serial number is a Base32-encoded version of the ATECC508A/608A's globally unique serial number. No additional care is needed to keep the number unique. """ @spec default_info(ATECC508A.Transport.t()) :: ProvisioningInfo.t() def default_info(transport) do {:ok, sn} = Config.device_sn(transport) %ProvisioningInfo{manufacturer_sn: Base.encode32(sn, padding: false), board_name: "NervesKey"} end @doc """ Return the max length of settings """ @spec max_settings_len(device_type()) :: integer() def max_settings_len(:nerves_key) do Enum.reduce(@settings_slots_nerves_key, 0, fn slot, acc -> acc + ATECC508A.DataZone.slot_size(slot) end) end def max_settings_len(:trust_and_go) do ATECC508A.DataZone.slot_size(@settings_slot_trust_and_go) end @doc """ Return the settings block as a binary """ @spec get_raw_settings(ATECC508A.Transport.t(), device_type()) :: {:ok, binary()} | {:error, atom()} def get_raw_settings(transport, device_type \\ :nerves_key) do if device_type == :nerves_key do all_reads = Enum.map(@settings_slots_nerves_key, &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 else case ATECC508A.DataZone.read(transport, @settings_slot_trust_and_go) do {:ok, data} -> {:ok, data} error -> error end end end @doc """ Return all of the setting stored in the NervesKey as a map """ @spec get_settings(ATECC508A.Transport.t(), device_type()) :: {:ok, map()} | {:error, atom()} def get_settings(transport, device_type \\ :nerves_key) do with {:ok, raw_settings} <- get_raw_settings(transport, device_type) 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(), device_type()) :: :ok def put_settings(transport, settings, device_type \\ :nerves_key) when is_map(settings) do raw_settings = :erlang.term_to_binary(settings) put_raw_settings(transport, raw_settings, device_type) 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(), device_type()) :: :ok def put_raw_settings(transport, raw_settings, device_type) when is_binary(raw_settings) do if byte_size(raw_settings) > max_settings_len(device_type) do raise "Settings are too large and won't fit in the NervesKey. The max raw size is #{max_settings_len(device_type)}." end padded_settings = pad(raw_settings, max_settings_len(device_type)) slots = case device_type do :nerves_key -> break_into_slots(padded_settings, @settings_slots_nerves_key) :trust_and_go -> break_into_slots(padded_settings, [@settings_slot_trust_and_go]) end 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)) <> 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 # times and so long as the part is configured in a compatible # way, then it succeeds. This is needed to recover from failures # in the provisioning process. defp configure(transport) do cond do Config.config_compatible?(transport) == {:ok, true} -> :ok Config.configured?(transport) == {:ok, true} -> {:error, :config_locked} true -> Config.configure(transport) end end defp check_time() do unless DateTime.utc_now().year >= @build_year do raise """ It doesn't look like the clock has been set. Check that `nerves_time` is running or something else is providing time. """ end end end