nerves-hub / nerves_hub_web

Manage firmware updates for Nerves devices
https://nerves-hub.org/
Apache License 2.0
190 stars 69 forks source link

Fatal - Unknown CA on rpi4 device with local hub web. #1155

Closed edwardzhou closed 10 months ago

edwardzhou commented 10 months ago

Environment MacOSX 14.1 (23B74)

Elixir 1.15.7-otp-26 Erlang 26.1.2 nerves 1.10.4 nerves_system_rpi4 1.24.1 nerves_hub_link 2.0.0

Nerves_hub_web

git clone https://github.com/nerves-hub/nerves_hub_web.git
cd nerves_hub_web

vi config/dev.exs , configure ssl

ssl_dir =
  (System.get_env("NERVES_HUB_CA_DIR") || Path.join([__DIR__, "../test/fixtures/ssl/"]))
  # (System.get_env("NERVES_HUB_CA_DIR") || Path.join([__DIR__, "../test/fixtures/certs/"]))
  |> Path.expand()

##
# NervesHub Device
#
config :nerves_hub, NervesHubWeb.DeviceEndpoint,
  debug_errors: true,
  code_reloader: false,
  check_origin: false,
  watchers: [],
  https: [
    ip: {0, 0, 0, 0},
    port: 4001,
    otp_app: :nerves_hub,
    thousand_island_options: [
      transport_options: [
        # Enable client SSL
        # Older versions of OTP 25 may break using using devices
        # that support TLS 1.3 or 1.2 negotiation. To mitigate that
        # potential error, we enforce TLS 1.2. If you're using OTP >= 25.1
        # on all devices, then it is safe to allow TLS 1.3 by removing
        # the versions constraint and setting `certificate_authorities: false`
        # See https://github.com/erlang/otp/issues/6492#issuecomment-1323874205
        #
        # certificate_authorities: false,
        versions: [:"tlsv1.2"],
        verify: :verify_peer,
        verify_fun: {&NervesHub.SSL.verify_fun/3, nil},
        fail_if_no_peer_cert: true,
        keyfile: Path.join(ssl_dir, "device.nerves-hub.org-key.pem"),
        certfile: Path.join(ssl_dir, "device.nerves-hub.org.pem"),
        cacertfile: Path.join(ssl_dir, "ca.pem")
      ]
    ]
  ]

device-1234-cert.pem and device-1234-key.pem and device-root-ca.pem were copied to rpi4 project, config.exs as following

config :nerves, :firmware,
  provisioning: :nerves_hub_link

config :nerves_hub_link,
  fwup_public_keys: [:devkey]

config :nerves_hub_link,
  fwup_public_keys: [
    # devkey
    "EB/Q9IwokYCYuPTnhYyRX9RCnmi+eTlIdTou5/pE8RI"
  ]

config :nerves_hub_link,
  socket: [
    json_lisbrary: Jason,
    heartbeat_interval: 45_000
  ],
  ssl: [
    certfile: "/certs/device-1234-cert.pem",
    keyfile: "/certs/device-1234-key.pem",
    cacertfile: "/certs/rootCACert.pem",
    server_name_indication: ~c"device.nerves-hub.org"
  ]

config :nerves_hub_link,
  device_api_host: "192.168.5.105",
  device_api_sni: '192.168.5.105',
  device_api_port: 4001

but got warning on both local nerves-hub-web and device. nerves-hub-web log:

iex -S mix phx.server

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

[notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA

rpi4 device log:


$ ssh nerves.local
Interactive Elixir (1.15.7) - press Ctrl+C to exit (type h() ENTER for help)
████▄▄    ▐███
█▌  ▀▀██▄▄  ▐█
█▌  ▄▄  ▀▀  ▐█   N  E  R  V  E  S
█▌  ▀▀██▄▄  ▐█
███▌    ▀▀████
dms_firmware 0.1.0 (76e96f91-ab3d-53b2-34ce-dfe2f911cc1b) arm rpi4
  Serial       : 0c0c
  Uptime       : 22.162 seconds
  Clock        : 2023-12-19 14:32:47 UTC (unsynchronized)
  Temperature  : 53.1°C

  Firmware     : Valid (A)               Applications : 110 started
  Memory usage : 207 MB (3%)             Part usage   : 910 MB (3%)
  Hostname     : nerves-0c0c             Load average : 0.20 0.05 0.02

  eth0         : 192.168.0.210/16
  wlan0        : 192.168.5.253/24, fe80::dea6:32ff:fedc:54f0/64
  usb0         : 172.31.136.77/30

Nerves CLI help: https://hexdocs.pm/nerves/iex-with-nerves.html

Toolshed imported. Run h(Toolshed) for more info.

14:32:49.831 [info] Description: ~c"Options [cacertfile] are ignored"
     Reason: ~c"Option cacerts is set"

14:32:49.862 [info] TLS :client: In state :certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA

14:32:49.864 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:unknown_ca, ~c"TLS client: In state certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA\n"}}}}

