From d2d18bdedc1c83a86aee9d44c7a3b548cd8054d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Thu, 6 Jan 2022 12:02:21 -0500
Subject: [PATCH] swarm: handle device's expiration

If a device expires, a migration will regenerate the device's
certificate. In this case, the device certificate in swarms must
be updated.
So, the idea of this patch is to verify that the current certificate
in the repository for this device is still correct. If the device or
the member got invalidated, it tries to replace the current certificate
(updated via the migration). So that the new commits will still be valid.

Change-Id: I75b19b0edbb5601a758a73a4c4a44678d77295e1
GitLab: #684
---
 src/jamidht/conversationrepository.cpp      | 224 ++++++++++++++------
 src/jamidht/jamiaccount.cpp                 |  12 ++
 src/jamidht/jamiaccount.h                   |   2 +
 test/unitTest/account_archive/migration.cpp | 138 ++++++++++++
 test/unitTest/conversation/conversation.cpp |   3 +-
 5 files changed, 319 insertions(+), 60 deletions(-)

diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index 5e0e702e24..7e76bf9291 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -77,9 +77,9 @@ public:
     std::string createMergeCommit(git_index* index, const std::string& wanted_ref);
 
     bool validCommits(const std::vector<ConversationCommit>& commits) const;
-    bool checkOnlyDeviceCertificate(const std::string& userDevice,
-                                    const std::string& commitId,
-                                    const std::string& parentId) const;
+    bool checkValidUserDiff(const std::string& userDevice,
+                            const std::string& commitId,
+                            const std::string& parentId) const;
     bool checkVote(const std::string& userDevice,
                    const std::string& commitId,
                    const std::string& parentId) const;
@@ -105,6 +105,8 @@ public:
 
     bool add(const std::string& path);
     void addUserDevice();
+    // Verify that the device in the repository is still valid
+    bool validateDevice();
     std::string commit(const std::string& msg);
     ConversationMode mode() const;
 
@@ -167,6 +169,40 @@ public:
 
     // Permissions
     MemberRole updateProfilePermLvl_ {MemberRole::ADMIN};
+
+    /**
+     * Retrieve the user related to a device
+     * @note deviceToUri_ is used to cache result and avoid to
+     * always load the certificate
+     */
+    std::string uriFromDevice(const std::string& deviceId) const
+    {
+        std::lock_guard<std::mutex> lk(deviceToUriMtx_);
+        auto it = deviceToUri_.find(deviceId);
+        if (it != deviceToUri_.end())
+            return it->second;
+
+        auto repo = repository();
+        if (!repo)
+            return {};
+
+        auto cert = tls::CertificateStore::instance().getCertificate(deviceId);
+        if (!cert || !cert->issuer) {
+            // Not pinned, so load certificate from repo
+            std::string deviceFile = git_repository_workdir(repo.get())
+                                     + fmt::format("devices/{}.crt", deviceId);
+            cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(deviceFile));
+            if (!cert)
+                return {};
+        }
+        auto issuerUid = cert->issuer ? cert->issuer->getId().toString() : cert->getIssuerUID();
+        if (issuerUid.empty())
+            return {};
+        deviceToUri_.insert({deviceId, issuerUid});
+        return issuerUid;
+    }
+    mutable std::mutex deviceToUriMtx_;
+    mutable std::map<std::string, std::string> deviceToUri_;
 };
 
 /////////////////////////////////////////////////////////////////////////////////
@@ -663,37 +699,59 @@ ConversationRepository::Impl::add(const std::string& path)
 }
 
 bool
