diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp
index 0177e43d901ec865085ccca46a2592d823f75aef..9c392958faea1047f9a6af1db7a628f0b9889f67 100644
--- a/include/mtx/requests.hpp
+++ b/include/mtx/requests.hpp
@@ -281,6 +281,22 @@ struct KeySignaturesUpload
 void
 to_json(json &obj, const KeySignaturesUpload &req);
 
+//! Upload cross signing keys
+struct DeviceSigningUpload
+{
+    //! Optional. The user's master key.
+    std::optional<mtx::crypto::CrossSigningKeys> master_key;
+    //! Optional. The user's self-signing key. Must be signed by the accompanying master key, or by
+    //! the user's most recently uploaded master key if no master key is included in the request.
+    std::optional<mtx::crypto::CrossSigningKeys> self_signing_key;
+    //! Optional. The user's user-signing key. Must be signed by the accompanying master key, or by
+    //! the user's most recently uploaded master key if no master key is included in the request.
+    std::optional<mtx::crypto::CrossSigningKeys> user_signing_key;
+};
+
+void
+to_json(json &obj, const DeviceSigningUpload &req);
+
 struct PusherData
 {
     //! Required if `kind` is http. The URL to use to send notifications to.
diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp
index 2542cae12bfa2774437993ef3e569c1ff652bed8..eb4ff14e49e7364e286d2f2b044492095bbb6fb0 100644
--- a/include/mtxclient/crypto/client.hpp
+++ b/include/mtxclient/crypto/client.hpp
@@ -188,15 +188,22 @@ struct PkSigning
 {
     //! Construct from base64 key
     static PkSigning from_seed(std::string seed);
+    //! construct a new random key
+    static PkSigning new_key();
+
+    //! sign an arbitrary message
     std::string sign(const std::string &message);
 
     //! base64 public key
     std::string public_key() const { return public_key_; }
+    //! base64 private key (seed)
+    std::string seed() const { return seed_; }
 
 private:
     PkSigning() {}
     std::unique_ptr<OlmPkSigning, OlmDeleter> signing;
-    std::string public_key_;
+    std::string public_key_; // base64
+    std::string seed_;       // base64
 };
 
 //! Client for all the cryptography related functionality like olm accounts, session keys
@@ -216,6 +223,35 @@ public:
     //! A signed set of one time keys indexed by `<algorithm>:<key_id>`.
     using SignedOneTimeKeys = std::map<std::string, requests::SignedOneTimeKey>;
 
+    //! Data needed for bootstrapping crosssigning
+    struct CrossSigningSetup
+    {
+        //! The public key objects, signed and ready for upload.
+        CrossSigningKeys master_key, user_signing_key, self_signing_key;
+        //! The private keys to store in SSSS
+        std::string private_master_key, private_user_signing_key,
+          private_self_signing_key; // base64
+    };
+
+    //! Data needed to setup the online key backup
+    struct OnlineKeyBackupSetup
+    {
+        //! private key to decrypt sessions with.
+        mtx::crypto::BinaryBuf privateKey;
+        //! The backup version data including auth data to be sent to the server.
+        mtx::responses::backup::BackupVersion backupVersion;
+    };
+    //! Data needed to setup SSSS
+    struct SSSSSetup
+    {
+        //! Key to encrypt/decrypt secrets with.
+        mtx::crypto::BinaryBuf privateKey;
+        //! The key description to be stored in account data.
+        mtx::secret_storage::AesHmacSha2KeyDescription keyDescription;
+        //! The name of this key.
+        std::string key_name;
+    };
+
     //! Set the id of this device.
     void set_device_id(std::string device_id) { device_id_ = std::move(device_id); }
     //! Set the id of this user.
@@ -259,6 +295,15 @@ public:
     //! Prepare an empty /keys/upload request.
     mtx::requests::UploadKeys create_upload_keys_request();
 
+    //! Create the cross-signing keys (including signatures). Needs to be uploaded to the server
+    //! after this.
+    std::optional<CrossSigningSetup> create_crosssigning_keys();
+
+    //! Create a new online key backup. Needs to be uploaded to the server after this.
+    std::optional<OnlineKeyBackupSetup> create_online_key_backup(const std::string &masterKey);
+    //! Create a new SSSS storage key. Should be uploaded to account_data. The password is optional.
+    static std::optional<SSSSSetup> create_ssss_key(const std::string &password = "");
+
     //! Decrypt a message using megolm.
     GroupPlaintext decrypt_group_message(OlmInboundGroupSession *session,
                                          const std::string &message,
diff --git a/include/mtxclient/crypto/utils.hpp b/include/mtxclient/crypto/utils.hpp
index 7f0bca7d73722eece1df9df7e4ef2c9c105874a6..33f519f8e64a90ff1e8ade5d03eab820dccee68a 100644
--- a/include/mtxclient/crypto/utils.hpp
+++ b/include/mtxclient/crypto/utils.hpp
@@ -53,6 +53,14 @@ to_string(const BinaryBuf &buf)
     return std::string(reinterpret_cast<const char *>(buf.data()), buf.size());
 }
 
+//! Sets bit 63 to 0 to be compatible with other AES implementations.
+BinaryBuf
+compatible_iv(BinaryBuf incompatible_iv);
+
+//! encodes a recovery key in base58 with parity and version tag,
+std::string
+key_to_recoverykey(const BinaryBuf &key);
+
 //! Simple wrapper around the OpenSSL PKCS5_PBKDF2_HMAC function
 BinaryBuf
 PBKDF2_HMAC_SHA_512(const std::string pass,
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 94fae087d57268fd3b0c8a1448517b9ebb89d0a9..4a4dba84383a24b686965b76773a33762de784af 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -117,6 +117,30 @@ template<class Response>
 using HeadersCallback    = std::function<void(const Response &, HeaderFields, RequestErr)>;
 using TypeErasedCallback = std::function<void(HeaderFields, const std::string_view &, int, int)>;
 
+//! A helper to handle user interactive authentication. This will cache the request and call the
+//! prompt every time there is a new stage. Advance the flow by calling next().
+class UIAHandler
+{
+public:
+    //! The callback for when a new UIA stage needs to be completed
+    using UIAPrompt =
+      std::function<void(const UIAHandler &, const user_interactive::Unauthorized &)>;
+
+    //! Create a new UIA handler. Pass a callback for when a new stage needs to be completed.
+    UIAHandler(UIAPrompt prompt_)
+      : prompt(std::move(prompt_))
+    {}
+
+    void next(const user_interactive::Auth &auth) const;
+
+private:
+    UIAPrompt prompt;
+
+    std::function<void(const UIAHandler &, const nlohmann::json &)> next_;
+
+    friend class Client;
+};
+
 //! Sync configuration options.
 struct SyncOpts
 {
@@ -242,6 +266,13 @@ public:
                       const user_interactive::Auth &auth,
                       Callback<mtx::responses::Register> cb);
 
+    //! Register with an UIA handler so you don't need to repeat the request manually.
+    //! register failed with 401
+    void registration(const std::string &user,
+                      const std::string &pass,
+                      UIAHandler uia_handler,
+                      Callback<mtx::responses::Register> cb);
+
     //! Check the validity of a registration token
     void registration_token_validity(const std::string token,
                                      Callback<mtx::responses::RegistrationTokenValidity> cb);
@@ -564,6 +595,11 @@ public:
     void keys_signatures_upload(const mtx::requests::KeySignaturesUpload &req,
                                 Callback<mtx::responses::KeySignaturesUpload> cb);
 
+    //! Upload cross signing keys
+    void device_signing_upload(const mtx::requests::DeviceSigningUpload,
+                               UIAHandler uia_handler,
+                               ErrCallback cb);
+
     //! Returns the current devices and identity keys for the given users.
     void query_keys(const mtx::requests::QueryKeys &req, Callback<mtx::responses::QueryKeys> cb);
 
diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp
index 10db860ddba1e91f59844ff179e478cf20cdea54..fe55f43ec1c338ee710435cdc4425f08b2f07999 100644
--- a/lib/crypto/client.cpp
+++ b/lib/crypto/client.cpp
@@ -206,6 +206,92 @@ OlmClient::create_upload_keys_request(const mtx::crypto::OneTimeKeys &one_time_k
     return req;
 }
 
+std::optional<OlmClient::CrossSigningSetup>
+OlmClient::create_crosssigning_keys()
+{
+    auto master       = PkSigning::new_key();
+    auto user_signing = PkSigning::new_key();
+    auto self_signing = PkSigning::new_key();
+
+    CrossSigningSetup setup{};
+    setup.private_master_key       = master.seed();
+    setup.private_user_signing_key = user_signing.seed();
+    setup.private_self_signing_key = self_signing.seed();
+
+    // master key
+    setup.master_key.usage                                  = {"master"};
+    setup.master_key.user_id                                = user_id_;
+    setup.master_key.keys["ed25519:" + master.public_key()] = master.public_key();
+
+    nlohmann::json master_j = setup.master_key;
+    master_j.erase("unsigned");
+    master_j.erase("signatures");
+    setup.master_key.signatures[user_id_]["ed25519:" + master.public_key()] =
+      master.sign(master_j.dump());
+    setup.master_key.signatures[user_id_]["ed25519:" + device_id_] = sign_message(master_j.dump());
+
+    // user_signing_key
+    setup.user_signing_key.usage                                        = {"user_signing"};
+    setup.user_signing_key.user_id                                      = user_id_;
+    setup.user_signing_key.keys["ed25519:" + user_signing.public_key()] = user_signing.public_key();
+
+    nlohmann::json user_signing_j = setup.user_signing_key;
+    user_signing_j.erase("unsigned");
+    user_signing_j.erase("signatures");
+    setup.user_signing_key.signatures[user_id_]["ed25519:" + user_signing.public_key()] =
+      user_signing.sign(user_signing_j.dump());
+    setup.user_signing_key.signatures[user_id_]["ed25519:" + master.public_key()] =
+      master.sign(user_signing_j.dump());
+
+    // self_signing_key
+    setup.self_signing_key.usage                                        = {"self_signing"};
+    setup.self_signing_key.user_id                                      = user_id_;
+    setup.self_signing_key.keys["ed25519:" + self_signing.public_key()] = self_signing.public_key();
+
+    nlohmann::json self_signing_j = setup.self_signing_key;
+    self_signing_j.erase("unsigned");
+    self_signing_j.erase("signatures");
+    setup.self_signing_key.signatures[user_id_]["ed25519:" + self_signing.public_key()] =
+      self_signing.sign(self_signing_j.dump());
+    setup.self_signing_key.signatures[user_id_]["ed25519:" + master.public_key()] =
+      master.sign(self_signing_j.dump());
+
+    return setup;
+}
+
+std::optional<OlmClient::SSSSSetup>
+OlmClient::create_ssss_key(const std::string &password)
+{
+    OlmClient::SSSSSetup setup{};
+
+    if (password.empty()) {
+        setup.privateKey = create_buffer(32);
+    } else {
+        mtx::secret_storage::PBKDF2 pbkdf2{};
+        pbkdf2.algorithm  = "m.pbkdf2";
+        pbkdf2.iterations = 500'000;
+        pbkdf2.bits       = 256; // 32 * 8
+        pbkdf2.salt       = bin2base64(to_string(create_buffer(32)));
+
+        setup.privateKey = mtx::crypto::PBKDF2_HMAC_SHA_512(
+          password, to_binary_buf(pbkdf2.salt), pbkdf2.iterations, pbkdf2.bits / 8);
+        setup.keyDescription.passphrase = pbkdf2;
+    }
+
+    setup.keyDescription.algorithm = "m.secret_storage.v1.aes-hmac-sha2";
+    setup.keyDescription.name = bin2base58(to_string(create_buffer(16))); // create a random name
+    setup.keyDescription.iv   = bin2base64(to_string(compatible_iv(create_buffer(32))));
+
+    auto testKeys = HKDF_SHA256(setup.privateKey, BinaryBuf(32, 0), BinaryBuf{});
+
+    auto encrypted = AES_CTR_256_Encrypt(
+      std::string(32, '\0'), testKeys.aes, to_binary_buf(base642bin(setup.keyDescription.iv)));
+
+    setup.keyDescription.mac = bin2base64(to_string(HMAC_SHA256(testKeys.mac, encrypted)));
+
+    return setup;
+}
+
 OutboundGroupSessionPtr
 OlmClient::init_outbound_group_session()
 {
@@ -561,10 +647,18 @@ SAS::calculate_mac(std::string input_data, std::string info)
     return to_string(output_buffer);
 }
 
+PkSigning
+PkSigning::new_key()
+{
+    auto priv_seed = bin2base64(to_string(create_buffer(olm_pk_signing_seed_length())));
+    return from_seed(priv_seed);
+}
+
 PkSigning
 PkSigning::from_seed(std::string seed)
 {
     PkSigning s{};
+    s.seed_   = seed;
     s.signing = create_olm_object<PkSigningObject>();
 
     auto seed_ = base642bin(seed);
diff --git a/lib/crypto/encoding.cpp b/lib/crypto/encoding.cpp
index 5589a8343914a8c7aa2b0f79e8a488042c378205..727bc669e84cc9f78a9b284a25a3dd897b278e63 100644
--- a/lib/crypto/encoding.cpp
+++ b/lib/crypto/encoding.cpp
@@ -1,5 +1,6 @@
 #include <algorithm>
 #include <array>
+#include <cassert>
 #include <string>
 #include <vector>
 
@@ -60,23 +61,25 @@ encode_base58(const std::array<char, 58> &alphabet, const std::string &input)
     if (input.empty())
         return "";
 
-    std::vector<uint8_t> digits(input.size() * 137 / 100 + 1);
+    std::vector<uint8_t> digits(input.size() * 138 / 100 + 1);
     std::size_t digitslen = 1;
-    for (uint32_t carry : input) {
+    for (uint8_t carry_ : input) {
+        uint32_t carry = static_cast<uint32_t>(carry_);
         for (size_t j = 0; j < digitslen; j++) {
-            carry += (uint32_t)(digits[j]) << 8;
+            carry += (uint32_t)(digits[j]) * 256;
             digits[j] = static_cast<uint8_t>(carry % 58);
             carry /= 58;
         }
         while (carry > 0) {
+            assert(digitslen < digits.size());
             digits[digitslen++] = static_cast<uint8_t>(carry % 58);
             carry /= 58;
         }
     }
-    std::size_t resultlen = 0;
     std::string result(digits.size(), ' ');
 
     // leading zero bytes
+    std::size_t resultlen = 0;
     for (; resultlen < input.length() && input[resultlen] == 0;)
         result[resultlen++] = '1';
 
diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp
index 96ca7a567a39e0cc8b547e0b83f61f7117388694..ad1253f65d1843179a34ce0a74e6f0c2c41d09b6 100644
--- a/lib/crypto/utils.cpp
+++ b/lib/crypto/utils.cpp
@@ -108,6 +108,22 @@ key_from_recoverykey(const std::string &recoverykey,
     return decryptionKey;
 }
 
+std::string
+key_to_recoverykey(const BinaryBuf &key)
+{
+    auto buf = BinaryBuf(key.size() + 3);
+    buf[0]   = 0x8b;
+    buf[1]   = 0x01;
+    std::copy(begin(key), end(key), begin(buf) + 2);
+
+    uint8_t parity = buf[0] ^ buf[1];
+    for (uint8_t b : key)
+        parity ^= b;
+    buf.back() = parity;
+
+    return bin2base58(to_string(buf));
+};
+
 std::string
 decrypt(const mtx::secret_storage::AesHmacSha2EncryptedData &data,
         BinaryBuf decryptionKey,
@@ -170,6 +186,19 @@ HKDF_SHA256(const BinaryBuf &key, const BinaryBuf &salt, const BinaryBuf &info)
     return {std::move(buf), std::move(macKey)};
 }
 
+BinaryBuf
+compatible_iv(BinaryBuf incompatible_iv)
+{
+    // need to set bit 63 to 0
+    // Element and everyone else seems to be counting bytes from the back, i.e. iv_data[15] is
+    // the last byte. So we need to clear byte 15 - 63%8 = 15 - 7 = 8, the highest bit, 1 << 7
+    // see:
+    // https://github.com/matrix-org/matrix-js-sdk/blob/529fe93ab14b93c515e9ab0d0277c1942a5d73c5/src/crypto/aes.ts#L144
+    uint8_t *data = incompatible_iv.data();
+    data[15 - 63 % 8] &= ~(1UL << (63 / 8));
+    return incompatible_iv;
+}
+
 BinaryBuf
 AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, BinaryBuf iv)
 {
@@ -180,23 +209,14 @@ AES_CTR_256_Encrypt(const std::string plaintext, const BinaryBuf aes256Key, Bina
     int ciphertext_len;
 
     // The ciphertext expand up to block size, which is 128 for AES256
-    BinaryBuf encrypted = create_buffer(plaintext.size() + AES_BLOCK_SIZE);
-
-    uint8_t *iv_data = iv.data();
-    // need to set bit 63 to 0
-    // Element and everyone else seems to be counting bytes from the back, i.e. iv_data[15] is
-    // the last byte. So we need to clear byte 15 - 63%8 = 15 - 7 = 8, the highest bit, 1 << 7
-    // see:
-    // https://github.com/matrix-org/matrix-js-sdk/blob/529fe93ab14b93c515e9ab0d0277c1942a5d73c5/src/crypto/aes.ts#L144
-    iv_data[15 - 63 % 8] &= ~(1UL << (63 / 8));
-    //*iv_data &= ~(1UL << (63));
+    BinaryBuf encrypted = compatible_iv(create_buffer(plaintext.size() + AES_BLOCK_SIZE));
 
     /* Create and initialise the context */
     if (!(ctx = EVP_CIPHER_CTX_new())) {
         // handleErrors();
     }
 
-    if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, aes256Key.data(), iv_data)) {
+    if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, aes256Key.data(), iv.data())) {
         // handleErrors();
     }
 
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index beb9a58680a9442b13d990fe36b6f57160fbaf39..13731ea377f8721d96d98b6551a9915267ccf327 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -23,6 +23,11 @@ struct ClientPrivate
     coeurl::Client client;
 };
 
+void
+UIAHandler::next(const user_interactive::Auth &auth) const
+{
+    next_(*this, auth);
+}
 }
 
 Client::Client(const std::string &server, uint16_t port)
