diff --git a/src/client.cpp b/src/client.cpp
index e1e359b4aad7c9b92e24ba1676ed8537e16b8dcb..b370b0fdce37c3e09c0c6ef111a5718f4b65173a 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -561,11 +561,14 @@ Client::flow_response(const std::string &user,
 //
 
 void
-Client::upload_identity_keys(
+Client::upload_keys(
   const nlohmann::json &identity_keys,
+  const std::map<std::string, nlohmann::json> &one_time_keys,
   std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
 {
         mtx::requests::UploadKeys req;
+        req.one_time_keys = one_time_keys;
+
         req.device_keys.user_id   = user_id().to_string();
         req.device_keys.device_id = device_id();
         req.device_keys.keys.emplace("curve25519:" + device_id(), identity_keys["curve25519"]);
@@ -576,17 +579,29 @@ Client::upload_identity_keys(
 }
 
 void
-Client::upload_one_time_keys(
+Client::upload_identity_keys(
   const nlohmann::json &identity_keys,
   std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
 {
         mtx::requests::UploadKeys req;
         req.device_keys.user_id   = user_id().to_string();
         req.device_keys.device_id = device_id();
+        req.device_keys.keys.emplace("curve25519:" + device_id(), identity_keys["curve25519"]);
+        req.device_keys.keys.emplace("ed25519:" + device_id(), identity_keys["ed25519"]);
 
-        auto obj = identity_keys.at("curve25519");
-        for (auto it = obj.begin(); it != obj.end(); ++it)
-                req.one_time_keys.emplace("curve25519:" + it.key(), it.value());
+        post<mtx::requests::UploadKeys, mtx::responses::UploadKeys>(
+          "/client/r0/keys/upload", req, callback);
+}
+
+void
+Client::upload_one_time_keys(
+  const std::map<std::string, nlohmann::json> &one_time_keys,
+  std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
+{
+        mtx::requests::UploadKeys req;
+        req.device_keys.user_id   = user_id().to_string();
+        req.device_keys.device_id = device_id();
+        req.one_time_keys         = one_time_keys;
 
         post<mtx::requests::UploadKeys, mtx::responses::UploadKeys>(
           "/client/r0/keys/upload", req, callback);
diff --git a/src/client.hpp b/src/client.hpp
index be4f16c1cd5041173faf7283a50645f79b0f29cc..19ea23c353eff60d7f38e12cadd507144ff1d9cb 100644
--- a/src/client.hpp
+++ b/src/client.hpp
@@ -200,6 +200,13 @@ public:
         // Encryption related endpoints.
         //
 
+        //! Upload identity keys & one time keys.
+        // TODO: Replace json with a proper type. API methods shouldn't throw.
+        void upload_keys(
+          const nlohmann::json &identity_keys,
+          const std::map<std::string, nlohmann::json> &one_time_keys,
+          std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> cb);
+
         //! Upload identity keys.
         // TODO: Replace json with a proper type. API methods shouldn't throw.
         void upload_identity_keys(
@@ -209,7 +216,7 @@ public:
         //! Upload one time keys.
         // TODO: Replace json with a proper type. API methods shouldn't throw.
         void upload_one_time_keys(
-          const nlohmann::json &one_time_keys,
+          const std::map<std::string, nlohmann::json> &one_time_keys,
           std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> cb);
 
 private:
diff --git a/src/crypto.cpp b/src/crypto.cpp
index 17c8a003c0434de88dd66b24aff975a974f7bcc0..c8282c3133a25c39c90dc669ba9e74803ae9489c 100644
--- a/src/crypto.cpp
+++ b/src/crypto.cpp
@@ -1,6 +1,8 @@
+#include <iostream>
 #include <sodium.h>
 
 #include "crypto.hpp"
+#include "olm/base64.hh"
 
 using json = nlohmann::json;
 using namespace mtx::client::crypto;
@@ -71,3 +73,37 @@ mtx::client::crypto::one_time_keys(std::shared_ptr<olm::Account> account)
 
         return json::parse(data);
 }
+
+std::string
+mtx::client::crypto::sign_one_time_key(std::shared_ptr<olm::Account> account,
+                                       const std::string &key)
+{
+        json j{{"key", key}};
+        auto str_json = j.dump();
+
+        constexpr std::size_t SIGNATURE_SIZE = 64;
+
+        // Message
+        std::vector<std::uint8_t> tmp(str_json.begin(), str_json.end());
+        std::uint8_t *buf  = &tmp[0];
+        std::size_t nbytes = str_json.size();
+
+        // Signature
+        auto signature_buf = create_buffer(SIGNATURE_SIZE);
+        account->sign(buf, nbytes, signature_buf.get(), SIGNATURE_SIZE);
+
+        auto encoded_buf = create_buffer(SIGNATURE_SIZE);
+        olm::encode_base64(signature_buf.get(), SIGNATURE_SIZE, encoded_buf.get());
+
+        return std::string(encoded_buf.get(), encoded_buf.get() + SIGNATURE_SIZE);
+}
+
+json
+mtx::client::crypto::signed_one_time_key_json(const mtx::identifiers::User &user_id,
+                                              const std::string &device_id,
+                                              const std::string &key,
+                                              const std::string &signature)
+{
+        return json{{"key", key},
+                    {"signatures", {{user_id.to_string(), {{"ed25519:" + device_id, signature}}}}}};
+}
diff --git a/src/crypto.hpp b/src/crypto.hpp
index 4f033cbef531efa245a3dfca5ba250857899945b..68d930b336d52aea8fa83f6a6268329b70bc787a 100644
--- a/src/crypto.hpp
+++ b/src/crypto.hpp
@@ -4,6 +4,7 @@
 #include <memory>
 
 #include <json.hpp>
+#include <mtx/identifiers.hpp>
 #include <olm/account.hh>
 #include <olm/error.h>
 
@@ -49,6 +50,16 @@ one_time_keys(std::shared_ptr<olm::Account> user);
 std::unique_ptr<uint8_t[]>
 create_buffer(std::size_t nbytes);
 
+//! Sign the given one time keys.
+std::string
+sign_one_time_key(std::shared_ptr<olm::Account> account, const std::string &key);
+
+//! Generate the json structure for the signed one time key.
+nlohmann::json
+signed_one_time_key_json(const mtx::identifiers::User &user_id,
+                         const std::string &device_id,
+                         const std::string &key,
+                         const std::string &signature);
 } // namespace crypto
 } // namespace client
 } // namespace mtx
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index 9af76e64ae61c0916cf07d3ffd5d3e1394b9b89c..10573c8b6131dcd2672e2c716bbb192fe3e7199e 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -80,17 +80,99 @@ TEST(Encryption, UploadOneTimeKeys)
         while (alice->access_token().empty())
                 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 
-        auto number_of_keys = mtx::client::crypto::generate_one_time_keys(olm_account, 5);
-        EXPECT_EQ(number_of_keys, 5);
+        auto nkeys = mtx::client::crypto::generate_one_time_keys(olm_account, 5);
+        EXPECT_EQ(nkeys, 5);
 
         auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
 
-        alice->upload_one_time_keys(one_time_keys,
-                                    [](const mtx::responses::UploadKeys &res, ErrType err) {
-                                            check_error(err);
-                                            EXPECT_EQ(res.one_time_key_counts.size(), 1);
-                                            EXPECT_EQ(res.one_time_key_counts.at("curve25519"), 5);
-                                    });
+        // Create the proper structure for uploading.
+        std::map<std::string, json> keys;
+
+        auto obj = one_time_keys.at("curve25519");
+        for (auto it = obj.begin(); it != obj.end(); ++it)
+                keys["curve25519:" + it.key()] = it.value();
+
+        alice->upload_one_time_keys(keys, [](const mtx::responses::UploadKeys &res, ErrType err) {
+                check_error(err);
+                EXPECT_EQ(res.one_time_key_counts.size(), 1);
+                EXPECT_EQ(res.one_time_key_counts.at("curve25519"), 5);
+        });
+
+        alice->close();
+}
+
+TEST(Encryption, UploadSignedOneTimeKeys)
+{
+        auto alice       = std::make_shared<Client>("localhost");
+        auto olm_account = mtx::client::crypto::olm_new_account();
+
+        alice->login(
+          "alice", "secret", [](const mtx::responses::Login &, ErrType err) { check_error(err); });
+
+        while (alice->access_token().empty())
+                std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+        auto nkeys = mtx::client::crypto::generate_one_time_keys(olm_account, 5);
+        EXPECT_EQ(nkeys, 5);
+
+        auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
+
+        // Create the proper structure for uploading.
+        std::map<std::string, json> signed_keys;
+
+        auto obj = one_time_keys.at("curve25519");
+        for (auto it = obj.begin(); it != obj.end(); ++it) {
+                auto sig = mtx::client::crypto::sign_one_time_key(olm_account, it.value());
+
+                signed_keys["signed_curve25519:" + it.key()] =
+                  mtx::client::crypto::signed_one_time_key_json(
+                    alice->user_id(), alice->device_id(), it.value(), sig);
+        }
+
+        alice->upload_one_time_keys(
+          signed_keys, [nkeys](const mtx::responses::UploadKeys &res, ErrType err) {
+                  check_error(err);
+                  EXPECT_EQ(res.one_time_key_counts.size(), 1);
+                  EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), nkeys);
+          });
+
+        alice->close();
+}
+
+TEST(Encryption, UploadKeys)
+{
+        auto alice       = std::make_shared<Client>("localhost");
+        auto olm_account = mtx::client::crypto::olm_new_account();
+
+        alice->login(
+          "alice", "secret", [](const mtx::responses::Login &, ErrType err) { check_error(err); });
+
+        while (alice->access_token().empty())
+                std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+        json identity_keys = mtx::client::crypto::identity_keys(olm_account);
+
+        mtx::client::crypto::generate_one_time_keys(olm_account, 1);
+        auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
+
+        // Create the proper structure for uploading.
+        std::map<std::string, json> signed_keys;
+
+        auto obj = one_time_keys.at("curve25519");
+        for (auto it = obj.begin(); it != obj.end(); ++it) {
+                auto sig = mtx::client::crypto::sign_one_time_key(olm_account, it.value());
+
+                signed_keys["signed_curve25519:" + it.key()] =
+                  mtx::client::crypto::signed_one_time_key_json(
+                    alice->user_id(), alice->device_id(), it.value(), sig);
+        }
+
+        alice->upload_keys(
+          identity_keys, signed_keys, [](const mtx::responses::UploadKeys &res, ErrType err) {
+                  check_error(err);
+                  EXPECT_EQ(res.one_time_key_counts.size(), 1);
+                  EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), 1);
+          });
 
         alice->close();
 }