Skip to content

Commit 2dfbba6

Browse files
committed
Add support for cert auth, including on_connect logic
1 parent 4058a4d commit 2dfbba6

File tree

3 files changed

+72
-64
lines changed

3 files changed

+72
-64
lines changed

lib/nerves_hub/devices.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ defmodule NervesHub.Devices do
9999
|> Repo.one!()
100100
end
101101

102+
def get_device_by_x509(cert) do
103+
fingerprint = NervesHub.Certificate.fingerprint(cert)
104+
105+
Device
106+
|> join(:inner, [d], dc in assoc(d, :device_certificates))
107+
|> where([d, dc], dc.fingerprint == ^fingerprint)
108+
|> Repo.one()
109+
|> case do
110+
nil -> {:error, :not_found}
111+
certificate -> {:ok, certificate}
112+
end
113+
end
114+
102115
@spec filter(%Product{}, map()) :: %{
103116
entries: list(Device.t()),
104117
current_page: non_neg_integer(),

lib/nerves_hub/rpc/device_auth.ex

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,34 @@ defmodule NervesHub.RPC.DeviceAuth do
22
@moduledoc false
33

44
alias NervesHub.Devices
5+
alias NervesHub.Devices.Connections
6+
alias NervesHub.Devices.DeviceConnection
7+
alias NervesHub.Devices.Device
58
alias NervesHub.Products
9+
alias NervesHub.Tracker
610

711
alias Plug.Crypto
812

913
# Default 90 seconds max age for the signature
1014
@default_max_hmac_age 90
1115

16+
def connect_device({:ssl_certs, ssl_cert}) do
17+
X509.Certificate.from_der!(ssl_cert)
18+
|> Devices.get_device_by_x509()
19+
|> case do
20+
{:ok, device} ->
21+
{:ok, on_connect(device)}
22+
23+
error ->
24+
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
25+
auth: :cert,
26+
reason: error
27+
})
28+
29+
{:error, :invalid_auth}
30+
end
31+
end
32+
1233
def connect_device({:shared_secrets, x_headers}) do
1334
headers = Map.new(x_headers)
1435

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

55+
defp on_connect(%Device{status: :registered} = device) do
56+
Devices.set_as_provisioned!(device)
57+
|> on_connect()
58+
end
59+
60+
defp on_connect(device) do
61+
# disconnect devices using the same identifier
62+
Phoenix.Channel.Server.broadcast_from!(
63+
NervesHub.PubSub,
64+
self(),
65+
"device_socket:#{device.id}",
66+
"disconnect",
67+
%{}
68+
)
69+
70+
{:ok, %DeviceConnection{id: connection_id}} = Connections.device_connected(device.id)
71+
72+
:telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{
73+
ref_id: connection_id,
74+
identifier: device.identifier,
75+
firmware_uuid: get_in(device, [Access.key(:firmware_metadata), Access.key(:uuid)])
76+
})
77+
78+
Tracker.online(device)
79+
80+
{connection_id, Devices.preload_product(device)}
81+
end
82+
3483
defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do
3584
with [digest_str, iter_str, klen_str] <- String.split(alg, "-"),
3685
digest <- String.to_existing_atom(String.downcase(digest_str)),

lib/nerves_hub_web/channels/device_socket.ex

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ defmodule NervesHubWeb.DeviceSocket do
44

55
require Logger
66

7-
alias NervesHub.Devices
87
alias NervesHub.Devices.Connections
9-
alias NervesHub.Devices.Device
10-
alias NervesHub.Devices.DeviceConnection
118
alias NervesHub.Tracker
129

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

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

21-
@impl Phoenix.Socket.Transport
22-
def init(state_tuple) do
23-
{:ok, {state, socket}} = super(state_tuple)
24-
socket = on_connect(socket)
25-
{:ok, {state, socket}}
26-
end
27-
2818
@impl Phoenix.Socket.Transport
2919
@decorate with_span("Channels.DeviceSocket.terminate")
3020
def terminate(reason, {_channels_info, socket} = state) do
@@ -71,40 +61,25 @@ defmodule NervesHubWeb.DeviceSocket do
7161
@decorate with_span("Channels.DeviceSocket.connect")
7262
def connect(_params, socket, %{peer_data: %{ssl_cert: ssl_cert}})
7363
when not is_nil(ssl_cert) do
74-
X509.Certificate.from_der!(ssl_cert)
75-
|> Devices.get_device_certificate_by_x509()
76-
|> case do
77-
{:ok, %{device: %Device{} = device}} ->
78-
socket_and_assigns(socket, Devices.preload_product(device))
64+
case DeviceAuth.connect_device({:ssl_certs, ssl_cert}) do
65+
{:ok, ref_and_device} ->
66+
socket_and_assigns(socket, ref_and_device)
7967

8068
error ->
81-
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
82-
auth: :cert,
83-
reason: error
84-
})
85-
86-
{:error, :invalid_auth}
69+
error
8770
end
8871
end
8972

9073
# Used by Devices connecting with HMAC Shared Secrets
9174
@decorate with_span("Channels.DeviceSocket.connect")
9275
def connect(_params, socket, %{x_headers: x_headers})
9376
when is_list(x_headers) and length(x_headers) > 0 do
94-
headers = Map.new(x_headers)
95-
9677
case DeviceAuth.connect_device({:shared_secrets, x_headers}) do
97-
{:ok, device} ->
98-
socket_and_assigns(socket, Devices.preload_product(device))
78+
{:ok, ref_and_device} ->
79+
socket_and_assigns(socket, ref_and_device)
9980

10081
error ->
101-
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
102-
auth: :shared_secrets,
103-
reason: error,
104-
product_key: Map.get(headers, "x-nh-key", "*empty*")
105-
})
106-
107-
{:error, :invalid_auth}
82+
error
10883
end
10984
end
11085

@@ -126,44 +101,15 @@ defmodule NervesHubWeb.DeviceSocket do
126101
]
127102
end
128103

129-
defp socket_and_assigns(socket, device) do
130-
# disconnect devices using the same identifier
131-
_ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{})
132-
104+
defp socket_and_assigns(socket, {ref_id, device}) do
133105
socket =
134106
socket
135107
|> assign(:device, device)
108+
|> assign(:reference_id, ref_id)
136109

137110
{:ok, socket}
138111
end
139112

140-
@decorate with_span("Channels.DeviceSocket.on_connect#registered")
141-
defp on_connect(%{assigns: %{device: %{status: :registered} = device}} = socket) do
142-
socket
143-
|> assign(device: Devices.set_as_provisioned!(device))
144-
|> on_connect()
145-
end
146-
147-
@decorate with_span("Channels.DeviceSocket.on_connect#provisioned")
148-
defp on_connect(%{assigns: %{device: device}} = socket) do
149-
# Report connection and use connection id as reference
150-
{:ok, %DeviceConnection{id: connection_id}} =
151-
Connections.device_connected(device.id)
152-
153-
:telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{
154-
ref_id: connection_id,
155-
identifier: socket.assigns.device.identifier,
156-
firmware_uuid:
157-
get_in(socket.assigns.device, [Access.key(:firmware_metadata), Access.key(:uuid)])
158-
})
159-
160-
Tracker.online(device)
161-
162-
socket
163-
|> assign(:device, device)
164-
|> assign(:reference_id, connection_id)
165-
end
166-
167113
@decorate with_span("Channels.DeviceSocket.on_disconnect")
168114
defp on_disconnect(exit_reason, socket)
169115

0 commit comments

Comments
 (0)