fix(launcher): bootstrap release state and signed manifest

This commit is contained in:
Comely
2026-07-10 02:38:04 -07:00
parent 9d632f3323
commit bec05cdbc8
13 changed files with 150 additions and 10 deletions
+19 -1
View File
@@ -117,12 +117,30 @@ endif()
add_subdirectory(Bootstrap) add_subdirectory(Bootstrap)
add_subdirectory(Common) add_subdirectory(Common)
add_library(UpdateClientUpdaterLogic STATIC
Updater/UpdaterLogic.h
Updater/UpdaterLogic.cpp
)
target_compile_features(UpdateClientUpdaterLogic PUBLIC cxx_std_17)
target_include_directories(UpdateClientUpdaterLogic PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/Updater"
)
target_link_libraries(UpdateClientUpdaterLogic
PUBLIC
Common
Qt5::Core
Qt5::Network
Qt5::Gui
Qt5::Widgets
)
add_subdirectory(Launcher) add_subdirectory(Launcher)
add_subdirectory(Updater) add_subdirectory(Updater)
add_subdirectory(MainApp) add_subdirectory(MainApp)
foreach(_update_client_primary_target IN ITEMS foreach(_update_client_primary_target IN ITEMS
Bootstrap Common Launcher Updater MainApp) Bootstrap Common UpdateClientUpdaterLogic Launcher Updater MainApp)
add_dependencies("${_update_client_primary_target}" UpdateClientNoHanCheck) add_dependencies("${_update_client_primary_target}" UpdateClientNoHanCheck)
endforeach() endforeach()
+2
View File
@@ -5,6 +5,8 @@ set(_common_sources
FileHelper.cpp FileHelper.cpp
ConfigHelper.h ConfigHelper.h
ConfigHelper.cpp ConfigHelper.cpp
InitialStatePolicy.h
ManifestBootstrapPolicy.h
PolicyHelper.h PolicyHelper.h
PolicyHelper.cpp PolicyHelper.cpp
LocalStateHelper.h LocalStateHelper.h
+14 -2
View File
@@ -1,4 +1,5 @@
#include "ConfigHelper.h" #include "ConfigHelper.h"
#include "InitialStatePolicy.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
@@ -583,9 +584,20 @@ bool ConfigHelper::initializeStateDefaults()
for (const QString& key : mutableKeys()) for (const QString& key : mutableKeys())
{ {
if (!m_initialState.contains(key))
continue;
const QString storageKey = stateStorageKey(key); const QString storageKey = stateStorageKey(key);
if (!settings.contains(storageKey) && m_initialState.contains(key)) const bool storedValueExists = settings.contains(storageKey);
settings.setValue(storageKey, m_initialState.value(key).toString()); const QString storedValue = storedValueExists
? settings.value(storageKey).toString()
: QString();
const QString initialValue = m_initialState.value(key).toString();
if (UpdateClient::shouldApplyInitialStateValue(
key, storedValueExists, storedValue, initialValue))
{
settings.setValue(storageKey, initialValue);
}
} }
settings.sync(); settings.sync();
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QString>
namespace UpdateClient
{
inline bool shouldApplyInitialStateValue(const QString& key,
bool storedValueExists,
const QString& storedValue,
const QString& initialValue)
{
if (!storedValueExists)
return true;
return key == QStringLiteral("license_key")
&& storedValue.trimmed().isEmpty()
&& !initialValue.trimmed().isEmpty();
}
}
+12 -1
View File
@@ -19,6 +19,11 @@ IntegrityHelper::IntegrityHelper(const QString& installDir)
QString IntegrityHelper::errorString() const { return m_error; } QString IntegrityHelper::errorString() const { return m_error; }
bool IntegrityHelper::isManifestCacheMissing() const
{
return m_manifestCacheMissing;
}
bool IntegrityHelper::safeRelativePath(const QString& path) const bool IntegrityHelper::safeRelativePath(const QString& path) const
{ {
const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path)); const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path));
@@ -82,14 +87,20 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
const QString& version) const QString& version)
{ {
m_error.clear(); m_error.clear();
m_manifestCacheMissing = false;
QString cachePath = QDir(ConfigHelper::instance().updateRoot()).filePath( QString cachePath = QDir(ConfigHelper::instance().updateRoot()).filePath(
"manifest_cache/manifest_" + version + ".json"); "manifest_cache/manifest_" + version + ".json");
const QString legacyCachePath = QDir(m_installDir).filePath( const QString legacyCachePath = QDir(m_installDir).filePath(
"update/manifest_cache/manifest_" + version + ".json"); "update/manifest_cache/manifest_" + version + ".json");
if (!QFile::exists(cachePath)) if (!QFile::exists(cachePath))
cachePath = legacyCachePath; cachePath = legacyCachePath;
if (!QFile::exists(cachePath)) {
m_manifestCacheMissing = true;
m_error = "signed manifest cache missing for " + version;
return false;
}
QFile cache(cachePath); QFile cache(cachePath);
if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; } if (!cache.open(QIODevice::ReadOnly)) { m_error = "cannot read signed manifest cache for " + version; return false; }
QJsonParseError wrapperError; QJsonParseError wrapperError;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError); const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError);
if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
+2
View File
@@ -7,6 +7,7 @@ public:
explicit IntegrityHelper(const QString& installDir); explicit IntegrityHelper(const QString& installDir);
bool verifyInstalledVersion(const QString& appId, const QString& channel, bool verifyInstalledVersion(const QString& appId, const QString& channel,
const QString& version); const QString& version);
bool isManifestCacheMissing() const;
QString errorString() const; QString errorString() const;
private: private:
@@ -17,4 +18,5 @@ private:
QString m_installDir; QString m_installDir;
QString m_error; QString m_error;
bool m_manifestCacheMissing = false;
}; };
+24
View File
@@ -0,0 +1,24 @@
#pragma once
namespace UpdateClient
{
enum class ManifestBootstrapAction
{
None,
FetchCurrentManifest,
CurrentVersionNotPublished
};
inline ManifestBootstrapAction manifestBootstrapAction(
bool manifestCacheMissing,
bool networkAvailable,
bool updateAvailable,
int serverVersionId)
{
if (!manifestCacheMissing || !networkAvailable || updateAvailable)
return ManifestBootstrapAction::None;
if (serverVersionId <= 0)
return ManifestBootstrapAction::CurrentVersionNotPublished;
return ManifestBootstrapAction::FetchCurrentManifest;
}
}
+1
View File
@@ -17,6 +17,7 @@ target_compile_features(Launcher PRIVATE cxx_std_17)
target_link_libraries(Launcher target_link_libraries(Launcher
PRIVATE PRIVATE
Common Common
UpdateClientUpdaterLogic
Qt5::Core Qt5::Core
Qt5::Network Qt5::Network
Qt5::Gui Qt5::Gui
+44 -2
View File
@@ -12,7 +12,9 @@
#include "../Common/TicketHelper.h" #include "../Common/TicketHelper.h"
#include "../Common/DeviceIdentityHelper.h" #include "../Common/DeviceIdentityHelper.h"
#include "../Common/IntegrityHelper.h" #include "../Common/IntegrityHelper.h"
#include "../Common/ManifestBootstrapPolicy.h"
#include "../Common/TranslationHelper.h" #include "../Common/TranslationHelper.h"
#include "../Updater/UpdaterLogic.h"
#include <QFile> #include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QDir> #include <QDir>
@@ -198,7 +200,47 @@ int main(int argc, char *argv[])
const QString mainAppPath = QDir(appDir).filePath(mainExecutable); const QString mainAppPath = QDir(appDir).filePath(mainExecutable);
const QString updaterPath = QDir(appDir).filePath(updaterExecutable); const QString updaterPath = QDir(appDir).filePath(updaterExecutable);
IntegrityHelper integrity(config.installRoot()); IntegrityHelper integrity(config.installRoot());
if (!integrity.verifyInstalledVersion(appId, channel, currentVersion)) bool integrityVerified = integrity.verifyInstalledVersion(
appId, channel, currentVersion);
QString integrityError = integrity.errorString();
const UpdateClient::ManifestBootstrapAction bootstrapAction =
UpdateClient::manifestBootstrapAction(
integrity.isManifestCacheMissing(), networkOk, needUpdate,
targetVersionId);
if (!integrityVerified
&& bootstrapAction
== UpdateClient::ManifestBootstrapAction::FetchCurrentManifest)
{
UpdaterLogic manifestLogic;
manifestLogic.getManifest(
appId, channel, currentVersion, targetVersionId);
const QString cacheDirectory = QDir(config.updateRoot()).filePath(
QStringLiteral("manifest_cache"));
if (manifestLogic.verifyManifestSignature()
&& manifestLogic.saveManifestCache(cacheDirectory))
{
integrityVerified = integrity.verifyInstalledVersion(
appId, channel, currentVersion);
integrityError = integrity.errorString();
}
else
{
integrityError = QCoreApplication::translate(
"Launcher",
"The signed manifest for version %1 could not be fetched or cached.")
.arg(currentVersion);
}
}
else if (!integrityVerified
&& bootstrapAction
== UpdateClient::ManifestBootstrapAction::CurrentVersionNotPublished)
{
integrityError = QCoreApplication::translate(
"Launcher",
"Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified.")
.arg(currentVersion);
}
if (!integrityVerified)
{ {
progress.close(); progress.close();
QMessageBox::critical( QMessageBox::critical(
@@ -207,7 +249,7 @@ int main(int argc, char *argv[])
QCoreApplication::translate( QCoreApplication::translate(
"Launcher", "Launcher",
"Application files failed signed manifest verification:\n%1") "Application files failed signed manifest verification:\n%1")
.arg(integrity.errorString())); .arg(integrityError));
return -1; return -1;
} }
const auto importOfflinePackage = [&]() const auto importOfflinePackage = [&]()
+1 -2
View File
@@ -1,7 +1,5 @@
set(_updater_sources set(_updater_sources
main.cpp main.cpp
UpdaterLogic.h
UpdaterLogic.cpp
UpdateTransaction.h UpdateTransaction.h
UpdateTransaction.cpp UpdateTransaction.cpp
) )
@@ -11,6 +9,7 @@ target_compile_features(Updater PRIVATE cxx_std_17)
target_link_libraries(Updater target_link_libraries(Updater
PRIVATE PRIVATE
Common Common
UpdateClientUpdaterLogic
Qt5::Core Qt5::Core
Qt5::Network Qt5::Network
Qt5::Gui Qt5::Gui
+3 -1
View File
@@ -118,6 +118,8 @@
带签名的 JSON 合约必须使用确定性序列化,从已签名的负载中省略签名字段,标识算法和密钥 ID,并支持受控的公钥轮换窗口。验证失败将导致系统关闭。 带签名的 JSON 合约必须使用确定性序列化,从已签名的负载中省略签名字段,标识算法和密钥 ID,并支持受控的公钥轮换窗口。验证失败将导致系统关闭。
首次安装可能尚无当前版本的本地清单缓存。当设备在线、当前版本已在服务器显式发布且没有更高目标版本时,Launcher 会通过与 Updater 相同的校验逻辑获取当前版本清单,验证签名和身份后写入缓存,再执行完整文件校验。服务器未发布当前版本、返回无效版本 ID、清单签名无效或缓存写入失败时,启动保持关闭失败。此流程不会自动创建或发布服务器版本。
## 运行时布局 ## 运行时布局
父安装程序拥有最终的布局。预期的可执行文件关系为: 父安装程序拥有最终的布局。预期的可执行文件关系为:
@@ -157,4 +159,4 @@ config/
* 频道选择和禁止降级行为; * 频道选择和禁止降级行为;
* 中断更新恢复; * 中断更新恢复;
* 候选失败和成功回滚; * 候选失败和成功回滚;
* 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。 * 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。
+1 -1
View File
@@ -37,7 +37,7 @@
所选的源也包含一个 `initial_state` 对象。它仅提供以下可变键的首次运行默认值。它不会将这些键变为不可变策略,之后的运行时更改仍保留在本地状态存储中。 所选的源也包含一个 `initial_state` 对象。它仅提供以下可变键的首次运行默认值。它不会将这些键变为不可变策略,之后的运行时更改仍保留在本地状态存储中。
发布者必须在未跟踪的 `config/config.json` 中提供已签发且非空的 `initial_state.license_key`。示例配置中的空值只支持开发构建;父级 `package_installer` 会拒绝缺失、空值或明显占位符。该值会作为首次运行默认状态嵌入二进制文件,并由 Launcher 写入系统级状态,不会作为可编辑的安装目录 JSON 交付。 发布者必须在未跟踪的 `config/config.json` 中提供已签发且非空的 `initial_state.license_key`。示例配置中的空值只支持开发构建;父级 `package_installer` 会拒绝缺失、空值或明显占位符。该值会作为初始状态嵌入二进制文件,并在系统级授权状态缺失或为空时由 Launcher 写入;已有的非空机器授权不会被构建默认值覆盖。授权不会作为可编辑的安装目录 JSON 交付。
这些值可能在不同构建之间有所不同,但安装的文件不得覆盖它们。在可执行文件旁边放置名为 `app_config.json``client.ini``config.json` 的文件,不是受支持的运行时覆盖。 这些值可能在不同构建之间有所不同,但安装的文件不得覆盖它们。在可执行文件旁边放置名为 `app_config.json``client.ini``config.json` 的文件,不是受支持的运行时覆盖。
+8
View File
@@ -225,6 +225,14 @@ Target version: %1</source>
<translation> <translation>
%1</translation> %1</translation>
</message> </message>
<message>
<source>The signed manifest for version %1 could not be fetched or cached.</source>
<translation> %1 </translation>
</message>
<message>
<source>Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified.</source>
<translation> %1 便</translation>
</message>
</context> </context>
<context> <context>
<name>LauncherErrorMarker</name> <name>LauncherErrorMarker</name>