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 Jul 1, 2022
1 parent 28a9687 commit 9d4d6ad
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 16 deletions.
22 changes: 6 additions & 16 deletions apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 9d4d6ad

Please sign in to comment.