@@ -895,6 +900,34 @@ Client::registration(const std::string &user,
     post<nlohmann::json, mtx::responses::Register>("/client/r0/register", req, callback, false);
 }
 
+void
+Client::registration(const std::string &user,
+                     const std::string &pass,
+                     UIAHandler uia_handler,
+                     Callback<mtx::responses::Register> cb)
+{
+    nlohmann::json req = {{"username", user}, {"password", pass}};
+
+    uia_handler.next_ = [this, req, cb](const UIAHandler &h, const nlohmann::json &auth) {
+        auto request = req;
+        if (!auth.empty())
+            request["auth"] = auth;
+
+        post<nlohmann::json, mtx::responses::Register>(
+          "/client/r0/register",
+          request,
+          [cb, h](auto &r, RequestErr e) {
+              if (e && e->status_code == 401)
+                  h.prompt(h, e->matrix_error.unauthorized);
+              else
+                  cb(r, e);
+          },
+          false);
+    };
+
+    uia_handler.next_(uia_handler, {});
+}
+
 void
 Client::registration_token_validity(const std::string token,
                                     Callback<mtx::responses::RegistrationTokenValidity> cb)
@@ -1069,6 +1102,30 @@ Client::keys_signatures_upload(const mtx::requests::KeySignaturesUpload &req,
       "/client/unstable/keys/signatures/upload", req, cb);
 }
 
