VintageNet with Wired 802.1x under Nerves

This is some ongoing work I’m doing for [ REDACTED ] (one of my favorite clients, makes me seem very cool and mysterious). I am reporting the progress and procedure here to gives some findable context for others who want to do this stuff and also some context for the PR.

Wired 802.1x using EAP-TLS with device certificates is a pretty decent bump in network security and control, or so I hear.

I’ve confirmed the Linux setup for wired 802.1x using this helpful note and a freeradius server along with a Unifi managed switch. So we have a Raspbian OS install doing the right song and dance.

Currently we are hacking apart VintageNetWiFi and VintageNetEthernet to reproduce the config and setup.

We got the thing working but hit a fairly unexpected snag. No wired driver for wpa_supplicant.

So Add wired driver to WPA Supplicant by tomielee · Pull Request #234 · nerves-project/nerves_system_rpi4 · GitHub should address the immediate need we have and we’re currently building that system to see if it works out. It passes then smell check at least.

@fhunleth is this handled in each system separately or should this go somewhere in nerves_system_br?

1 Like

It worked!

@fhunleth the separate supplicant library we discussed is taking shape here: GitHub - underjord/vintage_net_supplicant: Vintage Net Supplicant

We are not 100% certain about the dividing line between Supplicant and WiFi libraries. Currently we brought a lot of WiFi-stuff over to the Supplicant library, because that worked and it all seemed relevant to the supplicant’s work. But much of it is pointless for the Supplicant under Ethernet. I think that’s fine but let us know what you think.

Plenty of tidying up before you get PRs for WiFi, Ethernet and we consider the Supplicant library ready to move over into the nerves-networking org but it is on it’s way.

Using those forks of VintageNet I’ve had no real issue doing PEAP-MSCHAPv2 over ethernet.

Not my goal.

I want EAP-TLS using NervesKey.

Last run gave some promising results:

{:ok, engine} = NervesKey.PKCS11.load_engine()
{:ok, i2c} = ATECC508A.Transport.I2C.init([])

signer_cert = X509.Certificate.to_der(NervesKey.signer_cert(i2c))
cert = X509.Certificate.to_der(NervesKey.device_cert(i2c))
%{key_id: uri} = NervesKey.PKCS11.private_key(engine, {:i2c, 1})
File.write("/tmp/client_cert.der", cert)
File.write("/tmp/ca_cert.der", signer_cert)

path = Path.join(Application.app_dir(:nerves_key_pkcs11), "priv/nerves_key_pkcs11.so")
VintageNet.configure("eth0", %{
       type: VintageNetEthernet,
       ipv4: %{method: :dhcp},
       vintage_net_ethernet: %{
        wpa_supplicant_conf: """
        pkcs11_module_path="#{path}"
        network={
          key_mgmt=WPA-EAP
          identity="User"
          eap=TLS
          eapol_flags=3
          ca_cert="/tmp/ca_cert.der"
          client_cert="/tmp/client_cert.der"
          private_key="#{uri}"
        }
        """
       }
     })

The wrinkle is that OpenSSL doesn’t seem to have engine support compiled in (curious how Erlang does that differently) so I need to add something to Buildroot:

BR2_PACKAGE_OPENSSL_ENGINES=y

in nerves_defconfig is my theory.

The log was wildly misleading. The big read error seems irrelevant and is probably due to negotiations breaking down somehow. The real stuff is about failing to load and missing engine support by SSL and TLS higher up.

14:14:44.705 [debug] wpa_supplicant: eth0: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=13

14:14:44.706 [debug] WPASupplicant ignoring {:event, "CTRL-EVENT-EAP-PROPOSED-METHOD", %{"method" => "13", "vendor" => "0"}}

14:14:44.707 [debug] wpa_supplicant: OpenSSL: tls_connection_ca_cert - Failed to load root certificates error:05800088:x509 certificate routines::no certificate or crl found

14:14:44.712 [debug] wpa_supplicant: SSL: Configuration uses engine, but engine support was not compiled in

14:14:44.712 [debug] wpa_supplicant: TLS: Failed to load private key

14:14:44.712 [debug] wpa_supplicant: TLS: Failed to set TLS connection parameters

