From f30d773251d3d435dbc5d9c2a31f5a93c9a0e1aa Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Wed, 28 Aug 2024 07:17:01 -0400 Subject: [PATCH] Signing of update packages. --- CMakeLists.txt | 5 +- config/updatekey.gpg | 41 ++++++ react/src/Updater.tsx | 6 +- signPackage.sh | 10 ++ src/AdminInstallUpdate.cpp | 45 ++++++- src/CMakeLists.txt | 6 +- src/ConfigMain.cpp | 211 ++++++++++++++++++------------ src/PiPedalModel.cpp | 3 +- src/UpdateMain.cpp | 1 + src/UpdateResults.cpp | 60 +++++++++ src/UpdateResults.hpp | 39 ++++++ src/Updater.cpp | 258 ++++++++++++++++++++++++++++++------- src/Updater.hpp | 8 +- src/UpdaterSecurity.hpp | 61 +++++++++ 14 files changed, 612 insertions(+), 142 deletions(-) create mode 100644 config/updatekey.gpg create mode 100755 signPackage.sh create mode 100644 src/UpdateResults.cpp create mode 100644 src/UpdateResults.hpp create mode 100644 src/UpdaterSecurity.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7323415..b6b204d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,6 @@ set (CMAKE_INSTALL_PREFIX "/usr/") include(CTest) enable_testing() -# Frameworks -# catch - add_subdirectory("submodules/pipedal_p2pd") add_subdirectory("PiPedalCommon") @@ -103,6 +100,8 @@ set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) set (CPACK_STRIP_FILES ON) +set(CPACK_DEBIAN_PACKAGE_SIGN_ALGORITHM "detached") +set(CPACK_DEBIAN_PACKAGE_SIGN_TYPE "origin") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/debian/prerm") diff --git a/config/updatekey.gpg b/config/updatekey.gpg new file mode 100644 index 0000000..30b843f --- /dev/null +++ b/config/updatekey.gpg @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +QOsNQMsqIgaFJgS/9Yc= +=d5rj +-----END PGP PUBLIC KEY BLOCK----- diff --git a/react/src/Updater.tsx b/react/src/Updater.tsx index 7f7563a..993f836 100644 --- a/react/src/Updater.tsx +++ b/react/src/Updater.tsx @@ -44,13 +44,15 @@ export class UpdateRelease { upgradeVersionDisplayName: string = ""; // display name for the version. assetName: string = ""; // filename only updateUrl: string = ""; // url from which to download the .deb file. + gpgSignatureUrl: string = ""; equals(other: UpdateRelease): boolean { return (this.updateAvailable === other.updateAvailable) && (this.upgradeVersion === other.upgradeVersion) && (this.upgradeVersionDisplayName === other.upgradeVersionDisplayName) && (this.assetName === other.assetName) && - (this.updateUrl === other.updateUrl) + (this.updateUrl === other.updateUrl) && + (this.gpgSignatureUrl === other.gpgSignatureUrl) ; } }; @@ -93,7 +95,7 @@ export class UpdateStatus { switch (this.updatePolicy) { case UpdatePolicyT.Disable: // show available updates in the settings dialog. case UpdatePolicyT.ReleaseOnly: - return this.releaseOnlyRelease; + return this.releaseOnlyRelease; case UpdatePolicyT.ReleaseOrBeta: return this.releaseOrBetaRelease; case UpdatePolicyT.Development: diff --git a/signPackage.sh b/signPackage.sh new file mode 100755 index 0000000..04fef7f --- /dev/null +++ b/signPackage.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#sign a package with rerdavies@gmail.com's private key. +cd build +for filename in *.deb; do + echo gpg --armor --output "$filename".asc -b "$filename" + + gpg --armor --output "$filename".asc -b "$filename" +done +cd .. \ No newline at end of file diff --git a/src/AdminInstallUpdate.cpp b/src/AdminInstallUpdate.cpp index 0dae2cf..6c40f4a 100644 --- a/src/AdminInstallUpdate.cpp +++ b/src/AdminInstallUpdate.cpp @@ -29,6 +29,8 @@ #include #include #include +#include "UpdateResults.hpp" +#include "Updater.cpp" using namespace pipedal; namespace fs = std::filesystem; @@ -100,11 +102,28 @@ void updateLog(const std::string &message) write(1,message.c_str(),message.length()); write(1,"\n",1); } + + +static std::string getVersion(const fs::path path) +{ + std::string fileNameOnly = path.filename(); + std::vector segments = split(path,'_'); + if (segments.size() != 3) return "Unknown"; + return SS("v" << segments[1]); +} void pipedal::AdminInstallUpdate(const std::filesystem::path path) { + UpdateResults updateResults; + updateResults.updated_ = true; + + updateResults.updateVersion_ = getVersion(path); try { + // client needs a disconnect/reconnect even if the update doesn't happen. + + exec("/usr/bin/systemctl stop pipedald"); + fs:create_directories(ROOT_INSTALL_DIRECTORY); Lv2Log::info(SS("Installing " << path)); @@ -138,20 +157,36 @@ void pipedal::AdminInstallUpdate(const std::filesystem::path path) dup2(fdNull, 0); close(fdNull); - updateLog("**Stopping pipedald"); + // verify the signature (again), now that we're running as root and could do real damage + // if we install somebody else's package. - exec("/usr/bin/systemctl stop pipedald"); + Updater::ValidateSignature(path, SS(path.string() << ".asc")); // errors are thrown. updateLog("** Installing update"); int retcode = exec(cmd); - + if (retcode != EXIT_SUCCESS) + { + updateResults.updateSuccessful_ = false; + updateResults.updateMessage_ = SS("Update failed. See " << INSTALLER_LOG_FILE_PATH.c_str() << " for further details."); + } else { + updateResults.updateSuccessful_ = true; + updateResults.updateMessage_ = SS("Successfully updated to version " << updateResults.updateVersion_ << "."); + } + // in case we didn't actually update for some reason. - updateLog("** Starting pipedald"); - exec("/usr/bin/systemctl start pipedald"); } catch (const std::exception &e) { updateLog(e.what()); + + updateResults.updated_ = false; + updateResults.updateSuccessful_ = false; + updateResults.updateMessage_ = e.what(); } + updateResults.Save(); + + // restart pipedal to get the client to remove the "Updating..." message. + exec("/usr/bin/systemctl start pipedald"); + } \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3b63da..416e77d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -137,6 +137,8 @@ else() endif() set (PIPEDAL_SOURCES + UpdateResults.cpp UpdateResults.hpp + UpdaterSecurity.hpp Updater.cpp Updater.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp @@ -615,12 +617,14 @@ target_link_libraries(capturepresets ${PIPEDAL_LIBS}) add_executable(pipedal_update UpdateMain.cpp + UpdateResults.cpp UpdateResults.hpp Lv2Log.hpp Lv2Log.cpp Lv2SystemdLogger.cpp Lv2SystemdLogger.hpp + UpdateResults.cpp UpdateResults.hpp AdminInstallUpdate.cpp AdminInstallUpdate.hpp ) -target_link_libraries(pipedal_update PRIVATE stdc++fs systemd ) +target_link_libraries(pipedal_update PRIVATE stdc++fs systemd PiPedalCommon ) add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index a4a489c..4b37300 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -57,7 +57,6 @@ using namespace std; using namespace pipedal; namespace fs = std::filesystem; - #define SERVICE_ACCOUNT_NAME "pipedal_d" #define SERVICE_GROUP_NAME "pipedal_d" #define JACK_SERVICE_ACCOUNT_NAME "jack" @@ -425,9 +424,9 @@ void InstallAudioService() #endif } -int SudoExec(int argc, char**argv) +int SudoExec(int argc, char **argv) { - // re-execute with SUDO in order to prompt for SUDO credentials once only. + // re-execute with SUDO in order to prompt for SUDO credentials once only. std::vector args; std::string pkexec = "/usr/bin/sudo"; // staged because "ISO C++ forbids converting a string constant to std::vector::value_type"(!) args.push_back((char *)(pkexec.c_str())); @@ -471,7 +470,6 @@ void Uninstall() { OnWifiUninstall(true); - StopService(); DisableService(); @@ -584,62 +582,72 @@ static std::string RandomChars(int nchars) return s.str(); } -static std::map sPermissionExceptions -({ - { - "/var/pipedal/config/NetworkManagerP2P.json", - fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write - - } -}); +static std::map sPermissionExceptions({{"/var/pipedal/config/NetworkManagerP2P.json", + fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write +}}); void SetVarPermissions( - const fs::path &path, + const fs::path &path, fs::perms directoryPermissions, fs::perms filePermissions, uid_t uid, gid_t gid) { using namespace std::filesystem; - try { - if (fs::exists(path)) { - std::ignore = chown(path.c_str(),uid,gid); - if (fs::is_directory(path)) { + try + { + if (fs::exists(path)) + { + std::ignore = chown(path.c_str(), uid, gid); + if (fs::is_directory(path)) + { fs::permissions(path, directoryPermissions, fs::perm_options::replace); - for (const auto& entry : fs::recursive_directory_iterator(path)) { + for (const auto &entry : fs::recursive_directory_iterator(path)) + { - if (chown(entry.path().c_str(),uid,gid) != 0) + if (chown(entry.path().c_str(), uid, gid) != 0) { std::cout << "Error: failed to set ownership of file " << entry.path() << std::endl; } - try { + try + { if (fs::is_directory(entry.path())) { fs::permissions(entry.path(), directoryPermissions, fs::perm_options::replace); - } else { - fs::permissions(entry.path(),filePermissions, fs::perm_options::replace); } - } catch (const std::exception &e) - { - std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; + else + { + fs::permissions(entry.path(), filePermissions, fs::perm_options::replace); + } + } + catch (const std::exception &e) + { + std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; } - - } - } else { - if (sPermissionExceptions.contains(path)) - { - fs::permissions(path,sPermissionExceptions[path], fs::perm_options::replace); - } else { - fs::permissions(path,filePermissions, fs::perm_options::replace); } } - } else { - std::cout << "Error: Path does not exist: " << path << std::endl; + else + { + if (sPermissionExceptions.contains(path)) + { + fs::permissions(path, sPermissionExceptions[path], fs::perm_options::replace); + } + else + { + fs::permissions(path, filePermissions, fs::perm_options::replace); + } + } } - } catch (const fs::filesystem_error& e) { + else + { + std::cout << "Error: Path does not exist: " << path << std::endl; + } + } + catch (const fs::filesystem_error &e) + { std::cerr << "Error: " << e.what() << std::endl; - } + } } namespace fs = std::filesystem; @@ -651,17 +659,17 @@ static void FixPermissions() fs::create_directories("/var/pipedal"); - fs::perms directoryPermissions = + fs::perms directoryPermissions = fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec | fs::perms::group_read | fs::perms::group_write | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec | fs::perms::set_gid; - - fs::perms filePermissions = - fs::perms::owner_read | fs::perms::owner_write | - fs::perms::group_read | fs::perms::group_write | - fs::perms::others_read ; - + + fs::perms filePermissions = + fs::perms::owner_read | fs::perms::owner_write | + fs::perms::group_read | fs::perms::group_write | + fs::perms::others_read; + uid_t uid; struct passwd *passwd; if ((passwd = getpwnam("pipedal_d")) == nullptr) @@ -671,25 +679,27 @@ static void FixPermissions() } uid = passwd->pw_uid; gid_t gid = passwd->pw_gid; - SetVarPermissions("/var/pipedal",directoryPermissions,filePermissions,uid,gid); + SetVarPermissions("/var/pipedal", directoryPermissions, filePermissions, uid, gid); - fs::perms wpa_supplicant_perms = + fs::perms wpa_supplicant_perms = fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write; - - fs::path wpa_config_path{ "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"}; - try { - fs::permissions(wpa_config_path,wpa_supplicant_perms, fs::perm_options::replace); - struct group*grp = getgrnam("netdev"); + fs::path wpa_config_path{"/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"}; + try + { + fs::permissions(wpa_config_path, wpa_supplicant_perms, fs::perm_options::replace); + + struct group *grp = getgrnam("netdev"); if (grp != nullptr) { - if (chown(wpa_config_path.c_str(),0,grp->gr_gid) != 0) + if (chown(wpa_config_path.c_str(), 0, grp->gr_gid) != 0) { cout << "Error: Failed to change ownership of " << wpa_config_path << endl; } } - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "Error: Failed to set permissions on " << wpa_config_path << ". " << e.what() << endl; } @@ -721,15 +731,17 @@ static void PrepareServiceConfigurationFile(uint16_t portNumber) void CopyWpaSupplicantConfigFile() { - try { + try + { const char NM_SUPPLICANT_CONFIG_PATH[] = "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"; const char NM_SUPPLICANT_CONFIG_SOURCE_PATH[] = "/etc/pipedal/config/templates/wpa_supplicant-pipedal.conf"; fs::path destinationPath = NM_SUPPLICANT_CONFIG_PATH; fs::path sourcePath = NM_SUPPLICANT_CONFIG_SOURCE_PATH; - + fs::create_directories(destinationPath.parent_path()); - fs::copy_file(sourcePath,destinationPath,fs::copy_options::overwrite_existing); - } catch (const std::exception&e) + fs::copy_file(sourcePath, destinationPath, fs::copy_options::overwrite_existing); + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; } @@ -743,14 +755,48 @@ void DeployVarConfig() auto directory = varConfig.parent_path(); fs::create_directories(directory); fs::path templateFile = "/etc/pipedal/config/templates/var_config.json"; - try { - fs::copy(templateFile,varConfig); - } catch (const std::exception &e) + try + { + fs::copy(templateFile, varConfig); + } + catch (const std::exception &e) { std::cout << "Error: Failed to create " << varConfig << std::endl; } } } + +void InstallPgpKey() +{ + fs::path homeDir = "/var/pipedal/config/gpg"; + fs::path keyPath = "/etc/pipedal/config/updatekey.gpg"; + + fs::create_directories(homeDir); + std::ignore = chmod(homeDir.c_str(), 0700); + + { + std::stringstream s; + s << (CHOWN_BIN " " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); + sysExec(s.str().c_str()); + } + std::ostringstream ss; + ss << "/usr/bin/gpg --homedir " << homeDir.c_str() << " --import " << keyPath.c_str(); + + int rc = sysExec(ss.str().c_str()); + if (rc != EXIT_SUCCESS) + { + cout << "Error: Failed to create update keyring." << endl; + } + + { + std::stringstream ss; + ss << (CHOWN_BIN " -R " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); + std::string cmd = ss.str(); + cout << cmd << endl; + sysExec(cmd.c_str()); + } + cout << "Debug: Finished creating keystring." << endl; +} void Install(const fs::path &programPrefix, const std::string endpointAddress) { cout << "Configuring pipedal" << endl; @@ -954,7 +1000,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) // Restart WiFi Direct if neccessary. OnWifiReinstall(); - + InstallPgpKey(); } catch (const std::exception &e) { @@ -968,7 +1014,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) static std::string GetCurrentWebServicePort() { PiPedalConfiguration config; - config.Load("/etc/pipedal/config",""); + config.Load("/etc/pipedal/config", ""); std::ostringstream ss; ss << config.GetSocketServerPort(); @@ -1066,7 +1112,6 @@ static void PrintHelp() << HangingIndent() << " --list-p2p-channels [] \tList valid p2p channels for the current/specified country." << "\n\n" - // << HangingIndent() << " --enable-legacy-ap\t \tEnable a legacy Wi-Fi access point." // << "\n\n" // << "Enable a legacy Wi-Fi access point. \n\n" @@ -1117,17 +1162,21 @@ static void PrintHelp() "therefore preferrable under almost all circumstances.\n\n"; } -static int ListP2PChannels(const std::vector&arguments) +static int ListP2PChannels(const std::vector &arguments) { - try { + try + { std::string country; - if (arguments.size() >= 2) + if (arguments.size() >= 2) { throw std::runtime_error("Invalid arguments."); - } if (arguments.size() == 1) + } + if (arguments.size() == 1) { country = arguments[0]; - } else { + } + else + { // use the currently selected country by default. WifiDirectConfigSettings settings; settings.Load(); @@ -1137,18 +1186,18 @@ static int ListP2PChannels(const std::vector&arguments) } country = settings.countryCode_; } - auto channelSelectors = getWifiChannelSelectors(country.c_str(),true); - for (const auto &channelSelector: channelSelectors) + auto channelSelectors = getWifiChannelSelectors(country.c_str(), true); + for (const auto &channelSelector : channelSelectors) { cout << channelSelector.channelName_ << endl; } - } catch (const std::exception &e) + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; return EXIT_FAILURE; } return EXIT_SUCCESS; - } int main(int argc, char **argv) @@ -1182,13 +1231,12 @@ int main(int argc, char **argv) parser.AddOption("--help", &help); parser.AddOption("--prefix", &prefixOption); parser.AddOption("--port", &portOption); - //parser.AddOption("--enable-legacy-ap", &enable_ap); + // parser.AddOption("--enable-legacy-ap", &enable_ap); parser.AddOption("--disable-ap", &disable_ap); parser.AddOption("--enable-p2p", &enable_p2p); parser.AddOption("--disable-p2p", &disable_p2p); parser.AddOption("--list-p2p-channels", &list_p2p_channels); parser.AddOption("--fix-permissions", &fix_permissions); - parser.AddOption("--get-current-port", &get_current_port); // private. For debug use only. @@ -1197,11 +1245,8 @@ int main(int argc, char **argv) { parser.Parse(argc, (const char **)argv); - int actionCount = - help + get_current_port + install + uninstall + stop + start + enable + disable - + enable_ap + disable_ap + restart + enable_p2p + disable_p2p - + list_p2p_channels + fix_permissions - ; + int actionCount = + help + get_current_port + install + uninstall + stop + start + enable + disable + enable_ap + disable_ap + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; if (actionCount > 1) { throw std::runtime_error("Please provide only one action."); @@ -1260,7 +1305,7 @@ int main(int argc, char **argv) auto uid = getuid(); if (uid != 0 && !nosudo) { - return SudoExec(argc,argv); + return SudoExec(argc, argv); } try @@ -1330,7 +1375,8 @@ int main(int argc, char **argv) } else if (enable_p2p) { - try { + try + { auto argv = parser.Arguments(); WifiDirectConfigSettings settings; settings.ParseArguments(argv); @@ -1338,7 +1384,8 @@ int main(int argc, char **argv) settings.enable_ = true; SetWifiDirectConfig(settings); RestartService(true); // also have to retart web service so that it gets the correct device name. - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; return EXIT_FAILURE; diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index e5ff1cd..6dc7be6 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2174,7 +2174,8 @@ UpdateStatus PiPedalModel::GetUpdateStatus() void PiPedalModel::UpdateNow(const std::string &updateUrl) { std::lock_guard lock(mutex); - auto fileName = updater.DownloadUpdate(updateUrl); + std::filesystem::path fileName,signatureName; + updater.DownloadUpdate(updateUrl,&fileName,&signatureName); adminClient.InstallUpdate(fileName); } diff --git a/src/UpdateMain.cpp b/src/UpdateMain.cpp index 971e886..c0bb4ab 100644 --- a/src/UpdateMain.cpp +++ b/src/UpdateMain.cpp @@ -35,4 +35,5 @@ int main(int argc, char**argv) } std::filesystem::path path = argv[1]; AdminInstallUpdate(path); + return EXIT_SUCCESS; } \ No newline at end of file diff --git a/src/UpdateResults.cpp b/src/UpdateResults.cpp new file mode 100644 index 0000000..1bc8b6a --- /dev/null +++ b/src/UpdateResults.cpp @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "UpdateResults.hpp" +#include +#include +#include "Lv2Log.hpp" +#include "ss.hpp" + + + +using namespace pipedal; +namespace fs = std::filesystem; + +const fs::path UPDATE_RESULT_PATH{"/var/pipedal/config"}; +void UpdateResults::Load() +{ + std::ifstream f(UPDATE_RESULT_PATH); + if (f.is_open()) + { + json_reader reader(f); + reader.read(this); + } +} +void UpdateResults::Save() +{ + std::ofstream f {UPDATE_RESULT_PATH}; + if (f.is_open()) + { + json_writer writer(f); + writer.write(this); + } else { + Lv2Log::error(SS("Can't write to " << UPDATE_RESULT_PATH)); + } +} + + +JSON_MAP_BEGIN(UpdateResults) +JSON_MAP_REFERENCE(UpdateResults, updated) +JSON_MAP_REFERENCE(UpdateResults, updateSuccessful) +JSON_MAP_REFERENCE(UpdateResults, updateVersion) +JSON_MAP_REFERENCE(UpdateResults, updateMessage) +JSON_MAP_REFERENCE(UpdateResults, installerLog) +JSON_MAP_END() diff --git a/src/UpdateResults.hpp b/src/UpdateResults.hpp new file mode 100644 index 0000000..207f023 --- /dev/null +++ b/src/UpdateResults.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "json.hpp" + +namespace pipedal { + class UpdateResults { + public: + bool updated_ = false; + bool updateSuccessful_ = false; + std::string updateVersion_; + std::string updateMessage_; + std::string installerLog_; + + void Load(); + void Save(); + + DECLARE_JSON_MAP(UpdateResults); + }; + +} \ No newline at end of file diff --git a/src/Updater.cpp b/src/Updater.cpp index c0a32fb..a888930 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -32,8 +32,11 @@ #include "ss.hpp" #include "Lv2Log.hpp" #include +#include "UpdaterSecurity.hpp" +#include "SysExec.hpp" using namespace pipedal; +namespace fs = std::filesystem; #define TEST_UPDATE @@ -45,12 +48,9 @@ static constexpr uint64_t CLOSE_EVENT = 0; static constexpr uint64_t CHECK_NOW_EVENT = 1; static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2; - static std::filesystem::path WORKING_DIRECTORY = "/var/pipedal/updates"; static std::filesystem::path UPDATE_STATUS_CACHE_FILE = WORKING_DIRECTORY / "updateStatus.json"; -static std::string GITHUB_RELEASES_URL = "https://api.github.com/repos/rerdavies/pipedal/releases"; - Updater::clock::duration Updater::updateRate = std::chrono::duration_cast(std::chrono::days(1)); static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast(std::chrono::minutes(30)); @@ -194,7 +194,8 @@ void Updater::ThreadProc() if (value == CHECK_NOW_EVENT) { CheckForUpdate(true); - } else if (value == UNCACHED_CHECK_NOW_EVENT) + } + else if (value == UNCACHED_CHECK_NOW_EVENT) { CheckForUpdate(false); } @@ -221,6 +222,8 @@ public: GithubRelease(json_variant &v); const GithubAsset *GetDownloadForCurrentArchitecture() const; + const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; + bool draft = true; bool prerelease = true; std::string name; @@ -349,8 +352,6 @@ static bool IsCacheValid(const UpdateStatus &updateStatus) return now >= validStart && now < validEnd; } - - UpdateRelease Updater::getUpdateRelease( const std::vector &githubReleases, const std::string ¤tVersion, @@ -361,6 +362,11 @@ UpdateRelease Updater::getUpdateRelease( auto *asset = githubRelease.GetDownloadForCurrentArchitecture(); if (!asset) continue; + auto *pgpKey = githubRelease.GetGpgKeyForAsset(asset->name); + if (!pgpKey) + { + continue; + } if (!predicate(githubRelease)) continue; @@ -370,6 +376,7 @@ UpdateRelease Updater::getUpdateRelease( updateRelease.upgradeVersionDisplayName_ = normalizeReleaseName(githubRelease.name); updateRelease.assetName_ = asset->name; updateRelease.updateUrl_ = asset->browser_download_url; + updateRelease.gpgSignatureUrl_ = pgpKey->browser_download_url; return updateRelease; } return UpdateRelease(); @@ -377,10 +384,10 @@ UpdateRelease Updater::getUpdateRelease( void Updater::CheckForUpdate(bool useCache) { - UpdateStatus updateResult; + UpdateStatus updateResult; { - std::lock_guard lock {mutex}; + std::lock_guard lock{mutex}; if (useCache && IsCacheValid(cachedUpdateStatus)) { this->currentResult = cachedUpdateStatus; @@ -395,7 +402,6 @@ void Updater::CheckForUpdate(bool useCache) } std::string args = SS("-s " << GITHUB_RELEASES_URL); - updateResult.errorMessage_ = ""; try @@ -569,7 +575,6 @@ void UpdateStatus::ResetCurrentVersion() #endif } - std::chrono::system_clock::time_point UpdateStatus::LastUpdateTime() const { std::chrono::system_clock::duration duration{this->lastUpdateTime_}; @@ -582,6 +587,19 @@ void UpdateStatus::LastUpdateTime(const std::chrono::system_clock::time_point &t this->lastUpdateTime_ = timePoint.time_since_epoch().count(); } +const GithubAsset *GithubRelease::GetGpgKeyForAsset(const std::string &name) const +{ + std::string targetName = name + ".asc"; + + for (auto &asset : assets) + { + if (asset.name == targetName) + { + return &asset; + } + } + return nullptr; +} const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const { // deb package names end in {DEBIAN_ARCHITECTURE}.deb @@ -601,14 +619,31 @@ const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const return nullptr; } - UpdateRelease::UpdateRelease() { } +std::string Updater::GetSignatureUrl(const std::string &url) +{ + + // partialy whitelisting, partly avoiding having to parse a URL. + if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOnlyRelease_.GpgSignatureUrl(); + } + if (this->currentResult.releaseOrBetaRelease_.UpdateUrl() == url) + { + return this->currentResult.releaseOrBetaRelease_.GpgSignatureUrl(); + } + if (this->currentResult.devRelease_.UpdateUrl() == url) + { + return this->currentResult.devRelease_.GpgSignatureUrl(); + } + throw std::runtime_error("Permission denied. No signature URL."); +} + std::string Updater::GetUpdateFilename(const std::string &url) { - std::lock_guard lock(mutex); // partialy whitelisting, partly avoiding having to parse a URL. if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url) @@ -624,7 +659,6 @@ std::string Updater::GetUpdateFilename(const std::string &url) return this->currentResult.devRelease_.AssetName(); } throw std::runtime_error("Permission denied. Invalid url."); - } static std::string unCRLF(const std::string &text) { @@ -633,9 +667,11 @@ static std::string unCRLF(const std::string &text) { if (c == '\r') continue; - if (c == '\n') { + if (c == '\n') + { ss << '/'; - } else + } + else { ss << c; } @@ -645,80 +681,207 @@ static std::string unCRLF(const std::string &text) static void removeOldSiblings(int numberToKeep, const std::filesystem::path &fileToKeep) { - namespace fs = std::filesystem; + auto extension = fileToKeep.extension(); auto directory = fileToKeep.parent_path(); - if (directory.empty()) return; // superstition. - struct RemoveEntry { + if (directory.empty()) + return; // superstition. + struct RemoveEntry + { fs::path path; fs::file_time_type time; }; std::vector entries; - for (const auto&dirEntry : fs::directory_iterator(directory)) + for (const auto &dirEntry : fs::directory_iterator(directory)) { if (dirEntry.is_regular_file()) { - if (dirEntry.path() != fileToKeep) + auto thisPath = dirEntry.path(); + if (thisPath != fileToKeep) { - dirEntry.last_write_time(); - entries.push_back(RemoveEntry { .path = dirEntry.path(), .time = dirEntry.last_write_time()}); + if (thisPath.extension() == extension) + { + entries.push_back( + RemoveEntry{ + .path = thisPath, + .time = dirEntry.last_write_time()}); + } } } } std::sort( - entries.begin(),entries.end(), - [](const RemoveEntry&left, const RemoveEntry&right) + entries.begin(), entries.end(), + [](const RemoveEntry &left, const RemoveEntry &right) { return left.time > right.time; // by time descending - } - ); + }); for (size_t i = numberToKeep; i < entries.size(); ++i) { fs::remove(entries[i].path); } } -std::filesystem::path Updater::DownloadUpdate(const std::string &url) + +static std::string getKeyId(const std::string&gpgText) { - namespace fs = std::filesystem; - std::string filename = GetUpdateFilename(url); - if (filename.empty()) + std::string keyPosition = "using RSA key "; + size_t nPos = gpgText.find(keyPosition); + if (nPos = std::string::npos) { - throw std::runtime_error("Permission denied. Invalid url."); - } + return ""; + } + nPos += keyPosition.size(); + + auto start = nPos; + + while (true) + { + char c = gpgText[nPos]; + if (nPos >= gpgText.length() || c == ' ' || c == '\r' && c == '\n' && c == '\t') + { + break; + } + ++nPos; + } + return gpgText.substr(start,nPos-start); +} +static std::string getAddress(const std::string&gpgText) +{ + std::string originPosition = "gpg: Good signature from \""; + + size_t nPos = gpgText.find(originPosition); + if (nPos == std::string::npos) + { + return ""; + } + auto start = nPos; + while (true) + { + if (nPos >= gpgText.length() || gpgText[nPos] == '\"' || gpgText[nPos] == '\r' || gpgText[nPos] == '\n') + { + break; + } + ++nPos; + } + return gpgText.substr(start,nPos-start); + +} +void Updater::ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile) +{ + // sudo gpg --home /var/pipedal/config/gpg --verify pipedal_1.2.41_arm64.deb.asc pipedal_1.2.41_arm64.deb + // gpg: assuming signed data in 'pipedal_1.2.41_arm64.deb' + // gpg: Signature made Tue 27 Aug 2024 09:25:06 PM EDT + // gpg: using RSA key 381124E2BB4478D225D2313B2AEF3F7BD53EAA59 + // gpg: Good signature from "Robin Davies " [ultimate] + std::ostringstream ss; + ss << "-home " << PGP_UPDATE_KEYRING_PATH + << " --verify " + << signatureFile << " " << file; + + auto gpgOutput = sysExecForOutput("/usr/bin/gpg",ss.str()); + if (gpgOutput.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error("Update file is not correctly signed."); + } + const std::string&gpgText = gpgOutput.output; + + std::string keyId = getKeyId(gpgText); + + if (keyId != UPDATE_GPG_FINGERPRINT) + { + throw std::runtime_error("Update signature has the wrong fingerprint."); + } + std::string origin = getAddress(gpgText); + if (origin != UPDATE_GPG_ADDRESS) + { + throw std::runtime_error("Update signature has an incorrect address."); + } +} + +static bool badOutput(const std::filesystem::path &filePath) +{ + if (!fs::is_regular_file(filePath)) + { + return false; + } + return fs::file_size(filePath) != 0; +} +void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) +{ + std::string filename, signatureUrl; + { + std::lock_guard lock{mutex}; + filename = GetUpdateFilename(url); + signatureUrl = GetSignatureUrl(url); + } + + // Only permit downloading of updates from the github releases for the pipedal project. + // I don't think this will get past GetUpdateFileName, but defense in depth. We will + // not download from anywhere we don't trust. + if (!WhitelistDownloadUrl(url)) + { + throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << url)); + } + if (!WhitelistDownloadUrl(signatureUrl)) + { + throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << signatureUrl)); + } auto downloadDirectory = WORKING_DIRECTORY / "downloads"; std::filesystem::create_directories(downloadDirectory); - auto downloadPath = downloadDirectory / filename; + auto downloadFilePath = downloadDirectory / filename; + auto downloadSignaturePath = downloadDirectory / SS(filename << ".asc"); - try { - fs::remove(downloadPath); - std::string args = SS("-s -L " << url << " -o " << downloadPath << " 2>&1"); + try + { + fs::remove(downloadFilePath); + fs::remove(downloadSignaturePath); + + std::string args = SS("-s -L " << url << " -o " << downloadFilePath << " 2>&1"); auto curlOutput = sysExecForOutput("curl", args); - if (curlOutput.exitCode != EXIT_SUCCESS) + if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadFilePath)) { Lv2Log::error(SS("Update download failed." << unCRLF(curlOutput.output))); throw std::runtime_error("PiPedal server does not have access to the internet."); } - if (!fs::exists(downloadPath) || fs::file_size(downloadPath) == 0) + + args = SS("-s -L " << signatureUrl << " -o " << downloadSignaturePath << " 2>&1"); + + curlOutput = sysExecForOutput("curl", args); + if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadSignaturePath)) { - throw std::runtime_error("Download failed."); + Lv2Log::error(SS("Update download failed." << unCRLF(curlOutput.output))); + throw std::runtime_error("PiPedal server does not have access to the internet."); } - try { - removeOldSiblings(2, downloadPath); - } catch (const std::exception&e) + + try + { + removeOldSiblings(2, downloadFilePath); + removeOldSiblings(2, downloadSignaturePath); + } + catch (const std::exception &e) { Lv2Log::error(SS("Can't remove download siblings" << e.what())); // and carry on. } - return downloadPath; - } catch (const std::exception &e) + + // Can't only be performed under pipedal_d or root accouts. + // This is as far as you can go while debugging. + + // The admin process will actually check the signature again running as root; but this is our last + // chance to give a reasonable error message, so do it here as well, because any subsequent errors + // can only go to systemd logs. + ValidateSignature(downloadFilePath,downloadSignaturePath); + *file = downloadFilePath; + *signatureFile = downloadSignaturePath; + return; + } + catch (const std::exception &e) { - std::filesystem::remove(downloadPath); + std::filesystem::remove(downloadFilePath); + std::filesystem::remove(downloadSignaturePath); throw; } - } - /////////////////////////////////////////////////////////////////////////////////////////////// JSON_MAP_BEGIN(UpdateRelease) @@ -727,6 +890,7 @@ JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion) JSON_MAP_REFERENCE(UpdateRelease, upgradeVersionDisplayName) JSON_MAP_REFERENCE(UpdateRelease, assetName) JSON_MAP_REFERENCE(UpdateRelease, updateUrl) +JSON_MAP_REFERENCE(UpdateRelease, gpgSignatureUrl) JSON_MAP_END(); JSON_MAP_BEGIN(UpdateStatus) diff --git a/src/Updater.hpp b/src/Updater.hpp index 3672964..0737bce 100644 --- a/src/Updater.hpp +++ b/src/Updater.hpp @@ -52,6 +52,7 @@ namespace pipedal std::string upgradeVersionDisplayName_; // display name for the version. std::string assetName_; // filename only std::string updateUrl_; // url from which to download the .deb file. + std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. public: UpdateRelease(); @@ -60,6 +61,7 @@ namespace pipedal const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } const std::string &AssetName() const { return assetName_; } const std::string &UpdateUrl() const { return updateUrl_; } + const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} bool operator==(const UpdateRelease &other) const; DECLARE_JSON_MAP(UpdateRelease); }; @@ -108,6 +110,9 @@ namespace pipedal public: Updater(); ~Updater(); + + static void ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile); + using UpdateListener = std::function; void SetUpdateListener(UpdateListener &&listener); @@ -117,9 +122,10 @@ namespace pipedal UpdatePolicyT GetUpdatePolicy(); void SetUpdatePolicy(UpdatePolicyT updatePolicy); void ForceUpdateCheck(); - std::filesystem::path DownloadUpdate(const std::string &url); + void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile); private: std::string GetUpdateFilename(const std::string &url); + std::string GetSignatureUrl(const std::string &url); UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; using UpdateReleasePredicate = std::function; diff --git a/src/UpdaterSecurity.hpp b/src/UpdaterSecurity.hpp new file mode 100644 index 0000000..774ee10 --- /dev/null +++ b/src/UpdaterSecurity.hpp @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2022 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/* + PiPedal uses whitelisting and GPG signatures to verify that updates are valid. + You must modify these defines appropriately if you want to host a fork. + + If you don't, PiPedal will prompt to update your installs with the latest + installs published on github/rerdavies/pipedal. +*/ + +#ifndef GITHUB_PROJECT +#define GITHUB_PROJECT "rerdavies/pipedal" +#endif + + +// public key info for the private key you used to sign .deb files. + +#ifndef UPDATE_GPG_ADDRESS +#define UPDATE_GPG_ADDRESS "Robin Davies " +#endif + +#ifndef UPDATE_GPG_FINGERPRINT +#define UPDATE_GPG_FINGERPRINT "381124E2BB4478D225D2313B2AEF3F7BD53EAA59" +#endif + +// Configuration for downloading of updates. + +#define GITHUB_DOWNLOAD_PREFIX "https://github.com/" GITHUB_PROJECT "/releases/" + +#define GITHUB_RELEASES_URL "https://api.github.com/repos/" GITHUB_PROJECT "/releases" + +#define PGP_UPDATE_KEYRING_PATH "/var/pipedal/config/gpg" + +inline bool WhitelistDownloadUrl(const std::string &downloadUrl) +{ + return downloadUrl.starts_with(GITHUB_DOWNLOAD_PREFIX); +} \ No newline at end of file