14:32:54.989 [info] Description: ~c"Options [cacertfile] are ignored"
     Reason: ~c"Option cacerts is set"

14:32:55.059 [info] TLS :client: In state :certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA

14:32:55.060 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:unknown_ca, ~c"TLS client: In state certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA\n"}}}}
** (EXIT) interrupted
** (EXIT) interrupted
iex(1)> exit
Connection to nerves.local closed.
joshk commented 10 months ago

This doesn't directly answer your question, but https://github.com/nerves-hub/nerves_hub_web/pull/1139 and https://github.com/nerves-hub/nerves_hub_link/pull/141 might be a better localhost experience.

The PRs should be merged in within the coming weeks with the focus on an easier, quicker, and more straight forward experience for running smaller hub installs.

jjcarstens commented 10 months ago

This isn't neccessarily a problem with NervesHub, but rather configuration. There are a few things to note here:

# In IEx or some other place. This maybe need to be IO.inspect/1 and printed so it can be copied to config
cert = File.read!("device-1234-cert.pem") |> X509.Certificate.from_pem!() |> X509.Certificate.to_der()
key_der = File.read!("device-1234-key.pem") |> X509.PrivateKey.from_pem!() |> X509.PrivateKey.to_der()
cacerts = File.read!("ca.pem") |> X509.from_pem() |> Enum.map(&X509.Certificate.to_der/1)

# in your config of rpi4
config :nerves_hub_link,
  ssl: [cert: cert, key: {:ECPrivateKey, key_der}, cacerts: cacerts]
edwardzhou commented 10 months ago

May I know the script of creating ceritifications which in test/fixtures/ssl?

I will shadow the steps to trying to create my own.

guillego commented 10 months ago

I've also been a bit stuck here for some weeks. I made a Mix task for converting certificates to DER (not the prettiest code 😅):

defmodule Mix.Tasks.Device.Certs do
  use Mix.Task

  @shortdoc "Generate DER certs for device"
  @ssl_path "rootfs_overlay/etc/ssl"

  @spec process_args([String.t()]) :: {:ok, %{ca: String.t(), key: String.t(), cert: String.t()}} | {:error, String.t()}
  defp process_args(args) do
    options = OptionParser.parse(args, switches: [ca: :string, key: :string, cert: :string])

    case options do
      {[ca: ca_path, key: key_path, cert: cert_path], _, _} when is_binary(ca_path) and is_binary(key_path) and is_binary(cert_path) ->
        {:ok, %{ca: ca_path, key: key_path, cert: cert_path}}

      _ ->
        {:error, "Invalid arguments. Please specify --ca, --key, and --cert paths correctly."}
    end
  end

  @spec run([String.t()]) :: :ok | {:error, String.t()}
  def run(args) do
    with {:ok, paths} <- process_args(args),
         :ok <- convert_and_save(paths.ca, "#{@ssl_path}/ca.der", :cert),
         :ok <- convert_and_save(paths.key, "#{@ssl_path}/key.der", :key),
         :ok <- convert_and_save(paths.cert, "#{@ssl_path}/cert.der", :cert) do
      IO.puts("DER certificates generated successfully.")
      :ok
    else
      {:error, message} ->
        Mix.raise(message)
    end
  end

  @spec convert_and_save(String.t(), String.t(), :cert | :key) :: :ok | {:error, String.t()}
  defp convert_and_save(input_path, output_path, type) do
    try do
      # Remember parent directories
      :ok = File.mkdir_p(Path.dirname(output_path))

      pem = File.read!(input_path)
      der = case type do
        :cert -> pem |> X509.Certificate.from_pem!() |> X509.Certificate.to_der()
        :key -> pem |> X509.PrivateKey.from_pem!() |> X509.PrivateKey.to_der()
      end

      File.write!(output_path, der)
      :ok
    rescue
      e in File.Error ->
        {:error, "File error for #{input_path}: #{e.message}"}
      e in X509.DecodeError ->
        {:error, "X509 conversion error for #{input_path}: #{e.message}"}
    end
  end

