diff --git a/Cargo.lock b/Cargo.lock
index b14833ff9..34d528a9f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -403,7 +403,7 @@ dependencies = [
  "serde_json",
  "serde_qs",
  "serde_repr",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tokio",
  "tsify-next",
  "uniffi",
@@ -439,11 +439,12 @@ dependencies = [
  "rsa",
  "schemars",
  "serde",
+ "serde_bytes",
  "serde_json",
  "sha1",
  "sha2",
  "subtle",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tsify-next",
  "typenum",
  "uniffi",
@@ -477,7 +478,7 @@ dependencies = [
  "quote",
  "serde",
  "syn",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tsify-next",
  "wasm-bindgen",
 ]
@@ -499,7 +500,7 @@ dependencies = [
  "rand 0.8.5",
  "serde",
  "serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tsify-next",
  "uniffi",
  "uuid",
@@ -525,7 +526,7 @@ dependencies = [
  "reqwest",
  "serde",
  "serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "uniffi",
  "uuid",
 ]
@@ -543,7 +544,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tokio",
  "tsify-next",
  "uniffi",
@@ -559,7 +560,7 @@ dependencies = [
  "js-sys",
  "serde",
  "serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tokio",
  "tsify-next",
  "wasm-bindgen",
@@ -577,7 +578,7 @@ dependencies = [
  "chrono",
  "serde",
  "serde_repr",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "uniffi",
  "uuid",
  "wasm-bindgen",
@@ -596,7 +597,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tokio",
  "uuid",
  "validator",
@@ -617,7 +618,7 @@ dependencies = [
  "rsa",
  "serde",
  "ssh-key",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tsify-next",
  "uniffi",
  "wasm-bindgen",
@@ -645,7 +646,7 @@ dependencies = [
  "oslog",
  "rustls-platform-verifier",
  "schemars",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "uniffi",
  "uuid",
 ]
@@ -670,7 +671,7 @@ dependencies = [
  "serde_repr",
  "sha1",
  "sha2",
- "thiserror 1.0.69",
+ "thiserror 2.0.12",
  "tokio",
  "tsify-next",
  "uniffi",
@@ -682,6 +683,7 @@ dependencies = [
 name = "bitwarden-wasm-internal"
 version = "0.1.0"
 dependencies = [
+ "base64",
  "bitwarden-core",
  "bitwarden-crypto",
  "bitwarden-error",
@@ -3502,6 +3504,15 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "serde_bytes"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.219"
diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs
index 94ebe9445..0fe7ddff2 100644
--- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs
+++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs
@@ -15,6 +15,8 @@ pub struct IdentityTokenSuccessResponse {
     pub(crate) private_key: Option<String>,
     #[serde(alias = "Key")]
     pub(crate) key: Option<String>,
+    #[serde(alias = "userKeyEncryptedSigningKey")]
+    pub(crate) user_key_encrypted_signing_key: Option<String>,
     #[serde(rename = "twoFactorToken")]
     two_factor_token: Option<String>,
     #[serde(alias = "Kdf")]
@@ -53,6 +55,7 @@ mod test {
                 refresh_token: Default::default(),
                 token_type: Default::default(),
                 private_key: Default::default(),
+                user_key_encrypted_signing_key: Default::default(),
                 key: Default::default(),
                 two_factor_token: Default::default(),
                 kdf: KdfType::default(),
diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs
index c265584f4..9033b7ff8 100644
--- a/crates/bitwarden-core/src/auth/auth_request.rs
+++ b/crates/bitwarden-core/src/auth/auth_request.rs
@@ -162,7 +162,7 @@ mod tests {
         let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
         client
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key, private_key)
+            .initialize_user_crypto_master_key(master_key, user_key, private_key, None)
             .unwrap();
 
         let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB";
@@ -229,7 +229,12 @@ mod tests {
 
         existing_device
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key, private_key.parse().unwrap())
+            .initialize_user_crypto_master_key(
+                master_key,
+                user_key,
+                private_key.parse().unwrap(),
+                None,
+            )
             .unwrap();
 
         // Initialize a new device which will request to be logged in
@@ -246,6 +251,7 @@ mod tests {
                 kdf_params: kdf,
                 email: email.to_owned(),
                 private_key: private_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::AuthRequest {
                     request_private_key: auth_req.private_key,
                     method: AuthRequestMethod::UserKey {
diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs
index 649e448f8..d6d70743a 100644
--- a/crates/bitwarden-core/src/auth/login/api_key.rs
+++ b/crates/bitwarden-core/src/auth/login/api_key.rs
@@ -51,9 +51,12 @@ pub(crate) async fn login_api_key(
         let user_key: EncString = require!(r.key.as_deref()).parse()?;
         let private_key: EncString = require!(r.private_key.as_deref()).parse()?;
 
-        client
-            .internal
-            .initialize_user_crypto_master_key(master_key, user_key, private_key)?;
+        client.internal.initialize_user_crypto_master_key(
+            master_key,
+            user_key,
+            private_key,
+            None,
+        )?;
     }
 
     Ok(ApiKeyLoginResponse::process_response(response))
diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs
index 6d2abd203..17ba9a234 100644
--- a/crates/bitwarden-core/src/auth/login/auth_request.rs
+++ b/crates/bitwarden-core/src/auth/login/auth_request.rs
@@ -118,6 +118,7 @@ pub(crate) async fn complete_auth_request(
                 kdf_params: kdf,
                 email: auth_req.email,
                 private_key: require!(r.private_key),
+                signing_key: None,
                 method: InitUserCryptoMethod::AuthRequest {
                     request_private_key: auth_req.private_key,
                     method,
diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs
index 9d9390b85..7c4b7fbd1 100644
--- a/crates/bitwarden-core/src/auth/login/password.rs
+++ b/crates/bitwarden-core/src/auth/login/password.rs
@@ -52,10 +52,18 @@ pub(crate) async fn login_password(
 
         let user_key: EncString = require!(r.key.as_deref()).parse()?;
         let private_key: EncString = require!(r.private_key.as_deref()).parse()?;
+        let signing_key = r
+            .user_key_encrypted_signing_key
+            .clone()
+            .map(|s| s.parse())
+            .transpose()?;
 
-        client
-            .internal
-            .initialize_user_crypto_master_key(master_key, user_key, private_key)?;
+        client.internal.initialize_user_crypto_master_key(
+            master_key,
+            user_key,
+            private_key,
+            signing_key,
+        )?;
     }
 
     Ok(PasswordLoginResponse::process_response(response))
diff --git a/crates/bitwarden-core/src/auth/password/validate.rs b/crates/bitwarden-core/src/auth/password/validate.rs
index 39abee276..229c7fde7 100644
--- a/crates/bitwarden-core/src/auth/password/validate.rs
+++ b/crates/bitwarden-core/src/auth/password/validate.rs
@@ -140,7 +140,12 @@ mod tests {
 
         client
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
+            .initialize_user_crypto_master_key(
+                master_key,
+                user_key.parse().unwrap(),
+                private_key,
+                None,
+            )
             .unwrap();
 
         let result =
@@ -183,7 +188,12 @@ mod tests {
 
         client
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
+            .initialize_user_crypto_master_key(
+                master_key,
+                user_key.parse().unwrap(),
+                private_key,
+                None,
+            )
             .unwrap();
 
         let result =
diff --git a/crates/bitwarden-core/src/auth/pin.rs b/crates/bitwarden-core/src/auth/pin.rs
index 93e172f25..c337f9327 100644
--- a/crates/bitwarden-core/src/auth/pin.rs
+++ b/crates/bitwarden-core/src/auth/pin.rs
@@ -75,7 +75,12 @@ mod tests {
 
         client
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key)
+            .initialize_user_crypto_master_key(
+                master_key,
+                user_key.parse().unwrap(),
+                private_key,
+                None,
+            )
             .unwrap();
 
         client
diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs
index e8bc8470e..2fd57ea97 100644
--- a/crates/bitwarden-core/src/auth/tde.rs
+++ b/crates/bitwarden-core/src/auth/tde.rs
@@ -37,9 +37,11 @@ pub(super) fn make_register_tde_keys(
                 kdf: Kdf::default(),
             },
         ));
-    client
-        .internal
-        .initialize_user_crypto_decrypted_key(user_key.0, key_pair.private.clone())?;
+    client.internal.initialize_user_crypto_decrypted_key(
+        user_key.0,
+        key_pair.private.clone(),
+        None,
+    )?;
 
     Ok(RegisterTdeKeyResponse {
         private_key: key_pair.private,
diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs
index 97351cb9f..daee0968a 100644
--- a/crates/bitwarden-core/src/client/encryption_settings.rs
+++ b/crates/bitwarden-core/src/client/encryption_settings.rs
@@ -1,12 +1,11 @@
-use bitwarden_crypto::{AsymmetricCryptoKey, KeyStore, SymmetricCryptoKey};
 #[cfg(feature = "internal")]
-use bitwarden_crypto::{EncString, UnsignedSharedKey};
+use bitwarden_crypto::{EncString, KeyStore, SymmetricCryptoKey, UnsignedSharedKey};
 use bitwarden_error::bitwarden_error;
 use thiserror::Error;
 use uuid::Uuid;
 
 use crate::{
-    key_management::{AsymmetricKeyId, KeyIds, SymmetricKeyId},
+    key_management::{KeyIds, SymmetricKeyId},
     MissingPrivateKeyError, VaultLockedError,
 };
 
@@ -40,12 +39,13 @@ impl EncryptionSettings {
     pub(crate) fn new_decrypted_key(
         user_key: SymmetricCryptoKey,
         private_key: EncString,
+        signing_key: Option<EncString>,
         store: &KeyStore<KeyIds>,
     ) -> Result<(), EncryptionSettingsError> {
-        use bitwarden_crypto::KeyDecryptable;
+        use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable, SigningKey};
         use log::warn;
 
-        use crate::key_management::{AsymmetricKeyId, SymmetricKeyId};
+        use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId};
 
         let private_key = {
             let dec: Vec<u8> = private_key.decrypt_with_key(&user_key)?;
@@ -63,6 +63,12 @@ impl EncryptionSettings {
             //         .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?,
             // )
         };
+        let signing_key = signing_key
+            .map(|key| {
+                let dec: Vec<u8> = key.decrypt_with_key(&user_key)?;
+                SigningKey::from_cose(dec.as_slice())
+            })
+            .transpose()?;
 
         // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods
         #[allow(deprecated)]
@@ -72,6 +78,10 @@ impl EncryptionSettings {
             if let Some(private_key) = private_key {
                 ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?;
             }
+
+            if let Some(signing_key) = signing_key {
+                ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?;
+            }
         }
 
         Ok(())
@@ -98,6 +108,8 @@ impl EncryptionSettings {
         org_enc_keys: Vec<(Uuid, UnsignedSharedKey)>,
         store: &KeyStore<KeyIds>,
     ) -> Result<(), EncryptionSettingsError> {
+        use crate::key_management::AsymmetricKeyId;
+
         let mut ctx = store.context_mut();
 
         // FIXME: [PM-11690] - Early abort to handle private key being corrupt
diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs
index 725d95d33..c29e144ef 100644
--- a/crates/bitwarden-core/src/client/internal.rs
+++ b/crates/bitwarden-core/src/client/internal.rs
@@ -8,13 +8,11 @@ use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey};
 use chrono::Utc;
 use uuid::Uuid;
 
+use super::encryption_settings::EncryptionSettings;
 #[cfg(feature = "secrets")]
 use super::login_method::ServiceAccountLoginMethod;
 use crate::{
-    auth::renew::renew_token,
-    client::{encryption_settings::EncryptionSettings, login_method::LoginMethod},
-    key_management::KeyIds,
-    DeviceType,
+    auth::renew::renew_token, client::login_method::LoginMethod, key_management::KeyIds, DeviceType,
 };
 #[cfg(feature = "internal")]
 use crate::{
@@ -178,9 +176,10 @@ impl InternalClient {
         master_key: MasterKey,
         user_key: EncString,
         private_key: EncString,
+        signing_key: Option<EncString>,
     ) -> Result<(), EncryptionSettingsError> {
         let user_key = master_key.decrypt_user_key(user_key)?;
-        EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?;
+        EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?;
 
         Ok(())
     }
@@ -190,8 +189,9 @@ impl InternalClient {
         &self,
         user_key: SymmetricCryptoKey,
         private_key: EncString,
+        signing_key: Option<EncString>,
     ) -> Result<(), EncryptionSettingsError> {
-        EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?;
+        EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?;
 
         Ok(())
     }
@@ -202,9 +202,10 @@ impl InternalClient {
         pin_key: PinKey,
         pin_protected_user_key: EncString,
         private_key: EncString,
+        signing_key: Option<EncString>,
     ) -> Result<(), EncryptionSettingsError> {
         let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?;
-        self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key)
+        self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key, signing_key)
     }
 
     #[cfg(feature = "secrets")]
diff --git a/crates/bitwarden-core/src/client/test_accounts.rs b/crates/bitwarden-core/src/client/test_accounts.rs
index 367c36495..f5d1819a7 100644
--- a/crates/bitwarden-core/src/client/test_accounts.rs
+++ b/crates/bitwarden-core/src/client/test_accounts.rs
@@ -123,6 +123,8 @@ pub fn test_bitwarden_com_account() -> TestAccount {
             email: "test@bitwarden.com".to_owned(),
             private_key: "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".to_owned(),
 
+            signing_key: None,
+
             method: InitUserCryptoMethod::Password {
                 password: "asdfasdfasdf".to_owned(),
                 user_key: "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".to_owned(),
@@ -179,6 +181,7 @@ pub fn test_legacy_user_key_account() -> TestAccount {
             },
             email: "legacy@bitwarden.com".to_owned(),
             private_key: "2.leBIE5u0aQUeXi++JzAnrA==|P8x+hs00RJx7epw+49qVtBhLJxE/JTL5dEHg6kq5pbZLdUY8ZvWK49v0EqgHbv1r298N9+msoO9hmdSIVIAZyycemYDSoc1rX4S1KpS/ZMA/Vd3VLFb+o13Ts62GFQ5ygHKgQZfzjU6jO5P/B/0igzFoxyJDomhW5NBC1P9+e/5qNRZN8loKvAaWc/7XtpRayPQqWx+AgYc2ntb1GF5hRVrW4M47bG5ZKllbJWtQKg2sXIy2lDBbKLRFWF4RFzNVcXQGMoPdWLY0f3uTwUH01dyGmFFMbOvfBEuYqmZyPdd93ve8zuFOEqkj46Ulpq2CVG8NvZARTwsdKl6XB0wGuHFoTsDJT2SJGl67pBBKsVRGxy059QW+9hAIB+emIV0T/7+0rvdeSXZ4AbG+oXGEXFTkHefwJKfeT0MBTAjYKr7ZRLgqvf7n39+nCEJU4l22kp8FmjcWIU7AgNipdGHC+UT2yfOcYlvgBgWDcMXcbVDMyus9105RgcW6PHozUj7yjbohI/A3XWmAFufP6BSnmEFCKoik78X/ry09xwiH2rN4KVXe/k9LpRNB2QBGIVsfgCrkxjeE8r0nA59Rvwrhny1z5BkvMW/N1KrGuafg/IYgegx72gJNuZPZlFu1Vs7HxySHmzYvm3DPV7bzCaAxxNtvZmQquNIEnsDQfjJO76iL1JCtDqNJVzGLHTMTr7S5hkOcydcH3kfKwZdA1ULVd2qu0SwOUEP/ECjU/cS5INy6WPYzNMAe/g2DISpQjNwBb5K17PIiGOR7/Q/A6E8pVnkHiAXuUFr9aLOYN9BWSu5Z+BPHH65na2FDmssix5WV09I2sUBfvdNCjkrUGdYgo8E+vOTn35x9GJHF45uhmgC1yAn/+/RSpORlrSVJ7NNP11dn3htUpSsIy/b7ituAu8Ry5mhicFU8CXJL4NeMlXThUt8P++wxs4wMkBvJ8J9NJAVKbAOA2o+GOdjbh6Ww3IRegkurWh4oL/dFSx0LpaXJuw6HFT/LzticPlSwHtUP11hZ81seMsXmkSZd8IugRFfwpPl7N6PVRWDOKxLf4gPqcnJ11TvfasXy1uolV2vZCPbrbbVzQMPdVwL/OzwfhqsIgQZI8rsDMK5D2EX8MaT8MDfGcsYcVTL9PmuZYLpOUnnHX0A1opAAa9iPw3d+eWB/GAyLvKPnMTUqVNos8HcCktXckCshihA8QuBJOwg3m0j2LPSZ5Jvf8gbXauBmt9I4IlJq0xfpgquYY1WNnO8IcWE4N9W+ASvOr9gnduA6CkDeAlyMUFmdpkeCjGMcsV741bTCPApSQlL3/TOT1cjK3iejWpz0OaVHXyg02hW2fNkOfYfr81GvnLvlHxIg4Prw89gKuWU+kQk82lFQo6QQpqbCbJC2FleurD8tYoSY0srhuioVInffvTxw2NMF7FQEqUcsK9AMKSEiDqzBi35Um/fiE3JL4XZBFw8Xzl7X3ab5nlg8X+xD5uSZY+oxD3sDVXjLaQ5JUoys+MCm0FkUj85l0zT6rvM4QLhU1RDK1U51T9HJhh8hsFJsqL4abRzwEWG7PSi859zN4UsgyuQfmBJv/n7QAFCbrJhVBlGB1TKLZRzvgmKoxTYTG3cJFkjetLcUTwrwC9naxAQRfF4=|ufHf73IzJ707dx44w4fjkuD7tDa50OwmmkxcypAT9uQ=".to_owned(),
+            signing_key: None,
             method: InitUserCryptoMethod::Password {
                 password: "asdfasdfasdf".to_owned(),
                 user_key: "0.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=".to_owned(),
diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs
index dd13ab21c..efcfb86db 100644
--- a/crates/bitwarden-core/src/key_management/mod.rs
+++ b/crates/bitwarden-core/src/key_management/mod.rs
@@ -26,7 +26,12 @@ key_ids! {
         Local(&'static str),
     }
 
-    pub KeyIds => SymmetricKeyId, AsymmetricKeyId;
+    #[signing]
+    pub enum SigningKeyId {
+        UserSigningKey,
+    }
+
+    pub KeyIds => SymmetricKeyId, AsymmetricKeyId, SigningKeyId;
 }
 
 /// This is a helper function to create a test KeyStore with a single user key.
diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs
index 6e2905ce8..a3c04957d 100644
--- a/crates/bitwarden-core/src/mobile/crypto.rs
+++ b/crates/bitwarden-core/src/mobile/crypto.rs
@@ -8,16 +8,18 @@ use std::collections::HashMap;
 
 use base64::{engine::general_purpose::STANDARD, Engine};
 use bitwarden_crypto::{
-    AsymmetricCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey,
+    AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable,
+    KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKeyOwnershipClaim, SigningKey,
     SymmetricCryptoKey, UnsignedSharedKey, UserKey,
 };
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "wasm")]
 use {tsify_next::Tsify, wasm_bindgen::prelude::*};
 
 use crate::{
     client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod},
-    key_management::SymmetricKeyId,
+    key_management::{AsymmetricKeyId, SymmetricKeyId},
     Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError,
 };
 
@@ -45,6 +47,10 @@ pub struct InitUserCryptoRequest {
     pub email: String,
     /// The user's encrypted private key
     pub private_key: String,
+
+    /// The user's signing key
+    pub signing_key: Option<String>,
+
     /// The initialization method to use
     pub method: InitUserCryptoMethod,
 }
@@ -136,15 +142,18 @@ pub async fn initialize_user_crypto(
             let user_key: EncString = user_key.parse()?;
 
             let master_key = MasterKey::derive(&password, &req.email, &req.kdf_params)?;
-            client
-                .internal
-                .initialize_user_crypto_master_key(master_key, user_key, private_key)?;
+            client.internal.initialize_user_crypto_master_key(
+                master_key,
+                user_key,
+                private_key,
+                None,
+            )?;
         }
         InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => {
             let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?;
             client
                 .internal
-                .initialize_user_crypto_decrypted_key(user_key, private_key)?;
+                .initialize_user_crypto_decrypted_key(user_key, private_key, None)?;
         }
         InitUserCryptoMethod::Pin {
             pin,
@@ -155,6 +164,7 @@ pub async fn initialize_user_crypto(
                 pin_key,
                 pin_protected_user_key,
                 private_key,
+                None,
             )?;
         }
         InitUserCryptoMethod::AuthRequest {
@@ -176,7 +186,7 @@ pub async fn initialize_user_crypto(
             };
             client
                 .internal
-                .initialize_user_crypto_decrypted_key(user_key, private_key)?;
+                .initialize_user_crypto_decrypted_key(user_key, private_key, None)?;
         }
         InitUserCryptoMethod::DeviceKey {
             device_key,
@@ -189,7 +199,7 @@ pub async fn initialize_user_crypto(
 
             client
                 .internal
-                .initialize_user_crypto_decrypted_key(user_key, private_key)?;
+                .initialize_user_crypto_decrypted_key(user_key, private_key, None)?;
         }
         InitUserCryptoMethod::KeyConnector {
             master_key,
@@ -201,9 +211,12 @@ pub async fn initialize_user_crypto(
             let master_key = MasterKey::try_from(master_key_bytes.as_mut_slice())?;
             let user_key: EncString = user_key.parse()?;
 
-            client
-                .internal
-                .initialize_user_crypto_master_key(master_key, user_key, private_key)?;
+            client.internal.initialize_user_crypto_master_key(
+                master_key,
+                user_key,
+                private_key,
+                None,
+            )?;
         }
     }
 
@@ -542,6 +555,55 @@ pub(super) fn verify_asymmetric_keys(
     })
 }
 
+#[derive(Serialize, Deserialize, Debug, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
+#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
+pub struct MakeUserSigningKeysResponse {
+    /// The verifying key
+    verifying_key: String,
+    /// Signing key, encrypted with a symmetric key (user key, org key)
+    signing_key: EncString,
+
+    /// A signed object claiming ownership of a public key. This ties the public key to the
+    /// signature key
+    signed_public_key_ownership_claim: String,
+}
+
+/// Makes a new set of signing keys for a user. This also creates a signed public-key ownership
+/// claim for the currently used public key.
+#[allow(deprecated)]
+pub fn make_user_signing_keys(client: &Client) -> Result<MakeUserSigningKeysResponse, CryptoError> {
+    let key_store = client.internal.get_key_store();
+    let ctx = key_store.context();
+    let public_key = ctx
+        .dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey)
+        .map_err(|_| CryptoError::InvalidKey)?
+        .to_public_der()?;
+    let public_key =
+        AsymmetricPublicCryptoKey::from_der(&public_key).map_err(|_| CryptoError::InvalidKey)?;
+
+    let wrapping_key = ctx
+        .dangerous_get_symmetric_key(SymmetricKeyId::User)
+        .map_err(|_| CryptoError::InvalidKey)?;
+    let signature_keypair =
+        SigningKey::make(SignatureAlgorithm::Ed25519).map_err(|_| CryptoError::InvalidKey)?;
+    // This needs to be changed to use the correct cose content format before rolling out to real
+    // accounts
+    let encrypted_signing_key = signature_keypair.to_cose()?;
+    let serialized_verifying_key = signature_keypair.to_verifying_key().to_cose()?;
+    let serialized_verifying_key_b64 = STANDARD.encode(serialized_verifying_key);
+    let signed_public_key_ownership_claim =
+        SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signature_keypair)?;
+
+    Ok(MakeUserSigningKeysResponse {
+        verifying_key: serialized_verifying_key_b64,
+        signing_key: encrypted_signing_key.encrypt_with_key(wrapping_key)?,
+        signed_public_key_ownership_claim: STANDARD
+            .encode(signed_public_key_ownership_claim.as_bytes()),
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use std::num::NonZeroU32;
@@ -567,6 +629,7 @@ mod tests {
                 kdf_params: kdf.clone(),
                 email: "test@bitwarden.com".into(),
                 private_key: priv_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::Password {
                     password: "asdfasdfasdf".into(),
                     user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(),
@@ -586,6 +649,7 @@ mod tests {
                 kdf_params: kdf.clone(),
                 email: "test@bitwarden.com".into(),
                 private_key: priv_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::Password {
                     password: "123412341234".into(),
                     user_key: new_password_response.new_key.to_string(),
@@ -643,6 +707,7 @@ mod tests {
                 },
                 email: "test@bitwarden.com".into(),
                 private_key: priv_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::Password {
                     password: "asdfasdfasdf".into(),
                     user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(),
@@ -664,6 +729,7 @@ mod tests {
                 },
                 email: "test@bitwarden.com".into(),
                 private_key: priv_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::Pin {
                     pin: "1234".into(),
                     pin_protected_user_key: pin_key.pin_protected_user_key,
@@ -706,6 +772,7 @@ mod tests {
                 },
                 email: "test@bitwarden.com".into(),
                 private_key: priv_key.to_owned(),
+                signing_key: None,
                 method: InitUserCryptoMethod::Pin {
                     pin: "1234".into(),
                     pin_protected_user_key,
@@ -756,7 +823,7 @@ mod tests {
         let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
         client
             .internal
-            .initialize_user_crypto_master_key(master_key, user_key, private_key)
+            .initialize_user_crypto_master_key(master_key, user_key, private_key, None)
             .unwrap();
 
         let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB";
diff --git a/crates/bitwarden-core/src/mobile/crypto_client.rs b/crates/bitwarden-core/src/mobile/crypto_client.rs
index 42634ce32..69de145a7 100644
--- a/crates/bitwarden-core/src/mobile/crypto_client.rs
+++ b/crates/bitwarden-core/src/mobile/crypto_client.rs
@@ -3,9 +3,10 @@ use bitwarden_crypto::CryptoError;
 use bitwarden_crypto::{EncString, UnsignedSharedKey};
 
 use super::crypto::{
-    derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorError,
-    DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, MakeKeyPairResponse,
-    MobileCryptoError, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
+    derive_key_connector, make_key_pair, make_user_signing_keys, verify_asymmetric_keys,
+    DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError,
+    MakeKeyPairResponse, MakeUserSigningKeysResponse, MobileCryptoError,
+    VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
 };
 #[cfg(feature = "internal")]
 use crate::mobile::crypto::{
@@ -101,6 +102,10 @@ impl CryptoClient {
     ) -> Result<VerifyAsymmetricKeysResponse, CryptoError> {
         verify_asymmetric_keys(request)
     }
+
+    pub fn make_signing_keys(&self) -> Result<MakeUserSigningKeysResponse, CryptoError> {
+        make_user_signing_keys(&self.client)
+    }
 }
 
 impl Client {
diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs
index 21a16e976..4e1bfe009 100644
--- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs
+++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs
@@ -107,6 +107,7 @@ mod tests {
                 master_key,
                 user_key.parse().unwrap(),
                 private_key.parse().unwrap(),
+                None,
             )
             .unwrap();
 
diff --git a/crates/bitwarden-core/tests/register.rs b/crates/bitwarden-core/tests/register.rs
index 3f01b4763..c8ac264e8 100644
--- a/crates/bitwarden-core/tests/register.rs
+++ b/crates/bitwarden-core/tests/register.rs
@@ -33,6 +33,8 @@ async fn test_register_initialize_crypto() {
             email: email.to_owned(),
             private_key: register_response.keys.private.to_string(),
 
+            signing_key: None,
+
             method: InitUserCryptoMethod::Password {
                 password: password.to_owned(),
                 user_key: register_response.encrypted_user_key,
diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml
index e6a9fbdbe..8d5495e65 100644
--- a/crates/bitwarden-crypto/Cargo.toml
+++ b/crates/bitwarden-crypto/Cargo.toml
@@ -46,6 +46,7 @@ rayon = ">=1.8.1, <2.0"
 rsa = ">=0.9.2, <0.10"
 schemars = { workspace = true }
 serde = { workspace = true }
+serde_bytes = ">=0.11.17, <0.12.0"
 sha1 = ">=0.10.5, <0.11"
 sha2 = ">=0.10.6, <0.11"
 subtle = ">=2.5.0, <3.0"
diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs
index d5d09c53a..c4314bdaf 100644
--- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs
+++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs
@@ -162,7 +162,7 @@ impl UnsignedSharedKey {
         encapsulation_key: &dyn AsymmetricEncryptable,
     ) -> Result<UnsignedSharedKey> {
         let enc = encrypt_rsa2048_oaep_sha1(
-            encapsulation_key.to_public_key(),
+            encapsulation_key.to_public_rsa_key(),
             &encapsulated_key.to_encoded(),
         )?;
         Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data: enc })
diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs
index 2cb623e3b..ec5645d08 100644
--- a/crates/bitwarden-crypto/src/error.rs
+++ b/crates/bitwarden-crypto/src/error.rs
@@ -59,6 +59,12 @@ pub enum CryptoError {
 
     #[error("Signature error, {0}")]
     SignatureError(#[from] SignatureError),
+
+    #[error("Cose encoding error")]
+    CoseEncodingError,
+
+    #[error("Invalid encoding")]
+    InvalidEncoding,
 }
 
 #[derive(Debug, Error)]
diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
index 284c4ce25..d7adb45a8 100644
--- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
+++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
@@ -1,14 +1,14 @@
 use std::pin::Pin;
 
-use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey};
+use rsa::{pkcs8::DecodePublicKey, traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey};
 
-use super::key_encryptable::CryptoKey;
+use super::{fingerprint::FingerprintableKey, key_encryptable::CryptoKey};
 use crate::error::{CryptoError, Result};
 
 /// Trait to allow both [`AsymmetricCryptoKey`] and [`AsymmetricPublicCryptoKey`] to be used to
 /// encrypt [UnsignedSharedKey](crate::UnsignedSharedKey).
 pub trait AsymmetricEncryptable {
-    fn to_public_key(&self) -> &RsaPublicKey;
+    fn to_public_rsa_key(&self) -> &RsaPublicKey;
 }
 
 /// An asymmetric public encryption key. Can only encrypt
@@ -29,11 +29,20 @@ impl AsymmetricPublicCryptoKey {
 }
 
 impl AsymmetricEncryptable for AsymmetricPublicCryptoKey {
-    fn to_public_key(&self) -> &RsaPublicKey {
+    fn to_public_rsa_key(&self) -> &RsaPublicKey {
         &self.key
     }
 }
 
+impl FingerprintableKey for AsymmetricPublicCryptoKey {
+    fn fingerprint_parts(&self) -> Vec<Vec<u8>> {
+        vec![
+            self.key.n().to_bytes_le().as_slice().to_vec(),
+            self.key.e().to_bytes_le().as_slice().to_vec(),
+        ]
+    }
+}
+
 /// An asymmetric encryption key. Contains both the public and private key. Can be used to both
 /// encrypt and decrypt [`UnsignedSharedKey`](crate::UnsignedSharedKey).
 #[derive(Clone)]
@@ -93,16 +102,22 @@ impl AsymmetricCryptoKey {
     pub fn to_public_der(&self) -> Result<Vec<u8>> {
         use rsa::pkcs8::EncodePublicKey;
         Ok(self
-            .to_public_key()
+            .to_public_rsa_key()
             .to_public_key_der()
             .map_err(|_| CryptoError::InvalidKey)?
             .as_bytes()
             .to_owned())
     }
+
+    pub fn to_public_key(&self) -> AsymmetricPublicCryptoKey {
+        AsymmetricPublicCryptoKey {
+            key: self.key.to_public_key().clone(),
+        }
+    }
 }
 
 impl AsymmetricEncryptable for AsymmetricCryptoKey {
-    fn to_public_key(&self) -> &RsaPublicKey {
+    fn to_public_rsa_key(&self) -> &RsaPublicKey {
         (*self.key).as_ref()
     }
 }
diff --git a/crates/bitwarden-crypto/src/keys/fingerprint.rs b/crates/bitwarden-crypto/src/keys/fingerprint.rs
new file mode 100644
index 000000000..1c7bc451f
--- /dev/null
+++ b/crates/bitwarden-crypto/src/keys/fingerprint.rs
@@ -0,0 +1,113 @@
+//! This module provides functionality to generate a cryptographic fingerprint for a public key.
+//! This is based on a set of parts of a public key, for RSA this can be the modulus and exponent,
+//! in canonical form.
+//!
+//! Currently, only SHA256 is supported, but the format is designed to be extensible, to more
+//! algorithms in the future, should SHA256 ever not fulfill the required security properties.
+use serde::{Deserialize, Serialize};
+use sha2::Digest;
+
+/// Security assumption:
+/// - The hash function has second pre-image resistance
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) enum PublicKeyFingerprintAlgorithm {
+    Sha256 = 1,
+}
+
+/// A fingerprint represents a short, canonical representation of a public key.
+/// When signing a key, or showing a key to a user, this representation is used.
+///
+/// Note: This implies that a key can have multiple fingerprints. Under a given algorithm,
+/// the fingerprint is always the same, but under different algorithms, the fingerprint is also
+/// different.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct PublicKeyFingerprint {
+    pub(crate) digest: serde_bytes::ByteBuf,
+    pub(crate) algorithm: PublicKeyFingerprintAlgorithm,
+}
+
+/// A trait for objects that can have a canonical cryptographic fingerprint derived from them. To
+/// implement this trait, the object should implement the `FingerprintableKey` trait.
+pub(crate) trait Fingerprintable {
+    /// Returns a fingerprint for the public key, using the currently recommended algorithm.
+    fn fingerprint(&self) -> PublicKeyFingerprint;
+    /// Verify that a fingerprint is valid for the public key
+    fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool;
+}
+
+pub(crate) trait FingerprintableKey: Fingerprintable {
+    /// Returns a canonical representation of the public key.
+    /// The entries of the returned vector should not contain data that is a non-injective mapping
+    /// of the public key. For instance, for RSA, the modulus and exponent should be returned
+    /// separately, not concatenated.
+    fn fingerprint_parts(&self) -> Vec<Vec<u8>>;
+}
+
+impl<T: FingerprintableKey> Fingerprintable for T {
+    fn fingerprint(&self) -> PublicKeyFingerprint {
+        let fingerprint_parts = self.fingerprint_parts();
+        derive_fingerprint(fingerprint_parts)
+    }
+
+    fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool {
+        let fingerprint_parts = self.fingerprint_parts();
+        verify_fingerprint(fingerprint, fingerprint_parts)
+    }
+}
+
+/// Derives a fingerprint using a currently supported algorithm.
+/// Fingerprint_parts must be a canonical set of parts representing the public key.
+///
+/// The encoding needs to be canonical. That is, something like DER or PEM does *not* work,
+/// because the encoding could differ slightly between implementations. For RSA, using the modulus
+/// and exponent directly works.
+fn derive_fingerprint(fingerprint_parts: Vec<Vec<u8>>) -> PublicKeyFingerprint {
+    derive_fingerprint_from_parts(fingerprint_parts)
+}
+
+/// This function ensures an injective mapping of the inputs to the output hash.
+/// Concatenating the inputs does not work. For RSA this could mean that:
+/// with data = [N,E], |nnnnnn|ee|, and |nnnnnnn|e| would both be valid interpretations of the
+/// concatenation of the bytes, and thus may lead to the same hash for different (N,E) pairs.
+///
+/// This function hashes each input separately, concatenates the hashes, and then hashes the result.
+///
+/// Assumption: H is a cryptographic hash function, with respect to:
+/// - Second pre-image resistance
+///
+/// Assumption: H's output has a constant length output HS
+///
+/// Specifically, the construction is:
+/// H(H(data1)|H(data2)|...|H(dataN))
+///
+/// Given the assumptions above, then hashing each input separately, and concatenating the hashes is
+/// an injective mapping. Because there is an injective mapping, and because of collision resistance
+/// w.r.t. the final hash functions inputs, this also implies collision resistance w.r.t. data.
+fn derive_fingerprint_from_parts(data: Vec<Vec<u8>>) -> PublicKeyFingerprint {
+    let hash_set = data
+        .iter()
+        .map(|d| derive_fingerprint_single(d))
+        .collect::<Vec<_>>();
+    let concat = hash_set
+        .iter()
+        .flat_map(|h| h.digest.clone())
+        .collect::<Vec<_>>();
+    derive_fingerprint_single(&concat)
+}
+
+fn derive_fingerprint_single(data: &[u8]) -> PublicKeyFingerprint {
+    PublicKeyFingerprint {
+        digest: sha2::Sha256::digest(data).to_vec().into(),
+        algorithm: PublicKeyFingerprintAlgorithm::Sha256,
+    }
+}
+
+/// Verifies a fingerprint for a given public key, represented as a canonical list of parts.
+fn verify_fingerprint(fingerprint: &PublicKeyFingerprint, fingerprint_parts: Vec<Vec<u8>>) -> bool {
+    match fingerprint.algorithm {
+        PublicKeyFingerprintAlgorithm::Sha256 => {
+            let hash = derive_fingerprint_from_parts(fingerprint_parts);
+            hash.digest == fingerprint.digest
+        }
+    }
+}
diff --git a/crates/bitwarden-crypto/src/keys/key_id.rs b/crates/bitwarden-crypto/src/keys/key_id.rs
index 525a74183..cb772d519 100644
--- a/crates/bitwarden-crypto/src/keys/key_id.rs
+++ b/crates/bitwarden-crypto/src/keys/key_id.rs
@@ -10,7 +10,13 @@ pub(crate) const KEY_ID_SIZE: usize = 16;
 /// bytes, so something like a user key rotation is replacing the key with ID A with a new key with
 /// ID B.
 #[derive(Clone)]
-pub(crate) struct KeyId(uuid::Uuid);
+pub(crate) struct KeyId(Uuid);
+
+impl zeroize::Zeroize for KeyId {
+    fn zeroize(&mut self) {
+        self.0 = Uuid::nil();
+    }
+}
 
 /// Fixed length identifiers for keys.
 /// These are intended to be unique and constant per-key.
diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs
index 73362c3c8..55e341356 100644
--- a/crates/bitwarden-crypto/src/keys/mod.rs
+++ b/crates/bitwarden-crypto/src/keys/mod.rs
@@ -15,14 +15,17 @@ pub use asymmetric_crypto_key::{
     AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey,
 };
 mod signing_crypto_key;
+pub use signing_crypto_key::{SigningKey, *};
 mod user_key;
 pub use user_key::UserKey;
 mod device_key;
 pub use device_key::{DeviceKey, TrustDeviceResponse};
 mod pin_key;
 pub use pin_key::PinKey;
+mod fingerprint;
 mod kdf;
 mod key_id;
+pub(crate) use fingerprint::{Fingerprintable, FingerprintableKey, PublicKeyFingerprint};
 pub use kdf::{
     default_argon2_iterations, default_argon2_memory, default_argon2_parallelism,
     default_pbkdf2_iterations, Kdf,
diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs
index 4b50c084c..222474482 100644
--- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs
+++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs
@@ -1,14 +1,19 @@
-//! This file implements creation and verification of detached signatures
-
 use ciborium::{value::Integer, Value};
 use coset::{
-    iana::{self, Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter},
+    iana::{
+        self, Algorithm, CoapContentFormat, EllipticCurve, EnumI64, KeyOperation, KeyType,
+        OkpKeyParameter,
+    },
     CborSerializable, CoseKey, CoseSign1, Label, RegisteredLabel, RegisteredLabelWithPrivate,
 };
-use ed25519_dalek::Signer;
 use rand::rngs::OsRng;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "wasm")]
+use tsify_next::Tsify;
+use zeroize::ZeroizeOnDrop;
 
-use super::{key_id::KeyId, KEY_ID_SIZE};
+use super::{key_id::KeyId, CryptoKey, KEY_ID_SIZE};
 use crate::{
     cose::SIGNING_NAMESPACE,
     error::{Result, SignatureError},
@@ -16,51 +21,74 @@ use crate::{
     CryptoError,
 };
 
-#[allow(unused)]
-enum SigningCryptoKeyEnum {
+/// The type of key / signature scheme used for signing and verifying.
+#[derive(Serialize, Deserialize, Debug, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
+#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
+pub enum SignatureAlgorithm {
+    Ed25519,
+}
+
+impl SignatureAlgorithm {
+    /// Returns the currently accepted safe algorithm for new keys.
+    pub fn default_algorithm() -> Self {
+        SignatureAlgorithm::Ed25519
+    }
+}
+
+/// A `SigningKey` without the key id. This enum contains a variant for each supported signature
+/// scheme.
+#[derive(Clone, zeroize::ZeroizeOnDrop)]
+pub(crate) enum RawSigningKey {
     Ed25519(ed25519_dalek::SigningKey),
 }
 
-#[allow(unused)]
-enum VerifyingKeyEnum {
+/// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature
+/// scheme.
+pub(crate) enum RawVerifyingKey {
     Ed25519(ed25519_dalek::VerifyingKey),
 }
 
 /// A signing key is a private key used for signing data. An associated `VerifyingKey` can be
 /// derived from it.
-#[allow(unused)]
-struct SigningKey {
-    id: KeyId,
-    inner: SigningCryptoKeyEnum,
+#[derive(Clone, ZeroizeOnDrop)]
+pub struct SigningKey {
+    pub(crate) id: KeyId,
+    pub(crate) inner: RawSigningKey,
 }
 
+impl CryptoKey for SigningKey {}
+
 /// A verifying key is a public key used for verifying signatures. It can be published to other
 /// users, who can use it to verify that messages were signed by the holder of the corresponding
 /// `SigningKey`.
-#[allow(unused)]
-struct VerifyingKey {
+pub struct VerifyingKey {
     id: KeyId,
-    inner: VerifyingKeyEnum,
+    pub(crate) inner: RawVerifyingKey,
 }
 
-#[allow(unused)]
 impl SigningKey {
-    fn make_ed25519() -> Result<Self> {
-        Ok(SigningKey {
-            id: KeyId::make(),
-            inner: SigningCryptoKeyEnum::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)),
-        })
+    /// Makes a new signing key for the given signature scheme.
+    pub fn make(key_algorithm: SignatureAlgorithm) -> Result<Self> {
+        match key_algorithm {
+            SignatureAlgorithm::Ed25519 => Ok(SigningKey {
+                id: KeyId::make(),
+                inner: RawSigningKey::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)),
+            }),
+        }
     }
 
-    fn cose_algorithm(&self) -> Algorithm {
+    pub(crate) fn cose_algorithm(&self) -> Algorithm {
         match &self.inner {
-            SigningCryptoKeyEnum::Ed25519(_) => Algorithm::EdDSA,
+            RawSigningKey::Ed25519(_) => Algorithm::EdDSA,
         }
     }
 
-    fn to_cose(&self) -> Result<Vec<u8>> {
+    /// Serializes the signing key to a COSE-formatted byte array.
+    pub fn to_cose(&self) -> Result<Vec<u8>> {
         match &self.inner {
-            SigningCryptoKeyEnum::Ed25519(key) => {
+            RawSigningKey::Ed25519(key) => {
                 coset::CoseKeyBuilder::new_okp_key()
                     .key_id((&self.id).into())
                     .algorithm(Algorithm::EdDSA)
@@ -81,7 +109,8 @@ impl SigningKey {
         }
     }
 
-    fn from_cose(bytes: &[u8]) -> Result<Self> {
+    /// Deserializes a COSE-formatted byte array into a signing key.
+    pub fn from_cose(bytes: &[u8]) -> Result<Self> {
         let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?;
         let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty)
         else {
@@ -129,7 +158,7 @@ impl SigningKey {
                     let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes);
                     Ok(SigningKey {
                         id: key_id,
-                        inner: SigningCryptoKeyEnum::Ed25519(key),
+                        inner: RawSigningKey::Ed25519(key),
                     })
                 } else {
                     Err(CryptoError::InvalidKey)
@@ -139,81 +168,31 @@ impl SigningKey {
         }
     }
 
-    /// Signs the given payload with the signing key, under a given namespace.
-    /// This returns a [`Signature`] object, that does not contain the payload.
-    /// The payload must be stored separately, and needs to be provided when verifying the
-    /// signature.
-    ///
-    /// This should be used when multiple signers are required, or when signatures need to be
-    /// replaceable without re-uploading the object, or if the signed object should be parseable
-    /// by the server side, without the use of COSE on the server.
-    pub(crate) fn sign_detached(&self, namespace: &SigningNamespace, data: &[u8]) -> Signature {
-        Signature::from(
-            coset::CoseSign1Builder::new()
-                .protected(
-                    coset::HeaderBuilder::new()
-                        .algorithm(self.cose_algorithm())
-                        .key_id((&self.id).into())
-                        .value(
-                            SIGNING_NAMESPACE,
-                            ciborium::Value::Integer(Integer::from(namespace.as_i64())),
-                        )
-                        .build(),
-                )
-                .create_detached_signature(data, &[], |pt| self.sign_raw(pt))
-                .build(),
-        )
-    }
-
-    /// Signs the given payload with the signing key, under a given namespace.
-    /// This returns a [`SignedObject`] object, that contains the payload.
-    /// The payload is included in the signature, and does not need to be provided when verifying
-    /// the signature.
-    ///
-    /// This should be used when only one signer is required, so that only one object needs to be
-    /// kept track of.
-    pub(crate) fn sign(&self, namespace: &SigningNamespace, data: &[u8]) -> Result<SignedObject> {
-        let cose_sign1 = coset::CoseSign1Builder::new()
-            .protected(
-                coset::HeaderBuilder::new()
-                    .algorithm(self.cose_algorithm())
-                    .key_id((&self.id).into())
-                    .value(
-                        SIGNING_NAMESPACE,
-                        ciborium::Value::Integer(Integer::from(namespace.as_i64())),
-                    )
-                    .build(),
-            )
-            .payload(data.to_vec())
-            .create_signature(&[], |pt| self.sign_raw(pt))
-            .build();
-        Ok(SignedObject(cose_sign1))
-    }
-
-    /// Signs the given byte array with the signing key.
-    /// This should never be used directly, but only through the `sign` method, to enforce
-    /// strong domain separation of the signatures.
-    fn sign_raw(&self, data: &[u8]) -> Vec<u8> {
+    /// Derives the verifying key from the signing key. The key id is the same for the signing and
+    /// verifying key, since they are a pair.
+    pub fn to_verifying_key(&self) -> VerifyingKey {
         match &self.inner {
-            SigningCryptoKeyEnum::Ed25519(key) => key.sign(data).to_bytes().to_vec(),
+            RawSigningKey::Ed25519(key) => VerifyingKey {
+                id: self.id.clone(),
+                inner: RawVerifyingKey::Ed25519(key.verifying_key()),
+            },
         }
     }
 
-    fn to_verifying_key(&self) -> VerifyingKey {
+    #[allow(unused)]
+    fn algorithm(&self) -> SignatureAlgorithm {
         match &self.inner {
-            SigningCryptoKeyEnum::Ed25519(key) => VerifyingKey {
-                id: self.id.clone(),
-                inner: VerifyingKeyEnum::Ed25519(key.verifying_key()),
-            },
+            RawSigningKey::Ed25519(_) => SignatureAlgorithm::Ed25519,
         }
     }
 }
 
 #[allow(unused)]
 impl VerifyingKey {
-    fn to_cose(&self) -> Result<Vec<u8>> {
+    /// Serializes the verifying key to a COSE-formatted byte array.
+    pub fn to_cose(&self) -> Result<Vec<u8>> {
         match &self.inner {
-            VerifyingKeyEnum::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key()
+            RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key()
                 .key_id((&self.id).into())
                 .algorithm(Algorithm::EdDSA)
                 .param(
@@ -235,7 +214,8 @@ impl VerifyingKey {
         }
     }
 
-    fn from_cose(bytes: &[u8]) -> Result<Self> {
+    /// Deserializes a COSE-formatted byte array into a verifying key.
+    pub fn from_cose(bytes: &[u8]) -> Result<Self> {
         let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?;
 
         let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty)
@@ -285,7 +265,7 @@ impl VerifyingKey {
                             .map_err(|_| CryptoError::InvalidKey)?;
                     Ok(VerifyingKey {
                         id: key_id,
-                        inner: VerifyingKeyEnum::Ed25519(verifying_key),
+                        inner: RawVerifyingKey::Ed25519(verifying_key),
                     })
                 } else {
                     Err(CryptoError::InvalidKey)
@@ -295,67 +275,10 @@ impl VerifyingKey {
         }
     }
 
-    /// Verifies the signature of the given data, for the given namespace.
-    /// This should never be used directly, but only through the `verify` method, to enforce
-    /// strong domain separation of the signatures.
-    pub(crate) fn verify_signature(
-        &self,
-        namespace: &SigningNamespace,
-        signature: &Signature,
-        data: &[u8],
-    ) -> bool {
-        let Some(_alg) = &signature.inner().protected.header.alg else {
-            return false;
-        };
-
-        let Ok(signature_namespace) = signature.namespace() else {
-            return false;
-        };
-        if signature_namespace != *namespace {
-            return false;
-        }
-
-        signature
-            .inner()
-            .verify_detached_signature(data, &[], |sig, data| self.verify_raw(sig, data))
-            .is_ok()
-    }
-
-    /// Verifies the signature of a signed object, for the given namespace, and returns the payload.
-    pub(crate) fn get_verified_payload(
-        &self,
-        namespace: &SigningNamespace,
-        signature: &SignedObject,
-    ) -> Result<Vec<u8>> {
-        let Some(_alg) = &signature.inner().protected.header.alg else {
-            return Err(SignatureError::InvalidSignature.into());
-        };
-
-        let signature_namespace = signature.namespace()?;
-        if signature_namespace != *namespace {
-            return Err(SignatureError::InvalidNamespace.into());
-        }
-
-        signature
-            .inner()
-            .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?;
-        signature.payload()
-    }
-
-    /// Verifies the signature of the given data, for the given namespace.
-    /// This should never be used directly, but only through the `verify` method, to enforce
-    /// strong domain separation of the signatures.
-    fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<()> {
+    /// Returns the signature scheme used by the verifying key.
+    pub fn algorithm(&self) -> SignatureAlgorithm {
         match &self.inner {
-            VerifyingKeyEnum::Ed25519(key) => {
-                let sig = ed25519_dalek::Signature::from_bytes(
-                    signature
-                        .try_into()
-                        .map_err(|_| SignatureError::InvalidSignature)?,
-                );
-                key.verify_strict(data, &sig)
-                    .map_err(|_| SignatureError::InvalidSignature.into())
-            }
+            RawVerifyingKey::Ed25519(_) => SignatureAlgorithm::Ed25519,
         }
     }
 }
@@ -363,8 +286,7 @@ impl VerifyingKey {
 /// A signature cryptographically attests to a (namespace, data) pair. The namespace is included in
 /// the signature object, the data is not. One data object can be signed multiple times, with
 /// different namespaces / by different signers, depending on the application needs.
-#[allow(unused)]
-struct Signature(CoseSign1);
+pub struct Signature(CoseSign1);
 
 impl From<CoseSign1> for Signature {
     fn from(cose_sign1: CoseSign1) -> Self {
@@ -374,24 +296,24 @@ impl From<CoseSign1> for Signature {
 
 #[allow(unused)]
 impl Signature {
-    fn from_cose(bytes: &[u8]) -> Result<Self, CryptoError> {
+    pub(crate) fn from_cose(bytes: &[u8]) -> Result<Self, CryptoError> {
         let cose_sign1 =
             CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?;
         Ok(Signature(cose_sign1))
     }
 
-    fn to_cose(&self) -> Result<Vec<u8>> {
+    pub(crate) fn to_cose(&self) -> Result<Vec<u8>> {
         self.0
             .clone()
             .to_vec()
             .map_err(|_| SignatureError::InvalidSignature.into())
     }
 
-    fn inner(&self) -> &CoseSign1 {
+    pub(crate) fn inner(&self) -> &CoseSign1 {
         &self.0
     }
 
-    fn namespace(&self) -> Result<SigningNamespace> {
+    pub(crate) fn namespace(&self) -> Result<SigningNamespace> {
         let mut namespace = None;
         for (key, value) in &self.0.protected.header.rest {
             if let Label::Int(key) = key {
@@ -409,12 +331,26 @@ impl Signature {
         let namespace: i128 = namespace.into();
         SigningNamespace::try_from_i64(namespace as i64)
     }
+
+    pub(crate) fn content_type(&self) -> Result<CoapContentFormat, CryptoError> {
+        if let RegisteredLabel::Assigned(content_format) = self
+            .0
+            .protected
+            .header
+            .content_type
+            .clone()
+            .ok_or(CryptoError::from(SignatureError::InvalidSignature))?
+        {
+            Ok(content_format)
+        } else {
+            Err(SignatureError::InvalidSignature.into())
+        }
+    }
 }
 
 /// A signed object has a cryptographical attestation to a (namespace, data) pair. The namespace and
 /// data are included in the signature object.
-#[allow(unused)]
-struct SignedObject(CoseSign1);
+pub struct SignedObject(pub(crate) CoseSign1);
 
 impl From<CoseSign1> for SignedObject {
     fn from(cose_sign1: CoseSign1) -> Self {
@@ -422,26 +358,43 @@ impl From<CoseSign1> for SignedObject {
     }
 }
 
+impl SignedObject {
+    pub fn content_type(&self) -> Result<CoapContentFormat, CryptoError> {
+        if let RegisteredLabel::Assigned(content_format) = self
+            .0
+            .protected
+            .header
+            .content_type
+            .clone()
+            .ok_or(CryptoError::from(SignatureError::InvalidSignature))?
+        {
+            Ok(content_format)
+        } else {
+            Err(SignatureError::InvalidSignature.into())
+        }
+    }
+}
+
 #[allow(unused)]
 impl SignedObject {
-    fn from_cose(bytes: &[u8]) -> Result<Self, CryptoError> {
+    pub(crate) fn from_cose(bytes: &[u8]) -> Result<Self, CryptoError> {
         let cose_sign1 =
             CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?;
         Ok(SignedObject(cose_sign1))
     }
 
-    fn to_cose(&self) -> Result<Vec<u8>> {
+    pub(crate) fn to_cose(&self) -> Result<Vec<u8>> {
         self.0
             .clone()
             .to_vec()
             .map_err(|_| SignatureError::InvalidSignature.into())
     }
 
-    fn inner(&self) -> &CoseSign1 {
+    pub(crate) fn inner(&self) -> &CoseSign1 {
         &self.0
     }
 
-    fn namespace(&self) -> Result<SigningNamespace> {
+    pub(crate) fn namespace(&self) -> Result<SigningNamespace> {
         let mut namespace = None;
         for (key, value) in &self.0.protected.header.rest {
             if let Label::Int(key) = key {
@@ -456,11 +409,14 @@ impl SignedObject {
         let Some(namespace) = namespace.as_integer() else {
             return Err(SignatureError::InvalidNamespace.into());
         };
-        let namespace: i128 = namespace.into();
-        SigningNamespace::try_from_i64(namespace as i64)
+        SigningNamespace::try_from_i64(
+            namespace
+                .try_into()
+                .map_err(|_| SignatureError::InvalidNamespace)?,
+        )
     }
 
-    fn payload(&self) -> Result<Vec<u8>> {
+    pub fn payload(&self) -> Result<Vec<u8>> {
         self.0
             .payload
             .as_ref()
@@ -471,146 +427,24 @@ impl SignedObject {
 
 #[cfg(test)]
 mod tests {
-    use super::*;
-
-    const SIGNING_KEY: &[u8] = &[
-        166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197,
-        3, 39, 4, 130, 1, 2, 35, 88, 32, 31, 72, 18, 5, 81, 182, 75, 229, 106, 91, 174, 171, 136,
-        48, 87, 10, 231, 220, 24, 134, 42, 189, 54, 217, 51, 206, 23, 49, 140, 165, 23, 125, 32, 6,
-    ];
-    const VERIFYING_KEY: &[u8] = &[
-        166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197,
-        3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 40, 62, 139, 254, 182, 152, 40, 135, 232, 175, 93,
-        191, 16, 31, 208, 54, 5, 136, 208, 14, 159, 199, 204, 209, 11, 161, 171, 213, 128, 101,
-        224, 160,
-    ];
-    /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data
-    const SIGNATURE: &[u8] = &[
-        132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249,
-        52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117,
-        179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72,
-        208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50,
-        154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5,
-    ];
-    const SIGNED_OBJECT: &[u8] = &[
-        132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249,
-        52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 76, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97,
-        103, 101, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225,
-        126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144,
-        84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50,
-        73, 36, 74, 34, 132, 5,
-    ];
-
-    #[test]
-    fn test_signature_using_test_vectors() {
-        let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap();
-        let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap();
-        let signature = Signature::from_cose(SIGNATURE).unwrap();
-
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-
-        assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY);
-        assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY);
-        assert_eq!(signature.to_cose().unwrap(), SIGNATURE);
-
-        assert!(verifying_key.verify_signature(&namespace, &signature, data));
-    }
-
-    #[test]
-    fn test_signed_object_using_test_vectors() {
-        let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap();
-        let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap();
-        let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap();
-
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-
-        assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY);
-        assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY);
-        assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT);
-
-        let payload = verifying_key
-            .get_verified_payload(&namespace, &signed_object)
-            .unwrap();
-        assert_eq!(payload, data);
-    }
+    use coset::CoseSign1Builder;
 
-    #[test]
-    fn test_sign_detached_roundtrip() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let verifying_key = signing_key.to_verifying_key();
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-
-        let signature = signing_key.sign_detached(&namespace, data);
-        assert!(verifying_key.verify_signature(&namespace, &signature, data));
-    }
-
-    #[test]
-    fn test_sign_roundtrip() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let verifying_key = signing_key.to_verifying_key();
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-        let signed_object = signing_key.sign(&namespace, data).unwrap();
-        let payload = verifying_key
-            .get_verified_payload(&namespace, &signed_object)
-            .unwrap();
-        assert_eq!(payload, data);
-    }
-
-    #[test]
-    fn test_changed_payload_fails() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let verifying_key = signing_key.to_verifying_key();
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-
-        let signature = signing_key.sign_detached(&namespace, data);
-        assert!(!verifying_key.verify_signature(&namespace, &signature, b"Test message 2"));
-    }
-
-    #[test]
-    fn test_changed_namespace_fails() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let verifying_key = signing_key.to_verifying_key();
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-        let other_namespace = SigningNamespace::Test;
-
-        let signature = signing_key.sign_detached(&namespace, data);
-        assert!(!verifying_key.verify_signature(&other_namespace, &signature, data));
-    }
-
-    #[test]
-    fn test_changed_namespace_fails_signed_object() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let verifying_key = signing_key.to_verifying_key();
-        let data = b"Test message";
-        let namespace = SigningNamespace::EncryptionMetadata;
-        let other_namespace = SigningNamespace::Test;
-        let signed_object = signing_key.sign(&namespace, data).unwrap();
-        assert!(verifying_key
-            .get_verified_payload(&other_namespace, &signed_object)
-            .is_err());
-    }
+    use super::*;
 
     #[test]
     fn test_cose_roundtrip_signature() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let cose =
-            signing_key.sign_detached(&SigningNamespace::EncryptionMetadata, b"Test message");
-        let cose = cose.to_cose().unwrap();
+        let sig = CoseSign1Builder::new().build();
+        let signature = Signature(sig.clone());
+        let cose = signature.to_cose().unwrap();
         let parsed_cose = Signature::from_cose(&cose).unwrap();
         assert_eq!(cose, parsed_cose.to_cose().unwrap());
     }
 
     #[test]
     fn test_cose_roundtrip_signed_object() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
         let cose = signing_key
-            .sign(&SigningNamespace::EncryptionMetadata, b"Test message")
+            .sign(&"test", &SigningNamespace::ExampleNamespace)
             .unwrap();
         let cose = cose.to_cose().unwrap();
         let parsed_cose = SignedObject::from_cose(&cose).unwrap();
@@ -619,7 +453,7 @@ mod tests {
 
     #[test]
     fn test_cose_roundtrip_encode_signing() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
         let cose = signing_key.to_cose().unwrap();
         let parsed_key = SigningKey::from_cose(&cose).unwrap();
 
@@ -628,16 +462,4 @@ mod tests {
             parsed_key.to_cose().unwrap()
         );
     }
-
-    #[test]
-    fn test_cose_roundtrip_encode_verifying() {
-        let signing_key = SigningKey::make_ed25519().unwrap();
-        let cose = signing_key.to_verifying_key().to_cose().unwrap();
-        let parsed_key = VerifyingKey::from_cose(&cose).unwrap();
-
-        assert_eq!(
-            signing_key.to_verifying_key().to_cose().unwrap(),
-            parsed_key.to_cose().unwrap()
-        );
-    }
 }
diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
index 91e001290..ea616c35a 100644
--- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
+++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
@@ -298,6 +298,12 @@ impl TryFrom<&mut [u8]> for SymmetricCryptoKey {
     }
 }
 
+impl From<Aes256CbcHmacKey> for SymmetricCryptoKey {
+    fn from(key: Aes256CbcHmacKey) -> Self {
+        SymmetricCryptoKey::Aes256CbcHmacKey(key)
+    }
+}
+
 impl CryptoKey for SymmetricCryptoKey {}
 
 // We manually implement these to make sure we don't print any sensitive data
diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs
index 70ee3aff3..e7c5526ed 100644
--- a/crates/bitwarden-crypto/src/lib.rs
+++ b/crates/bitwarden-crypto/src/lib.rs
@@ -32,6 +32,7 @@ mod store;
 pub use store::{KeyStore, KeyStoreContext};
 mod cose;
 mod signing;
+pub use signing::*;
 mod traits;
 mod xchacha20;
 pub use traits::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds};
diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs
new file mode 100644
index 000000000..c9da88667
--- /dev/null
+++ b/crates/bitwarden-crypto/src/signing/claims.rs
@@ -0,0 +1,100 @@
+use serde::{Deserialize, Serialize};
+
+use super::SigningNamespace;
+use crate::{
+    keys::Fingerprintable, AsymmetricPublicCryptoKey, CryptoError, FingerprintableKey,
+    PublicKeyFingerprint, SignedObject, SigningKey, VerifyingKey,
+};
+
+/// The non-serialized version of `PublicKeyOwnershipClaim`
+#[derive(Serialize, Deserialize, Debug)]
+pub(crate) struct PublicKeyOwnershipClaim {
+    pub(crate) fingerprint: PublicKeyFingerprint,
+}
+
+impl PublicKeyOwnershipClaim {
+    pub(crate) fn for_public_key(public_key: &impl FingerprintableKey) -> Self {
+        Self {
+            fingerprint: public_key.fingerprint(),
+        }
+    }
+}
+
+/// A user or org shall only have one long-term cryptographic identity. This is the signing key. A
+/// user also needs to receive messages asymmetrically shared to them. Thus, an object tying the
+/// signing key to the asymmetric encryption public key is needed. A signed public key ownership
+/// claim represents a claim by a signing key that it owns a specific public encryption key. This is
+/// used to tie the cryptographic identity (signing) to the encryption receiving identity
+/// (asymmetric encryption key).
+///
+/// 1. Initially, Alice knows Bob's cryptographic identity (verifying key).
+/// 2. Alice wants to send a message to Bob using his public encryption key.
+/// 3. Alice gets Bob's public encryption key from the server, along with the
+///    [`SignedPublicKeyOwnershipClaim`].
+/// 4. Alice verifies the claim using Bob's verifying key that she trusts.
+/// ```
+/// use rand::rngs::OsRng;
+/// use bitwarden_crypto::{AsymmetricCryptoKey, CryptoError, SigningKey, VerifyingKey, SignedPublicKeyOwnershipClaim, SignatureAlgorithm};
+///
+/// // Initial setup
+/// let bob_signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+/// let bob_verifying_key = bob_signing_key.to_verifying_key();
+/// let bob_public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key();
+///
+/// // Alice trusts Bob's verifying key - this becomes Bob's cryptographic identity.
+/// let bob_claim = SignedPublicKeyOwnershipClaim::make_claim_with_key(&bob_public_key, &bob_signing_key).unwrap();
+/// // Alice downloads Bob's public key from the server.
+/// // Alice verifies the claim using Bob's verifying key.
+/// assert!(bob_claim.verify_claim(&bob_public_key, &bob_verifying_key).unwrap());
+/// // Alice can now send a message to Bob using his public encryption key.
+pub struct SignedPublicKeyOwnershipClaim(Vec<u8>);
+
+impl SignedPublicKeyOwnershipClaim {
+    /// Creates a new `SignedPublicKeyOwnershipClaim` for the provided public key and signing key.
+    pub fn make_claim_with_key(
+        public_key: &AsymmetricPublicCryptoKey,
+        signing_key: &SigningKey,
+    ) -> Result<Self, CryptoError> {
+        let claim = PublicKeyOwnershipClaim::for_public_key(public_key);
+        let signature = signing_key.sign(&claim, &SigningNamespace::PublicKeyOwnershipClaim)?;
+        Ok(Self(signature.to_cose()?))
+    }
+
+    /// Verifies the signed claim using the provided public key and verifying key.
+    pub fn verify_claim(
+        &self,
+        public_key: &AsymmetricPublicCryptoKey,
+        verifying_key: &VerifyingKey,
+    ) -> Result<bool, CryptoError> {
+        let signed_object = SignedObject::from_cose(&self.0)?;
+        let claim: PublicKeyOwnershipClaim = verifying_key
+            .get_verified_payload(&signed_object, &SigningNamespace::PublicKeyOwnershipClaim)?;
+        Ok(public_key.verify_fingerprint(&claim.fingerprint))
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0
+    }
+
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
+        Ok(Self(bytes.to_vec()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rand::rngs::OsRng;
+
+    use super::*;
+    use crate::{AsymmetricCryptoKey, SignatureAlgorithm};
+
+    #[test]
+    fn test_public_key_ownership_claim() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key();
+        let claim =
+            SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signing_key).unwrap();
+        assert!(claim.verify_claim(&public_key, &verifying_key).unwrap());
+    }
+}
diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs
index 4025a6f44..b9cdfd7fc 100644
--- a/crates/bitwarden-crypto/src/signing/mod.rs
+++ b/crates/bitwarden-crypto/src/signing/mod.rs
@@ -1,27 +1,8 @@
-use crate::{error::SignatureError, CryptoError};
+//! Note
 
-/// Signing is domain-separated within bitwarden, to prevent cross protocol attacks.
-///
-/// A new signed entity or protocol shall use a new signing namespace.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SigningNamespace {
-    #[allow(dead_code)]
-    EncryptionMetadata = 1,
-    #[cfg(test)]
-    Test = -1,
-}
-
-impl SigningNamespace {
-    pub fn as_i64(&self) -> i64 {
-        *self as i64
-    }
-
-    pub fn try_from_i64(value: i64) -> Result<Self, CryptoError> {
-        match value {
-            1 => Ok(Self::EncryptionMetadata),
-            #[cfg(test)]
-            -1 => Ok(Self::Test),
-            _ => Err(SignatureError::InvalidNamespace.into()),
-        }
-    }
-}
+mod claims;
+pub use claims::SignedPublicKeyOwnershipClaim;
+mod namespace;
+pub use namespace::SigningNamespace;
+mod sign;
+pub use sign::*;
diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs
new file mode 100644
index 000000000..b94218475
--- /dev/null
+++ b/crates/bitwarden-crypto/src/signing/namespace.rs
@@ -0,0 +1,28 @@
+use crate::{error::SignatureError, CryptoError};
+
+/// Signing is domain-separated within bitwarden, to prevent cross protocol attacks.
+///
+/// A new signed entity or protocol shall use a new signing namespace. Generally, this means
+/// that a signing namespace has exactly one associated valid message struct.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SigningNamespace {
+    /// The namespace for
+    /// [`PublicKeyOwnershipClaim`](crate::signing::claims::PublicKeyOwnershipClaim).
+    PublicKeyOwnershipClaim = 1,
+    /// This namespace is only used in tests and documentation.
+    ExampleNamespace = -1,
+}
+
+impl SigningNamespace {
+    pub fn as_i64(&self) -> i64 {
+        *self as i64
+    }
+
+    pub fn try_from_i64(value: i64) -> Result<Self, CryptoError> {
+        match value {
+            1 => Ok(Self::PublicKeyOwnershipClaim),
+            -1 => Ok(Self::ExampleNamespace),
+            _ => Err(SignatureError::InvalidNamespace.into()),
+        }
+    }
+}
diff --git a/crates/bitwarden-crypto/src/signing/sign.rs b/crates/bitwarden-crypto/src/signing/sign.rs
new file mode 100644
index 000000000..33b07ff3c
--- /dev/null
+++ b/crates/bitwarden-crypto/src/signing/sign.rs
@@ -0,0 +1,588 @@
+//! Signing is used to assert integrity of a message to others or to oneself.
+//!
+//! Signing and signature verification operations are divided into three layers here:
+//! - (public) High-level: Give a struct, namespace, and get a signed object or signature +
+//!   serialized message. Purpose: Serialization should not be decided by the consumer of this
+//!   interface, but rather by the signing implementation. Each consumer shouldn't have to make the
+//!   decision on how to serialize. Further, the serialization format is written to the signature
+//!   object, and verified.
+//! - Mid-level: Give a byte array, content format, namespace, and get a signed object or signature.
+//!   Purpose: All signatures should be domain-separated, so that any proofs only need to consider
+//!   the allowed messages under the current namespace, and cross-protocol attacks are not possible.
+//! - Low-level: Give a byte array, and get a signature. Purpose: This just implements the signing
+//!   of byte arrays. Digital signature schemes generally just care about a set of input bytes to
+//!   sign; and this operation implements that per-supported digital signature scheme. To add
+//!   support for a new scheme, only this operation needs to be implemented for the new signing key
+//!   type.
+//!
+//! Further, there are two kinds of signing operations supported here:
+//! - Sign: Create a signed object that contains the payload. Purpose: If only one signature is
+//!   needed for an object then it is simpler to keep the signature and payload together in one
+//!   blob, so they cannot be separated.
+//! - Sign detached: Create a signature that does not contain the payload; but the serialized
+//!   payload is returned. Purpose: If multiple signatures are needed for one object, then sign
+//!   detached can be used.
+
+use ciborium::value::Integer;
+use coset::iana::CoapContentFormat;
+use ed25519_dalek::Signer;
+use serde::{de::DeserializeOwned, Serialize};
+
+use super::SigningNamespace;
+use crate::{
+    cose::SIGNING_NAMESPACE, error::SignatureError, CryptoError, RawSigningKey, RawVerifyingKey,
+    Signature, SignedObject, SigningKey, VerifyingKey,
+};
+
+impl SigningKey {
+    /// Signs the given payload with the signing key, under a given [`SigningNamespace`].
+    /// This returns a [`Signature`] object, that does not contain the payload.
+    /// The payload must be stored separately, and needs to be provided when verifying the
+    /// signature.
+    ///
+    /// This should be used when multiple signers are required, or when signatures need to be
+    /// replaceable without re-uploading the object, or if the signed object should be parseable
+    /// by the server side, without the use of COSE on the server.
+    /// ```
+    /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey};
+    /// use serde::{Serialize, Deserialize};
+    ///
+    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
+    /// struct TestMessage {
+    ///  field1: String,
+    /// }
+    ///
+    /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+    /// let message = TestMessage {
+    ///  field1: "Test message".to_string(),
+    /// };
+    /// let namespace = SigningNamespace::ExampleNamespace;
+    /// let (signature, serialized_message) = signing_key.sign_detached(&message, &namespace).unwrap();
+    /// // Verification
+    /// let verifying_key = signing_key.to_verifying_key();
+    /// assert!(verifying_key.verify_signature(&serialized_message.as_ref(), &namespace, &signature));
+    /// ```
+    #[allow(unused)]
+    pub fn sign_detached<Message: Serialize>(
+        &self,
+        message: &Message,
+        namespace: &SigningNamespace,
+    ) -> Result<(Signature, SerializedMessage), CryptoError> {
+        let message = encode_message(message)?;
+        Ok((self.sign_detached_bytes(&message, namespace), message))
+    }
+
+    /// Given a serialized message, signature, this counter-signs the message. That is, if multiple
+    /// parties want to sign the same message, one party creates the initial message, and the
+    /// other parties then counter-sign it, and submit their signatures. This can be done as
+    /// follows: ```
+    /// let alice_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+    /// let bob_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+    ///
+    /// let message = TestMessage {
+    ///    field1: "Test message".to_string(),
+    /// };
+    /// let namespace = SigningNamespace::ExampleNamespace;
+    /// let (signature, serialized_message) = alice_key.sign_detached(&message,
+    /// &namespace).unwrap();\ // Alice shares (signature, serialized_message) with Bob.
+    /// // Bob verifies the contents of serialized_message using application logic, then signs it:
+    /// let (bob_signature, serialized_message) = bob_key.counter_sign(&serialized_message,
+    /// &signature, &namespace).unwrap(); ```
+    #[allow(unused)]
+    pub fn counter_sign_detached(
+        &self,
+        serialized_message_bytes: Vec<u8>,
+        initial_signature: &Signature,
+        namespace: &SigningNamespace,
+    ) -> Result<Signature, CryptoError> {
+        // The namespace should be passed in to make sure the namespace the counter-signer is
+        // expecting to sign for is the same as the one that the signer used
+        if initial_signature.namespace()? != *namespace {
+            return Err(SignatureError::InvalidNamespace.into());
+        }
+
+        Ok(self.sign_detached_bytes(
+            &SerializedMessage {
+                serialized_message_bytes,
+                content_type: initial_signature.content_type()?,
+            },
+            namespace,
+        ))
+    }
+
+    /// Signs the given payload with the signing key, under a given namespace.
+    /// This is is the underlying implementation of the `sign_detached` method, and takes
+    /// a raw byte array as input.
+    fn sign_detached_bytes(
+        &self,
+        message: &SerializedMessage,
+        namespace: &SigningNamespace,
+    ) -> Signature {
+        Signature::from(
+            coset::CoseSign1Builder::new()
+                .protected(
+                    coset::HeaderBuilder::new()
+                        .algorithm(self.cose_algorithm())
+                        .key_id((&self.id).into())
+                        .content_format(message.content_type)
+                        .value(
+                            SIGNING_NAMESPACE,
+                            ciborium::Value::Integer(Integer::from(namespace.as_i64())),
+                        )
+                        .build(),
+                )
+                .create_detached_signature(&message.serialized_message_bytes, &[], |pt| {
+                    self.sign_raw(pt)
+                })
+                .build(),
+        )
+    }
+
+    /// Signs the given payload with the signing key, under a given namespace.
+    /// This returns a [`SignedObject`] object, that contains the payload.
+    /// The payload is included in the signature, and does not need to be provided when verifying
+    /// the signature.
+    ///
+    /// This should be used when only one signer is required, so that only one object needs to be
+    /// kept track of.
+    /// ```
+    /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey};
+    /// use serde::{Serialize, Deserialize};
+    ///
+    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
+    /// struct TestMessage {
+    ///   field1: String,
+    /// }
+    ///
+    /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+    /// let message = TestMessage {
+    ///   field1: "Test message".to_string(),
+    /// };
+    /// let namespace = SigningNamespace::ExampleNamespace;
+    /// let signed_object = signing_key.sign(&message, &namespace).unwrap();
+    /// // The signed object can be verified using the verifying key:
+    /// let verifying_key = signing_key.to_verifying_key();
+    /// let payload: TestMessage = verifying_key.get_verified_payload(&signed_object, &namespace).unwrap();
+    /// assert_eq!(payload, message);
+    /// ```
+    pub fn sign<Message: Serialize>(
+        &self,
+        message: &Message,
+        namespace: &SigningNamespace,
+    ) -> Result<SignedObject, CryptoError> {
+        let message = encode_message(message)?;
+        self.sign_bytes(&message, namespace)
+    }
+
+    /// Signs the given payload with the signing key, under a given namespace.
+    /// This is is the underlying implementation of the `sign` method, and takes
+    /// a raw byte array as input.
+    fn sign_bytes(
+        &self,
+        serialized_message: &SerializedMessage,
+        namespace: &SigningNamespace,
+    ) -> Result<SignedObject, CryptoError> {
+        let cose_sign1 = coset::CoseSign1Builder::new()
+            .protected(
+                coset::HeaderBuilder::new()
+                    .algorithm(self.cose_algorithm())
+                    .key_id((&self.id).into())
+                    .content_format(serialized_message.content_type)
+                    .value(
+                        SIGNING_NAMESPACE,
+                        ciborium::Value::Integer(Integer::from(namespace.as_i64())),
+                    )
+                    .build(),
+            )
+            .payload(serialized_message.serialized_message_bytes.clone())
+            .create_signature(&[], |pt| self.sign_raw(pt))
+            .build();
+        Ok(SignedObject(cose_sign1))
+    }
+
+    /// Signs the given byte array with the signing key.
+    /// This should never be used directly, but only through the `sign` method, to enforce
+    /// strong domain separation of the signatures.
+    fn sign_raw(&self, data: &[u8]) -> Vec<u8> {
+        match &self.inner {
+            RawSigningKey::Ed25519(key) => key.sign(data).to_bytes().to_vec(),
+        }
+    }
+}
+
+/// A message (struct), serialized to a byte array, along with the content format of the bytes.
+pub struct SerializedMessage {
+    serialized_message_bytes: Vec<u8>,
+    content_type: CoapContentFormat,
+}
+
+impl AsRef<[u8]> for SerializedMessage {
+    fn as_ref(&self) -> &[u8] {
+        &self.serialized_message_bytes
+    }
+}
+
+impl SerializedMessage {
+    pub fn from_bytes(bytes: Vec<u8>, content_type: CoapContentFormat) -> Self {
+        SerializedMessage {
+            serialized_message_bytes: bytes,
+            content_type,
+        }
+    }
+}
+
+impl VerifyingKey {
+    /// Verifies the signature of the given serialized message bytes, created by
+    /// [`SigningKey::sign_detached`], for the given namespace. The namespace must match the one
+    /// used to create the signature.
+    #[allow(unused)]
+    pub fn verify_signature(
+        &self,
+        serialized_message_bytes: &[u8],
+        namespace: &SigningNamespace,
+        signature: &Signature,
+    ) -> bool {
+        let Some(_alg) = &signature.inner().protected.header.alg else {
+            return false;
+        };
+
+        let Ok(signature_namespace) = signature.namespace() else {
+            return false;
+        };
+        if signature_namespace != *namespace {
+            return false;
+        }
+
+        signature
+            .inner()
+            .verify_detached_signature(serialized_message_bytes, &[], |sig, data| {
+                self.verify_raw(sig, data)
+            })
+            .is_ok()
+    }
+
+    /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given
+    /// namespace and returns the deserialized payload, if the signature is valid.
+    pub fn get_verified_payload<Message: DeserializeOwned>(
+        &self,
+        signed_object: &SignedObject,
+        namespace: &SigningNamespace,
+    ) -> Result<Message, CryptoError> {
+        let payload_bytes = self.get_verified_payload_bytes(signed_object, namespace)?;
+        decode_message(&SerializedMessage {
+            serialized_message_bytes: payload_bytes,
+            content_type: signed_object.content_type()?,
+        })
+    }
+
+    /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given
+    /// namespace and returns the raw payload bytes, if the signature is valid.
+    fn get_verified_payload_bytes(
+        &self,
+        signed_object: &SignedObject,
+        namespace: &SigningNamespace,
+    ) -> Result<Vec<u8>, CryptoError> {
+        let Some(_alg) = &signed_object.inner().protected.header.alg else {
+            return Err(SignatureError::InvalidSignature.into());
+        };
+
+        let signature_namespace = signed_object.namespace()?;
+        if signature_namespace != *namespace {
+            return Err(SignatureError::InvalidNamespace.into());
+        }
+
+        signed_object
+            .inner()
+            .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?;
+        signed_object.payload()
+    }
+
+    /// Verifies the signature of the given data, for the given namespace.
+    /// This should never be used directly, but only through the `verify` method, to enforce
+    /// strong domain separation of the signatures.
+    fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<(), CryptoError> {
+        match &self.inner {
+            RawVerifyingKey::Ed25519(key) => {
+                let sig = ed25519_dalek::Signature::from_bytes(
+                    signature
+                        .try_into()
+                        .map_err(|_| SignatureError::InvalidSignature)?,
+                );
+                key.verify_strict(data, &sig)
+                    .map_err(|_| SignatureError::InvalidSignature.into())
+            }
+        }
+    }
+}
+
+fn encode_message<Message: Serialize>(message: &Message) -> Result<SerializedMessage, CryptoError> {
+    let mut buffer = Vec::new();
+    ciborium::ser::into_writer(message, &mut buffer).map_err(|_| CryptoError::CoseEncodingError)?;
+    Ok(SerializedMessage {
+        serialized_message_bytes: buffer,
+        content_type: CoapContentFormat::Cbor,
+    })
+}
+
+fn decode_message<Message: DeserializeOwned>(
+    message: &SerializedMessage,
+) -> Result<Message, CryptoError> {
+    if message.content_type != CoapContentFormat::Cbor {
+        return Err(CryptoError::CoseEncodingError);
+    }
+
+    let decoded = ciborium::de::from_reader(message.serialized_message_bytes.as_slice())
+        .map_err(|_| CryptoError::CoseEncodingError)?;
+    Ok(decoded)
+}
+
+#[cfg(test)]
+mod tests {
+    use serde::Deserialize;
+
+    use super::*;
+    use crate::SignatureAlgorithm;
+
+    /// The function used to create the test vectors below, and can be used to re-generate them.
+    /// Once rolled out to user accounts, this function can be removed, because at that point we
+    /// cannot introduce format-breaking changes anymore.
+    #[test]
+    fn make_test_vectors() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let test_message = TestMessage {
+            field1: "Test message".to_string(),
+        };
+        let namespace = SigningNamespace::ExampleNamespace;
+
+        let (signature, serialized_message) = signing_key
+            .sign_detached(&test_message, &namespace)
+            .unwrap();
+        let signed_object = signing_key.sign(&test_message, &namespace).unwrap();
+
+        println!(
+            "const SIGNING_KEY: &[u8] = &{:?};",
+            signing_key.to_cose().unwrap()
+        );
+        println!(
+            "const VERIFYING_KEY: &[u8] = &{:?};",
+            verifying_key.to_cose().unwrap()
+        );
+        println!(
+            "const SIGNATURE: &[u8] = &{:?};",
+            signature.to_cose().unwrap()
+        );
+        println!(
+            "const SERIALIZED_MESSAGE: &[u8] = &{:?};",
+            serialized_message.serialized_message_bytes
+        );
+        println!(
+            "const SIGNED_OBJECT: &[u8] = &{:?};",
+            signed_object.to_cose().unwrap()
+        );
+    }
+
+    const SIGNING_KEY: &[u8] = &[
+        166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155,
+        16, 3, 39, 4, 130, 1, 2, 35, 88, 32, 114, 65, 45, 133, 77, 188, 130, 57, 89, 250, 113, 125,
+        108, 138, 255, 68, 3, 202, 189, 96, 31, 218, 197, 24, 35, 127, 52, 168, 232, 85, 95, 199,
+        32, 6,
+    ];
+    const VERIFYING_KEY: &[u8] = &[
+        166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155,
+        16, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 91, 255, 95, 169, 53, 21, 222, 134, 102, 103, 105,
+        224, 58, 210, 82, 121, 141, 60, 76, 68, 9, 26, 242, 215, 111, 150, 228, 154, 141, 143, 108,
+        38,
+    ];
+    const SIGNATURE: &[u8] = &[
+        132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226,
+        105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 246, 88, 64, 110, 91, 1, 209, 74,
+        57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243, 61, 167,
+        248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168, 10, 173,
+        145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85, 247, 99,
+        14,
+    ];
+    const SERIALIZED_MESSAGE: &[u8] = &[
+        161, 102, 102, 105, 101, 108, 100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97,
+        103, 101,
+    ];
+    const SIGNED_OBJECT: &[u8] = &[
+        132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226,
+        105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 85, 161, 102, 102, 105, 101, 108,
+        100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 88, 64, 110, 91, 1,
+        209, 74, 57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243,
+        61, 167, 248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168,
+        10, 173, 145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85,
+        247, 99, 14,
+    ];
+
+    #[derive(Serialize, Deserialize, Debug, PartialEq)]
+    struct TestMessage {
+        field1: String,
+    }
+
+    #[test]
+    fn test_vectors() {
+        let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap();
+        let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap();
+        let signature = Signature::from_cose(SIGNATURE).unwrap();
+        let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap();
+
+        assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY);
+        assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY);
+        assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT);
+
+        assert_eq!(
+            signature.namespace().unwrap(),
+            SigningNamespace::ExampleNamespace
+        );
+        assert_eq!(signature.content_type().unwrap(), CoapContentFormat::Cbor);
+        assert_eq!(signature.to_cose().unwrap(), SIGNATURE);
+
+        assert_eq!(signed_object.payload().unwrap(), SERIALIZED_MESSAGE);
+        assert_eq!(
+            signed_object.namespace().unwrap(),
+            SigningNamespace::ExampleNamespace
+        );
+        assert_eq!(
+            signed_object.content_type().unwrap(),
+            CoapContentFormat::Cbor
+        );
+        assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT);
+
+        let verified_payload: TestMessage = verifying_key
+            .get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace)
+            .unwrap();
+        assert_eq!(
+            verified_payload,
+            TestMessage {
+                field1: "Test message".to_string()
+            }
+        );
+        assert!(verifying_key.verify_signature(
+            SERIALIZED_MESSAGE,
+            &SigningNamespace::ExampleNamespace,
+            &signature
+        ));
+    }
+
+    #[test]
+    fn test_sign_detached_roundtrip() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = TestMessage {
+            field1: "Test message".to_string(),
+        };
+        let namespace = SigningNamespace::ExampleNamespace;
+        let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap();
+        assert!(verifying_key.verify_signature(
+            &serialized_message.serialized_message_bytes,
+            &namespace,
+            &signature
+        ));
+        let decoded_message: TestMessage = decode_message(&serialized_message).unwrap();
+        assert_eq!(decoded_message, data);
+    }
+
+    #[test]
+    fn test_sign_roundtrip() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = "Test message".to_string();
+        let namespace = SigningNamespace::ExampleNamespace;
+        let signed_object = signing_key.sign(&data, &namespace).unwrap();
+        let payload: String = verifying_key
+            .get_verified_payload(&signed_object, &namespace)
+            .unwrap();
+        assert_eq!(payload, data);
+    }
+
+    #[test]
+    fn test_countersign_roundtrip() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = "Test message".to_string();
+        let namespace = SigningNamespace::ExampleNamespace;
+        let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap();
+        let countersignature = signing_key
+            .counter_sign_detached(
+                serialized_message.serialized_message_bytes.clone(),
+                &signature,
+                &namespace,
+            )
+            .unwrap();
+        assert!(verifying_key.verify_signature(
+            &serialized_message.serialized_message_bytes,
+            &namespace,
+            &countersignature
+        ));
+    }
+
+    #[test]
+    fn test_changed_payload_fails() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = "Test message".to_string();
+        let namespace = SigningNamespace::ExampleNamespace;
+
+        let (signature, mut serialized_message) =
+            signing_key.sign_detached(&data, &namespace).unwrap();
+        let modified_message = serialized_message
+            .serialized_message_bytes
+            .get_mut(0)
+            .unwrap();
+        *modified_message = 0xFF;
+        assert!(!verifying_key.verify_signature(
+            &serialized_message.serialized_message_bytes,
+            &namespace,
+            &signature
+        ));
+    }
+
+    #[test]
+    fn test_changed_namespace_fails() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = b"Test message";
+        let namespace = SigningNamespace::ExampleNamespace;
+        let other_namespace = SigningNamespace::PublicKeyOwnershipClaim;
+
+        let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap();
+        assert!(!verifying_key.verify_signature(
+            &serialized_message.serialized_message_bytes,
+            &other_namespace,
+            &signature
+        ));
+        assert!(verifying_key.verify_signature(
+            &serialized_message.serialized_message_bytes,
+            &namespace,
+            &signature
+        ));
+    }
+
+    #[test]
+    fn test_changed_namespace_fails_signed_object() {
+        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = signing_key.to_verifying_key();
+        let data = b"Test message";
+        let namespace = SigningNamespace::ExampleNamespace;
+        let other_namespace = SigningNamespace::PublicKeyOwnershipClaim;
+        let signed_object = signing_key.sign(data, &namespace).unwrap();
+        assert!(verifying_key
+            .get_verified_payload::<Vec<u8>>(&signed_object, &other_namespace)
+            .is_err());
+        assert!(verifying_key
+            .get_verified_payload::<Vec<u8>>(&signed_object, &namespace)
+            .is_ok());
+    }
+
+    #[test]
+    fn test_encode_decode_message() {
+        let message = TestMessage {
+            field1: "Hello".to_string(),
+        };
+        let encoded = encode_message(&message).unwrap();
+        let decoded = decode_message(&encoded).unwrap();
+        assert_eq!(message, decoded);
+    }
+}
diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs
index 261067af8..7d2f7d321 100644
--- a/crates/bitwarden-crypto/src/store/context.rs
+++ b/crates/bitwarden-crypto/src/store/context.rs
@@ -3,13 +3,14 @@ use std::{
     sync::{RwLockReadGuard, RwLockWriteGuard},
 };
 
+use serde::Serialize;
 use zeroize::Zeroizing;
 
 use super::KeyStoreInner;
 use crate::{
     derive_shareable_key, error::UnsupportedOperation, store::backend::StoreBackend,
-    AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, SymmetricCryptoKey,
-    UnsignedSharedKey,
+    AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature,
+    SignatureAlgorithm, SignedObject, SigningKey, SymmetricCryptoKey, UnsignedSharedKey,
 };
 
 /// The context of a crypto operation using [super::KeyStore]
@@ -39,7 +40,11 @@ use crate::{
 /// #     pub enum AsymmKeyId {
 /// #         UserPrivate,
 /// #     }
-/// #     pub Ids => SymmKeyId, AsymmKeyId;
+/// #     #[signing]
+/// #     pub enum SigningKeyId {
+/// #         UserSigning,
+/// #     }
+/// #     pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId;
 /// # }
 /// struct Data {
 ///     key: EncString,
@@ -66,6 +71,7 @@ pub struct KeyStoreContext<'a, Ids: KeyIds> {
 
     pub(super) local_symmetric_keys: Box<dyn StoreBackend<Ids::Symmetric>>,
     pub(super) local_asymmetric_keys: Box<dyn StoreBackend<Ids::Asymmetric>>,
+    pub(super) local_signing_keys: Box<dyn StoreBackend<Ids::Signing>>,
 
     // Make sure the context is !Send & !Sync
     pub(super) _phantom: std::marker::PhantomData<(Cell<()>, RwLockReadGuard<'static, ()>)>,
@@ -104,6 +110,7 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
     pub fn clear_local(&mut self) {
         self.local_symmetric_keys.clear();
         self.local_asymmetric_keys.clear();
+        self.local_signing_keys.clear();
     }
 
     /// Remove all symmetric keys from the context for which the predicate returns false
@@ -244,6 +251,11 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         self.get_asymmetric_key(key_id).is_ok()
     }
 
+    // Returns `true` if the context has a signing key with the given identifier
+    pub fn has_signing_key(&self, key_id: Ids::Signing) -> bool {
+        self.get_signing_key(key_id).is_ok()
+    }
+
     /// Generate a new random symmetric key and store it in the context
     pub fn generate_symmetric_key(&mut self, key_id: Ids::Symmetric) -> Result<Ids::Symmetric> {
         let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
@@ -252,6 +264,14 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         Ok(key_id)
     }
 
+    // Generate a new signature key using the current default algorithm, and store it in the context
+    pub fn make_signing_key(&mut self, key_id: Ids::Signing) -> Result<Ids::Signing> {
+        let key = SigningKey::make(SignatureAlgorithm::default_algorithm())?;
+        #[allow(deprecated)]
+        self.set_signing_key(key_id, key)?;
+        Ok(key_id)
+    }
+
     /// Derive a shareable key using hkdf from secret and name and store it in the context.
     ///
     /// A specialized variant of this function was called `CryptoService.makeSendKey` in the
@@ -287,6 +307,11 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         self.get_asymmetric_key(key_id)
     }
 
+    #[deprecated(note = "This function should ideally never be used outside this crate")]
+    pub fn dangerous_get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
+        self.get_signing_key(key_id)
+    }
+
     fn get_symmetric_key(&self, key_id: Ids::Symmetric) -> Result<&SymmetricCryptoKey> {
         if key_id.is_local() {
             self.local_symmetric_keys.get(key_id)
@@ -305,6 +330,16 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
     }
 
+    #[allow(unused)]
+    fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
+        if key_id.is_local() {
+            self.local_signing_keys.get(key_id)
+        } else {
+            self.global_keys.get().signing_keys.get(key_id)
+        }
+        .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
+    }
+
     #[deprecated(note = "This function should ideally never be used outside this crate")]
     pub fn set_symmetric_key(
         &mut self,
@@ -339,6 +374,16 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
         Ok(())
     }
 
+    #[deprecated(note = "This function should ideally never be used outside this crate")]
+    pub fn set_signing_key(&mut self, key_id: Ids::Signing, key: SigningKey) -> Result<()> {
+        if key_id.is_local() {
+            self.local_signing_keys.upsert(key_id, key);
+        } else {
+            self.global_keys.get_mut()?.signing_keys.upsert(key_id, key);
+        }
+        Ok(())
+    }
+
     pub(crate) fn decrypt_data_with_symmetric_key(
         &self,
         key: Ids::Symmetric,
@@ -374,17 +419,62 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
             }
         }
     }
+
+    /// Signs the given data using the specified signing key, for the given
+    /// [crate::SigningNamespace] and returns the signature and the serialized message. See
+    /// [crate::SigningKey::sign]
+    #[allow(unused)]
+    pub(crate) fn sign<Message: Serialize>(
+        &self,
+        key: Ids::Signing,
+        message: &Message,
+        namespace: &crate::SigningNamespace,
+    ) -> Result<SignedObject> {
+        let key = self.get_signing_key(key)?;
+        key.sign(message, namespace)
+    }
+
+    /// Signs the given data using the specified signing key, for the given
+    /// [crate::SigningNamespace] and returns the signature and the serialized message. See
+    /// [crate::SigningKey::sign_detached]
+    #[allow(unused)]
+    pub(crate) fn sign_detached<Message: Serialize>(
+        &self,
+        key: Ids::Signing,
+        message: &Message,
+        namespace: &crate::SigningNamespace,
+    ) -> Result<(Signature, Vec<u8>)> {
+        let key = self.get_signing_key(key)?;
+        let (signature, serialized_message) = key.sign_detached(message, namespace)?;
+        Ok((signature, serialized_message.as_ref().to_vec()))
+    }
 }
 
 #[cfg(test)]
 #[allow(deprecated)]
 mod tests {
+    use serde::{Deserialize, Serialize};
+
     use crate::{
         store::{tests::DataView, KeyStore},
-        traits::tests::{TestIds, TestSymmKey},
-        Decryptable, Encryptable, SymmetricCryptoKey,
+        traits::tests::{TestIds, TestSigningKey, TestSymmKey},
+        CryptoError, Decryptable, Encryptable, SignatureAlgorithm, SigningKey, SigningNamespace,
+        SymmetricCryptoKey,
     };
 
+    #[test]
+    fn test_set_signing_key() {
+        let store: KeyStore<TestIds> = KeyStore::default();
+
+        // Generate and insert a key
+        let key_a0_id = TestSigningKey::A(0);
+        let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        store
+            .context_mut()
+            .set_signing_key(key_a0_id, key_a0)
+            .unwrap();
+    }
+
     #[test]
     fn test_set_keys_for_encryption() {
         let store: KeyStore<TestIds> = KeyStore::default();
@@ -448,4 +538,55 @@ mod tests {
         // Assert that the decrypted data is the same
         assert_eq!(decrypted1.0, decrypted2.0);
     }
+
+    #[test]
+    fn test_signing() {
+        let store: KeyStore<TestIds> = KeyStore::default();
+
+        // Generate and insert a key
+        let key_a0_id = TestSigningKey::A(0);
+        let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap();
+        let verifying_key = key_a0.to_verifying_key();
+        store
+            .context_mut()
+            .set_signing_key(key_a0_id, key_a0)
+            .unwrap();
+
+        assert!(store.context().has_signing_key(key_a0_id));
+
+        // Sign some data with the key
+        #[derive(Serialize, Deserialize)]
+        struct TestData {
+            data: String,
+        }
+        let signed_object = store
+            .context()
+            .sign(
+                key_a0_id,
+                &TestData {
+                    data: "Hello".to_string(),
+                },
+                &SigningNamespace::ExampleNamespace,
+            )
+            .unwrap();
+        let payload: Result<TestData, CryptoError> =
+            verifying_key.get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace);
+        assert!(payload.is_ok());
+
+        let (signature, serialized_message) = store
+            .context()
+            .sign_detached(
+                key_a0_id,
+                &TestData {
+                    data: "Hello".to_string(),
+                },
+                &SigningNamespace::ExampleNamespace,
+            )
+            .unwrap();
+        assert!(verifying_key.verify_signature(
+            &serialized_message,
+            &SigningNamespace::ExampleNamespace,
+            &signature
+        ));
+    }
 }
diff --git a/crates/bitwarden-crypto/src/store/mod.rs b/crates/bitwarden-crypto/src/store/mod.rs
index f447f58b2..ff133a9c4 100644
--- a/crates/bitwarden-crypto/src/store/mod.rs
+++ b/crates/bitwarden-crypto/src/store/mod.rs
@@ -58,7 +58,11 @@ pub use context::KeyStoreContext;
 ///     pub enum AsymmKeyId {
 ///         UserPrivate,
 ///     }
-///     pub Ids => SymmKeyId, AsymmKeyId;
+///     #[signing]
+///     pub enum SigningKeyId {
+///        UserSigning,
+///     }
+///     pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId;
 /// }
 ///
 /// // Initialize the store and insert a test key
@@ -101,6 +105,7 @@ impl<Ids: KeyIds> std::fmt::Debug for KeyStore<Ids> {
 struct KeyStoreInner<Ids: KeyIds> {
     symmetric_keys: Box<dyn StoreBackend<Ids::Symmetric>>,
     asymmetric_keys: Box<dyn StoreBackend<Ids::Asymmetric>>,
+    signing_keys: Box<dyn StoreBackend<Ids::Signing>>,
 }
 
 /// Create a new key store with the best available implementation for the current platform.
@@ -110,6 +115,7 @@ impl<Ids: KeyIds> Default for KeyStore<Ids> {
             inner: Arc::new(RwLock::new(KeyStoreInner {
                 symmetric_keys: create_store(),
                 asymmetric_keys: create_store(),
+                signing_keys: create_store(),
             })),
         }
     }
@@ -122,6 +128,7 @@ impl<Ids: KeyIds> KeyStore<Ids> {
         let mut keys = self.inner.write().expect("RwLock is poisoned");
         keys.symmetric_keys.clear();
         keys.asymmetric_keys.clear();
+        keys.signing_keys.clear();
     }
 
     /// Initiate an encryption/decryption context. This context will have read only access to the
@@ -160,6 +167,7 @@ impl<Ids: KeyIds> KeyStore<Ids> {
             global_keys: GlobalKeys::ReadOnly(self.inner.read().expect("RwLock is poisoned")),
             local_symmetric_keys: create_store(),
             local_asymmetric_keys: create_store(),
+            local_signing_keys: create_store(),
             _phantom: std::marker::PhantomData,
         }
     }
@@ -189,6 +197,7 @@ impl<Ids: KeyIds> KeyStore<Ids> {
             global_keys: GlobalKeys::ReadWrite(self.inner.write().expect("RwLock is poisoned")),
             local_symmetric_keys: create_store(),
             local_asymmetric_keys: create_store(),
+            local_signing_keys: create_store(),
             _phantom: std::marker::PhantomData,
         }
     }
diff --git a/crates/bitwarden-crypto/src/traits/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs
index ba997a5b0..854149424 100644
--- a/crates/bitwarden-crypto/src/traits/key_id.rs
+++ b/crates/bitwarden-crypto/src/traits/key_id.rs
@@ -2,7 +2,7 @@ use std::{fmt::Debug, hash::Hash};
 
 use zeroize::ZeroizeOnDrop;
 
-use crate::{AsymmetricCryptoKey, CryptoKey, SymmetricCryptoKey};
+use crate::{AsymmetricCryptoKey, CryptoKey, SigningKey, SymmetricCryptoKey};
 
 /// Represents a key identifier that can be used to identify cryptographic keys in the
 /// key store. It is used to avoid exposing the key material directly in the public API.
@@ -30,6 +30,7 @@ pub trait KeyId:
 pub trait KeyIds {
     type Symmetric: KeyId<KeyValue = SymmetricCryptoKey>;
     type Asymmetric: KeyId<KeyValue = AsymmetricCryptoKey>;
+    type Signing: KeyId<KeyValue = SigningKey>;
 }
 
 /// Just a small derive_like macro that can be used to generate the key identifier enums.
@@ -49,7 +50,13 @@ pub trait KeyIds {
 ///     pub enum AsymmKeyId {
 ///         PrivateKey,
 ///     }
-///     pub Ids => SymmKeyId, AsymmKeyId;
+///
+///     #[signing]
+///     pub enum SigningKeyId {
+///        SigningKey,
+///     }
+///
+///     pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId;
 /// }
 #[macro_export]
 macro_rules! key_ids {
@@ -63,7 +70,7 @@ macro_rules! key_ids {
             $(,)?
         }
     )+
-    $ids_vis:vis $ids_name:ident => $symm_name:ident, $asymm_name:ident;
+    $ids_vis:vis $ids_name:ident => $symm_name:ident, $asymm_name:ident, $signing_name:ident;
     ) => {
         $(
             #[derive(std::fmt::Debug, Clone, Copy, std::hash::Hash, Eq, PartialEq, Ord, PartialOrd)]
@@ -88,11 +95,15 @@ macro_rules! key_ids {
         impl $crate::KeyIds for $ids_name {
             type Symmetric = $symm_name;
             type Asymmetric = $asymm_name;
+            type Signing = $signing_name;
         }
     };
 
     ( @key_type symmetric ) => { $crate::SymmetricCryptoKey };
     ( @key_type asymmetric ) => { $crate::AsymmetricCryptoKey };
+    ( @key_type signing ) => { $crate::SigningKey };
+
+    ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant ( _ ) };
 
     ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant (_) };
     ( @variant_match $variant:ident ) => { $variant };
@@ -104,7 +115,7 @@ macro_rules! key_ids {
 #[cfg(test)]
 pub(crate) mod tests {
     use crate::{
-        traits::tests::{TestAsymmKey, TestSymmKey},
+        traits::tests::{TestAsymmKey, TestSigningKey, TestSymmKey},
         KeyId,
     };
 
@@ -117,5 +128,9 @@ pub(crate) mod tests {
         assert!(!TestAsymmKey::A(0).is_local());
         assert!(!TestAsymmKey::B.is_local());
         assert!(TestAsymmKey::C("test").is_local());
+
+        assert!(!TestSigningKey::A(0).is_local());
+        assert!(!TestSigningKey::B.is_local());
+        assert!(TestSigningKey::C("test").is_local());
     }
 }
diff --git a/crates/bitwarden-crypto/src/traits/mod.rs b/crates/bitwarden-crypto/src/traits/mod.rs
index 28b811e36..9110a7508 100644
--- a/crates/bitwarden-crypto/src/traits/mod.rs
+++ b/crates/bitwarden-crypto/src/traits/mod.rs
@@ -36,6 +36,14 @@ pub(crate) mod tests {
             C(&'static str),
         }
 
-       pub TestIds => TestSymmKey, TestAsymmKey;
+        #[signing]
+        pub enum TestSigningKey {
+            A(u8),
+            B,
+            #[local]
+            C(&'static str),
+        }
+
+       pub TestIds => TestSymmKey, TestAsymmKey, TestSigningKey;
     }
 }
diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml
index 46713b9eb..e3e2c2bb2 100644
--- a/crates/bitwarden-wasm-internal/Cargo.toml
+++ b/crates/bitwarden-wasm-internal/Cargo.toml
@@ -25,6 +25,7 @@ bitwarden-ipc = { workspace = true, features = ["wasm"] }
 bitwarden-ssh = { workspace = true, features = ["wasm"] }
 bitwarden-vault = { workspace = true, features = ["wasm"] }
 chrono = { workspace = true }
+base64 = ">=0.22.1, <0.23.0"
 console_error_panic_hook = "0.1.7"
 console_log = { version = "1.0.0", features = ["color"] }
 js-sys = "0.3.68"
diff --git a/crates/bitwarden-wasm-internal/src/crypto.rs b/crates/bitwarden-wasm-internal/src/crypto.rs
index 044ffbcec..8bae18955 100644
--- a/crates/bitwarden-wasm-internal/src/crypto.rs
+++ b/crates/bitwarden-wasm-internal/src/crypto.rs
@@ -2,7 +2,7 @@ use bitwarden_core::{
     client::encryption_settings::EncryptionSettingsError,
     mobile::crypto::{
         InitOrgCryptoRequest, InitUserCryptoRequest, MakeKeyPairResponse,
-        VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
+        MakeUserSigningKeysResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
     },
 };
 use bitwarden_crypto::CryptoError;
@@ -52,4 +52,10 @@ impl CryptoClient {
     ) -> Result<VerifyAsymmetricKeysResponse, CryptoError> {
         self.0.verify_asymmetric_keys(request)
     }
+
+    /// Generates a new signing key pair and encrypts the signing key with the provided symmetric
+    /// key. Crypto initialization not required.
+    pub fn make_signing_keys(&self) -> Result<MakeUserSigningKeysResponse, CryptoError> {
+        self.0.make_signing_keys()
+    }
 }
diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs
index 85a34b49b..8c19efccc 100644
--- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs
+++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs
@@ -3,8 +3,8 @@ use std::str::FromStr;
 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
 use bitwarden_crypto::{
     AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, Decryptable, EncString,
-    Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, SymmetricCryptoKey,
-    UnsignedSharedKey,
+    Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, SignatureAlgorithm,
+    SignedPublicKeyOwnershipClaim, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey,
 };
 use wasm_bindgen::prelude::*;
 
@@ -265,6 +265,40 @@ impl PureCrypto {
             )?)?
             .to_encoded())
     }
+
+    pub fn verifying_key_for_signing_key(
+        signing_key: String,
+        wrapping_key: Vec<u8>,
+    ) -> Result<Vec<u8>, CryptoError> {
+        let bytes = Self::symmetric_decrypt_bytes(signing_key, wrapping_key)?;
+        let signing_key = SigningKey::from_cose(&bytes)?;
+        let verifying_key = signing_key.to_verifying_key();
+        verifying_key.to_cose()
+    }
+
+    /// Returns the algorithm used for the given verifying key.
+    pub fn key_algorithm_for_verifying_key(
+        verifying_key: Vec<u8>,
+    ) -> Result<SignatureAlgorithm, CryptoError> {
+        let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?;
+        let algorithm = verifying_key.algorithm();
+        Ok(algorithm)
+    }
+
+    /// For a given signing identity (verifying key), this function verifies that the signing
+    /// identity claimed ownership of the public key. This is a one-sided claim and merely shows
+    /// that the signing identity has the intent to receive messages encrypted to the public
+    /// key.
+    pub fn verify_public_key_ownership_claim(
+        claim: Vec<u8>,
+        public_key: Vec<u8>,
+        verifying_key: Vec<u8>,
+    ) -> Result<bool, CryptoError> {
+        let claim = SignedPublicKeyOwnershipClaim::from_bytes(claim.as_slice())?;
+        let public_key = AsymmetricPublicCryptoKey::from_der(public_key.as_slice())?;
+        let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?;
+        claim.verify_claim(&public_key, &verifying_key)
+    }
 }
 
 #[cfg(test)]