diff --git a/src/linux/wpa-controller/Hostapd.cxx b/src/linux/wpa-controller/Hostapd.cxx index 4cdc8320..c52d7ac8 100644 --- a/src/linux/wpa-controller/Hostapd.cxx +++ b/src/linux/wpa-controller/Hostapd.cxx @@ -332,3 +332,56 @@ Hostapd::SetPairwiseCipherSuites(std::unordered_map saePasswords, EnforceConfigurationChange enforceConfigurationChange) +{ + try { + // First set the special value which clears all saved passwords. + SetProperty(ProtocolHostapd::PropertyNameSaePassword, ProtocolHostapd::PropertyValueSaePasswordClearAll, EnforceConfigurationChange::Defer); + + // Now set the new passwords, deferring the configuration change (if requested) until the end. + for (const auto& saePassword : saePasswords) { + AddSaePassword(saePassword, EnforceConfigurationChange::Defer); + } + + // Now that all passwords are set, enforce the configuration change if requested. + if (enforceConfigurationChange == EnforceConfigurationChange::Now) { + Reload(); + } + } catch (const HostapdException& e) { + throw HostapdException(std::format("Failed to set sae passwords ({})", e.what())); + } +} + +void +Hostapd::AddSaePassword(SaePassword saePassword, EnforceConfigurationChange enforceConfigurationChange) +{ + std::string credentialValue(std::cbegin(saePassword.Credential), std::cend(saePassword.Credential)); + std::string saePasswordValue; + { + std::ostringstream saePasswordValueBuilder{}; + + saePasswordValueBuilder << credentialValue; + if (saePassword.PasswordId.has_value()) { + saePasswordValueBuilder << ProtocolHostapd::PropertyValueSaeKeyValueSeparator + << ProtocolHostapd::PropertyValueSaePasswordKeyPasswordId << ProtocolHostapd::KeyValueDelimiter << saePassword.PasswordId.value(); + } + if (saePassword.PeerMacAddress.has_value()) { + saePasswordValueBuilder << ProtocolHostapd::PropertyValueSaeKeyValueSeparator + << ProtocolHostapd::PropertyValueSaePasswordKeyPeerMac << ProtocolHostapd::KeyValueDelimiter << saePassword.PeerMacAddress.value(); + } + if (saePassword.VlanId.has_value()) { + saePasswordValueBuilder << ProtocolHostapd::PropertyValueSaeKeyValueSeparator + << ProtocolHostapd::PropertyValueSaePasswordKeyVlanId << ProtocolHostapd::KeyValueDelimiter << saePassword.VlanId.value(); + } + + saePasswordValue = saePasswordValueBuilder.str(); + } + + try { + SetProperty(ProtocolHostapd::PropertyNameSaePassword, saePasswordValue, enforceConfigurationChange); + } catch (const HostapdException& e) { + throw HostapdException(std::format("Failed to add sae password ({})", e.what())); + } +} diff --git a/src/linux/wpa-controller/include/Wpa/Hostapd.hxx b/src/linux/wpa-controller/include/Wpa/Hostapd.hxx index cfa22442..004f80a2 100644 --- a/src/linux/wpa-controller/include/Wpa/Hostapd.hxx +++ b/src/linux/wpa-controller/include/Wpa/Hostapd.hxx @@ -145,6 +145,24 @@ struct Hostapd : void SetPairwiseCipherSuites(std::unordered_map> protocolPairwiseCipherMap, EnforceConfigurationChange enforceConfigurationChange) override; + /** + * @brief Set the accepted SAE passwords for the interface. + * + * @param saePasswords The SAE passwords to set. + * @param enforceConfigurationChange When to enforce the configuration change. A value of 'Now' will trigger a configuration reload. + */ + void + SetSaePasswords(std::vector saePasswords, EnforceConfigurationChange enforceConfigurationChange = EnforceConfigurationChange::Now) override; + + /** + * @brief Add an SAE password. + * + * @param saePassword The SAE password to add. + * @param enforceConfigurationChange When to enforce the configuration change. A value of 'Now' will trigger a configuration reload. + */ + void + AddSaePassword(SaePassword saePassword, EnforceConfigurationChange enforceConfigurationChange = EnforceConfigurationChange::Defer) override; + private: const std::string m_interface; WpaController m_controller; diff --git a/src/linux/wpa-controller/include/Wpa/IHostapd.hxx b/src/linux/wpa-controller/include/Wpa/IHostapd.hxx index 6a06b64b..84655c2c 100644 --- a/src/linux/wpa-controller/include/Wpa/IHostapd.hxx +++ b/src/linux/wpa-controller/include/Wpa/IHostapd.hxx @@ -2,7 +2,9 @@ #ifndef I_HOSTAPD_HXX #define I_HOSTAPD_HXX +#include #include +#include #include #include #include @@ -169,6 +171,24 @@ struct IHostapd */ virtual void SetPairwiseCipherSuites(std::unordered_map> protocolCipherMap, EnforceConfigurationChange enforceConfigurationChange) = 0; + + /** + * @brief Set the accepted SAE passwords for the interface. + * + * @param saePasswords The SAE passwords to set. + * @param enforceConfigurationChange When to enforce the configuration change. A value of 'Now' will trigger a configuration reload. + */ + virtual void + SetSaePasswords(std::vector saePasswords, EnforceConfigurationChange enforceConfigurationChange) = 0; + + /** + * @brief Add an SAE password. + * + * @param saePassword The SAE password to add. + * @param enforceConfigurationChange When to enforce the configuration change. A value of 'Now' will trigger a configuration reload. + */ + virtual void + AddSaePassword(SaePassword saePassword, EnforceConfigurationChange enforceConfigurationChange) = 0; }; /** diff --git a/src/linux/wpa-controller/include/Wpa/ProtocolHostapd.hxx b/src/linux/wpa-controller/include/Wpa/ProtocolHostapd.hxx index 749d2872..3d410813 100644 --- a/src/linux/wpa-controller/include/Wpa/ProtocolHostapd.hxx +++ b/src/linux/wpa-controller/include/Wpa/ProtocolHostapd.hxx @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -501,6 +502,13 @@ struct ProtocolHostapd : static constexpr auto PropertyNameWpaPsk = "wpa_psk"; static constexpr auto PropertyNameRsnPairwise = "rsn_pairwise"; + static constexpr auto PropertyNameSaePassword = "sae_password"; + static constexpr auto PropertyValueSaePasswordKeyPeerMac = "mac"; + static constexpr auto PropertyValueSaePasswordKeyPasswordId = "id"; + static constexpr auto PropertyValueSaePasswordKeyVlanId = "vlanid"; + static constexpr auto PropertyValueSaePasswordClearAll = ""; + static constexpr auto PropertyValueSaeKeyValueSeparator = "|"; + // Response properties for the "STATUS" command. // Note: all properties must be terminated with the key-value delimeter (=). static constexpr auto ResponseStatusPropertyKeyState = PropertyNameState; @@ -702,6 +710,17 @@ WpaAuthenticationAlgorithmPropertyValue(WpaAuthenticationAlgorithm wpaAuthentica { return std::to_underlying(wpaAuthenticationAlgorithm); } + +/** + * @brief SAE password entry. + */ +struct SaePassword +{ + std::vector Credential; + std::optional PasswordId; + std::optional PeerMacAddress; + std::optional VlanId; +}; } // namespace Wpa #endif // HOSTAPD_PROTOCOL_HXX diff --git a/src/linux/wpa-controller/include/Wpa/WpaController.hxx b/src/linux/wpa-controller/include/Wpa/WpaController.hxx index ea59ccf5..495a9119 100644 --- a/src/linux/wpa-controller/include/Wpa/WpaController.hxx +++ b/src/linux/wpa-controller/include/Wpa/WpaController.hxx @@ -50,6 +50,16 @@ struct WpaController */ WpaController(std::string_view interfaceName, WpaType type, std::filesystem::path controlSocketPath); + WpaController(const WpaController&) = delete; + + WpaController(WpaController&&) = delete; + + WpaController& + operator=(const WpaController&) = delete; + + WpaController& + operator=(WpaController&&) = delete; + /** * @brief Destroy the WpaController object. */ diff --git a/tests/unit/linux/wpa-controller/CMakeLists.txt b/tests/unit/linux/wpa-controller/CMakeLists.txt index ad0d7f42..f3d4c3da 100644 --- a/tests/unit/linux/wpa-controller/CMakeLists.txt +++ b/tests/unit/linux/wpa-controller/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(wpa-controller-test-unit target_include_directories(wpa-controller-test-unit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} ) target_link_libraries(wpa-controller-test-unit @@ -25,4 +26,10 @@ target_link_libraries(wpa-controller-test-unit wpa-controller ) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/detail/config/hostapd.conf.format.hxx.in + ${CMAKE_CURRENT_BINARY_DIR}/hostapd.conf.format.hxx + @ONLY +) + catch_discover_tests(wpa-controller-test-unit) diff --git a/tests/unit/linux/wpa-controller/TestHostapd.cxx b/tests/unit/linux/wpa-controller/TestHostapd.cxx index fe4e2b6b..7ededb3f 100644 --- a/tests/unit/linux/wpa-controller/TestHostapd.cxx +++ b/tests/unit/linux/wpa-controller/TestHostapd.cxx @@ -1,5 +1,6 @@ #include // NOLINT +#include #include #include #include @@ -8,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -604,8 +607,6 @@ TEST_CASE("Send SetPairwiseCipherSuites() command (root)", "[wpa][hostapd][clien { std::unordered_map> protocolCipherMap{}; - std::vector keyManagementValidValues{}; - for (const auto WpaSecurityProtocol : magic_enum::enum_values() | std::views::filter(IsWpaSecurityProtocolSupported)) { for (const auto wpaCipher : WpaCiphersAll | std::views::filter(IsWpaCipherSupported)) { protocolCipherMap[WpaSecurityProtocol].push_back(wpaCipher); @@ -674,4 +675,90 @@ TEST_CASE("Send SetAuthenticationAlgorithms() command (root)", "[wpa][hostapd][c REQUIRE_NOTHROW(hostapd.SetAuthenticationAlgorithms({ WpaAuthenticationAlgorithm::OpenSystem, WpaAuthenticationAlgorithm::OpenSystem }, EnforceConfigurationChange::Now)); REQUIRE_NOTHROW(hostapd.SetAuthenticationAlgorithms({ WpaAuthenticationAlgorithm::OpenSystem, WpaAuthenticationAlgorithm::OpenSystem }, EnforceConfigurationChange::Defer)); } -} \ No newline at end of file +} + +namespace Wpa::Test +{ +constexpr std::initializer_list AsciiPassword{ 0x70, 0x61, 0x73, 0x73, 0x77, 0x6F, 0x72, 0x64 }; +constexpr auto PasswordIdValid{ "someid" }; +constexpr auto PeerMacAddressValid{ "00:11:22:33:44:55" }; +constexpr int32_t VlanIdValid{ 1 }; + +// NOLINTBEGIN(cert-err58-cpp) + +const SaePassword SaePasswordValid1{ + .Credential = AsciiPassword, + .PasswordId = std::nullopt, + .PeerMacAddress = std::nullopt, + .VlanId = std::nullopt, +}; + +const SaePassword SaePasswordValid2{ + .Credential = AsciiPassword, + .PasswordId = PasswordIdValid, + .PeerMacAddress = std::nullopt, + .VlanId = std::nullopt, +}; + +const SaePassword SaePasswordValid3{ + .Credential = AsciiPassword, + .PasswordId = PasswordIdValid, + .PeerMacAddress = PeerMacAddressValid, + .VlanId = std::nullopt, +}; + +const SaePassword SaePasswordValidComplete{ + .Credential = AsciiPassword, + .PasswordId = PasswordIdValid, + .PeerMacAddress = PeerMacAddressValid, + .VlanId = VlanIdValid, +}; + +const std::vector SaePasswordsValid{ SaePasswordValid1, SaePasswordValid2, SaePasswordValid3, SaePasswordValidComplete }; + +// NOLINTEND(cert-err58-cpp) +} // namespace Wpa::Test + +TEST_CASE("Send AddSaePassword() command (root)", "[wpa][hostapd][client][remote]") +{ + using namespace Wpa; + using namespace Wpa::Test; + + Hostapd hostapd(WpaDaemonManager::InterfaceNameDefault); + + SECTION("Doesn't throw") + { + REQUIRE_NOTHROW(hostapd.AddSaePassword(SaePasswordValidComplete, EnforceConfigurationChange::Now)); + REQUIRE_NOTHROW(hostapd.AddSaePassword(SaePasswordValidComplete, EnforceConfigurationChange::Defer)); + } + + SECTION("Succeeds with valid inputs") + { + for (const auto& saePassword : SaePasswordsValid) { + REQUIRE_NOTHROW(hostapd.AddSaePassword(saePassword, EnforceConfigurationChange::Now)); + REQUIRE_NOTHROW(hostapd.AddSaePassword(saePassword, EnforceConfigurationChange::Defer)); + } + } +} + +TEST_CASE("Send SetSaePasswords() command (root)", "[wpa][hostapd][client][remote]") +{ + using namespace Wpa; + using namespace Wpa::Test; + + Hostapd hostapd(WpaDaemonManager::InterfaceNameDefault); + + SECTION("Doesn't throw") + { + REQUIRE_NOTHROW(hostapd.SetSaePasswords({ SaePasswordValidComplete }, EnforceConfigurationChange::Now)); + REQUIRE_NOTHROW(hostapd.SetSaePasswords({ SaePasswordValidComplete }, EnforceConfigurationChange::Defer)); + } + + SECTION("Succeeds with valid inputs") + { + for (const auto& saePassword : SaePasswordsValid) { + REQUIRE_NOTHROW(hostapd.SetSaePasswords({ saePassword }, EnforceConfigurationChange::Now)); + REQUIRE_NOTHROW(hostapd.SetSaePasswords({ saePassword }, EnforceConfigurationChange::Defer)); + } + } +} diff --git a/tests/unit/linux/wpa-controller/detail/WpaDaemonManager.cxx b/tests/unit/linux/wpa-controller/detail/WpaDaemonManager.cxx index 34251e6c..12a188d1 100644 --- a/tests/unit/linux/wpa-controller/detail/WpaDaemonManager.cxx +++ b/tests/unit/linux/wpa-controller/detail/WpaDaemonManager.cxx @@ -15,33 +15,10 @@ #include #include +#include "hostapd.conf.format.hxx" + namespace detail { -/** - * @brief Default hostapd configuration file contents. - * - * This configuration file supports WPA2 with typical security settings. It is - * meant to be used with a virtualized wlan device created with the - * mac80211_hwsim kernel module. The intention is to use this for basic control - * socket tests, not to exercise specifically wlan functionality. - * - * This string must be formatted with std::format() to provide the interface - * name as the first interpolation argument. - */ -static constexpr auto WpaDaemonHostapdConfigurationFileContentsFormat = R"CONFIG( -interface={} -driver=nl80211 -ctrl_interface=/var/run/hostapd -ssid=wificontrollertest -hw_mode=g -channel=1 -auth_algs=3 -wpa=2 -wpa_passphrase=password -wpa_key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE -wpa_pairwise=TKIP CCMP -rsn_pairwise=CCMP -)CONFIG"; /** * @brief Write the default configuration file contents for the specified wpa diff --git a/tests/unit/linux/wpa-controller/detail/config/hostapd.conf.format.hxx.in b/tests/unit/linux/wpa-controller/detail/config/hostapd.conf.format.hxx.in new file mode 100644 index 00000000..ca9cb8f8 --- /dev/null +++ b/tests/unit/linux/wpa-controller/detail/config/hostapd.conf.format.hxx.in @@ -0,0 +1,24 @@ + +#ifndef HOSTAPD_CONF_FORMAT_HXX +#define HOSTAPD_CONF_FORMAT_HXX + +namespace detail +{ +static constexpr auto WpaDaemonHostapdConfigurationFileContentsFormat = R"CONFIG( +interface={} +driver=nl80211 +ctrl_interface=@CMAKE_INSTALL_FULL_RUNSTATEDIR@/hostapd +ssid=wificontrollertest +hw_mode=g +channel=1 +auth_algs=3 +wpa=2 +wpa_passphrase=password +wpa_key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE +wpa_pairwise=TKIP CCMP +rsn_pairwise=CCMP +)CONFIG"; + +} // namespace detail + +#endif // HOSTAPD_CONF_FORMAT_HXX