fix(launcher): bootstrap release state and signed manifest
This commit is contained in:
+19
-1
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
@@ -118,6 +118,8 @@
|
|||||||
|
|
||||||
带签名的 JSON 合约必须使用确定性序列化,从已签名的负载中省略签名字段,标识算法和密钥 ID,并支持受控的公钥轮换窗口。验证失败将导致系统关闭。
|
带签名的 JSON 合约必须使用确定性序列化,从已签名的负载中省略签名字段,标识算法和密钥 ID,并支持受控的公钥轮换窗口。验证失败将导致系统关闭。
|
||||||
|
|
||||||
|
首次安装可能尚无当前版本的本地清单缓存。当设备在线、当前版本已在服务器显式发布且没有更高目标版本时,Launcher 会通过与 Updater 相同的校验逻辑获取当前版本清单,验证签名和身份后写入缓存,再执行完整文件校验。服务器未发布当前版本、返回无效版本 ID、清单签名无效或缓存写入失败时,启动保持关闭失败。此流程不会自动创建或发布服务器版本。
|
||||||
|
|
||||||
## 运行时布局
|
## 运行时布局
|
||||||
|
|
||||||
父安装程序拥有最终的布局。预期的可执行文件关系为:
|
父安装程序拥有最终的布局。预期的可执行文件关系为:
|
||||||
@@ -157,4 +159,4 @@ config/
|
|||||||
* 频道选择和禁止降级行为;
|
* 频道选择和禁止降级行为;
|
||||||
* 中断更新恢复;
|
* 中断更新恢复;
|
||||||
* 候选失败和成功回滚;
|
* 候选失败和成功回滚;
|
||||||
* 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。
|
* 从实际安装位置启动并更新,通过正常的最终用户交互并需要提升权限。
|
||||||
|
|||||||
@@ -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` 的文件,不是受支持的运行时覆盖。
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user