end

So I ran this which converted the certificates to DER and saved them to my rootfs_overlay/etc/ssl directory:

mix device.certs --ca /Users/guille/repos/nerves_hub_web/test/fixtures/ssl/ca.pem --key /Users/guille/repos/nerves_hub_web/test/fixtures/ssl/device-1234-key.pem --cert /Users/guille/repos/nerves_hub_web/test/fixtures/ssl/device-1234-cert.pem

In my config.exs:

base_path = "rootfs_overlay/etc/ssl"
cert = File.read!("#{base_path}/cert.der")
key_der = File.read!("#{base_path}/key.der")
cacerts = File.read!("#{base_path}/ca.der")

config :nerves_hub_link,
remote_iex: true,
socket: [
  json_library: Jason,
  heartbeat_interval: 45_000
  ],
  ssl: [cert: cert, key: {:ECPrivateKey, key_der}, cacerts: cacerts],
  device_api_host: "192.168.1.163",
  device_api_sni: '192.168.1.163',
  device_api_port: 4001

I compiled and flashed the firmware but when the Device starts I see the following error. It seems like it can't properly handle the binary stream from the DER cert/key.

21:45:15.177 [warn] [NervesHubLink] No CA store or :cacerts have been specified. Request will fail

21:45:15.178 [error] GenServer #PID<0.2191.0> terminating
** (Protocol.UndefinedError) protocol Enumerable not implemented for <<48, 130, 1, 168, 48, 130, 1, 77, 160, 3, 2, 1, 2, 2, 21, 0, 136, 78, 75, 120, 25, 130, 154, 40, 67, 250, 98, 145, 1, 28, 77, 163, 110, 156, 2, 100, 48, 10, 6, 8, 42, 134, 72, 206, 61, 4, 3, 2, 48, 48, ...>> of type BitString. This protocol is implemented for the following type(s): CircularBuffer, Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, List, Map, MapSet, Range, Stream
    (elixir 1.14.5) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.14.5) lib/enum.ex:166: Enumerable.reduce/3
    (elixir 1.14.5) lib/enum.ex:4307: Enum.map/2
    (mint 1.5.2) lib/mint/core/transport/ssl.ex:568: Mint.Core.Transport.SSL.add_partial_chain_fun/1
    (mint 1.5.2) lib/mint/core/transport/ssl.ex:443: Mint.Core.Transport.SSL.add_verify_opts/2
    (mint 1.5.2) lib/mint/core/transport/ssl.ex:432: Mint.Core.Transport.SSL.ssl_opts/2
    (mint 1.5.2) lib/mint/core/transport/ssl.ex:328: Mint.Core.Transport.SSL.connect/4
    (mint 1.5.2) lib/mint/http1.ex:133: Mint.HTTP1.connect/4
