[fix] use WinHTTP and Windows security checks for installer updates

This commit is contained in:
dijunkun
2026-09-09 22:07:42 +08:00
parent c2c74913e3
commit 211d4de241
12 changed files with 369 additions and 148 deletions
+3 -2
View File
@@ -92,8 +92,9 @@ jobs:
- name: Generate or update version.json
run: |
# If version.json exists, try to preserve downloads section
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
# Preserve download URLs only for this release, never an older version.
if [ -f version.json ] && jq -e --arg tag "${{ steps.version.outputs.TAG_NAME }}" \
'.tagName == $tag and (.downloads | type == "object")' version.json > /dev/null 2>&1; then
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
@@ -3286,9 +3286,12 @@ void GuiApplication::SyncWindowsUpdate() {
}
const State state = windows_updater_.GetState();
const bool downloading = state == State::Downloading;
ui_->main->set_update_downloading(downloading || state == State::Launching);
ui_->main->set_update_downloading(downloading || state == State::Checking ||
state == State::Launching);
ui_->main->set_update_security_blocked(state == State::SecurityBlocked);
ui_->main->set_update_failed(state == State::Failed ||
state == State::LaunchFailed);
state == State::LaunchFailed ||
state == State::SecurityBlocked);
const int language = localization_language_index_;
std::string status;
if (downloading) {
@@ -3307,6 +3310,11 @@ void GuiApplication::SyncWindowsUpdate() {
status = localization::update_download_failed[language];
} else if (state == State::LaunchFailed) {
status = localization::update_launch_failed[language];
} else if (state == State::Checking) {
status = localization::update_checking[language];
ui_->main->set_update_progress(1.0f);
} else if (state == State::SecurityBlocked) {
status = localization::update_security_blocked[language];
} else if (state == State::Launching) {
status = localization::update_launching[language];
} else if (state == State::Launched) {
@@ -186,6 +186,12 @@ struct TranslationRow {
u8"Доступна новая версия") \
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
u8"Загрузка установщика…") \
X(update_checking, u8"正在进行 Windows 安全检查…", \
"Running Windows security checks…", u8"Проверка безопасности Windows…") \
X(update_security_blocked, \
u8"Windows 安全检查未通过,请查看安全软件日志并核实发布者。", \
"Windows security checks failed. Review security logs and verify the publisher.", \
u8"Проверка Windows не пройдена. Проверьте журнал защиты и издателя.") \
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
"Opening installer. Please confirm the administrator permission request…", \
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
+2 -1
View File
@@ -212,6 +212,7 @@ export component MainWindow inherits Window {
in property <bool> update-available: false;
in property <bool> update-downloading: false;
in property <bool> update-failed: false;
in property <bool> update-security-blocked: false;
in property <float> update-progress: 0;
in property <string> update-status: "";
in property <string> current-version: "";
@@ -1696,7 +1697,7 @@ export component MainWindow inherits Window {
background: #334fd1;
}
}
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading; clicked => { root.start-update(); } }
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading && !root.update-security-blocked; clicked => { root.start-update(); } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
}
}
@@ -0,0 +1,224 @@
#include "windows_installer.h"
#include <windows.h>
#include <shobjidl.h>
#include <winhttp.h>
#include <wrl/client.h>
#include <array>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <system_error>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr uint64_t kMaxInstallerSize = 1024ull * 1024 * 1024;
// Stable identity used only for Windows Attachment Services policy/UI.
constexpr GUID kAttachmentClient = {
0x72d17a6b,
0xeac1,
0x4816,
{0x91, 0x40, 0x33, 0x1b, 0xa7, 0x76, 0x22, 0xcd}};
void CheckWin32(BOOL success, const char* operation) {
if (!success) {
throw std::system_error(static_cast<int>(GetLastError()),
std::system_category(), operation);
}
}
struct InternetCloser {
void operator()(HINTERNET handle) const { WinHttpCloseHandle(handle); }
};
using InternetHandle = std::unique_ptr<void, InternetCloser>;
struct PartialFile {
std::filesystem::path path;
~PartialFile() {
std::error_code error;
std::filesystem::remove(path, error);
}
};
bool IsWindowsExecutable(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
std::array<unsigned char, 64> header{};
input.read(reinterpret_cast<char*>(header.data()), header.size());
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
const uint32_t pe_offset =
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
if (pe_offset < header.size()) return false;
input.seekg(pe_offset);
std::array<char, 4> signature{};
input.read(signature.data(), signature.size());
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
}
uint64_t ContentLength(HINTERNET request) {
std::array<wchar_t, 32> value{};
DWORD bytes = static_cast<DWORD>(sizeof(value));
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH,
WINHTTP_HEADER_NAME_BY_INDEX, value.data(), &bytes,
WINHTTP_NO_HEADER_INDEX)) {
if (GetLastError() == ERROR_WINHTTP_HEADER_NOT_FOUND) return 0;
CheckWin32(FALSE, "WinHttpQueryHeaders(Content-Length)");
}
const std::wstring length(value.data());
if (length.empty() ||
length.find_first_not_of(L"0123456789") != std::wstring::npos) {
throw std::runtime_error("Invalid installer content length");
}
return std::stoull(length);
}
class ComApartment {
public:
ComApartment() : status_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
~ComApartment() {
if (SUCCEEDED(status_)) CoUninitialize();
}
bool Ready() const {
return SUCCEEDED(status_) || status_ == RPC_E_CHANGED_MODE;
}
private:
HRESULT status_;
};
bool HandleAttachment(const std::filesystem::path& installer,
const std::string& source, bool execute) {
ComApartment apartment;
if (!apartment.Ready()) {
LOG_WARN("Cannot initialize Windows Attachment Services");
return false;
}
Microsoft::WRL::ComPtr<IAttachmentExecute> attachment;
HRESULT result =
CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&attachment));
const std::wstring source_url(source.begin(), source.end());
if (SUCCEEDED(result)) result = attachment->SetClientGuid(kAttachmentClient);
if (SUCCEEDED(result))
result = attachment->SetClientTitle(L"CrossDesk Update");
if (SUCCEEDED(result)) result = attachment->SetLocalPath(installer.c_str());
if (SUCCEEDED(result)) result = attachment->SetSource(source_url.c_str());
if (SUCCEEDED(result)) {
if (execute) {
HANDLE process = nullptr;
// Execute applies attachment policy, scanning, and any required prompts.
// The installer's manifest, rather than this process, requests elevation.
result = attachment->Execute(GetActiveWindow(), L"open", &process);
if (process) CloseHandle(process);
} else {
// Save records the source zone and invokes installed trust/AV services.
result = attachment->Save();
}
}
if (FAILED(result)) {
LOG_WARN("Windows Attachment Services {} failed/blocked, HRESULT=0x{:08X}",
execute ? "execute" : "save", static_cast<uint32_t>(result));
return false;
}
return true;
}
} // namespace
bool DownloadWindowsInstaller(
const InstallerDownload& download, const std::filesystem::path& destination,
const std::function<bool(uint64_t, uint64_t)>& progress) {
auto partial_path = destination;
partial_path += ".part";
PartialFile partial{partial_path};
try {
if (!progress(0, 0)) return false;
const std::string agent = "CrossDesk/" CROSSDESK_VERSION " WindowsUpdater";
const std::wstring user_agent(agent.begin(), agent.end());
InternetHandle session(
WinHttpOpen(user_agent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
CheckWin32(session != nullptr, "WinHttpOpen");
CheckWin32(WinHttpSetTimeouts(session.get(), 5000, 5000, 5000, 5000),
"WinHttpSetTimeouts");
InternetHandle connection(WinHttpConnect(session.get(),
L"downloads.crossdesk.cn",
INTERNET_DEFAULT_HTTPS_PORT, 0));
CheckWin32(connection != nullptr, "WinHttpConnect");
const std::wstring path(download.path.begin(), download.path.end());
InternetHandle request(WinHttpOpenRequest(
connection.get(), L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
CheckWin32(request != nullptr, "WinHttpOpenRequest");
DWORD disabled = WINHTTP_DISABLE_REDIRECTS;
CheckWin32(WinHttpSetOption(request.get(), WINHTTP_OPTION_DISABLE_FEATURE,
&disabled, sizeof(disabled)),
"Disable redirects");
// Use Windows certificate validation and configured proxies as-is. Do not
// ignore TLS errors, spoof a browser, or retry through alternative hosts.
CheckWin32(WinHttpSendRequest(request.get(), WINHTTP_NO_ADDITIONAL_HEADERS,
0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0),
"WinHttpSendRequest");
CheckWin32(WinHttpReceiveResponse(request.get(), nullptr),
"WinHttpReceiveResponse");
DWORD status = 0;
DWORD status_size = sizeof(status);
CheckWin32(WinHttpQueryHeaders(
request.get(),
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size,
WINHTTP_NO_HEADER_INDEX),
"WinHttpQueryHeaders(status)");
if (status != 200) {
LOG_WARN("Windows installer download returned HTTP {}", status);
return false;
}
const uint64_t total = ContentLength(request.get());
if (total > kMaxInstallerSize || !progress(0, total)) return false;
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
if (!output)
throw std::runtime_error("Cannot create installer download file");
uint64_t received = 0;
std::array<unsigned char, 64 * 1024> buffer{};
while (progress(received, total)) {
DWORD count = 0;
CheckWin32(WinHttpReadData(request.get(), buffer.data(),
static_cast<DWORD>(buffer.size()), &count),
"WinHttpReadData");
if (count == 0) break;
received += count;
if (received > kMaxInstallerSize || (total > 0 && received > total))
return false;
output.write(reinterpret_cast<const char*>(buffer.data()), count);
if (!output)
throw std::runtime_error("Cannot write installer download file");
}
output.close();
if (output.fail() || received == 0 || (total > 0 && received != total) ||
!progress(received, total) || !IsWindowsExecutable(partial.path))
return false;
LOG_INFO("Windows installer downloaded, bytes={}", received);
std::filesystem::rename(partial.path, destination);
return true;
} catch (const std::exception& error) {
LOG_WARN("Windows installer download failed: {}", error.what());
return false;
}
}
bool CheckWindowsInstaller(const std::filesystem::path& installer,
const std::string& source) {
return HandleAttachment(installer, source, false);
}
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
const std::string& source) {
return HandleAttachment(installer, source, true);
}
} // namespace crossdesk
@@ -0,0 +1,33 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOWS_INSTALLER_H_
#define _WINDOWS_INSTALLER_H_
#include <cstdint>
#include <filesystem>
#include <functional>
#include <string>
#include "installer_download.h"
namespace crossdesk {
bool DownloadWindowsInstaller(
const InstallerDownload& download, const std::filesystem::path& destination,
const std::function<bool(uint64_t, uint64_t)>& progress);
// Attachment Services records the Internet source and applies Windows policy
// and installed antivirus checks. Failure must never fall back to direct
// launch.
bool CheckWindowsInstaller(const std::filesystem::path& installer,
const std::string& source);
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
const std::string& source);
} // namespace crossdesk
#endif
@@ -1,7 +1,6 @@
#include "windows_updater.h"
#include <httplib.h>
#include <shellapi.h>
#include <shlobj.h>
#include <windows.h>
#include <random>
@@ -9,6 +8,7 @@
#include "installer_download.h"
#include "rd_log.h"
#include "windows_installer.h"
namespace crossdesk {
@@ -20,7 +20,7 @@ WindowsUpdater::~WindowsUpdater() {
void WindowsUpdater::RemoveDownload() {
// A running NSIS installer still needs its source file. Leave that one in
// the user's temporary directory; clean up cancelled/failed attempts.
// the user's update cache; clean up cancelled/failed attempts.
if (installer_.empty() || installer_launched_) return;
std::error_code error;
std::filesystem::remove(installer_, error);
@@ -29,7 +29,9 @@ void WindowsUpdater::RemoveDownload() {
void WindowsUpdater::Start(const nlohmann::json& metadata) {
const State state = GetState();
if (state == State::Downloading || state == State::Launching) return;
if (state == State::Downloading || state == State::Checking ||
state == State::Launching || state == State::SecurityBlocked)
return;
if (state == State::Ready || state == State::LaunchFailed ||
state == State::Launched) {
cancelled_ = false;
@@ -51,11 +53,18 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
}
try {
std::random_device random;
const auto temp = std::filesystem::temp_directory_path();
PWSTR local_app_data = nullptr;
const HRESULT path_result = SHGetKnownFolderPath(
FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &local_app_data);
if (FAILED(path_result))
throw std::runtime_error("Cannot locate LocalAppData");
const std::filesystem::path data_path(local_app_data);
CoTaskMemFree(local_app_data);
const auto updates = data_path / L"CrossDesk" / L"Updates";
std::filesystem::create_directories(updates);
for (int attempt = 0; attempt < 10; ++attempt) {
const auto directory =
temp / ("CrossDesk-update-" + std::to_string(random()) + "-" +
std::to_string(random()));
const auto directory = updates / ("download-" + std::to_string(random()) +
"-" + std::to_string(random()));
if (std::filesystem::create_directory(directory)) {
installer_ = directory / download->filename;
break;
@@ -63,20 +72,23 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
}
if (installer_.empty())
throw std::runtime_error("Cannot create update directory");
client_ = std::make_shared<httplib::Client>(download->origin);
client_->set_connection_timeout(5);
client_->set_read_timeout(5);
client_->set_write_timeout(5);
source_url_ = download->origin + download->path;
state_ = State::Downloading;
worker_ = std::thread([this, path = download->path] {
const bool success =
DownloadInstaller(*client_, path, installer_,
[this](uint64_t received, uint64_t total) {
downloaded_ = received;
total_ = total;
return !cancelled_.load();
});
state_ = success ? State::Ready : State::Failed;
worker_ = std::thread([this, download = *download] {
const bool success = DownloadWindowsInstaller(
download, installer_, [this](uint64_t received, uint64_t total) {
downloaded_ = received;
total_ = total;
return !cancelled_.load();
});
if (success && !cancelled_) {
state_ = State::Checking;
state_ = CheckWindowsInstaller(installer_, source_url_)
? State::Ready
: State::SecurityBlocked;
} else {
state_ = State::Failed;
}
if (cancelled_) state_ = State::Idle;
});
} catch (const std::exception& error) {
@@ -87,7 +99,6 @@ void WindowsUpdater::Start(const nlohmann::json& metadata) {
void WindowsUpdater::Cancel() {
cancelled_ = true;
if (client_) client_->stop();
if (GetState() == State::Ready) state_ = State::Idle;
}
@@ -98,22 +109,11 @@ void WindowsUpdater::Launch() {
state != State::Launched)
return;
if (worker_.joinable()) worker_.join();
const std::wstring executable = installer_.wstring();
const std::wstring directory = installer_.parent_path().wstring();
state_ = State::Launching;
SHELLEXECUTEINFOW info{};
info.cbSize = sizeof(info);
info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI;
info.lpVerb = L"runas";
info.lpFile = executable.c_str();
info.lpDirectory = directory.c_str();
info.nShow = SW_SHOWNORMAL;
if (!ShellExecuteExW(&info)) {
LOG_WARN("Cannot launch Windows installer, error={}", GetLastError());
if (!LaunchWindowsInstaller(installer_, source_url_)) {
state_ = State::LaunchFailed;
return;
}
if (info.hProcess) CloseHandle(info.hProcess);
installer_launched_ = true;
state_ = State::Launched;
// NSIS already prompts to close CrossDesk and stops the installed service.
@@ -9,14 +9,10 @@
#include <atomic>
#include <filesystem>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <thread>
namespace httplib {
class Client;
}
namespace crossdesk {
class WindowsUpdater {
@@ -24,9 +20,11 @@ class WindowsUpdater {
enum class State {
Idle,
Downloading,
Checking,
Ready,
Launching,
Failed,
SecurityBlocked,
LaunchFailed,
Launched
};
@@ -46,12 +44,12 @@ class WindowsUpdater {
std::atomic<bool> cancelled_{false};
std::atomic<uint64_t> downloaded_{0};
std::atomic<uint64_t> total_{0};
std::shared_ptr<httplib::Client> client_;
std::thread worker_;
std::filesystem::path installer_;
std::string source_url_;
bool installer_launched_ = false;
};
} // namespace crossdesk
#endif
#endif
@@ -1,42 +1,6 @@
#include "installer_download.h"
#include <httplib.h>
#include <array>
#include <fstream>
#include "rd_log.h"
namespace crossdesk {
namespace {
// Reject error pages and files without Windows executable headers before
// invoking the loader. The download uses the verified official HTTPS URL.
bool IsWindowsExecutable(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
std::array<unsigned char, 64> header{};
input.read(reinterpret_cast<char*>(header.data()), header.size());
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
const uint32_t pe_offset =
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
if (pe_offset < header.size()) return false;
input.seekg(pe_offset);
std::array<char, 4> signature{};
input.read(signature.data(), signature.size());
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
}
struct PartialFile {
std::filesystem::path path;
~PartialFile() {
std::error_code error;
std::filesystem::remove(path, error);
}
};
} // namespace
std::optional<InstallerDownload> GetWindowsInstallerDownload(
const nlohmann::json& metadata) {
if (!metadata.is_object()) return std::nullopt;
@@ -74,55 +38,4 @@ std::optional<InstallerDownload> GetWindowsInstallerDownload(
return result;
}
bool DownloadInstaller(
httplib::Client& client, const std::string& path,
const std::filesystem::path& destination,
const std::function<bool(uint64_t, uint64_t)>& progress) {
auto partial_path = destination;
partial_path += ".part";
PartialFile partial{partial_path};
try {
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
if (!output) return false;
uint64_t received = 0;
uint64_t total = 0;
bool accepted = false;
// Do not follow a redirect to an unverified origin or downgrade to HTTP.
client.set_follow_location(false);
client.set_decompress(false);
const auto response = client.Get(
path,
[&](const httplib::Response& headers) {
if (headers.status != 200) return false;
if (headers.has_header("Content-Length")) {
total = headers.get_header_value_u64("Content-Length");
}
accepted = progress(0, total);
return accepted;
},
[&](const char* data, size_t size) {
if (!accepted || !progress(received, total)) return false;
output.write(data, static_cast<std::streamsize>(size));
received += size;
return output.good() && progress(received, total);
});
output.close();
if (!response || response->status != 200 || !accepted || output.fail() ||
received == 0 || (total != 0 && received != total) ||
!progress(received, total) || !IsWindowsExecutable(partial.path)) {
LOG_WARN(
"Installer download failed or cancelled: HTTP={}, error={}, "
"received={}, expected={}",
response ? response->status : 0, httplib::to_string(response.error()),
received, total);
return false;
}
std::filesystem::rename(partial.path, destination);
return true;
} catch (const std::exception& error) {
LOG_WARN("Installer download failed: {}", error.what());
return false;
}
}
} // namespace crossdesk
@@ -7,17 +7,10 @@
#ifndef _INSTALLER_DOWNLOAD_H_
#define _INSTALLER_DOWNLOAD_H_
#include <cstdint>
#include <filesystem>
#include <functional>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
namespace httplib {
class Client;
}
namespace crossdesk {
struct InstallerDownload {
@@ -30,12 +23,6 @@ struct InstallerDownload {
std::optional<InstallerDownload> GetWindowsInstallerDownload(
const nlohmann::json& metadata);
// Streams to a .part file and publishes the installer only after a complete,
// successful response. Returning false from progress cancels the transfer.
bool DownloadInstaller(httplib::Client& client, const std::string& path,
const std::filesystem::path& destination,
const std::function<bool(uint64_t, uint64_t)>& progress);
} // namespace crossdesk
#endif
#endif
+3 -1
View File
@@ -286,7 +286,9 @@ function setup_targets()
add_deps("rd_log")
add_files("apps/desktop/src/version_checker/*.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/windows_updater.cpp")
add_files("apps/desktop/src/platform/windows/windows_updater.cpp",
"apps/desktop/src/platform/windows/windows_installer.cpp")
add_links("winhttp", "ole32", "uuid")
end
add_includedirs("apps/desktop/src/version_checker", {public = true})
if is_os("macosx") then
+48
View File
@@ -0,0 +1,48 @@
# Windows 更新与安全软件兼容性
Windows 的更新按钮在后台下载发布信息中指定版本的安装包,完成校验及
Windows 附件安全检查后打开安装向导。客户端使用 WinHTTP,保留系统的
HTTPS 证书检查和代理配置,并用 `CrossDesk/<version> WindowsUpdater`
标识请求。
安装包保存在 `%LOCALAPPDATA%\CrossDesk\Updates` 下的独立目录中。
下载过程写入 `.part` 文件;HTTP 响应、文件长度、Windows 可执行文件头
均通过检查后才生成最终安装包。
客户端通过 Windows Attachment Services 的 `Save` 记录下载来源并请求
系统安全检查,再通过 `Execute` 打开安装包。这些 API 会按 Windows 策略
调用扫描、信任检查和必要的提示。安装程序由自身的管理员权限清单请求
UAC 授权。安全检查失败时停止流程,不回退到直接启动,也不删除来源标记。
## 发行版签名
应使用受信任的正式代码签名证书签署发布的主程序、服务、辅助程序、DLL
和最终安装包。只签安装包不能建立已安装主程序的发布者身份。
SignPath 的 `test-signing` 用于测试,不能代替公开发行所需的受信任签名。
当前流水线中的测试签名配置仍保持为可选项;正式策略需在取得证书及
SignPath 授权后配置。
检查实际分发文件的签名:
```powershell
Get-AuthenticodeSignature .\crossdesk.exe
Get-AuthenticodeSignature .\crossdesk-win-x64-<version>.exe
```
## 网络防护告警的处理
2026-09-09 的天擎日志记录了在域名解析阶段对
`downloads.crossdesk.cn` 和 `101.132.143.203` 的阻断。
该记录不能单独证明 HTTP 库、提权方式或文件内容是命中原因。
切换到标准 Windows API 也不能保证域名/IP 信誉告警消失。
出现同类告警时,保留告警时间、策略名称、域名/IP、程序版本及签名状态,
由安全管理员向厂商核查命中原因,同时检查服务器是否存在异常活动。
确认误报后按厂商流程纠正信誉或企业策略。应在策略允许的测试环境中
验证下载、取消、不完整文件、安全检查失败及安装向导启动流程。
相关 API:
- [WinHttpOpen](https://learn.microsoft.com/windows/win32/api/winhttp/nf-winhttp-winhttpopen)
- [IAttachmentExecute::Save](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-save)
- [IAttachmentExecute::Execute](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-execute)