Repository: canonical/multipass
Pull request #3909: Standard grpc mTLS 
Author: login: georgeliao
Created: 2025-01-30T11:41:43Z | Merged: 2025-03-31T09:52:38Z

[MULTI-1765](https://warthogs.atlassian.net/browse/MULTI-1765)
Public side of https://github.com/canonical/multipass-private/pull/719

**A few things to note**
The `make_cert_key_pair` function originally handles the generation of both server and client key-certificate pairs . The `server_name` parameter determines the use case, when `server_name` is an empty string, it indicated the client key-certificate case. In the standard gRPC TLS scheme, the root key-certificate pair is added. 

Now, the role of the `make_cert_key_pair` function is still generating and holding server and client key-certificate pairs. However, the server certificate has evolved into a signed server certificate. As the result, the server side branch must first generate root key-certificate pair and use it to sign the newly created server certificate. 

Previously the constructor of  `X509Cert` only handled server and client certifcate generation, distinguishing between them based on whether `server_address` was empty. Now, with the introduction of root, server and client certifcate, the dispatch is managed by using the `CertType` enumeration. 

Additionally, the `X509Cert` constructor has been refined to generate certificates in a standard format. Key differences between before and after include adjustments to the serial number format and the inclusion of X509v3 extensions. To inspect a certificate, you can use the following command:  `openssl x509 -in <cert_path> -noout -text` 

The certificate paths are as follows:

- Server certificate:`/root/.local/share/multipassd/certificates/localhost.pem` 
- Client certificate: `/home/<user name>/.local/share/multipass-client-certificate/multipass_cert.pem` 
- Root certificate: `/usr/local/share/ca-certificates/multipass_root_cert.pem`

**Snap environment considerations**
In the snap environment, the root certificate is stored at `/var/snap/multipass/common/data/multipassd/certificates/multipass_root_cert.pem` 
Unlike `/root/`, `/var/snap/` directory allows other users view its files, making this setup feasible. 

**Certificate regeneration and migration**
The root certificate and server key-certificate pairs area automatically regenerated if either is missing. This mechanism ensures a smooth server key-certificate pair migration when updating Multipass. Upon upgrading, the server startup process will automatically generates root key-certificate pair and use it to sign a fresh server certificate. Consequently, the original server key-certificate pair will be overwritten, enabling successful verification under the new standard grpc TLS. 

The Multipass upgrade process should be included in the functional testing as well, both the cmd and gui clients should be tested. 

**Unit test adaptations**
The unit tests have been modified to accommodate changes in the gRPC TLS verification process. The key adjustments include:
1. Mocking the `get_root_cert_path` to allow usage of string-based certificates. 
2. Merge the two server and client key-certificate pairs into one,  With `MockCertProvider` being used on both server side and client to provide key-certificate pair. In the unit testing environment, they can be the same. 

Following the commit history and messages is also a helpful way to understand changes that have been made. 

[MULTI-1765]: https://warthogs.atlassian.net/browse/MULTI-1765?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/3rd-party/vcpkg-ports/grpc/portfile.cmake b/3rd-party/vcpkg-ports/grpc/portfile.cmake
index ef1f48b9843..874028294e5 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 28c06ee8d43..a3e3a0ba953 100644
--- a/include/multipass/platform.h
+++ b/include/multipass/platform.h
@@ -82,6 +82,7 @@ class Platform : public Singleton<Platform>
     [[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 a038bd59ca1..95f726bf6e1 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 cfd6141dadd..73f9936a8a5 100644
--- a/include/multipass/utils.h
+++ b/include/multipass/utils.h
@@ -203,14 +203,14 @@ class Utils : public Singleton<Utils>
     Utils(const Singleton<Utils>::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 3e88f83067d..86dd849c46d 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 7699ccd4374..210da72b0b2 100644
--- a/src/cert/ssl_cert_provider.cpp
+++ b/src/cert/ssl_cert_provider.cpp
@@ -16,19 +16,17 @@
  */
 
 #include <multipass/format.h>
+#include <multipass/platform.h>
 #include <multipass/ssl_cert_provider.h>
 #include <multipass/utils.h>
 
 #include "biomem.h"
 
+#include <openssl/core_names.h>
 #include <openssl/pem.h>
 #include <openssl/rand.h>
-#include <openssl/x509.h>
 #include <openssl/x509v3.h>
 
-#include <QFile>
-
-#include <array>
 #include <cerrno>
 #include <cstring>
 #include <memory>
@@ -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 <typename T>
+void openssl_check(T result, const std::string& errorMessage)
+{
+    // TODO: expand std::is_pointer_v<T> check to cover all smart pointers as well
+    if constexpr (std::is_pointer_v<T>)
+    {
+        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<FILE, std::function<int(FILE*)>> fp;
+    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
+    // compilation error
+    using FilePtr = std::unique_ptr<FILE, int (*)(FILE*)>;
+    [[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, decltype(EC_KEY_free)*> 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<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    using EVPKeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
+
+    [[nodiscard]] static EVPKeyPtr create_key()
+    {
+        std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> 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<OSSL_PARAM, 2> 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<char*>("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<unsigned char> as_vector(const std::string& v)
@@ -111,109 +161,196 @@ std::vector<unsigned char> 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<uint8_t, 20> 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<BIGNUM, decltype(&BN_free)> 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<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& 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, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> 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<X509, decltype(&X509_free)> 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 2472e395286..4128dfc5698 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::SSLCertProvider> 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<SSLCertProvider>(common_client_cert_dir_path);
 }
 
diff --git a/src/daemon/daemon_config.cpp b/src/daemon/daemon_config.cpp
index 93679577fea..79e06f0b7d4 100644
--- a/src/daemon/daemon_config.cpp
+++ b/src/daemon/daemon_config.cpp
@@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(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<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
         server_address = platform::default_server_address();
     if (ssh_key_provider == nullptr)
         ssh_key_provider = std::make_unique<OpenSSHKeyProvider>(data_directory);
-    if (cert_provider == nullptr)
-        cert_provider = std::make_unique<mp::SSLCertProvider>(MP_UTILS.make_dir(data_directory, "certificates"),
-                                                              server_name_from(server_address));
     if (client_cert_store == nullptr)
         client_cert_store = std::make_unique<mp::ClientCertStore>(data_directory);
     if (ssh_username.empty())
@@ -192,17 +189,28 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(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::SSLCertProvider>(
+            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<const DaemonConfig>(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 6b1245ffc2f..c5a0e46da8c 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<QFile> 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 dfb628154e1..7ed79b1351d 100644
--- a/src/platform/platform_unix.cpp
+++ b/src/platform/platform_unix.cpp
@@ -18,6 +18,7 @@
 #include <multipass/format.h>
 #include <multipass/platform.h>
 #include <multipass/platform_unix.h>
+#include <multipass/snap_utils.h>
 #include <multipass/timer.h>
 #include <multipass/utils.h>
 
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
index eaa3da921a2..877b6ece067 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 2c61102ba38..137140629a8 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 40d88a62e68..16d4025ee14 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 aa95b70cbc0..5d56ff5e87c 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 7bda4a0546b..c2698257e80 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 1a049d07929..8123f893e4f 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<NiceMock>();
+    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 033fe962fb8..303d83babe9 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<NiceMock<mpt::MockCertProvider>> daemon_cert_provider{
         std::make_unique<NiceMock<mpt::MockCertProvider>>()};
-    mpt::MockPlatform::GuardedMock attr{mpt::MockPlatform::inject<NiceMock>()};
-    mpt::MockPlatform* mock_platform = attr.first;
+    const mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject<NiceMock>()};
+    const mpt::MockPlatform* mock_platform = platform_attr.first;
+    const mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject<NiceMock>()};
+    const mpt::MockUtils* mock_utils = utils_attr.first;
+
     mpt::StubCertStore cert_store;
     StrictMock<MockDaemonRpc> 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<std::string>
 
 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 c7eb110a9c4..f315ffc3bd1 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<const QDir&>(), A<const QString&>(), A<std::filesystem::perms>()))
+            .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<const QDir&>(), A<std::filesystem::perms>()))
+            .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<NiceMock<mpt::MockCertProvider>> mock_cert_provider{
         std::make_unique<NiceMock<mpt::MockCertProvider>>()};
     std::unique_ptr<mpt::MockCertStore> mock_cert_store{std::make_unique<mpt::MockCertStore>()};
+    const mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject<NiceMock>()};
+    const mpt::MockUtils* mock_utils = utils_attr.first;
 
     const mpt::MockPermissionUtils::GuardedMock mock_permission_utils_injection =
         mpt::MockPermissionUtils::inject<NiceMock>();
@@ -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 c49ff552d62..ff5ee59f636 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 <src/daemon/default_vm_image_vault.h>
@@ -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<NiceMock>()};
     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<NiceMock>()};
     mpt::MockPlatform& mock_platform = *mock_platform_injection.first;
 
     mpt::MockSettings::GuardedMock mock_settings_injection = mpt::MockSettings::inject<StrictMock>();
diff --git a/tests/test_daemon_find.cpp b/tests/test_daemon_find.cpp
index f6f54262fdb..04dfbedc430 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<NiceMock>()};
@@ -60,6 +63,9 @@ struct DaemonFind : public mpt::DaemonTestFixture
     const mpt::MockPermissionUtils::GuardedMock mock_permission_utils_injection =
         mpt::MockPermissionUtils::inject<NiceMock>();
     mpt::MockPermissionUtils& mock_permission_utils = *mock_permission_utils_injection.first;
+
+    mpt::MockUtils::GuardedMock mock_utils_injection{mpt::MockUtils::inject<NiceMock>()};
+    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 28215f3256e..c9475ffd343 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 <multipass/ssl_cert_provider.h>
@@ -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<NiceMock>();
+    // 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 7ec3b86af5a..e366e38fce3 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<NiceMock<mpt::MockCertProvider>>()};
     std::unique_ptr<mpt::MockCertStore> mock_cert_store{std::make_unique<mpt::MockCertStore>()};
 
-    mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject()};
+    mpt::MockPlatform::GuardedMock platform_attr{mpt::MockPlatform::inject<NiceMock>()};
     mpt::MockPlatform* mock_platform = platform_attr.first;
 
     mpt::MockUtils::GuardedMock attr{mpt::MockUtils::inject<NiceMock>()};
@@ -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);


Reviews:
user: login: andrei-toterman
body: Just a quick pass with some nitpicks, didn't test functionally yet.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T11:08:31Z
user: login: xmkg
body: ~LGTM, except for one small nit.~

EDIT: Updated review for the new code changes.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-02-11T07:44:33Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T11:53:36Z
user: login: andrei-toterman
body: Hey, @georgeliao! So now, in the local environment, the GUI works fine both when upgrading from the old certificates and when starting with no certificates. But in the case of the CLI, it works when starting with no certificates, but if I upgrade from old certificates, I get the following error
```
E0211 14:23:49.406977684   36809 ssl_transport_security.cc:1316]       Handshake failed with fatal error SSL_ERROR_SSL: error:0A000086:SSL routines::certificate verify failed.
```
And in the snap, both when starting fresh or when upgrading, I get the following
```
[error] [client] Caught an unhandled exception: failed to open file '/var/snap/multipass/commondata/multipassd/certificates/multipass_root_cert.pem': No such file or directory(2)
```
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-11T13:30:47Z
user: login: andrei-toterman
body: Great, now both the CLI and the GUI work as expected, both locally and in the snap, both when upgrading or installing fresh. Thanks, @georgeliao!

Now, one thing to discuss with everyone: the purpose of these changes was so that we could remove our grpc patches, and that goal is accomplished by this PR. And now the CLI verifies the server certificate, which couldn't be avoided without the patches. But the GUI can avoid verifying the server certificate, using the plain vanilla grpc dart library. My question is: should the GUI also verify the server certificate, to be in line with the CLI, or should we keep the existing, functioning behavior of not having the GUI verify the server certificate?
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-02-12T10:16:21Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T14:31:39Z
user: login: ricab
body: Thanks @georgeliao, lots of good work in here!

I am only doing a *tertiary review* in this case, so I glossed over most of the scary low-level certificate stuff. @andrei-toterman and @xmkg, we're relying on you on that front :muscle: If you could provide assurance that you've verified sanity of all the nitty-gritty, that would be much appreciated. All of this would be for nothing if security were somehow broken on the foundation...

Other than that, I have some proposals for path derivation and the initialization. Let me know what you think.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-02-13T19:33:02Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T10:54:31Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T11:46:54Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-14T12:21:44Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-14T12:22:23Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T12:59:20Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T13:25:28Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T13:47:07Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:07:07Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:16:21Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:22:21Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:30:59Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:31:16Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T14:31:24Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-14T15:04:14Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-14T15:23:23Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-14T15:28:23Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-17T16:00:48Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-17T18:53:11Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-20T18:10:52Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-20T18:21:53Z
user: login: xmkg
body: Looks good overall, a couple of nitpicks and a proposal for handling all the error codes returned by the OpenSSL API:

> It'd be better if we could check the return value of the functions that can return an error. It might be cumbersome to repeat for each of them, so we could do something like:
> 
> ```c++
> auto ossl_throw_if_failed = [](int result, const char* op) noexcept(false) {
>     if(result == 1) return;
>     throw std::runtime_error{fmt::format("{} failed with {}.", op, result)};
> };
> 
> ossl_throw_if_failed(set_random_serial_number(cert.get()), "set_random_serial_number"));
> /* ... and the others */
> ```


state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-03T13:04:57Z
user: login: Sploder12
body: Looks pretty good, just a little issue with windows string to path conversion, and some questions
(only looking at the permission related things)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T18:21:59Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:43:57Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:45:16Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:57:57Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:58:36Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:44:27Z
user: login: Sploder12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:51:02Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T14:02:24Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:12:52Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T11:59:26Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:06:27Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:08:45Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:49:53Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:54:46Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T14:24:35Z
user: login: xmkg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T14:36:20Z
user: login: ricab
body: Hey @georgeliao, I've reviewed the permissions part and it looks mostly good to me, provided you're in sync with @Sploder12 on it. Only a couple of things inline
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T21:34:36Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:13:50Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:48:39Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:00:08Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T12:10:49Z
user: login: ricab
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T12:29:38Z
user: login: Sploder12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:08:39Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:10:50Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:14:33Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:04:16Z
user: login: Sploder12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:15:34Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:07:28Z
user: login: xmkg
body: LGTM :+1: Good work!
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-07T11:23:12Z
user: login: ricab
body: Yep, looks good to me, modulo a small request inline. Thanks Jia!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-07T11:26:03Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:29:09Z
user: login: georgeliao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T12:09:22Z
user: login: ricab
body: Approving the latest changes as well (secondary review, no testing).
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:37:03Z
user: login: Sploder12
body: Functional testing on Ubuntu (both snap and not) revealed no issues and permissions on the directories are as expected. Looks good to me, great work!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:08:43Z
user: login: Sploder12
body: LGTM, everything works as expected on Ubuntu!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:44:59Z
user: login: Sploder12
body: LGTM, good catch with the restart issue! This time I tested the restart as well. Everything looks good to me on the Linux side of things! 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T19:52:47Z
user: login: ricab
body: Thanks Jia, just a small comment, but otherwise looks good. Approving modulo suggestion
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-27T20:30:07Z
user: login: ricab
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-28T15:52:58Z
user: login: xmkg
body: Re-approving the latest changes.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T16:06:10Z

Review comments:
diff_hunk: @@ -34,8 +34,7 @@ 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);
+    SSLCertProvider(const Path& data_dir, const std::string& server_name = "");
path: include/multipass/ssl_cert_provider.h
user: login: andrei-toterman
body: Now that this constructor can be called with only one param, I think it should be marked as explicit.
created_at: 2025-02-07T10:06:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -620,6 +622,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<NiceMock>();
+    EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(multipass::test::root_cert));
path: tests/test_alias_dict.cpp
user: login: andrei-toterman
body: ```suggestion
    EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(mpt::root_cert));
```
created_at: 2025-02-07T10:22:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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(multipass::test::root_cert));
path: tests/test_cli_client.cpp
user: login: andrei-toterman
body: ```suggestion
        EXPECT_CALL(*mock_utils, contents_of(_)).WillRepeatedly(Return(mpt::root_cert));
```
created_at: 2025-02-07T10:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,12 +43,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(multipass::test::root_cert));
path: tests/test_client_common.cpp
user: login: andrei-toterman
body: ```suggestion
        ON_CALL(*mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert));
```
created_at: 2025-02-07T10:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,7 +112,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(multipass::test::root_cert));
path: tests/test_daemon.cpp
user: login: andrei-toterman
body: ```suggestion
        ON_CALL(mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert));
```
created_at: 2025-02-07T10:29:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,13 +50,17 @@ 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(multipass::test::root_cert));
path: tests/test_daemon_find.cpp
user: login: andrei-toterman
body: ```suggestion
        ON_CALL(mock_utils, contents_of(_)).WillByDefault(Return(mpt::root_cert));
```
created_at: 2025-02-07T10:29:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -241,3 +242,13 @@ long long mp::platform::Platform::get_total_ram() const
 {
     return static_cast<long long>(sysconf(_SC_PHYS_PAGES)) * sysconf(_SC_PAGESIZE);
 }
