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)]