From a98ebfbd2a6ad045e1f0594e4914179cb49b63a7 Mon Sep 17 00:00:00 2001 From: Andrea Marchesini Date: Wed, 22 Jun 2022 17:16:29 +0200 Subject: [PATCH] Signature for addon manifests (#3751) * Signature for addon manifests * Set the addon URL for production --- .github/workflows/wasm.yaml | 9 + scripts/linux/script.sh | 6 +- signature/Cargo.lock | 212 ++++++++++++++++++++++ signature/Cargo.toml | 12 ++ signature/src/lib.rs | 32 ++++ src/addonmanager.cpp | 101 ++++++++--- src/addonmanager.h | 9 +- src/cmake/linux.cmake | 2 + src/cmake/macos.cmake | 4 +- src/cmake/signature.cmake | 33 ++++ src/cmake/sources.cmake | 3 + src/cmake/windows.cmake | 2 + src/constants.cpp | 22 +++ src/constants.h | 20 +- src/networkrequest.cpp | 34 ++-- src/qmake/signature.pri | 25 +++ src/qmake/sources.pri | 3 + src/resources/public_keys/production.der | Bin 0 -> 270 bytes src/resources/public_keys/public_keys.qrc | 6 + src/resources/public_keys/staging.der | Bin 0 -> 270 bytes src/settingsholder.cpp | 22 --- src/settingsholder.h | 2 - src/settingslist.h | 13 +- src/signature.cpp | 30 +++ src/signature.h | 16 ++ src/src.pro | 3 +- src/tasks/addonindex/taskaddonindex.cpp | 62 +++++-- src/tasks/addonindex/taskaddonindex.h | 7 + tests/qml/mocconstants.cpp | 6 + tests/unit/CMakeLists.txt | 2 + tests/unit/mocnetworkrequest.cpp | 4 +- tests/unit/unit.pro | 3 + 32 files changed, 600 insertions(+), 105 deletions(-) create mode 100644 signature/Cargo.lock create mode 100644 signature/Cargo.toml create mode 100644 signature/src/lib.rs create mode 100644 src/cmake/signature.cmake create mode 100644 src/qmake/signature.pri create mode 100644 src/resources/public_keys/production.der create mode 100644 src/resources/public_keys/public_keys.qrc create mode 100644 src/resources/public_keys/staging.der create mode 100644 src/signature.cpp create mode 100644 src/signature.h diff --git a/.github/workflows/wasm.yaml b/.github/workflows/wasm.yaml index 0afff36794..4f9361843f 100644 --- a/.github/workflows/wasm.yaml +++ b/.github/workflows/wasm.yaml @@ -126,6 +126,15 @@ jobs: export PATH=/opt/$QTVERSION/gcc_64/bin:$PATH python3 scripts/addon/generate_all.py + - name: Sign manifest + shell: bash + env: + ADDON_PRIVATE_KEY: ${{ secrets.ADDON_PRIVATE_KEY }} + run: | + echo -n "$ADDON_PRIVATE_KEY" > addon_private_key.pem + openssl dgst -sha256 -sign addon_private_key.pem -out addons/generated/addons/manifest.json.sign addons/generated/addons/manifest.json + rm addon_private_key.pem + - name: Uploading uses: actions/upload-artifact@v1 with: diff --git a/scripts/linux/script.sh b/scripts/linux/script.sh index 637a4018b7..7df6bb0f52 100755 --- a/scripts/linux/script.sh +++ b/scripts/linux/script.sh @@ -170,10 +170,14 @@ printn Y "Downloading Go dependencies..." (cd $WORKDIR/linux/netfilter && go mod vendor) print G "done." -printn Y "Downloading Rust dependencies..." +printn Y "Downloading Rust dependencies (extension)..." (cd $WORKDIR/extension/bridge && mkdir -p .cargo && cargo vendor > .cargo/config.toml) print G "done." +printn Y "Downloading Rust dependencies (signature)..." +(cd $WORKDIR/signature && mkdir -p .cargo && cargo vendor > .cargo/config.toml) +print G "done." + printn Y "Removing the packaging templates... " rm -f $WORKDIR/linux/mozillavpn.spec || die "Failed" rm -rf $WORKDIR/linux/debian || die "Failed" diff --git a/signature/Cargo.lock b/signature/Cargo.lock new file mode 100644 index 0000000000..93669afb27 --- /dev/null +++ b/signature/Cargo.lock @@ -0,0 +1,212 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "once_cell" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "signature" +version = "0.1.0" +dependencies = [ + "ring", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/signature/Cargo.toml b/signature/Cargo.toml new file mode 100644 index 0000000000..5cbd584fea --- /dev/null +++ b/signature/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "signature" +version = "0.1.0" +edition = "2021" + +[dependencies] +ring = "0.16.20" + +[lib] +name = "signature" +path = "src/lib.rs" +crate-type = ["staticlib"] diff --git a/signature/src/lib.rs b/signature/src/lib.rs new file mode 100644 index 0000000000..21f52cf662 --- /dev/null +++ b/signature/src/lib.rs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use ring::signature; +use std::os::raw::c_uchar; + +#[no_mangle] +pub extern "C" fn verify_rsa( + public_key_ptr: *const c_uchar, + public_key_length: usize, + message_ptr: *const c_uchar, + message_length: usize, + message_signature_ptr: *const c_uchar, + message_signature_length: usize, +) -> bool { + let public_key_str = unsafe { std::slice::from_raw_parts(public_key_ptr, public_key_length) }; + let public_key = + signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, &public_key_str); + + let message_str = unsafe { std::slice::from_raw_parts(message_ptr, message_length) }; + let message_signature_str = + unsafe { std::slice::from_raw_parts(message_signature_ptr, message_signature_length) }; + + match public_key.verify(message_str, message_signature_str) { + Err(e) => { + eprintln!("{}", e); + false + } + Ok(_) => true, + } +} diff --git a/src/addonmanager.cpp b/src/addonmanager.cpp index 94bcb21f1b..5ec0243c54 100644 --- a/src/addonmanager.cpp +++ b/src/addonmanager.cpp @@ -7,6 +7,7 @@ #include "leakdetector.h" #include "logger.h" #include "models/feature.h" +#include "signature.h" #include "taskscheduler.h" #include "tasks/addon/taskaddon.h" @@ -22,6 +23,7 @@ constexpr const char* ADDON_FOLDER = "addons"; constexpr const char* ADDON_INDEX_FILENAME = "manifest.json"; +constexpr const char* ADDON_INDEX_SIGNATURE_FILENAME = "manifest.json.sign"; namespace { @@ -70,29 +72,49 @@ void AddonManager::initialize() { } } - if (!validateIndex(readIndex())) { + QByteArray index; + QByteArray indexSignature; + if (!readIndex(index, indexSignature)) { + logger.info() << "Unable to read the addon index"; + return; + } + + if (!validateIndex(index, indexSignature)) { logger.debug() << "Unable to validate the index"; } } -void AddonManager::updateIndex(const QByteArray& index) { - QByteArray currentIndex = readIndex(); - - if (currentIndex == index) { +void AddonManager::updateIndex(const QByteArray& index, + const QByteArray& indexSignature) { + QByteArray currentIndex; + QByteArray currentIndexSignature; + if (readIndex(currentIndex, currentIndexSignature) && currentIndex == index && + currentIndexSignature == indexSignature) { logger.debug() << "The index has not changed"; return; } - if (!validateIndex(index)) { + if (!validateIndex(index, indexSignature)) { logger.debug() << "Unable to validate the index"; return; } - writeIndex(index); + writeIndex(index, indexSignature); } -bool AddonManager::validateIndex(const QByteArray& index) { - // TODO: signature validation +bool AddonManager::validateIndex(const QByteArray& index, + const QByteArray& indexSignature) { + QFile publicKeyFile(Constants::addonPublicKeyFile()); + if (!publicKeyFile.open(QIODevice::ReadOnly)) { + logger.warning() << "Unable to open the addon public key file"; + return false; + } + + QByteArray publicKey = publicKeyFile.readAll(); + if (!Signature::verify(publicKey, index, indexSignature)) { + logger.warning() << "Unable to verify the signature of the addon index"; + return false; + } QJsonDocument doc = QJsonDocument::fromJson(index); if (!doc.isObject()) { @@ -228,36 +250,69 @@ bool AddonManager::addonDir(QDir* dir) { } // static -QByteArray AddonManager::readIndex() { +bool AddonManager::readIndex(QByteArray& index, QByteArray& indexSignature) { QDir dir; if (!addonDir(&dir)) { - return ""; + return false; } - QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); - if (!indexFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - logger.warning() << "Unable to open the addon index"; - return ""; + // Index file + { + QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); + if (!indexFile.open(QIODevice::ReadOnly)) { + logger.warning() << "Unable to open the addon index"; + return false; + } + + index = indexFile.readAll(); } - return indexFile.readAll(); + // Index signature file + { + QFile indexSignatureFile(dir.filePath(ADDON_INDEX_SIGNATURE_FILENAME)); + if (!indexSignatureFile.open(QIODevice::ReadOnly)) { + logger.warning() << "Unable to open the addon index signature"; + return false; + } + + indexSignature = indexSignatureFile.readAll(); + } + + return true; } // static -void AddonManager::writeIndex(const QByteArray& index) { +void AddonManager::writeIndex(const QByteArray& index, + const QByteArray& indexSignature) { QDir dir; if (!addonDir(&dir)) { return; } - QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); - if (!indexFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - logger.warning() << "Unable to open the addon index"; - return; + // Index file + { + QFile indexFile(dir.filePath(ADDON_INDEX_FILENAME)); + if (!indexFile.open(QIODevice::WriteOnly)) { + logger.warning() << "Unable to open the addon index file"; + return; + } + + if (!indexFile.write(index)) { + logger.warning() << "Unable to write the addon index file"; + } } - if (!indexFile.write(index)) { - logger.warning() << "Unable to write the addon file"; + // Index signature file + { + QFile indexSignatureFile(dir.filePath(ADDON_INDEX_SIGNATURE_FILENAME)); + if (!indexSignatureFile.open(QIODevice::WriteOnly)) { + logger.warning() << "Unable to open the addon index signature file"; + return; + } + + if (!indexSignatureFile.write(indexSignature)) { + logger.warning() << "Unable to write the addon index signature file"; + } } } diff --git a/src/addonmanager.h b/src/addonmanager.h index c06e9c45a1..aa9ac77e9c 100644 --- a/src/addonmanager.h +++ b/src/addonmanager.h @@ -21,7 +21,7 @@ class AddonManager final : public QObject { ~AddonManager(); - void updateIndex(const QByteArray& index); + void updateIndex(const QByteArray& index, const QByteArray& indexSignature); void storeAndLoadAddon(const QByteArray& addonData, const QString& addonId, const QByteArray& sha256); @@ -38,13 +38,14 @@ class AddonManager final : public QObject { void initialize(); - bool validateIndex(const QByteArray& index); + bool validateIndex(const QByteArray& index, const QByteArray& indexSignature); bool validateAndLoad(const QString& addonId, const QByteArray& sha256, bool checkSha256 = true); static bool addonDir(QDir* dir); - static QByteArray readIndex(); - static void writeIndex(const QByteArray& index); + static bool readIndex(QByteArray& index, QByteArray& indexSignature); + static void writeIndex(const QByteArray& index, + const QByteArray& indexSignature); static void removeAddon(const QString& addonId); diff --git a/src/cmake/linux.cmake b/src/cmake/linux.cmake index 0e44005fb9..fb4bb68c30 100644 --- a/src/cmake/linux.cmake +++ b/src/cmake/linux.cmake @@ -66,6 +66,8 @@ target_sources(mozillavpn PRIVATE add_definitions(-DPROTOCOL_VERSION=\"1\") +include(cmake/signature.cmake) + set(DBUS_GENERATED_SOURCES) qt_add_dbus_interface(DBUS_GENERATED_SOURCES platforms/linux/daemon/org.mozilla.vpn.dbus.xml dbus_interface) qt_add_dbus_adaptor(DBUS_GENERATED_SOURCES diff --git a/src/cmake/macos.cmake b/src/cmake/macos.cmake index 2ef8674cfc..693ba46c18 100644 --- a/src/cmake/macos.cmake +++ b/src/cmake/macos.cmake @@ -103,6 +103,8 @@ target_sources(mozillavpn PRIVATE update/balrog.h ) +include(cmake/signature.cmake) + ## A helper to copy files into the application bundle function(add_bundle_file SOURCE) add_custom_command(TARGET mozillavpn POST_BUILD @@ -177,4 +179,4 @@ add_bundle_file(${CMAKE_CURRENT_SOURCE_DIR}/../extension/manifests/macos/mozilla #GCC_PREPROCESSOR_DEFINITIONS.name = "GCC_PREPROCESSOR_DEFINITIONS" #GCC_PREPROCESSOR_DEFINITIONS.value = 'GROUP_ID=\"$${MVPN_DEVELOPMENT_TEAM}.$${MVPN_APP_ID_MACOS}\"' #QMAKE_MAC_XCODE_SETTINGS += GCC_PREPROCESSOR_DEFINITIONS -# \ No newline at end of file +# diff --git a/src/cmake/signature.cmake b/src/cmake/signature.cmake new file mode 100644 index 0000000000..966e18c6cb --- /dev/null +++ b/src/cmake/signature.cmake @@ -0,0 +1,33 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CARGO_CMD cargo build) + set(TARGET_DIR "debug") +else () + set(CARGO_CMD cargo build --release) + set(TARGET_DIR "release") +endif () + +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + set(LIBNAME "signature.lib") +else() + set(LIBNAME "libsignature.a") +endif() + +get_filename_component(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR} ABSOLUTE) + +add_custom_command( + OUTPUT ${GENERATED_DIR}/${LIBNAME} + MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../signature/src/lib.rs + COMMAND ${CARGO_CMD} --target-dir "${CMAKE_CURRENT_BINARY_DIR}" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../signature) + +target_sources(mozillavpn PRIVATE + ${GENERATED_DIR}/${LIBNAME} +) +target_link_libraries(mozillavpn PRIVATE ${GENERATED_DIR}/${LIBNAME}) +target_link_libraries(mozillavpn PUBLIC ${CMAKE_DL_LIBS}) + +add_definitions(-DMVPN_SIGNATURE) diff --git a/src/cmake/sources.cmake b/src/cmake/sources.cmake index 098f99301e..4a8b1cf044 100644 --- a/src/cmake/sources.cmake +++ b/src/cmake/sources.cmake @@ -240,6 +240,8 @@ target_sources(mozillavpn PRIVATE serveri18n.h settingsholder.cpp settingsholder.h + signature.h + signature.cpp simplenetworkmanager.cpp simplenetworkmanager.h statusicon.cpp @@ -315,6 +317,7 @@ target_sources(mozillavpn PRIVATE ui/resources.qrc ui/ui.qrc resources/certs/certs.qrc + resources/public_keys/public_keys.qrc ) # Signal handling for unix platforms diff --git a/src/cmake/windows.cmake b/src/cmake/windows.cmake index a801b8fd7c..0b79b222e9 100644 --- a/src/cmake/windows.cmake +++ b/src/cmake/windows.cmake @@ -88,6 +88,8 @@ target_sources(mozillavpn PRIVATE update/balrog.h ) +include(cmake/signature.cmake) + install(TARGETS mozillavpn DESTINATION .) install(FILES $ DESTINATION . OPTIONAL) diff --git a/src/constants.cpp b/src/constants.cpp index 794f0fb1ff..d94a9b2667 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -6,8 +6,10 @@ #include "settingsholder.h" #include "version.h" +#include #include #include +#include namespace { bool s_inProduction = true; @@ -29,3 +31,23 @@ void Constants::setStaging() { QString Constants::versionString() { return QStringLiteral(APP_VERSION); } QString Constants::buildNumber() { return QStringLiteral(BUILD_ID); } + +QString Constants::envOrDefault(const QString& name, + const QString& defaultValue) { + QString env; + + QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); + if (pe.contains(name)) { + env = pe.value(name); + } + + if (env.isEmpty()) { + return defaultValue; + } + + if (!QUrl(env).isValid()) { + return defaultValue; + } + + return env; +} diff --git a/src/constants.h b/src/constants.h index fb5967a9c6..363be20e23 100644 --- a/src/constants.h +++ b/src/constants.h @@ -6,8 +6,7 @@ #define CONSTANTS_H #include - -class QString; +#include namespace Constants { @@ -19,6 +18,7 @@ void setStaging(); // Project version and build strings. QString versionString(); QString buildNumber(); +QString envOrDefault(const QString& name, const QString& defaultValue); // Number of msecs for the captive-portal block alert. constexpr uint32_t CAPTIVE_PORTAL_ALERT_MSEC = 4000; @@ -93,8 +93,9 @@ constexpr auto CRASH_STAGING_URL = "https://crash-reports.allizom.org/submit"; constexpr const char* LOGO_URL = ":/nebula/resources/logo-dock.png"; -PRODBETAEXPR(const char*, fxaApiBaseUrl, "https://api.accounts.firefox.com", - "https://api-accounts.stage.mozaws.net") +PRODBETAEXPR(QString, fxaApiBaseUrl, "https://api.accounts.firefox.com", + envOrDefault("MVPN_FXA_API_BASE_URL", + "https://api-accounts.stage.mozaws.net")) PRODBETAEXPR(const char*, fxaUrl, "https://accounts.firefox.com", "https://accounts.stage.mozaws.net") PRODBETAEXPR( @@ -106,8 +107,15 @@ PRODBETAEXPR( const char*, balrogRootCertFingerprint, "97e8ba9cf12fb3de53cc42a4e6577ed64df493c247b414fea036818d3823560e", "3c01446abe9036cea9a09acaa3a520ac628f20a7ae32ce861cb2efb70fa0c745"); -PRODBETAEXPR(const char*, addonSourceUrl, "TODO", - "https://mozilla-mobile.github.io/mozilla-vpn-client/addons/") +PRODBETAEXPR( + QString, addonSourceUrl, + "https://mozilla-mobile.github.io/mozilla-vpn-client/addons/", // TODO + envOrDefault("MVPN_ADDON_URL", + "https://mozilla-mobile.github.io/mozilla-vpn-client/addons/")) + +PRODBETAEXPR(const char*, addonPublicKeyFile, + ":/addons_signature/production.der", + ":/addons_signature/staging.der"); #undef PRODBETAEXPR diff --git a/src/networkrequest.cpp b/src/networkrequest.cpp index d8488deddb..c74566163b 100644 --- a/src/networkrequest.cpp +++ b/src/networkrequest.cpp @@ -34,16 +34,6 @@ constexpr const char* IPINFO_URL_IPV6 = "https://[%1]/api/v1/vpn/ipinfo"; namespace { Logger logger(LOG_NETWORKING, "NetworkRequest"); QList s_intervention_certs; - -QString fxaApiBaseUrl() { - if (Constants::inProduction()) { - return Constants::fxaApiBaseUrl(); - } - - return SettingsHolder::instance()->envOrDefault("MVPN_FXA_API_BASE_URL", - Constants::fxaApiBaseUrl()); -} - } // namespace NetworkRequest::NetworkRequest(Task* parent, int status, @@ -473,7 +463,7 @@ NetworkRequest* NetworkRequest::createForFxaAccountStatus( r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/status"); r->m_request.setUrl(url); @@ -494,7 +484,7 @@ NetworkRequest* NetworkRequest::createForFxaAccountCreation( const QString& fxaFlowId, double fxaFlowBeginTime) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/create"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -527,7 +517,7 @@ NetworkRequest* NetworkRequest::createForFxaLogin( const QString& fxaFlowId, double fxaFlowBeginTime) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/login"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -571,7 +561,7 @@ NetworkRequest* NetworkRequest::createForFxaSendUnblockCode( r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/login/send_unblock_code"); r->m_request.setUrl(url); @@ -591,7 +581,7 @@ NetworkRequest* NetworkRequest::createForFxaSessionVerifyByEmailCode( const QString& fxaClientId, const QString& fxaScope) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/session/verify_code"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -629,7 +619,7 @@ NetworkRequest* NetworkRequest::createForFxaSessionResendCode( Task* parent, const QByteArray& sessionToken) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/session/resend_code"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -652,7 +642,7 @@ NetworkRequest* NetworkRequest::createForFxaSessionVerifyByTotpCode( const QString& fxaClientId, const QString& fxaScope) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/session/verify/totp"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -683,7 +673,7 @@ NetworkRequest* NetworkRequest::createForFxaAuthz( const QString& fxaAccessType) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/oauth/authorization"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -711,7 +701,7 @@ NetworkRequest* NetworkRequest::createForFxaTotpCreation( Task* parent, const QByteArray& sessionToken) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/totp/create"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -733,7 +723,7 @@ NetworkRequest* NetworkRequest::createForFxaAttachedClients( Task* parent, const QByteArray& sessionToken) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/attached_clients"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -753,7 +743,7 @@ NetworkRequest* NetworkRequest::createForFxaAccountDeletion( const QByteArray& authpw) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/account/destroy"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, @@ -778,7 +768,7 @@ NetworkRequest* NetworkRequest::createForFxaSessionDestroy( Task* parent, const QByteArray& sessionToken) { NetworkRequest* r = new NetworkRequest(parent, 200, false); - QUrl url(fxaApiBaseUrl()); + QUrl url(Constants::fxaApiBaseUrl()); url.setPath("/v1/session/destroy"); r->m_request.setUrl(url); r->m_request.setHeader(QNetworkRequest::ContentTypeHeader, diff --git a/src/qmake/signature.pri b/src/qmake/signature.pri new file mode 100644 index 0000000000..9eb9bbed9a --- /dev/null +++ b/src/qmake/signature.pri @@ -0,0 +1,25 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +linux:!android|macos|win* { + win* { + LIBNAME=signature.lib + } else { + LIBNAME=libsignature.a + } + + signatureCrate.input = SIGNATURE_CRATE + signatureCrate.output = ${QMAKE_FILE_IN}/target/release/$$LIBNAME + signatureCrate.commands = @echo Building signature rust create staticlib \ + && cd ${QMAKE_FILE_IN} \ + && cargo build --release + signatureCrate.clean = ${QMAKE_FILE_OUT} + signatureCrate.CONFIG = target_predeps + signatureCrate.depends += ${QMAKE_FILE_IN}/Cargo.toml ${QMAKE_FILE_IN}/src/lib.rs + signatureCrate.variable_out = LIBS + + QMAKE_EXTRA_COMPILERS += signatureCrate + SIGNATURE_CRATE = $$PWD/../../signature + DEFINES += MVPN_SIGNATURE +} diff --git a/src/qmake/sources.pri b/src/qmake/sources.pri index dd903250fd..39c8de4e23 100644 --- a/src/qmake/sources.pri +++ b/src/qmake/sources.pri @@ -119,6 +119,7 @@ SOURCES += \ rfc/rfc5735.cpp \ serveri18n.cpp \ settingsholder.cpp \ + signature.cpp \ simplenetworkmanager.cpp \ statusicon.cpp \ subscriptiondata.cpp \ @@ -270,6 +271,7 @@ HEADERS += \ rfc/rfc5735.h \ serveri18n.h \ settingsholder.h \ + signature.h \ simplenetworkmanager.h \ statusicon.h \ subscriptiondata.h \ @@ -315,3 +317,4 @@ RESOURCES += ui/resources.qrc RESOURCES += ui/license.qrc RESOURCES += ui/ui.qrc RESOURCES += resources/certs/certs.qrc +RESOURCES += resources/public_keys/public_keys.qrc diff --git a/src/resources/public_keys/production.der b/src/resources/public_keys/production.der new file mode 100644 index 0000000000000000000000000000000000000000..9419aa969e0cb582049015c699953f725413f564 GIT binary patch literal 270 zcmV+p0rCDYf&mHwf&l>l!TZUXEe`Ws#Z}}^Wd22+!k;7jSN$+`4%N~IVIZWbzq8)E z{`~WtaqC`_w|RrXQ!{ao8-SOxWm+ut*EVXOnj-GO=-O%-F;o)bH z`usMcE<39FH{4RMP_3{>C94J3qIpm@z}1WsS>~^x@dB8mi0Kf;Yw^=jAIly{IJ>;) z5L&YXf=SQ5b181j1j)J1-~)G$+!bWtHbDh3G>Xd%sBL)Fl6kcQkBh`SFKG9@dyNvL zl<8F*BgYXiPGI*4@GUx}$a>-jh^;0b=`IzIUR!gL-xKbO2?<>YQvgvy2CCuZ>#R-N UKpfe4?~P!eD1gxy0s{d60Z0OYn*aa+ literal 0 HcmV?d00001 diff --git a/src/resources/public_keys/public_keys.qrc b/src/resources/public_keys/public_keys.qrc new file mode 100644 index 0000000000..26a8230e60 --- /dev/null +++ b/src/resources/public_keys/public_keys.qrc @@ -0,0 +1,6 @@ + + + staging.der + production.der + + diff --git a/src/resources/public_keys/staging.der b/src/resources/public_keys/staging.der new file mode 100644 index 0000000000000000000000000000000000000000..9419aa969e0cb582049015c699953f725413f564 GIT binary patch literal 270 zcmV+p0rCDYf&mHwf&l>l!TZUXEe`Ws#Z}}^Wd22+!k;7jSN$+`4%N~IVIZWbzq8)E z{`~WtaqC`_w|RrXQ!{ao8-SOxWm+ut*EVXOnj-GO=-O%-F;o)bH z`usMcE<39FH{4RMP_3{>C94J3qIpm@z}1WsS>~^x@dB8mi0Kf;Yw^=jAIly{IJ>;) z5L&YXf=SQ5b181j1j)J1-~)G$+!bWtHbDh3G>Xd%sBL)Fl6kcQkBh`SFKG9@dyNvL zl<8F*BgYXiPGI*4@GUx}$a>-jh^;0b=`IzIUR!gL-xKbO2?<>YQvgvy2CCuZ>#R-N UKpfe4?~P!eD1gxy0s{d60Z0OYn*aa+ literal 0 HcmV?d00001 diff --git a/src/settingsholder.cpp b/src/settingsholder.cpp index 2021c0d10c..9d4abc0fff 100644 --- a/src/settingsholder.cpp +++ b/src/settingsholder.cpp @@ -10,8 +10,6 @@ #include "models/feature.h" #include -#include -#include namespace { @@ -154,23 +152,3 @@ void SettingsHolder::removeEntryServer() { m_settings.remove("entryServer/countryCode"); m_settings.remove("entryServer/city"); } - -QString SettingsHolder::envOrDefault(const QString& name, - const QString& defaultValue) const { - QString env; - - QProcessEnvironment pe = QProcessEnvironment::systemEnvironment(); - if (pe.contains(name)) { - env = pe.value(name); - } - - if (env.isEmpty()) { - return defaultValue; - } - - if (!QUrl(env).isValid()) { - return defaultValue; - } - - return env; -} diff --git a/src/settingsholder.h b/src/settingsholder.h index 51c9ae51cd..68a2824559 100644 --- a/src/settingsholder.h +++ b/src/settingsholder.h @@ -63,8 +63,6 @@ class SettingsHolder final : public QObject { void removeEntryServer(); - QString envOrDefault(const QString& name, const QString& defaultValue) const; - // Delete _ALL_ the settings. Probably this method is not what you want to // use. void hardReset(); diff --git a/src/settingslist.h b/src/settingslist.h index f1f7fca0e4..f079cc2321 100644 --- a/src/settingslist.h +++ b/src/settingslist.h @@ -280,13 +280,14 @@ SETTING_BOOL(serverSwitchNotification, // getter false // remove when reset ) -SETTING_STRING(stagingServerAddress, // getter - setStagingServerAddress, // setter - hasStagingServerAddress, // has - "stagingServerAddress", // key - envOrDefault("MVPN_API_BASE_URL", +SETTING_STRING( + stagingServerAddress, // getter + setStagingServerAddress, // setter + hasStagingServerAddress, // has + "stagingServerAddress", // key + Constants::envOrDefault("MVPN_API_BASE_URL", Constants::API_STAGING_URL), // default value - false // remove when reset + false // remove when reset ) SETTING_BOOL(stagingServer, // getter diff --git a/src/signature.cpp b/src/signature.cpp new file mode 100644 index 0000000000..7cfb629e61 --- /dev/null +++ b/src/signature.cpp @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "signature.h" + +#ifdef MVPN_SIGNATURE +extern "C" { +// Implemented in rust. See the `signature` folder. +bool verify_rsa(const char* publicKey, size_t pubKeyLen, const char* message, + size_t messageLen, const char* signature, size_t signatureLen); +}; +#endif + +// static +bool Signature::verify(const QByteArray& publicKey, const QByteArray& content, + const QByteArray& signature) { +#ifdef MVPN_SIGNATURE + return verify_rsa(publicKey.constData(), publicKey.length(), + content.constData(), content.length(), + signature.constData(), signature.length()); +#endif + + Q_UNUSED(publicKey); + Q_UNUSED(content); + Q_UNUSED(signature); + + // No RSA-signature implementation + return true; +} diff --git a/src/signature.h b/src/signature.h new file mode 100644 index 0000000000..f2e60d3126 --- /dev/null +++ b/src/signature.h @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef SIGNATURE_H +#define SIGNATURE_H + +#include + +class Signature final { + public: + static bool verify(const QByteArray& publicKey, const QByteArray& content, + const QByteArray& signature); +}; + +#endif // SIGNATURE_H diff --git a/src/src.pro b/src/src.pro index 22d6bde54c..40e40c6fad 100644 --- a/src/src.pro +++ b/src/src.pro @@ -7,12 +7,13 @@ # Because this is a complex project, with dependencies, extra compilers, # modules, etc, each logical block is included as a separate PRI file. -TEMPLATE = app +TEMPLATE = app include($$PWD/qmake/balrog.pri) include($$PWD/qmake/debug.pri) include($$PWD/qmake/includes_and_defines.pri) include($$PWD/qmake/qt.pri) +include($$PWD/qmake/signature.pri) include($$PWD/qmake/webextension.pri) include($$PWD/../glean/glean.pri) include($$PWD/../nebula/nebula.pri) diff --git a/src/tasks/addonindex/taskaddonindex.cpp b/src/tasks/addonindex/taskaddonindex.cpp index 8804b51c43..1615535a31 100644 --- a/src/tasks/addonindex/taskaddonindex.cpp +++ b/src/tasks/addonindex/taskaddonindex.cpp @@ -20,19 +20,51 @@ TaskAddonIndex::TaskAddonIndex() : Task("TaskAddonIndex") { TaskAddonIndex::~TaskAddonIndex() { MVPN_COUNT_DTOR(TaskAddonIndex); } void TaskAddonIndex::run() { - NetworkRequest* request = NetworkRequest::createForGetUrl( - this, QString("%1manifest.json").arg(Constants::addonSourceUrl()), 200); - - connect(request, &NetworkRequest::requestFailed, this, - [this](QNetworkReply::NetworkError error, const QByteArray&) { - logger.error() << "Get addon index failed" << error; - emit completed(); - }); - - connect(request, &NetworkRequest::requestCompleted, this, - [this](const QByteArray& data) { - logger.debug() << "Get addon index completed"; - AddonManager::instance()->updateIndex(data); - emit completed(); - }); + // Index file + { + NetworkRequest* request = NetworkRequest::createForGetUrl( + this, QString("%1manifest.json").arg(Constants::addonSourceUrl()), 200); + + connect(request, &NetworkRequest::requestFailed, this, + [this](QNetworkReply::NetworkError error, const QByteArray&) { + logger.error() << "Get addon index failed" << error; + emit completed(); + }); + + connect(request, &NetworkRequest::requestCompleted, this, + [this](const QByteArray& data) { + logger.debug() << "Get addon index completed"; + m_indexData = data; + maybeComplete(); + }); + } + + // Index file signature + { + NetworkRequest* request = NetworkRequest::createForGetUrl( + this, QString("%1manifest.json.sign").arg(Constants::addonSourceUrl()), + 200); + + connect(request, &NetworkRequest::requestFailed, this, + [this](QNetworkReply::NetworkError error, const QByteArray&) { + logger.error() << "Get addon index signature failed" << error; + emit completed(); + }); + + connect(request, &NetworkRequest::requestCompleted, this, + [this](const QByteArray& data) { + logger.debug() << "Get addon index signature completed"; + m_indexSignData = data; + maybeComplete(); + }); + } +} + +void TaskAddonIndex::maybeComplete() { + if (m_indexData.isEmpty() || m_indexSignData.isEmpty()) { + return; + } + + AddonManager::instance()->updateIndex(m_indexData, m_indexSignData); + emit completed(); } diff --git a/src/tasks/addonindex/taskaddonindex.h b/src/tasks/addonindex/taskaddonindex.h index adcac5a447..d1023ab09b 100644 --- a/src/tasks/addonindex/taskaddonindex.h +++ b/src/tasks/addonindex/taskaddonindex.h @@ -17,6 +17,13 @@ class TaskAddonIndex final : public Task { ~TaskAddonIndex(); void run() override; + + private: + void maybeComplete(); + + private: + QByteArray m_indexData; + QByteArray m_indexSignData; }; #endif // TASKADDONINDEX_H diff --git a/tests/qml/mocconstants.cpp b/tests/qml/mocconstants.cpp index 226924a78b..38cbfad994 100644 --- a/tests/qml/mocconstants.cpp +++ b/tests/qml/mocconstants.cpp @@ -21,3 +21,9 @@ QString Constants::versionString() { } QString Constants::buildNumber() { return QStringLiteral("QMLTest_BuildID"); } + +QString Constants::envOrDefault(const QString& name, + const QString& defaultValue) { + Q_UNUSED(name); + return defaultValue; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 054231977c..bd40149567 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -178,6 +178,8 @@ target_sources(unit_tests PRIVATE ${MVPN_SOURCE_DIR}/serveri18n.h ${MVPN_SOURCE_DIR}/settingsholder.cpp ${MVPN_SOURCE_DIR}/settingsholder.h + ${MVPN_SOURCE_DIR}/signature.cpp + ${MVPN_SOURCE_DIR}/signature.h ${MVPN_SOURCE_DIR}/simplenetworkmanager.cpp ${MVPN_SOURCE_DIR}/simplenetworkmanager.h ${MVPN_SOURCE_DIR}/statusicon.cpp diff --git a/tests/unit/mocnetworkrequest.cpp b/tests/unit/mocnetworkrequest.cpp index aa76988e6c..10a255501f 100644 --- a/tests/unit/mocnetworkrequest.cpp +++ b/tests/unit/mocnetworkrequest.cpp @@ -39,8 +39,8 @@ NetworkRequest::~NetworkRequest() { MVPN_COUNT_DTOR(NetworkRequest); } // static QString NetworkRequest::apiBaseUrl() { - return SettingsHolder::instance()->envOrDefault( - "MVPN_API_BASE_URL", Constants::API_PRODUCTION_URL); + return Constants::envOrDefault("MVPN_API_BASE_URL", + Constants::API_PRODUCTION_URL); } // static diff --git a/tests/unit/unit.pro b/tests/unit/unit.pro index 5ce02b9edb..e38748e254 100644 --- a/tests/unit/unit.pro +++ b/tests/unit/unit.pro @@ -37,6 +37,7 @@ include($$PWD/../../version.pri) include($$PWD/../../glean/glean.pri) include($$PWD/../../nebula/nebula.pri) include($$PWD/../../translations/translations.pri) +include($$PWD/../../src/qmake/signature.pri) # Remove resouce files that we intend to mock out RESOURCES ~= 's/.*servers.qrc//g' @@ -114,6 +115,7 @@ HEADERS += \ ../../src/rfc/rfc5735.h \ ../../src/serveri18n.h \ ../../src/settingsholder.h \ + ../../src/signature.h \ ../../src/simplenetworkmanager.h \ ../../src/statusicon.h \ ../../src/subscriptiondata.h \ @@ -229,6 +231,7 @@ SOURCES += \ ../../src/rfc/rfc5735.cpp \ ../../src/serveri18n.cpp \ ../../src/settingsholder.cpp \ + ../../src/signature.cpp \ ../../src/simplenetworkmanager.cpp \ ../../src/statusicon.cpp \ ../../src/subscriptiondata.cpp \