diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c4dc1e..e3b7c79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,6 +157,10 @@ endif() # Package config # if(LIBVIRTUALHID_INSTALL) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" + DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/libvirtualhid" + COMPONENT library) + configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libvirtualhid-config.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid-config.cmake" diff --git a/LICENSES/README.md b/LICENSES/README.md index 282b050..0f1231b 100644 --- a/LICENSES/README.md +++ b/LICENSES/README.md @@ -7,10 +7,16 @@ Windows driver package. - Cross-platform library source, public headers, non-driver backends, examples, tests, build scripts, and documentation unless listed below: [MIT](https://github.com/LizardByte/libvirtualhid/blob/master/LICENSE). -- Windows UMDF driver source under `src/platform/windows/driver/`: +- Windows UMDF driver source under `src/platform/windows/driver/`, the broker + service under `src/platform/windows/broker/`, and the broker entitlement and + evaluation sources + `src/platform/windows/shared/lvh_windows_broker_config.hpp` and + `src/platform/windows/shared/lvh_windows_github_actions_evaluation.hpp`: [LizardByte Source-Available License 1.0](https://github.com/LizardByte/libvirtualhid/blob/master/LICENSES/LicenseRef-LizardByte-SAL-1.0.md). - Generated Windows driver package artifacts, including the driver MSI: [LizardByte Source-Available License 1.0](https://github.com/LizardByte/libvirtualhid/blob/master/LICENSES/LicenseRef-LizardByte-SAL-1.0.md). -The Windows driver MSI may include MIT-licensed helper components from this -repository; packaged installs include both license texts for that reason. +Every installed library distribution includes the MIT notice under +`share/licenses/libvirtualhid`. The Windows driver MSI may include MIT-licensed +helper components from this repository; packaged installs include both license +texts for that reason. diff --git a/README.md b/README.md index 220714c..6539940 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ test app path and does not currently advertise an ARM64 build. ## 📄 License The cross-platform `libvirtualhid` library is licensed under the MIT License. -The Windows UMDF driver source and generated Windows -driver package artifacts, including the driver MSI, are licensed under the -LizardByte Source-Available License 1.0 (LB-SAL 1.0). See the +The Windows UMDF driver, broker, proprietary entitlement/evaluation sources, +and generated Windows driver package artifacts, including the driver MSI, are +licensed under the LizardByte Source-Available License 1.0 (LB-SAL 1.0). See the [license map](LICENSES/README.md) for the full repository split. diff --git a/docs/platform-support.md b/docs/platform-support.md index 39a80a5..a72e69f 100644 --- a/docs/platform-support.md +++ b/docs/platform-support.md @@ -107,7 +107,7 @@ effects back into the public callback. Each requested playback repetition restarts the effect's ramp and envelope timing. A zero-length effect remains active until its explicit stop event, matching the infinite-effect contract used by SDL and Steam. The Linux backend lets a new uinput device settle before -reading those effects so an early poll error cannot disable feedback for the +reading those effects, so an early poll error cannot disable feedback for the device lifetime. Generated UHID nodes are correlated by stable physical and unique identifiers when available, with device-name matching used only as a fallback. PlayStation rumble is read from native UHID interrupt-channel output @@ -120,7 +120,7 @@ which avoids changing the raw button capability surface. It uses a compact Generic button layout rather than the sparse Xbox button slots. Xbox 360 retains its `0x045E:0x028E` identity, while its Linux uinput device uses -the Bluetooth bus so consumers select the sparse button mapping. +the Bluetooth bus, so consumers select the sparse button mapping. Xbox One and Xbox Series retain their public USB identities, but their Linux uinput devices use the corresponding Bluetooth product identities (`0x0B20` and `0x0B13`, respectively), whose standard consumer mappings match the events @@ -230,7 +230,7 @@ Current macOS capabilities: codes. - Mouse relative movement, absolute movement on the main display, left/middle/ right button transitions, and pixel-based vertical/horizontal scroll. -- Shared keyboard modifier state on mouse events so combinations such as +- Shared keyboard modifier state on mouse events, so combinations such as shift-click continue to work. Unsupported macOS capabilities currently return `unsupported_profile`: diff --git a/docs/store-review-validation.md b/docs/store-review-validation.md index eb5162b..3d35d80 100644 --- a/docs/store-review-validation.md +++ b/docs/store-review-validation.md @@ -29,7 +29,7 @@ C:\Program Files\libvirtualhid\services\windows\libvirtualhid_broker.exe Required validation: $installRoot = Join-Path $env:ProgramFiles "libvirtualhid" -& "$installRoot\tools\windows\virtualhid_control.exe" +Start-Process "$installRoot\tools\windows\virtualhid_control.exe" In the libvirtualhid control window, paste the supplied review key into the License key field and click Activate license. Confirm the status changes to Licensed. Then leave the default Xbox Series profile selected and click Create. Use the button and axis controls in the UI to submit input to the virtual controller. diff --git a/docs/usage.md b/docs/usage.md index bc9389e..b03332d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -104,23 +104,21 @@ buttons, sticks, triggers, and battery state, show backend and profile capabilities, list device nodes reported for UI-created devices, and display normalized gamepad output such as rumble, RGB LED, adaptive trigger, trigger rumble, and raw report events delivered through the normal callback path. Button -controls are momentary by default so they behave like physical gamepad buttons; +controls are momentary by default, so they behave like physical gamepad buttons; on Windows, the UI also displays broker license status and can activate, -refresh, or deactivate a machine license. Outside the explicitly marked GitHub -Actions test environment, every Windows UMDF gamepad creation requires a -current successful license validation response and there is no offline grace -period. The CI-only exception is a single five-minute window that begins with -the first gamepad creation attempt. Purchase and account-management buttons use +refresh, or deactivate a machine license without elevation. Every Windows UMDF +gamepad creation requires a current successful license validation response and +there is no offline grace period. Purchase and account-management buttons use the compiled URLs in `src/platform/windows/shared/lvh_windows_broker_config.hpp`. -enable `Lock buttons` to keep the old click-to-toggle behavior for held inputs. +Enable `Lock buttons` to click-to-toggle behavior for held inputs. The resizable window supports a compact width. Its device and control panels -stack, and the button grid reflows, to keep controls usable when it is narrowed. -The UI intentionally does not use gamepad navigation so virtual devices created +stack, and the button grid reflows to keep controls usable when it is narrowed. +The UI intentionally does not use gamepad navigation, so virtual devices created by the tool cannot drive the tool's own controls. External devices created by another process, such as Sunshine, are not -enumerated yet. That requires backend protocol support so the Windows driver or +enumerated yet. That requires backend protocol support, so the Windows driver or Linux backend can expose cross-process device snapshots without letting two processes race to control the same virtual device. @@ -133,7 +131,9 @@ The API centers on portable device concepts: - `get_license_status`, `activate_license`, `validate_license`, and `deactivate_license`: provider-neutral machine license operations for host applications. On Windows these call the installed local broker; license keys - are not retained by the client library or returned to the application. + are not retained by the client library or returned to the application. The + client verifies that the named-pipe server is the SCM-registered running + broker before sending any request. - `VirtualDevice`: common lifecycle for created devices. - `Gamepad`: submits normalized gamepad state and receives output callbacks. - `Keyboard`: submits key press/release and UTF-8 text input. @@ -183,18 +183,18 @@ touch, motion, battery, feedback, and lifecycle updates onto the platform-neutra Built-in gamepad profiles and their platform-neutral default device names are: -| Profile | Default device name | -| --- | --- | -| Generic HID gamepad | `(libvirtualhid) Generic Controller` | -| Xbox 360 | `(libvirtualhid) X-Box 360 Controller` | -| Xbox One | `(libvirtualhid) X-Box One Controller` | -| Xbox Series | `(libvirtualhid) X-Box Series Controller` | -| DualShock 4 USB and Bluetooth | `(libvirtualhid) PS4 Controller` | -| DualSense USB and Bluetooth | `(libvirtualhid) PS5 Controller` | -| Nintendo Switch Pro | `(libvirtualhid) Nintendo Pro Controller` | +| Profile | Default device name | +|-------------------------------|-------------------------------------------| +| Generic HID gamepad | `(libvirtualhid) Generic Controller` | +| Xbox 360 | `(libvirtualhid) X-Box 360 Controller` | +| Xbox One | `(libvirtualhid) X-Box One Controller` | +| Xbox Series | `(libvirtualhid) X-Box Series Controller` | +| DualShock 4 USB and Bluetooth | `(libvirtualhid) PS4 Controller` | +| DualSense USB and Bluetooth | `(libvirtualhid) PS5 Controller` | +| Nintendo Switch Pro | `(libvirtualhid) Nintendo Pro Controller` | Consumers may replace `DeviceProfile::name` before creating a gamepad, for -example to prepend an application name while preserving the default controller +example, to prepend an application name while preserving the default controller identity across platform backends. The platform-neutral Generic HID descriptor reports the D-pad as buttons 13 @@ -203,8 +203,8 @@ through 16 in the input report. Linux may still route that profile through standard `ABS_HAT0X` and `ABS_HAT0Y` axes. Profiles advertise support for features such as rumble, trigger rumble, RGB -LEDs, adaptive triggers, motion sensors, touchpads, battery state, profile -specific buttons, and raw output reports. Consumers should query profile and +LEDs, adaptive triggers, motion sensors, touchpads, battery state, +profile-specific buttons, and raw output reports. Consumers should query profile and backend capabilities before warning users about unsupported client features. The `misc1` button represents Share/Capture/Mic Mute-style controls and is available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox diff --git a/docs/windows-driver.md b/docs/windows-driver.md index da419fd..94516e6 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -43,6 +43,21 @@ normal desktop application to use the broker without running as administrator while keeping broker ownership and privileged device operations in the Windows service. +Status, current-license validation, activation, replacement, deactivation, +gamepad creation, and owned-device destruction are available to authenticated +local users without elevation. Before sending any request, clients compare the +named-pipe server PID to the SCM-registered, currently running +`libvirtualhid_broker` service. This prevents another local process from +impersonating an unavailable broker and collecting a license key. The service +also requests first ownership of the pipe name and rejects remote clients. + +All broker messages are fixed-size and fully validated before use, including +protocol versions, exact byte counts, request types, reserved fields, enums, +array bounds, string terminators, and unused payload bytes. Connection, request, +and response operations use cancellable overlapped I/O with explicit completion +and byte-count checks, so a stopped service or disconnected client cannot leave +an operation using expired stack state. + The backend sends fixed-size C protocol structures to the broker. A create request identifies the backend's existing control handle; the broker duplicates that handle from the named-pipe client process and issues `DeviceIoControl` on @@ -101,7 +116,7 @@ cpack -G WIX -C Release --config .\cmake-build-windows-driver\CPackConfig.cmake The package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver while still exposing the framework APIs used by libvirtualhid. The driver links -the MSVC runtime statically so the UMDF host process does not need VC runtime +the MSVC runtime statically, so the UMDF host process does not need VC runtime DLLs beside the driver. ## Developer Install and Validation @@ -180,7 +195,8 @@ diagnostics. On Windows, the UI also shows broker license status. It can activate a license key, refresh validation, deactivate the current machine, and open -compiled purchase or account-management URLs. +compiled purchase or account-management URLs. License management and normal +virtual-gamepad use do not require elevation. ## Installation Notes @@ -230,8 +246,12 @@ The broker stores machine-scoped license state in: C:\ProgramData\libvirtualhid\license.dat ``` -The file is protected with Windows DPAPI local-machine scope. GitHub Actions -evaluation timing is stored separately with the same protection in +The file is protected with Windows DPAPI local-machine scope. The state +directory and both state files are owned by LocalSystem and use protected DACLs +that grant full access only to `NT SERVICE\libvirtualhid_broker`, LocalSystem, +and built-in administrators; reparse-point state paths are rejected. GitHub +Actions evaluation timing is +stored separately with the same DPAPI and ACL protection in `C:\ProgramData\libvirtualhid\github-actions-evaluation.dat`. Broker entitlement configuration is compiled into the Windows broker and diagnostic UI. Update `src/platform/windows/shared/lvh_windows_broker_config.hpp` when the Polar @@ -314,9 +334,9 @@ and must not ship the local pull-request test certificate. ## License -The Windows UMDF driver source and generated Windows driver package artifacts, -including the driver MSI, are licensed under the LizardByte Source-Available -License 1.0 (LB-SAL 1.0). See the [license map](../LICENSES/README.md) for the -full repository license split. The MSI may also include MIT-licensed helper -components from this repository, so packaged installs include both license -texts. +The Windows UMDF driver, broker, proprietary entitlement/evaluation sources, +and generated Windows driver package artifacts, including the driver MSI, are +licensed under the LizardByte Source-Available License 1.0 (LB-SAL 1.0). See +the [license map](../LICENSES/README.md) for the full repository license split. +The MSI may also include MIT-licensed helper components from this repository, +so packaged installs include both license texts. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c767414..66c66f0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -62,6 +62,7 @@ elseif(WIN32) _WIN32_WINNT=0x0600) target_link_libraries(${PROJECT_NAME} PRIVATE + advapi32 setupapi lizardbyte::common) elseif(APPLE) diff --git a/src/include/libvirtualhid/license.hpp b/src/include/libvirtualhid/license.hpp index 993a54b..e87b257 100644 --- a/src/include/libvirtualhid/license.hpp +++ b/src/include/libvirtualhid/license.hpp @@ -81,6 +81,7 @@ namespace lvh { * * The license key is sent directly to the platform license service. The library does not * persist a copy or expose it in the returned status. + * On Windows, authenticated local clients can activate or replace a license without elevation. * * @param license_key License key supplied by the customer. * @param instance_name Optional customer-visible name for this machine activation. @@ -98,6 +99,8 @@ namespace lvh { /** * @brief Deactivate the stored license from this machine. * + * On Windows, authenticated local clients can deactivate a license without elevation. + * * @return Deactivation result and latest license details. */ LicenseResult deactivate_license(); diff --git a/src/platform/windows/broker/broker_request_validation.hpp b/src/platform/windows/broker/broker_request_validation.hpp new file mode 100644 index 0000000..0d0e43a --- /dev/null +++ b/src/platform/windows/broker/broker_request_validation.hpp @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file src/platform/windows/broker/broker_request_validation.hpp + * @brief Validation helpers for untrusted Windows broker protocol messages. + */ +#pragma once + +// standard includes +#include +#include +#include +#include +#include +#include + +// local includes +#include "lvh_windows_broker_protocol.h" + +namespace lvh::windows::broker_validation { + + inline constexpr std::uint32_t known_gamepad_flags = + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE | + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION | + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD | + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED | + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY | + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS; + + template + bool all_zero(const Value (&values)[Size]) { + return std::ranges::all_of(values, [](const auto value) { + return value == Value {}; + }); + } + + template + bool valid_c_string(const char (&value)[Size], bool allow_empty = true) { + const auto terminator = std::ranges::find(value, '\0'); + if (terminator == std::end(value) || (!allow_empty && terminator == std::begin(value))) { + return false; + } + + return std::ranges::all_of(std::next(terminator), std::end(value), [](const auto character) { + return character == '\0'; + }); + } + + template + bool valid_sized_c_string(const char (&value)[Size], std::uint32_t size) { + if (size >= Size || value[size] != '\0') { + return false; + } + + const auto content_end = std::begin(value) + size; + if (std::ranges::find(std::begin(value), content_end, '\0') != content_end) { + return false; + } + + return std::ranges::all_of(content_end, std::end(value), [](const auto character) { + return character == '\0'; + }); + } + + inline bool valid_header( + const LvhWindowsBrokerRequestHeader &header, + LvhWindowsBrokerRequestType expected_type, + std::uint32_t expected_size + ) { + return header.version == LVH_WINDOWS_BROKER_PROTOCOL_VERSION && + header.size == expected_size && + header.type == std::to_underlying(expected_type) && + header.reserved0 == 0U; + } + + inline bool valid_gamepad_request(const LvhWindowsCreateGamepadRequest &request) { + const auto &sizes = request.report_sizes; + const auto known_bus = request.bus_type == LVH_WINDOWS_BUS_UNKNOWN || + request.bus_type == LVH_WINDOWS_BUS_USB || + request.bus_type == LVH_WINDOWS_BUS_BLUETOOTH; + const auto known_profile = request.gamepad_kind <= LVH_WINDOWS_GAMEPAD_DUALSHOCK4; + + return request.version == LVH_WINDOWS_CONTROL_PROTOCOL_VERSION && + request.size == sizeof(request) && + request.client_device_id != 0U && + known_bus && + known_profile && + (request.flags & ~known_gamepad_flags) == 0U && + all_zero(request.hardware_ids.reserved0) && + sizes.input_report_size > 0U && + sizes.input_report_size <= LVH_WINDOWS_MAX_INPUT_REPORT_SIZE && + sizes.output_report_size <= LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE && + sizes.report_descriptor_size > 0U && + sizes.report_descriptor_size <= LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE && + valid_sized_c_string(request.name, sizes.name_size) && + valid_sized_c_string(request.manufacturer, sizes.manufacturer_size) && + valid_sized_c_string(request.stable_id, sizes.stable_id_size) && + std::ranges::all_of( + std::begin(request.report_descriptor) + sizes.report_descriptor_size, + std::end(request.report_descriptor), + [](const auto value) { + return value == 0U; + } + ); + } + + inline bool valid_destroy_request(const LvhWindowsDestroyDeviceRequest &request) { + return request.version == LVH_WINDOWS_CONTROL_PROTOCOL_VERSION && + request.size == sizeof(request) && + request.driver_device_id != 0U && + !all_zero(request.session_token.bytes); + } + + inline bool valid_request(const LvhWindowsBrokerStatusRequest &request) { + return valid_header( + request.header, + LvhWindowsBrokerRequestType::status, + sizeof(request) + ); + } + + inline bool valid_request(const LvhWindowsBrokerCreateGamepadRequest &request) { + return valid_header( + request.header, + LvhWindowsBrokerRequestType::create_gamepad, + sizeof(request) + ) && + request.client_control_handle != 0U && + valid_gamepad_request(request.gamepad); + } + + inline bool valid_request(const LvhWindowsBrokerDestroyDeviceRequest &request) { + return valid_header( + request.header, + LvhWindowsBrokerRequestType::destroy_device, + sizeof(request) + ) && + valid_destroy_request(request.device); + } + + inline bool valid_request( + const LvhWindowsBrokerLicenseRequest &request, + LvhWindowsBrokerRequestType expected_type + ) { + using enum LvhWindowsBrokerRequestType; + + if (!valid_header(request.header, expected_type, sizeof(request))) { + return false; + } + + switch (expected_type) { + case activate_license: + return valid_c_string(request.license_key, false) && + valid_c_string(request.instance_name); + case validate_license: + case deactivate_license: + return all_zero(request.license_key) && all_zero(request.instance_name); + case status: + case create_gamepad: + case destroy_device: + return false; + } + + return false; + } + +} // namespace lvh::windows::broker_validation diff --git a/src/platform/windows/broker/libvirtualhid_broker.cpp b/src/platform/windows/broker/libvirtualhid_broker.cpp index 21d9ad0..a2f47e2 100644 --- a/src/platform/windows/broker/libvirtualhid_broker.cpp +++ b/src/platform/windows/broker/libvirtualhid_broker.cpp @@ -15,6 +15,7 @@ // platform includes // clang-format off +#include #include #include #include @@ -22,6 +23,7 @@ // clang-format on // local includes +#include "broker_request_validation.hpp" #include "lvh_windows_broker_config.hpp" #include "lvh_windows_broker_protocol.h" #include "lvh_windows_github_actions_evaluation.hpp" @@ -48,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -58,11 +61,15 @@ namespace { using UniqueHandle = std::unique_ptr; + using UniqueLocalMemory = std::unique_ptr; using UniqueWinHttpHandle = std::unique_ptr; constexpr auto service_name = L"libvirtualhid_broker"; + constexpr auto service_account_name = L"NT SERVICE\\libvirtualhid_broker"; constexpr auto broker_instance_name = "libvirtualhid Windows broker"; constexpr auto pipe_buffer_size = 8192U; + constexpr auto pipe_connect_timeout = 1000U; + constexpr auto pipe_io_timeout = 5000U; // Message-mode clients need the complete GENERIC_READ mapping plus individual write rights. constexpr auto pipe_client_granted_access = FILE_GENERIC_READ | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES; constexpr auto pipe_security_descriptor = L"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x0012018B;;;AU)"; @@ -106,6 +113,94 @@ namespace { return {handle, &::WinHttpCloseHandle}; } + bool complete_pending_pipe_io( + HANDLE pipe, + OVERLAPPED &overlapped, + HANDLE requested_stop_event, + DWORD timeout, + DWORD &bytes_transferred + ) { + std::array wait_handles { + overlapped.hEvent, + requested_stop_event, + }; + const auto handle_count = requested_stop_event == nullptr ? 1U : 2U; + if (const auto wait_result = ::WaitForMultipleObjects(handle_count, wait_handles.data(), FALSE, timeout); wait_result == WAIT_OBJECT_0) { + return ::GetOverlappedResult( + pipe, + &overlapped, + &bytes_transferred, + FALSE + ) != FALSE; + } + + static_cast(::CancelIoEx(pipe, &overlapped)); + DWORD ignored = 0; + static_cast(::GetOverlappedResult(pipe, &overlapped, &ignored, TRUE)); + return false; + } + + bool read_pipe_message( + HANDLE pipe, + std::span buffer, + HANDLE requested_stop_event, + DWORD &bytes_read + ) { + auto operation_event = make_unique_handle(::CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!operation_event) { + return false; + } + + OVERLAPPED overlapped {}; + overlapped.hEvent = operation_event.get(); + bytes_read = 0; + if (::ReadFile(pipe, buffer.data(), static_cast(buffer.size()), &bytes_read, &overlapped) != FALSE) { + return true; + } + if (::GetLastError() != ERROR_IO_PENDING) { + return false; + } + + return complete_pending_pipe_io( + pipe, + overlapped, + requested_stop_event, + pipe_io_timeout, + bytes_read + ); + } + + bool write_pipe_message( + HANDLE pipe, + std::span buffer, + HANDLE requested_stop_event + ) { + auto operation_event = make_unique_handle(::CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!operation_event) { + return false; + } + + OVERLAPPED overlapped {}; + overlapped.hEvent = operation_event.get(); + const auto buffer_size = static_cast(buffer.size()); + DWORD bytes_written = 0; + if (::WriteFile(pipe, buffer.data(), buffer_size, &bytes_written, &overlapped) != FALSE) { + return bytes_written == buffer_size; + } + if (::GetLastError() != ERROR_IO_PENDING) { + return false; + } + + return complete_pending_pipe_io( + pipe, + overlapped, + requested_stop_event, + pipe_io_timeout, + bytes_written + ) && + bytes_written == buffer_size; + } + template void copy_c_string(char (&target)[Size], std::string_view value) { std::ranges::fill(target, '\0'); @@ -200,6 +295,173 @@ namespace { return broker_state_path("github-actions-evaluation.dat"); } + std::optional> lookup_account_sid(const wchar_t *account_name) { + DWORD sid_size = 0; + DWORD domain_size = 0; + SID_NAME_USE sid_name_use {}; + static_cast(::LookupAccountNameW( + nullptr, + account_name, + nullptr, + &sid_size, + nullptr, + &domain_size, + &sid_name_use + )); + if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER || sid_size == 0U) { + return std::nullopt; + } + + auto sid = std::vector(sid_size); + if (auto domain = std::wstring(domain_size, L'\0'); ::LookupAccountNameW(nullptr, account_name, sid.data(), &sid_size, domain.data(), &domain_size, &sid_name_use) == FALSE) { + return std::nullopt; + } + + sid.resize(sid_size); + return sid; + } + + std::optional make_state_security_descriptor( + std::string_view state_name, + std::string &message + ) { + auto service_sid = lookup_account_sid(service_account_name); + if (!service_sid || ::IsValidSid(service_sid->data()) == FALSE) { + message = "Unable to resolve the broker service identity for " + + std::string {state_name} + " state."; + return std::nullopt; + } + + LPWSTR raw_service_sid = nullptr; + if (::ConvertSidToStringSidW(service_sid->data(), &raw_service_sid) == FALSE) { + message = "Unable to format the broker service identity for " + + std::string {state_name} + " state: " + + windows_error_message(::GetLastError()); + return std::nullopt; + } + auto service_sid_text = UniqueLocalMemory {raw_service_sid, &::LocalFree}; + + const auto sddl = std::format( + L"O:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;{})", + static_cast(service_sid_text.get()) + ); + PSECURITY_DESCRIPTOR raw_descriptor = nullptr; + if (::ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), SDDL_REVISION_1, &raw_descriptor, nullptr) == FALSE) { + message = "Unable to create the access policy for " + + std::string {state_name} + " state: " + + windows_error_message(::GetLastError()); + return std::nullopt; + } + + return UniqueLocalMemory {raw_descriptor, &::LocalFree}; + } + + PACL security_descriptor_dacl(PSECURITY_DESCRIPTOR descriptor) { + BOOL dacl_present = FALSE; + BOOL dacl_defaulted = FALSE; + PACL dacl = nullptr; + if (::GetSecurityDescriptorDacl(descriptor, &dacl_present, &dacl, &dacl_defaulted) == FALSE || dacl_present == FALSE || dacl == nullptr) { + return nullptr; + } + return dacl; + } + + DWORD set_state_path_security( + const std::filesystem::path &path, + PSID owner, + PACL dacl + ) { + auto mutable_path = path.native(); + return ::SetNamedSecurityInfoW( + mutable_path.data(), + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | + PROTECTED_DACL_SECURITY_INFORMATION, + owner, + nullptr, + dacl, + nullptr + ); + } + + bool apply_state_path_acl( + const std::filesystem::path &path, + PSECURITY_DESCRIPTOR descriptor, + std::string_view state_name, + std::string &message + ) { + const auto dacl = security_descriptor_dacl(descriptor); + if (dacl == nullptr) { + message = "Unable to read the access policy for " + + std::string {state_name} + " state."; + return false; + } + + BOOL owner_defaulted = FALSE; + PSID owner = nullptr; + if (::GetSecurityDescriptorOwner(descriptor, &owner, &owner_defaulted) == FALSE || owner == nullptr || ::IsValidSid(owner) == FALSE) { + message = "Unable to read the owner policy for " + + std::string {state_name} + " state."; + return false; + } + + if (const auto result = set_state_path_security(path, owner, dacl); result != ERROR_SUCCESS) { + message = "Unable to restrict " + std::string {state_name} + + " state access: " + windows_error_message(result); + return false; + } + return true; + } + + bool ensure_secure_state_directory( + const std::filesystem::path &directory, + PSECURITY_DESCRIPTOR descriptor, + std::string_view state_name, + std::string &message + ) { + if (SECURITY_ATTRIBUTES security_attributes { + .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = descriptor, + .bInheritHandle = FALSE, + }; + ::CreateDirectoryW(directory.c_str(), &security_attributes) == FALSE && ::GetLastError() != ERROR_ALREADY_EXISTS) { + message = "Unable to create " + std::string {state_name} + + " state directory: " + windows_error_message(::GetLastError()); + return false; + } + + if (const auto attributes = ::GetFileAttributesW(directory.c_str()); attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0U || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0U) { + message = "The " + std::string {state_name} + + " state directory is missing or unsafe."; + return false; + } + + return apply_state_path_acl( + directory, + descriptor, + state_name, + message + ); + } + + bool secure_existing_state_file( + const std::filesystem::path &path, + PSECURITY_DESCRIPTOR descriptor, + std::string_view state_name, + std::string &message + ) { + const auto attributes = ::GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + return ::GetLastError() == ERROR_FILE_NOT_FOUND; + } + if ((attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0U) { + message = "The " + std::string {state_name} + " state file is unsafe."; + return false; + } + + return apply_state_path_acl(path, descriptor, state_name, message); + } + struct PolarLicenseState { std::string provider; std::string license_key; @@ -236,7 +498,15 @@ namespace { return parsed; } - std::optional load_protected_state(const std::filesystem::path &path) { + std::optional load_protected_state( + const std::filesystem::path &path, + std::string_view state_name + ) { + std::string security_message; + if (auto security_descriptor = make_state_security_descriptor(state_name, security_message); !security_descriptor || !ensure_secure_state_directory(path.parent_path(), security_descriptor->get(), state_name, security_message) || !secure_existing_state_file(path, security_descriptor->get(), state_name, security_message)) { + return std::nullopt; + } + std::ifstream input {path, std::ios::binary}; if (!input) { return std::nullopt; @@ -284,27 +554,45 @@ namespace { return false; } - std::error_code ec; - std::filesystem::create_directories(path.parent_path(), ec); - if (ec) { - static_cast(::LocalFree(encrypted_blob.pbData)); - message = "Unable to create " + std::string {state_name} + " state directory: " + ec.message(); + auto encrypted_data = UniqueLocalMemory {encrypted_blob.pbData, &::LocalFree}; + auto security_descriptor = make_state_security_descriptor(state_name, message); + if (!security_descriptor || !ensure_secure_state_directory(path.parent_path(), security_descriptor->get(), state_name, message) || !secure_existing_state_file(path, security_descriptor->get(), state_name, message)) { return false; } - std::ofstream output {path, std::ios::binary | std::ios::trunc}; - if (!output) { - static_cast(::LocalFree(encrypted_blob.pbData)); - message = "Unable to write " + std::string {state_name} + " state."; + SECURITY_ATTRIBUTES security_attributes { + .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = security_descriptor->get(), + .bInheritHandle = FALSE, + }; + const auto output = ::CreateFileW(path.c_str(), GENERIC_WRITE, 0, &security_attributes, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (output == INVALID_HANDLE_VALUE) { + message = "Unable to write " + std::string {state_name} + + " state: " + windows_error_message(::GetLastError()); return false; } - output.write( - reinterpret_cast(encrypted_blob.pbData), - encrypted_blob.cbData - ); - static_cast(::LocalFree(encrypted_blob.pbData)); - return true; + DWORD bytes_written = 0; + const auto persisted = ::WriteFile(output, encrypted_data.get(), encrypted_blob.cbData, &bytes_written, nullptr) != FALSE && + bytes_written == encrypted_blob.cbData && + ::FlushFileBuffers(output) != FALSE; + const auto persist_error = persisted ? ERROR_SUCCESS : ::GetLastError(); + const auto closed = ::CloseHandle(output) != FALSE; + const auto close_error = closed ? ERROR_SUCCESS : ::GetLastError(); + + if (!persisted) { + message = "Unable to persist " + std::string {state_name} + + " state: " + windows_error_message(persist_error); + return false; + } + + if (!closed) { + message = "Unable to close " + std::string {state_name} + + " state: " + windows_error_message(close_error); + return false; + } + + return apply_state_path_acl(path, security_descriptor->get(), state_name, message); } PolarLicenseState deserialize_license_state(std::string_view serialized) { @@ -348,7 +636,7 @@ namespace { } std::optional load_license_state() { - const auto serialized = load_protected_state(license_state_path()); + const auto serialized = load_protected_state(license_state_path(), "license"); if (!serialized) { return std::nullopt; } @@ -409,7 +697,10 @@ namespace { } std::optional load_github_actions_evaluation_state() { - const auto serialized = load_protected_state(github_actions_evaluation_state_path()); + const auto serialized = load_protected_state( + github_actions_evaluation_state_path(), + "GitHub Actions evaluation" + ); if (!serialized) { return std::nullopt; } @@ -594,16 +885,6 @@ namespace { benefit->plan_name; } - bool valid_broker_header( - const LvhWindowsBrokerRequestHeader &header, - LvhWindowsBrokerRequestType expected_type, - std::uint32_t expected_size - ) { - return header.version == LVH_WINDOWS_BROKER_PROTOCOL_VERSION && - header.size == expected_size && - header.type == std::to_underlying(expected_type); - } - bool session_token_matches( const LvhWindowsSessionToken &lhs, const LvhWindowsSessionToken &rhs @@ -837,11 +1118,7 @@ namespace { response.gamepad.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; response.gamepad.size = sizeof(response.gamepad); - if (!valid_broker_header( - request.header, - LvhWindowsBrokerRequestType::create_gamepad, - sizeof(request) - )) { + if (!lvh::windows::broker_validation::valid_request(request)) { response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Invalid broker create request."); fill_license_status(response.license); @@ -915,11 +1192,7 @@ namespace { response.version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION; response.size = sizeof(response); - if (!valid_broker_header( - request.header, - LvhWindowsBrokerRequestType::destroy_device, - sizeof(request) - )) { + if (!lvh::windows::broker_validation::valid_request(request)) { response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Invalid broker destroy request."); fill_license_status(response.license); @@ -997,10 +1270,9 @@ namespace { response.version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION; response.size = sizeof(response); - if (!valid_broker_header( - request.header, - LvhWindowsBrokerRequestType::activate_license, - sizeof(request) + if (!lvh::windows::broker_validation::valid_request( + request, + LvhWindowsBrokerRequestType::activate_license )) { response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Invalid broker activate request."); @@ -1098,10 +1370,9 @@ namespace { response.version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION; response.size = sizeof(response); - if (!valid_broker_header( - request.header, - LvhWindowsBrokerRequestType::validate_license, - sizeof(request) + if (!lvh::windows::broker_validation::valid_request( + request, + LvhWindowsBrokerRequestType::validate_license )) { response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Invalid broker validate request."); @@ -1123,10 +1394,9 @@ namespace { response.version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION; response.size = sizeof(response); - if (!valid_broker_header( - request.header, - LvhWindowsBrokerRequestType::deactivate_license, - sizeof(request) + if (!lvh::windows::broker_validation::valid_request( + request, + LvhWindowsBrokerRequestType::deactivate_license )) { response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Invalid broker deactivate request."); @@ -1432,55 +1702,68 @@ namespace { } template - void write_response(HANDLE pipe, const Response &response) { - DWORD bytes_written = 0; - static_cast(::WriteFile( + bool write_response( + HANDLE pipe, + const Response &response, + HANDLE requested_stop_event + ) { + return write_pipe_message( pipe, - &response, - sizeof(response), - &bytes_written, - nullptr - )); + std::as_bytes(std::span {&response, 1}), + requested_stop_event + ); } template - Request request_from_buffer(const std::array &buffer) { + Request request_from_buffer(const std::array &buffer) { Request request {}; std::memcpy(&request, buffer.data(), sizeof(request)); return request; } - void handle_pipe_client(HANDLE pipe) { + void handle_pipe_client(HANDLE pipe, HANDLE requested_stop_event) { broker_state().cleanup_devices(); - std::array request_buffer {}; + const auto send_response = [pipe, requested_stop_event](const auto &response) { + return write_response(pipe, response, requested_stop_event); + }; + + std::array request_buffer {}; DWORD bytes_read = 0; - if (::ReadFile(pipe, request_buffer.data(), static_cast(request_buffer.size()), &bytes_read, nullptr) == FALSE || bytes_read < sizeof(LvhWindowsBrokerRequestHeader)) { + if (!read_pipe_message(pipe, request_buffer, requested_stop_event, bytes_read) || bytes_read < sizeof(LvhWindowsBrokerRequestHeader)) { auto response = broker_state().handle_status(); response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Broker request was empty or truncated."); - write_response(pipe, response); + static_cast(send_response(response)); return; } const auto header = request_from_buffer(request_buffer); - if (header.version != LVH_WINDOWS_BROKER_PROTOCOL_VERSION || header.size > bytes_read) { + if (header.version != LVH_WINDOWS_BROKER_PROTOCOL_VERSION || header.size != bytes_read) { auto response = broker_state().handle_status(); response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Broker request header is invalid."); - write_response(pipe, response); + static_cast(send_response(response)); return; } switch (static_cast(header.type)) { case LvhWindowsBrokerRequestType::status: - write_response(pipe, broker_state().handle_status()); - return; + if (header.size == sizeof(LvhWindowsBrokerStatusRequest)) { + const auto request = request_from_buffer(request_buffer); + if (lvh::windows::broker_validation::valid_request(request)) { + static_cast(send_response(broker_state().handle_status())); + return; + } + } + break; case LvhWindowsBrokerRequestType::create_gamepad: if (header.size == sizeof(LvhWindowsBrokerCreateGamepadRequest)) { const auto request = request_from_buffer(request_buffer); - write_response(pipe, broker_state().handle_create(request, pipe_client_process_id(pipe))); + static_cast(send_response( + broker_state().handle_create(request, pipe_client_process_id(pipe)) + )); return; } break; @@ -1488,7 +1771,7 @@ namespace { case LvhWindowsBrokerRequestType::destroy_device: if (header.size == sizeof(LvhWindowsBrokerDestroyDeviceRequest)) { const auto request = request_from_buffer(request_buffer); - write_response(pipe, broker_state().handle_destroy(request)); + static_cast(send_response(broker_state().handle_destroy(request))); return; } break; @@ -1496,7 +1779,7 @@ namespace { case LvhWindowsBrokerRequestType::activate_license: if (header.size == sizeof(LvhWindowsBrokerLicenseRequest)) { const auto request = request_from_buffer(request_buffer); - write_response(pipe, broker_state().handle_activate_license(request)); + static_cast(send_response(broker_state().handle_activate_license(request))); return; } break; @@ -1504,7 +1787,7 @@ namespace { case LvhWindowsBrokerRequestType::validate_license: if (header.size == sizeof(LvhWindowsBrokerLicenseRequest)) { const auto request = request_from_buffer(request_buffer); - write_response(pipe, broker_state().handle_validate_license(request)); + static_cast(send_response(broker_state().handle_validate_license(request))); return; } break; @@ -1512,7 +1795,7 @@ namespace { case LvhWindowsBrokerRequestType::deactivate_license: if (header.size == sizeof(LvhWindowsBrokerLicenseRequest)) { const auto request = request_from_buffer(request_buffer); - write_response(pipe, broker_state().handle_deactivate_license(request)); + static_cast(send_response(broker_state().handle_deactivate_license(request))); return; } break; @@ -1521,7 +1804,7 @@ namespace { auto response = broker_state().handle_status(); response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument); copy_c_string(response.message, "Broker request type or size is unsupported."); - write_response(pipe, response); + static_cast(send_response(response)); } std::optional wait_for_pipe_client(HANDLE requested_stop_event) { @@ -1539,12 +1822,12 @@ namespace { .bInheritHandle = FALSE, }; - auto pipe = make_unique_handle(::CreateNamedPipeA(LVH_WINDOWS_BROKER_PIPE_PATH, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, pipe_buffer_size, pipe_buffer_size, 1000, &security_attributes)); + auto pipe = make_unique_handle(::CreateNamedPipeA(LVH_WINDOWS_BROKER_PIPE_PATH, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, pipe_buffer_size, pipe_buffer_size, pipe_connect_timeout, &security_attributes)); if (!pipe) { return std::nullopt; } - auto connected_event = make_unique_handle(::CreateEventA(nullptr, TRUE, FALSE, nullptr)); + auto connected_event = make_unique_handle(::CreateEventW(nullptr, TRUE, FALSE, nullptr)); if (!connected_event) { return std::nullopt; } @@ -1554,37 +1837,22 @@ namespace { if (::ConnectNamedPipe(pipe.get(), &overlapped) == FALSE) { const auto error = ::GetLastError(); if (error == ERROR_PIPE_CONNECTED) { - static_cast(::SetEvent(connected_event.get())); - } else if (error != ERROR_IO_PENDING) { + return pipe; + } + if (error != ERROR_IO_PENDING) { return std::nullopt; } - } - - std::array wait_handles { - connected_event.get(), - requested_stop_event, - }; - const auto wait_result = ::WaitForMultipleObjects( - static_cast(wait_handles.size()), - wait_handles.data(), - FALSE, - 1000 - ); - if (wait_result == WAIT_TIMEOUT) { - static_cast(::CancelIoEx(pipe.get(), &overlapped)); - return std::nullopt; - } - if (wait_result == WAIT_OBJECT_0 + 1U) { - static_cast(::CancelIoEx(pipe.get(), &overlapped)); - return std::nullopt; - } - if (wait_result != WAIT_OBJECT_0) { - static_cast(::CancelIoEx(pipe.get(), &overlapped)); - return std::nullopt; - } - if (DWORD ignored = 0; ::GetOverlappedResult(pipe.get(), &overlapped, &ignored, FALSE) == FALSE && ::GetLastError() != ERROR_PIPE_CONNECTED) { - return std::nullopt; + DWORD ignored = 0; + if (!complete_pending_pipe_io( + pipe.get(), + overlapped, + requested_stop_event, + pipe_connect_timeout, + ignored + )) { + return std::nullopt; + } } return pipe; @@ -1622,8 +1890,7 @@ namespace { continue; } - handle_pipe_client(pipe->get()); - static_cast(::FlushFileBuffers(pipe->get())); + handle_pipe_client(pipe->get(), requested_stop_event); static_cast(::DisconnectNamedPipe(pipe->get())); } diff --git a/src/platform/windows/windows_broker_client.cpp b/src/platform/windows/windows_broker_client.cpp index ad86db6..4852861 100644 --- a/src/platform/windows/windows_broker_client.cpp +++ b/src/platform/windows/windows_broker_client.cpp @@ -8,11 +8,13 @@ // standard includes #include +#include #include #include #include #include #include +#include #include #include @@ -27,11 +29,15 @@ #include namespace lvh::detail::windows_broker { - namespace { + namespace windows_broker_client_implementation { using UniqueHandle = std::unique_ptr; + using UniqueServiceHandle = std::unique_ptr< + std::remove_pointer_t, + decltype(&::CloseServiceHandle)>; // GENERIC_READ is required when switching the client end to message-read mode. + constexpr auto broker_service_name = L"libvirtualhid_broker"; constexpr auto pipe_client_access = GENERIC_READ | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES; constexpr auto pipe_client_granted_access = FILE_GENERIC_READ | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES; constexpr auto pipe_wait_timeout = 5000U; @@ -40,14 +46,18 @@ namespace lvh::detail::windows_broker { static_assert(pipe_client_granted_access == 0x0012018BU); static_assert((pipe_client_granted_access & FILE_CREATE_PIPE_INSTANCE) == 0U); - UniqueHandle make_unique_handle(HANDLE handle) { + static UniqueHandle make_unique_handle(HANDLE handle) { if (handle == INVALID_HANDLE_VALUE) { handle = nullptr; } return {handle, &::CloseHandle}; } - std::string windows_error_message(DWORD error_code) { + static UniqueServiceHandle make_unique_service_handle(SC_HANDLE handle) { + return {handle, &::CloseServiceHandle}; + } + + static std::string windows_error_message(DWORD error_code) { std::array message_buffer {}; const auto message_size = ::FormatMessageA( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, @@ -70,7 +80,58 @@ namespace lvh::detail::windows_broker { return message; } - } // namespace + static OperationStatus verify_broker_service(HANDLE pipe, std::string_view operation) { + ULONG server_process_id = 0; + if (::GetNamedPipeServerProcessId(pipe, &server_process_id) == FALSE || server_process_id == 0U) { + const auto error_message = windows_error_message(::GetLastError()); // GCOVR_EXCL_BR_LINE + return OperationStatus::failure( + ErrorCode::backend_unavailable, + std::format("{}: unable to identify named-pipe server: {}", operation, error_message) + ); + } + + auto service_manager = make_unique_service_handle( + ::OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT) + ); + if (!service_manager) { + const auto error_message = windows_error_message(::GetLastError()); // GCOVR_EXCL_BR_LINE + return OperationStatus::failure( + ErrorCode::backend_unavailable, + std::format("{}: unable to open Windows service manager: {}", operation, error_message) + ); + } + + auto service = make_unique_service_handle( + ::OpenServiceW(service_manager.get(), broker_service_name, SERVICE_QUERY_STATUS) + ); + if (!service) { + const auto error_message = windows_error_message(::GetLastError()); // GCOVR_EXCL_BR_LINE + return OperationStatus::failure( + ErrorCode::backend_unavailable, + std::format("{}: installed Windows broker service is unavailable: {}", operation, error_message) + ); + } + + SERVICE_STATUS_PROCESS service_status {}; + if (DWORD bytes_needed = 0; ::QueryServiceStatusEx(service.get(), SC_STATUS_PROCESS_INFO, std::bit_cast(std::as_writable_bytes(std::span {&service_status, 1}).data()), sizeof(service_status), &bytes_needed) == FALSE) { + const auto error_message = windows_error_message(::GetLastError()); // GCOVR_EXCL_BR_LINE + return OperationStatus::failure( + ErrorCode::backend_unavailable, + std::format("{}: unable to verify Windows broker service: {}", operation, error_message) + ); + } + + if (service_status.dwCurrentState != SERVICE_RUNNING || service_status.dwProcessId != server_process_id) { + return OperationStatus::failure( + ErrorCode::backend_unavailable, + std::format("{}: named-pipe server is not the running installed Windows broker service", operation) + ); + } + + return OperationStatus::success(); + } + + } // namespace windows_broker_client_implementation LvhWindowsBrokerRequestHeader make_request_header(LvhWindowsBrokerRequestType type, std::uint32_t size) { return { @@ -115,21 +176,45 @@ namespace lvh::detail::windows_broker { std::span response, std::string_view operation ) { - auto pipe = make_unique_handle(::CreateFileA(LVH_WINDOWS_BROKER_PIPE_PATH, pipe_client_access, 0, nullptr, OPEN_EXISTING, 0, nullptr)); - if (!pipe && ::GetLastError() == ERROR_PIPE_BUSY && ::WaitNamedPipeA(LVH_WINDOWS_BROKER_PIPE_PATH, pipe_wait_timeout) != FALSE) { - pipe = make_unique_handle(::CreateFileA(LVH_WINDOWS_BROKER_PIPE_PATH, pipe_client_access, 0, nullptr, OPEN_EXISTING, 0, nullptr)); + auto pipe = windows_broker_client_implementation::make_unique_handle( + ::CreateFileA( + LVH_WINDOWS_BROKER_PIPE_PATH, + windows_broker_client_implementation::pipe_client_access, + 0, + nullptr, + OPEN_EXISTING, + 0, + nullptr + ) + ); + if (!pipe && ::GetLastError() == ERROR_PIPE_BUSY && ::WaitNamedPipeA(LVH_WINDOWS_BROKER_PIPE_PATH, windows_broker_client_implementation::pipe_wait_timeout) != FALSE) { + pipe = windows_broker_client_implementation::make_unique_handle( + ::CreateFileA( + LVH_WINDOWS_BROKER_PIPE_PATH, + windows_broker_client_implementation::pipe_client_access, + 0, + nullptr, + OPEN_EXISTING, + 0, + nullptr + ) + ); } if (!pipe) { return OperationStatus::failure( ErrorCode::backend_unavailable, - std::format("{}: {}", operation, windows_error_message(::GetLastError())) + std::format("{}: {}", operation, windows_broker_client_implementation::windows_error_message(::GetLastError())) ); } + if (auto status = windows_broker_client_implementation::verify_broker_service(pipe.get(), operation); !status.ok()) { + return status; + } + if (DWORD read_mode = PIPE_READMODE_MESSAGE; ::SetNamedPipeHandleState(pipe.get(), &read_mode, nullptr, nullptr) == FALSE) { return OperationStatus::failure( ErrorCode::backend_unavailable, - std::format("{}: {}", operation, windows_error_message(::GetLastError())) + std::format("{}: {}", operation, windows_broker_client_implementation::windows_error_message(::GetLastError())) ); } @@ -138,7 +223,7 @@ namespace lvh::detail::windows_broker { if (::TransactNamedPipe(pipe.get(), request_copy.data(), static_cast(request_copy.size()), response.data(), static_cast(response.size()), &bytes_read, nullptr) == FALSE) { return OperationStatus::failure( ErrorCode::backend_unavailable, - std::format("{}: {}", operation, windows_error_message(::GetLastError())) + std::format("{}: {}", operation, windows_broker_client_implementation::windows_error_message(::GetLastError())) ); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4bc4a83..4097d48 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,7 @@ set(LIBVIRTUALHID_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_profiles.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_report.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_runtime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_broker_validation.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_protocol.cpp") if(TARGET virtualhid_control_model) @@ -54,8 +55,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif(WIN32) list(APPEND LIBVIRTUALHID_TEST_SOURCES "${PROJECT_SOURCE_DIR}/src/platform/windows/driver/rotating_trace_log.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_broker_client_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_backend_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_backend.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_broker_client.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_consumers.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_driver_protocol.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_rotating_trace_log.cpp") @@ -71,6 +74,7 @@ add_executable(${TEST_BINARY} target_include_directories(${TEST_BINARY} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/include" + "${PROJECT_SOURCE_DIR}/src/platform/windows/broker" "${PROJECT_SOURCE_DIR}/src/platform/windows/shared" "${PROJECT_SOURCE_DIR}/src") diff --git a/tests/fixtures/include/fixtures/windows_broker_client_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_broker_client_test_hooks.hpp new file mode 100644 index 0000000..1659bc7 --- /dev/null +++ b/tests/fixtures/include/fixtures/windows_broker_client_test_hooks.hpp @@ -0,0 +1,37 @@ +/** + * @file tests/fixtures/include/fixtures/windows_broker_client_test_hooks.hpp + * @brief Private Windows broker client test hooks. + */ +#pragma once + +// standard includes +#include + +// lib includes +#include + +namespace lvh::detail::test { + + enum class BrokerServiceScenario { + pipe_process_failure, + zero_pipe_process, + service_manager_failure, + service_failure, + service_query_failure, + service_stopped, + service_process_mismatch, + success, + }; + + struct BrokerServiceVerificationResult { + OperationStatus status; + std::uint32_t closed_pipe_handles = 0; + std::uint32_t closed_service_handles = 0; + bool transacted = false; + }; + + BrokerServiceVerificationResult verify_broker_service_scenario( + BrokerServiceScenario scenario + ); + +} // namespace lvh::detail::test diff --git a/tests/fixtures/windows_broker_client_test_hooks.cpp b/tests/fixtures/windows_broker_client_test_hooks.cpp new file mode 100644 index 0000000..fcce62d --- /dev/null +++ b/tests/fixtures/windows_broker_client_test_hooks.cpp @@ -0,0 +1,207 @@ +/** + * @file tests/fixtures/windows_broker_client_test_hooks.cpp + * @brief Windows broker client test hook definitions. + */ + +// local includes +#include "fixtures/windows_broker_client_test_hooks.hpp" + +// system includes +#include +#include + +// standard includes +#include +#include +#include +#include +#include +#include +#include + +namespace { + + constexpr auto broker_process_id = 42UL; + + struct FakeBrokerClientState { + lvh::detail::test::BrokerServiceScenario scenario = + lvh::detail::test::BrokerServiceScenario::success; + DWORD last_error = ERROR_ACCESS_DENIED; + std::uint32_t closed_pipe_handles = 0; + std::uint32_t closed_service_handles = 0; + bool transacted = false; + }; + + FakeBrokerClientState &fake_state() { + static FakeBrokerClientState state; + return state; + } + + HANDLE fake_pipe_handle() { + static std::byte storage {}; + return &storage; + } + + SC_HANDLE fake_service_manager_handle() { + static SC_HANDLE__ storage {}; + return &storage; + } + + SC_HANDLE fake_service_handle() { + static SC_HANDLE__ storage {}; + return &storage; + } + + BOOL WINAPI fake_close_handle(HANDLE) { + ++fake_state().closed_pipe_handles; + return TRUE; + } + + BOOL WINAPI fake_close_service_handle(SC_HANDLE) { + ++fake_state().closed_service_handles; + return TRUE; + } + + DWORD WINAPI fake_get_last_error() { + return fake_state().last_error; + } + + HANDLE WINAPI fake_create_file_a( + LPCSTR, + DWORD, + DWORD, + LPSECURITY_ATTRIBUTES, + DWORD, + DWORD, + HANDLE + ) { + return fake_pipe_handle(); + } + + BOOL WINAPI fake_wait_named_pipe_a(LPCSTR, DWORD) { + return FALSE; + } + + BOOL WINAPI fake_get_named_pipe_server_process_id(HANDLE, PULONG process_id) { + using enum lvh::detail::test::BrokerServiceScenario; + + if (fake_state().scenario == pipe_process_failure) { + return FALSE; + } + + *process_id = fake_state().scenario == zero_pipe_process ? 0UL : broker_process_id; + return TRUE; + } + + SC_HANDLE WINAPI fake_open_service_manager(LPCWSTR, LPCWSTR, DWORD) { + if (fake_state().scenario == lvh::detail::test::BrokerServiceScenario::service_manager_failure) { + return nullptr; + } + return fake_service_manager_handle(); + } + + SC_HANDLE WINAPI fake_open_service(SC_HANDLE, LPCWSTR, DWORD) { + if (fake_state().scenario == lvh::detail::test::BrokerServiceScenario::service_failure) { + return nullptr; + } + return fake_service_handle(); + } + + BOOL WINAPI fake_query_service_status( + SC_HANDLE, + SC_STATUS_TYPE, + LPBYTE buffer, + DWORD, + LPDWORD bytes_needed + ) { + using enum lvh::detail::test::BrokerServiceScenario; + + if (fake_state().scenario == service_query_failure) { + return FALSE; + } + + SERVICE_STATUS_PROCESS status {}; + status.dwCurrentState = fake_state().scenario == service_stopped ? SERVICE_STOPPED : SERVICE_RUNNING; + status.dwProcessId = fake_state().scenario == service_process_mismatch ? broker_process_id + 1UL : broker_process_id; + *bytes_needed = sizeof(status); + std::memcpy(buffer, &status, sizeof(status)); + return TRUE; + } + + BOOL WINAPI fake_set_named_pipe_handle_state(HANDLE, LPDWORD, LPDWORD, LPDWORD) { + return TRUE; + } + + BOOL WINAPI fake_transact_named_pipe( + HANDLE, + std::byte *, + DWORD, + std::byte *response, + DWORD response_size, + LPDWORD bytes_read, + LPOVERLAPPED + ) { + std::memset(response, 0, response_size); + *bytes_read = response_size; + fake_state().transacted = true; + return TRUE; + } + +} // namespace + +#define CloseHandle fake_close_handle +#define CloseServiceHandle fake_close_service_handle +#define CreateFileA fake_create_file_a +#define GetLastError fake_get_last_error +#define GetNamedPipeServerProcessId fake_get_named_pipe_server_process_id +#define OpenSCManagerW fake_open_service_manager +#define OpenServiceW fake_open_service +#define QueryServiceStatusEx fake_query_service_status +#define SetNamedPipeHandleState fake_set_named_pipe_handle_state +#define TransactNamedPipe fake_transact_named_pipe +#define WaitNamedPipeA fake_wait_named_pipe_a +#define call_bytes call_bytes_for_windows_broker_client_test_hooks +#define make_request_header make_request_header_for_windows_broker_client_test_hooks +#define response_status response_status_for_windows_broker_client_test_hooks +#include "../../src/platform/windows/windows_broker_client.cpp" +#undef CloseHandle +#undef CloseServiceHandle +#undef CreateFileA +#undef GetLastError +#undef GetNamedPipeServerProcessId +#undef OpenSCManagerW +#undef OpenServiceW +#undef QueryServiceStatusEx +#undef SetNamedPipeHandleState +#undef TransactNamedPipe +#undef WaitNamedPipeA +#undef call_bytes +#undef make_request_header +#undef response_status + +namespace lvh::detail::test { + + BrokerServiceVerificationResult verify_broker_service_scenario( + BrokerServiceScenario scenario + ) { + fake_state() = { + .scenario = scenario, + }; + + std::array request {}; + std::array response {}; + auto status = windows_broker::call_bytes_for_windows_broker_client_test_hooks( + std::span {request}, + std::span {response}, + "Test broker request" + ); + + return { + .status = std::move(status), + .closed_pipe_handles = fake_state().closed_pipe_handles, + .closed_service_handles = fake_state().closed_service_handles, + .transacted = fake_state().transacted, + }; + } + +} // namespace lvh::detail::test diff --git a/tests/unit/test_windows_broker_client.cpp b/tests/unit/test_windows_broker_client.cpp new file mode 100644 index 0000000..02c019b --- /dev/null +++ b/tests/unit/test_windows_broker_client.cpp @@ -0,0 +1,58 @@ +/** + * @file tests/unit/test_windows_broker_client.cpp + * @brief Tests for Windows broker service identity verification. + */ + +// local includes +#include "fixtures/windows_broker_client_test_hooks.hpp" + +// test includes +#include + +// standard includes +#include + +namespace { + + struct FailureCase { + lvh::detail::test::BrokerServiceScenario scenario; + std::string_view message; + std::uint32_t closed_service_handles; + }; + +} // namespace + +TEST(WindowsBrokerClientTest, RejectsUnverifiedBrokerServiceEndpoints) { + using enum lvh::detail::test::BrokerServiceScenario; + + for (const auto &[scenario, message, closed_service_handles] : { + FailureCase {pipe_process_failure, "unable to identify named-pipe server", 0U}, + FailureCase {zero_pipe_process, "unable to identify named-pipe server", 0U}, + FailureCase {service_manager_failure, "unable to open Windows service manager", 0U}, + FailureCase {service_failure, "installed Windows broker service is unavailable", 1U}, + FailureCase {service_query_failure, "unable to verify Windows broker service", 2U}, + FailureCase {service_stopped, "is not the running installed Windows broker service", 2U}, + FailureCase {service_process_mismatch, "is not the running installed Windows broker service", 2U}, + }) { + SCOPED_TRACE(message); + const auto result = lvh::detail::test::verify_broker_service_scenario(scenario); + + EXPECT_FALSE(result.status.ok()); + EXPECT_EQ(result.status.code(), lvh::ErrorCode::backend_unavailable); + EXPECT_NE(result.status.message().find(message), std::string::npos); + EXPECT_EQ(result.closed_pipe_handles, 1U); + EXPECT_EQ(result.closed_service_handles, closed_service_handles); + EXPECT_FALSE(result.transacted); + } +} + +TEST(WindowsBrokerClientTest, TransactsOnlyWithRunningInstalledBrokerService) { + const auto result = lvh::detail::test::verify_broker_service_scenario( + lvh::detail::test::BrokerServiceScenario::success + ); + + EXPECT_TRUE(result.status.ok()); + EXPECT_EQ(result.closed_pipe_handles, 1U); + EXPECT_EQ(result.closed_service_handles, 2U); + EXPECT_TRUE(result.transacted); +} diff --git a/tests/unit/test_windows_broker_validation.cpp b/tests/unit/test_windows_broker_validation.cpp new file mode 100644 index 0000000..edf729d --- /dev/null +++ b/tests/unit/test_windows_broker_validation.cpp @@ -0,0 +1,286 @@ +/** + * @file tests/unit/test_windows_broker_validation.cpp + * @brief Unit tests for validation of untrusted Windows broker messages. + */ + +// local includes +#include "broker_request_validation.hpp" +#include "platform/windows/control_protocol.hpp" + +// test includes +#include + +// standard includes +#include +#include +#include + +// lib includes +#include + +namespace { + + LvhWindowsBrokerRequestHeader request_header( + LvhWindowsBrokerRequestType type, + std::uint32_t size + ) { + return { + .version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION, + .size = size, + .type = std::to_underlying(type), + .reserved0 = 0U, + }; + } + + LvhWindowsBrokerCreateGamepadRequest valid_create_request() { + lvh::CreateGamepadOptions options; + options.profile.device_type = lvh::DeviceType::gamepad; + options.profile.gamepad_kind = lvh::GamepadProfileKind::generic; + options.profile.bus_type = lvh::BusType::usb; + options.profile.vendor_id = 0x1209; + options.profile.product_id = 0x0001; + options.profile.version = 0x0001; + options.profile.input_report_size = 4; + options.profile.output_report_size = 2; + options.profile.name = "Test gamepad"; + options.profile.manufacturer = "LizardByte"; + options.profile.report_descriptor = {0x05, 0x01, 0x09, 0x05}; + + LvhWindowsBrokerCreateGamepadRequest request {}; + request.header = request_header( + LvhWindowsBrokerRequestType::create_gamepad, + sizeof(request) + ); + request.client_control_handle = 1U; + request.gamepad = lvh::detail::windows::make_create_gamepad_request(1U, options); + return request; + } + + LvhWindowsBrokerDestroyDeviceRequest valid_destroy_request() { + LvhWindowsSessionToken token {}; + token.bytes[0] = 1U; + + LvhWindowsBrokerDestroyDeviceRequest request {}; + request.header = request_header( + LvhWindowsBrokerRequestType::destroy_device, + sizeof(request) + ); + request.device = lvh::detail::windows::make_destroy_device_request(1U, token); + return request; + } + + LvhWindowsBrokerLicenseRequest valid_license_request(LvhWindowsBrokerRequestType type) { + LvhWindowsBrokerLicenseRequest request {}; + request.header = request_header(type, sizeof(request)); + if (type == LvhWindowsBrokerRequestType::activate_license) { + std::memcpy(request.license_key, "test-key", sizeof("test-key")); + std::memcpy(request.instance_name, "test-machine", sizeof("test-machine")); + } + return request; + } + +} // namespace + +TEST(WindowsBrokerValidationTest, ValidatesStatusHeaderAndReservedField) { + LvhWindowsBrokerStatusRequest request {}; + request.header = request_header( + LvhWindowsBrokerRequestType::status, + sizeof(request) + ); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(request)); + + request.header.reserved0 = 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + request.header.reserved0 = 0U; + ++request.header.size; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + request.header.size = sizeof(request); + ++request.header.version; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + request.header.version = LVH_WINDOWS_BROKER_PROTOCOL_VERSION; + request.header.type = std::to_underlying(LvhWindowsBrokerRequestType::validate_license); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); +} + +TEST(WindowsBrokerValidationTest, RejectsMalformedCreateFields) { + const auto valid = valid_create_request(); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(valid)); + + auto request = valid; + request.client_control_handle = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + ++request.header.version; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + ++request.gamepad.version; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.client_device_id = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + ++request.gamepad.size; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.bus_type = LVH_WINDOWS_BUS_UNKNOWN; + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.bus_type = LVH_WINDOWS_BUS_BLUETOOTH; + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.bus_type = 99U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.gamepad_kind = 99U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.flags = 0x80000000U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.hardware_ids.reserved0[0] = 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.report_descriptor_size = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.input_report_size = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.report_descriptor_size = LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_sizes.name_size = sizeof(request.gamepad.name); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.name[1] = '\0'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.manufacturer[1] = '\0'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + std::memcpy(request.gamepad.stable_id, "stable", sizeof("stable")); + request.gamepad.report_sizes.stable_id_size = sizeof("stable") - 1U; + request.gamepad.stable_id[1] = '\0'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.name[request.gamepad.report_sizes.name_size] = 'x'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.gamepad.report_descriptor[request.gamepad.report_sizes.report_descriptor_size] = 1U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); +} + +TEST(WindowsBrokerValidationTest, RejectsMalformedDestroyFields) { + const auto valid = valid_destroy_request(); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(valid)); + + auto request = valid; + request.device.size = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + ++request.header.version; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + ++request.device.version; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + request.device.driver_device_id = 0U; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); + + request = valid; + std::ranges::fill(request.device.session_token.bytes, std::uint8_t {}); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request)); +} + +TEST(WindowsBrokerValidationTest, RequiresBoundedActivationStrings) { + const auto valid = valid_license_request(LvhWindowsBrokerRequestType::activate_license); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(valid, LvhWindowsBrokerRequestType::activate_license)); + + auto request = valid; + std::ranges::fill(request.license_key, 'x'); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, LvhWindowsBrokerRequestType::activate_license)); + + request = valid; + std::ranges::fill(request.instance_name, 'x'); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, LvhWindowsBrokerRequestType::activate_license)); + + request = valid; + std::ranges::fill(request.license_key, '\0'); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, LvhWindowsBrokerRequestType::activate_license)); + + request = valid; + std::ranges::fill(request.instance_name, '\0'); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(request, LvhWindowsBrokerRequestType::activate_license)); + + request = valid; + request.license_key[sizeof("test-key")] = 'x'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, LvhWindowsBrokerRequestType::activate_license)); +} + +TEST(WindowsBrokerValidationTest, RequiresEmptyValidationAndDeactivationPayloads) { + for (const auto type : { + LvhWindowsBrokerRequestType::validate_license, + LvhWindowsBrokerRequestType::deactivate_license, + }) { + auto request = valid_license_request(type); + EXPECT_TRUE(lvh::windows::broker_validation::valid_request(request, type)); + + request.license_key[0] = 'x'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, type)); + + request = valid_license_request(type); + request.instance_name[0] = 'x'; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, type)); + } +} + +TEST(WindowsBrokerValidationTest, RejectsInvalidLicenseHeadersAndRequestTypes) { + using enum LvhWindowsBrokerRequestType; + + auto request = valid_license_request(validate_license); + ++request.header.size; + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, validate_license)); + + for (const auto type : { + status, + create_gamepad, + destroy_device, + }) { + request = valid_license_request(type); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, type)); + } + + const auto unknown_type = static_cast(999U); + request = valid_license_request(unknown_type); + EXPECT_FALSE(lvh::windows::broker_validation::valid_request(request, unknown_type)); +}