mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62fa095153 |
@@ -1,6 +1,9 @@
|
||||
#include "updateController.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QNetworkReply>
|
||||
#include <QProcess>
|
||||
#include <QVersionNumber>
|
||||
#include <QUrl>
|
||||
#include <QJsonDocument>
|
||||
@@ -29,6 +32,17 @@ namespace
|
||||
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
|
||||
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
// Pinned signing identity of release installers. spctl prints the origin as
|
||||
// origin=Developer ID Installer: <Name> (<TEAMID>)
|
||||
// We require this exact substring so a different (even validly notarized)
|
||||
// package is rejected.
|
||||
// TODO(#1): replace with the real release identity before shipping, e.g.
|
||||
// "Developer ID Installer: Amnezia OU (XXXXXXXXXX)".
|
||||
// Until set, macOS update verification fails closed (updates are rejected).
|
||||
const QLatin1String kExpectedMacSigningOrigin("Developer ID Installer: SET_ME_BEFORE_RELEASE");
|
||||
#endif
|
||||
}
|
||||
|
||||
UpdateController::UpdateController(SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
|
||||
@@ -121,6 +135,10 @@ void UpdateController::fetchGatewayUrl()
|
||||
}
|
||||
m_baseUrl = baseUrl;
|
||||
|
||||
// Expected installer SHA-256 for the requesting OS, delivered over the
|
||||
// encrypted gateway channel (used by Windows/Linux verification).
|
||||
m_expectedSha256 = gatewayData.value("sha256").toString().trimmed().toLower();
|
||||
|
||||
fetchVersionInfo();
|
||||
});
|
||||
});
|
||||
@@ -209,6 +227,53 @@ QString UpdateController::composeDownloadUrl() const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdateController::verifySha256(const QByteArray &data) const
|
||||
{
|
||||
if (m_expectedSha256.isEmpty()) {
|
||||
logger.error() << "No expected installer checksum provided by gateway; rejecting installer";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString actual = QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex());
|
||||
if (actual.compare(m_expectedSha256, Qt::CaseInsensitive) != 0) {
|
||||
logger.error() << "Installer checksum mismatch. expected:" << m_expectedSha256 << "actual:" << actual;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
bool UpdateController::verifyMacInstallerSignature(const QString &installerPath) const
|
||||
{
|
||||
// Gatekeeper assessment for installer packages: the .pkg must be signed AND
|
||||
// notarized under our Apple Developer ID. spctl writes its verdict to stderr.
|
||||
QProcess spctl;
|
||||
spctl.start(QStringLiteral("/usr/sbin/spctl"),
|
||||
{ QStringLiteral("--assess"), QStringLiteral("--type"), QStringLiteral("install"),
|
||||
QStringLiteral("-vv"), installerPath });
|
||||
if (!spctl.waitForFinished(15000)) {
|
||||
logger.error() << "spctl assessment did not finish in time";
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString output = QString::fromUtf8(spctl.readAllStandardError()) + QString::fromUtf8(spctl.readAllStandardOutput());
|
||||
|
||||
if (spctl.exitStatus() != QProcess::NormalExit || spctl.exitCode() != 0) {
|
||||
logger.error() << "spctl rejected the installer:" << output.trimmed();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pin the signing identity so a different (even validly notarized) package is rejected.
|
||||
if (!output.contains(kExpectedMacSigningOrigin)) {
|
||||
logger.error() << "Installer signed by an unexpected identity:" << output.trimmed();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void UpdateController::runInstaller()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
@@ -225,6 +290,8 @@ void UpdateController::runInstaller()
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
const QByteArray data = reply->readAll();
|
||||
|
||||
QFile file(kInstallerLocalPath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
logger.error() << "Failed to open installer file for writing:" << kInstallerLocalPath << "Error:" << file.errorString();
|
||||
@@ -232,7 +299,7 @@ void UpdateController::runInstaller()
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.write(reply->readAll()) == -1) {
|
||||
if (file.write(data) == -1) {
|
||||
logger.error() << "Failed to write installer data to file:" << kInstallerLocalPath << "Error:" << file.errorString();
|
||||
file.close();
|
||||
reply->deleteLater();
|
||||
@@ -241,6 +308,25 @@ void UpdateController::runInstaller()
|
||||
|
||||
file.close();
|
||||
|
||||
// Fail-closed: never launch an installer whose authenticity we could not verify.
|
||||
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
if (!verifyMacInstallerSignature(kInstallerLocalPath)) {
|
||||
logger.error() << "Installer signature verification failed; refusing to launch";
|
||||
QFile::remove(kInstallerLocalPath);
|
||||
emit installerVerificationFailed(tr("Update verification failed: the installer signature is invalid. The update was not launched."));
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (!verifySha256(data)) {
|
||||
logger.error() << "Installer checksum verification failed; refusing to launch";
|
||||
QFile::remove(kInstallerLocalPath);
|
||||
emit installerVerificationFailed(tr("Update verification failed: the installer checksum does not match. The update was not launched."));
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
runWindowsInstaller(kInstallerLocalPath);
|
||||
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
|
||||
|
||||
@@ -23,6 +23,7 @@ public slots:
|
||||
|
||||
signals:
|
||||
void updateFound();
|
||||
void installerVerificationFailed(const QString &reason);
|
||||
|
||||
private:
|
||||
void finishUpdateCheck();
|
||||
@@ -43,11 +44,18 @@ private:
|
||||
QString m_version;
|
||||
QString m_releaseDate;
|
||||
QString m_downloadUrl;
|
||||
QString m_expectedSha256;
|
||||
bool m_updateCheckRunning = false;
|
||||
|
||||
// Verify the downloaded installer before launching it (fail-closed):
|
||||
// Windows/Linux compare a SHA-256 delivered over the encrypted gateway channel.
|
||||
bool verifySha256(const QByteArray &data) const;
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
int runWindowsInstaller(const QString &installerPath);
|
||||
#elif defined(Q_OS_MACOS)
|
||||
// macOS verifies the .pkg Gatekeeper assessment (notarized + pinned Developer ID).
|
||||
bool verifyMacInstallerSignature(const QString &installerPath) const;
|
||||
int runMacInstaller(const QString &installerPath);
|
||||
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
int runLinuxInstaller(const QString &installerPath);
|
||||
|
||||
@@ -5,6 +5,8 @@ UpdateUiController::UpdateUiController(UpdateController* updateController, QObje
|
||||
{
|
||||
if (m_updateController) {
|
||||
connect(m_updateController, &UpdateController::updateFound, this, &UpdateUiController::updateFound);
|
||||
connect(m_updateController, &UpdateController::installerVerificationFailed, this,
|
||||
&UpdateUiController::installerVerificationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public slots:
|
||||
|
||||
signals:
|
||||
void updateFound();
|
||||
void installerVerificationFailed(const QString &message);
|
||||
|
||||
private:
|
||||
UpdateController* m_updateController;
|
||||
|
||||
@@ -454,4 +454,12 @@ Window {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: UpdateController
|
||||
function onInstallerVerificationFailed(message) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user