/** * @file tests/unit/test_nvhttp_client_auth.cpp * @brief Test exact paired-client certificate authorization and persistence. */ #include "../certificate_test_utils.h" #include "../tests_common.h" // standard includes #include #include #include #include // local includes #include #include namespace fs = std::filesystem; /** * @brief Isolate paired-client authorization tests from the user's Sunshine state. */ class ClientAuthorizationTest: public BaseTest { protected: /** * @brief Redirect persisted client state to the test build directory. */ void SetUp() override { BaseTest::SetUp(); original_state_file = config::nvhttp.file_state; original_fresh_state = config::sunshine.flags[config::flag::FRESH_STATE]; state_file = fs::path {SUNSHINE_TEST_BIN_DIR} / "client_authorization_state.json"; config::nvhttp.file_state = state_file.string(); config::sunshine.flags[config::flag::FRESH_STATE] = false; nvhttp::test_support::reset_client_state(); std::error_code remove_error; fs::remove(state_file, remove_error); } /** * @brief Remove test state and restore the caller's configuration. */ void TearDown() override { nvhttp::test_support::reset_client_state(); std::error_code remove_error; fs::remove(state_file, remove_error); config::nvhttp.file_state = original_state_file; config::sunshine.flags[config::flag::FRESH_STATE] = original_fresh_state; BaseTest::TearDown(); } private: fs::path state_file; ///< Task-specific persisted state fixture. std::string original_state_file; ///< State-file setting restored after each test. bool original_fresh_state; ///< Fresh-state flag restored after each test. }; TEST_F(ClientAuthorizationTest, CanonicalIdentityFailsClosedAndTracksEnableState) { const auto paired_credentials = test_utils::certificates::generate_ca_credentials(); const auto crlf_certificate = test_utils::certificates::to_crlf_pem(paired_credentials.x509); const auto unknown_credentials = crypto::gen_creds("Sunshine Unknown Client", 2048); const auto uuid = nvhttp::test_support::add_client("paired", crlf_certificate, true); EXPECT_TRUE(nvhttp::test_support::add_client("invalid", "not a certificate", true).empty()); ASSERT_FALSE(uuid.empty()); EXPECT_EQ(nvhttp::get_cert_by_uuid(uuid), paired_credentials.x509); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(crlf_certificate)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(unknown_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate("not a certificate")); ASSERT_TRUE(nvhttp::set_client_enabled(uuid, false)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); ASSERT_TRUE(nvhttp::set_client_enabled(uuid, true)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); } TEST_F(ClientAuthorizationTest, DerivedLeafDoesNotInheritPairedAuthorization) { const auto paired_credentials = test_utils::certificates::generate_ca_credentials(); const auto derived_credentials = test_utils::certificates::generate_derived_leaf(paired_credentials); const auto uuid = nvhttp::test_support::add_client("paired issuer", paired_credentials.x509, true); ASSERT_FALSE(uuid.empty()); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(derived_credentials.x509)); ASSERT_TRUE(nvhttp::set_client_enabled(uuid, false)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(derived_credentials.x509)); } TEST_F(ClientAuthorizationTest, MultipleClientsPersistAndUnpairIndependently) { const auto enabled_credentials = test_utils::certificates::generate_ca_credentials("Sunshine Enabled Client"); const auto disabled_credentials = crypto::gen_creds("Sunshine Disabled Client", 2048); const auto expired_credentials = test_utils::certificates::expire_credentials( test_utils::certificates::generate_ca_credentials("Sunshine Expired Client") ); const auto enabled_uuid = nvhttp::test_support::add_client("enabled", enabled_credentials.x509, true); const auto disabled_uuid = nvhttp::test_support::add_client("disabled", disabled_credentials.x509, false); const auto expired_uuid = nvhttp::test_support::add_client("expired", expired_credentials.x509, true); ASSERT_FALSE(enabled_uuid.empty()); ASSERT_FALSE(disabled_uuid.empty()); ASSERT_FALSE(expired_uuid.empty()); EXPECT_EQ(nvhttp::get_all_clients().size(), 3); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(enabled_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(disabled_credentials.x509)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(expired_credentials.x509)); nvhttp::test_support::reset_client_state(); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(enabled_credentials.x509)); nvhttp::test_support::reload_client_state(); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(enabled_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(disabled_credentials.x509)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(expired_credentials.x509)); ASSERT_TRUE(nvhttp::set_client_enabled(disabled_uuid, true)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(disabled_credentials.x509)); ASSERT_TRUE(nvhttp::unpair_client(enabled_uuid)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(enabled_credentials.x509)); nvhttp::erase_all_clients(); nvhttp::test_support::reset_client_state(); nvhttp::test_support::reload_client_state(); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(disabled_credentials.x509)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(expired_credentials.x509)); } TEST_F(ClientAuthorizationTest, DuplicateCertificateIdentityFailsClosed) { const auto credentials = test_utils::certificates::generate_ca_credentials(); const auto uuid = nvhttp::test_support::add_client("first", credentials.x509, true); ASSERT_FALSE(uuid.empty()); EXPECT_FALSE(nvhttp::test_support::duplicate_client("missing")); ASSERT_TRUE(nvhttp::test_support::duplicate_client(uuid)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(credentials.x509)); } TEST_F(ClientAuthorizationTest, RePairingReplacesDuplicateCertificateIdentity) { const auto paired_credentials = test_utils::certificates::generate_ca_credentials(); const auto other_credentials = test_utils::certificates::generate_ca_credentials("Sunshine Other Client"); const auto original_uuid = nvhttp::test_support::add_client("original", paired_credentials.x509, true); const auto other_uuid = nvhttp::test_support::add_client("other", other_credentials.x509, true); ASSERT_FALSE(original_uuid.empty()); ASSERT_FALSE(other_uuid.empty()); ASSERT_TRUE(nvhttp::test_support::duplicate_client(original_uuid)); ASSERT_EQ(nvhttp::get_all_clients().size(), 3); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); const auto repaired_uuid = nvhttp::test_support::add_client( "repaired", test_utils::certificates::to_crlf_pem(paired_credentials.x509), true ); ASSERT_FALSE(repaired_uuid.empty()); EXPECT_NE(repaired_uuid, original_uuid); EXPECT_EQ(nvhttp::get_all_clients().size(), 2); EXPECT_EQ(nvhttp::get_cert_by_uuid(repaired_uuid), paired_credentials.x509); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(other_credentials.x509)); nvhttp::test_support::reset_client_state(); nvhttp::test_support::reload_client_state(); EXPECT_EQ(nvhttp::get_all_clients().size(), 2); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(paired_credentials.x509)); EXPECT_TRUE(nvhttp::test_support::authorize_client_certificate(other_credentials.x509)); } TEST_F(ClientAuthorizationTest, ConcurrentStateChangesRemainConsistent) { const auto credentials = test_utils::certificates::generate_ca_credentials(); const auto uuid = nvhttp::test_support::add_client("concurrent", credentials.x509, true); ASSERT_FALSE(uuid.empty()); std::atomic_bool operations_succeeded {true}; std::vector workers; for (std::size_t worker = 0; worker < 4; ++worker) { workers.emplace_back([&operations_succeeded, uuid, certificate = credentials.x509, worker]() { for (std::size_t iteration = 0; iteration < 8; ++iteration) { if (!nvhttp::set_client_enabled(uuid, (worker + iteration) % 2 == 0)) { operations_succeeded = false; } static_cast(nvhttp::test_support::authorize_client_certificate(certificate)); } }); } workers.clear(); EXPECT_TRUE(operations_succeeded); ASSERT_TRUE(nvhttp::set_client_enabled(uuid, false)); EXPECT_FALSE(nvhttp::test_support::authorize_client_certificate(credentials.x509)); }