From 92bcbd8b3f3b1ae211ae72b154cc8ac9ce93db14 Mon Sep 17 00:00:00 2001 From: Jon Carstens Date: Fri, 1 Jul 2022 11:14:18 -0600 Subject: [PATCH] Allow pinned device certs without a registered CA Fixes #838 We were previously requiring that the signer CA to be registered even if the device certificate was already pinned, which was incorrect. This fixes that to skip the check if we're being presented with a signer CA so that validation of the device certificate can happen. If the device cert is pinned, the check will pass. If not, it will go through the normal validation flow and fail due to an expired, unregistered signer CA anyway --- .../lib/nerves_hub_device/ssl.ex | 8 ++- .../channels/websocket_test.exs | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex b/apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex index 2837bd7fe..b14107c19 100644 --- a/apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex +++ b/apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex @@ -43,13 +43,17 @@ defmodule NervesHubDevice.SSL do # This can happen if the Signer CA is not included in the request # and only the device cert/key is. Or if some other unknown CA # was included. - def verify_fun(otp_cert, {:bad_cert, :unknown_ca}, state) do + def verify_fun(otp_cert, {:bad_cert, err}, state) when err in [:unknown_ca, :cert_expired] do aki = Certificate.get_aki(otp_cert) ski = Certificate.get_ski(otp_cert) if aki == ski do # Because Signer CAs are required to be registered first, we don't - # really care about it coming in here + # really care about it coming in here. Likewise, if this is an + # unregistered CA, we can just move on so that the device can + # still attempt to present it's certificate to check if it has been + # pinned or not. Veririfcation will fail there if the device cert + # and it's signer CA is unknown {:valid, state} else do_verify(otp_cert, state) diff --git a/apps/nerves_hub_device/test/nerves_hub_device_web/channels/websocket_test.exs b/apps/nerves_hub_device/test/nerves_hub_device_web/channels/websocket_test.exs index 51a36d537..ce0d88ed3 100644 --- a/apps/nerves_hub_device/test/nerves_hub_device_web/channels/websocket_test.exs +++ b/apps/nerves_hub_device/test/nerves_hub_device_web/channels/websocket_test.exs @@ -118,6 +118,70 @@ defmodule NervesHubDeviceWeb.WebsocketTest do refute_receive({"presence_diff", _}) Socket.stop(socket) end + + test "already registered expired certificate without signer CA can connect", %{user: user} do + org = Fixtures.org_fixture(user, %{name: "custom_ca_test"}) + {device, _firmware} = device_fixture(user, %{identifier: @valid_serial}, org) + + ca_key = X509.PrivateKey.new_ec(:secp256r1) + ca = X509.Certificate.self_signed(ca_key, "CN=#{org.name}", template: :root_ca) + serial = NervesHubWebCore.Certificate.get_serial_number(ca) + + # Ensure this signer CA does not exist in the DB + assert {:error, :not_found} = Devices.get_ca_certificate_by_serial(serial) + + key = X509.PrivateKey.new_ec(:secp256r1) + + not_before = Timex.now() |> Timex.shift(days: -2) + not_after = Timex.now() |> Timex.shift(days: -1) + + cert = + key + |> X509.PublicKey.derive() + |> X509.Certificate.new("CN=#{device.identifier}", ca, ca_key, + validity: X509.Certificate.Validity.new(not_before, not_after) + ) + + # Verify our cert is indeed expired + assert {:error, {:bad_cert, :cert_expired}} = + :public_key.pkix_path_validation( + X509.Certificate.to_der(ca), + [X509.Certificate.to_der(cert)], + [] + ) + + _ = Fixtures.device_certificate_fixture(device, cert) + + nerves_hub_ca_cert = + Path.expand("../../test/fixtures/ssl/ca.pem") + |> File.read!() + |> X509.Certificate.from_pem!() + + opts = [ + url: "wss://127.0.0.1:#{@device_port}/socket/websocket", + serializer: Jason, + ssl_verify: :verify_peer, + transport_opts: [ + socket_opts: [ + cert: X509.Certificate.to_der(cert), + key: {:ECPrivateKey, X509.PrivateKey.to_der(key)}, + cacerts: [X509.Certificate.to_der(ca), X509.Certificate.to_der(nerves_hub_ca_cert)], + server_name_indication: 'device.nerves-hub.org' + ] + ] + ] + + {:ok, socket} = Socket.start_link(opts) + wait_for_socket(socket) + {:ok, _reply, _channel} = Channel.join(socket, "device") + + device = + NervesHubWebCore.Repo.get(Device, device.id) + |> NervesHubWebCore.Repo.preload(:org) + + assert Presence.device_status(device) == "online" + refute_receive({"presence_diff", _}) + end end describe "firmware update" do