+
+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()
+               ? std::filesystem::path{QString{mp::utils::snap_common_dir()}.toStdString().c_str()} /
path: src/platform/platform_unix.cpp
user: login: andrei-toterman
body: ```suggestion
               ? std::filesystem::path{mp::utils::snap_common_dir().toStdString()} /
```
created_at: 2025-02-07T11:05:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -470,3 +479,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";
path: src/platform/platform_linux.cpp
user: login: xmkg
body: Is there a reason for using `auto*` instead of just plain `auto`? Both should result in `const char*`.
created_at: 2025-02-11T07:41:14Z
author_association: MEMBER
diff_hunk: @@ -470,3 +479,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";
path: src/platform/platform_linux.cpp
user: login: georgeliao
body: Yes, they are equivalent in this case. But normally I prefer appending `*` or `&` to add a bit expressiveness of data type.
created_at: 2025-02-11T11:53:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: src/platform/platform_linux.cpp
user: login: georgeliao
body: @ricab Can you review the implementation of `get_root_cert_path` function, especially its sub-function `multipass_final_storage_location()`, make sure the path looks correct to you. Besides that, please also have a look of the whole private PR [diff](https://github.com/canonical/multipass-private/pull/719). Thanks. 
created_at: 2025-02-13T14:31:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,10 +39,19 @@ namespace
 class WritableFile
 {
 public:
-    explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose}
+    explicit WritableFile(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: It's best not to move initialization from the initializer list to the body of the constructor (we've been discussing just this [here](https://github.com/canonical/multipass/pull/3924#discussion_r1952924188)). Would it be difficult to extract this to an unnamed namespace helper that you'd initialize `fp` with? I.e. the constructor would be simply

```cpp
explicit WritableFile(const QString& name) : fp{helper(), fclose} {}
```

Whereas all the work would happen in `helper()`. That way you prevent (future) use of uninitialized internals by mistake.
created_at: 2025-02-13T16:16:22Z
author_association: COLLABORATOR
diff_hunk: @@ -53,30 +60,41 @@ class WritableFile
     }
 
 private:
-    std::unique_ptr<FILE, std::function<int(FILE*)>> fp;
+    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
+    // compilation error
+    std::unique_ptr<FILE, int (*)(FILE*)> fp{nullptr, fclose};
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: With the proposal above, you'd no longer need the two step initialization.
created_at: 2025-02-13T17:03:31Z
author_association: COLLABORATOR
diff_hunk: @@ -53,30 +60,41 @@ class WritableFile
     }
 
 private:
-    std::unique_ptr<FILE, std::function<int(FILE*)>> fp;
+    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
+    // compilation error
+    std::unique_ptr<FILE, int (*)(FILE*)> fp{nullptr, fclose};
 };
 
 class EVPKey
 {
 public:
     EVPKey()
     {
-        if (key == nullptr)
-            throw std::runtime_error("Failed to allocate EVP_PKEY");
-
-        std::unique_ptr<EC_KEY, decltype(EC_KEY_free)*> 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::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
+            EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr),
+            EVP_PKEY_CTX_free);
+
+        if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0)
+        {
+            throw std::runtime_error("Failed to initialize key generation");
+        }
+
+        // Set EC curve (P-256)
+        const std::array<OSSL_PARAM, 2> 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<char*>("P-256"), 0),
+            OSSL_PARAM_construct_end()};
+        EVP_PKEY_CTX_set_params(ctx.get(), params.data());
+
+        // Generate the key
+        EVP_PKEY* raw_key = nullptr;
+        if (EVP_PKEY_generate(ctx.get(), &raw_key) <= 0)
+        {
+            throw std::runtime_error("Failed to generate EC key");
+        }
+
+        // Assign generated key to unique_ptr for RAII management
+        key.reset(raw_key);
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: Same thing: initialization shouldn't be brought into the body of the constructor. You should be able to pull all of this out into the unnamed namespace, or a static method of the class if you prefer that organization.
created_at: 2025-02-13T17:15:21Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: I wouldn't mind an assert that it didn't end up negative.
created_at: 2025-02-13T17:20:34Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: Was this code not being used?
created_at: 2025-02-13T17:25:06Z
author_association: COLLABORATOR
diff_hunk: @@ -167,8 +167,8 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     if (ssh_key_provider == nullptr)
         ssh_key_provider = std::make_unique<OpenSSHKeyProvider>(data_directory);
     if (cert_provider == nullptr)