+void
+Client::device_signing_upload(const mtx::requests::DeviceSigningUpload deviceKeys,
+                              UIAHandler uia_handler,
+                              ErrCallback cb)
+{
+    nlohmann::json req = deviceKeys;
+
+    uia_handler.next_ = [this, req, cb](const UIAHandler &h, const nlohmann::json &auth) {
+        auto request = req;
+        if (!auth.empty())
+            request["auth"] = auth;
+
+        post<nlohmann::json, mtx::responses::Empty>(
+          "/client/unstable/keys/device_signing/upload", request, [cb, h](auto &, RequestErr e) {
+              if (e && e->status_code == 401 && !e->matrix_error.unauthorized.flows.empty())
+                  h.prompt(h, e->matrix_error.unauthorized);
+              else
+                  cb(e);
+          });
+    };
+
+    uia_handler.next_(uia_handler, {});
+}
+
 void
 Client::query_keys(const mtx::requests::QueryKeys &req,
                    Callback<mtx::responses::QueryKeys> callback)
diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp
index 6217dfdb8d09923a9bb344d11c8b7cff0ebb40aa..0c1e10935388eb61acf1714cb492b9a9d2b18070 100644
--- a/lib/structs/requests.cpp
+++ b/lib/structs/requests.cpp
@@ -196,6 +196,17 @@ to_json(json &obj, const KeySignaturesUpload &req)
             obj[user_id][key_id] = std::visit([](const auto &e) { return json(e); }, keyVar);
 }
 
