diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a06055e4..c955020f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: update brew and install dependencies - run: brew update && brew install boost hidapi zmq libpgm miniupnpc ldns expat libunwind-headers protobuf qt5 + run: brew update && brew install boost hidapi zmq libpgm miniupnpc ldns expat libunwind-headers protobuf qt5 libgcrypt - name: build run: export PATH=$PATH:/usr/local/opt/qt/bin && ./build.sh - name: test qml @@ -34,7 +34,7 @@ jobs: - name: install monero dependencies run: sudo apt -y install build-essential cmake libboost-all-dev miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev - name: install monero gui dependencies - run: sudo apt -y install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev xvfb + run: sudo apt -y install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev xvfb - name: build run: ./build.sh - name: test qml @@ -50,7 +50,7 @@ jobs: - name: update pacman run: msys2do pacman -Syu --noconfirm - name: install monero dependencies - run: msys2do pacman -S --noconfirm mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake mingw-w64-x86_64-boost mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-protobuf-c mingw-w64-x86_64-libusb git mingw-w64-x86_64-qt5 + run: msys2do pacman -S --noconfirm mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake mingw-w64-x86_64-boost mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-protobuf-c mingw-w64-x86_64-libusb git mingw-w64-x86_64-qt5 mingw-w64-x86_64-libgcrypt - name: build run: msys2do ./build.sh release - name: test qml diff --git a/README.md b/README.md index 3fe96a7c..ea152aff 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ The following instructions will fetch Qt from your distribution's repositories i - For Ubuntu 17.10+ - `sudo apt install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev` + `sudo apt install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev` - For Gentoo @@ -144,7 +144,7 @@ The executable can be found in the build/release/bin folder. 3. Install [monero](https://github.com/monero-project/monero) dependencies: - `brew install boost hidapi zmq libpgm miniupnpc ldns expat libunwind-headers protobuf` + `brew install boost hidapi zmq libpgm miniupnpc ldns expat libunwind-headers protobuf libgcrypt` 4. Install Qt: @@ -180,7 +180,7 @@ The Monero GUI on Windows is 64 bits only; 32-bit Windows GUI builds are not off 3. Install MSYS2 packages for Monero dependencies; the needed 64-bit packages have `x86_64` in their names ``` - pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake mingw-w64-x86_64-boost mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-protobuf-c mingw-w64-x86_64-libusb + pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake mingw-w64-x86_64-boost mingw-w64-x86_64-openssl mingw-w64-x86_64-zeromq mingw-w64-x86_64-libsodium mingw-w64-x86_64-hidapi mingw-w64-x86_64-protobuf-c mingw-w64-x86_64-libusb mingw-w64-x86_64-libgcrypt ``` Optional : To build the flag `WITH_SCANNER` diff --git a/monero-wallet-gui.pro b/monero-wallet-gui.pro index 04242c66..549a4de2 100644 --- a/monero-wallet-gui.pro +++ b/monero-wallet-gui.pro @@ -96,6 +96,7 @@ SOURCES += src/main/main.cpp \ src/libwalletqt/TransactionInfo.cpp \ src/libwalletqt/QRCodeImageProvider.cpp \ src/main/oshelper.cpp \ + src/openpgp/openpgp.cpp \ src/TranslationManager.cpp \ src/model/TransactionHistoryModel.cpp \ src/model/TransactionHistorySortFilterModel.cpp \ @@ -116,6 +117,7 @@ SOURCES += src/main/main.cpp \ src/qt/ipc.cpp \ src/qt/KeysFiles.cpp \ src/qt/network.cpp \ + src/qt/updater.cpp \ src/qt/utils.cpp \ src/qt/MoneroSettings.cpp \ src/qt/TailsOS.cpp @@ -157,6 +159,8 @@ ios:arm64 { } LIBS_COMMON = \ + -lgcrypt \ + -lgpg-error \ -lwallet_merged \ -llmdb \ -lepee \ @@ -390,6 +394,20 @@ macx { INCLUDEPATH += /usr/local/include } + GCRYPT_DIR = $$system(brew --prefix libgcrypt, lines, EXIT_CODE) + equals(EXIT_CODE, 0) { + INCLUDEPATH += $$GCRYPT_DIR/include + } else { + INCLUDEPATH += /usr/local/include + } + + GPGP_ERROR_DIR = $$system(brew --prefix libgpg-error, lines, EXIT_CODE) + equals(EXIT_CODE, 0) { + INCLUDEPATH += $$GPGP_ERROR_DIR/include + } else { + INCLUDEPATH += /usr/local/include + } + QT += macextras OBJECTIVE_SOURCES += src/qt/macoshelper.mm LIBS+= -Wl,-dead_strip diff --git a/qml.qrc b/qml.qrc index 04c447c0..b63ca9a4 100644 --- a/qml.qrc +++ b/qml.qrc @@ -9,6 +9,9 @@ images/whatIsIcon@2x.png images/lockIcon.png components/MenuButton.qml + monero/utils/gpg_keys/binaryfate.asc + monero/utils/gpg_keys/fluffypony.asc + monero/utils/gpg_keys/luigi1111.asc pages/Account.qml pages/Transfer.qml pages/History.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9cfe114b..de84824b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(QR-Code-scanner) add_subdirectory(daemon) add_subdirectory(libwalletqt) add_subdirectory(model) +add_subdirectory(openpgp) add_subdirectory(zxcvbn-c) qt5_add_resources(RESOURCES ../qml.qrc) @@ -149,6 +150,7 @@ target_link_libraries(monero-gui ${QT5_LIBRARIES} ${EXTRA_LIBRARIES} ${ICU_LIBRARIES} + openpgp ) if(WITH_SCANNER) diff --git a/src/main/main.cpp b/src/main/main.cpp index c6c0ea52..5906e93e 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -63,6 +63,7 @@ #include "MainApp.h" #include "qt/ipc.h" #include "qt/network.h" +#include "qt/updater.h" #include "qt/utils.h" #include "qt/TailsOS.h" #include "qt/KeysFiles.h" @@ -221,6 +222,14 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "Log to specified file"), QCoreApplication::translate("main", "file")); + QCommandLineOption verifyUpdateOption("verify-update", "\ +Verify update binary using 'shasum'-compatible (SHA256 algo) output signed by two maintainers.\n\ +* Requires 'hashes.txt' - signed 'shasum' output \ +(i.e. 'gpg -o hashes.txt --clear-sign ') generated by a maintainer.\n\ +* Requires 'hashes.txt.sig' - detached signature of 'hashes.txt' \ +(i.e. 'gpg -b hashes.txt') generated by another maintainer.", "update-binary"); + parser.addOption(verifyUpdateOption); + QCommandLineOption testQmlOption("test-qml"); testQmlOption.setFlags(QCommandLineOption::HiddenFromHelp); parser.addOption(logPathOption); @@ -244,6 +253,32 @@ int main(int argc, char *argv[]) } qWarning().noquote() << "app startd" << "(log: " + logPath + ")"; + if (parser.isSet(verifyUpdateOption)) + { + const QString updateBinaryFullPath = parser.value(verifyUpdateOption); + const QFileInfo updateBinaryInfo(updateBinaryFullPath); + const QString updateBinaryDir = QDir::toNativeSeparators(updateBinaryInfo.absolutePath()) + QDir::separator(); + const QString hashesTxt = updateBinaryDir + "hashes.txt"; + const QString hashesTxtSig = hashesTxt + ".sig"; + try + { + const QByteArray updateBinaryContents = fileGetContents(updateBinaryFullPath); + const QPair signers = Updater().verifySignaturesAndHashSum( + fileGetContents(hashesTxt), + fileGetContents(hashesTxtSig), + updateBinaryInfo.fileName(), + updateBinaryContents.data(), + updateBinaryContents.size()); + qCritical() << "successfully verified, signed by" << signers.first << "and" << signers.second; + return 0; + } + catch (const std::exception &e) + { + qCritical() << e.what(); + } + return 1; + } + // Desktop entry #ifdef Q_OS_LINUX registerXdgMime(app); diff --git a/src/openpgp/CMakeLists.txt b/src/openpgp/CMakeLists.txt new file mode 100644 index 00000000..a5a36ee7 --- /dev/null +++ b/src/openpgp/CMakeLists.txt @@ -0,0 +1,18 @@ +file(GLOB_RECURSE SOURCES *.cpp) +file(GLOB_RECURSE HEADERS *.h) + +find_library(GCRYPT_LIBRARY gcrypt) +find_library(GPG_ERROR_LIBRARY gpg-error) + +add_library(openpgp + ${SOURCES} + ${HEADERS}) + +target_include_directories(openpgp + PUBLIC + ${CMAKE_SOURCE_DIR}/monero/contrib/epee/include) + +target_link_libraries(openpgp + PUBLIC + ${GCRYPT_LIBRARY} + ${GPG_ERROR_LIBRARY}) diff --git a/src/openpgp/hash.h b/src/openpgp/hash.h new file mode 100644 index 00000000..0952c602 --- /dev/null +++ b/src/openpgp/hash.h @@ -0,0 +1,107 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +#include +#include + +namespace openpgp +{ + +class hash +{ +public: + enum algorithm : uint8_t + { + sha256 = 8, + }; + + hash(const hash &) = delete; + hash &operator=(const hash &) = delete; + + hash(uint8_t algorithm) + : algorithm(algorithm) + , consumed(0) + { + if (gcry_md_open(&md, algorithm, 0) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to create message digest object"); + } + } + + ~hash() + { + gcry_md_close(md); + } + + hash &operator<<(uint8_t byte) + { + gcry_md_putc(md, byte); + ++consumed; + return *this; + } + + hash &operator<<(const epee::span &bytes) + { + gcry_md_write(md, &bytes[0], bytes.size()); + consumed += bytes.size(); + return *this; + } + + hash &operator<<(const std::vector &bytes) + { + return *this << epee::to_span(bytes); + } + + std::vector finish() const + { + std::vector result(gcry_md_get_algo_dlen(algorithm)); + const void *digest = gcry_md_read(md, algorithm); + if (digest == nullptr) + { + throw std::runtime_error("failed to read the digest"); + } + memcpy(&result[0], digest, result.size()); + return result; + } + + size_t consumed_bytes() const + { + return consumed; + } + +private: + const uint8_t algorithm; + gcry_md_hd_t md; + size_t consumed; +}; + +} diff --git a/src/openpgp/mpi.h b/src/openpgp/mpi.h new file mode 100644 index 00000000..800bcac5 --- /dev/null +++ b/src/openpgp/mpi.h @@ -0,0 +1,78 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +namespace openpgp +{ + +class mpi +{ +public: + mpi(const mpi &) = delete; + mpi &operator=(const mpi &) = delete; + + mpi(mpi &&other) + : data(other.data) + { + other.data = nullptr; + } + + template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> + mpi(const byte_container &buffer, gcry_mpi_format format = GCRYMPI_FMT_USG) + : mpi(&buffer[0], buffer.size(), format) + { + } + + mpi(const void *buffer, size_t size, gcry_mpi_format format = GCRYMPI_FMT_USG) + { + if (gcry_mpi_scan(&data, format, buffer, size, nullptr) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to read mpi from buffer"); + } + } + + ~mpi() + { + gcry_mpi_release(data); + } + + const gcry_mpi_t &get() const + { + return data; + } + +private: + gcry_mpi_t data; +}; + +} // namespace openpgp diff --git a/src/openpgp/openpgp.cpp b/src/openpgp/openpgp.cpp new file mode 100644 index 00000000..108990fb --- /dev/null +++ b/src/openpgp/openpgp.cpp @@ -0,0 +1,380 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "openpgp.h" + +#include +#include +#include + +#include + +#include "hash.h" +#include "mpi.h" +#include "packet_stream.h" +#include "s_expression.h" +#include "serialization.h" + +namespace openpgp +{ +namespace +{ + +std::string::const_iterator find_next_line(std::string::const_iterator begin, const std::string::const_iterator &end) +{ + begin = std::find(begin, end, '\n'); + return begin != end ? ++begin : end; +} + +std::string::const_iterator find_line_starting_with( + std::string::const_iterator it, + const std::string::const_iterator &end, + const std::string &starts_with) +{ + for (std::string::const_iterator next_line; it != end; it = next_line) + { + next_line = find_next_line(it, end); + const size_t line_length = static_cast(std::distance(it, next_line)); + if (line_length >= starts_with.size() && std::equal(starts_with.begin(), starts_with.end(), it)) + { + return it; + } + } + return end; +} + +std::string::const_iterator find_empty_line(std::string::const_iterator it, const std::string::const_iterator &end) +{ + for (; it != end && *it != '\r' && *it != '\n'; it = find_next_line(it, end)) + { + } + return it; +} + +std::string get_armored_block_contents(const std::string &text, const std::string &block_name) +{ + static constexpr const char dashes[] = "-----"; + const std::string armor_header = dashes + block_name + dashes; + auto block_start = find_line_starting_with(text.begin(), text.end(), armor_header); + auto block_headers = find_next_line(block_start, text.end()); + auto block_end = find_line_starting_with(block_headers, text.end(), dashes); + auto contents_begin = find_next_line(find_empty_line(block_headers, block_end), block_end); + if (contents_begin == block_end) + { + throw std::runtime_error("armored block not found"); + } + return std::string(contents_begin, block_end); +} + +} // namespace + +public_key_rsa::public_key_rsa(const std::string &armored) + : public_key_rsa(decode(armored)) +{ +} + +public_key_rsa::public_key_rsa(std::tuple params) + : m_expression(std::move(std::get<1>(params))) + , m_bits(std::get<2>(params)) + , m_user_id(std::move(std::get<0>(params))) +{ +} + +const gcry_sexp_t &public_key_rsa::get() const +{ + return m_expression.get(); +} + +size_t public_key_rsa::bits() const +{ + return m_bits; +} + +std::string public_key_rsa::user_id() const +{ + return m_user_id; +} + +std::tuple public_key_rsa::decode(const std::string &armored) +{ + const std::string buffer = epee::string_encoding::base64_decode( + strip_line_breaks(get_armored_block_contents(armored, "BEGIN PGP PUBLIC KEY BLOCK"))); + return decode(epee::to_byte_span(epee::to_span(buffer))); +} + +std::tuple public_key_rsa::decode(const epee::span buffer) +{ + packet_stream packets(buffer); + + const std::vector *data = packets.find_first(packet_tag::type::user_id); + if (data == nullptr) + { + throw std::runtime_error("user id is missing"); + } + std::string user_id(data->begin(), data->end()); + + data = packets.find_first(packet_tag::type::public_key); + if (data == nullptr) + { + throw std::runtime_error("public key is missing"); + } + + deserializer> serialized(*data); + + const auto version = serialized.read_big_endian(); + if (version != 4) + { + throw std::runtime_error("unsupported public key version"); + } + + /* const auto timestamp = */ serialized.read_big_endian(); + + const auto algorithm = serialized.read_big_endian(); + if (algorithm != algorithm::rsa) + { + throw std::runtime_error("unsupported public key algorithm"); + } + + const mpi public_key_n = serialized.read_mpi(); + const mpi public_key_e = serialized.read_mpi(); + + s_expression expression("(public-key (rsa (n %m) (e %m)))", public_key_n.get(), public_key_e.get()); + + return {std::move(user_id), std::move(expression), gcry_mpi_get_nbits(public_key_n.get())}; +} + +signature_rsa::signature_rsa( + uint8_t algorithm, + std::pair hash_leftmost_bytes, + uint8_t hash_algorithm, + const std::vector &hashed_data, + type type, + s_expression signature, + uint8_t version) + : m_hash_algorithm(hash_algorithm) + , m_hash_leftmost_bytes(hash_leftmost_bytes) + , m_hashed_appendix(format_hashed_appendix(algorithm, hash_algorithm, hashed_data, type, version)) + , m_signature(std::move(signature)) + , m_type(type) +{ +} + +signature_rsa signature_rsa::from_armored(const std::string &armored_signed_message) +{ + return from_base64(get_armored_block_contents(armored_signed_message, "BEGIN PGP SIGNATURE")); +} + +signature_rsa signature_rsa::from_base64(const std::string &base64) +{ + std::string decoded = epee::string_encoding::base64_decode(strip_line_breaks(base64)); + epee::span buffer(reinterpret_cast(&decoded[0]), decoded.size()); + return from_buffer(buffer); +} + +signature_rsa signature_rsa::from_buffer(const epee::span input) +{ + packet_stream packets(input); + + const std::vector *data = packets.find_first(packet_tag::type::signature); + if (data == nullptr) + { + throw std::runtime_error("signature is missing"); + } + + deserializer> buffer(*data); + + const auto version = buffer.read_big_endian(); + if (version != 4) + { + throw std::runtime_error("unsupported signature version"); + } + + const auto signature_type = static_cast(buffer.read_big_endian()); + + const auto algorithm = buffer.read_big_endian(); + if (algorithm != algorithm::rsa) + { + throw std::runtime_error("unsupported signature algorithm"); + } + + const auto hash_algorithm = buffer.read_big_endian(); + + const auto hashed_data_length = buffer.read_big_endian(); + std::vector hashed_data = buffer.read(hashed_data_length); + + const auto unhashed_data_length = buffer.read_big_endian(); + buffer.read_span(unhashed_data_length); + + std::pair hash_leftmost_bytes{buffer.read_big_endian(), buffer.read_big_endian()}; + + const mpi signature = buffer.read_mpi(); + + return signature_rsa( + algorithm, + std::move(hash_leftmost_bytes), + hash_algorithm, + hashed_data, + signature_type, + s_expression("(sig-val (rsa (s %m)))", signature.get()), + version); +} + +bool signature_rsa::verify(const epee::span message, const public_key_rsa &public_key) const +{ + const s_expression signed_data = hash_message(message, public_key.bits()); + return gcry_pk_verify(m_signature.get(), signed_data.get(), public_key.get()) == 0; +} + +s_expression signature_rsa::hash_message(const epee::span message, size_t public_key_bits) const +{ + switch (m_type) + { + case type::binary_document: + return hash_bytes(message, public_key_bits); + case type::canonical_text_document: + { + std::vector crlf_formatted; + crlf_formatted.reserve(message.size()); + const size_t message_size = message.size(); + for (size_t offset = 0; offset < message_size; ++offset) + { + const auto &character = message[offset]; + if (character == '\r') + { + continue; + } + if (character == '\n') + { + const bool skip_last_crlf = offset + 1 == message_size; + if (skip_last_crlf) + { + break; + } + crlf_formatted.push_back('\r'); + } + crlf_formatted.push_back(character); + } + return hash_bytes(epee::to_span(crlf_formatted), public_key_bits); + } + default: + throw std::runtime_error("unsupported signature type"); + } +} + +std::vector signature_rsa::hash_asn_object_id() const +{ + size_t size; + if (gcry_md_algo_info(m_hash_algorithm, GCRYCTL_GET_ASNOID, nullptr, &size) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to get ASN.1 Object Identifier (OID) size"); + } + + std::vector asn_object_id(size); + if (gcry_md_algo_info(m_hash_algorithm, GCRYCTL_GET_ASNOID, &asn_object_id[0], &size) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to get ASN.1 Object Identifier (OID)"); + } + + return asn_object_id; +} + +s_expression signature_rsa::hash_bytes(const epee::span message, size_t public_key_bits) const +{ + const std::vector plain_hash = (hash(m_hash_algorithm) << message << m_hashed_appendix).finish(); + if (plain_hash.size() < 2) + { + throw std::runtime_error("insufficient message hash size"); + } + if (plain_hash[0] != m_hash_leftmost_bytes.first || plain_hash[1] != m_hash_leftmost_bytes.second) + { + throw std::runtime_error("signature checksum doesn't match the expected value"); + } + + std::vector asn_object_id = hash_asn_object_id(); + + const size_t public_key_bytes = bits_to_bytes(public_key_bits); + if (public_key_bytes < plain_hash.size() + asn_object_id.size() + 11) + { + throw std::runtime_error("insufficient public key bit length"); + } + + std::vector emsa_pkcs1_v1_5_encoded; + emsa_pkcs1_v1_5_encoded.reserve(public_key_bytes); + emsa_pkcs1_v1_5_encoded.push_back(0); + emsa_pkcs1_v1_5_encoded.push_back(1); + const size_t ps_size = public_key_bytes - plain_hash.size() - asn_object_id.size() - 3; + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), ps_size, 0xff); + emsa_pkcs1_v1_5_encoded.push_back(0); + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), asn_object_id.begin(), asn_object_id.end()); + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), plain_hash.begin(), plain_hash.end()); + + mpi value(emsa_pkcs1_v1_5_encoded); + return s_expression("(data (flags raw) (value %m))", value.get()); +} + +std::vector signature_rsa::format_hashed_appendix( + uint8_t algorithm, + uint8_t hash_algorithm, + const std::vector &hashed_data, + uint8_t type, + uint8_t version) +{ + const uint16_t hashed_data_size = static_cast(hashed_data.size()); + const uint32_t hashed_pefix_size = sizeof(version) + sizeof(type) + sizeof(algorithm) + sizeof(hash_algorithm) + + sizeof(hashed_data_size) + hashed_data.size(); + + std::vector appendix; + appendix.reserve(hashed_pefix_size + sizeof(version) + sizeof(uint8_t) + sizeof(hashed_pefix_size)); + appendix.push_back(version); + appendix.push_back(type); + appendix.push_back(algorithm); + appendix.push_back(hash_algorithm); + appendix.push_back(static_cast(hashed_data_size >> 8)); + appendix.push_back(static_cast(hashed_data_size)); + appendix.insert(appendix.end(), hashed_data.begin(), hashed_data.end()); + appendix.push_back(version); + appendix.push_back(0xff); + appendix.push_back(static_cast(hashed_pefix_size >> 24)); + appendix.push_back(static_cast(hashed_pefix_size >> 16)); + appendix.push_back(static_cast(hashed_pefix_size >> 8)); + appendix.push_back(static_cast(hashed_pefix_size)); + + return appendix; +} + +message_armored::message_armored(const std::string &message_armored) + : m_message(get_armored_block_contents(message_armored, "BEGIN PGP SIGNED MESSAGE")) +{ +} + +message_armored::operator epee::span() const +{ + return epee::to_byte_span(epee::to_span(m_message)); +} + +} // namespace openpgp diff --git a/src/openpgp/openpgp.h b/src/openpgp/openpgp.h new file mode 100644 index 00000000..cef003ed --- /dev/null +++ b/src/openpgp/openpgp.h @@ -0,0 +1,122 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +#include + +#include + +#include "s_expression.h" + +namespace openpgp +{ + +enum algorithm : uint8_t +{ + rsa = 1, +}; + +class public_key_rsa +{ +public: + public_key_rsa(const std::string &armored); + public_key_rsa(std::tuple params); + + size_t bits() const; + const gcry_sexp_t &get() const; + std::string user_id() const; + +private: + static std::tuple decode(const std::string &armored); + static std::tuple decode(const epee::span buffer); + +private: + s_expression m_expression; + size_t m_bits; + std::string m_user_id; +}; + +class signature_rsa +{ +public: + enum type : uint8_t + { + binary_document = 0, + canonical_text_document = 1, + }; + + signature_rsa( + uint8_t algorithm, + std::pair hash_leftmost_bytes, + uint8_t hash_algorithm, + const std::vector &hashed_data, + type type, + s_expression signature, + uint8_t version); + + static signature_rsa from_armored(const std::string &armored_signed_message); + static signature_rsa from_base64(const std::string &base64); + static signature_rsa from_buffer(const epee::span input); + + bool verify(const epee::span message, const public_key_rsa &public_key) const; + +private: + s_expression hash_message(const epee::span message, size_t public_key_bits) const; + std::vector hash_asn_object_id() const; + s_expression hash_bytes(const epee::span message, size_t public_key_bits) const; + + static std::vector format_hashed_appendix( + uint8_t algorithm, + uint8_t hash_algorithm, + const std::vector &hashed_data, + uint8_t type, + uint8_t version); + +private: + uint8_t m_hash_algorithm; + std::pair m_hash_leftmost_bytes; + std::vector m_hashed_appendix; + s_expression m_signature; + type m_type; +}; + +class message_armored +{ +public: + message_armored(const std::string &message_armored); + + operator epee::span() const; + +private: + std::string m_message; +}; + +} // namespace openpgp diff --git a/src/openpgp/packet_stream.h b/src/openpgp/packet_stream.h new file mode 100644 index 00000000..b93b8747 --- /dev/null +++ b/src/openpgp/packet_stream.h @@ -0,0 +1,76 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +#include + +#include "serialization.h" + +namespace openpgp +{ + +class packet_stream +{ +public: + packet_stream(const epee::span buffer) + : packet_stream(deserializer>(buffer)) + { + } + + template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> + packet_stream(deserializer buffer) + { + while (!buffer.empty()) + { + packet_tag tag = buffer.read_packet_tag(); + packets.push_back({std::move(tag), buffer.read(tag.length)}); + } + } + + const std::vector *find_first(packet_tag::type type) const + { + for (const auto &packet : packets) + { + if (packet.first.packet_type == type) + { + return &packet.second; + } + } + return nullptr; + } + +private: + std::vector>> packets; +}; + +} // namespace openpgp diff --git a/src/openpgp/s_expression.h b/src/openpgp/s_expression.h new file mode 100644 index 00000000..b46bf216 --- /dev/null +++ b/src/openpgp/s_expression.h @@ -0,0 +1,78 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include + +namespace openpgp +{ + +class s_expression +{ +public: + s_expression(const s_expression &) = delete; + s_expression &operator=(const s_expression &) = delete; + + template + s_expression(Args... args) + { + if (gcry_sexp_build(&data, nullptr, args...) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to build S-expression"); + } + } + + s_expression(s_expression &&other) + { + std::swap(data, other.data); + } + + s_expression(gcry_sexp_t data) + : data(data) + { + } + + ~s_expression() + { + gcry_sexp_release(data); + } + + const gcry_sexp_t &get() const + { + return data; + } + +private: + gcry_sexp_t data = nullptr; +}; + +} // namespace openpgp diff --git a/src/openpgp/serialization.h b/src/openpgp/serialization.h new file mode 100644 index 00000000..33de9216 --- /dev/null +++ b/src/openpgp/serialization.h @@ -0,0 +1,171 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "mpi.h" + +namespace openpgp +{ + +size_t bits_to_bytes(size_t bits) +{ + constexpr const uint16_t bits_in_byte = 8; + return (bits + bits_in_byte - 1) / bits_in_byte; +} + +std::string strip_line_breaks(const std::string &string) +{ + std::string result; + result.reserve(string.size()); + for (const auto &character : string) + { + if (character != '\r' && character != '\n') + { + result.push_back(character); + } + } + return result; +} + +struct packet_tag +{ + enum type : uint8_t + { + signature = 2, + public_key = 6, + user_id = 13, + }; + + const type packet_type; + const size_t length; +}; + +template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> +class deserializer +{ +public: + deserializer(byte_container buffer) + : buffer(std::move(buffer)) + , cursor(0) + { + } + + bool empty() const + { + return buffer.size() - cursor == 0; + } + + packet_tag read_packet_tag() + { + const auto tag = read_big_endian(); + + constexpr const uint8_t format_mask = 0b11000000; + constexpr const uint8_t format_old_tag = 0b10000000; + if ((tag & format_mask) != format_old_tag) + { + throw std::runtime_error("invalid packet tag"); + } + + const packet_tag::type packet_type = static_cast((tag & 0b00111100) >> 2); + const uint8_t length_type = tag & 0b00000011; + + size_t length; + switch (length_type) + { + case 0: + length = read_big_endian(); + break; + case 1: + length = read_big_endian(); + break; + case 2: + length = read_big_endian(); + break; + default: + throw std::runtime_error("unsupported packet length type"); + } + + return {packet_type, length}; + } + + mpi read_mpi() + { + const size_t bit_length = read_big_endian(); + return mpi(read_span(bits_to_bytes(bit_length))); + } + + std::vector read(size_t size) + { + if (buffer.size() - cursor < size) + { + throw std::runtime_error("insufficient buffer size"); + } + + const size_t offset = cursor; + cursor += size; + + return {&buffer[offset], &buffer[cursor]}; + } + + template ::value>::type> + T read_big_endian() + { + if (buffer.size() - cursor < sizeof(T)) + { + throw std::runtime_error("insufficient buffer size"); + } + T result = 0; + for (size_t read = 0; read < sizeof(T); ++read) + { + result = (result << 8) | static_cast(buffer[cursor++]); + } + return result; + } + + epee::span read_span(size_t size) + { + if (buffer.size() - cursor < size) + { + throw std::runtime_error("insufficient buffer size"); + } + + const size_t offset = cursor; + cursor += size; + + return {reinterpret_cast(&buffer[offset]), size}; + } + +private: + byte_container buffer; + size_t cursor; +}; + +} // namespace openpgp diff --git a/src/qt/updater.cpp b/src/qt/updater.cpp new file mode 100644 index 00000000..5968c2b5 --- /dev/null +++ b/src/qt/updater.cpp @@ -0,0 +1,130 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "updater.h" + +#include + +#include "utils.h" + +Updater::Updater() +{ + m_maintainers.emplace_back(fileGetContents(":/monero/utils/gpg_keys/binaryfate.asc").toStdString()); + m_maintainers.emplace_back(fileGetContents(":/monero/utils/gpg_keys/fluffypony.asc").toStdString()); + m_maintainers.emplace_back(fileGetContents(":/monero/utils/gpg_keys/luigi1111.asc").toStdString()); +} + +QPair Updater::verifySignaturesAndHashSum( + const QByteArray &armoredSignedHashes, + const QByteArray &secondDetachedSignature, + const QString &binaryFilename, + const void *binaryData, + size_t binarySize) const +{ + QString firstSigner; + const QString signedMessage = verifySignature(armoredSignedHashes, firstSigner); + + QString secondSigner = verifySignature( + epee::span( + reinterpret_cast(armoredSignedHashes.data()), + armoredSignedHashes.size()), + openpgp::signature_rsa::from_buffer(epee::span( + reinterpret_cast(secondDetachedSignature.data()), + secondDetachedSignature.size()))); + + if (firstSigner == secondSigner) + { + throw std::runtime_error("both signatures were generated by the same person"); + } + + const QByteArray signedHash = parseShasumOutput(signedMessage, binaryFilename); + const QByteArray calculatedHash = getHash(binaryData, binarySize); + if (signedHash != calculatedHash) + { + throw std::runtime_error("hash sum mismatch"); + } + + return {firstSigner, secondSigner}; +} + +QByteArray Updater::getHash(const void *data, size_t size) const +{ + openpgp::hash hasher(openpgp::hash::algorithm::sha256); + hasher << epee::span(reinterpret_cast(data), size); + const std::vector hash = hasher.finish(); + return QByteArray(reinterpret_cast(&hash[0]), hash.size()); +} + +QByteArray Updater::parseShasumOutput(const QString &message, const QString &filename) const +{ + for (const auto &line : message.splitRef("\n")) + { + const auto trimmed = line.trimmed(); + if (trimmed.endsWith(filename)) + { + const int pos = trimmed.indexOf(' '); + if (pos != -1) + { + return QByteArray::fromHex(trimmed.left(pos).toUtf8()); + } + } + else if (trimmed.startsWith(filename)) + { + const int pos = trimmed.lastIndexOf(' '); + if (pos != -1) + { + return QByteArray::fromHex(trimmed.right(trimmed.size() - pos).toUtf8()); + } + } + } + + throw std::runtime_error("hash not found"); +} + +QString Updater::verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const +{ + const std::string messageString = armoredSignedMessage.toStdString(); + const openpgp::message_armored signedMessage(messageString); + signer = verifySignature(signedMessage, openpgp::signature_rsa::from_armored(messageString)); + + const epee::span message = signedMessage; + return QString(QByteArray(reinterpret_cast(&message[0]), message.size())); +} + +QString Updater::verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const +{ + for (const auto &maintainer : m_maintainers) + { + if (signature.verify(data, maintainer)) + { + return QString::fromStdString(maintainer.user_id()); + } + } + + throw std::runtime_error("not signed by a maintainer"); +} diff --git a/src/qt/updater.h b/src/qt/updater.h new file mode 100644 index 00000000..bc06d0c0 --- /dev/null +++ b/src/qt/updater.h @@ -0,0 +1,55 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include + +#include + +class Updater +{ +public: + Updater(); + + QPair verifySignaturesAndHashSum( + const QByteArray &armoredSignedHashes, + const QByteArray &secondDetachedSignature, + const QString &binaryFilename, + const void *binaryData, + size_t binarySize) const; + +private: + QByteArray getHash(const void *data, size_t size) const; + QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const; + QString verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const; + QByteArray parseShasumOutput(const QString &message, const QString &filename) const; + +private: + std::vector m_maintainers; +}; diff --git a/src/qt/utils.cpp b/src/qt/utils.cpp index bcf69917..258400c3 100644 --- a/src/qt/utils.cpp +++ b/src/qt/utils.cpp @@ -37,6 +37,24 @@ bool fileExists(QString path) { return check_file.exists() && check_file.isFile(); } +QByteArray fileGetContents(QString path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + throw std::runtime_error(QString("failed to open %1").arg(path).toStdString()); + } + + QByteArray data; + data.resize(file.size()); + if (file.read(data.data(), data.size()) != data.size()) + { + throw std::runtime_error(QString("failed to read %1").arg(path).toStdString()); + } + + return data; +} + QByteArray fileOpen(QString path) { QFile file(path); if(!file.open(QFile::ReadOnly | QFile::Text)) diff --git a/src/qt/utils.h b/src/qt/utils.h index 17fb44d9..aace5d00 100644 --- a/src/qt/utils.h +++ b/src/qt/utils.h @@ -34,6 +34,7 @@ #include bool fileExists(QString path); +QByteArray fileGetContents(QString path); QByteArray fileOpen(QString path); bool fileWrite(QString path, QString data); QString getAccountName();