-        cert_provider = std::make_unique<mp::SSLCertProvider>(MP_UTILS.make_dir(data_directory, "certificates"),
path: src/daemon/daemon_config.cpp
user: login: ricab
body: Where is the directory first created now?
created_at: 2025-02-13T17:28:19Z
author_association: COLLABORATOR
diff_hunk: @@ -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
path: src/platform/platform_linux.cpp
user: login: ricab
body: Hmm, in other places we have always stuck to `(Q)StandardPaths`. That keeps us out of the platform layer and usually takes care of snap specificities. Can't we keep doing so? I thought we couldn't because of the chosen directory, but now I see `/usr/local/share/`. 

For example, couldn't we do something like below (outside of the platform layer)?

```cpp
if (!storage_path.isEmpty())
    return user_specified_mp_storage / "data" / "multipassd" / "certificates" / root_cert_file_name
else
    writable_location_that_can_be_read_by_all() / "multipassd" / "certificates" / root_cert_file_name
```

where

```cpp
path writable_location_that_can_be_read_by_all()
{
    auto possibilities = StandardPaths::standardLocations(StandardPaths::AppDataLocation);
    for (const auto& path : possibilities)
    {
        if (QFileInfo{path}.isWritable() && QFileInfo{path}.permissions() & QFileDevice::ReadOther)
            return path;
    }
    throw give_up();
}
```
?

created_at: 2025-02-13T17:58:19Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: Was this case only ever hit in clients, before?
created_at: 2025-02-13T19:00:42Z
author_association: COLLABORATOR
diff_hunk: @@ -41,10 +39,19 @@ namespace
 class WritableFile
 {
 public:
-    explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose}
+    explicit WritableFile(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: That makes sense. What about a small variation of that, meaning makes the `helper` function lambda and invoke it right away. In this case, the exclusivity and localization of the functor is better. 
That makes the result code:
```
class WritableFile
{
public:
    explicit WritableFile(const QString& file_path)
        : fp{[](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");

                 if (raw_fp == nullptr)
                     throw std::runtime_error(
                         fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno));
                 return raw_fp;
             }(file_path),
             fclose}
    {
    }

    FILE* get() const
    {
        return fp.get();
    }

private:
    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
    // compilation error
    std::unique_ptr<FILE, int (*)(FILE*)> fp;
};
```

created_at: 2025-02-14T10:54:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,8 +167,8 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     if (ssh_key_provider == nullptr)
         ssh_key_provider = std::make_unique<OpenSSHKeyProvider>(data_directory);
     if (cert_provider == nullptr)
-        cert_provider = std::make_unique<mp::SSLCertProvider>(MP_UTILS.make_dir(data_directory, "certificates"),
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: This stems from the change I have made in the `Writable` class constructor, the line  `std::filesystem::create_directories(file_path_std.parent_path());` was added, meaning that the parent directory of the certificate and key is guaranteed to exist. That includes server certificate directory (that is this one), client certificate directory, and root certificate directory. As a result, the `make_dir` call is no longer needed. 
created_at: 2025-02-14T11:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,10 +39,19 @@ namespace
 class WritableFile
 {
 public:
-    explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose}
+    explicit WritableFile(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: Sure, I am not opposed to that. I would personally prefer a well-named function, but I think that lambda is also fine.
created_at: 2025-02-14T12:21:44Z
author_association: COLLABORATOR
diff_hunk: @@ -167,8 +167,8 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     if (ssh_key_provider == nullptr)
         ssh_key_provider = std::make_unique<OpenSSHKeyProvider>(data_directory);
     if (cert_provider == nullptr)
-        cert_provider = std::make_unique<mp::SSLCertProvider>(MP_UTILS.make_dir(data_directory, "certificates"),
path: src/daemon/daemon_config.cpp
user: login: ricab
body: Got it, thanks.
created_at: 2025-02-14T12:22:23Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: Are you talking about the original `cn_name_from` function? That one was used both by the server certificate and client certificate. `server_name` non-empty indicates the server case. In our new implementation, this dispatch is preserved by the line
```
        const auto cn = as_vector(cert_type == CertType::Root     ? "Multipass Root CA"
                                  : cert_type == CertType::Client ? mp::utils::make_uuid().toStdString()
                                                                  : server_name);
```
created_at: 2025-02-14T12:59:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: Which code? `GENERAL_NAMES_free` function is a deprecated api. The whole `set_san_name` function was re-written based on the modern openssl c-apis. Particularly, this call and the implementation of `add_extension` does that, 
```
        if (cert_type == CertType::Server)
        {
            add_extension(ctx, NID_subject_alt_name, ("DNS:" + server_name).c_str());
        }
```
created_at: 2025-02-14T13:25:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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(x509.get(), 2);
+        X509_set_version(cert.get(), 2); // 0 index based, 2 amounts to 3
 
-        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);
+        set_random_serial_number(cert.get());
+        X509_gmtime_adj(X509_get_notBefore(cert.get()), 0); // Start time: now
+        const long valid_duration_sec = cert_type == CertType::Root ? 3650L * 24L * 60L * 60L : 365L * 24L * 60L * 60L;
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: We can get rid of the magic numbers here in favor of something more readable:

```C++
    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());
```

We can also simplify this even further when we switch to C++20 with the help of "std::chrono::years".
created_at: 2025-02-14T13:37:42Z
author_association: MEMBER
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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(x509.get(), 2);
+        X509_set_version(cert.get(), 2); // 0 index based, 2 amounts to 3
 
-        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);
+        set_random_serial_number(cert.get());
+        X509_gmtime_adj(X509_get_notBefore(cert.get()), 0); // Start time: now
+        const long valid_duration_sec = cert_type == CertType::Root ? 3650L * 24L * 60L * 60L : 365L * 24L * 60L * 60L;
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: Yep, it is definitely better with chrono time units and automatic unit conversion. 
created_at: 2025-02-14T14:07:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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(x509.get(), 2);
+        X509_set_version(cert.get(), 2); // 0 index based, 2 amounts to 3
 
-        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);
+        set_random_serial_number(cert.get());
+        X509_gmtime_adj(X509_get_notBefore(cert.get()), 0); // Start time: now
+        const long valid_duration_sec = cert_type == CertType::Root ? 3650L * 24L * 60L * 60L : 365L * 24L * 60L * 60L;
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: It is a bit funny that `hours` is the biggest unit before c++20. 
created_at: 2025-02-14T14:16:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,10 +39,19 @@ namespace
 class WritableFile
 {
 public:
-    explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose}
+    explicit WritableFile(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: Indeed, named function is better. 
created_at: 2025-02-14T14:22:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,30 +60,41 @@ class WritableFile
     }
 
 private:
-    std::unique_ptr<FILE, std::function<int(FILE*)>> fp;
+    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
+    // compilation error
+    std::unique_ptr<FILE, int (*)(FILE*)> fp{nullptr, fclose};
 };
 
 class EVPKey
 {
 public:
     EVPKey()
     {
-        if (key == nullptr)
-            throw std::runtime_error("Failed to allocate EVP_PKEY");
-
-        std::unique_ptr<EC_KEY, decltype(EC_KEY_free)*> 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::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
+            EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr),
+            EVP_PKEY_CTX_free);
+
+        if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0)
+        {
+            throw std::runtime_error("Failed to initialize key generation");
+        }
+
+        // Set EC curve (P-256)
+        const std::array<OSSL_PARAM, 2> 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<char*>("P-256"), 0),
+            OSSL_PARAM_construct_end()};
+        EVP_PKEY_CTX_set_params(ctx.get(), params.data());
+
+        // Generate the key
+        EVP_PKEY* raw_key = nullptr;
+        if (EVP_PKEY_generate(ctx.get(), &raw_key) <= 0)
+        {
+            throw std::runtime_error("Failed to generate EC key");
+        }
+
+        // Assign generated key to unique_ptr for RAII management
+        key.reset(raw_key);
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: done. 
created_at: 2025-02-14T14:30:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,30 +60,41 @@ class WritableFile
     }
 
 private:
-    std::unique_ptr<FILE, std::function<int(FILE*)>> fp;
+    // decltype(&fclose) does not preserve these some extra function attributes of fclose, leads to warning and
+    // compilation error
+    std::unique_ptr<FILE, int (*)(FILE*)> fp{nullptr, fclose};
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: done

created_at: 2025-02-14T14:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,10 +39,19 @@ namespace
 class WritableFile
 {
 public:
-    explicit WritableFile(const QString& name) : fp{fopen(name.toStdString().c_str(), "wb"), fclose}
+    explicit WritableFile(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: done
created_at: 2025-02-14T14:31:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
+
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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(), NULL);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: OK, thanks for clarifying.
created_at: 2025-02-14T15:04:14Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: OK, it might be good to add a comment next to the signature of the `SSLCertProvider` constructor. Something like: `// leave server_name empty for clients; choose a (non-empty) name for servers.`
created_at: 2025-02-14T15:23:22Z
author_association: COLLABORATOR
diff_hunk: @@ -103,128 +129,216 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key{nullptr, EVP_PKEY_free};
 };
 
 std::vector<unsigned char> as_vector(const std::string& v)
 {
     return {v.begin(), v.end()};
 }
 
-std::string cn_name_from(const std::string& server_name)
+void set_random_serial_number(X509* cert)
 {
-    if (server_name.empty())
-        return mp::utils::make_uuid().toStdString();
-    return server_name;
-}
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // Set the highest bit to 0 (unsigned) to ensure it's positive
+    serial_bytes[0] &= 0x7F;
 
-void set_san_name(X509* c, const std::string& server_name)
-{
-    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);
+    // Convert bytes to an BIGNUM, an arbitrary-precision integer type
+    std::unique_ptr<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), NULL), BN_free);
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: That indeed an good addition, and you are in luck. openssl has an utility function `BN_is_negative` for this. 
created_at: 2025-02-14T15:28:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: src/platform/platform_linux.cpp
user: login: georgeliao
body: @ricab 
I have dug into this direction and I would like to share my findings. 
There are two challenges. 
1. How to correctly check the readability of the file to other users? 
    I do not think QFile related api can do that, we will have to loop over all parent directories and check one by one. Besides that. I am not even sure whether this check is compatible with windows. 