+void
+to_json(json &obj, const DeviceSigningUpload &req)
+{
+    if (req.master_key)
+        obj["master_key"] = req.master_key.value();
+    if (req.self_signing_key)
+        obj["self_signing_key"] = req.self_signing_key.value();
+    if (req.user_signing_key)
+        obj["user_signing_key"] = req.user_signing_key.value();
+}
+
 void
 to_json(json &obj, const PusherData &data)
 {
diff --git a/tests/crypto.cpp b/tests/crypto.cpp
index 909c9a8ff72253779e407b4dff0160fc1d02ea17..6420bb060c1df78573c3f8626bd7e3347765d8e2 100644
--- a/tests/crypto.cpp
+++ b/tests/crypto.cpp
@@ -184,6 +184,7 @@ TEST(Base58, EncodingDecoding)
     EXPECT_EQ(bin2base58("foob"), "3csAg9");
     EXPECT_EQ(bin2base58("fooba"), "CZJRhmz");
     EXPECT_EQ(bin2base58("foobar"), "t1Zv2yaZ");
+    EXPECT_FALSE(bin2base58(to_string(create_buffer(32))).empty());
 
     EXPECT_EQ("", base582bin(""));
     EXPECT_EQ("f", base582bin("2m"));
@@ -371,3 +372,26 @@ TEST(SecretStorage, SecretKey)
     ASSERT_EQ(desc.signatures["@alice:localhost"]["ed25519:adkfajfgaefkdahfzguerhtgduifghes"],
               "ksfjvkrfbnrtnwublrjkgnorthgnrdtjbiortbjdlbiutr");
 }