Last message: {:continue, :connect}
State: %Slipstream.Connection.State{connection_id: "a95e90b20b1c204c", trace_id: "5a184d4b2cdb5e4635c60e82050683d2", client_pid: #PID<0.2190.0>, client_ref: #Reference<0.902908580.3528196100.212553>, config: %Slipstream.Configuration{uri: %URI{scheme: "wss", authority: "192.168.1.163:4001", userinfo: nil, host: "192.168.1.163", port: 4001, path: "/socket/websocket", query: nil, fragment: nil}, heartbeat_interval_msec: 30000, headers: [], serializer: Slipstream.Serializer.PhoenixSocketV2Serializer, json_parser: Jason, reconnect_after_msec: [1096, 2036, 5935, 11595, 21504, 32890, 62803], rejoin_after_msec: [5000], mint_opts: [protocols: [:http1], transport_opts: [server_name_indication: '192.168.1.163', versions: [:"tlsv1.2"], verify: :verify_peer, cert: <<48, 130, 1, 213, 48, 130, 1, 123, 160, 3, 2, 1, 2, 2, 21, 0, 183, 38, 240, 145, 34, 215, 157, 121, 125, 226, 176, 62, 76, 89, 115, ...>>, key: {:ECPrivateKey, <<48, 119, 2, 1, 1, 4, 32, 76, 68, 12, 57, 248, 89, 250, 122, 82, 104, 126, 255, 253, 167, 150, 132, 14, 185, 177, 93, 146, ...>>}, cacerts: <<48, 130, 1, 168, 48, 130, 1, 77, 160, 3, 2, 1, 2, 2, 21, 0, 136, 78, 75, 120, 25, 130, 154, 40, 67, 250, 98, 145, 1, ...>>]]
guillego commented 10 months ago

Ok saw my error now:

config.exs

  ssl: [cert: cert, key: {:ECPrivateKey, key_der}, cacerts: [cacerts]],

Seems like cacerts needs to be a list!

Now I get the following error:

22:31:25.764 [info] TLS :client: In state :certify at ssl_handshake.erl:2140 generated CLIENT ALERT: Fatal - Handshake Failure
 - {:bad_cert, :hostname_check_failed}

22:31:25.766 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:handshake_failure, 'TLS client: In state certify at ssl_handshake.erl:2140 generated CLIENT ALERT: Fatal - Handshake Failure\n {bad_cert,hostname_check_failed}'}}}}

and if I set

device_api_sni: ~c"device.nerves-hub.org",

I get

22:42:35.499 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:handshake_failure, 'TLS client: In state cipher received SERVER ALERT: Fatal - Handshake Failure\n'}}}}
jjcarstens commented 10 months ago

@guillego ca.pem is a few certs compiled into one file. You're effectively giving one giant cacerts DER binary but it should be broken up into their individual CA DERs and then included in the list.

Or simply change your ca.pem to be the single top level root Ca which I think should work as well

guillego commented 10 months ago

That makes a lot more sense! Refactored my CA cert conversion and loading:

On my mix task

  @spec process_ca_file(String.t(), String.t()) :: :ok | {:error, String.t()}
  defp process_ca_file(input_path, output_dir) do
    try do
      :ok = File.mkdir_p(output_dir)

      input_path
      |> File.read!()
      |> String.split("\n\n", trim: true)
      |> Enum.with_index(1)
      |> Enum.map(fn {cert, index} ->
        {Certificate.pem_to_der(cert), "cacert_#{pad_zero(index, 3)}.der"}
      end)
      |> Enum.each(fn {der, filename} -> File.write!(Path.join(output_dir, filename), der) end)

      :ok
    rescue
      e in [File.Error, X509.DecodeError] ->
        {:error, "Error processing CA file: #{e.message}"}
    end
  end

config.exs

cacerts = "#{base_path}/nerves_hub_ca/*.der" |> Path.wildcard() |> Enum.map(&File.read!(&1))

However I still get a Handshake failure. I'll keep investigating, seems to be closer now thanks for all the help!

08:40:44.945 [info] TLS :client: In state :cipher received SERVER ALERT: Fatal - Handshake Failure

08:40:44.946 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:handshake_failure, 'TLS client: In state cipher received SERVER ALERT: Fatal - Handshake Failure\n'}}}}

@edwardzhou hope my code helps!

guillego commented 10 months ago

Hello! Hope you've all been having great Christmas days! 🎄

I've been trying different things around this over the past few days and can't figure out what else to do 😅

Starting from scratch, steps I followed:

Device root CA

  1. Generate an EC Private key for device CA: openssl genrsa -des3 -out nerves_hub_dev_ca.key 2048
  2. Sign a device root CA certificate with it: openssl req -x509 -new -nodes -key nerves_hub_dev_ca.key -sha256 -days 1825 -out nerves_hub_dev_rootca.pem
  3. Log into nerveshubweb/MyNewOrg/Certificates and follow instructions to generate a verificationCert and upload that and the CA certificate. image

Device

  1. Generate a device private key openssl ecparam -name prime256v1 -genkey -noout -out device-40928a.pem
  2. Create a device certificate signing request openssl req -new -key device-40928a.pem -out device-40928a.csr
  3. Self sign my device certificate openssl x509 -req -in device-40928a.csr -CA nerves_hub_dev_rootca.pem -CAkey nerves_hub_dev_ca.key -CAcreateserial -out device-40928a.crt -days 825 -sha256
  4. Convert the PEM key, the device certificate and the root CA certificate to DER format (using mix task described in earlier comments, did it also with openssl just in case but the result was the same).
  5. Load those into my config.exs:
    
    base_path = "rootfs_overlay/etc/ssl"