2. How can we make the client locates the server generated data folder? 
    The [locate function](https://doc.qt.io/qt-6/qstandardpaths.html#locate) still requires `QStandardPaths::AppDataLocation` as an input and that will be resolved to a client user based path. 

So overall, I do not think it is easy to implement. 

Another thing is the Windows multipass storage folder permission. Indeed, the restriction is enforced by Multipass instead of `C:/ProgramData/` itself. The exact line does it is [here](https://github.com/canonical/multipass/blob/24247990441d5d4c95e0c16a9411467e31d2b0df/src/daemon/daemon_config.cpp#L113). In case you are wondering why it hits the `storage_path` non-empty branch despite there is no user defined path, that is because the implementation of `MP_PLATFORM.multipass_storage_location()` on windows derived that. That also means the client process will use `MP_PLATFORM.multipass_storage_location()` as opposed to `QStandardPaths::AppDataLocation` to locate the path if we want to go down this route. 

We could limit `std::filesystem::perms::owner_all` to all sub-directories except this chain `multipass/data/certificates`. But the code probably will still be tedious. If we do not care too much about granularity, then just appending `| std::filesystem::perms::others_read` can solve this problem easily. 
created_at: 2025-02-17T15:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: src/platform/platform_linux.cpp
user: login: ricab
body: > 1\. How to correctly check the readability of the file to other users?

I think we could skip the verification of the parent directories. We know what directories can be returned on each platform and we know what their permissions are supposed to be. In practice, we know that `C:/ProgramData`, `/Library/Application\ Support` and `/usr/local/share/` are all readable by all. IOW, I think it is safe to assume that the first `GenericDataLocation` entry that isn't bound to a user is readable by all.

> I am not even sure whether this check is compatible with windows.

That would need to be verified first but it would be weird if it wasn't. Qt is supposed to abstract the platform.

> 2\. How can we make the client locates the server generated data folder?
The [locate function](https://doc.qt.io/qt-6/qstandardpaths.html#locate) still requires `QStandardPaths::AppDataLocation` as an input and that will be resolved to a client user based path.

`AppDataLocation` is bound to the current app (paths include `<APPNAME>` and `<APPDIR>`), so we would need to locate `multipassd/.../root.cert` in `GenericDataLocation`. As discussed, the locate function tries each possible location in turn so, as long as we choose a unique name for the root certificate, it would eventually look in the second entry (the one that does not depend on the user, e.g. ` /usr/local/share`) and find it there.

> Indeed, the restriction is enforced by Multipass instead of C:/ProgramData/ itself. The exact line does it is [here](https://github.com/canonical/multipass/blob/24247990441d5d4c95e0c16a9411467e31d2b0df/src/daemon/daemon_config.cpp#L113).

OK, that is as I suspected. It's a pretty recent change. You'd need to fix it to be more granular.

> That also means the client process will use `MP_PLATFORM.multipass_storage_location()` as opposed to `QStandardPaths::AppDataLocation` to locate the path if we want to go down this route.

Yes, we'd always need to use it if we wanted to `MULTIPASS_STORAGE` for the root certificate.

created_at: 2025-02-17T18:53:11Z
author_association: COLLABORATOR
diff_hunk: @@ -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
path: src/platform/platform_linux.cpp
user: login: ricab
body: As discussed, this would break in the snap case, where different paths are returned in the daemon and in the clients. We'd still need platform code to circumvent that, so we figured it isn't worth the trouble. 
created_at: 2025-02-20T18:10:52Z
author_association: COLLABORATOR
diff_hunk: @@ -470,3 +478,46 @@ 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
+{
+    const auto paths = MP_STDPATHS.standardLocations(StandardPaths::AppDataLocation);
+
+    for (const auto& path : paths)
+    {
+        mpl::log(mpl::Level::info, "general", fmt::format("path AppDataLocation value is : {}", path.toStdString()));
+    }
+
+    mpl::log(mpl::Level::warning, "general", "\n");
+
+    const auto paths2 = MP_STDPATHS.standardLocations(StandardPaths::GenericDataLocation);
+    for (const auto& path : paths2)
+    {
+        mpl::log(mpl::Level::info,
+                 "general",
+                 fmt::format("path GenericDataLocation value is : {}", path.toStdString()));
+    }
+    mpl::log(mpl::Level::info, "general", "\n");
+
+    mpl::log(
+        mpl::Level::info,
+        "general",
+        fmt::format(
+            "located path in GenericDataLocation value is : {}",
+            MP_STDPATHS.locate(mp::StandardPaths::GenericDataLocation, "certificates/localhost.pem").toStdString()));
+
+    mpl::log(mpl::Level::info,
+             "general",
+             fmt::format("final AppDataLocation value is : {}",
+                         MP_STDPATHS.writableLocation(StandardPaths::AppDataLocation).toStdString()));
+
+    mpl::log(mpl::Level::info,
+             "general",
+             fmt::format("final GenericDataLocation value is : {}",
+                         MP_STDPATHS.writableLocation(StandardPaths::GenericDataLocation).toStdString()));
+
+    constexpr auto* root_cert_file_name = "multipass_root_cert.pem";
+    return mp::utils::in_multipass_snap()
+               ? multipass_final_storage_location() / "data" / "multipassd" / "certificates" / root_cert_file_name
path: src/platform/platform_linux.cpp
user: login: ricab
body: We have [a constant for the name of the daemon](https://github.com/canonical/multipass/blob/edd295984e239ff47d367701efe191f4c4cce935/include/multipass/constants.h#L29).
created_at: 2025-02-20T18:21:53Z
author_association: COLLABORATOR
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* open_file(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: `[[nodiscard]]` would be appropriate here.
created_at: 2025-03-03T12:22:09Z
author_association: MEMBER
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* 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");
+
+    if (raw_fp == nullptr)
+        throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno));
+
+    return raw_fp;
+}
+
+EVP_PKEY* create_raw_key_pointer()
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: `[[nodiscard]]` would be appropriate here.
created_at: 2025-03-03T12:26:14Z
author_association: MEMBER
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* 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");
+
+    if (raw_fp == nullptr)
+        throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno));
+
+    return raw_fp;
+}
+
+EVP_PKEY* create_raw_key_pointer()
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: IMO, it would be better if this function returned a std::unique_ptr<> directly, which would guarantee that EVP_PKEY_free would be called.
created_at: 2025-03-03T12:28:32Z
author_association: MEMBER
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* open_file(const QString& file_path)
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: Same as `create_raw_key_pointer`. (we can return std::unique_ptr<> directly).
created_at: 2025-03-03T12:29:19Z
author_association: MEMBER
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* 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");
+
+    if (raw_fp == nullptr)
+        throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno));
+
+    return raw_fp;
+}
+
+EVP_PKEY* create_raw_key_pointer()
+{
+    std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr),
+                                                                    EVP_PKEY_CTX_free);
+
+    if (!ctx || EVP_PKEY_keygen_init(ctx.get()) <= 0)
+    {
+        throw std::runtime_error("Failed to initialize key generation");
+    }
+
+    // Set EC curve (P-256)
+    const std::array<OSSL_PARAM, 2> 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<char*>("P-256"), 0),
+        OSSL_PARAM_construct_end()};
+    EVP_PKEY_CTX_set_params(ctx.get(), params.data());
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: The code does not check the result of EVP_PKEY_CTX_set_params.
created_at: 2025-03-03T12:30:08Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: This might return an error.
created_at: 2025-03-03T12:32:11Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: This might return an error.
created_at: 2025-03-03T12:33:22Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: It'd be better if we could check the return value of the functions that can return an error. It might be cumbersome to repeat for each of them, so we could do something like:

```c++
auto ossl_throw_if_failed = [](int result, const char* op) noexcept(false) {
    if(result == 1) return;
    throw std::runtime_error{fmt::format("{} failed with {}.", op, result)};
};

ossl_throw_if_failed(set_random_serial_number(cert.get()), "set_random_serial_number"));
/* ... and the others */

```
created_at: 2025-03-03T12:40:31Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        const auto& issuer_cert = cert_type == CertType::Server ? root_certificate.value().cert : cert;
+        // Add X509v3 extensions
+        X509V3_CTX ctx;
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: We can ensure a zero initialization by adding braces `{}` here.
created_at: 2025-03-03T12:43:56Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        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);
 
-        if (!X509_set_pubkey(x509.get(), key.get()))
-            throw std::runtime_error("Failed to set certificate public key");
+        // 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());
 
-        if (!X509_sign(x509.get(), key.get(), EVP_sha256()))
+        const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key;
+        if (!X509_sign(cert.get(), signing_key.get(), EVP_sha256()))
             throw std::runtime_error("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());
+        auto bytes = PEM_write_bio_X509(mem.get(), cert.get());
         if (bytes == 0)
             throw std::runtime_error("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};
+        if (!PEM_write_X509(file.get(), cert.get()))
+            throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", cert_path));
     }
 
 private:
-    std::unique_ptr<X509, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> ext(
+            X509V3_EXT_conf_nid(nullptr, &ctx, nid, value),
+            X509_EXTENSION_free);
+
+        if (!ext)
+        {
+            throw std::runtime_error("Failed to create X509 extension");
+        }
+
+        if (!X509_add_ext(cert.get(), ext.get(), -1))
+        {
+            throw std::runtime_error("Failed to add X509 extension");
+        }
+    }
+
+    std::unique_ptr<X509, decltype(&X509_free)> 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))
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: What's the reason for using `QFile::exists()` for the last two? It looks inconsistent with the initial `std::filesystem::exists`. We can break the QT type chain if we cast cert_dir to `std::filesystem::directory` at the beginning, so everything after it becomes a `std::filesystem` type naturally.
created_at: 2025-03-03T12:47:54Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        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);
 
-        if (!X509_set_pubkey(x509.get(), key.get()))
-            throw std::runtime_error("Failed to set certificate public key");
+        // 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());
 
-        if (!X509_sign(x509.get(), key.get(), EVP_sha256()))
+        const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key;
+        if (!X509_sign(cert.get(), signing_key.get(), EVP_sha256()))
             throw std::runtime_error("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());
+        auto bytes = PEM_write_bio_X509(mem.get(), cert.get());
         if (bytes == 0)
             throw std::runtime_error("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};
+        if (!PEM_write_X509(file.get(), cert.get()))
+            throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", cert_path));
     }
 
 private:
-    std::unique_ptr<X509, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> ext(
+            X509V3_EXT_conf_nid(nullptr, &ctx, nid, value),
+            X509_EXTENSION_free);
+
+        if (!ext)
+        {
+            throw std::runtime_error("Failed to create X509 extension");
+        }
+
+        if (!X509_add_ext(cert.get(), ext.get(), -1))
+        {
+            throw std::runtime_error("Failed to add X509 extension");
+        }
+    }
+
+    std::unique_ptr<X509, decltype(&X509_free)> 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))
+        {
+            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;
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: We can guarantee zero initialization with `{}`.
created_at: 2025-03-03T12:50:42Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        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);
 
-        if (!X509_set_pubkey(x509.get(), key.get()))
-            throw std::runtime_error("Failed to set certificate public key");
+        // 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());
 
-        if (!X509_sign(x509.get(), key.get(), EVP_sha256()))
+        const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key;
+        if (!X509_sign(cert.get(), signing_key.get(), EVP_sha256()))
             throw std::runtime_error("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());
+        auto bytes = PEM_write_bio_X509(mem.get(), cert.get());
         if (bytes == 0)
             throw std::runtime_error("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};
+        if (!PEM_write_X509(file.get(), cert.get()))
+            throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", cert_path));
     }
 
 private:
-    std::unique_ptr<X509, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> ext(
+            X509V3_EXT_conf_nid(nullptr, &ctx, nid, value),
+            X509_EXTENSION_free);
+
+        if (!ext)
+        {
+            throw std::runtime_error("Failed to create X509 extension");
+        }
+
+        if (!X509_add_ext(cert.get(), ext.get(), -1))
+        {
+            throw std::runtime_error("Failed to add X509 extension");
+        }
+    }
+
+    std::unique_ptr<X509, decltype(&X509_free)> 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))
+        {
+            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;
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: ^ zero init
created_at: 2025-03-03T12:51:01Z
author_association: MEMBER
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        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);
 