+
+TEST(SecretStorage, CreateSecretKey)
+{
+    auto ssss1 = mtx::crypto::OlmClient::create_ssss_key();
+    ASSERT_TRUE(ssss1.has_value());
+    EXPECT_FALSE(ssss1->keyDescription.passphrase.has_value());
+    EXPECT_EQ(ssss1->keyDescription.algorithm, "m.secret_storage.v1.aes-hmac-sha2");
+    EXPECT_GE(ssss1->keyDescription.iv.length(), 32);
+    EXPECT_EQ((ssss1->keyDescription.mac.length() - 1) * 3 / 4, 32);
+
+    EXPECT_EQ(key_from_recoverykey(key_to_recoverykey(ssss1->privateKey), ssss1->keyDescription),
+              ssss1->privateKey);
+
+    auto ssss2 = mtx::crypto::OlmClient::create_ssss_key("some passphrase");
+    ASSERT_TRUE(ssss2.has_value());
+    ASSERT_TRUE(ssss2->keyDescription.passphrase.has_value());
+    EXPECT_EQ(ssss2->keyDescription.algorithm, "m.secret_storage.v1.aes-hmac-sha2");
+    EXPECT_GE(ssss2->keyDescription.iv.length(), 32);
+    EXPECT_EQ((ssss2->keyDescription.mac.length() - 1) * 3 / 4, 32);
+
+    EXPECT_EQ(mtx::crypto::key_from_passphrase("some passphrase", ssss2->keyDescription),
+              ssss2->privateKey);
+}
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index 8aecd46c51a8ccc2650dc090efd6c17b4bf401dd..c06e2d7a9f325341df9a3bc40d428c29afff67bb 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -463,6 +463,70 @@ TEST(Encryption, ClaimMultipleDeviceKeys)
     alice3->close();
 }
 