-ConversationRepository::Impl::checkOnlyDeviceCertificate(const std::string& userDevice,
-                                                         const std::string& commitId,
-                                                         const std::string& parentId) const
+ConversationRepository::Impl::checkValidUserDiff(const std::string& userDevice,
+                                                 const std::string& commitId,
+                                                 const std::string& parentId) const
 {
-    // Here, we check that a file device is modified or not.
-    auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
-    if (changedFiles.size() == 0)
-        return true;
-    else if (changedFiles.size() > 1)
-        return false;
-    // If modified, it's the first commit of a device, we check
-    // that the file wasn't there previously
-    // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
-    std::string deviceFile = std::string("devices") + "/" + userDevice + ".crt";
-    if (changedFiles[0] != deviceFile) {
-        return false;
-    }
-
     // Retrieve tree for recent commit
     auto repo = repository();
     if (!repo)
         return false;
     auto treeNew = treeAtCommit(repo.get(), commitId);
-    auto treeOld = treeAtCommit(repo.get(), parentId);
-    if (not treeNew or not treeOld)
+    if (not treeNew)
         return false;
+    std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
     if (!fileAtTree(deviceFile, treeNew)) {
         JAMI_ERR("%s announced but not found", deviceFile.c_str());
         return false;
     }
-    return !fileAtTree(deviceFile, treeOld);
+
+    // Here, we check that a file device is modified or not.
+    auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
+    if (changedFiles.size() == 0)
+        return true;
+
+    // If a certificate is modified (in the changedFiles), it MUST be a certificate from the user
+    // Retrieve userUri
+    auto userUri = uriFromDevice(userDevice);
+    if (userUri.empty())
+        return false;
+
+    std::string adminsFile = fmt::format("admins/{}.crt", userUri);
+    std::string membersFile = fmt::format("members/{}.crt", userUri);
+    for (const auto& changedFile : changedFiles) {
+        if (changedFile == adminsFile || changedFile == membersFile) {
+            // In this case, we should verify it's not added (normal commit, not a member change)
+            // but only updated
+            auto treeOld = treeAtCommit(repo.get(), parentId);
+            if (not treeNew or not treeOld)
+                return false;
+
+            if (!fileAtTree(changedFile, treeOld)) {
+                JAMI_ERR("Invalid file modified: %s", changedFile.c_str());
+                return false;
+            }
+        } else if (changedFile == deviceFile) {
+            // In this case, device is added or modified (certificate expiration)
+            continue;
+        } else {
+            // Invalid file detected
+            JAMI_ERR("Invalid add file detected: %s %u", changedFile.c_str(), (int) *mode_);
+            return false;
+        }
+    }
+
+    return true;
 }
 
 bool
@@ -745,12 +803,9 @@ ConversationRepository::Impl::checkVote(const std::string& userDevice,
         }
     }
 
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert || !cert->issuer)
-        return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = uriFromDevice(userDevice);
     // Check that voter is admin
-    auto adminFile = std::string("admins") + "/" + userUri + ".crt";
+    auto adminFile = fmt::format("admins/{}.crt", userUri);
 
     if (!fileAtTree(adminFile, treeOld)) {
         JAMI_ERR("Vote from non admin: %s", userUri.c_str());
@@ -829,11 +884,10 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
                                             const std::string& commitId,
                                             const std::string& parentId) const
 {
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
+    auto userUri = uriFromDevice(userDevice);
     auto repo = repository();
-    if (not cert or not cert->issuer or not repo)
+    if (userUri.empty() or not repo)
         return false;
-    auto userUri = cert->issuer->getId().toString();
 
     std::string repoPath = git_repository_workdir(repo.get());
     if (mode() == ConversationMode::ONE_TO_ONE) {
@@ -915,24 +969,22 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
                                               const std::string& commitId,
                                               const std::string& parentId) const
 {
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert || !cert->issuer)
+    auto userUri = uriFromDevice(userDevice);
+    if (userUri.empty())
         return false;
-    auto userUri = cert->issuer->getId().toString();
     // Check no other files changed
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
     std::size_t wantedChanged = 3;
     if (changedFiles.size() != wantedChanged)
         return false;
 
-    auto invitedFile = std::string("invited") + "/" + uriMember;
-    auto membersFile = std::string("members") + "/" + uriMember + ".crt";
-    auto deviceFile = std::string("devices") + "/" + userDevice + ".crt";
+    auto invitedFile = fmt::format("invited/{}", uriMember);
+    auto membersFile = fmt::format("members/{}.crt", uriMember);
+    auto deviceFile = fmt::format("devices/{}.crt", userDevice);
 
     // Retrieve tree for commits
     auto repo = repository();
-    if (!repo)
-        return false;
+    assert(repo);
     auto treeNew = treeAtCommit(repo.get(), commitId);
     auto treeOld = treeAtCommit(repo.get(), parentId);
     if (not treeNew or not treeOld)
@@ -977,10 +1029,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
                                                const std::string& commitId,
                                                const std::string& parentId) const
 {
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert || !cert->issuer)
-        return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = uriFromDevice(userDevice);
     auto removeSelf = userUri == uriMember;
 
     // Retrieve tree for recent commit
