Skip to content

Commit

Permalink
Add support for cert auth, including on_connect logic
Browse files Browse the repository at this point in the history
  • Loading branch information
joshk committed Jan 27, 2025
1 parent 4058a4d commit 2dfbba6
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 64 deletions.
13 changes: 13 additions & 0 deletions lib/nerves_hub/devices.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ defmodule NervesHub.Devices do
|> Repo.one!()
end

def get_device_by_x509(cert) do
fingerprint = NervesHub.Certificate.fingerprint(cert)

Device
|> join(:inner, [d], dc in assoc(d, :device_certificates))
|> where([d, dc], dc.fingerprint == ^fingerprint)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
certificate -> {:ok, certificate}
end
end

@spec filter(%Product{}, map()) :: %{
entries: list(Device.t()),
current_page: non_neg_integer(),
Expand Down
51 changes: 50 additions & 1 deletion lib/nerves_hub/rpc/device_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,34 @@ defmodule NervesHub.RPC.DeviceAuth do
@moduledoc false

alias NervesHub.Devices
alias NervesHub.Devices.Connections
alias NervesHub.Devices.DeviceConnection
alias NervesHub.Devices.Device
alias NervesHub.Products
alias NervesHub.Tracker

alias Plug.Crypto

# Default 90 seconds max age for the signature
@default_max_hmac_age 90

def connect_device({:ssl_certs, ssl_cert}) do
X509.Certificate.from_der!(ssl_cert)
|> Devices.get_device_by_x509()
|> case do
{:ok, device} ->
{:ok, on_connect(device)}

error ->
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
auth: :cert,
reason: error
})

{:error, :invalid_auth}
end
end

def connect_device({:shared_secrets, x_headers}) do
headers = Map.new(x_headers)

Expand All @@ -18,7 +39,7 @@ defmodule NervesHub.RPC.DeviceAuth do
{:ok, signature} <- Map.fetch(headers, "x-nh-signature"),
{:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts),
{:ok, device} <- get_or_maybe_create_device(auth, identifier) do
{:ok, Devices.preload_product(device)}
{:ok, on_connect(device)}
else
error ->
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
Expand All @@ -31,6 +52,34 @@ defmodule NervesHub.RPC.DeviceAuth do
end
end

defp on_connect(%Device{status: :registered} = device) do
Devices.set_as_provisioned!(device)
|> on_connect()
end

defp on_connect(device) do
# disconnect devices using the same identifier
Phoenix.Channel.Server.broadcast_from!(
NervesHub.PubSub,
self(),
"device_socket:#{device.id}",
"disconnect",
%{}
)

{:ok, %DeviceConnection{id: connection_id}} = Connections.device_connected(device.id)

:telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{
ref_id: connection_id,
identifier: device.identifier,
firmware_uuid: get_in(device, [Access.key(:firmware_metadata), Access.key(:uuid)])
})

Tracker.online(device)

{connection_id, Devices.preload_product(device)}
end

defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do
with [digest_str, iter_str, klen_str] <- String.split(alg, "-"),
digest <- String.to_existing_atom(String.downcase(digest_str)),
Expand Down
72 changes: 9 additions & 63 deletions lib/nerves_hub_web/channels/device_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ defmodule NervesHubWeb.DeviceSocket do

require Logger

alias NervesHub.Devices
alias NervesHub.Devices.Connections
alias NervesHub.Devices.Device
alias NervesHub.Devices.DeviceConnection
alias NervesHub.Tracker

alias NervesHub.RPC.DeviceAuth
Expand All @@ -18,13 +15,6 @@ defmodule NervesHubWeb.DeviceSocket do

defoverridable init: 1, handle_in: 2, terminate: 2

@impl Phoenix.Socket.Transport
def init(state_tuple) do
{:ok, {state, socket}} = super(state_tuple)
socket = on_connect(socket)
{:ok, {state, socket}}
end

@impl Phoenix.Socket.Transport
@decorate with_span("Channels.DeviceSocket.terminate")
def terminate(reason, {_channels_info, socket} = state) do
Expand Down Expand Up @@ -71,40 +61,25 @@ defmodule NervesHubWeb.DeviceSocket do
@decorate with_span("Channels.DeviceSocket.connect")
def connect(_params, socket, %{peer_data: %{ssl_cert: ssl_cert}})
when not is_nil(ssl_cert) do
X509.Certificate.from_der!(ssl_cert)
|> Devices.get_device_certificate_by_x509()
|> case do
{:ok, %{device: %Device{} = device}} ->
socket_and_assigns(socket, Devices.preload_product(device))
case DeviceAuth.connect_device({:ssl_certs, ssl_cert}) do
{:ok, ref_and_device} ->
socket_and_assigns(socket, ref_and_device)

error ->
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
auth: :cert,
reason: error
})

{:error, :invalid_auth}
error
end
end

# Used by Devices connecting with HMAC Shared Secrets
@decorate with_span("Channels.DeviceSocket.connect")
def connect(_params, socket, %{x_headers: x_headers})
when is_list(x_headers) and length(x_headers) > 0 do
headers = Map.new(x_headers)

case DeviceAuth.connect_device({:shared_secrets, x_headers}) do
{:ok, device} ->
socket_and_assigns(socket, Devices.preload_product(device))
{:ok, ref_and_device} ->
socket_and_assigns(socket, ref_and_device)

error ->
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
auth: :shared_secrets,
reason: error,
product_key: Map.get(headers, "x-nh-key", "*empty*")
})

{:error, :invalid_auth}
error
end
end

Expand All @@ -126,44 +101,15 @@ defmodule NervesHubWeb.DeviceSocket do
]
end

defp socket_and_assigns(socket, device) do
# disconnect devices using the same identifier
_ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{})

defp socket_and_assigns(socket, {ref_id, device}) do
socket =
socket
|> assign(:device, device)
|> assign(:reference_id, ref_id)

{:ok, socket}
end

@decorate with_span("Channels.DeviceSocket.on_connect#registered")
defp on_connect(%{assigns: %{device: %{status: :registered} = device}} = socket) do
socket
|> assign(device: Devices.set_as_provisioned!(device))
|> on_connect()
end

@decorate with_span("Channels.DeviceSocket.on_connect#provisioned")
defp on_connect(%{assigns: %{device: device}} = socket) do
# Report connection and use connection id as reference
{:ok, %DeviceConnection{id: connection_id}} =
Connections.device_connected(device.id)

:telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{
ref_id: connection_id,
identifier: socket.assigns.device.identifier,
firmware_uuid:
get_in(socket.assigns.device, [Access.key(:firmware_metadata), Access.key(:uuid)])
})

Tracker.online(device)

socket
|> assign(:device, device)
|> assign(:reference_id, connection_id)
end

@decorate with_span("Channels.DeviceSocket.on_disconnect")
defp on_disconnect(exit_reason, socket)

Expand Down

0 comments on commit 2dfbba6

Please sign in to comment.