diff --git a/3rd-party/vcpkg-ports/grpc/portfile.cmake b/3rd-party/vcpkg-ports/grpc/portfile.cmake index ef1f48b984..874028294e 100644 --- a/3rd-party/vcpkg-ports/grpc/portfile.cmake +++ b/3rd-party/vcpkg-ports/grpc/portfile.cmake @@ -4,9 +4,9 @@ endif() vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH - REPO canonical/grpc - REF e3acf245a91630fe4d464091ba5446f6a638d82f - SHA512 18574197f4a5070de07c39c096ead2175c150a2b790adbb3d9639b0637641015fb91f5cffa916b50863d6ee62203ad2a6964ce87566b6ae7b41716594c445c06 + REPO grpc/grpc + REF v1.52.1 + SHA512 06c69fb817af75b2610761a3a193178b749755eb7bed58875aa251def7c0c253cdaf02cf834c31c8b2cae7b01a6081e2aece4b131a162f64bd45ff0aff4d7758 HEAD_REF master PATCHES 00002-static-linking-in-linux.patch diff --git a/include/multipass/platform.h b/include/multipass/platform.h index 28c06ee8d4..a3e3a0ba95 100644 --- a/include/multipass/platform.h +++ b/include/multipass/platform.h @@ -82,6 +82,7 @@ class Platform : public Singleton [[nodiscard]] virtual std::string bridge_nomenclature() const; virtual int get_cpus() const; virtual long long get_total_ram() const; + [[nodiscard]] virtual std::filesystem::path get_root_cert_path() const; }; QString interpret_setting(const QString& key, const QString& val); diff --git a/include/multipass/ssl_cert_provider.h b/include/multipass/ssl_cert_provider.h index a038bd59ca..95f726bf6e 100644 --- a/include/multipass/ssl_cert_provider.h +++ b/include/multipass/ssl_cert_provider.h @@ -34,8 +34,9 @@ class SSLCertProvider : public CertProvider std::string pem_priv_key; }; - explicit SSLCertProvider(const Path& data_dir); - SSLCertProvider(const Path& data_dir, const std::string& server_name); + explicit SSLCertProvider(const Path& data_dir, const std::string& server_name = ""); + // leave server_name empty for clients; choose a (non-empty) name for servers. + std::string PEM_certificate() const override; std::string PEM_signing_key() const override; diff --git a/include/multipass/utils.h b/include/multipass/utils.h index cfd6141dad..73f9936a8a 100644 --- a/include/multipass/utils.h +++ b/include/multipass/utils.h @@ -203,14 +203,14 @@ class Utils : public Singleton Utils(const Singleton::PrivatePass&) noexcept; virtual qint64 filesystem_bytes_available(const QString& data_directory) const; - virtual void exit(int code); + virtual void exit(int code) const; virtual std::string contents_of(const multipass::Path& file_path) const; virtual void make_file_with_content(const std::string& file_name, const std::string& content, const bool& overwrite = false); virtual Path make_dir(const QDir& a_dir, const QString& name, - std::filesystem::perms permissions = std::filesystem::perms::none); - virtual Path make_dir(const QDir& dir, std::filesystem::perms permissions = std::filesystem::perms::none); + std::filesystem::perms permissions = std::filesystem::perms::none) const; + virtual Path make_dir(const QDir& dir, std::filesystem::perms permissions = std::filesystem::perms::none) const; // command and process helpers virtual std::string run_cmd_for_output(const QString& cmd, const QStringList& args, diff --git a/snap/hooks/install b/snap/hooks/install index 3e88f83067..86dd849c46 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -2,6 +2,8 @@ rm -f $SNAP_COMMON/snap_refresh +mkdir -p "$SNAP_COMMON/data" + # GDK pixbuf setup export LD_LIBRARY_PATH=/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void:${SNAP}/graphics/lib:${SNAP}/lib:${SNAP}/usr/lib:${SNAP}/lib/${ARCH_TRIPLET}:${SNAP}/usr/lib/${ARCH_TRIPLET} $SNAP/usr/lib/${ARCH_TRIPLET}/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders > "$GDK_PIXBUF_MODULE_FILE" diff --git a/src/cert/ssl_cert_provider.cpp b/src/cert/ssl_cert_provider.cpp index 7699ccd437..210da72b0b 100644 --- a/src/cert/ssl_cert_provider.cpp +++ b/src/cert/ssl_cert_provider.cpp @@ -16,19 +16,17 @@ */ #include +#include #include #include #include "biomem.h" +#include #include #include -#include #include -#include - -#include #include #include #include @@ -38,13 +36,33 @@ namespace mp = multipass; namespace { +// utility function for checking return code or raw pointer from openssl C-apis +// TODO: constrain T to int or raw pointer once C++20 concepts is available +template +void openssl_check(T result, const std::string& errorMessage) +{ + // TODO: expand std::is_pointer_v check to cover all smart pointers as well + if constexpr (std::is_pointer_v) + { + if (result == nullptr) + { + throw std::runtime_error(errorMessage); + } + } + else + { + if (result <= 0) + { + throw std::runtime_error(fmt::format("{}, with the error code {}", errorMessage, result)); + } + } +} + class WritableFile { public: - explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose} + explicit WritableFile(const QString& file_path) : fp{open_file(file_path)} { - if (fp == nullptr) - throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", name, strerror(errno), errno)); } FILE* get() const @@ -53,48 +71,54 @@ class WritableFile } private: - std::unique_ptr> fp; + // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and + // compilation error + using FilePtr = std::unique_ptr; + [[nodiscard]] static FilePtr open_file(const QString& file_path) + { + const std::filesystem::path file_path_std{file_path.toStdString()}; + std::filesystem::create_directories(file_path_std.parent_path()); + // make sure the parent directory exist + + const auto raw_fp = fopen(file_path_std.u8string().c_str(), "wb"); + openssl_check(raw_fp, fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno)); + + return FilePtr{raw_fp, fclose}; + } + + FilePtr fp; }; class EVPKey { public: - EVPKey() + EVPKey() : key{create_key()} { - if (key == nullptr) - throw std::runtime_error("Failed to allocate EVP_PKEY"); - - std::unique_ptr ec_key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), - EC_KEY_free); - if (ec_key == nullptr) - throw std::runtime_error("Failed to allocate ec key structure"); - - if (!EC_KEY_generate_key(ec_key.get())) - throw std::runtime_error("Failed to generate key"); - - if (!EVP_PKEY_assign_EC_KEY(key.get(), ec_key.get())) - throw std::runtime_error("Failed to assign key"); - - // EVPKey has ownership now - ec_key.release(); } std::string as_pem() const { mp::BIOMem mem; - auto bytes = PEM_write_bio_PrivateKey(mem.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr); - if (bytes == 0) - throw std::runtime_error("Failed to export certificate in PEM format"); + openssl_check(PEM_write_bio_PrivateKey(mem.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr), + "Failed to export certificate in PEM format"); return mem.as_string(); } - void write(const QString& name) + void write(const QString& key_path) const { - WritableFile file{name}; - if (!PEM_write_PrivateKey(file.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr)) - throw std::runtime_error(fmt::format("Failed writing certificate private key to file '{}'", name)); - - QFile::setPermissions(name, QFile::ReadOwner); + const std::filesystem::path key_path_std = key_path.toStdU16String(); + if (std::filesystem::exists(key_path_std)) + { + // enable fopen in WritableFile with wb mode + MP_PLATFORM.set_permissions(key_path_std, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); + } + + WritableFile file{key_path}; + openssl_check(PEM_write_PrivateKey(file.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr), + fmt::format("Failed writing certificate private key to file '{}'", key_path)); + + MP_PLATFORM.set_permissions(key_path_std, std::filesystem::perms::owner_read); } EVP_PKEY* get() const @@ -103,7 +127,33 @@ class EVPKey } private: - std::unique_ptr key{EVP_PKEY_new(), EVP_PKEY_free}; + using EVPKeyPtr = std::unique_ptr; + + [[nodiscard]] static EVPKeyPtr create_key() + { + std::unique_ptr ctx( + EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr), + EVP_PKEY_CTX_free); + + openssl_check(ctx.get(), "Failed to create EVP_PKEY_CTX"); + openssl_check(EVP_PKEY_keygen_init(ctx.get()), "Failed to initialize key generation"); + + // Set EC curve (P-256) + const std::array params = { + // the 3rd argument is length of the buffer, which is 0 in the case of static buffer like "P-256" + OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, const_cast("P-256"), 0), + OSSL_PARAM_construct_end()}; + + openssl_check(EVP_PKEY_CTX_set_params(ctx.get(), params.data()), "EVP_PKEY_CTX_set_params() failed"); + + // Generate the key + EVP_PKEY* raw_key = nullptr; + openssl_check(EVP_PKEY_generate(ctx.get(), &raw_key), "Failed to generate EC key"); + + return EVPKeyPtr(raw_key, EVP_PKEY_free); + } + + EVPKeyPtr key; }; std::vector as_vector(const std::string& v) @@ -111,109 +161,196 @@ std::vector as_vector(const std::string& v) return {v.begin(), v.end()}; } -std::string cn_name_from(const std::string& server_name) -{ - if (server_name.empty()) - return mp::utils::make_uuid().toStdString(); - return server_name; -} - -void set_san_name(X509* c, const std::string& server_name) +void set_random_serial_number(X509* cert) { - std::string san_dns = server_name; - GENERAL_NAMES* gens = sk_GENERAL_NAME_new_null(); - GENERAL_NAME* gen = GENERAL_NAME_new(); - ASN1_IA5STRING* ia5 = ASN1_IA5STRING_new(); - ASN1_STRING_set(ia5, san_dns.data(), san_dns.length()); - GENERAL_NAME_set0_value(gen, GEN_DNS, ia5); - sk_GENERAL_NAME_push(gens, gen); - - X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT); - - GENERAL_NAMES_free(gens); + // OpenSSL recommends a 20-byte (160-bit) serial number + std::array serial_bytes{}; + openssl_check(RAND_bytes(serial_bytes.data(), serial_bytes.size()), "Failed to set random bytes\n"); + // Set the highest bit to 0 (unsigned) to ensure it's positive + serial_bytes[0] &= 0x7F; + + // Convert bytes to an BIGNUM, an arbitrary-precision integer type + std::unique_ptr bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr), + BN_free); + openssl_check(bn.get(), "Failed to convert serial bytes to BIGNUM\n"); + assert(!BN_is_negative(bn.get())); + + // Convert BIGNUM to ASN1_INTEGER and set it as the certificate serial number + // ASN1 is a standard binary format for encoding data like serial numbers in X.509 certificates + ASN1_INTEGER* serial = BN_to_ASN1_INTEGER(bn.get(), nullptr); + openssl_check(serial, "Failed to convert serial bytes to BIGNUM\n"); + + // Set the serial number in the certificate + openssl_check(X509_set_serialNumber(cert, serial), "Failed to set serial number!\n"); } class X509Cert { public: - explicit X509Cert(const EVPKey& key, const std::string& server_name) + enum class CertType + { + Root, + Client, + Server + }; + + explicit X509Cert(const EVPKey& key, + CertType cert_type, + const std::string& server_name = "", + const std::optional& root_certificate_key = std::nullopt, + const std::optional& root_certificate = std::nullopt) + // generate root, client or signed server certificate, the third one requires the last three arguments populated { - if (x509 == nullptr) - throw std::runtime_error("Failed to allocate x509 cert structure"); + openssl_check(cert.get(), "Failed to allocate x509 cert structure"); - long big_num{0}; - auto rand_bytes = MP_UTILS.random_bytes(4); - for (unsigned int i = 0; i < 4u; i++) - big_num |= rand_bytes[i] << i * 8u; + X509_set_version(cert.get(), 2); // 0 index based, 2 amounts to 3 - X509_set_version(x509.get(), 2); + set_random_serial_number(cert.get()); + X509_gmtime_adj(X509_get_notBefore(cert.get()), 0); // Start time: now - ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), big_num); - X509_gmtime_adj(X509_get_notBefore(x509.get()), 0); - X509_gmtime_adj(X509_get_notAfter(x509.get()), 31536000L); + constexpr std::chrono::seconds one_year = std::chrono::hours{24} * 365; + constexpr std::chrono::seconds ten_years = one_year * 10; + const auto valid_duration = cert_type == CertType::Root ? ten_years : one_year; + X509_gmtime_adj(X509_get_notAfter(cert.get()), valid_duration.count()); constexpr int APPEND_ENTRY{-1}; constexpr int ADD_RDN{0}; - auto country = as_vector("US"); - auto org = as_vector("Canonical"); - auto cn = as_vector(cn_name_from(server_name)); - - auto name = X509_get_subject_name(x509.get()); - X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, country.data(), country.size(), APPEND_ENTRY, ADD_RDN); - X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, org.data(), org.size(), APPEND_ENTRY, ADD_RDN); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, cn.data(), cn.size(), APPEND_ENTRY, ADD_RDN); - X509_set_issuer_name(x509.get(), name); - set_san_name(x509.get(), server_name); - - if (!X509_set_pubkey(x509.get(), key.get())) - throw std::runtime_error("Failed to set certificate public key"); - - if (!X509_sign(x509.get(), key.get(), EVP_sha256())) - throw std::runtime_error("Failed to sign certificate"); + const auto country = as_vector("US"); + const auto org = as_vector("Canonical"); + const auto cn = as_vector(cert_type == CertType::Root ? "Multipass Root CA" + : cert_type == CertType::Client ? mp::utils::make_uuid().toStdString() + : server_name); + const auto subject_name = X509_get_subject_name(cert.get()); + X509_NAME_add_entry_by_txt(subject_name, + "C", + MBSTRING_ASC, + country.data(), + country.size(), + APPEND_ENTRY, + ADD_RDN); + X509_NAME_add_entry_by_txt(subject_name, "O", MBSTRING_ASC, org.data(), org.size(), APPEND_ENTRY, ADD_RDN); + X509_NAME_add_entry_by_txt(subject_name, "CN", MBSTRING_ASC, cn.data(), cn.size(), APPEND_ENTRY, ADD_RDN); + + const auto issuer_name = + cert_type == CertType::Server ? X509_get_subject_name(root_certificate.value().cert.get()) : subject_name; + X509_set_issuer_name(cert.get(), issuer_name); + + openssl_check(X509_set_pubkey(cert.get(), key.get()), "Failed to set certificate public key"); + + const auto& issuer_cert = cert_type == CertType::Server ? root_certificate.value().cert : cert; + // Add X509v3 extensions + X509V3_CTX ctx{}; + X509V3_set_ctx(&ctx, issuer_cert.get(), cert.get(), nullptr, nullptr, 0); + + // Subject Alternative Name + if (cert_type == CertType::Server) + { + add_extension(ctx, NID_subject_alt_name, ("DNS:" + server_name).c_str()); + } + + // Subject Key Identifier + add_extension(ctx, NID_subject_key_identifier, "hash"); + + // Authority Key Identifier + add_extension(ctx, + NID_authority_key_identifier, + (std::string("keyid:always") + (cert_type == CertType::Server ? ",issuer" : "")).c_str()); + + // Basic Constraints: critical, CA:TRUE or CA:FALSE + add_extension(ctx, + NID_basic_constraints, + (std::string("critical,CA:") + (cert_type == CertType::Root ? "TRUE" : "FALSE")).c_str()); + + const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key; + + openssl_check(X509_sign(cert.get(), signing_key.get(), EVP_sha256()), "Failed to sign certificate"); } - std::string as_pem() + std::string as_pem() const { mp::BIOMem mem; - auto bytes = PEM_write_bio_X509(mem.get(), x509.get()); - if (bytes == 0) - throw std::runtime_error("Failed to write certificate in PEM format"); + openssl_check(PEM_write_bio_X509(mem.get(), cert.get()), "Failed to write certificate in PEM format"); return mem.as_string(); } - void write(const QString& name) + void write(const QString& cert_path) const { - WritableFile file{name}; - if (!PEM_write_X509(file.get(), x509.get())) - throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", name)); + WritableFile file{cert_path}; + + openssl_check(PEM_write_X509(file.get(), cert.get()), + fmt::format("Failed writing certificate to file '{}'", cert_path)); + const std::filesystem::path cert_path_std = cert_path.toStdU16String(); + MP_PLATFORM.set_permissions(cert_path_std, + std::filesystem::perms::owner_all | std::filesystem::perms::group_read | + std::filesystem::perms::others_read); } private: - std::unique_ptr x509{X509_new(), X509_free}; + void add_extension(X509V3_CTX& ctx, int nid, const char* value) + { + const std::unique_ptr ext( + X509V3_EXT_conf_nid(nullptr, &ctx, nid, value), + X509_EXTENSION_free); + + openssl_check(ext.get(), "Failed to create X509 extension"); + openssl_check(X509_add_ext(cert.get(), ext.get(), -1), "Failed to add X509 extension"); + } + + std::unique_ptr cert{X509_new(), X509_free}; }; mp::SSLCertProvider::KeyCertificatePair make_cert_key_pair(const QDir& cert_dir, const std::string& server_name) { - QString prefix = server_name.empty() ? "multipass_cert" : QString::fromStdString(server_name); + const QString prefix = server_name.empty() ? "multipass_cert" : QString::fromStdString(server_name); - auto priv_key_path = cert_dir.filePath(prefix + "_key.pem"); - auto cert_path = cert_dir.filePath(prefix + ".pem"); + const auto priv_key_path = cert_dir.filePath(prefix + "_key.pem"); + const auto cert_path = cert_dir.filePath(prefix + ".pem"); - if (QFile::exists(priv_key_path) && QFile::exists(cert_path)) + if (!server_name.empty()) { - return {mp::utils::contents_of(cert_path), mp::utils::contents_of(priv_key_path)}; + const std::filesystem::path root_cert_path = MP_PLATFORM.get_root_cert_path(); + if (std::filesystem::exists(root_cert_path) && QFile::exists(priv_key_path) && QFile::exists(cert_path)) + { + // Unlike other daemon files, the root certificate needs to be accessible by everyone + MP_PLATFORM.set_permissions(root_cert_path, + std::filesystem::perms::owner_all | std::filesystem::perms::group_read | + std::filesystem::perms::others_read); + return {mp::utils::contents_of(cert_path), mp::utils::contents_of(priv_key_path)}; + } + + const auto priv_root_key_path = cert_dir.filePath(prefix + "_root_key.pem"); + + EVPKey root_cert_key{}; + X509Cert root_cert{root_cert_key, X509Cert::CertType::Root}; + root_cert_key.write(priv_root_key_path); + root_cert.write(root_cert_path.u8string().c_str()); + + const EVPKey server_cert_key{}; + const X509Cert signed_server_cert{server_cert_key, + X509Cert::CertType::Server, + server_name, + std::move(root_cert_key), + std::move(root_cert)}; + server_cert_key.write(priv_key_path); + signed_server_cert.write(cert_path); + return {signed_server_cert.as_pem(), server_cert_key.as_pem()}; } + else + { + if (QFile::exists(priv_key_path) && QFile::exists(cert_path)) + { + return {mp::utils::contents_of(cert_path), mp::utils::contents_of(priv_key_path)}; + } - EVPKey key; - X509Cert cert{key, server_name}; - - key.write(priv_key_path); - cert.write(cert_path); + const EVPKey client_cert_key{}; + const X509Cert client_cert{client_cert_key, X509Cert::CertType::Client}; + client_cert_key.write(priv_key_path); + client_cert.write(cert_path); - return {cert.as_pem(), key.as_pem()}; + return {client_cert.as_pem(), client_cert_key.as_pem()}; + } } - } // namespace mp::SSLCertProvider::SSLCertProvider(const multipass::Path& cert_dir, const std::string& server_name) @@ -221,10 +358,6 @@ mp::SSLCertProvider::SSLCertProvider(const multipass::Path& cert_dir, const std: { } -mp::SSLCertProvider::SSLCertProvider(const multipass::Path& data_dir) : SSLCertProvider(data_dir, "") -{ -} - std::string mp::SSLCertProvider::PEM_certificate() const { return key_cert_pair.pem_cert; diff --git a/src/client/common/client_common.cpp b/src/client/common/client_common.cpp index 2472e39528..4128dfc569 100644 --- a/src/client/common/client_common.cpp +++ b/src/client/common/client_common.cpp @@ -74,19 +74,12 @@ grpc::SslCredentialsOptions get_ssl_credentials_opts_from(const mp::CertProvider { auto opts = grpc::SslCredentialsOptions(); - opts.server_certificate_request = GRPC_SSL_REQUEST_SERVER_CERTIFICATE_BUT_DONT_VERIFY; + opts.pem_root_certs = MP_UTILS.contents_of(MP_PLATFORM.get_root_cert_path().u8string().c_str()); opts.pem_cert_chain = cert_provider.PEM_certificate(); opts.pem_private_key = cert_provider.PEM_signing_key(); return opts; } - -bool client_certs_exist(const QString& cert_dir_path) -{ - QDir cert_dir{cert_dir_path}; - - return cert_dir.exists(mp::client_cert_file) && cert_dir.exists(mp::client_key_file); -} } // namespace mp::ReturnCode mp::cmd::standard_failure_handler_for(const std::string& command, std::ostream& cerr, @@ -157,11 +150,6 @@ std::unique_ptr mp::client::get_cert_provider() auto data_location{MP_STDPATHS.writableLocation(StandardPaths::GenericDataLocation)}; auto common_client_cert_dir_path{data_location + common_client_cert_dir}; - if (!client_certs_exist(common_client_cert_dir_path)) - { - MP_UTILS.make_dir(common_client_cert_dir_path); - } - return std::make_unique(common_client_cert_dir_path); } diff --git a/src/daemon/daemon_config.cpp b/src/daemon/daemon_config.cpp index 93679577fe..79e06f0b7d 100644 --- a/src/daemon/daemon_config.cpp +++ b/src/daemon/daemon_config.cpp @@ -109,7 +109,7 @@ std::unique_ptr mp::DaemonConfigBuilder::build() auto multiplexing_logger = std::make_shared(std::move(logger)); mpl::set_logger(multiplexing_logger); - MP_PLATFORM.setup_permission_inheritance(); + MP_PLATFORM.setup_permission_inheritance(true); auto storage_path = MP_PLATFORM.multipass_storage_location(); if (!storage_path.isEmpty()) @@ -169,9 +169,6 @@ std::unique_ptr mp::DaemonConfigBuilder::build() server_address = platform::default_server_address(); if (ssh_key_provider == nullptr) ssh_key_provider = std::make_unique(data_directory); - if (cert_provider == nullptr) - cert_provider = std::make_unique(MP_UTILS.make_dir(data_directory, "certificates"), - server_name_from(server_address)); if (client_cert_store == nullptr) client_cert_store = std::make_unique(data_directory); if (ssh_username.empty()) @@ -192,17 +189,28 @@ std::unique_ptr mp::DaemonConfigBuilder::build() std::make_unique(url_downloader.get(), cache_directory, manifest_ttl); } - // restrict permissions for all existing files and folders + // tighten permissions for cache and data if (!storage_path.isEmpty()) { MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String()); + MP_PLATFORM.set_permissions(storage_path.toStdU16String(), + fs::perms::owner_all | fs::perms::group_exec | fs::perms::others_exec); } else { MP_PERMISSIONS.restrict_permissions(data_directory.toStdU16String()); + MP_PLATFORM.set_permissions(data_directory.toStdU16String(), + fs::perms::owner_all | fs::perms::group_exec | fs::perms::others_exec); MP_PERMISSIONS.restrict_permissions(cache_directory.toStdU16String()); } + if (cert_provider == nullptr) + cert_provider = std::make_unique( + MP_UTILS.make_dir(data_directory, + "certificates", + fs::perms::owner_all | fs::perms::group_exec | fs::perms::others_exec), + server_name_from(server_address)); + return std::unique_ptr(new DaemonConfig{ std::move(url_downloader), std::move(factory), std::move(image_hosts), std::move(vault), std::move(name_generator), std::move(ssh_key_provider), std::move(cert_provider), std::move(client_cert_store), diff --git a/src/platform/platform_linux.cpp b/src/platform/platform_linux.cpp index 6b1245ffc2..c5a0e46da8 100644 --- a/src/platform/platform_linux.cpp +++ b/src/platform/platform_linux.cpp @@ -179,6 +179,14 @@ std::string get_alias_script_path(const std::string& alias) return aliases_folder.absoluteFilePath(QString::fromStdString(alias)).toStdString(); } + +std::filesystem::path multipass_final_storage_location() +{ + const auto user_specified_mp_storage = MP_PLATFORM.multipass_storage_location(); + const auto mp_final_storage = + user_specified_mp_storage.isEmpty() ? mp::utils::snap_common_dir() : user_specified_mp_storage; + return std::filesystem::path{mp_final_storage.toStdString()}; +} } // namespace std::unique_ptr multipass::platform::detail::find_os_release() @@ -470,3 +478,11 @@ std::string multipass::platform::host_version() return mpu::in_multipass_snap() ? multipass::platform::detail::read_os_release() : fmt::format("{}-{}", QSysInfo::productType(), QSysInfo::productVersion()); } + +std::filesystem::path mp::platform::Platform::get_root_cert_path() const +{ + constexpr auto* root_cert_file_name = "multipass_root_cert.pem"; + return mp::utils::in_multipass_snap() + ? multipass_final_storage_location() / "data" / daemon_name / "certificates" / root_cert_file_name + : std::filesystem::path{"/usr/local/share/ca-certificates"} / root_cert_file_name; +} diff --git a/src/platform/platform_unix.cpp b/src/platform/platform_unix.cpp index dfb628154e..7ed79b1351 100644 --- a/src/platform/platform_unix.cpp +++ b/src/platform/platform_unix.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index eaa3da921a..877b6ece06 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -67,7 +67,7 @@ qint64 mp::Utils::filesystem_bytes_available(const QString& data_directory) cons return QStorageInfo(QDir(data_directory)).bytesAvailable(); } -void mp::Utils::exit(int code) +void mp::Utils::exit(int code) const { std::exit(code); } @@ -275,7 +275,7 @@ std::string mp::Utils::run_in_ssh_session(mp::SSHSession& session, const std::st return mp::utils::trim_end(output); } -mp::Path mp::Utils::make_dir(const QDir& a_dir, const QString& name, std::filesystem::perms permissions) +mp::Path mp::Utils::make_dir(const QDir& a_dir, const QString& name, std::filesystem::perms permissions) const { mp::Path dir_path; bool success{false}; @@ -304,7 +304,7 @@ mp::Path mp::Utils::make_dir(const QDir& a_dir, const QString& name, std::filesy return dir_path; } -mp::Path mp::Utils::make_dir(const QDir& dir, std::filesystem::perms permissions) +mp::Path mp::Utils::make_dir(const QDir& dir, std::filesystem::perms permissions) const { return make_dir(dir, QString(), permissions); } diff --git a/tests/linux/test_platform_linux.cpp b/tests/linux/test_platform_linux.cpp index 2c61102ba3..137140629a 100644 --- a/tests/linux/test_platform_linux.cpp +++ b/tests/linux/test_platform_linux.cpp @@ -707,4 +707,12 @@ TEST_F(PlatformLinux, remove_alias_script_throws_if_cannot_remove_script) MP_EXPECT_THROW_THAT(MP_PLATFORM.remove_alias_script("alias_name"), std::runtime_error, mpt::match_what(StrEq("No such file or directory"))); } + +TEST_F(PlatformLinux, test_snap_multipass_storage_location) +{ + mpt::SetEnvScope env{"SNAP_NAME", "multipass"}; + mpt::SetEnvScope env2("SNAP_COMMON", "common"); + + EXPECT_EQ(MP_PLATFORM.get_root_cert_path(), "data/multipassd/certificates/multipass_root_cert.pem"); +} } // namespace diff --git a/tests/mock_cert_provider.h b/tests/mock_cert_provider.h index 40d88a62e6..16d4025ee1 100644 --- a/tests/mock_cert_provider.h +++ b/tests/mock_cert_provider.h @@ -24,46 +24,45 @@ using namespace testing; namespace multipass::test { -constexpr auto client_cert = "-----BEGIN CERTIFICATE-----\n" - "MIIBUjCB+AIBKjAKBggqhkjOPQQDAjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJ\n" - "Q2Fub25pY2FsMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwNjIxMTM0MjI5WhcN\n" - "MTkwNjIxMTM0MjI5WjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJQ2Fub25pY2Fs\n" - "MRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQA\n" - "FGNAqq7c5IMDeQ/cV4+EmogmkfpbTLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqa\n" - "J7sAPJJwlVj1xD0r5DX5MAoGCCqGSM49BAMCA0kAMEYCIQCvI0PYv9f201fbe4LP\n" - "BowTeYWSqMQtLNjvZgd++AAGhgIhALNPW+NRSKCXwadiIFgpbjPInLPqXPskLWSc\n" - "aXByaQyt\n" - "-----END CERTIFICATE-----\n"; +constexpr auto root_cert = "-----BEGIN CERTIFICATE-----\n" + "MIIBvjCCAWWgAwIBAgIEUlzMbjAKBggqhkjOPQQDAjA9MQswCQYDVQQGEwJVUzES\n" + "MBAGA1UECgwJQ2Fub25pY2FsMRowGAYDVQQDDBFNdWx0aXBhc3MgUm9vdCBDQTAe\n" + "Fw0yNTAxMjkxMzAzNDBaFw0zNTAxMjcxMzAzNDBaMD0xCzAJBgNVBAYTAlVTMRIw\n" + "EAYDVQQKDAlDYW5vbmljYWwxGjAYBgNVBAMMEU11bHRpcGFzcyBSb290IENBMFkw\n" + "EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExnNTmDW0EMAg/ADMNJIc5V4BP/si6MlA\n" + "+YSHDTy+nbgXJ9q0/ORKZETjydqIkVRLzu1LR5sWpdbSzSPo3ft/9KNTMFEwHQYD\n" + "VR0OBBYEFAYlQy+QMhSOh/gACsgSdWbIH5NoMB8GA1UdIwQYMBaAFAYlQy+QMhSO\n" + "h/gACsgSdWbIH5NoMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIg\n" + "ErA3KYoWLTkQ9J6cu/bSS539veIBO7p0xvb2x0u2UA0CIEiJ0mMdATr/I8tovsZm\n" + "xgvZMY2ColjLunUiNG8H096n\n" + "-----END CERTIFICATE-----\n"; -constexpr auto client_key = "-----BEGIN PRIVATE KEY-----\n" - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsSAz5ggzrLjai0I/\n" - "F0hYg5oG/shpXJiBQtJdBCG3lUShRANCAAQAFGNAqq7c5IMDeQ/cV4+Emogmkfpb\n" - "TLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqaJ7sAPJJwlVj1xD0r5DX5\n" - "-----END PRIVATE KEY-----\n"; +// cert and key are used as both server certificate and client certificate in the unit test environment +constexpr auto cert = "-----BEGIN CERTIFICATE-----\n" + "MIIByjCCAXCgAwIBAgIENvdePTAKBggqhkjOPQQDAjA9MQswCQYDVQQGEwJVUzES\n" + "MBAGA1UECgwJQ2Fub25pY2FsMRowGAYDVQQDDBFNdWx0aXBhc3MgUm9vdCBDQTAe\n" + "Fw0yNTAxMjkxMzAzNDBaFw0yNjAxMjkxMzAzNDBaMDUxCzAJBgNVBAYTAlVTMRIw\n" + "EAYDVQQKDAlDYW5vbmljYWwxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49\n" + "AgEGCCqGSM49AwEHA0IABGAw4mRhGqCg7uSIsVgBIzMOoGnlEFWga2dxUzA1YwNe\n" + "8SB679smyb7KVsPg4fK/P7XS4ORxSnMVnKWvTAfYKXWjZjBkMBQGA1UdEQQNMAuC\n" + "CWxvY2FsaG9zdDAdBgNVHQ4EFgQU++FdgRpFokGT+7Fdgqe4SxmSD9UwHwYDVR0j\n" + "BBgwFoAUBiVDL5AyFI6H+AAKyBJ1Zsgfk2gwDAYDVR0TAQH/BAIwADAKBggqhkjO\n" + "PQQDAgNIADBFAiAesF7z8ItZVxK6fgUwhWfgN5rUFzCO5tBGJFDHU7eIZgIhALdl\n" + "2mAn2oocQZfHohrbVUIuWDiUr0SxOkdGUISX0ElJ\n" + "-----END CERTIFICATE-----\n"; -constexpr auto daemon_cert = "-----BEGIN CERTIFICATE-----\n" - "MIIBUjCB+AIBKjAKBggqhkjOPQQDAjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJ\n" - "Q2Fub25pY2FsMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwNjIxMTM0MjI5WhcN\n" - "MTkwNjIxMTM0MjI5WjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJQ2Fub25pY2Fs\n" - "MRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQA\n" - "FGNAqq7c5IMDeQ/cV4+EmogmkfpbTLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqa\n" - "J7sAPJJwlVj1xD0r5DX5MAoGCCqGSM49BAMCA0kAMEYCIQCvI0PYv9f201fbe4LP\n" - "BowTeYWSqMQtLNjvZgd++AAGhgIhALNPW+NRSKCXwadiIFgpbjPInLPqXPskLWSc\n" - "aXByaQyt\n" - "-----END CERTIFICATE-----\n"; - -constexpr auto daemon_key = "-----BEGIN PRIVATE KEY-----\n" - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsSAz5ggzrLjai0I/\n" - "F0hYg5oG/shpXJiBQtJdBCG3lUShRANCAAQAFGNAqq7c5IMDeQ/cV4+Emogmkfpb\n" - "TLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqaJ7sAPJJwlVj1xD0r5DX5\n" - "-----END PRIVATE KEY-----\n"; +constexpr auto key = "-----BEGIN PRIVATE KEY-----\n" + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwRNA3VMqakM32i0C\n" + "PHE5i4qRNGdvgXtCWwpp0gXv+oGhRANCAARgMOJkYRqgoO7kiLFYASMzDqBp5RBV\n" + "oGtncVMwNWMDXvEgeu/bJsm+ylbD4OHyvz+10uDkcUpzFZylr0wH2Cl1\n" + "-----END PRIVATE KEY-----\n"; struct MockCertProvider : public CertProvider { MockCertProvider() { - ON_CALL(*this, PEM_certificate).WillByDefault(Return(client_cert)); - ON_CALL(*this, PEM_signing_key).WillByDefault(Return(client_key)); + ON_CALL(*this, PEM_certificate).WillByDefault(Return(cert)); + ON_CALL(*this, PEM_signing_key).WillByDefault(Return(key)); } MOCK_METHOD(std::string, PEM_certificate, (), (override, const)); diff --git a/tests/mock_platform.h b/tests/mock_platform.h index aa95b70cbc..5d56ff5e87 100644 --- a/tests/mock_platform.h +++ b/tests/mock_platform.h @@ -59,6 +59,7 @@ class MockPlatform : public platform::Platform MOCK_METHOD(bool, is_image_url_supported, (), (const, override)); MOCK_METHOD(QString, get_username, (), (const, override)); MOCK_METHOD(std::string, bridge_nomenclature, (), (const, override)); + MOCK_METHOD(std::filesystem::path, get_root_cert_path, (), (const, override)); MP_MOCK_SINGLETON_BOILERPLATE(MockPlatform, Platform); }; diff --git a/tests/mock_utils.h b/tests/mock_utils.h index 7bda4a0546..c2698257e8 100644 --- a/tests/mock_utils.h +++ b/tests/mock_utils.h @@ -32,14 +32,14 @@ class MockUtils : public Utils public: using Utils::Utils; MOCK_METHOD(qint64, filesystem_bytes_available, (const QString&), (const, override)); - MOCK_METHOD(void, exit, (int), (override)); + MOCK_METHOD(void, exit, (int), (const override)); MOCK_METHOD(std::string, run_cmd_for_output, (const QString&, const QStringList&, const int), (const, override)); MOCK_METHOD(bool, run_cmd_for_status, (const QString&, const QStringList&, const int), (const, override)); MOCK_METHOD(std::string, contents_of, (const multipass::Path&), (const, override)); MOCK_METHOD(void, make_file_with_content, (const std::string&, const std::string&), ()); MOCK_METHOD(void, make_file_with_content, (const std::string&, const std::string&, const bool&), (override)); - MOCK_METHOD(Path, make_dir, (const QDir&, const QString&, std::filesystem::perms), (override)); - MOCK_METHOD(Path, make_dir, (const QDir&, std::filesystem::perms), (override)); + MOCK_METHOD(Path, make_dir, (const QDir&, const QString&, std::filesystem::perms), (const override)); + MOCK_METHOD(Path, make_dir, (const QDir&, std::filesystem::perms), (const override)); MOCK_METHOD(std::string, get_kernel_version, (), (const, override)); MOCK_METHOD(QString, generate_scrypt_hash_for, (const QString&), (const, override)); MOCK_METHOD(bool, client_certs_exist, (const QString&), (const)); diff --git a/tests/test_alias_dict.cpp b/tests/test_alias_dict.cpp index 1a049d0792..8123f893e4 100644 --- a/tests/test_alias_dict.cpp +++ b/tests/test_alias_dict.cpp @@ -27,10 +27,12 @@ #include "fake_alias_config.h" #include "file_operations.h" #include "json_test_utils.h" +#include "mock_cert_provider.h" #include "mock_file_ops.h" #include "mock_permission_utils.h" #include "mock_platform.h" #include "mock_settings.h" +#include "mock_utils.h" #include "mock_vm_image_vault.h" #include "stub_terminal.h" @@ -624,6 +626,9 @@ TEST_P(DaemonAliasTestsuite, purge_removes_purged_instance_aliases_and_scripts) EXPECT_CALL(*mock_image_vault, prune_expired_images()).WillRepeatedly(Return()); EXPECT_CALL(*mock_image_vault, has_record_for(_)).WillRepeatedly(Return(true)); + const auto [mock_utils, guard] = mpt::MockUtils::inject(); + EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(mpt::root_cert)); + config_builder.vault = std::move(mock_image_vault); auto mock_factory = use_a_mock_vm_factory(); diff --git a/tests/test_cli_client.cpp b/tests/test_cli_client.cpp index 033fe962fb..303d83babe 100644 --- a/tests/test_cli_client.cpp +++ b/tests/test_cli_client.cpp @@ -161,6 +161,7 @@ struct Client : public Test EXPECT_CALL(mock_settings, get(Eq(mp::mounts_key))).WillRepeatedly(Return("true")); EXPECT_CALL(mock_settings, register_handler(_)).WillRepeatedly(Return(nullptr)); EXPECT_CALL(mock_settings, unregister_handler).Times(AnyNumber()); + EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(mpt::root_cert)); EXPECT_CALL(mpt::MockStandardPaths::mock_instance(), locate(_, _, _)) .Times(AnyNumber()); // needed to allow general calls once we have added the specific expectation below @@ -379,8 +380,11 @@ struct Client : public Test }; std::unique_ptr> daemon_cert_provider{ std::make_unique>()}; - mpt::MockPlatform::GuardedMock attr{mpt::MockPlatform::inject()}; - mpt::MockPlatform* mock_platform = attr.first; + const mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject()}; + const mpt::MockPlatform* mock_platform = platform_attr.first; + const mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject()}; + const mpt::MockUtils* mock_utils = utils_attr.first; + mpt::StubCertStore cert_store; StrictMock mock_daemon{server_address, *daemon_cert_provider, &cert_store}; // strict to fail on unexpected calls and play well with sharing @@ -3569,7 +3573,6 @@ struct TimeoutSuite : Client, WithParamInterface TEST_P(TimeoutSuite, command_exits_on_timeout) { - auto [mock_utils, guard] = mpt::MockUtils::inject(); EXPECT_CALL(mock_daemon, launch).Times(AtMost(1)); EXPECT_CALL(mock_daemon, start).Times(AtMost(1)); diff --git a/tests/test_client_common.cpp b/tests/test_client_common.cpp index c7eb110a9c..f315ffc3bd 100644 --- a/tests/test_client_common.cpp +++ b/tests/test_client_common.cpp @@ -44,12 +44,22 @@ struct TestClientCommon : public mpt::DaemonTestFixture { ON_CALL(mpt::MockStandardPaths::mock_instance(), writableLocation(mp::StandardPaths::GenericDataLocation)) .WillByDefault(Return(temp_dir.path())); + ON_CALL(*mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert)); + // delegate some functions to the orignal implementation + ON_CALL(*mock_utils, make_dir(A(), A(), A())) + .WillByDefault([](const QDir& dir, const QString& name, std::filesystem::perms permissions) -> mp::Path { + return MP_UTILS.Utils::make_dir(dir, name, permissions); + }); + ON_CALL(*mock_utils, make_dir(A(), A())) + .WillByDefault([](const QDir& dir, std::filesystem::perms permissions) -> mp::Path { + return MP_UTILS.Utils::make_dir(dir, permissions); + }); } mpt::MockDaemon make_secure_server() { - EXPECT_CALL(*mock_cert_provider, PEM_certificate()).WillOnce(Return(mpt::daemon_cert)); - EXPECT_CALL(*mock_cert_provider, PEM_signing_key()).WillOnce(Return(mpt::daemon_key)); + EXPECT_CALL(*mock_cert_provider, PEM_certificate()).Times(1); + EXPECT_CALL(*mock_cert_provider, PEM_signing_key()).Times(1); config_builder.server_address = server_address; config_builder.cert_provider = std::move(mock_cert_provider); @@ -60,6 +70,8 @@ struct TestClientCommon : public mpt::DaemonTestFixture std::unique_ptr> mock_cert_provider{ std::make_unique>()}; std::unique_ptr mock_cert_store{std::make_unique()}; + const mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject()}; + const mpt::MockUtils* mock_utils = utils_attr.first; const mpt::MockPermissionUtils::GuardedMock mock_permission_utils_injection = mpt::MockPermissionUtils::inject(); @@ -75,8 +87,8 @@ TEST_F(TestClientCommon, usesCommonCertWhenItExists) const auto common_client_cert_file = common_cert_dir + "/" + mp::client_cert_file; const auto common_client_key_file = common_cert_dir + "/" + mp::client_key_file; - mpt::make_file_with_content(common_client_cert_file, mpt::client_cert); - mpt::make_file_with_content(common_client_key_file, mpt::client_key); + mpt::make_file_with_content(common_client_cert_file, mpt::cert); + mpt::make_file_with_content(common_client_key_file, mpt::key); EXPECT_TRUE(mp::client::make_channel(server_address, *mp::client::get_cert_provider())); } diff --git a/tests/test_daemon.cpp b/tests/test_daemon.cpp index c49ff552d6..ff5ee59f63 100644 --- a/tests/test_daemon.cpp +++ b/tests/test_daemon.cpp @@ -21,6 +21,7 @@ #include "dummy_ssh_key_provider.h" #include "fake_alias_config.h" #include "json_test_utils.h" +#include "mock_cert_provider.h" #include "mock_daemon.h" #include "mock_environment_helpers.h" #include "mock_file_ops.h" @@ -38,7 +39,6 @@ #include "mock_vm_image_vault.h" #include "path.h" #include "stub_virtual_machine.h" -#include "stub_vm_image_vault.h" #include "tracking_url_downloader.h" #include @@ -113,7 +113,7 @@ struct Daemon : public mpt::DaemonTestFixture ON_CALL(mock_utils, filesystem_bytes_available(_)).WillByDefault([this](const QString& data_directory) { return mock_utils.Utils::filesystem_bytes_available(data_directory); }); - + ON_CALL(mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert)); EXPECT_CALL(mock_platform, get_blueprints_url_override()).WillRepeatedly([] { return QString{}; }); EXPECT_CALL(mock_platform, multipass_storage_location()).Times(AnyNumber()).WillRepeatedly(Return(QString())); EXPECT_CALL(mock_platform, create_alias_script(_, _)).WillRepeatedly(Return()); @@ -134,7 +134,7 @@ struct Daemon : public mpt::DaemonTestFixture mpt::MockUtils::GuardedMock mock_utils_injection{mpt::MockUtils::inject()}; mpt::MockUtils& mock_utils = *mock_utils_injection.first; - mpt::MockPlatform::GuardedMock mock_platform_injection{mpt::MockPlatform::inject()}; + mpt::MockPlatform::GuardedMock mock_platform_injection{mpt::MockPlatform::inject()}; mpt::MockPlatform& mock_platform = *mock_platform_injection.first; mpt::MockSettings::GuardedMock mock_settings_injection = mpt::MockSettings::inject(); diff --git a/tests/test_daemon_find.cpp b/tests/test_daemon_find.cpp index f6f54262fd..04dfbedc43 100644 --- a/tests/test_daemon_find.cpp +++ b/tests/test_daemon_find.cpp @@ -17,10 +17,12 @@ #include "common.h" #include "daemon_test_fixture.h" +#include "mock_cert_provider.h" #include "mock_image_host.h" #include "mock_permission_utils.h" #include "mock_platform.h" #include "mock_settings.h" +#include "mock_utils.h" #include "mock_vm_blueprint_provider.h" #include "mock_vm_image_vault.h" @@ -49,6 +51,7 @@ struct DaemonFind : public mpt::DaemonTestFixture EXPECT_CALL(mock_settings, register_handler).WillRepeatedly(Return(nullptr)); EXPECT_CALL(mock_settings, unregister_handler).Times(AnyNumber()); EXPECT_CALL(mock_settings, get(Eq(mp::winterm_key))).WillRepeatedly(Return("none")); + ON_CALL(mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert)); } mpt::MockPlatform::GuardedMock attr{mpt::MockPlatform::inject()}; @@ -60,6 +63,9 @@ struct DaemonFind : public mpt::DaemonTestFixture const mpt::MockPermissionUtils::GuardedMock mock_permission_utils_injection = mpt::MockPermissionUtils::inject(); mpt::MockPermissionUtils& mock_permission_utils = *mock_permission_utils_injection.first; + + mpt::MockUtils::GuardedMock mock_utils_injection{mpt::MockUtils::inject()}; + mpt::MockUtils& mock_utils = *mock_utils_injection.first; }; TEST_F(DaemonFind, blankQueryReturnsAllData) diff --git a/tests/test_ssl_cert_provider.cpp b/tests/test_ssl_cert_provider.cpp index 28215f3256..c9475ffd34 100644 --- a/tests/test_ssl_cert_provider.cpp +++ b/tests/test_ssl_cert_provider.cpp @@ -17,6 +17,7 @@ #include "common.h" #include "file_operations.h" +#include "mock_platform.h" #include "temp_dir.h" #include @@ -27,17 +28,13 @@ namespace mpt = multipass::test; using namespace testing; -struct SSLCertProvider : public testing::Test +struct SSLCertProviderFixture : public testing::Test { - SSLCertProvider() - { - cert_dir = MP_UTILS.make_dir(temp_dir.path(), "test-cert"); - } mpt::TempDir temp_dir; - mp::Path cert_dir; + mp::Path cert_dir{temp_dir.path() + "/test-cert"}; }; -TEST_F(SSLCertProvider, creates_cert_and_key) +TEST_F(SSLCertProviderFixture, creates_cert_and_key) { mp::SSLCertProvider cert_provider{cert_dir}; @@ -47,7 +44,7 @@ TEST_F(SSLCertProvider, creates_cert_and_key) EXPECT_THAT(pem_key, StrNe("")); } -TEST_F(SSLCertProvider, imports_existing_cert_and_key) +TEST_F(SSLCertProviderFixture, imports_existing_cert_and_key) { constexpr auto key_data = "-----BEGIN PRIVATE KEY-----\n" "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsSAz5ggzrLjai0I/\n" @@ -79,7 +76,7 @@ TEST_F(SSLCertProvider, imports_existing_cert_and_key) EXPECT_THAT(cert_provider.PEM_certificate(), StrEq(cert_data)); } -TEST_F(SSLCertProvider, persists_cert_and_key) +TEST_F(SSLCertProviderFixture, persists_cert_and_key) { QDir dir{cert_dir}; auto key_file = dir.filePath("multipass_cert_key.pem"); @@ -94,8 +91,13 @@ TEST_F(SSLCertProvider, persists_cert_and_key) EXPECT_TRUE(QFile::exists(cert_file)); } -TEST_F(SSLCertProvider, creates_different_certs_per_server_name) +TEST_F(SSLCertProviderFixture, creates_different_certs_per_server_name) { + const auto [mock_platform, _] = mpt::MockPlatform::inject(); + // move the multipass_root_cert.pem into the temporary directory so it will be deleted automatically later + EXPECT_CALL(*mock_platform, get_root_cert_path()) + .WillRepeatedly(Return(std::filesystem::path{cert_dir.toStdU16String()} / "multipass_root_cert.pem")); + mp::SSLCertProvider cert_provider1{cert_dir, "test_server1"}; mp::SSLCertProvider cert_provider2{cert_dir, "test_server2"}; diff --git a/tests/unix/test_daemon_rpc.cpp b/tests/unix/test_daemon_rpc.cpp index 7ec3b86af5..e366e38fce 100644 --- a/tests/unix/test_daemon_rpc.cpp +++ b/tests/unix/test_daemon_rpc.cpp @@ -34,38 +34,22 @@ using namespace testing; namespace { -constexpr auto key_data = "-----BEGIN PRIVATE KEY-----\n" - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsSAz5ggzrLjai0I/\n" - "F0hYg5oG/shpXJiBQtJdBCG3lUShRANCAAQAFGNAqq7c5IMDeQ/cV4+Emogmkfpb\n" - "TLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqaJ7sAPJJwlVj1xD0r5DX5\n" - "-----END PRIVATE KEY-----\n"; - -constexpr auto cert_data = "-----BEGIN CERTIFICATE-----\n" - "MIIBUjCB+AIBKjAKBggqhkjOPQQDAjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJ\n" - "Q2Fub25pY2FsMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwNjIxMTM0MjI5WhcN\n" - "MTkwNjIxMTM0MjI5WjA1MQswCQYDVQQGEwJDQTESMBAGA1UECgwJQ2Fub25pY2Fs\n" - "MRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQA\n" - "FGNAqq7c5IMDeQ/cV4+EmogmkfpbTLSPfXgXVLHRsvL04xUAkqGpL+eyGFVE6dqa\n" - "J7sAPJJwlVj1xD0r5DX5MAoGCCqGSM49BAMCA0kAMEYCIQCvI0PYv9f201fbe4LP\n" - "BowTeYWSqMQtLNjvZgd++AAGhgIhALNPW+NRSKCXwadiIFgpbjPInLPqXPskLWSc\n" - "aXByaQyt\n" - "-----END CERTIFICATE-----\n"; - struct TestDaemonRpc : public mpt::DaemonTestFixture { TestDaemonRpc() { - EXPECT_CALL(*mock_cert_provider, PEM_certificate()).WillOnce(Return(cert_data)); - EXPECT_CALL(*mock_cert_provider, PEM_signing_key()).WillOnce(Return(key_data)); + EXPECT_CALL(*mock_cert_provider, PEM_certificate()).Times(1); + EXPECT_CALL(*mock_cert_provider, PEM_signing_key()).Times(1); EXPECT_CALL(*mock_platform, multipass_storage_location()).Times(AnyNumber()).WillRepeatedly(Return(QString())); + EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(mpt::root_cert)); } mp::Rpc::Stub make_secure_stub() { auto opts = grpc::SslCredentialsOptions(); - opts.server_certificate_request = GRPC_SSL_REQUEST_SERVER_CERTIFICATE_BUT_DONT_VERIFY; - opts.pem_cert_chain = mpt::client_cert; - opts.pem_private_key = mpt::client_key; + opts.pem_root_certs = mpt::root_cert; + opts.pem_cert_chain = mpt::cert; + opts.pem_private_key = mpt::key; auto channel = grpc::CreateChannel(server_address, grpc::SslCredentials(opts)); @@ -94,7 +78,7 @@ struct TestDaemonRpc : public mpt::DaemonTestFixture std::make_unique>()}; std::unique_ptr mock_cert_store{std::make_unique()}; - mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject()}; + mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject()}; mpt::MockPlatform* mock_platform = platform_attr.first; mpt::MockUtils::GuardedMock attr{mpt::MockUtils::inject()}; @@ -130,7 +114,7 @@ TEST_F(TestDaemonRpc, authenticateCompletesSuccessfully) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).WillOnce(Return(true)); - EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).Times(1); + EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::cert))).Times(1); mpt::MockDaemon daemon{make_secure_server()}; EXPECT_CALL(daemon, authenticate(_, _, _)).WillOnce([](auto, auto, auto* status_promise) { @@ -180,7 +164,7 @@ TEST_F(TestDaemonRpc, pingReturnsOkWhenCertIsVerified) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).WillOnce(Return(false)); - EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(true)); + EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::cert))).WillOnce(Return(true)); mpt::MockDaemon daemon{make_secure_server()}; mp::Rpc::Stub stub{make_secure_stub()}; @@ -197,7 +181,7 @@ TEST_F(TestDaemonRpc, pingReturnsUnauthenticatedWhenCertIsNotVerified) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).WillOnce(Return(false)); - EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(false)); + EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::cert))).WillOnce(Return(false)); mpt::MockDaemon daemon{make_secure_server()}; mp::Rpc::Stub stub{make_secure_stub()}; @@ -215,7 +199,7 @@ TEST_F(TestDaemonRpc, listCertExistsCompletesSuccessfully) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).Times(2).WillRepeatedly(Return(false)); - EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(true)); + EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::cert))).WillOnce(Return(true)); mpt::MockDaemon daemon{make_secure_server()}; mock_empty_list_reply(daemon); @@ -229,7 +213,7 @@ TEST_F(TestDaemonRpc, listNoCertsExistWillVerifyAndComplete) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).Times(1); + EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::cert))).Times(1); mpt::MockDaemon daemon{make_secure_server()}; mock_empty_list_reply(daemon); @@ -242,7 +226,7 @@ TEST_F(TestDaemonRpc, listCertNotVerifiedHasError) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, false)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).Times(2).WillRepeatedly(Return(false)); - EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(false)); + EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::cert))).WillOnce(Return(false)); mpt::MockDaemon daemon{make_secure_server()}; @@ -262,8 +246,8 @@ TEST_F(TestDaemonRpc, listTCPSocketNoCertsExistHasError) EXPECT_CALL(*mock_platform, set_server_socket_restrictions).Times(1); EXPECT_CALL(*mock_cert_store, empty()).Times(1); - EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).Times(0); - EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::client_cert))).WillOnce(Return(false)); + EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::cert))).Times(0); + EXPECT_CALL(*mock_cert_store, verify_cert(StrEq(mpt::cert))).WillOnce(Return(false)); mpt::MockDaemon daemon{make_secure_server()}; @@ -282,7 +266,7 @@ TEST_F(TestDaemonRpc, listAcceptCertFailsHasError) EXPECT_CALL(*mock_platform, set_server_socket_restrictions(_, true)).Times(1); EXPECT_CALL(*mock_cert_store, empty()).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).WillOnce(Throw(std::runtime_error(error_msg))); + EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::cert))).WillOnce(Throw(std::runtime_error(error_msg))); mpt::MockDaemon daemon{make_secure_server()}; @@ -302,7 +286,7 @@ TEST_F(TestDaemonRpc, listSettingServerPermissionsFailLogsErrorAndExits) .WillOnce(Throw(std::runtime_error(error_msg))); EXPECT_CALL(*mock_cert_store, empty()).Times(2).WillRepeatedly(Return(true)); - EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::client_cert))).Times(1); + EXPECT_CALL(*mock_cert_store, add_cert(StrEq(mpt::cert))).Times(1); // Detects if the daemon would actually exit EXPECT_CALL(*mock_utils, exit(EXIT_FAILURE)).Times(1);