updater: fetch signed hashes from getmonero.org, verify downloads

This commit is contained in:
xiphon 2020-04-14 21:03:15 +00:00
parent 8e4124f06a
commit ea25b71ca6
10 changed files with 131 additions and 33 deletions

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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";

View file

@ -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:

View file

@ -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);

View file

@ -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:

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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;