@@ -1043,10 +1092,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
             JAMI_ERR("device not found added (%s)", deviceFile.c_str());
             return false;
         }
-        cert = tls::CertificateStore::instance().getCertificate(deviceUri);
-        if (!cert || !cert->issuer)
-            return false;
-        if (uriMember != cert->issuer->getId().toString()
+        if (uriMember != uriFromDevice(deviceUri)
             and uriMember != deviceUri /* If device is removed */) {
             JAMI_ERR("device removed but not for removed user (%s)", deviceFile.c_str());
             return false;
@@ -1058,7 +1104,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
     }
 
     // If not for self check that user device is admin
-    adminFile = std::string("admins") + "/" + userUri + ".crt";
+    adminFile = fmt::format("admins/{}.crt", userUri);
     if (!fileAtTree(adminFile, treeOld)) {
         JAMI_ERR("admin file (%s) not found", adminFile.c_str());
         return false;
@@ -1094,10 +1140,7 @@ ConversationRepository::Impl::checkValidProfileUpdate(const std::string& userDev
                                                       const std::string& commitId,
                                                       const std::string& parentId) const
 {
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert || !cert->issuer)
-        return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = uriFromDevice(userDevice);
     auto valid = false;
     {
         std::lock_guard<std::mutex> lk(membersMtx_);
@@ -1224,12 +1267,7 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
     auto account = account_.lock();
     if (!account)
         return false;
-    auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert || !cert->issuer) {
-        JAMI_ERR("Cannot find certificate for %s", userDevice.c_str());
-        return false;
-    }
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = uriFromDevice(userDevice);
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
 
@@ -1280,9 +1318,77 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
     return hasDevice && hasAdmin;
 }
 
+bool
+ConversationRepository::Impl::validateDevice()
+{
+    auto repo = repository();
+    auto account = account_.lock();
+    if (!account || !repo) {
+        JAMI_WARN("Invalid repository detected");
+        return false;
+    }
+    auto path = fmt::format("devices/{}.crt", account->currentDeviceId());
+    std::string devicePath = git_repository_workdir(repo.get()) + path;
+    if (!fileutils::isFile(devicePath)) {
+        JAMI_WARN("Couldn't find file %s", devicePath.c_str());
+        return false;
+    }
+
+    auto deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
+
+    if (!account->isValidAccountDevice(deviceCert)) {
+        JAMI_WARN("Device's certificate is not valid anymore. Trying to replace certificate with "
+                  "current one.");
+        // Replace certificate with current cert
+        auto cert = account->identity().second;
+        if (!account->isValidAccountDevice(*cert)) {
+            JAMI_ERR("Current device's certificate is invalid. A migration is needed");
+            return false;
+        }
+        auto file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary);
+        if (!file.is_open()) {
+            JAMI_ERR("Could not write data to %s", devicePath.c_str());
+            return false;
+        }
+        file << cert->toString(false);
+        file.close();
+        if (!add(path)) {
+            JAMI_WARN("Couldn't add file %s", devicePath.c_str());
+            return false;
+        }
+
+        auto parentCert = cert->issuer;
+        auto adminPath = fmt::format("admins/{}.crt", account->getUsername());
+        auto memberPath = fmt::format("members/{}.crt", account->getUsername());
+        std::string fullpath = git_repository_workdir(repo.get());
+        if (fileutils::isFile(git_repository_workdir(repo.get()) + adminPath))
+            path = adminPath;
+        else if (fileutils::isFile(git_repository_workdir(repo.get()) + memberPath))
+            path = memberPath;
+        fullpath += path;
+        if (fileutils::isFile(fullpath)) {
+            auto file = fileutils::ofstream(fullpath, std::ios::trunc | std::ios::binary);
+            if (!file.is_open()) {
+                JAMI_ERR("Could not write data to %s", path.c_str());
+                return false;
+            }
+            file << parentCert->toString(true);
+            file.close();
+            if (!add(path)) {
+                JAMI_WARN("Couldn't add file %s", path.c_str());
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
 std::string
 ConversationRepository::Impl::commit(const std::string& msg)
 {
+    if (!validateDevice())
+        return {};
     auto account = account_.lock();
     if (!account)
         return {};
@@ -2168,7 +2274,7 @@ ConversationRepository::Impl::validCommits(
                 // Note: accept all mimetype here, as we can have new mimetypes
                 // Just avoid to add weird files
                 // Check that no weird file is added outside device cert nor removed
-                if (!checkOnlyDeviceCertificate(userDevice, commit.id, commit.parents[0])) {
+                if (!checkValidUserDiff(userDevice, commit.id, commit.parents[0])) {
                     JAMI_WARN("Malformed %s commit %s. Please check you use the latest "
                               "version of Jami, or that your contact is not doing unwanted stuff.",
                               type.c_str(),
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 93887219b4..79bf286fdf 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -1062,6 +1062,18 @@ JamiAccount::unlinkConversations(const std::set<std::string>& removed)
     }
 }
 
+bool
+JamiAccount::isValidAccountDevice(const dht::crypto::Certificate& cert) const
+{
+    if (accountManager_) {
+        if (auto info = accountManager_->getInfo()) {
+            if (info->contacts)
+                return info->contacts->isValidAccountDevice(cert).isValid();
+        }
+    }
+    return false;
+}
+
 bool
 JamiAccount::revokeDevice(const std::string& password, const std::string& device)
 {
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index a24f77e9c5..3b6e620d29 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -604,6 +604,8 @@ public:
      */
     void unlinkConversations(const std::set<std::string>& removedConv);
 
+    bool isValidAccountDevice(const dht::crypto::Certificate& cert) const;
+
 private:
     NON_COPYABLE(JamiAccount);
 
diff --git a/test/unitTest/account_archive/migration.cpp b/test/unitTest/account_archive/migration.cpp
index 7cb5098db1..d1a285d79a 100644
--- a/test/unitTest/account_archive/migration.cpp
+++ b/test/unitTest/account_archive/migration.cpp
@@ -64,10 +64,12 @@ public:
 private:
     void testLoadExpiredAccount();
     void testMigrationAfterRevokation();
+    void testExpiredDeviceInSwarm();
 
     CPPUNIT_TEST_SUITE(MigrationTest);
     CPPUNIT_TEST(testLoadExpiredAccount);
     CPPUNIT_TEST(testMigrationAfterRevokation);
+    CPPUNIT_TEST(testExpiredDeviceInSwarm);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -223,6 +225,142 @@ MigrationTest::testMigrationAfterRevokation()
     CPPUNIT_ASSERT(bob2Device != bob2Account->currentDeviceId());
 }
 
+void
+MigrationTest::testExpiredDeviceInSwarm()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceDevice = std::string(aliceAccount->currentDeviceId());
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageBobReceived = 0, messageAliceReceived = 0;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == bobId) {
+                messageBobReceived += 1;
+            } else {
+                messageAliceReceived += 1;
+            }
+            cv.notify_one();
+        }));
+    bool requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    auto aliceMigrated = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::MigrationEnded>(
+        [&](const std::string& accountId, const std::string& state) {
+            if (accountId == aliceId && state == "SUCCESS") {
+                aliceMigrated = true;
+            }
+            cv.notify_one();
+        }));
+    bool aliceStopped = false, aliceAnnounced = false, aliceRegistered = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = aliceAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "UNREGISTERED")
+                    aliceStopped = true;
+                else if (daemonStatus == "REGISTERED")
+                    aliceRegistered = true;
+                auto announced = details[DRing::Account::VolatileProperties::DEVICE_ANNOUNCED];
+                if (announced == "true")
+                    aliceAnnounced = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->setValidity("", {}, 90));
+    auto now = std::chrono::system_clock::now();
+    aliceRegistered = false;
+    aliceAccount->forceReloadAccount();
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]() { return aliceRegistered; }));
+    CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
+
+    aliceStopped = false;
+    Manager::instance().sendRegister(aliceId, false);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&]() { return aliceStopped; }));
+    aliceAnnounced = false;
+    Manager::instance().sendRegister(aliceId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return aliceAnnounced; }));
+
+    CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
+
+    // Create conversation
+    auto convId = DRing::startConversation(aliceId);
+
+    DRing::addConversationMember(aliceId, convId, bobUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return requestReceived; }));
+
+    messageAliceReceived = 0;
+    DRing::acceptConversationRequest(bobId, convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return conversationReady; }));
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Wait that alice sees Bob
+    cv.wait_for(lk, 20s, [&]() { return messageAliceReceived == 1; });
+
+    messageBobReceived = 0;
+    DRing::sendMessage(aliceId, convId, "hi"s, "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageBobReceived == 1; }));
+
+    // Wait for certificate to expire
+    std::this_thread::sleep_until(now + 100s);
+    // Check migration is triggered and expiration updated
+    aliceAnnounced = false;
+    aliceAccount->forceReloadAccount();
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return aliceAnnounced; }));
+    CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
+
+    // check that certificate in conversation is expired
+    auto devicePath = fmt::format("{}/devices/{}.crt", repoPath, aliceAccount->currentDeviceId());
+    CPPUNIT_ASSERT(fileutils::isFile(devicePath));
+    auto cert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
+    now = std::chrono::system_clock::now();
+    CPPUNIT_ASSERT(cert.getExpiration() < now);
+
+    // Resend a new message
+    messageBobReceived = 0;
+    DRing::sendMessage(aliceId, convId, "hi again"s, "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageBobReceived == 1; }));
+    messageAliceReceived = 0;
+    DRing::sendMessage(bobId, convId, "hi!"s, "");
+    CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageAliceReceived == 1; }));
+
+    // check that certificate in conversation is updated
+    CPPUNIT_ASSERT(fileutils::isFile(devicePath));
+    cert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
+    now = std::chrono::system_clock::now();
+    CPPUNIT_ASSERT(cert.getExpiration() > now);
+
+    // Check same device as begining
+    CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
+}
+
 } // namespace test
 } // namespace jami
 
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index 570afa1661..dccdd208e8 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -1084,7 +1084,8 @@ ConversationTest::createFakeConversation(std::shared_ptr<JamiAccount> account)
     }
 
     // /devices
-    std::string devicePath = devicesPath + DIR_SEPARATOR_STR + cert->getId().toString() + ".crt";
+    std::string devicePath = devicesPath + DIR_SEPARATOR_STR + cert->getLongId().toString()
+                             + ".crt";
     file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary);
     if (!file.is_open()) {
         JAMI_ERR("Could not write data to %s", devicePath.c_str());
-- 
GitLab