-        if (!X509_set_pubkey(x509.get(), key.get()))
-            throw std::runtime_error("Failed to set certificate public key");
+        // 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());
 
-        if (!X509_sign(x509.get(), key.get(), EVP_sha256()))
+        const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key;
+        if (!X509_sign(cert.get(), signing_key.get(), EVP_sha256()))
             throw std::runtime_error("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());
+        auto bytes = PEM_write_bio_X509(mem.get(), cert.get());
         if (bytes == 0)
             throw std::runtime_error("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};
+        if (!PEM_write_X509(file.get(), cert.get()))
+            throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", cert_path));
     }
 
 private:
-    std::unique_ptr<X509, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> ext(
+            X509V3_EXT_conf_nid(nullptr, &ctx, nid, value),
+            X509_EXTENSION_free);
+
+        if (!ext)
+        {
+            throw std::runtime_error("Failed to create X509 extension");
+        }
+
+        if (!X509_add_ext(cert.get(), ext.get(), -1))
+        {
+            throw std::runtime_error("Failed to add X509 extension");
+        }
+    }
+
+    std::unique_ptr<X509, decltype(&X509_free)> 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))
+        {
+            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;
path: src/cert/ssl_cert_provider.cpp
user: login: xmkg
body: ^ zero init
created_at: 2025-03-03T12:51:11Z
author_association: MEMBER
diff_hunk: @@ -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
path: tests/test_client_common.cpp
user: login: xmkg
body: Does this mean the tests will alter the filesystem?
created_at: 2025-03-03T12:59:30Z
author_association: MEMBER
diff_hunk: @@ -2867,7 +2867,10 @@ void mp::Daemon::persist_instances()
     }
     QDir data_dir{
         mp::utils::backend_directory_path(config->data_directory, config->factory->get_backend_directory_name())};
-    MP_JSONUTILS.write_json(instance_records_json, data_dir.filePath(instance_db_name));
+
+    const QString instance_db_path = data_dir.filePath(instance_db_name);
+    MP_JSONUTILS.write_json(instance_records_json, instance_db_path);
+    MP_PLATFORM.set_permissions(fs::path{instance_db_path.toStdString()}, fs::perms::owner_all);
path: src/daemon/daemon.cpp
user: login: Sploder12
body: `fs::path{instance_db_path.toStdString()}` isn't safe on Windows. `.toStdU16String()`/`.toStdU32String()` or (the now deprecated) `std::filesystem::u8path()` should be used instead
created_at: 2025-03-03T17:44:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: This has no effect unless Multipass is running as a subprocess since it wasn't called with `true` previously. Fine to keep though, doesn't hurt anything.
created_at: 2025-03-03T17:51:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: should the comment say "list" instead of "execute" since it's being applied to a directory?
created_at: 2025-03-03T18:14:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: Should we consider the `group` permissions here as well?
created_at: 2025-03-03T18:14:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: But there is no "list" permission, right? There is only a read permission on directory and that let you read the directory (list the items in it). However, that permission is not relevant to this use case because we want other users to view some files in the sub-directory. That requires the permission to enter all the parent directories and that is execute permission on directory is about. 
created_at: 2025-03-04T09:43:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: Yep, I was kind of aware that removing it and setting it false are more or less the same. Thanks for pointing out the subtle difference. 
created_at: 2025-03-04T09:45:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: I think we only want to let the client process view the file and the client process is run from a normal user. Meanwhile, we also want to keep the directory as restrictive as possible. So just adding the other users execute on the directory is exactly what we need, right? 
created_at: 2025-03-04T09:57:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2867,7 +2867,10 @@ void mp::Daemon::persist_instances()
     }
     QDir data_dir{
         mp::utils::backend_directory_path(config->data_directory, config->factory->get_backend_directory_name())};
-    MP_JSONUTILS.write_json(instance_records_json, data_dir.filePath(instance_db_name));
+
+    const QString instance_db_path = data_dir.filePath(instance_db_name);
+    MP_JSONUTILS.write_json(instance_records_json, instance_db_path);
+    MP_PLATFORM.set_permissions(fs::path{instance_db_path.toStdString()}, fs::perms::owner_all);
path: src/daemon/daemon.cpp
user: login: georgeliao
body: Will do 
created_at: 2025-03-04T09:58:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,128 +134,219 @@ class EVPKey
     }
 
 private:
-    std::unique_ptr<EVP_PKEY, decltype(EVP_PKEY_free)*> key{EVP_PKEY_new(), EVP_PKEY_free};
+    std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key;
 };
 
 std::vector<unsigned char> 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);
+    // OpenSSL recommends a 20-byte (160-bit) serial number
+    std::array<uint8_t, 20> serial_bytes{};
+    RAND_bytes(serial_bytes.data(), serial_bytes.size());
+    // 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<BIGNUM, decltype(&BN_free)> bn(BN_bin2bn(serial_bytes.data(), serial_bytes.size(), nullptr),
+                                                   BN_free);
+    if (!bn)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
+    assert(!BN_is_negative(bn.get()));
 
-    X509_add1_ext_i2d(c, NID_subject_alt_name, gens, 0, X509V3_ADD_DEFAULT);
+    // 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);
+    if (!serial)
+    {
+        throw std::runtime_error("Failed to convert serial bytes to BIGNUM\n");
+    }
 
-    GENERAL_NAMES_free(gens);
+    // Set the serial number in the certificate
+    X509_set_serialNumber(cert, serial);
 }
 
 class X509Cert
 {
 public:
-    explicit X509Cert(const EVPKey& key, const std::string& server_name)
+    enum class CertType
     {
-        if (x509 == nullptr)
+        Root,
+        Client,
+        Server
+    };
+
+    explicit X509Cert(const EVPKey& key,
+                      CertType cert_type,
+                      const std::string& server_name = "",
+                      const std::optional<EVPKey>& root_certificate_key = std::nullopt,
+                      const std::optional<X509Cert>& root_certificate = std::nullopt)
+    // generate root, client or signed server certificate, the third one requires the last three arguments populated
+    {
+        if (cert == nullptr)
             throw std::runtime_error("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));
+        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);
+
+        if (!X509_set_pubkey(cert.get(), key.get()))
+            throw std::runtime_error("Failed to set certificate public key");
 
-        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);
+        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);
 
-        if (!X509_set_pubkey(x509.get(), key.get()))
-            throw std::runtime_error("Failed to set certificate public key");
+        // 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());
 
-        if (!X509_sign(x509.get(), key.get(), EVP_sha256()))
+        const auto& signing_key = cert_type == CertType::Server ? *root_certificate_key : key;
+        if (!X509_sign(cert.get(), signing_key.get(), EVP_sha256()))
             throw std::runtime_error("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());
+        auto bytes = PEM_write_bio_X509(mem.get(), cert.get());
         if (bytes == 0)
             throw std::runtime_error("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};
+        if (!PEM_write_X509(file.get(), cert.get()))
+            throw std::runtime_error(fmt::format("Failed writing certificate to file '{}'", cert_path));
     }
 
 private:
-    std::unique_ptr<X509, decltype(X509_free)*> x509{X509_new(), X509_free};
+    void add_extension(X509V3_CTX& ctx, int nid, const char* value)
+    {
+        const std::unique_ptr<X509_EXTENSION, decltype(&X509_EXTENSION_free)> ext(
+            X509V3_EXT_conf_nid(nullptr, &ctx, nid, value),
+            X509_EXTENSION_free);
+
+        if (!ext)
+        {
+            throw std::runtime_error("Failed to create X509 extension");
+        }
+
+        if (!X509_add_ext(cert.get(), ext.get(), -1))
+        {
+            throw std::runtime_error("Failed to add X509 extension");
+        }
+    }
+
+    std::unique_ptr<X509, decltype(&X509_free)> 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))
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: `if (std::filesystem::exists(root_cert_path)` is newly added code and the other two are legacy. I would prefer to replace all qt file system utilities in this file if we want to do that. But that will be another PR. 
created_at: 2025-03-04T10:44:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: You're right, sometimes i get read/execute mixed up on directories.
created_at: 2025-03-04T12:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,13 +36,52 @@ namespace mp = multipass;
 
 namespace
 {
+FILE* 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");
+
+    if (raw_fp == nullptr)
+        throw std::runtime_error(fmt::format("failed to open file '{}': {}({})", file_path, strerror(errno), errno));
+
+    return raw_fp;
+}
+
+EVP_PKEY* create_raw_key_pointer()
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: Yes, indeed. We have avoid delegating the deallocation responsibility to the client code. 
created_at: 2025-03-04T14:02:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: tests/test_client_common.cpp
user: login: georgeliao
body: Let me sort out the whole story. These changes are added because 
```
    const mpt::MockUtils::GuardedMock utils_attr{mpt::MockUtils::inject<NiceMock>()};
    const mpt::MockUtils* mock_utils = utils_attr.first;
``` 
is added down there to override the `Utils` singleton class. As a result of that, some unit tests failed because MockUtils without mock implementation does nothing in the function body. In order to restore the original behavior, the delegation of these two functions is needed. As to whether the original code alters the filesystem, I think yes, but it happens in a confined temporary directory and the directory got removed after the unit test run. 
created_at: 2025-03-04T15:12:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: xmkg
body: I think this check will depend on the C API being used. Some of the C API's use 0 to indicate success. We can decouple the checker implementation to have more flexibility:

