Skip to content

Commit

Permalink
Account for expired CA certificates in requests
Browse files Browse the repository at this point in the history
With OTP 24, an expired CA in the chain needs to be handled explicitly. In our case,
an expired CA may still be presented in the chain for already registered device certs
and CA certs in which we should still allow the connection.

This change adds a lookup for the expired certificate to see if it is a CA we
already know about then allows the connection if it is. This should be safe as
any attempts to use this expired CA will still fail later one once the new device
certificate is presented and goes through validation
  • Loading branch information
jjcarstens committed Mar 31, 2022
1 parent 9a24e97 commit e67f4a6
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 4 deletions.
14 changes: 14 additions & 0 deletions apps/nerves_hub_device/lib/nerves_hub_device/ssl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ defmodule NervesHubDevice.SSL do
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 @@ -279,12 +279,71 @@ defmodule NervesHubDeviceWeb.WebsocketTest do
refute_receive({"presence_diff", _})
end

test "already registered expired certificate can connect", %{user: user} do
org = Fixtures.org_fixture(user, %{name: "custom_ca_test"})
{device, _firmware} = device_fixture(user, %{identifier: @valid_serial}, org)

%{cert: ca, key: ca_key} = Fixtures.ca_certificate_fixture(org)

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

test "vaild certificate expired signer can connect", %{user: user} do
org = Fixtures.org_fixture(user, %{name: "custom_ca_test"})
{device, _firmware} = device_fixture(user, %{identifier: @valid_serial}, org)

not_before = Timex.now() |> Timex.shift(days: -1)
not_after = Timex.now() |> Timex.shift(days: 1)
not_before = Timex.now() |> Timex.shift(days: -2)
not_after = Timex.now() |> Timex.shift(days: -1)

template =
X509.Certificate.Template.new(:root_ca,
Expand Down Expand Up @@ -321,8 +380,6 @@ defmodule NervesHubDeviceWeb.WebsocketTest do
]
]

:timer.sleep(2_000)

{:ok, socket} = Socket.start_link(opts)
wait_for_socket(socket)
{:ok, _reply, _channel} = Channel.join(socket, "device")
Expand Down

0 comments on commit e67f4a6

Please sign in to comment.