diff --git a/Cargo.lock b/Cargo.lock index 5de9f7f42..b73ca6026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,7 @@ dependencies = [ "ciborium", "coset", "criterion", + "ed25519-dalek", "generic-array", "hkdf", "hmac", @@ -412,7 +413,9 @@ dependencies = [ "rsa", "schemars", "serde", + "serde_bytes", "serde_json", + "serde_repr", "sha1", "sha2", "subtle", @@ -694,6 +697,7 @@ dependencies = [ name = "bitwarden-wasm-internal" version = "0.1.0" dependencies = [ + "base64", "bitwarden-core", "bitwarden-crypto", "bitwarden-error", @@ -1267,6 +1271,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -1465,8 +1470,11 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", + "serde", "sha2", "subtle", + "zeroize", ] [[package]] @@ -3527,6 +3535,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/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 306f2696d..5655f2110 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ fingerprint, generate_random_alphanumeric, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, - CryptoError, UnsignedSharedKey, + CryptoError, PublicKeyEncryptionAlgorithm, UnsignedSharedKey, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, SymmetricCryptoKey}; @@ -31,11 +31,9 @@ pub struct AuthRequestResponse { /// to another device. Where the user confirms the validity by confirming the fingerprint. The user /// key is then encrypted using the public key and returned to the initiating device. pub(crate) fn new_auth_request(email: &str) -> Result { - let mut rng = rand::thread_rng(); + let key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); - let key = AsymmetricCryptoKey::generate(&mut rng); - - let spki = key.to_public_der()?; + let spki = key.to_public_key().to_der()?; let fingerprint = fingerprint(email, &spki)?; let b64 = STANDARD.encode(&spki); @@ -124,7 +122,7 @@ fn test_auth_request() { let encrypted = UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(secret.clone()).unwrap(), - &private_key, + &private_key.to_public_key(), ) .unwrap(); @@ -162,7 +160,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 +227,7 @@ mod tests { existing_device .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key.clone()) + .initialize_user_crypto_master_key(master_key, user_key, private_key.clone(), None) .unwrap(); // Initialize a new device which will request to be logged in @@ -247,6 +245,7 @@ mod tests { kdf_params: kdf, email: email.to_owned(), private_key, + 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 841c73255..782a6df34 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 d3f8ef4bf..363086fb1 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -120,6 +120,7 @@ pub(crate) async fn complete_auth_request( kdf_params: kdf, email: auth_req.email, private_key: require!(r.private_key).parse()?, + 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 df83ca5b7..76656d91b 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -50,9 +50,12 @@ 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()?; - 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(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 53a71f07c..570a28fd2 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -37,9 +37,13 @@ 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(), + // Note: Signing keys are not supported on registration yet. This needs to be changed as + // soon as registration is supported. + 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 54c1a96e0..ab8d84cd4 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,5 +1,5 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{AsymmetricCryptoKey, EncString, UnsignedSharedKey}; +use bitwarden_crypto::{EncString, UnsignedSharedKey}; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; @@ -7,8 +7,6 @@ use thiserror::Error; #[cfg(any(feature = "internal", feature = "secrets"))] use uuid::Uuid; -#[cfg(feature = "internal")] -use crate::key_management::AsymmetricKeyId; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; @@ -48,12 +46,15 @@ impl EncryptionSettings { pub(crate) fn new_decrypted_key( user_key: SymmetricCryptoKey, private_key: EncString, + signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::KeyDecryptable; + use bitwarden_crypto::{ + AsymmetricCryptoKey, CoseSerializable, CryptoError, KeyDecryptable, SigningKey, + }; use log::warn; - use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; + use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; let private_key = { let dec: Vec = private_key.decrypt_with_key(&user_key)?; @@ -71,6 +72,12 @@ impl EncryptionSettings { // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, // ) }; + let signing_key = signing_key + .map(|key| { + let dec: Vec = key.decrypt_with_key(&user_key)?; + SigningKey::from_cose(dec.as_slice()).map_err(Into::::into) + }) + .transpose()?; // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods #[allow(deprecated)] @@ -80,6 +87,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(()) @@ -106,6 +117,8 @@ impl EncryptionSettings { org_enc_keys: Vec<(Uuid, UnsignedSharedKey)>, store: &KeyStore, ) -> 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 76f22e725..8749a4d32 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -8,10 +8,9 @@ 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; -#[cfg(any(feature = "internal", feature = "secrets"))] -use crate::client::encryption_settings::EncryptionSettings; use crate::{ auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, key_management::KeyIds, DeviceType, @@ -199,9 +198,10 @@ impl InternalClient { master_key: MasterKey, user_key: EncString, private_key: EncString, + signing_key: Option, ) -> 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(()) } @@ -211,8 +211,9 @@ impl InternalClient { &self, user_key: SymmetricCryptoKey, private_key: EncString, + signing_key: Option, ) -> 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(()) } @@ -223,9 +224,10 @@ impl InternalClient { pin_key: PinKey, pin_protected_user_key: EncString, private_key: EncString, + signing_key: Option, ) -> 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 8555d1989..48a5cdba9 100644 --- a/crates/bitwarden-core/src/client/test_accounts.rs +++ b/crates/bitwarden-core/src/client/test_accounts.rs @@ -125,6 +125,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=".parse::().unwrap().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=".parse().unwrap(), @@ -182,6 +184,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=".parse::().unwrap().to_owned(), + signing_key: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), user_key: "0.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=".parse().unwrap(), diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index c5cdba909..1aaca34c2 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,7 +8,8 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, + AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Encryptable, Kdf, + KeyDecryptable, KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; @@ -19,7 +20,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, - key_management::SymmetricKeyId, + key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}, Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError, }; @@ -50,6 +51,8 @@ pub struct InitUserCryptoRequest { pub email: String, /// The user's encrypted private key pub private_key: EncString, + /// The user's signing key + pub signing_key: Option, /// The initialization method to use pub method: InitUserCryptoMethod, } @@ -145,13 +148,16 @@ pub(super) async fn initialize_user_crypto( master_key, user_key, req.private_key, + req.signing_key, )?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?; - client - .internal - .initialize_user_crypto_decrypted_key(user_key, req.private_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::Pin { pin, @@ -162,6 +168,7 @@ pub(super) async fn initialize_user_crypto( pin_key, pin_protected_user_key, req.private_key, + req.signing_key, )?; } InitUserCryptoMethod::AuthRequest { @@ -181,9 +188,11 @@ pub(super) async fn initialize_user_crypto( auth_request_key, )?, }; - client - .internal - .initialize_user_crypto_decrypted_key(user_key, req.private_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -194,9 +203,11 @@ pub(super) async fn initialize_user_crypto( let user_key = device_key .decrypt_user_key(protected_device_private_key, device_protected_user_key)?; - client - .internal - .initialize_user_crypto_decrypted_key(user_key, req.private_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::KeyConnector { master_key, @@ -211,6 +222,7 @@ pub(super) async fn initialize_user_crypto( master_key, user_key, req.private_key, + req.signing_key, )?; } } @@ -528,7 +540,8 @@ pub(super) fn verify_asymmetric_keys( .map_err(VerifyError::ParseFailed)?; let derived_public_key_vec = private_key - .to_public_der() + .to_public_key() + .to_der() .map_err(VerifyError::PublicFailed)?; let derived_public_key = STANDARD.encode(derived_public_key_vec); @@ -557,6 +570,47 @@ pub(super) fn verify_asymmetric_keys( }) } +/// A new signing key pair along with the signed public key +#[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 { + /// Base64 encoded verifying key + verifying_key: String, + /// Signing key, encrypted with the user's symmetric key + signing_key: EncString, + /// The user's public key, signed by the signing key + signed_public_key: SignedPublicKey, +} + +/// Makes a new set of signing keys for a user, which should only be done during +/// once. This also signs the public key with the signing key +/// and returns the signed public key. +pub fn make_user_signing_keys_for_enrollment( + client: &Client, +) -> Result { + let key_store = client.internal.get_key_store(); + let mut ctx = key_store.context(); + + // Make new keypair and sign the public key with it + let signature_keypair = SigningKey::make(SignatureAlgorithm::Ed25519); + let signed_public_key = ctx.make_signed_public_key( + AsymmetricKeyId::UserPrivateKey, + SigningKeyId::UserSigningKey, + )?; + + Ok(MakeUserSigningKeysResponse { + verifying_key: STANDARD.encode(signature_keypair.to_verifying_key().to_cose()), + // This needs to be changed to use the correct COSE content format before rolling out to + // users: https://bitwarden.atlassian.net/browse/PM-22189 + signing_key: signature_keypair + .to_cose() + .encrypt(&mut ctx, SymmetricKeyId::User)?, + signed_public_key, + }) +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; @@ -583,6 +637,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=".parse().unwrap(), @@ -603,6 +658,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, @@ -661,6 +717,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=".parse().unwrap(), @@ -683,6 +740,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, @@ -726,6 +784,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, @@ -759,7 +818,6 @@ mod tests { #[test] fn test_enroll_admin_password_reset() { use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::AsymmetricCryptoKey; let client = Client::new(None); @@ -776,7 +834,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/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 9c5bdd440..03390939a 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -5,9 +5,10 @@ use bitwarden_crypto::{EncString, UnsignedSharedKey}; use wasm_bindgen::prelude::*; use super::crypto::{ - derive_key_connector, make_key_pair, verify_asymmetric_keys, CryptoClientError, - DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, - MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, + derive_key_connector, make_key_pair, make_user_signing_keys_for_enrollment, + verify_asymmetric_keys, DeriveKeyConnectorError, DeriveKeyConnectorRequest, + EnrollAdminPasswordResetError, MakeKeyPairResponse, MakeUserSigningKeysResponse, + VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }; #[cfg(feature = "internal")] use crate::key_management::crypto::{ @@ -15,7 +16,10 @@ use crate::key_management::crypto::{ initialize_org_crypto, initialize_user_crypto, update_password, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse, }; -use crate::{client::encryption_settings::EncryptionSettingsError, Client}; +use crate::{ + client::encryption_settings::EncryptionSettingsError, + key_management::crypto::CryptoClientError, Client, +}; /// A client for the crypto operations. #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -58,6 +62,13 @@ impl CryptoClient { ) -> Result { verify_asymmetric_keys(request) } + + /// Makes a new signing key pair and signs the public key for the user + pub fn make_user_signing_keys_for_enrollment( + &self, + ) -> Result { + make_user_signing_keys_for_enrollment(&self.client) + } } impl CryptoClient { diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 394ae5db9..972e1b1dc 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -31,7 +31,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/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 21a16e976..311039d74 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -69,7 +69,7 @@ pub(crate) fn generate_user_fingerprint( #[allow(deprecated)] let private_key = ctx.dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey)?; - let public_key = private_key.to_public_der()?; + let public_key = private_key.to_public_key().to_der()?; let fingerprint = fingerprint(&fingerprint_material, &public_key)?; Ok(fingerprint) @@ -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 0a275ad17..2b93f8227 100644 --- a/crates/bitwarden-core/tests/register.rs +++ b/crates/bitwarden-core/tests/register.rs @@ -33,7 +33,7 @@ async fn test_register_initialize_crypto() { kdf_params: kdf, email: email.to_owned(), private_key: register_response.keys.private, - + 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 fc2cff55b..38623b463 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -33,6 +33,7 @@ cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] } chacha20poly1305 = { version = "0.10.1" } ciborium = { version = ">=0.2.2, <0.3" } coset = { version = ">=0.3.8, <0.4" } +ed25519-dalek = { version = ">=2.1.1, <=2.2.0", features = ["rand_core"] } generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] } hkdf = ">=0.12.3, <0.13" hmac = ">=0.12.1, <0.13" @@ -45,6 +46,8 @@ 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" +serde_repr.workspace = true sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" subtle = ">=2.5.0, <3.0" diff --git a/crates/bitwarden-crypto/examples/signature.rs b/crates/bitwarden-crypto/examples/signature.rs new file mode 100644 index 000000000..8524e2943 --- /dev/null +++ b/crates/bitwarden-crypto/examples/signature.rs @@ -0,0 +1,148 @@ +//! This example demonstrates how to create signatures and countersignatures for a message, and how +//! to verify them. + +use bitwarden_crypto::{CoseSerializable, SigningNamespace}; +use serde::{Deserialize, Serialize}; + +const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; + +fn main() { + // Alice wants to create a message, sign it, and send it to Bob. Bob should sign it too, and + // then finally Charlie should be able to verify both. + + // Setup + let mut mock_server = MockServer::new(); + let alice_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519); + let alice_verifying_key = alice_signature_key.to_verifying_key(); + let bob_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519); + let bob_verifying_key = bob_signature_key.to_verifying_key(); + // We assume bob knows and trusts this verifying key previously via e.g. fingerprints or + // auditable key directory. + + // Alice creates a message + #[derive(Serialize, Deserialize)] + struct MessageToCharlie { + content: String, + } + let (signature, serialized_message) = alice_signature_key + .sign_detached( + &MessageToCharlie { + content: "Hello Charlie, this is Alice and Bob!".to_string(), + }, + // The namespace should be unique per message type. It ensures no cross protocol + // attacks can happen. + EXAMPLE_NAMESPACE, + ) + .expect("Failed to sign message"); + + // Alice sends the signed object to Bob + mock_server.upload("signature", signature.to_cose()); + mock_server.upload("serialized_message", serialized_message.as_bytes().to_vec()); + + // Bob retrieves the signed object from the server + let retrieved_signature = bitwarden_crypto::Signature::from_cose( + mock_server + .download("signature") + .expect("Failed to download signature"), + ) + .expect("Failed to deserialize signature"); + let retrieved_serialized_message = bitwarden_crypto::SerializedMessage::from_bytes( + mock_server + .download("serialized_message") + .expect("Failed to download serialized message") + .clone(), + retrieved_signature + .content_type() + .expect("Failed to get content type from signature"), + ); + + // Bob verifies the signature using Alice's verifying key + if !retrieved_signature.verify( + retrieved_serialized_message.as_bytes(), + &alice_verifying_key, + EXAMPLE_NAMESPACE, + ) { + panic!("Alice's signature verification failed"); + } + + // Bob signs the message for Charlie + let bobs_signature = bob_signature_key + .counter_sign_detached( + retrieved_serialized_message.as_bytes().to_vec(), + &retrieved_signature, + EXAMPLE_NAMESPACE, + ) + .expect("Failed to counter sign message"); + // Bob sends the counter signature to Charlie + mock_server.upload("bobs_signature", bobs_signature.to_cose()); + + // Charlie retrieves the signatures, and the message + let retrieved_serialized_message = bitwarden_crypto::SerializedMessage::from_bytes( + mock_server + .download("serialized_message") + .expect("Failed to download serialized message") + .clone(), + retrieved_signature + .content_type() + .expect("Failed to get content type from signature"), + ); + let retrieved_alice_signature = bitwarden_crypto::Signature::from_cose( + mock_server + .download("signature") + .expect("Failed to download Alice's signature"), + ) + .expect("Failed to deserialize Alice's signature"); + let retrieved_bobs_signature = bitwarden_crypto::Signature::from_cose( + mock_server + .download("bobs_signature") + .expect("Failed to download Bob's signature"), + ) + .expect("Failed to deserialize Bob's signature"); + + // Charlie verifies Alice's signature + if !retrieved_alice_signature.verify( + retrieved_serialized_message.as_bytes(), + &alice_verifying_key, + EXAMPLE_NAMESPACE, + ) { + panic!("Alice's signature verification failed"); + } + // Charlie verifies Bob's signature + if !retrieved_bobs_signature.verify( + retrieved_serialized_message.as_bytes(), + &bob_verifying_key, + EXAMPLE_NAMESPACE, + ) { + panic!("Bob's signature verification failed"); + } + // Charlie can now access the content of the message + let verified_message: MessageToCharlie = retrieved_serialized_message + .decode() + .expect("Failed to decode serialized message"); + println!( + "Charlie received a message from Alice and Bob: {}", + verified_message.content + ); +} + +pub(crate) struct MockServer { + map: std::collections::HashMap>, +} + +impl MockServer { + pub(crate) fn new() -> Self { + MockServer { + map: std::collections::HashMap::new(), + } + } + + pub(crate) fn upload(&mut self, key: &str, value: Vec) { + self.map.insert(key.to_string(), value); + } + + pub(crate) fn download(&self, key: &str) -> Option<&Vec> { + self.map.get(key) + } +} diff --git a/crates/bitwarden-crypto/examples/signed_object.rs b/crates/bitwarden-crypto/examples/signed_object.rs new file mode 100644 index 000000000..c49b8ef1b --- /dev/null +++ b/crates/bitwarden-crypto/examples/signed_object.rs @@ -0,0 +1,75 @@ +//! This example demonstrates how to sign and verify structs. + +use bitwarden_crypto::{CoseSerializable, SignedObject, SigningNamespace}; +use serde::{Deserialize, Serialize}; + +const EXAMPLE_NAMESPACE: &SigningNamespace = &SigningNamespace::SignedPublicKey; + +fn main() { + // Alice wants to create a message, for which Bob is sure that Alice signed it. Bob should only + // access the payload if he verified the signatures validity. + + // Setup + let mut mock_server = MockServer::new(); + let alice_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519); + let alice_verifying_key = alice_signature_key.to_verifying_key(); + // We assume bob knows and trusts this verifying key previously via e.g. fingerprints or + // auditable key directory. + + // Alice creates a message + #[derive(Serialize, Deserialize)] + struct MessageToBob { + content: String, + } + let signed_object = alice_signature_key + .sign( + &MessageToBob { + content: "Hello Bob, this is Alice!".to_string(), + }, + // The namespace should be unique per message type. It ensures no cross protocol + // attacks can happen. + EXAMPLE_NAMESPACE, + ) + .expect("Failed to sign message"); + + // Alice sends the signed object to Bob + mock_server.upload("signed_object", signed_object.to_cose()); + + // Bob retrieves the signed object from the server + let retrieved_signed_object = SignedObject::from_cose( + mock_server + .download("signed_object") + .expect("Failed to download signed object"), + ) + .expect("Failed to deserialize signed object"); + // Bob verifies the signed object using Alice's verifying key + let verified_message: MessageToBob = retrieved_signed_object + .verify_and_unwrap(&alice_verifying_key, EXAMPLE_NAMESPACE) + .expect("Failed to verify signed object"); + // Bob can now access the content of the message + println!( + "Bob received a message from Alice: {}", + verified_message.content + ); +} + +pub(crate) struct MockServer { + map: std::collections::HashMap>, +} + +impl MockServer { + pub(crate) fn new() -> Self { + MockServer { + map: std::collections::HashMap::new(), + } + } + + pub(crate) fn upload(&mut self, key: &str, value: Vec) { + self.map.insert(key.to_string(), value); + } + + pub(crate) fn download(&self, key: &str) -> Option<&Vec> { + self.map.get(key) + } +} diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index 8b4b90075..04cb676e3 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -8,7 +8,8 @@ use generic_array::GenericArray; use typenum::U32; use crate::{ - error::EncStringParseError, xchacha20, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key, + error::{EncStringParseError, EncodingError}, + xchacha20, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key, }; /// XChaCha20 is used over ChaCha20 @@ -16,6 +17,11 @@ use crate::{ /// the draft was never published as an RFC, we use a private-use value for the algorithm. pub(crate) const XCHACHA20_POLY1305: i64 = -70000; +// Labels +// +/// The label used for the namespace ensuring strong domain separation when using signatures. +pub(crate) const SIGNING_NAMESPACE: i64 = -80000; + /// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message pub(crate) fn encrypt_xchacha20_poly1305( plaintext: &[u8], @@ -117,6 +123,15 @@ impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey { } } +/// Trait for structs that are serializable to COSE objects. +pub trait CoseSerializable { + /// Serializes the struct to COSE serialization + fn to_cose(&self) -> Vec; + /// Deserializes a serialized COSE object to a struct + fn from_cose(bytes: &[u8]) -> Result + where + Self: Sized; +} #[cfg(test)] mod test { use super::*; diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index fe9efc71a..4ce4b0252 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,9 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - AsymmetricCryptoKey, AsymmetricEncryptable, SymmetricCryptoKey, + util::FromStrVisitor, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, + SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion @@ -141,7 +143,7 @@ impl<'de> Deserialize<'de> for UnsignedSharedKey { where D: serde::Deserializer<'de>, { - deserializer.deserialize_str(super::FromStrVisitor::new()) + deserializer.deserialize_str(FromStrVisitor::new()) } } @@ -160,13 +162,18 @@ impl UnsignedSharedKey { /// and thus does not guarantee sender authenticity. pub fn encapsulate_key_unsigned( encapsulated_key: &SymmetricCryptoKey, - encapsulation_key: &dyn AsymmetricEncryptable, + encapsulation_key: &AsymmetricPublicCryptoKey, ) -> Result { - let enc = encrypt_rsa2048_oaep_sha1( - encapsulation_key.to_public_key(), - &encapsulated_key.to_encoded(), - )?; - Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data: enc }) + match encapsulation_key.inner() { + RawPublicKey::RsaOaepSha1(rsa_public_key) => { + Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { + data: encrypt_rsa2048_oaep_sha1( + rsa_public_key, + &encapsulated_key.to_encoded(), + )?, + }) + } + } } /// The numerical representation of the encryption type of the [UnsignedSharedKey]. @@ -190,25 +197,29 @@ impl UnsignedSharedKey { &self, decapsulation_key: &AsymmetricCryptoKey, ) -> Result { - use UnsignedSharedKey::*; - let mut key_data = match self { - Rsa2048_OaepSha256_B64 { data } => decapsulation_key - .key - .decrypt(Oaep::new::(), data), - Rsa2048_OaepSha1_B64 { data } => decapsulation_key - .key - .decrypt(Oaep::new::(), data), - #[allow(deprecated)] - Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => decapsulation_key - .key - .decrypt(Oaep::new::(), data), - #[allow(deprecated)] - Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => decapsulation_key - .key - .decrypt(Oaep::new::(), data), + match decapsulation_key.inner() { + RawPrivateKey::RsaOaepSha1(rsa_private_key) => { + use UnsignedSharedKey::*; + let mut key_data = match self { + Rsa2048_OaepSha256_B64 { data } => { + rsa_private_key.decrypt(Oaep::new::(), data) + } + Rsa2048_OaepSha1_B64 { data } => { + rsa_private_key.decrypt(Oaep::new::(), data) + } + #[allow(deprecated)] + Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => { + rsa_private_key.decrypt(Oaep::new::(), data) + } + #[allow(deprecated)] + Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => { + rsa_private_key.decrypt(Oaep::new::(), data) + } + } + .map_err(|_| CryptoError::KeyDecrypt)?; + SymmetricCryptoKey::try_from(key_data.as_mut_slice()) + } } - .map_err(|_| CryptoError::KeyDecrypt)?; - SymmetricCryptoKey::try_from(key_data.as_mut_slice()) } } @@ -229,8 +240,8 @@ impl schemars::JsonSchema for UnsignedSharedKey { mod tests { use schemars::schema_for; - use super::{AsymmetricCryptoKey, UnsignedSharedKey}; - use crate::SymmetricCryptoKey; + use super::UnsignedSharedKey; + use crate::{AsymmetricCryptoKey, SymmetricCryptoKey}; const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS diff --git a/crates/bitwarden-crypto/src/enc_string/mod.rs b/crates/bitwarden-crypto/src/enc_string/mod.rs index fa001399b..51daf4fb0 100644 --- a/crates/bitwarden-crypto/src/enc_string/mod.rs +++ b/crates/bitwarden-crypto/src/enc_string/mod.rs @@ -9,8 +9,6 @@ mod asymmetric; mod symmetric; -use std::str::FromStr; - pub use asymmetric::UnsignedSharedKey; use base64::{engine::general_purpose::STANDARD, Engine}; pub use symmetric::EncString; @@ -59,30 +57,6 @@ fn split_enc_string(s: &str) -> (&str, Vec<&str>) { } } -struct FromStrVisitor(std::marker::PhantomData); -impl FromStrVisitor { - fn new() -> Self { - Self(Default::default()) - } -} -impl serde::de::Visitor<'_> for FromStrVisitor -where - T::Err: std::fmt::Debug, -{ - type Value = T; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "a valid string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - T::from_str(v).map_err(|e| E::custom(format!("{:?}", e))) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index 08ca7ca0d..010fb1a9e 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use super::{check_length, from_b64, from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result, UnsupportedOperation}, + util::FromStrVisitor, Aes256CbcHmacKey, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, XChaCha20Poly1305Key, }; @@ -235,7 +236,7 @@ impl<'de> Deserialize<'de> for EncString { where D: serde::Deserializer<'de>, { - deserializer.deserialize_str(super::FromStrVisitor::new()) + deserializer.deserialize_str(FromStrVisitor::new()) } } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index bb078b620..b411009c0 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -60,6 +60,12 @@ pub enum CryptoError { #[error("Invalid nonce length")] InvalidNonceLength, + + #[error("Signature error, {0}")] + SignatureError(#[from] SignatureError), + + #[error("Encoding error, {0}")] + EncodingError(#[from] EncodingError), } #[derive(Debug, Error)] @@ -96,5 +102,27 @@ pub enum RsaError { Rsa(#[from] rsa::Error), } +#[derive(Debug, Error)] +pub enum SignatureError { + #[error("Invalid signature")] + InvalidSignature, + #[error("Invalid namespace")] + InvalidNamespace, +} + +#[derive(Debug, Error)] +pub enum EncodingError { + #[error("Invalid cose encoding")] + InvalidCoseEncoding, + #[error("Cbor serialization error")] + InvalidCborSerialization, + #[error("Missing value {0}")] + MissingValue(&'static str), + #[error("Invalid value {0}")] + InvalidValue(&'static str), + #[error("Unsupported value {0}")] + UnsupportedValue(&'static str), +} + /// Alias for `Result`. pub(crate) type Result = std::result::Result; diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index bae60b4e2..8ef598e33 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -1,49 +1,72 @@ use std::pin::Pin; use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use super::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 { - #[allow(missing_docs)] - fn to_public_key(&self) -> &RsaPublicKey; +/// Algorithm / public key encryption scheme used for encryption/decryption. +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum PublicKeyEncryptionAlgorithm { + /// RSA with OAEP padding and SHA-1 hashing. + RsaOaepSha1 = 0, +} + +#[derive(Clone)] +pub(crate) enum RawPublicKey { + RsaOaepSha1(RsaPublicKey), } -/// An asymmetric public encryption key. Can only encrypt -/// [UnsignedSharedKey](crate::UnsignedSharedKey), usually accompanied by a -/// [AsymmetricCryptoKey] +/// Public key of a key pair used in a public key encryption scheme. It is used for +/// encrypting data. +#[derive(Clone)] pub struct AsymmetricPublicCryptoKey { - key: RsaPublicKey, + inner: RawPublicKey, } impl AsymmetricPublicCryptoKey { + pub(crate) fn inner(&self) -> &RawPublicKey { + &self.inner + } + /// Build a public key from the SubjectPublicKeyInfo DER. pub fn from_der(der: &[u8]) -> Result { - Ok(Self { - key: rsa::RsaPublicKey::from_public_key_der(der) - .map_err(|_| CryptoError::InvalidKey)?, + Ok(AsymmetricPublicCryptoKey { + inner: RawPublicKey::RsaOaepSha1( + RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?, + ), }) } -} -impl AsymmetricEncryptable for AsymmetricPublicCryptoKey { - fn to_public_key(&self) -> &RsaPublicKey { - &self.key + /// Makes a SubjectPublicKeyInfo DER serialized version of the public key. + pub fn to_der(&self) -> Result> { + use rsa::pkcs8::EncodePublicKey; + match &self.inner { + RawPublicKey::RsaOaepSha1(public_key) => Ok(public_key + .to_public_key_der() + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_owned()), + } } } -/// An asymmetric encryption key. Contains both the public and private key. Can be used to both -/// encrypt and decrypt [`UnsignedSharedKey`](crate::UnsignedSharedKey). #[derive(Clone)] -pub struct AsymmetricCryptoKey { +pub(crate) enum RawPrivateKey { // RsaPrivateKey is not a Copy type so this isn't completely necessary, but // to keep the compiler from making stack copies when moving this struct around, // we use a Box to keep the values on the heap. We also pin the box to make sure // that the contents can't be pulled out of the box and moved - pub(crate) key: Pin>, + RsaOaepSha1(Pin>), +} + +/// Private key of a key pair used in a public key encryption scheme. It is used for +/// decrypting data that was encrypted with the corresponding public key. +#[derive(Clone)] +pub struct AsymmetricCryptoKey { + inner: RawPrivateKey, } // Note that RsaPrivateKey already implements ZeroizeOnDrop, so we don't need to do anything @@ -54,16 +77,25 @@ const _: () = { assert_zeroize_on_drop::(); } }; - impl zeroize::ZeroizeOnDrop for AsymmetricCryptoKey {} +impl CryptoKey for AsymmetricCryptoKey {} impl AsymmetricCryptoKey { /// Generate a random AsymmetricCryptoKey (RSA-2048). - pub fn generate(rng: &mut R) -> Self { - let bits = 2048; + pub fn make(algorithm: PublicKeyEncryptionAlgorithm) -> Self { + Self::make_internal(algorithm, &mut rand::thread_rng()) + } - Self { - key: Box::pin(RsaPrivateKey::new(rng, bits).expect("failed to generate a key")), + fn make_internal( + algorithm: PublicKeyEncryptionAlgorithm, + rng: &mut R, + ) -> Self { + match algorithm { + PublicKeyEncryptionAlgorithm::RsaOaepSha1 => Self { + inner: RawPrivateKey::RsaOaepSha1(Box::pin( + RsaPrivateKey::new(rng, 2048).expect("failed to generate a key"), + )), + }, } } @@ -71,7 +103,9 @@ impl AsymmetricCryptoKey { pub fn from_pem(pem: &str) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { - key: Box::pin(RsaPrivateKey::from_pkcs8_pem(pem).map_err(|_| CryptoError::InvalidKey)?), + inner: RawPrivateKey::RsaOaepSha1(Box::pin( + RsaPrivateKey::from_pkcs8_pem(pem).map_err(|_| CryptoError::InvalidKey)?, + )), }) } @@ -79,41 +113,41 @@ impl AsymmetricCryptoKey { pub fn from_der(der: &[u8]) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { - key: Box::pin(RsaPrivateKey::from_pkcs8_der(der).map_err(|_| CryptoError::InvalidKey)?), + inner: RawPrivateKey::RsaOaepSha1(Box::pin( + RsaPrivateKey::from_pkcs8_der(der).map_err(|_| CryptoError::InvalidKey)?, + )), }) } #[allow(missing_docs)] pub fn to_der(&self) -> Result> { - use rsa::pkcs8::EncodePrivateKey; - Ok(self - .key - .to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_owned()) + match &self.inner { + RawPrivateKey::RsaOaepSha1(private_key) => { + use rsa::pkcs8::EncodePrivateKey; + Ok(private_key + .to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_owned()) + } + } } - #[allow(missing_docs)] - pub fn to_public_der(&self) -> Result> { - use rsa::pkcs8::EncodePublicKey; - Ok(self - .to_public_key() - .to_public_key_der() - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_owned()) + /// Derives the public key corresponding to this private key. This is deterministic + /// and always derives the same public key. + pub fn to_public_key(&self) -> AsymmetricPublicCryptoKey { + match &self.inner { + RawPrivateKey::RsaOaepSha1(private_key) => AsymmetricPublicCryptoKey { + inner: RawPublicKey::RsaOaepSha1(private_key.to_public_key()), + }, + } } -} -impl AsymmetricEncryptable for AsymmetricCryptoKey { - fn to_public_key(&self) -> &RsaPublicKey { - (*self.key).as_ref() + pub(crate) fn inner(&self) -> &RawPrivateKey { + &self.inner } } -impl CryptoKey for AsymmetricCryptoKey {} - // We manually implement these to make sure we don't print any sensitive data impl std::fmt::Debug for AsymmetricCryptoKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -165,7 +199,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= // Load the two different formats and check they are the same key let pem_key = AsymmetricCryptoKey::from_pem(pem_key_str).unwrap(); let der_key = AsymmetricCryptoKey::from_der(&der_key_vec).unwrap(); - assert_eq!(pem_key.key, der_key.key); + assert_eq!(pem_key.to_der().unwrap(), der_key.to_der().unwrap()); // Check that the keys can be converted back to DER assert_eq!(der_key.to_der().unwrap(), der_key_vec); diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index f6828c824..6f083e718 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,6 +1,7 @@ +use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm}; use crate::{ - error::Result, AsymmetricCryptoKey, CryptoError, EncString, KeyDecryptable, KeyEncryptable, - SymmetricCryptoKey, UnsignedSharedKey, + error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + UnsignedSharedKey, }; /// Device Key @@ -30,16 +31,19 @@ impl DeviceKey { /// Note: Input has to be a SymmetricCryptoKey instead of UserKey because that's what we get /// from EncSettings. pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { - let mut rng = rand::thread_rng(); let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); - let device_private_key = AsymmetricCryptoKey::generate(&mut rng); + let device_private_key = + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); - let protected_user_key = - UnsignedSharedKey::encapsulate_key_unsigned(user_key, &device_private_key)?; + let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned( + user_key, + &device_private_key.to_public_key(), + )?; let protected_device_public_key = device_private_key - .to_public_der()? + .to_public_key() + .to_der()? .encrypt_with_key(user_key)?; let protected_device_private_key = device_private_key diff --git a/crates/bitwarden-crypto/src/keys/key_id.rs b/crates/bitwarden-crypto/src/keys/key_id.rs index ef7459ace..2184027a8 100644 --- a/crates/bitwarden-crypto/src/keys/key_id.rs +++ b/crates/bitwarden-crypto/src/keys/key_id.rs @@ -9,7 +9,8 @@ pub(crate) const KEY_ID_SIZE: usize = 16; /// A key id is a unique identifier for a single key. There is a 1:1 mapping between key ID and key /// bytes, so something like a user key rotation is replacing the key with ID A with a new key with /// ID B. -pub(crate) struct KeyId(uuid::Uuid); +#[derive(Clone)] +pub(crate) struct KeyId(Uuid); /// Fixed length identifiers for keys. /// These are intended to be unique and constant per-key. @@ -38,6 +39,12 @@ impl From for [u8; KEY_ID_SIZE] { } } +impl From<&KeyId> for Vec { + fn from(key_id: &KeyId) -> Self { + key_id.0.as_bytes().to_vec() + } +} + impl From<[u8; KEY_ID_SIZE]> for KeyId { fn from(bytes: [u8; KEY_ID_SIZE]) -> Self { KeyId(Uuid::from_bytes(bytes)) diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index efdbc52d4..4327d20c0 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -12,8 +12,11 @@ pub use symmetric_crypto_key::{ }; mod asymmetric_crypto_key; pub use asymmetric_crypto_key::{ - AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm, }; +pub(crate) use asymmetric_crypto_key::{RawPrivateKey, RawPublicKey}; +mod signed_public_key; +pub use signed_public_key::{SignedPublicKey, SignedPublicKeyMessage}; mod user_key; pub use user_key::UserKey; mod device_key; @@ -26,6 +29,5 @@ pub use kdf::{ default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, default_pbkdf2_iterations, Kdf, }; -#[cfg(test)] -pub(crate) use key_id::KEY_ID_SIZE; +pub(crate) use key_id::{KeyId, KEY_ID_SIZE}; mod utils; diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs new file mode 100644 index 000000000..ad2597b95 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -0,0 +1,181 @@ +//! A public encryption key alone is not authenticated. It needs to be tied to a cryptographic +//! identity, which is provided by a signature keypair. This is done by signing the public key, and +//! requiring consumers to verify the public key before consumption by using unwrap_and_verify. + +use std::str::FromStr; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use super::AsymmetricPublicCryptoKey; +use crate::{ + cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CryptoError, + PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey, SigningNamespace, + VerifyingKey, +}; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_CUSTOM_TYPES: &'static str = r#" +export type SignedPublicKey = string; +"#; + +/// `PublicKeyFormat` defines the format of the public key in a `SignedAsymmetricPublicKeyMessage`. +/// Currently, only ASN.1 Subject Public Key Info (SPKI) is used, but CoseKey may become another +/// option in the future. +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] +enum PublicKeyFormat { + Spki = 0, +} + +/// `SignedAsymmetricPublicKeyMessage` is a message that once signed, makes a claim towards owning a +/// public encryption key. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignedPublicKeyMessage { + /// The algorithm/crypto system used with this public key. + algorithm: PublicKeyEncryptionAlgorithm, + /// The format of the public key. + content_format: PublicKeyFormat, + /// The public key, serialized and formatted in the content format specified in + /// `content_format`. + /// + /// Note: [ByteBuf] is used here to ensure efficient serialization. Using [`Vec`] would + /// lead to an incompatible encoding of individual bytes, instead of a contiguous byte + /// buffer. + public_key: ByteBuf, +} + +impl SignedPublicKeyMessage { + /// Creates a new `SignedPublicKeyMessage` from an `AsymmetricPublicCryptoKey`. This message + /// can then be signed using a `SigningKey` to create a `SignedPublicKey`. + pub fn from_public_key(public_key: &AsymmetricPublicCryptoKey) -> Result { + match public_key.inner() { + RawPublicKey::RsaOaepSha1(_) => Ok(SignedPublicKeyMessage { + algorithm: PublicKeyEncryptionAlgorithm::RsaOaepSha1, + content_format: PublicKeyFormat::Spki, + public_key: ByteBuf::from(public_key.to_der()?), + }), + } + } + + /// Signs the `SignedPublicKeyMessage` using the provided `SigningKey`, and returns a + /// `SignedPublicKey`. + pub fn sign(&self, signing_key: &SigningKey) -> Result { + Ok(SignedPublicKey( + signing_key.sign(self, &SigningNamespace::SignedPublicKey)?, + )) + } +} + +/// `SignedAsymmetricPublicKey` is a public encryption key, signed by the owner of the encryption +/// keypair. This wrapping ensures that the consumer of the public key MUST verify the identity of +/// the Signer before they can use the public key for encryption. +#[derive(Clone, Debug)] +pub struct SignedPublicKey(pub(crate) SignedObject); + +impl From for Vec { + fn from(val: SignedPublicKey) -> Self { + val.0.to_cose() + } +} + +impl TryFrom> for SignedPublicKey { + type Error = EncodingError; + fn try_from(bytes: Vec) -> Result { + Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) + } +} + +impl From for String { + fn from(val: SignedPublicKey) -> Self { + let bytes: Vec = val.into(); + STANDARD.encode(&bytes) + } +} + +impl SignedPublicKey { + /// Verifies the signature of the public key against the provided `VerifyingKey`, and returns + /// the `AsymmetricPublicCryptoKey` if the verification is successful. + pub fn verify_and_unwrap( + self, + verifying_key: &VerifyingKey, + ) -> Result { + let public_key_message: SignedPublicKeyMessage = self + .0 + .verify_and_unwrap(verifying_key, &SigningNamespace::SignedPublicKey)?; + match ( + public_key_message.algorithm, + public_key_message.content_format, + ) { + (PublicKeyEncryptionAlgorithm::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( + AsymmetricPublicCryptoKey::from_der(&public_key_message.public_key.into_vec()) + .map_err(|_| EncodingError::InvalidValue("public key"))?, + ), + } + } +} + +impl FromStr for SignedPublicKey { + type Err = EncodingError; + + fn from_str(s: &str) -> Result { + let bytes = STANDARD + .decode(s) + .map_err(|_| EncodingError::InvalidCborSerialization)?; + Self::try_from(bytes) + } +} + +impl<'de> Deserialize<'de> for SignedPublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(FromStrVisitor::new()) + } +} + +impl serde::Serialize for SignedPublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let b64_serialized_signed_public_key: String = self.clone().into(); + serializer.serialize_str(&b64_serialized_signed_public_key) + } +} + +impl schemars::JsonSchema for SignedPublicKey { + fn schema_name() -> String { + "SignedPublicKey".to_string() + } + + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + generator.subschema_for::() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm, SignatureAlgorithm}; + + #[test] + fn test_signed_asymmetric_public_key() { + let public_key = + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1).to_public_key(); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let message = SignedPublicKeyMessage::from_public_key(&public_key).unwrap(); + let signed_public_key = message.sign(&signing_key).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let verified_public_key = signed_public_key.verify_and_unwrap(&verifying_key).unwrap(); + assert_eq!( + public_key.to_der().unwrap(), + verified_public_key.to_der().unwrap() + ); + } +} diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index d3a0e304d..6db9df997 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -31,6 +31,9 @@ pub use wordlist::EFF_LONG_WORD_LIST; mod store; pub use store::{KeyStore, KeyStoreContext}; mod cose; +pub use cose::CoseSerializable; +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/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs new file mode 100644 index 000000000..d34d359d0 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -0,0 +1,145 @@ +//! This file contains helper functions to aid in COSE deserialization + +use coset::{ + iana::{EllipticCurve, EnumI64, OkpKeyParameter}, + CoseKey, Label, ProtectedHeader, RegisteredLabel, +}; + +use super::SigningNamespace; +use crate::{ + cose::SIGNING_NAMESPACE, + error::{EncodingError, SignatureError}, + keys::KeyId, + CryptoError, KEY_ID_SIZE, +}; + +/// Helper function to extract the namespace from a `ProtectedHeader`. The namespace is a custom +/// header set on the protected headers of the signature object. +pub(super) fn namespace( + protected_header: &ProtectedHeader, +) -> Result { + let namespace = protected_header + .header + .rest + .iter() + .find_map(|(key, value)| { + if let Label::Int(key) = key { + if *key == SIGNING_NAMESPACE { + return value.as_integer(); + } + } + None + }) + .ok_or(SignatureError::InvalidNamespace)?; + + SigningNamespace::try_from(i128::from(namespace)) +} + +/// Helper function to extract the content type from a `ProtectedHeader`. The content type is a +/// standardized header set on the protected headers of the signature object. Currently we only +/// support registered values, but PrivateUse values are also allowed in the COSE specification. +pub(super) fn content_type( + protected_header: &ProtectedHeader, +) -> Result { + protected_header + .header + .content_type + .as_ref() + .and_then(|ct| match ct { + RegisteredLabel::Assigned(content_format) => Some(*content_format), + _ => None, + }) + .ok_or_else(|| SignatureError::InvalidSignature.into()) +} + +/// Helper function to extract the key ID from a `CoseKey`. The key ID is a standardized header +/// and always set in bitwarden-crypto generated encrypted messages or signatures. +pub(super) fn key_id(cose_key: &CoseKey) -> Result { + let key_id: [u8; KEY_ID_SIZE] = cose_key + .key_id + .as_slice() + .try_into() + .map_err(|_| EncodingError::InvalidValue("key id length"))?; + let key_id: KeyId = key_id.into(); + Ok(key_id) +} + +/// Helper function to parse a ed25519 signing key from a `CoseKey`. +pub(super) fn ed25519_signing_key( + cose_key: &CoseKey, +) -> Result { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let d = okp_d(cose_key)?; + let crv = okp_curve(cose_key)?; + if crv == EllipticCurve::Ed25519.to_i64().into() { + Ok(ed25519_dalek::SigningKey::from_bytes( + d.try_into() + .map_err(|_| EncodingError::InvalidCoseEncoding)?, + )) + } else { + Err(EncodingError::UnsupportedValue("OKP curve")) + } +} + +/// Helper function to parse a ed25519 verifying key from a `CoseKey`. +pub(super) fn ed25519_verifying_key( + cose_key: &CoseKey, +) -> Result { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let x = okp_x(cose_key)?; + let crv = okp_curve(cose_key)?; + if crv == EllipticCurve::Ed25519.to_i64().into() { + ed25519_dalek::VerifyingKey::from_bytes( + x.try_into() + .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key"))?, + ) + .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key")) + } else { + Err(EncodingError::UnsupportedValue("OKP curve")) + } +} + +/// Helper function to parse the private key `d` from a `CoseKey`. +fn okp_d(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + cose_key + .params + .iter() + .find_map(|(key, value)| match key { + Label::Int(i) if OkpKeyParameter::from_i64(*i) == Some(OkpKeyParameter::D) => { + value.as_bytes().map(|v| v.as_slice()) + } + _ => None, + }) + .ok_or(EncodingError::MissingValue("OKP private key")) +} + +/// Helper function to parse the public key `x` from a `CoseKey`. +fn okp_x(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + cose_key + .params + .iter() + .find_map(|(key, value)| match key { + Label::Int(i) if OkpKeyParameter::from_i64(*i) == Some(OkpKeyParameter::X) => { + value.as_bytes().map(|v| v.as_slice()) + } + _ => None, + }) + .ok_or(EncodingError::MissingValue("OKP public key")) +} + +/// Helper function to parse the OKP curve from a `CoseKey`. +fn okp_curve(cose_key: &CoseKey) -> Result { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + cose_key + .params + .iter() + .find_map(|(key, value)| match key { + Label::Int(i) if OkpKeyParameter::from_i64(*i) == Some(OkpKeyParameter::Crv) => { + value.as_integer().map(i128::from) + } + _ => None, + }) + .ok_or(EncodingError::MissingValue("OKP curve")) +} diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs new file mode 100644 index 000000000..6556a7a67 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -0,0 +1,118 @@ +//! This file contains message serialization for messages intended to be signed. +//! +//! Consumers of the signing API should not care about or implement individual ways to represent +//! structs. Thus, the only publicly exposed api takes a struct, and the signing module takes care +//! of the serialization under the hood. This requires converting the struct to a byte array +//! using some serialization format. Further, the serialization format must be written to the +//! signature object so that it can be used upon deserialization to use the correct deserializer. +//! +//! To provide this interface, the SerializedMessage struct is introduced. SerializedMessage +//! represents the serialized bytes along with the content format used for serialization. The latter +//! is stored on the signed object, in e.g. a COSE header, so that upon deserialization the correct +//! deserializer can be used. +//! +//! Currently, only CBOR serialization / deserialization is implemented, since it is compact and is +//! what COSE already uses. + +use coset::iana::CoapContentFormat; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::error::EncodingError; + +/// A message (struct) to be signed, serialized to a byte array, along with the content format of +/// the bytes. +pub struct SerializedMessage { + serialized_message_bytes: Vec, + content_type: CoapContentFormat, +} + +impl AsRef<[u8]> for SerializedMessage { + fn as_ref(&self) -> &[u8] { + &self.serialized_message_bytes + } +} + +impl SerializedMessage { + /// Creates a new `SerializedMessage` from a byte array and content type. + pub fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { + SerializedMessage { + serialized_message_bytes: bytes, + content_type, + } + } + + /// Returns the serialized message bytes as a slice. This representation needs to be used + /// together with a content type to deserialize the message correctly. + pub fn as_bytes(&self) -> &[u8] { + &self.serialized_message_bytes + } + + pub(super) fn content_type(&self) -> CoapContentFormat { + self.content_type + } + + /// Encodes a message into a `SerializedMessage` using CBOR serialization. + pub(super) fn encode(message: &Message) -> Result { + let mut buffer = Vec::new(); + ciborium::ser::into_writer(message, &mut buffer) + .map_err(|_| EncodingError::InvalidCborSerialization)?; + Ok(SerializedMessage { + serialized_message_bytes: buffer, + content_type: CoapContentFormat::Cbor, + }) + } + + /// Creates a new `SerializedMessage` from a byte array and content type. + /// This currently implements only CBOR serialization, so the content type must be `Cbor`. + pub fn decode(&self) -> Result { + if self.content_type != CoapContentFormat::Cbor { + return Err(EncodingError::InvalidValue("Unsupported content type")); + } + + ciborium::de::from_reader(self.serialized_message_bytes.as_slice()) + .map_err(|_| EncodingError::InvalidCborSerialization) + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct TestMessage { + field1: String, + field2: u32, + } + + #[test] + fn test_serialization() { + let message = TestMessage { + field1: "Hello".to_string(), + field2: 42, + }; + + let serialized = SerializedMessage::encode(&message).unwrap(); + let deserialized: TestMessage = serialized.decode().unwrap(); + + assert_eq!(message, deserialized); + } + + #[test] + fn test_bytes() { + let message = TestMessage { + field1: "Hello".to_string(), + field2: 42, + }; + + let serialized = SerializedMessage::encode(&message).unwrap(); + let deserialized: TestMessage = SerializedMessage::from_bytes( + serialized.as_bytes().to_vec(), + serialized.content_type(), + ) + .decode() + .unwrap(); + assert_eq!(message, deserialized); + } +} diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs new file mode 100644 index 000000000..89753263a --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -0,0 +1,112 @@ +//! 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. This is implemented in the ['signing_key'] and ['verifying_key'] modules. +//! +//! Signing operations are split into two types. The mid-level and high-level operations are +//! implemented for each type respectively. +//! - Sign: Create a [`signed_object::SignedObject`] 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::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. + +mod cose; +use cose::*; +mod namespace; +pub use namespace::SigningNamespace; +mod signed_object; +pub use signed_object::SignedObject; +mod signature; +pub use signature::Signature; +mod signing_key; +pub use signing_key::SigningKey; +mod verifying_key; +pub use verifying_key::VerifyingKey; +mod message; +pub use message::SerializedMessage; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use {tsify_next::Tsify, wasm_bindgen::prelude::*}; + +/// The type of key / signature scheme used for signing and verifying. +#[derive(Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +#[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 is the modern, secure recommended option for digital signatures on eliptic curves. + Ed25519, +} + +impl SignatureAlgorithm { + /// Returns the currently accepted safe algorithm for new keys. + pub fn default_algorithm() -> Self { + SignatureAlgorithm::Ed25519 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CoseSerializable; + + #[derive(Deserialize, Debug, PartialEq, Serialize)] + struct TestMessage { + field1: String, + } + + /// 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); + let verifying_key = signing_key.to_verifying_key(); + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let (signature, serialized_message) = signing_key + .sign_detached(&test_message, &SigningNamespace::ExampleNamespace) + .unwrap(); + let signed_object = signing_key + .sign(&test_message, &SigningNamespace::ExampleNamespace) + .unwrap(); + let raw_signed_array = signing_key.sign_raw("Test message".as_bytes()); + println!("const SIGNING_KEY: &[u8] = &{:?};", signing_key.to_cose()); + println!( + "const VERIFYING_KEY: &[u8] = &{:?};", + verifying_key.to_cose() + ); + println!("const SIGNATURE: &[u8] = &{:?};", signature.to_cose()); + println!( + "const SERIALIZED_MESSAGE: &[u8] = &{:?};", + serialized_message.as_bytes() + ); + println!( + "const SIGNED_OBJECT: &[u8] = &{:?};", + signed_object.to_cose() + ); + println!( + "const SIGNED_OBJECT_RAW: &[u8] = &{:?};", + raw_signed_array.as_slice() + ); + } +} diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs new file mode 100644 index 000000000..fd48bd2b9 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -0,0 +1,54 @@ +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. +/// +/// If there is a new version of a message added, it should (generally) use a new namespace, since +/// this prevents downgrades to the old type of message, and makes optional fields unnecessary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SigningNamespace { + /// The namespace for + /// [`SignedPublicKey`](crate::keys::SignedPublicKey). + SignedPublicKey = 1, + /// This namespace is only used in tests + #[cfg(test)] + ExampleNamespace = -1, + /// This namespace is only used in tests + #[cfg(test)] + ExampleNamespace2 = -2, +} + +impl SigningNamespace { + /// Returns the numeric value of the namespace. + pub fn as_i64(&self) -> i64 { + *self as i64 + } +} + +impl TryFrom for SigningNamespace { + type Error = CryptoError; + + fn try_from(value: i64) -> Result { + match value { + 1 => Ok(SigningNamespace::SignedPublicKey), + #[cfg(test)] + -1 => Ok(SigningNamespace::ExampleNamespace), + #[cfg(test)] + -2 => Ok(SigningNamespace::ExampleNamespace2), + _ => Err(SignatureError::InvalidNamespace.into()), + } + } +} + +impl TryFrom for SigningNamespace { + type Error = CryptoError; + + fn try_from(value: i128) -> Result { + let Ok(value) = i64::try_from(value) else { + return Err(SignatureError::InvalidNamespace.into()); + }; + Self::try_from(value) + } +} diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs new file mode 100644 index 000000000..47bbbe6be --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -0,0 +1,285 @@ +use ciborium::value::Integer; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1}; +use serde::Serialize; + +use super::{ + content_type, message::SerializedMessage, namespace, signing_key::SigningKey, SigningNamespace, + VerifyingKey, +}; +use crate::{ + cose::{CoseSerializable, SIGNING_NAMESPACE}, + error::{EncodingError, SignatureError}, + CryptoError, +}; + +/// 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. +pub struct Signature(CoseSign1); + +impl From for Signature { + fn from(cose_sign1: CoseSign1) -> Self { + Signature(cose_sign1) + } +} + +impl Signature { + fn inner(&self) -> &CoseSign1 { + &self.0 + } + + fn namespace(&self) -> Result { + namespace(&self.0.protected) + } + + /// Parses the signature headers and returns the content type of the signed data. The content + /// type indicates how the serialized message that was signed was encoded. + pub fn content_type(&self) -> Result { + content_type(&self.0.protected) + } + + /// 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. + /// + /// The first anticipated consumer will be signed org memberships / emergency access: + /// + pub fn verify( + &self, + serialized_message_bytes: &[u8], + verifying_key: &VerifyingKey, + namespace: &SigningNamespace, + ) -> bool { + if self.inner().protected.header.alg.is_none() { + return false; + } + + if self.namespace().ok().as_ref() != Some(namespace) { + return false; + } + + self.inner() + .verify_detached_signature(serialized_message_bytes, &[], |sig, data| { + verifying_key.verify_raw(sig, data) + }) + .is_ok() + } +} + +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}; + /// + /// const EXAMPLE_NAMESPACE: SigningNamespace = SigningNamespace::SignedPublicKey; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = EXAMPLE_NAMESPACE; + /// let (signature, serialized_message) = signing_key.sign_detached(&message, &namespace).unwrap(); + /// // Verification + /// let verifying_key = signing_key.to_verifying_key(); + /// assert!(signature.verify(&serialized_message.as_bytes(), &verifying_key, &namespace)); + /// ``` + pub fn sign_detached( + &self, + message: &Message, + namespace: &SigningNamespace, + ) -> Result<(Signature, SerializedMessage), CryptoError> { + let serialized_message = SerializedMessage::encode(message)?; + Ok(( + self.sign_detached_bytes(&serialized_message, namespace), + serialized_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); + /// let bob_key = SigningKey::make(SignatureAlgorithm::Ed25519); + /// + /// 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(); ``` + pub fn counter_sign_detached( + &self, + serialized_message_bytes: Vec, + initial_signature: &Signature, + namespace: &SigningNamespace, + ) -> Result { + // 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::from_bytes( + serialized_message_bytes, + 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.as_bytes(), &[], |pt| self.sign_raw(pt)) + .build(), + ) + } +} + +impl CoseSerializable for Signature { + fn from_cose(bytes: &[u8]) -> Result { + let cose_sign1 = + CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; + Ok(Signature(cose_sign1)) + } + + fn to_cose(&self) -> Vec { + self.0 + .clone() + .to_vec() + .expect("Signature is always serializable") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SignatureAlgorithm; + + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, 94, 152, 45, 63, 13, 71, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 93, 213, 35, 177, 81, 219, 226, 241, 147, 140, 238, + 32, 34, 183, 213, 107, 227, 92, 75, 84, 208, 47, 198, 80, 18, 188, 172, 145, 184, 154, 26, + 170, + ]; + const SIGNATURE: &[u8] = &[ + 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, + 94, 152, 45, 63, 13, 71, 58, 0, 1, 56, 127, 32, 160, 246, 88, 64, 206, 83, 177, 184, 37, + 103, 128, 39, 120, 174, 61, 4, 29, 184, 68, 46, 47, 203, 47, 246, 108, 160, 169, 114, 7, + 165, 119, 198, 3, 209, 52, 249, 89, 31, 156, 255, 212, 75, 224, 78, 183, 37, 174, 63, 112, + 70, 219, 246, 19, 213, 17, 121, 249, 244, 23, 182, 36, 193, 175, 55, 250, 65, 250, 6, + ]; + 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, + ]; + + #[test] + fn test_cose_roundtrip_encode_signature() { + let signature = Signature::from_cose(SIGNATURE).unwrap(); + let cose_bytes = signature.to_cose(); + let decoded_signature = Signature::from_cose(&cose_bytes).unwrap(); + assert_eq!(signature.inner(), decoded_signature.inner()); + } + + #[test] + fn test_verify_testvector() { + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let signature = Signature::from_cose(SIGNATURE).unwrap(); + let serialized_message = + SerializedMessage::from_bytes(SERIALIZED_MESSAGE.to_vec(), CoapContentFormat::Cbor); + + let namespace = SigningNamespace::ExampleNamespace; + + assert!(signature.verify(serialized_message.as_ref(), &verifying_key, &namespace)); + } + + #[test] + fn test_sign_detached_roundtrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let message = "Test message"; + let namespace = SigningNamespace::ExampleNamespace; + + let (signature, serialized_message) = + signing_key.sign_detached(&message, &namespace).unwrap(); + + let verifying_key = signing_key.to_verifying_key(); + assert!(signature.verify(serialized_message.as_ref(), &verifying_key, &namespace)); + } + + #[test] + fn test_countersign_detatched() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let message = "Test message"; + let namespace = SigningNamespace::ExampleNamespace; + + let (signature, serialized_message) = + signing_key.sign_detached(&message, &namespace).unwrap(); + + let countersignature = signing_key + .counter_sign_detached( + serialized_message.as_bytes().to_vec(), + &signature, + &namespace, + ) + .unwrap(); + + let verifying_key = signing_key.to_verifying_key(); + assert!(countersignature.verify(serialized_message.as_ref(), &verifying_key, &namespace)); + } + + #[test] + fn test_fail_namespace_changed() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let message = "Test message"; + let namespace = SigningNamespace::ExampleNamespace; + + let (signature, serialized_message) = + signing_key.sign_detached(&message, &namespace).unwrap(); + + let different_namespace = SigningNamespace::ExampleNamespace2; + let verifying_key = signing_key.to_verifying_key(); + + assert!(!signature.verify( + serialized_message.as_ref(), + &verifying_key, + &different_namespace + )); + } +} diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs new file mode 100644 index 000000000..289e52a23 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -0,0 +1,252 @@ +use ciborium::value::Integer; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1}; +use serde::{de::DeserializeOwned, Serialize}; + +use super::{ + content_type, message::SerializedMessage, namespace, signing_key::SigningKey, + verifying_key::VerifyingKey, SigningNamespace, +}; +use crate::{ + cose::{CoseSerializable, SIGNING_NAMESPACE}, + error::{EncodingError, SignatureError}, + CryptoError, +}; + +/// A signed object is a message containing a payload and signature that attests the payload's +/// integrity and authenticity for a specific namespace and signature key. In order to gain access +/// to the payload, the caller must provide the correct namespace and verifying key, ensuring that +/// the caller cannot forget to validate the signature before using the payload. +#[derive(Clone, Debug)] +pub struct SignedObject(pub(crate) CoseSign1); + +impl From for SignedObject { + fn from(cose_sign1: CoseSign1) -> Self { + SignedObject(cose_sign1) + } +} + +impl SignedObject { + /// Parses the signature headers and returns the content type of the signed data. The content + /// type indicates how the serialized message that was signed was encoded. + pub fn content_type(&self) -> Result { + content_type(&self.0.protected) + } + + fn inner(&self) -> &CoseSign1 { + &self.0 + } + + fn namespace(&self) -> Result { + namespace(&self.0.protected) + } + + fn payload(&self) -> Result, CryptoError> { + self.0 + .payload + .as_ref() + .ok_or(SignatureError::InvalidSignature.into()) + .map(|payload| payload.to_vec()) + } + + /// Verifies the signature of the signed object and returns the payload, if the signature is + /// valid. + pub fn verify_and_unwrap( + &self, + verifying_key: &VerifyingKey, + namespace: &SigningNamespace, + ) -> Result { + SerializedMessage::from_bytes( + self.verify_and_unwrap_bytes(verifying_key, namespace)?, + self.content_type()?, + ) + .decode() + .map_err(Into::into) + } + + /// Verifies the signature of the signed object and returns the payload as raw bytes, if the + /// signature is valid. + fn verify_and_unwrap_bytes( + &self, + verifying_key: &VerifyingKey, + namespace: &SigningNamespace, + ) -> Result, CryptoError> { + if self.inner().protected.header.alg.is_none() { + return Err(SignatureError::InvalidSignature.into()); + } + + if self.namespace()? != *namespace { + return Err(SignatureError::InvalidNamespace.into()); + } + + self.inner() + .verify_signature(&[], |sig, data| verifying_key.verify_raw(sig, data))?; + self.payload() + } +} + +impl SigningKey { + /// 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 { + 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.as_bytes().to_vec()) + .create_signature(&[], |pt| self.sign_raw(pt)) + .build(); + Ok(SignedObject(cose_sign1)) + } + + /// 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}; + /// + /// const EXAMPLE_NAMESPACE: SigningNamespace = SigningNamespace::SignedPublicKey; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = EXAMPLE_NAMESPACE; + /// 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 = signed_object.verify_and_unwrap(&verifying_key, &namespace).unwrap(); + /// assert_eq!(payload, message); + /// ``` + pub fn sign( + &self, + message: &Message, + namespace: &SigningNamespace, + ) -> Result { + self.sign_bytes(&SerializedMessage::encode(message)?, namespace) + } +} + +impl CoseSerializable for SignedObject { + fn from_cose(bytes: &[u8]) -> Result { + Ok(SignedObject( + CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?, + )) + } + + fn to_cose(&self) -> Vec { + self.0 + .clone() + .to_vec() + .expect("SignedObject is always serializable") + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use crate::{ + CoseSerializable, CryptoError, SignatureAlgorithm, SignedObject, SigningKey, + SigningNamespace, VerifyingKey, + }; + + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, 94, 152, 45, 63, 13, 71, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 93, 213, 35, 177, 81, 219, 226, 241, 147, 140, 238, + 32, 34, 183, 213, 107, 227, 92, 75, 84, 208, 47, 198, 80, 18, 188, 172, 145, 184, 154, 26, + 170, + ]; + const SIGNED_OBJECT: &[u8] = &[ + 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, + 94, 152, 45, 63, 13, 71, 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, 206, 83, 177, + 184, 37, 103, 128, 39, 120, 174, 61, 4, 29, 184, 68, 46, 47, 203, 47, 246, 108, 160, 169, + 114, 7, 165, 119, 198, 3, 209, 52, 249, 89, 31, 156, 255, 212, 75, 224, 78, 183, 37, 174, + 63, 112, 70, 219, 246, 19, 213, 17, 121, 249, 244, 23, 182, 36, 193, 175, 55, 250, 65, 250, + 6, + ]; + + #[derive(Deserialize, Debug, PartialEq, Serialize)] + struct TestMessage { + field1: String, + } + + #[test] + fn test_roundtrip_cose() { + let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + assert_eq!( + signed_object.content_type().unwrap(), + coset::iana::CoapContentFormat::Cbor + ); + let cose_bytes = signed_object.to_cose(); + assert_eq!(cose_bytes, SIGNED_OBJECT); + } + + #[test] + fn test_verify_and_unwrap_testvector() { + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let namespace = SigningNamespace::ExampleNamespace; + let payload: TestMessage = signed_object + .verify_and_unwrap(&verifying_key, &namespace) + .unwrap(); + assert_eq!(payload, test_message); + } + + #[test] + fn test_sign_verify_and_unwrap_roundtrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let namespace = SigningNamespace::ExampleNamespace; + let signed_object = signing_key.sign(&test_message, &namespace).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let payload: TestMessage = signed_object + .verify_and_unwrap(&verifying_key, &namespace) + .unwrap(); + assert_eq!(payload, test_message); + } + + #[test] + fn test_fail_namespace_changed() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let namespace = SigningNamespace::ExampleNamespace; + let signed_object = signing_key.sign(&test_message, &namespace).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + + let different_namespace = SigningNamespace::ExampleNamespace2; + let result: Result = + signed_object.verify_and_unwrap(&verifying_key, &different_namespace); + assert!(result.is_err()); + } +} diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs new file mode 100644 index 000000000..c8567ad34 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -0,0 +1,156 @@ +use std::pin::Pin; + +use ciborium::{value::Integer, Value}; +use coset::{ + iana::{Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, + CborSerializable, CoseKey, RegisteredLabel, RegisteredLabelWithPrivate, +}; +use ed25519_dalek::Signer; + +use super::{ + ed25519_signing_key, key_id, + verifying_key::{RawVerifyingKey, VerifyingKey}, + SignatureAlgorithm, +}; +use crate::{ + cose::CoseSerializable, + error::{EncodingError, Result}, + keys::KeyId, + CryptoKey, +}; + +/// A `SigningKey` without the key id. This enum contains a variant for each supported signature +/// scheme. +#[derive(Clone)] +enum RawSigningKey { + Ed25519(Pin>), +} + +/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be +/// derived from it. +#[derive(Clone)] +pub struct SigningKey { + pub(super) id: KeyId, + inner: RawSigningKey, +} + +// Note that `SigningKey` already implements ZeroizeOnDrop, so we don't need to do anything +// We add this assertion to make sure that this is still true in the future +// For any new keys, this needs to be checked +const _: () = { + fn assert_zeroize_on_drop() {} + fn assert_all() { + assert_zeroize_on_drop::(); + } +}; +impl zeroize::ZeroizeOnDrop for SigningKey {} +impl CryptoKey for SigningKey {} + +impl SigningKey { + /// Makes a new signing key for the given signature scheme. + pub fn make(algorithm: SignatureAlgorithm) -> Self { + match algorithm { + SignatureAlgorithm::Ed25519 => SigningKey { + id: KeyId::make(), + inner: RawSigningKey::Ed25519(Box::pin(ed25519_dalek::SigningKey::generate( + &mut rand::thread_rng(), + ))), + }, + } + } + + pub(super) fn cose_algorithm(&self) -> Algorithm { + match &self.inner { + RawSigningKey::Ed25519(_) => Algorithm::EdDSA, + } + } + + /// 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 { + RawSigningKey::Ed25519(key) => VerifyingKey { + id: self.id.clone(), + inner: RawVerifyingKey::Ed25519(key.verifying_key()), + }, + } + } + + /// Signs the given byte array with the signing key. + /// This should not be used directly other than for generating namespace separated signatures or + /// signed objects. + pub(super) fn sign_raw(&self, data: &[u8]) -> Vec { + match &self.inner { + RawSigningKey::Ed25519(key) => key.sign(data).to_bytes().to_vec(), + } + } +} + +impl CoseSerializable for SigningKey { + /// Serializes the signing key to a COSE-formatted byte array. + fn to_cose(&self) -> Vec { + match &self.inner { + RawSigningKey::Ed25519(key) => { + coset::CoseKeyBuilder::new_okp_key() + .key_id((&self.id).into()) + .algorithm(Algorithm::EdDSA) + .param( + OkpKeyParameter::D.to_i64(), // Signing key + Value::Bytes(key.to_bytes().into()), + ) + .param( + OkpKeyParameter::Crv.to_i64(), // Elliptic curve identifier + Value::Integer(Integer::from(EllipticCurve::Ed25519.to_i64())), + ) + .add_key_op(KeyOperation::Sign) + .add_key_op(KeyOperation::Verify) + .build() + .to_vec() + .expect("Signing key is always serializable") + } + } + } + + /// Deserializes a COSE-formatted byte array into a signing key. + fn from_cose(bytes: &[u8]) -> Result { + let cose_key = + CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; + + match (&cose_key.alg, &cose_key.kty) { + ( + Some(RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA)), + RegisteredLabel::Assigned(KeyType::OKP), + ) => Ok(SigningKey { + id: key_id(&cose_key)?, + inner: RawSigningKey::Ed25519(Box::pin(ed25519_signing_key(&cose_key)?)), + }), + _ => Err(EncodingError::UnsupportedValue( + "COSE key type or algorithm", + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cose_roundtrip_encode_signing() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let cose = signing_key.to_cose(); + let parsed_key = SigningKey::from_cose(&cose).unwrap(); + + assert_eq!(signing_key.to_cose(), parsed_key.to_cose()); + } + + #[test] + fn test_sign_rountrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + let signature = signing_key.sign_raw("Test message".as_bytes()); + let verifying_key = signing_key.to_verifying_key(); + assert!(verifying_key + .verify_raw(&signature, "Test message".as_bytes()) + .is_ok()); + } +} diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs new file mode 100644 index 000000000..372fa0cd7 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -0,0 +1,157 @@ +//! A verifying key is the public part of a signature key pair. It is used to verify signatures. +//! +//! This implements the lowest layer of the signature module, verifying signatures on raw byte +//! arrays. + +use ciborium::{value::Integer, Value}; +use coset::{ + iana::{Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, + CborSerializable, RegisteredLabel, RegisteredLabelWithPrivate, +}; + +use super::{ed25519_verifying_key, key_id, SignatureAlgorithm}; +use crate::{ + cose::CoseSerializable, + error::{EncodingError, SignatureError}, + keys::KeyId, + CryptoError, +}; + +/// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature +/// scheme. +pub(super) enum RawVerifyingKey { + Ed25519(ed25519_dalek::VerifyingKey), +} + +/// 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`. +pub struct VerifyingKey { + pub(super) id: KeyId, + pub(super) inner: RawVerifyingKey, +} + +impl VerifyingKey { + /// Returns the signature scheme used by the verifying key. + pub fn algorithm(&self) -> SignatureAlgorithm { + match &self.inner { + RawVerifyingKey::Ed25519(_) => SignatureAlgorithm::Ed25519, + } + } + + /// 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(super) 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()) + } + } + } +} + +impl CoseSerializable for VerifyingKey { + fn to_cose(&self) -> Vec { + match &self.inner { + RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() + .key_id((&self.id).into()) + .algorithm(Algorithm::EdDSA) + .param( + OkpKeyParameter::Crv.to_i64(), // Elliptic curve identifier + Value::Integer(Integer::from(EllipticCurve::Ed25519.to_i64())), + ) + // Note: X does not refer to the X coordinate of the public key curve point, but + // to the verifying key (signature public key), as represented by the curve spec. In + // the case of Ed25519, this is the compressed Y coordinate. This + // was ill-defined in earlier drafts of the standard. https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + .param( + OkpKeyParameter::X.to_i64(), // Verifying key (digital signature public key) + Value::Bytes(key.to_bytes().to_vec()), + ) + .add_key_op(KeyOperation::Verify) + .build() + .to_vec() + .expect("Verifying key is always serializable"), + } + } + + fn from_cose(bytes: &[u8]) -> Result + where + Self: Sized, + { + let cose_key = + coset::CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; + + let algorithm = cose_key + .alg + .as_ref() + .ok_or(EncodingError::MissingValue("COSE key algorithm"))?; + match (&cose_key.kty, algorithm) { + ( + RegisteredLabel::Assigned(KeyType::OKP), + RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA), + ) => Ok(VerifyingKey { + id: key_id(&cose_key)?, + inner: RawVerifyingKey::Ed25519(ed25519_verifying_key(&cose_key)?), + }), + _ => Err(EncodingError::UnsupportedValue( + "COSE key type or algorithm", + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 55, 131, 40, 191, 230, 137, 76, 182, 184, 139, 94, 152, 45, 63, 13, 71, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 93, 213, 35, 177, 81, 219, 226, 241, 147, 140, 238, + 32, 34, 183, 213, 107, 227, 92, 75, 84, 208, 47, 198, 80, 18, 188, 172, 145, 184, 154, 26, + 170, + ]; + const SIGNED_DATA_RAW: &[u8] = &[ + 247, 239, 74, 181, 75, 54, 137, 225, 2, 158, 14, 0, 61, 210, 254, 208, 255, 16, 8, 81, 173, + 33, 59, 67, 204, 31, 45, 38, 147, 118, 228, 84, 235, 252, 104, 38, 194, 173, 62, 52, 9, + 184, 1, 22, 113, 134, 154, 108, 24, 83, 78, 2, 23, 235, 80, 22, 57, 110, 100, 24, 151, 33, + 186, 12, + ]; + + #[test] + fn test_cose_roundtrip_encode_verifying() { + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let cose = verifying_key.to_cose(); + let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); + + assert_eq!(verifying_key.to_cose(), parsed_key.to_cose()); + } + + #[test] + fn test_testvector() { + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + assert_eq!(verifying_key.algorithm(), SignatureAlgorithm::Ed25519); + + verifying_key + .verify_raw(SIGNED_DATA_RAW, b"Test message") + .unwrap(); + } + + #[test] + fn test_invalid_testvector() { + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + assert_eq!(verifying_key.algorithm(), SignatureAlgorithm::Ed25519); + + // This should fail, as the signed object is not valid for the given verifying key. + assert!(verifying_key + .verify_raw(SIGNED_DATA_RAW, b"Invalid message") + .is_err()); + } +} diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index b09eadf7a..7647f2aef 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -3,13 +3,15 @@ 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, + derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, + AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature, + SignatureAlgorithm, SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, + SymmetricCryptoKey, UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] @@ -39,7 +41,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 +72,7 @@ pub struct KeyStoreContext<'a, Ids: KeyIds> { pub(super) local_symmetric_keys: Box>, pub(super) local_asymmetric_keys: Box>, + pub(super) local_signing_keys: Box>, // Make sure the context is !Send & !Sync pub(super) _phantom: std::marker::PhantomData<(Cell<()>, RwLockReadGuard<'static, ()>)>, @@ -104,6 +111,7 @@ impl 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 @@ -230,7 +238,7 @@ impl KeyStoreContext<'_, Ids> { ) -> Result { UnsignedSharedKey::encapsulate_key_unsigned( self.get_symmetric_key(shared_key)?, - self.get_asymmetric_key(encapsulation_key)?, + &self.get_asymmetric_key(encapsulation_key)?.to_public_key(), ) } @@ -244,6 +252,11 @@ impl 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 { let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); @@ -252,6 +265,15 @@ impl 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 { + 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 @@ -289,6 +311,21 @@ impl KeyStoreContext<'_, Ids> { self.get_asymmetric_key(key_id) } + /// Makes a signed public key from an asymmetric private key and signing key stored in context. + /// Signing a public key asserts ownership, and makes the claim to other users that if they want + /// to share with you, they can use this public key. + pub fn make_signed_public_key( + &self, + private_key_id: Ids::Asymmetric, + signing_key_id: Ids::Signing, + ) -> Result { + let public_key = self.get_asymmetric_key(private_key_id)?.to_public_key(); + let signing_key = self.get_signing_key(signing_key_id)?; + let signed_public_key = + SignedPublicKeyMessage::from_public_key(&public_key)?.sign(signing_key)?; + Ok(signed_public_key) + } + fn get_symmetric_key(&self, key_id: Ids::Symmetric) -> Result<&SymmetricCryptoKey> { if key_id.is_local() { self.local_symmetric_keys.get(key_id) @@ -307,6 +344,15 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } + 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")] #[allow(missing_docs)] pub fn set_symmetric_key( @@ -343,6 +389,17 @@ impl KeyStoreContext<'_, Ids> { Ok(()) } + /// Sets a signing key in the context + #[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, @@ -378,17 +435,59 @@ impl 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( + &self, + key: Ids::Signing, + message: &Message, + namespace: &crate::SigningNamespace, + ) -> Result { + self.get_signing_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( + &self, + key: Ids::Signing, + message: &Message, + namespace: &crate::SigningNamespace, + ) -> Result<(Signature, signing::SerializedMessage)> { + self.get_signing_key(key)?.sign_detached(message, namespace) + } } #[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 = KeyStore::default(); + + // Generate and insert a key + let key_a0_id = TestSigningKey::A(0); + let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519); + store + .context_mut() + .set_signing_key(key_a0_id, key_a0) + .unwrap(); + } + #[test] fn test_set_keys_for_encryption() { let store: KeyStore = KeyStore::default(); @@ -452,4 +551,55 @@ mod tests { // Assert that the decrypted data is the same assert_eq!(decrypted1.0, decrypted2.0); } + + #[test] + fn test_signing() { + let store: KeyStore = KeyStore::default(); + + // Generate and insert a key + let key_a0_id = TestSigningKey::A(0); + let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519); + 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 = + signed_object.verify_and_unwrap(&verifying_key, &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!(signature.verify( + serialized_message.as_bytes(), + &verifying_key, + &SigningNamespace::ExampleNamespace + )) + } } 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 std::fmt::Debug for KeyStore { struct KeyStoreInner { symmetric_keys: Box>, asymmetric_keys: Box>, + signing_keys: Box>, } /// Create a new key store with the best available implementation for the current platform. @@ -110,6 +115,7 @@ impl Default for KeyStore { inner: Arc::new(RwLock::new(KeyStoreInner { symmetric_keys: create_store(), asymmetric_keys: create_store(), + signing_keys: create_store(), })), } } @@ -122,6 +128,7 @@ impl KeyStore { 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 KeyStore { 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 KeyStore { 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/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index e9b13c831..7f339ca35 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -78,14 +78,14 @@ impl, Output> mod tests { use crate::{ traits::tests::*, AsymmetricCryptoKey, Decryptable, Encryptable, KeyStore, - SymmetricCryptoKey, + PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, }; fn test_store() -> KeyStore { let store = KeyStore::::default(); let symm_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); - let asymm_key = AsymmetricCryptoKey::generate(&mut rand::thread_rng()); + let asymm_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); #[allow(deprecated)] store diff --git a/crates/bitwarden-crypto/src/traits/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs index 21e1f9592..b9f8082c4 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. @@ -33,6 +33,8 @@ pub trait KeyIds { type Symmetric: KeyId; #[allow(missing_docs)] type Asymmetric: KeyId; + /// Signing keys are used to create detached signatures and to sign objects. + type Signing: KeyId; } /// Just a small derive_like macro that can be used to generate the key identifier enums. @@ -52,7 +54,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 { @@ -66,7 +74,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)] @@ -93,11 +101,13 @@ 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 ) => { $variant }; @@ -109,7 +119,7 @@ macro_rules! key_ids { #[cfg(test)] pub(crate) mod tests { use crate::{ - traits::tests::{TestAsymmKey, TestSymmKey}, + traits::tests::{TestAsymmKey, TestSigningKey, TestSymmKey}, KeyId, }; @@ -122,5 +132,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 528a973f8..96782d195 100644 --- a/crates/bitwarden-crypto/src/traits/mod.rs +++ b/crates/bitwarden-crypto/src/traits/mod.rs @@ -37,6 +37,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-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 99b77d400..8d66a0429 100644 --- a/crates/bitwarden-crypto/src/uniffi_support.rs +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -1,6 +1,6 @@ use std::{num::NonZeroU32, str::FromStr}; -use crate::{CryptoError, EncString, UnsignedSharedKey}; +use crate::{CryptoError, EncString, SignedPublicKey, UnsignedSharedKey}; uniffi::custom_type!(NonZeroU32, u32, { remote, @@ -23,3 +23,12 @@ uniffi::custom_type!(UnsignedSharedKey, String, { }, lower: |obj| obj.to_string(), }); + +uniffi::custom_type!(SignedPublicKey, String, { + try_lift: |val| { + val.parse().map_err(|e| { + CryptoError::EncodingError(e).into() + }) + }, + lower: |obj| obj.into(), +}); diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs index 48a899a8e..3b6673acf 100644 --- a/crates/bitwarden-crypto/src/util.rs +++ b/crates/bitwarden-crypto/src/util.rs @@ -1,4 +1,4 @@ -use std::pin::Pin; +use std::{pin::Pin, str::FromStr}; use ::aes::cipher::{ArrayLength, Unsigned}; use generic_array::GenericArray; @@ -53,6 +53,30 @@ pub fn pbkdf2(password: &[u8], salt: &[u8], rounds: u32) -> [u8; PBKDF_SHA256_HM .expect("hash is a valid fixed size") } +pub(crate) struct FromStrVisitor(std::marker::PhantomData); +impl FromStrVisitor { + pub(crate) fn new() -> Self { + Self(Default::default()) + } +} +impl serde::de::Visitor<'_> for FromStrVisitor +where + T::Err: std::fmt::Debug, +{ + type Value = T; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "a valid string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + T::from_str(v).map_err(|e| E::custom(format!("{:?}", e))) + } +} + #[cfg(test)] mod tests { use typenum::U64; diff --git a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt index 10526a213..dd759516b 100644 --- a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt +++ b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt @@ -254,6 +254,7 @@ class MainActivity : FragmentActivity() { kdfParams = kdf, email = EMAIL, privateKey = loginBody.PrivateKey, + signingKey = null, method = InitUserCryptoMethod.Password( password = PASSWORD, userKey = loginBody.Key ) @@ -339,6 +340,7 @@ class MainActivity : FragmentActivity() { kdfParams = kdf, email = EMAIL, privateKey = privateKey!!, + signingKey = null, method = InitUserCryptoMethod.DecryptedKey(decryptedUserKey = key) ) ) @@ -377,6 +379,7 @@ class MainActivity : FragmentActivity() { kdfParams = kdf, email = EMAIL, privateKey = privateKey!!, + signingKey = null, method = InitUserCryptoMethod.Pin( pinProtectedUserKey = pinProtectedUserKey, pin = PIN ) diff --git a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift index 37d302928..6651785d7 100644 --- a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift +++ b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift @@ -193,6 +193,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: loginData.PrivateKey, + signingKey: nil, method: InitUserCryptoMethod.password( password: PASSWORD, userKey: loginData.Key @@ -251,6 +252,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: privateKey, + signingKey: nil, method: InitUserCryptoMethod.decryptedKey( decryptedUserKey: key ) @@ -278,6 +280,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: privateKey, + signingKey: nil, method: InitUserCryptoMethod.pin(pin: PIN, pinProtectedUserKey: pinProtectedUserKey) )) } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 0f0f5c7a5..a18845414 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -16,6 +16,7 @@ keywords.workspace = true crate-type = ["cdylib"] [dependencies] +base64 = ">=0.22.1, <0.23.0" bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } bitwarden-error = { workspace = true } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 85a34b49b..371fa04d8 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -2,9 +2,10 @@ 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, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseSerializable, CryptoError, Decryptable, + EncString, Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, + SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, + VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -265,6 +266,42 @@ impl PureCrypto { )?)? .to_encoded()) } + + /// Given a wrapped signing key and the symmetric key it is wrapped with, this returns + /// the corresponding verifying key. + pub fn verifying_key_for_signing_key( + signing_key: String, + wrapping_key: Vec, + ) -> Result, 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(); + Ok(verifying_key.to_cose()) + } + + /// Returns the algorithm used for the given verifying key. + pub fn key_algorithm_for_verifying_key( + verifying_key: Vec, + ) -> Result { + 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_and_unwrap_signed_public_key( + signed_public_key: Vec, + verifying_key: Vec, + ) -> Result, CryptoError> { + let signed_public_key = SignedPublicKey::try_from(signed_public_key)?; + let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?; + signed_public_key + .verify_and_unwrap(&verifying_key) + .map(|public_key| public_key.to_der())? + } } #[cfg(test)] @@ -321,6 +358,65 @@ PFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnf DnqOsltgPomWZ7xVfMkm9niL2OA= -----END PRIVATE KEY-----"; + const SIGNING_KEY_WRAPPING_KEY: &[u8] = &[ + 40, 215, 110, 199, 183, 4, 182, 78, 213, 123, 251, 113, 72, 223, 57, 2, 3, 81, 136, 19, 88, + 78, 206, 176, 158, 251, 211, 84, 1, 199, 203, 142, 176, 227, 187, 136, 209, 79, 23, 13, 44, + 224, 90, 10, 191, 72, 22, 227, 171, 105, 107, 139, 24, 49, 9, 150, 103, 139, 151, 204, 165, + 121, 165, 71, + ]; + const SIGNING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 123, 226, 102, 228, 194, 232, 71, 30, 183, 42, 219, 193, 50, 30, 21, 43, + 3, 39, 4, 130, 1, 2, 35, 88, 32, 148, 2, 66, 69, 169, 57, 129, 240, 37, 18, 225, 211, 207, + 133, 66, 143, 204, 238, 113, 152, 43, 112, 133, 173, 179, 17, 202, 135, 175, 237, 1, 59, + 32, 6, + ]; + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 123, 226, 102, 228, 194, 232, 71, 30, 183, 42, 219, 193, 50, 30, 21, 43, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 63, 70, 49, 37, 246, 232, 146, 144, 83, 224, 0, 17, + 111, 248, 16, 242, 69, 195, 84, 46, 39, 218, 55, 63, 90, 112, 148, 91, 224, 186, 122, 4, + ]; + const PUBLIC_KEY: &[u8] = &[ + 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, + 48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148, + 115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250, + 229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134, + 243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92, + 88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100, + 76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88, + 195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130, + 14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39, + 80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213, + 211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16, + 12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144, + 255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190, + 248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108, + 201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1, + ]; + + const SIGNED_PUBLIC_KEY: &[u8] = &[ + 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 123, 226, 102, 228, 194, 232, 71, 30, 183, 42, + 219, 193, 50, 30, 21, 43, 58, 0, 1, 56, 127, 1, 160, 89, 1, 78, 163, 105, 97, 108, 103, + 111, 114, 105, 116, 104, 109, 0, 109, 99, 111, 110, 116, 101, 110, 116, 70, 111, 114, 109, + 97, 116, 0, 105, 112, 117, 98, 108, 105, 99, 75, 101, 121, 89, 1, 38, 48, 130, 1, 34, 48, + 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, + 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148, 115, 86, 140, 129, + 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250, 229, 242, 156, 67, + 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134, 243, 44, 243, 79, + 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92, 88, 22, 115, 135, + 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100, 76, 75, 118, 202, + 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88, 195, 88, 150, 230, + 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130, 14, 72, 123, 140, + 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39, 80, 143, 100, 194, + 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213, 211, 84, 255, 14, 205, + 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16, 12, 122, 72, 227, 95, + 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144, 255, 162, 248, 50, 28, + 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190, 248, 60, 114, 218, 32, + 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108, 201, 41, 160, 81, 133, + 171, 205, 221, 2, 3, 1, 0, 1, 88, 64, 207, 18, 4, 242, 149, 31, 37, 255, 243, 62, 78, 46, + 12, 150, 134, 159, 69, 89, 62, 222, 132, 12, 177, 74, 155, 80, 154, 37, 77, 176, 19, 142, + 73, 4, 134, 242, 24, 56, 54, 38, 178, 59, 11, 118, 230, 159, 87, 91, 20, 237, 188, 186, + 216, 86, 189, 50, 46, 173, 117, 36, 54, 105, 216, 9, + ]; #[test] fn test_symmetric_decrypt() { let enc_string = EncString::from_str(ENCRYPTED).unwrap(); @@ -430,7 +526,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_wrap_encapsulation_key() { let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); - let encapsulation_key = decapsulation_key.to_public_der().unwrap(); + let encapsulation_key = decapsulation_key.to_public_key().to_der().unwrap(); let wrapping_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let wrapped_key = PureCrypto::wrap_encapsulation_key(encapsulation_key.clone(), wrapping_key.clone()) @@ -458,7 +554,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= fn test_encapsulate_key_unsigned() { let shared_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); - let encapsulation_key = decapsulation_key.to_public_der().unwrap(); + let encapsulation_key = decapsulation_key.to_public_key().to_der().unwrap(); let encapsulated_key = PureCrypto::encapsulate_key_unsigned(shared_key.clone(), encapsulation_key.clone()) .unwrap(); @@ -469,4 +565,40 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= .unwrap(); assert_eq!(shared_key, unwrapped_key); } + + #[test] + fn test_key_algorithm_for_verifying_key() { + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let algorithm = + PureCrypto::key_algorithm_for_verifying_key(verifying_key.to_cose()).unwrap(); + assert_eq!(algorithm, SignatureAlgorithm::Ed25519); + } + + #[test] + fn test_verifying_key_for_signing_key() { + let wrapped_signing_key = PureCrypto::symmetric_encrypt_bytes( + SIGNING_KEY.to_vec(), + SIGNING_KEY_WRAPPING_KEY.to_vec(), + ) + .unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let verifying_key_derived = PureCrypto::verifying_key_for_signing_key( + wrapped_signing_key.to_string(), + SIGNING_KEY_WRAPPING_KEY.to_vec(), + ) + .unwrap(); + let verifying_key_derived = + VerifyingKey::from_cose(verifying_key_derived.as_slice()).unwrap(); + assert_eq!(verifying_key.to_cose(), verifying_key_derived.to_cose()); + } + + #[test] + fn test_verify_and_unwrap_signed_public_key() { + let public_key = PureCrypto::verify_and_unwrap_signed_public_key( + SIGNED_PUBLIC_KEY.to_vec(), + VERIFYING_KEY.to_vec(), + ) + .unwrap(); + assert_eq!(public_key, PUBLIC_KEY); + } }