Skip to content

Commit 234efcf

Browse files
committed
Add support for cert auth, including on_connect logic
1 parent 8644397 commit 234efcf

File tree

3 files changed

+73
-63
lines changed

3 files changed

+73
-63
lines changed

lib/nerves_hub/devices.ex

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

104+
def get_device_by_x509(cert) do
105+
fingerprint = NervesHub.Certificate.fingerprint(cert)
106+
107+
Device
108+
|> join(:inner, [d], dc in assoc(d, :device_certificates))
109+
|> where([d, dc], dc.fingerprint == ^fingerprint)
110+
|> Repo.one()
111+
|> case do
112+
nil -> {:error, :not_found}
113+
certificate -> {:ok, certificate}
114+
end
115+
end
116+
104117
@spec filter(Product.t(), map()) :: %{
105118
entries: list(Device.t()),
106119
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: 10 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ defmodule NervesHubWeb.DeviceSocket do
44

55
require Logger
66

7-
alias NervesHub.Devices
87
alias NervesHub.Devices.Connections
9-
alias NervesHub.Devices.DeviceConnection
8+
109
alias NervesHub.Tracker
1110

1211
alias NervesHub.RPC.DeviceAuth
@@ -17,13 +16,6 @@ defmodule NervesHubWeb.DeviceSocket do
1716

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

20-
@impl Phoenix.Socket.Transport
21-
def init(state_tuple) do
22-
{:ok, {state, socket}} = super(state_tuple)
23-
socket = on_connect(socket)
24-
{:ok, {state, socket}}
25-
end
26-
2719
@impl Phoenix.Socket.Transport
2820
@decorate with_span("Channels.DeviceSocket.terminate")
2921
def terminate(reason, {_channels_info, socket} = state) do
@@ -70,40 +62,25 @@ defmodule NervesHubWeb.DeviceSocket do
7062
@decorate with_span("Channels.DeviceSocket.connect")
7163
def connect(_params, socket, %{peer_data: %{ssl_cert: ssl_cert}})
7264
when not is_nil(ssl_cert) do
73-
X509.Certificate.from_der!(ssl_cert)
74-
|> Devices.get_device_by_x509()
75-
|> case do
76-
{:ok, device} ->
77-
socket_and_assigns(socket, device)
65+
case DeviceAuth.connect_device({:ssl_certs, ssl_cert}) do
66+
{:ok, ref_and_device} ->
67+
socket_and_assigns(socket, ref_and_device)
7868

7969
error ->
80-
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
81-
auth: :cert,
82-
reason: error
83-
})
84-
85-
{:error, :invalid_auth}
70+
error
8671
end
8772
end
8873

8974
# Used by Devices connecting with HMAC Shared Secrets
9075
@decorate with_span("Channels.DeviceSocket.connect")
9176
def connect(_params, socket, %{x_headers: x_headers})
9277
when is_list(x_headers) and length(x_headers) > 0 do
93-
headers = Map.new(x_headers)
94-
9578
case DeviceAuth.connect_device({:shared_secrets, x_headers}) do
96-
{:ok, device} ->
97-
socket_and_assigns(socket, Devices.preload_product(device))
79+
{:ok, ref_and_device} ->
80+
socket_and_assigns(socket, ref_and_device)
9881

9982
error ->
100-
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
101-
auth: :shared_secrets,
102-
reason: error,
103-
product_key: Map.get(headers, "x-nh-key", "*empty*")
104-
})
105-
106-
{:error, :invalid_auth}
83+
error
10784
end
10885
end
10986

@@ -125,44 +102,15 @@ defmodule NervesHubWeb.DeviceSocket do
125102
]
126103
end
127104

128-
defp socket_and_assigns(socket, device) do
129-
# disconnect devices using the same identifier
130-
_ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{})
131-
105+
defp socket_and_assigns(socket, {ref_id, device}) do
132106
socket =
133107
socket
134108
|> assign(:device, device)
109+
|> assign(:reference_id, ref_id)
135110

136111
{:ok, socket}
137112
end
138113

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

0 commit comments

Comments
 (0)