UnitTests: Check that ApprovedInis.json matches embedded hash

Previously we were only checking that game INIs matched
ApprovedInis.json, not that ApprovedInis.json matched the hash
embedded into the binary.
This commit is contained in:
JosJuice 2025-04-19 10:57:10 +02:00
commit bd72ae62a2
4 changed files with 42 additions and 17 deletions

View file

@ -79,29 +79,29 @@ void AchievementManager::Init()
} }
} }
picojson::value AchievementManager::LoadApprovedList() auto AchievementManager::LoadApprovedList() -> std::variant<picojson::value, ErrorString>
{ {
picojson::value temp; picojson::value temp;
std::string error; std::string error;
if (!JsonFromFile(fmt::format("{}{}{}", File::GetSysDirectory(), DIR_SEP, APPROVED_LIST_FILENAME), if (!JsonFromFile(fmt::format("{}{}{}", File::GetSysDirectory(), DIR_SEP, APPROVED_LIST_FILENAME),
&temp, &error)) &temp, &error))
{ {
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load approved game settings list {}", error = fmt::format("Failed to load approved game settings list {}. Error: {}",
APPROVED_LIST_FILENAME); APPROVED_LIST_FILENAME, error);
WARN_LOG_FMT(ACHIEVEMENTS, "Error: {}", error); WARN_LOG_FMT(ACHIEVEMENTS, "{}", error);
return {}; return error;
} }
auto context = Common::SHA1::CreateContext(); auto context = Common::SHA1::CreateContext();
context->Update(temp.serialize()); context->Update(temp.serialize());
auto digest = context->Finish(); auto digest = context->Finish();
if (digest != APPROVED_LIST_HASH) if (digest != APPROVED_LIST_HASH)
{ {
WARN_LOG_FMT(ACHIEVEMENTS, "Failed to verify approved game settings list {}", error = fmt::format(
APPROVED_LIST_FILENAME); "Failed to verify approved game settings list {}. Expected hash {}, found hash {}",
WARN_LOG_FMT(ACHIEVEMENTS, "Expected hash {}, found hash {}", APPROVED_LIST_FILENAME, Common::SHA1::DigestToString(APPROVED_LIST_HASH),
Common::SHA1::DigestToString(APPROVED_LIST_HASH),
Common::SHA1::DigestToString(digest)); Common::SHA1::DigestToString(digest));
return {}; WARN_LOG_FMT(ACHIEVEMENTS, "{}", error);
return error;
} }
return temp; return temp;
} }
@ -386,6 +386,15 @@ bool AchievementManager::IsHardcoreModeActive() const
return rc_client_is_processing_required(m_client); return rc_client_is_processing_required(m_client);
} }
bool AchievementManager::IsApprovedCodesListValid(std::string* error_out) const
{
std::lock_guard lg{m_lock};
const bool is_valid = std::holds_alternative<picojson::value>(*m_ini_root);
if (error_out && !is_valid)
*error_out = std::get<std::string>(*m_ini_root);
return is_valid;
}
template <typename T> template <typename T>
void AchievementManager::FilterApprovedIni(std::vector<T>& codes, const std::string& game_id, void AchievementManager::FilterApprovedIni(std::vector<T>& codes, const std::string& game_id,
u16 revision) const u16 revision) const
@ -402,7 +411,7 @@ void AchievementManager::FilterApprovedIni(std::vector<T>& codes, const std::str
return; return;
// Approved codes list failed to hash // Approved codes list failed to hash
if (!m_ini_root->is<picojson::value::object>()) if (!std::holds_alternative<picojson::value>(*m_ini_root))
{ {
codes.clear(); codes.clear();
return; return;
@ -423,11 +432,13 @@ bool AchievementManager::CheckApprovedCode(const T& code, const std::string& gam
return true; return true;
// Approved codes list failed to hash // Approved codes list failed to hash
if (!m_ini_root->is<picojson::value::object>()) if (!std::holds_alternative<picojson::value>(*m_ini_root))
return false; return false;
INFO_LOG_FMT(ACHIEVEMENTS, "Verifying code {}", code.name); INFO_LOG_FMT(ACHIEVEMENTS, "Verifying code {}", code.name);
const picojson::value& ini_root = std::get<picojson::value>(*m_ini_root);
bool verified = false; bool verified = false;
auto hash = Common::SHA1::DigestToString(GetCodeHash(code)); auto hash = Common::SHA1::DigestToString(GetCodeHash(code));
@ -435,7 +446,7 @@ bool AchievementManager::CheckApprovedCode(const T& code, const std::string& gam
for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision)) for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision))
{ {
auto config = filename.substr(0, filename.length() - 4); auto config = filename.substr(0, filename.length() - 4);
if (m_ini_root->contains(config) && m_ini_root->get(config).contains(hash)) if (ini_root.contains(config) && ini_root.get(config).contains(hash))
verified = true; verified = true;
} }

View file

@ -18,6 +18,7 @@
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <variant>
#include <vector> #include <vector>
#include <rcheevos/include/rc_api_runtime.h> #include <rcheevos/include/rc_api_runtime.h>
@ -134,6 +135,7 @@ public:
std::recursive_mutex& GetLock(); std::recursive_mutex& GetLock();
bool IsHardcoreModeActive() const; bool IsHardcoreModeActive() const;
bool IsApprovedCodesListValid(std::string* error_out = nullptr) const;
void FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches, const std::string& game_id, void FilterApprovedPatches(std::vector<PatchEngine::Patch>& patches, const std::string& game_id,
u16 revision) const; u16 revision) const;
void FilterApprovedGeckoCodes(std::vector<Gecko::GeckoCode>& codes, const std::string& game_id, void FilterApprovedGeckoCodes(std::vector<Gecko::GeckoCode>& codes, const std::string& game_id,
@ -176,7 +178,8 @@ private:
std::unique_ptr<DiscIO::Volume> volume; std::unique_ptr<DiscIO::Volume> volume;
}; };
static picojson::value LoadApprovedList(); using ErrorString = std::string;
static std::variant<picojson::value, ErrorString> LoadApprovedList();
static void* FilereaderOpenByFilepath(const char* path_utf8); static void* FilereaderOpenByFilepath(const char* path_utf8);
static void* FilereaderOpenByVolume(const char* path_utf8); static void* FilereaderOpenByVolume(const char* path_utf8);
@ -259,7 +262,7 @@ private:
std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point m_last_progress_message = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point m_last_progress_message = std::chrono::steady_clock::now();
Common::Lazy<picojson::value> m_ini_root{LoadApprovedList}; Common::Lazy<std::variant<picojson::value, ErrorString>> m_ini_root{LoadApprovedList};
std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map; std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map;
bool m_challenges_updated = false; bool m_challenges_updated = false;
@ -302,6 +305,8 @@ public:
constexpr bool IsHardcoreModeActive() { return false; } constexpr bool IsHardcoreModeActive() { return false; }
constexpr bool IsApprovedCodesListValid(std::string* error_out = nullptr) { return true; }
constexpr bool CheckApprovedGeckoCode(const Gecko::GeckoCode& code, const std::string& game_id) constexpr bool CheckApprovedGeckoCode(const Gecko::GeckoCode& code, const std::string& game_id)
{ {
return true; return true;

View file

@ -18,6 +18,7 @@
#include "Common/IOFile.h" #include "Common/IOFile.h"
#include "Common/IniFile.h" #include "Common/IniFile.h"
#include "Common/JsonUtil.h" #include "Common/JsonUtil.h"
#include "Core/AchievementManager.h"
#include "Core/ActionReplay.h" #include "Core/ActionReplay.h"
#include "Core/CheatCodes.h" #include "Core/CheatCodes.h"
#include "Core/GeckoCode.h" #include "Core/GeckoCode.h"
@ -38,7 +39,14 @@ void ReadVerified(const Common::IniFile& ini, const std::string& filename,
void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash, void CheckHash(const std::string& game_id, GameHashes* game_hashes, const std::string& hash,
const std::string& patch_name); const std::string& patch_name);
TEST(PatchAllowlist, VerifyHashes) TEST(PatchAllowlist, VerifyJsonMatchesExecutable)
{
std::string error;
if (!AchievementManager::GetInstance().IsApprovedCodesListValid(&error))
ADD_FAILURE() << error;
}
TEST(PatchAllowlist, VerifyInisMatchJson)
{ {
// Load allowlist // Load allowlist
static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json"; static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";

View file

@ -103,6 +103,7 @@
<Import Project="$(ExternalsDir)Bochs_disasm\exports.props" /> <Import Project="$(ExternalsDir)Bochs_disasm\exports.props" />
<Import Project="$(ExternalsDir)fmt\exports.props" /> <Import Project="$(ExternalsDir)fmt\exports.props" />
<Import Project="$(ExternalsDir)picojson\exports.props" /> <Import Project="$(ExternalsDir)picojson\exports.props" />
<Import Project="$(ExternalsDir)rcheevos\exports.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets"> <ImportGroup Label="ExtensionTargets">
</ImportGroup> </ImportGroup>