Skip to content

Commit

Permalink
Allow pinned device certs without a registered CA
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jjcarstens committed Aug 4, 2022
1 parent 7e809c1 commit 92bcbd8
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 2 deletions.
8 changes: 6 additions & 2 deletions apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 92bcbd8

Please sign in to comment.