14:14:44.712 [debug] wpa_supplicant: EAP-TLS: Failed to initialize SSL.

14:14:44.713 [debug] wpa_supplicant: eth0: CTRL-REQ-PASSPHRASE-0:Private key passphrase needed for SSID 

14:14:44.713 [debug] wpa_supplicant: eth0: EAP: Failed to initialize EAP method: vendor 0 method 13 (TLS)

14:14:44.715 [debug] WPASupplicant ignoring {:event, "CTRL-EVENT-EAP-STATUS", %{"parameter" => "PASSPHRASE", "status" => "eap parameter needed"}}

14:14:44.715 [error] GenServer {VintageNet.Interface.Registry, {VintageNetSupplicant.WPASupplicant, "eth0"}} terminating
** (MatchError) no match of right hand side value: ["PASSPHRASE", "0:Private key passphrase needed for SSID "]
    (vintage_net_supplicant 0.1.0) lib/vintage_net_supplicant/wpa_supplicant_decoder.ex:18: VintageNetSupplicant.WPASupplicantDecoder.decode_notification/1
    (vintage_net_supplicant 0.1.0) lib/vintage_net_supplicant/wpa_supplicant.ex:247: VintageNetSupplicant.WPASupplicant.handle_info/2
    (stdlib 5.2.3) gen_server.erl:1095: :gen_server.try_handle_info/3
    (stdlib 5.2.3) gen_server.erl:1183: :gen_server.handle_msg/6
    (stdlib 5.2.3) proc_lib.erl:241: :proc_lib.init_p_do_apply/3
Last message: {VintageNetSupplicant.WPASupplicantLL, 3, "CTRL-REQ-PASSPHRASE-0:Private key passphrase needed for SSID "}
State: %{driver: "wired", verbose: false, ll: #PID<0.12619.0>, ifname: "eth0", clients: [], access_points: %{}, peers: [], control_dir: "/tmp/vintage_net/wpa_supplicant", ap_mode: false, keep_alive_interval: 60000, wpa_supplicant: "wpa_supplicant", wpa_supplicant_conf_path: "/tmp/vintage_net/wpa_supplicant.conf.eth0", bssid_requester: #PID<0.12620.0>, current_ap: nil, eap_status: %VintageNet.Interface.EAPStatus{status: :started, method: "TLS", timestamp: ~U[2024-08-28 14:14:44.706670Z], remote_certificate_verified?: false}}

BOOM!

Quick note. It works:

{:ok, engine} = NervesKey.PKCS11.load_engine()
{:ok, i2c} = ATECC508A.Transport.I2C.init([])

signer_cert = X509.Certificate.to_der(NervesKey.signer_cert(i2c))
cert = X509.Certificate.to_der(NervesKey.device_cert(i2c))
%{key_id: uri} = NervesKey.PKCS11.private_key(engine, {:i2c, 1})
File.write("/tmp/client_cert.der", cert)
File.write("/tmp/ca_cert.der", signer_cert)

path = Path.join(Application.app_dir(:nerves_key_pkcs11), "priv/nerves_key_pkcs11.so")
VintageNet.configure("eth0", %{
       type: VintageNetEthernet,
       ipv4: %{method: :dhcp},
       vintage_net_ethernet: %{
        wpa_supplicant_conf: """
        pkcs11_engine_path=/usr/lib/engines/libpkcs11.so
        pkcs11_module_path=#{path}
        network={
          key_mgmt=WPA-EAP
          identity="User"
          eap=TLS
          eapol_flags=3
          client_cert="/tmp/client_cert.der"
          private_key="#{uri}"
        }
        """
       }
     })

Skip ca_cert to avoid CA verification unless you are doing more CA-related stuff and it matters to you. If it matters, add your signer cert to your CA list. Should not be hard.

On the freeradius side (what I tested against). I only really had to replace the ca cert in freeradius with the signer cert and then also add the signer cert to my system CA list so that it was in the root of trust or whatever that’s called.

Will post updates require on the system as well as the forked vintage_net stuff that will later need to be integrated. I did this on nerves_system_rpi3 but it should work on any.

1 Like