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 fe066bf6a..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,33 +43,23 @@ 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) end end - def verify_fun(otp_cert, {:bad_cert, :cert_expired}, state) do - # If the CA is expired but already registered then we should - # still allow it in the request. If this is a device attempting - # to register, the validation will fail later on due to the expired. - # - # If this is an existing device presenting an expired cert in the chain - # then allowing the expired CA cert prevents the request from being - # terminating prematurely - case check_known_ca(otp_cert) do - {:ok, _ca} -> {:valid, state} - _unknown_ca -> :cert_expired - end - end - def verify_fun(_certificate, {:extension, _}, state) do {:valid, state} end 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 67de56dc8..69e90b0a3 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