+TEST(Encryption, UploadCrossSigningKeys)
+{
+    auto alice       = make_test_client();
+    auto olm_account = std::make_shared<mtx::crypto::OlmClient>();
+
+    EXPECT_THROW(olm_account->identity_keys(), olm_exception);
+
+    olm_account->create_new_account();
+
+    alice->login(
+      "alice", "secret", [](const mtx::responses::Login &, RequestErr err) { check_error(err); });
+
+    while (alice->access_token().empty())
+        sleep();
+
+    olm_account->set_user_id(alice->user_id().to_string());
+    olm_account->set_device_id(alice->device_id());
+
+    auto id_keys = olm_account->identity_keys();
+
+    ASSERT_TRUE(id_keys.curve25519.size() > 10);
+    ASSERT_TRUE(id_keys.curve25519.size() > 10);
+
+    mtx::crypto::OneTimeKeys unused;
+    auto request = olm_account->create_upload_keys_request(unused);
+
+    // Make the request with the signed identity keys.
+    alice->upload_keys(request, [](const mtx::responses::UploadKeys &res, RequestErr err) {
+        check_error(err);
+        EXPECT_EQ(res.one_time_key_counts.size(), 0);
+    });
+
+    atomic<bool> done = false;
+    auto xsign_keys   = olm_account->create_crosssigning_keys();
+    ASSERT_TRUE(xsign_keys.has_value());
+    mtx::requests::DeviceSigningUpload u;
+    u.master_key       = xsign_keys->master_key;
+    u.user_signing_key = xsign_keys->user_signing_key;
+    u.self_signing_key = xsign_keys->self_signing_key;
+    alice->device_signing_upload(
+      u,
+      mtx::http::UIAHandler([](const mtx::http::UIAHandler &h,
+                               const mtx::user_interactive::Unauthorized &unauthorized) {
+          ASSERT_EQ(unauthorized.flows.size(), 1);
+          ASSERT_EQ(unauthorized.flows[0].stages.size(), 1);
+          ASSERT_EQ(unauthorized.flows[0].stages[0], mtx::user_interactive::auth_types::password);
+
+          mtx::user_interactive::Auth auth;
+          auth.session = unauthorized.session;
+          mtx::user_interactive::auth::Password pass{};
+          pass.password        = "secret";
+          pass.identifier_user = "alice";
+          pass.identifier_type = mtx::user_interactive::auth::Password::IdType::UserId;
+          auth.content         = pass;
+          h.next(auth);
+      }),
+      [&done](RequestErr e) {
+          done = true;
+          check_error(e);
+      });
+
+    alice->close();
+}
+
 TEST(Encryption, KeyChanges)
 {
     auto carl     = make_test_client();