```c++
// tag type to distinguish checker type
struct checker_base{};

// generic enough, can be placed in utils
struct nullptr_checker : private checker_base {
    bool operator()(const void* v){
        return v == nullptr;
    }
};

// openssl specific checker, better to be placed in a more localized file
struct ossl_checker : private checker_base{
    bool operator()(int v){
        return v == 1;
    }
};

template <typename CheckerT = nullptr_checker, typename T>
auto check(T result, const std::string& errorMessage) -> typename std::enable_if<std::is_base_of_v<checker_base, CheckerT>, void>::type
{
    if(!CheckerT{}(result)){
        throw std::runtime_error{errorMessage};
    }
}
```

usage:

```c++
int test(){
    return 5;
}

void* test_2(){
return nullptr;
}

int main(void){
    check<ossl_checker>(test(), "");
    check<>(test_2(), "failure");
    // check<int>(test(), ""); -> wont compile
}

```


created_at: 2025-03-05T11:59:26Z
author_association: MEMBER
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: xmkg
body: It'd also be good for troubleshooting if we could incorporate the error code into the thrown error.
created_at: 2025-03-05T12:06:27Z
author_association: MEMBER
diff_hunk: @@ -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
path: tests/test_client_common.cpp
user: login: xmkg
body: I say we should put this as a comment as well, so we won't get confused later on.
created_at: 2025-03-05T12:08:44Z
author_association: MEMBER
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: georgeliao
body: The error code can be added. However, about custom checker injection. It becomes that the user have to specify the checker even within the openssl c-api return pointer or integers cases. That defies one of the purpose of the utility function, abstraction. For now, I will move the utility function into `ssl_cert_provider.cpp` as a dedicated openssl c-api checker instead of trying to be a global c-api checker. Making a general checker for all c-apis (openssl, hyperv, libssh) can be done in another PR. 
created_at: 2025-03-05T13:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: georgeliao
body: It is a very good concern you brought up. 
created_at: 2025-03-05T13:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: xmkg
body: > It becomes that the user have to specify the checker even within the openssl c-api return pointer or integers cases

We can create [local aliases](https://gcc.godbolt.org/z/b3eb4E11o) for such cases, but the gist of it is that we have to leave a customization point for the condition (e.g., a predicate or a dedicated type) if we want it to be a global utility, so different API result codes can be accommodated too. 


> For now, I will move the utility function into `ssl_cert_provider.cpp` as a dedicated openssl c-api checker instead of trying to be a global c-api checker.

I'm OK with that, we can migrate to the global one later if we decide to implement one.
created_at: 2025-03-05T14:24:34Z
author_association: MEMBER
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: xmkg
body: ... or we can adopt a rather simple approach:

```c++
struct PreconditionFailureError : std::runtime_error {
    using std::runtime_error::runtime_error;
};

void expects(bool what, const std::string& failure_msg) {
    if (!what) {
        throw PreconditionFailureError(failure_msg);
    }
}

int test() { return 5; }
int main(void) { 
    expects(test() == 4, "test failed."); 
}
```


created_at: 2025-03-05T14:36:20Z
author_association: MEMBER
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
path: src/daemon/daemon_config.cpp
user: login: ricab
body: I had a hard time understanding this comment (had to read the code first). The code is clear enough though. Perhaps this could be more generic and say only:

```suggestion
    // tighten permissions for cache and data
```
created_at: 2025-03-05T19:31:48Z
author_association: COLLABORATOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: ricab
body: Why keep a line of code that has no effect though? 

Also, couldn't we keep this true and override selectively?
created_at: 2025-03-05T21:24:40Z
author_association: COLLABORATOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: ricab
body: What happens to the "takeown" part of the replaced calls? Is it no longer necessary? is it applied somewhere else?
created_at: 2025-03-05T21:31:38Z
author_association: COLLABORATOR
diff_hunk: @@ -195,6 +195,27 @@ void parallel_for_each(Container& input_container, UnaryOperation&& unary_op)
         empty_future.get();
     }
 }
+
+// utility function for checking return code or raw pointer from C-apis
+// TODO: constrain T to int or raw pointer once C++20 concepts is available
+template <typename T>
+void check(T result, const std::string& errorMessage)
+{
+    if constexpr (std::is_pointer_v<T>)
+    {
+        if (result == nullptr)
+        {
+            throw std::runtime_error(errorMessage);
+        }
+    }
+    else
+    {
+        if (result <= 0)
path: include/multipass/utils.h
user: login: georgeliao
body: Yep, that definitely can be a candidate for a global C-api checker. 
created_at: 2025-03-06T09:13:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: Hmm, actually, make it true and override it can be an alternative of this. However, since we already have the current implementation. I am more inclined to remove this `MP_PLATFORM.setup_permission_inheritance(false);` line and keep the way it is. If we want to change to the alternative, I would like to do it in a different PR. 
created_at: 2025-03-06T09:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: I did not get this question. What is "takeown" part ? You mean setting owner only permission?  
created_at: 2025-03-06T10:00:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: ricab
body: OK, that makes sense. @Sploder12 WDYT? Should we put it back in a later PR?
created_at: 2025-03-06T12:10:49Z
author_association: COLLABORATOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: ricab
body: The calls to `restrict_permissions`, which are now replaced, used to take ownership of the files (beside setting permissions). I wonder if you really meant to skip that and, if so, why. Do we no longer need it, or are we doing the same somewhere else?
created_at: 2025-03-06T12:29:38Z
author_association: COLLABORATOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: Putting it back in a later PR would be good
created_at: 2025-03-06T14:08:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: Hi, I have done a quick prototype of the `MP_PLATFORM.setup_permission_inheritance(true);` based implementation which is the last two commits. It is simpler than the original one where we set permission for each sub-directory one by one. With the new one, we only set the default permission of file/folder creation to owner only and overrides the permission of storage location, the certificates folder, and the certificate files. We get the desired permission for root certificate by this way as well. However, there is only one problem. It only works for newly created directory and files, not existing ones from the users. 
created_at: 2025-03-06T14:10:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: Ok, now I get the question. That is a very good point got brought up. I am also confused the fact that why we need it in the first place. Were all files and folders in the storage location owned by root user already? @Sploder12 Maybe you can share some context? 
created_at: 2025-03-06T14:14:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: tests/test_client_common.cpp
user: login: georgeliao
body: But that will be comment of code transitioning, the code might evolve later and the connotation will be lost.  I would say just leave it like this. As long as we know we do have temporary folder based unit tests, then we should be able to understand the code.   
created_at: 2025-03-06T15:04:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,14 +192,15 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
                 std::make_unique<DefaultVMBlueprintProvider>(url_downloader.get(), cache_directory, manifest_ttl);
     }
 
-    // restrict permissions for all existing files and folders
+    // restrict permissions for all existing files and folders in cache directory, the data directory opens execute
+    // permission for other users, the sub-directories of it will have a granular permissions setting
     if (!storage_path.isEmpty())
     {
-        MP_PERMISSIONS.restrict_permissions(storage_path.toStdU16String());
+        MP_PLATFORM.set_permissions(storage_path.toStdU16String(), fs::perms::owner_all | 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::others_exec);
path: src/daemon/daemon_config.cpp
user: login: Sploder12
body: If multipassd created the files and folders, they'd be owned by the root user already. So it's usually redundant and probably unneeded
created_at: 2025-03-06T15:15:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,7 +109,7 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
     auto multiplexing_logger = std::make_shared<mpl::MultiplexingLogger>(std::move(logger));
     mpl::set_logger(multiplexing_logger);
 
-    MP_PLATFORM.setup_permission_inheritance();
+    MP_PLATFORM.setup_permission_inheritance(false);
path: src/daemon/daemon_config.cpp
user: login: georgeliao
body: @ricab the new `daemon_config.cpp` code change is ready for review. Another concern of yours is whether the certificate permission will overwrite if the certificate already exists. The answer is no based on our current code. However, we are still fine with the migration because this PR guarantees the user's certificate recreation due to the missing root certificate. 
created_at: 2025-03-06T17:07:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,6 +280,9 @@ class X509Cert
 
         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::others_read);
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: I would add `group_all` just to be safe. Otherwise, a user who happened to be in the group but wasn't root would be unable to read, I think.

The same reasoning would apply in all other cases where you're giving others permissions.
created_at: 2025-03-07T11:18:58Z
author_association: COLLABORATOR
diff_hunk: @@ -280,6 +280,9 @@ class X509Cert
 
         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::others_read);
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: > I would add group_all just to be safe. Otherwise, a user who happened to be in the group but wasn't root would be unable to read, I think.