cert = File.read!("#{base_path}/nerves_hub_cert.der") key_der = File.read!("#{base_path}/nerves_hub_key.der") cacerts = "#{base_path}/nerves_hub_ca/*.der" |> Path.wildcard() |> Enum.map(&File.read!(&1))

config :nerves_hub_link, remote_iex: true, socket: [ json_library: Jason, heartbeat_interval: 45_000 ], cacerts: cacerts, ssl: [cert: cert, key: {:ECPrivateKey, key_der}, cacerts: cacerts, log_level: :debug], device_api_host: "192.168.1.108", device_api_sni: ~c"device.nerves-hub.org", device_api_port: 4001

(There's only one CA cert here, the one I created in step 2 of the Device root CA section).

6. I run mix firmware and mix burn and start my device
7. See the following errors

15:37:08.379 [info] TLS :client: In state :certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA

15:37:08.381 [warn] [NervesHubLink] error: {:error, %Mint.TransportError{reason: {:tls_alert, {:unknown_ca, 'TLS client: In state certify at ssl_handshake.erl:2138 generated CLIENT ALERT: Fatal - Unknown CA\n'}}}}

And in my nervesHubWeb:

0000 - 15 03 03 00 02 02 30 ......0 [notice] TLS :server: In state :certify received CLIENT ALERT: Fatal - Unknown CA


So it seems that even though I registered my CA it doesn't recognize it.

From what I understand I would also need to register my device into nerves hub with a command like this:

mix nerves_hub.device create --identifier 4917bafd-cb8d-4667-b6d1-1dfd1b40928a --description TestDevice --tag dev


However I can't run any of the nerves_hub_cli commands without errors, for instance this one:

NervesHub server: localhost:4001 (same error in port 4000, not sure which one I should connect to) NervesHub organization: MyNewOrg ** (ArgumentError) errors were found at the given arguments:

Is nerves_hub_cli still the way to do these things?

jjcarstens commented 10 months ago

@guillego SSL is vague by design. The Unknown CA could be referencing the device cert, signer CA, or even the NervesHub server CA which is unknown.

If your end goal here is just to get a device connected to a local instance, then I would abandon writing files and hardcode DER values directly into the application config and make the firmware. Below is a configuration which successfully connects to a fresh NervesHub instance for device-1234-cert.pem.

It also has the config needed to use nerves_hub_cli with the same local instance on the unauthenticated web port 4000. First run mix nerves_hub.user auth to create the token. Then you can run the mix tasks. (Note that you should use nerves_hub_cli main branch as it is undergoing lots of cleanup and the latest hex releases may work differently)

dev.exs

```elixir # Device HTTP connection. config :nerves_hub_link, device_api_host: "192.168.1.226", device_api_port: 4001, device_api_sni: ~c"device.nerves-hub.org", configurator: NervesHubLink.Configurator.Default, ssl: [ cert: # nerves_hub_web/test/fixtures/ssl/device-1234-cert.pem <<48, 130, 1, 213, 48, 130, 1, 123, 160, 3, 2, 1, 2, 2, 21, 0, 183, 38, 240, 145, 34, 215, 157, 121, 125, 226, 176, 62, 76, 89, 115, 14, 180, 240, 210, 177, 48, 10, 6, 8, 42, 134, 72, 206, 61, 4, 3, 2, 48, 55, 49, 18, 48, 16, 6, 3, 85, 4, 10, 12, 9, 78, 101, 114, 118, 101, 115, 72, 117, 98, 49, 33, 48, 31, 6, 3, 85, 4, 3, 12, 24, 78, 101, 114, 118, 101, 115, 72, 117, 98, 32, 68, 101, 118, 105, 99, 101, 32, 82, 111, 111, 116, 32, 67, 65, 48, 32, 23, 13, 50, 50, 49, 50, 49, 53, 50, 48, 48, 48, 48, 48, 90, 24, 15, 50, 48, 53, 50, 49, 50, 49, 53, 50, 49, 48, 48, 48, 48, 90, 48, 39, 49, 15, 48, 13, 6, 3, 85, 4, 10, 12, 6, 106, 111, 110, 106, 111, 110, 49, 20, 48, 18, 6, 3, 85, 4, 3, 12, 11, 100, 101, 118, 105, 99, 101, 45, 49, 50, 51, 52, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 203, 146, 85, 193, 252, 4, 13, 190, 69, 108, 79, 5, 0, 238, 0, 150, 97, 194, 97, 148, 60, 5, 220, 228, 181, 23, 149, 150, 19, 250, 207, 220, 46, 251, 233, 10, 167, 173, 237, 143, 129, 168, 183, 54, 40, 130, 5, 95, 211, 44, 224, 18, 184, 28, 255, 250, 151, 168, 205, 188, 155, 121, 115, 234, 163, 114, 48, 112, 48, 9, 6, 3, 85, 29, 19, 4, 2, 48, 0, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, 3, 2, 5, 160, 48, 19, 6, 3, 85, 29, 37, 4, 12, 48, 10, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 250, 22, 188, 70, 156, 245, 153, 172, 82, 156, 167, 111, 51, 189, 53, 17, 130, 101, 195, 114, 48, 31, 6, 3, 85, 29, 35, 4, 24, 48, 22, 128, 20, 202, 167, 15, 94, 238, 102, 229, 201, 146, 85, 31, 89, 159, 116, 230, 28, 193, 141, 15, 126, 48, 10, 6, 8, 42, 134, 72, 206, 61, 4, 3, 2, 3, 72, 0, 48, 69, 2, 33, 0, 181, 8, 140, 199, 123, 44, 193, 1, 103, 203, 56, 157, 172, 91, 213, 17, 118, 14, 81, 86, 226, 246, 235, 192, 88, 77, 110, 85, 7, 97, 156, 159, 2, 32, 28, 83, 169, 142, 175, 203, 226, 223, 118, 41, 0, 168, 100, 174, 26, 94, 82, 7, 122, 217, 90, 199, 35, 224, 24, 91, 224, 194, 115, 38, 168, 12>>, key: # nerves_hub_web/test/fixtures/ssl/device-1234-key.pem {:ECPrivateKey, <<48, 119, 2, 1, 1, 4, 32, 76, 68, 12, 57, 248, 89, 250, 122, 82, 104, 126, 255, 253, 167, 150, 132, 14, 185, 177, 93, 146, 77, 112, 213, 83, 106, 244, 176, 255, 142, 141, 139, 160, 10, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 161, 68, 3, 66, 0, 4, 203, 146, 85, 193, 252, 4, 13, 190, 69, 108, 79, 5, 0, 238, 0, 150, 97, 194, 97, 148, 60, 5, 220, 228, 181, 23, 149, 150, 19, 250, 207, 220, 46, 251, 233, 10, 167, 173, 237, 143, 129, 168, 183, 54, 40, 130, 5, 95, 211, 44, 224, 18, 184, 28, 255, 250, 151, 168, 205, 188, 155, 121, 115, 234>>}, cacerts: [ # nerves_hub_web/test/fixtures/ssl/root-ca.pem <<48, 130, 1, 168, 48, 130, 1, 77, 160, 3, 2, 1, 2, 2, 21, 0, 136, 78, 75, 120, 25, 130, 154, 40, 67, 250, 98, 145, 1, 28, 77, 163, 110, 156, 2, 100, 48, 10, 6, 8, 42, 134, 72, 206, 61, 4, 3, 2, 48, 48, 49, 18, 48, 16, 6, 3, 85, 4, 10, 12, 9, 78, 101, 114, 118, 101, 115, 72, 117, 98, 49, 26, 48, 24, 6, 3, 85, 4, 3, 12, 17, 78, 101, 114, 118, 101, 115, 72, 117, 98, 32, 82, 111, 111, 116, 32, 67, 65, 48, 32, 23, 13, 50, 50, 49, 50, 49, 53, 50, 48, 48, 48, 48, 48, 90, 24, 15, 50, 48, 53, 50, 49, 50, 49, 53, 50, 49, 48, 48, 48, 48, 90, 48, 48, 49, 18, 48, 16, 6, 3, 85, 4, 10, 12, 9, 78, 101, 114, 118, 101, 115, 72, 117, 98, 49, 26, 48, 24, 6, 3, 85, 4, 3, 12, 17, 78, 101, 114, 118, 101, 115, 72, 117, 98, 32, 82, 111, 111, 116, 32, 67, 65, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 33, 39, 185, 125, 50, 219, 186, 66, 209, 30, 159, 170, 5, 64, 110, 101, 2, 245, 141, 36, 23, 61, 49, 48, 209, 229, 106, 63, 219, 138, 217, 30, 169, 82, 52, 137, 53, 128, 210, 191, 214, 96, 50, 143, 100, 172, 111, 178, 71, 24, 51, 5, 234, 162, 180, 126, 62, 197, 139, 170, 214, 133, 69, 169, 163, 66, 48, 64, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, 3, 2, 1, 6, 48, 15, 6, 3, 85, 29, 19, 1, 1, 255, 4, 5, 48, 3, 1, 1, 255, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 158, 135, 68, 248, 62, 94, 43, 114, 152, 1, 154, 172, 147, 15, 142, 166, 114, 84, 48, 100, 48, 10, 6, 8, 42, 134, 72, 206, 61, 4, 3, 2, 3, 73, 0, 48, 70, 2, 33, 0, 238, 75, 105, 140, 209, 248, 171, 34, 40, 179, 2, 88, 96, 243, 213, 4, 205, 35, 209, 128, 137, 123, 133, 89, 112, 217, 100, 87, 114, 188, 174, 50, 2, 33, 0, 244, 57, 176, 222, 148, 112, 98, 16, 243, 98, 21, 199, 146, 211, 94, 79, 3, 252, 150, 56, 184, 214, 104, 0, 36, 215, 131, 249, 109, 243, 69, 165>> ] ] config :nerves_hub_cli, home_dir: Path.expand(".nerves-hub"), host: "192.168.1.226", port: 4000, scheme: "http" ```

This same config was used in nerves_hub_link/config/dev.exs and ran localizing with iex -S mix.


The problems that I see with using openssl to generate your own cert is that the entitlements are different/missing than what is typically expected for NervesHub. I'd recommend using the mix tasks for now. These run locally without making requests to a server.

$ git clone git@github.com:nerves-hub/nerves_key && cd nerves_key
$ mix nerves_key.signer create your-signer --years-valid 20

$ cd nerves_hub_web
$ mix nerves_hub.device create --identifier poser --tag poser --description "It's a poser device"
$ mix nerves_hub.device cert create poser --signer-cert nerves-hub/your-signer.cert --signer-key nerves-hub/your-signer.key

From there, you can use the CLI or just manually create the device and upload the device certificate via the web UI (which I think would be easier in your case). Once the device certificate exists in NervesHub, you do not need the signer cert registered or included in the requests.

guillego commented 10 months ago

Thanks a lot @jjcarstens! Makes sense to start with the simplest case by hardcoding the values and once that works, start to change things to a better approach.

I'll let you know how that goes in a bit.

Do I also need to register the root CA certificate in my NervesHubWeb organization like I was trying before?

jjcarstens commented 10 months ago

What you were doing before was registering a signer CA which is only needed for JITP

The root CA fixture was used to create a cert for the DeviceEndpoint. It's just like any other server certificate for a website except that it is self-signed for local testing which is the reason you need to explicitly include it in cacerts of a device connecting

joshk commented 10 months ago

@guillego @edwardzhou I was wondering, do you need to use SSL certs? as in, if there was a way to get this all running without SSL certs, would you be interested?

edwardzhou commented 10 months ago

For my understanding, the original design that nerves-hub using certs here is for security between nerves-hub and trusted-devices. I'm also fine if there is any alternative. because ssl certs is such painful now.

joshk commented 10 months ago

We should have https://github.com/nerves-hub/nerves_hub_web/pull/1139 merged within the next week, possibly sooner.

This setup is great for home use or prototyping. SSL secures the transport, and the websocket connection is secured using an AWS style signature. Plus, just-in-time Device registration is supported out of the box.

This also means it is easier to run NervesHub on a PaaS (eg. I have it running on Fly.io)

This is the NervesHub Link config that goes with it : https://github.com/nerves-hub/nerves_hub_link/pull/141

Certs are still recommended for production deployments, but for most people, where they are just getting started with NervesHub, these new Shared Secrets will make it a hell of a lot easier to get up and running.

guillego commented 10 months ago

@joshk Yes! I think that would be super useful, especially as you say to get it quickly up and running on a PaaS like fly.io! All the recent PRs in NervesHubWeb look amazing and I'm really excited to see them merged and test them!

guillego commented 10 months ago

@jjcarstens Thanks A LOT for the help! I hadn't thought about using nerves_key for generating the signer certificates.

With that I was able to finally generate correct certificates and have a successful connection between my device and my local nerves hub!! 🙌

I wrote my process in this gist. Perhaps with a little more love (and some reviews) it could become documentation? @edwardzhou let me know if this helps!

HOWEVER! I created firmware signing keys and signed/published firmware but I have an error when I try to deploy new firmware versions! I'll be chasing this a bit more but here is the error in case anyone has seen it before:

01:18:28.573 [info] Resuming download attempt number 8 http://localhost:4000/firmware/3/aac59f58-51f1-586e-7f31-699c7df18812.fw

01:18:28.574 [error] [NervesHubLink] Nonfatal HTTP download error: %Mint.TransportError{reason: :econnrefused}
guillego commented 10 months ago

HOWEVER! I created firmware signing keys and signed/published firmware but I have an error when I try to deploy new firmware versions! I'll be chasing this a bit more but here is the error in case anyone has seen it before:

01:18:28.573 [info] Resuming download attempt number 8 http://localhost:4000/firmware/3/aac59f58-51f1-586e-7f31-699c7df18812.fw

01:18:28.574 [error] [NervesHubLink] Nonfatal HTTP download error: %Mint.TransportError{reason: :econnrefused}

Found the issue! It was right in front of my face. Turns out I needed to change the config in nerves_hub_web/dev.exs so that the public path to the firmware is not referenced as localhost but as the actual network hostname of the server:

This is the default config for the Endpoint:

config :nerves_hub, NervesHubWeb.Endpoint,
  url: [
    host: System.get_env("WEB_HOST", "localhost"),
    scheme: System.get_env("WEB_SCHEME", "http"),
    port: String.to_integer(System.get_env("WEB_PORT", "4000"))
  ],
  http: [ip: {0, 0, 0, 0}, port: 4000],

So I just needed to set the env var for WEB_HOST to my LAN IP address. Et voilá it worked!

Thanks for all the help!

edwardzhou commented 10 months ago

HOWEVER! I created firmware signing keys and signed/published firmware but I have an error when I try to deploy new firmware versions! I'll be chasing this a bit more but here is the error in case anyone has seen it before:

01:18:28.573 [info] Resuming download attempt number 8 http://localhost:4000/firmware/3/aac59f58-51f1-586e-7f31-699c7df18812.fw

01:18:28.574 [error] [NervesHubLink] Nonfatal HTTP download error: %Mint.TransportError{reason: :econnrefused}

Found the issue! It was right in front of my face. Turns out I needed to change the config in nerves_hub_web/dev.exs so that the public path to the firmware is not referenced as localhost but as the actual network hostname of the server:

This is the default config for the Endpoint:

config :nerves_hub, NervesHubWeb.Endpoint,
  url: [
    host: System.get_env("WEB_HOST", "localhost"),
    scheme: System.get_env("WEB_SCHEME", "http"),
    port: String.to_integer(System.get_env("WEB_PORT", "4000"))
  ],
  http: [ip: {0, 0, 0, 0}, port: 4000],

So I just needed to set the env var for WEB_HOST to my LAN IP address. Et voilá it worked!

Thanks for all the help!

Great job. I will take a try as well.

guillego commented 10 months ago

The whole setup process is in this gist, not sure if you missed it in the previous message 😄

jjcarstens commented 10 months ago

🎉 !!

Thanks @guillego! I'm going to close this for now as the errors seen are with configuration (and the pains of learning SSL 😢). Thanks for the gist and hopefully we can get some of those more clear instructions into the main repo to aid others as well

guillego commented 10 months ago

Thanks @jjcarstens ! Indeed the pains of learning all the moving pieces of SSL, but it was so rewarding! The issue was opened by @edwardzhou even though it seems indeed from my experience to be a config/docs problem rather than an issue with the repo itself.

Thanks for all the awesome work on Nerves/NervesHub @joshk @jjcarstens, looking forward to gaining a bit more experience to contribute.