mirror of
https://github.com/monero-project/monero-gui.git
synced 2024-11-17 00:07:51 +00:00
updater: fetch signed hashes from getmonero.org, verify downloads
This commit is contained in:
parent
8e4124f06a
commit
ea25b71ca6
10 changed files with 131 additions and 33 deletions
|
@ -37,11 +37,12 @@ import "../components" as MoneroComponents
|
|||
Popup {
|
||||
id: updateDialog
|
||||
|
||||
property bool active: false
|
||||
property bool allowed: true
|
||||
property string error: ""
|
||||
property string filename: ""
|
||||
property string hash: ""
|
||||
property double progress: url && downloader.total > 0 ? downloader.loaded * 100 / downloader.total : 0
|
||||
property bool active: false
|
||||
property string url: ""
|
||||
property bool valid: false
|
||||
property string version: ""
|
||||
|
@ -55,8 +56,9 @@ Popup {
|
|||
padding: 20
|
||||
visible: active && allowed
|
||||
|
||||
function show(version, url) {
|
||||
function show(version, url, hash) {
|
||||
updateDialog.error = "";
|
||||
updateDialog.hash = hash;
|
||||
updateDialog.url = url;
|
||||
updateDialog.valid = false;
|
||||
updateDialog.version = version;
|
||||
|
@ -86,7 +88,7 @@ Popup {
|
|||
|
||||
Text {
|
||||
id: statusText
|
||||
color: MoneroComponents.Style.defaultFontColor
|
||||
color: updateDialog.valid ? MoneroComponents.Style.green : MoneroComponents.Style.defaultFontColor
|
||||
font.family: MoneroComponents.Style.fontRegular.name
|
||||
font.pixelSize: 18
|
||||
visible: !errorText.visible
|
||||
|
@ -102,9 +104,9 @@ Popup {
|
|||
+ translationManager.emptyString;
|
||||
}
|
||||
if (updateDialog.valid) {
|
||||
return qsTr("Download finished") + translationManager.emptyString;
|
||||
return qsTr("Update downloaded, signature verified") + translationManager.emptyString;
|
||||
}
|
||||
return qsTr("Do you want to download new version?") + translationManager.emptyString;
|
||||
return qsTr("Do you want to download and verify new version?") + translationManager.emptyString;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,8 +158,9 @@ Popup {
|
|||
onClicked: {
|
||||
updateDialog.error = "";
|
||||
updateDialog.filename = updateDialog.url.replace(/^.*\//, '');
|
||||
const downloadingStarted = downloader.get(updateDialog.url, function(error) {
|
||||
const downloadingStarted = downloader.get(updateDialog.url, updateDialog.hash, function(error) {
|
||||
if (error) {
|
||||
console.error("Download failed", error);
|
||||
updateDialog.error = qsTr("Download failed") + translationManager.emptyString;
|
||||
} else {
|
||||
updateDialog.valid = true;
|
||||
|
|
14
main.qml
14
main.qml
|
@ -1978,15 +1978,11 @@ ApplicationWindow {
|
|||
closeWallet(Qt.quit);
|
||||
}
|
||||
|
||||
function onWalletCheckUpdatesComplete(update) {
|
||||
if (update === "")
|
||||
return
|
||||
print("Update found: " + update)
|
||||
var parts = update.split("|")
|
||||
if (parts.length == 4) {
|
||||
updateDialog.show(parts[0], isMac || isWindows || isLinux ? parts[3] : "");
|
||||
} else {
|
||||
print("Failed to parse update spec")
|
||||
function onWalletCheckUpdatesComplete(version, downloadUrl, hash, firstSigner, secondSigner) {
|
||||
const alreadyAsked = updateDialog.url == downloadUrl && updateDialog.hash == hash;
|
||||
if (!alreadyAsked)
|
||||
{
|
||||
updateDialog.show(version, isMac || isWindows || isLinux ? downloadUrl : "", hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
#include <QMutexLocker>
|
||||
#include <QString>
|
||||
|
||||
#include "qt/updater.h"
|
||||
|
||||
class WalletPassphraseListenerImpl : public Monero::WalletListener
|
||||
{
|
||||
public:
|
||||
|
@ -464,12 +466,32 @@ bool WalletManager::saveQrCode(const QString &code, const QString &path) const
|
|||
void WalletManager::checkUpdatesAsync(const QString &software, const QString &subdir)
|
||||
{
|
||||
m_scheduler.run([this, software, subdir] {
|
||||
emit checkUpdatesComplete(checkUpdates(software, subdir));
|
||||
const auto updateInfo = Monero::WalletManager::checkUpdates(software.toStdString(), subdir.toStdString());
|
||||
if (!std::get<0>(updateInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString version = QString::fromStdString(std::get<1>(updateInfo));
|
||||
const QByteArray hashFromDns = QByteArray::fromHex(QString::fromStdString(std::get<2>(updateInfo)).toUtf8());
|
||||
const QString downloadUrl = QString::fromStdString(std::get<4>(updateInfo));
|
||||
|
||||
try
|
||||
{
|
||||
const QString binaryFilename = QUrl(downloadUrl).fileName();
|
||||
QPair<QString, QString> signers;
|
||||
const QString signedHash = Updater().fetchSignedHash(binaryFilename, hashFromDns, signers).toHex();
|
||||
|
||||
qInfo() << "Update found" << version << downloadUrl << "hash" << signedHash << "signed by" << signers;
|
||||
emit checkUpdatesComplete(version, downloadUrl, signedHash, signers.first, signers.second);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
qCritical() << "Failed to fetch and verify signed hash:" << e.what();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
QString WalletManager::checkUpdates(const QString &software, const QString &subdir) const
|
||||
{
|
||||
qDebug() << "Checking for updates";
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
#include <QMutex>
|
||||
#include <QPointer>
|
||||
#include <QWaitCondition>
|
||||
#include <QMutex>
|
||||
#include "qt/FutureScheduler.h"
|
||||
#include "NetworkType.h"
|
||||
|
||||
|
@ -192,7 +191,12 @@ signals:
|
|||
void walletPassphraseNeeded();
|
||||
void deviceButtonRequest(quint64 buttonCode);
|
||||
void deviceButtonPressed();
|
||||
void checkUpdatesComplete(const QString &result) const;
|
||||
void checkUpdatesComplete(
|
||||
const QString &version,
|
||||
const QString &downloadUrl,
|
||||
const QString &hash,
|
||||
const QString &firstSigner,
|
||||
const QString &secondSigner) const;
|
||||
void miningStatus(bool isMining) const;
|
||||
|
||||
public slots:
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
#include <QReadLocker>
|
||||
#include <QWriteLocker>
|
||||
|
||||
#include "updater.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
|
@ -112,10 +114,10 @@ void Downloader::cancel()
|
|||
m_contents.clear();
|
||||
}
|
||||
|
||||
bool Downloader::get(const QString &url, const QJSValue &callback)
|
||||
bool Downloader::get(const QString &url, const QString &hash, const QJSValue &callback)
|
||||
{
|
||||
auto future = m_scheduler.run(
|
||||
[this, url]() {
|
||||
[this, url, hash]() {
|
||||
DownloaderStateGuard stateGuard(m_active, m_mutex, [this]() {
|
||||
emit activeChanged();
|
||||
});
|
||||
|
@ -153,6 +155,19 @@ bool Downloader::get(const QString &url, const QJSValue &callback)
|
|||
return QJSValueList({"empty response"});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const QByteArray calculatedHash = Updater().getHash(&response[0], response.size());
|
||||
if (QByteArray::fromHex(hash.toUtf8()) != calculatedHash)
|
||||
{
|
||||
return QJSValueList({"hash sum mismatch"});
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return QJSValueList({e.what()});
|
||||
}
|
||||
|
||||
{
|
||||
QWriteLocker locker(&m_mutex);
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ public:
|
|||
~Downloader();
|
||||
|
||||
Q_INVOKABLE void cancel();
|
||||
Q_INVOKABLE bool get(const QString &url, const QJSValue &callback);
|
||||
Q_INVOKABLE bool get(const QString &url, const QString &hash, const QJSValue &callback);
|
||||
Q_INVOKABLE bool saveToFile(const QString &path) const;
|
||||
|
||||
signals:
|
||||
|
|
|
@ -117,6 +117,17 @@ void Network::getJSON(const QString &url, const QJSValue &callback) const
|
|||
get(url, callback, "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
std::string Network::get(const QString &url, const QString &contentType /* = {} */) const
|
||||
{
|
||||
std::string response;
|
||||
QString error = get(std::shared_ptr<http_simple_client>(new http_simple_client()), url, response, contentType);
|
||||
if (!error.isEmpty())
|
||||
{
|
||||
throw std::runtime_error(QString("failed to fetch %1: %2").arg(url).arg(error).toStdString());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
QString Network::get(
|
||||
std::shared_ptr<http_simple_client> httpClient,
|
||||
const QString &url,
|
||||
|
|
|
@ -77,6 +77,7 @@ public:
|
|||
Q_INVOKABLE void get(const QString &url, const QJSValue &callback, const QString &contentType = {}) const;
|
||||
Q_INVOKABLE void getJSON(const QString &url, const QJSValue &callback) const;
|
||||
|
||||
std::string get(const QString &url, const QString &contentType = {}) const;
|
||||
QString get(
|
||||
std::shared_ptr<epee::net_utils::http::http_simple_client> httpClient,
|
||||
const QString &url,
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
#include <openpgp/hash.h>
|
||||
|
||||
#include "network.h"
|
||||
#include "utils.h"
|
||||
|
||||
Updater::Updater()
|
||||
|
@ -39,17 +40,41 @@ Updater::Updater()
|
|||
m_maintainers.emplace_back(fileGetContents(":/monero/utils/gpg_keys/luigi1111.asc").toStdString());
|
||||
}
|
||||
|
||||
QPair<QString, QString> Updater::verifySignaturesAndHashSum(
|
||||
QByteArray Updater::fetchSignedHash(
|
||||
const QString &binaryFilename,
|
||||
const QByteArray &hashFromDns,
|
||||
QPair<QString, QString> &signers) const
|
||||
{
|
||||
static constexpr const char hashesTxtUrl[] = "https://web.getmonero.org/downloads/hashes.txt";
|
||||
static constexpr const char hashesTxtSigUrl[] = "https://web.getmonero.org/downloads/hashes.txt.sig";
|
||||
|
||||
const Network network;
|
||||
std::string hashesTxt = network.get(hashesTxtUrl);
|
||||
std::string hashesTxtSig = network.get(hashesTxtSigUrl);
|
||||
|
||||
const QByteArray signedHash = verifyParseSignedHahes(
|
||||
QByteArray(&hashesTxt[0], hashesTxt.size()),
|
||||
QByteArray(&hashesTxtSig[0], hashesTxtSig.size()),
|
||||
binaryFilename,
|
||||
signers);
|
||||
|
||||
if (signedHash != hashFromDns)
|
||||
{
|
||||
throw std::runtime_error("DNS hash mismatch");
|
||||
}
|
||||
|
||||
return signedHash;
|
||||
}
|
||||
|
||||
QByteArray Updater::verifyParseSignedHahes(
|
||||
const QByteArray &armoredSignedHashes,
|
||||
const QByteArray &secondDetachedSignature,
|
||||
const QString &binaryFilename,
|
||||
const void *binaryData,
|
||||
size_t binarySize) const
|
||||
QPair<QString, QString> &signers) const
|
||||
{
|
||||
QString firstSigner;
|
||||
const QString signedMessage = verifySignature(armoredSignedHashes, firstSigner);
|
||||
const QString signedMessage = verifySignature(armoredSignedHashes, signers.first);
|
||||
|
||||
QString secondSigner = verifySignature(
|
||||
signers.second = verifySignature(
|
||||
epee::span<const uint8_t>(
|
||||
reinterpret_cast<const uint8_t *>(armoredSignedHashes.data()),
|
||||
armoredSignedHashes.size()),
|
||||
|
@ -57,19 +82,31 @@ QPair<QString, QString> Updater::verifySignaturesAndHashSum(
|
|||
reinterpret_cast<const uint8_t *>(secondDetachedSignature.data()),
|
||||
secondDetachedSignature.size())));
|
||||
|
||||
if (firstSigner == secondSigner)
|
||||
if (signers.first == signers.second)
|
||||
{
|
||||
throw std::runtime_error("both signatures were generated by the same person");
|
||||
}
|
||||
|
||||
const QByteArray signedHash = parseShasumOutput(signedMessage, binaryFilename);
|
||||
return parseShasumOutput(signedMessage, binaryFilename);
|
||||
}
|
||||
|
||||
QPair<QString, QString> Updater::verifySignaturesAndHashSum(
|
||||
const QByteArray &armoredSignedHashes,
|
||||
const QByteArray &secondDetachedSignature,
|
||||
const QString &binaryFilename,
|
||||
const void *binaryData,
|
||||
size_t binarySize) const
|
||||
{
|
||||
QPair<QString, QString> signers;
|
||||
const QByteArray signedHash =
|
||||
verifyParseSignedHahes(armoredSignedHashes, secondDetachedSignature, binaryFilename, signers);
|
||||
const QByteArray calculatedHash = getHash(binaryData, binarySize);
|
||||
if (signedHash != calculatedHash)
|
||||
{
|
||||
throw std::runtime_error("hash sum mismatch");
|
||||
}
|
||||
|
||||
return {firstSigner, secondSigner};
|
||||
return signers;
|
||||
}
|
||||
|
||||
QByteArray Updater::getHash(const void *data, size_t size) const
|
||||
|
|
|
@ -37,6 +37,11 @@ class Updater
|
|||
public:
|
||||
Updater();
|
||||
|
||||
QByteArray fetchSignedHash(
|
||||
const QString &binaryFilename,
|
||||
const QByteArray &hashFromDns,
|
||||
QPair<QString, QString> &signers) const;
|
||||
QByteArray getHash(const void *data, size_t size) const;
|
||||
QPair<QString, QString> verifySignaturesAndHashSum(
|
||||
const QByteArray &armoredSignedHashes,
|
||||
const QByteArray &secondDetachedSignature,
|
||||
|
@ -45,7 +50,11 @@ public:
|
|||
size_t binarySize) const;
|
||||
|
||||
private:
|
||||
QByteArray getHash(const void *data, size_t size) const;
|
||||
QByteArray verifyParseSignedHahes(
|
||||
const QByteArray &armoredSignedHashes,
|
||||
const QByteArray &secondDetachedSignature,
|
||||
const QString &binaryFilename,
|
||||
QPair<QString, QString> &signers) const;
|
||||
QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const;
|
||||
QString verifySignature(const epee::span<const uint8_t> data, const openpgp::signature_rsa &signature) const;
|
||||
QByteArray parseShasumOutput(const QString &message, const QString &filename) const;
|
||||
|
|
Loading…
Reference in a new issue