I did not get this part. Whether the user is in the group or not, he is still the other user and `std::filesystem::perms::others_read` guarantees that he can read. 
created_at: 2025-03-07T11:29:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,6 +280,9 @@ class X509Cert
 
         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::others_read);
path: src/cert/ssl_cert_provider.cpp
user: login: georgeliao
body: After looking into some literature, I think I have a misconception and the group read permission can be added. 
created_at: 2025-03-07T12:09:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,6 +312,13 @@ mp::SSLCertProvider::KeyCertificatePair make_cert_key_pair(const QDir& cert_dir,
         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))
         {
+            // In cases where the root certificate exists within the data folder (such as in Snap or Windows
+            // environments), its file permissions is unintentionally overwritten due to recursive permission
+            // settings applied to the entire folder. To ensure the client process has access, we need to explicitly
+            // reset the file permissions on the root certificate.
path: src/cert/ssl_cert_provider.cpp
user: login: ricab
body: Hmm I don't think we need all these comments. They are valuable for the PR, not so much for future readers of the code IMO. I mean, permissions are no longer "unintentionally overwritten" once this is merged, right? How about something simpler, like

```suggestion
            // Unlike other daemon files, the root certificate needs to be accessible by everyone
```
created_at: 2025-03-27T20:29:28Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-01-31T11:11:19Z
body: ## [Codecov](https://app.codecov.io/gh/canonical/multipass/pull/3909?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) Report
Attention: Patch coverage is `93.38235%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 89.33%. Comparing base [(`23e6620`)](https://app.codecov.io/gh/canonical/multipass/commit/23e6620b205c9c4f2bfa497757e92a01f9e86a92?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) to head [(`6c97765`)](https://app.codecov.io/gh/canonical/multipass/commit/6c97765c86e0a09340b16ae28943c9b9104c4c3e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical).
> Report is 3520 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/canonical/multipass/pull/3909?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) | Patch % | Lines |
|---|---|---|
| [src/cert/ssl\_cert\_provider.cpp](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&filepath=src%2Fcert%2Fssl_cert_provider.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical#diff-c3JjL2NlcnQvc3NsX2NlcnRfcHJvdmlkZXIuY3Bw) | 95.68% | [5 Missing :warning: ](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) |
| [src/daemon/daemon\_config.cpp](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&filepath=src%2Fdaemon%2Fdaemon_config.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical#diff-c3JjL2RhZW1vbi9kYWVtb25fY29uZmlnLmNwcA==) | 57.14% | [3 Missing :warning: ](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) |
| [src/utils/utils.cpp](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&filepath=src%2Futils%2Futils.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical#diff-c3JjL3V0aWxzL3V0aWxzLmNwcA==) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/canonical/multipass/pull/3909?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3909      +/-   ##
==========================================
+ Coverage   89.26%   89.33%   +0.07%     
==========================================
  Files         260      260              
  Lines       14693    14735      +42     
==========================================
+ Hits        13116    13164      +48     
+ Misses       1577     1571       -6     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/canonical/multipass/pull/3909?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=canonical).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: georgeliao
created_at: 2025-02-11T15:48:53Z
body: > `E0211 14:23:49.406977684   36809 ssl_transport_security.cc:1316]       Handshake failed with fatal error SSL_ERROR_SSL: error:0A000086:SSL routines::certificate verify failed.`

About this root certificate is unsync with server certificate issue in development environment, maybe we can add a check at [here](https://github.com/canonical/multipass/blob/a64cdab881abc7591fbeb9a12fe1f459779b11cf/src/cert/ssl_cert_provider.cpp#L291), which checks not only the existence of the certificates but also whether the root certificate is the one who signed the server certificate. If not, everything will be re-created. This check can be done by openssl c-api. However, not sure the juice worth the squeeze. 

> `[error] [client] Caught an unhandled exception: failed to open file '/var/snap/multipass/commondata/multipassd/certificates/multipass_root_cert.pem': No such file or directory(2)`

This is fixed in the latest version of the PR. 
author_association: CONTRIBUTOR
user: login: andrei-toterman
created_at: 2025-02-11T15:53:48Z
body: Yeah, for the dev environment, I don't think it's worth doing the effort. We already have to remove stuff manually, so this is just an extra thing to remove.
author_association: CONTRIBUTOR
user: login: ricab
created_at: 2025-02-12T11:45:49Z
body: As discussed elsewhere, let's have the GUI also adhere to the new scheme, preferably in a separate PR.
author_association: COLLABORATOR
user: login: georgeliao
created_at: 2025-03-03T14:29:30Z
body: @ricab @Sploder12 
Please review this file permission related [part](https://github.com/canonical/multipass-private/compare/1351dd1cd821154ad6b7757b4cc2a487bf7fc47c...d70bb0304d9db6a9c21d02231cd0fc67aaec6033) which is based on our discussion. The current permission hierarchy in the storage location looks like the list below. 

```
drwx-----x 7 root root 4096 mrt  3 15:17 .
drwxr-xr-x 3 root root 4096 mrt  3 13:58 ..
drwx------ 2 root root 4096 mrt  3 13:58 authenticated-certs
drwxr-xr-x 2 root root 4096 mrt  3 13:58 certificates
-rwx------ 1 root root 1528 mrt  3 15:17 multipassd-vm-instances.json
drwxr-xr-x 2 root root 4096 mrt  3 13:58 lxd
drwx------ 2 root root 4096 mrt  3 13:58 network
drwx------ 2 root root 4096 mrt  3 13:58 ssh-keys
drwx------ 3 root root 4096 mrt  3 15:16 vault
drwxr-xr-x 3 root root 4096 mrt  3 15:16 virtualbox
```
`certificates` directory is untouched based on our discussion, and `lxd` and `virtualbox` directories are about to be removed so I also did not bother to set their permission. 

One thing to note is that the current implementation did not set the permissions recursively, which is enough based on the permission mechanism. If we really want the recursive permission, that involves more changes. It includes changing the `restrict_permissions` to accept a `fs::perm` argument in order to interface with the `make_dir` function, changing its name since the role of the function is changed, and adapting a bunch of other things. 
author_association: CONTRIBUTOR
user: login: georgeliao
created_at: 2025-03-10T10:38:38Z
body: Now it is time for a final round functional testing. Please make sure the grpc authentication works (`multipass list` can run and the GUI can connect to daemon) on migration and fresh installation. @Sploder12 can you check this on the linux platform especially the snap version? Maybe also check the directory permissions in this case. @andrei-toterman Can you test this on windows and macos? Also double check the uninstaller removes everything. Thanks. 
author_association: CONTRIBUTOR
user: login: georgeliao
created_at: 2025-03-27T11:31:49Z
body: @Sploder12 can you do another round functional testing on linux just as you did before? I will test the windows and macos part. 
author_association: CONTRIBUTOR
user: login: georgeliao
created_at: 2025-03-27T19:00:56Z
body: @Sploder12

Can you do a review on the last two commits? Basically, the recursive permission setting on the data folder overwrites the root cert permission to be not accessible to client process. The exact behavior is that the server restart causes the client process unable to read the root cert due to permission issue. The last two commits are to fix that. 

By the way, also feel free to review the file permission related code, very little code in `daemon_config.cpp`. The whole idea is calling `restrict_permissions` on the data folder first, and overwrite the permissions of root cert related files and folders. It includes data folder itself, `certificates` sub folder and `multipass_root_cert.pem` file itself. 

The functional testing is the same, includes multipass upgrade, new installation and plus the server restart. 
author_association: CONTRIBUTOR
user: login: xmkg
created_at: 2025-03-28T17:39:09Z
body: The merge queue pipeline should be fixed once https://github.com/canonical/multipass/pull/4017 lands.
author_association: MEMBER