From c9b80df60bba3158249c58b3f968131e25280fb9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 6 May 2025 19:27:49 +0200 Subject: [PATCH 001/112] Implement signing keys and signing --- Cargo.lock | 5 + crates/bitwarden-crypto/Cargo.toml | 1 + crates/bitwarden-crypto/src/cose.rs | 5 + crates/bitwarden-crypto/src/error.rs | 6 + crates/bitwarden-crypto/src/keys/key_id.rs | 7 + crates/bitwarden-crypto/src/keys/mod.rs | 1 + .../src/keys/signing_crypto_key.rs | 421 ++++++++++++++++++ crates/bitwarden-crypto/src/lib.rs | 1 + crates/bitwarden-crypto/src/signing/mod.rs | 27 ++ 9 files changed, 474 insertions(+) create mode 100644 crates/bitwarden-crypto/src/keys/signing_crypto_key.rs create mode 100644 crates/bitwarden-crypto/src/signing/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 21e2fd573..dfd61c8fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,7 @@ dependencies = [ "ciborium", "coset", "criterion", + "ed25519-dalek", "generic-array", "hkdf", "hmac", @@ -1261,6 +1262,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -1459,8 +1461,11 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", + "serde", "sha2", "subtle", + "zeroize", ] [[package]] diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 7cae1f34e..cc90f98c8 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", 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" diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index c98a9f89d..a8769be92 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -16,6 +16,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], diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index d2604b123..9536de1c3 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -56,6 +56,12 @@ pub enum CryptoError { #[error("Invalid nonce length")] InvalidNonceLength, + + #[error("Invalid signature")] + InvalidSignature, + + #[error("Invalid namespace")] + InvalidNamespace, } #[derive(Debug, Error)] diff --git a/crates/bitwarden-crypto/src/keys/key_id.rs b/crates/bitwarden-crypto/src/keys/key_id.rs index ef7459ace..525a74183 100644 --- a/crates/bitwarden-crypto/src/keys/key_id.rs +++ b/crates/bitwarden-crypto/src/keys/key_id.rs @@ -9,6 +9,7 @@ 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. +#[derive(Clone)] pub(crate) struct KeyId(uuid::Uuid); /// Fixed length identifiers for keys. @@ -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..823421e96 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -14,6 +14,7 @@ mod asymmetric_crypto_key; pub use asymmetric_crypto_key::{ AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey, }; +mod signing_crypto_key; mod user_key; pub use user_key::UserKey; mod device_key; diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs new file mode 100644 index 000000000..a5a3104d3 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -0,0 +1,421 @@ +//! This file implements creation and verification of detached signatures + +use ciborium::{value::Integer, Value}; +use coset::{ + iana::{self, Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, + CborSerializable, CoseKey, CoseSign1, Label, RegisteredLabel, RegisteredLabelWithPrivate, +}; +use ed25519_dalek::Signer; +use rand::rngs::OsRng; + +use super::key_id::KeyId; +use crate::{cose::SIGNING_NAMESPACE, error::Result, signing::SigningNamespace, CryptoError}; + +#[allow(unused)] +enum SigningCryptoKeyEnum { + Ed25519(ed25519_dalek::SigningKey), +} + +#[allow(unused)] +enum VerifyingKeyEnum { + Ed25519(ed25519_dalek::VerifyingKey), +} + +#[allow(unused)] +struct SigningKey { + id: KeyId, + inner: SigningCryptoKeyEnum, +} + +#[allow(unused)] +struct VerifyingKey { + id: KeyId, + inner: VerifyingKeyEnum, +} + +#[allow(unused)] +impl SigningKey { + fn make_ed25519() -> Result { + Ok(SigningKey { + id: KeyId::make(), + inner: SigningCryptoKeyEnum::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), + }) + } + + fn cose_algorithm(&self) -> Algorithm { + match &self.inner { + SigningCryptoKeyEnum::Ed25519(_) => Algorithm::EdDSA, + } + } + + fn to_cose(&self) -> Result> { + match &self.inner { + SigningCryptoKeyEnum::Ed25519(key) => { + coset::CoseKeyBuilder::new_okp_key() + .key_id((&self.id).into()) + .algorithm(Algorithm::EdDSA) + // Note: X does not refer to the X coordinate of the public key curve point, but + // to the verifying 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 + // + // Note: By the standard, the public key is optional (but RECOMMENDED) here, and + // can be derived on the fly. + .param( + OkpKeyParameter::X.to_i64(), + Value::Bytes(key.verifying_key().to_bytes().into()), + ) + .param( + OkpKeyParameter::D.to_i64(), + Value::Bytes(key.to_bytes().into()), + ) + .param( + OkpKeyParameter::Crv.to_i64(), + Value::Integer(Integer::from(EllipticCurve::Ed25519.to_i64())), + ) + .add_key_op(KeyOperation::Sign) + .add_key_op(KeyOperation::Verify) + .build() + .to_vec() + .map_err(|_| CryptoError::InvalidKey) + } + } + } + + fn from_cose(bytes: &[u8]) -> Result { + let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; + let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) + else { + return Err(CryptoError::InvalidKey); + }; + let key_id: [u8; 16] = key_id + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key_id: KeyId = key_id.into(); + + match (key_type, algorithm) { + (kty, alg) + if kty == RegisteredLabel::Assigned(KeyType::OKP) + && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let (mut crv, mut x, mut d) = (None, None, None); + for (key, value) in &cose_key.params { + if let Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + match key { + OkpKeyParameter::Crv => { + crv.replace(value); + } + OkpKeyParameter::X => { + x.replace(value); + } + OkpKeyParameter::D => { + d.replace(value); + } + _ => (), + } + } + } + + let (Some(_x), Some(d), Some(crv)) = (x, d, crv) else { + return Err(CryptoError::InvalidKey); + }; + let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); + if crv == EllipticCurve::Ed25519.to_i64().into() { + let secret_key_bytes: &[u8; 32] = d + .as_bytes() + .ok_or(CryptoError::InvalidKey)? + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes); + Ok(SigningKey { + id: key_id, + inner: SigningCryptoKeyEnum::Ed25519(key), + }) + } else { + Err(CryptoError::InvalidKey) + } + } + _ => Err(CryptoError::InvalidKey), + } + } + + pub(crate) fn sign(&self, namespace: &SigningNamespace, data: &[u8]) -> Signature { + Signature::from( + coset::CoseSign1Builder::new() + .protected( + coset::HeaderBuilder::new() + .algorithm(self.cose_algorithm()) + .key_id((&self.id).into()) + .value( + SIGNING_NAMESPACE, + ciborium::Value::Integer(Integer::from(namespace.as_i64())), + ) + .build(), + ) + .create_detached_signature(data, &[], |pt| self.sign_raw(pt)) + .build(), + ) + } + + /// Signs the given byte array with the signing key. + /// This should never be used directly, but only through the `sign` method, to enforce + /// strong domain separation of the signatures. + fn sign_raw(&self, data: &[u8]) -> Vec { + match &self.inner { + SigningCryptoKeyEnum::Ed25519(key) => key.sign(data).to_bytes().to_vec(), + } + } + + fn to_verifying_key(&self) -> VerifyingKey { + match &self.inner { + SigningCryptoKeyEnum::Ed25519(key) => VerifyingKey { + id: self.id.clone(), + inner: VerifyingKeyEnum::Ed25519(key.verifying_key()), + }, + } + } +} + +#[allow(unused)] +impl VerifyingKey { + fn to_cose(&self) -> Result> { + match &self.inner { + VerifyingKeyEnum::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() + .key_id((&self.id).into()) + .algorithm(Algorithm::EdDSA) + .param( + OkpKeyParameter::Crv.to_i64(), + Value::Integer(Integer::from(EllipticCurve::Ed25519.to_i64())), + ) + .param( + OkpKeyParameter::X.to_i64(), + Value::Bytes(key.to_bytes().to_vec()), + ) + .add_key_op(KeyOperation::Verify) + .build() + .to_vec() + .map_err(|_| CryptoError::InvalidKey), + } + } + + fn from_cose(bytes: &[u8]) -> Result { + let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; + + let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) + else { + return Err(CryptoError::InvalidKey); + }; + let key_id: [u8; 16] = key_id + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key_id: KeyId = key_id.into(); + + match (key_type, algorithm) { + (kty, alg) + if kty == RegisteredLabel::Assigned(KeyType::OKP) + && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + { + let (mut crv, mut x) = (None, None); + for (key, value) in &cose_key.params { + if let coset::Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + match key { + OkpKeyParameter::Crv => { + crv.replace(value); + } + OkpKeyParameter::X => { + x.replace(value); + } + _ => (), + } + } + } + let (Some(x), Some(crv)) = (x, crv) else { + return Err(CryptoError::InvalidKey); + }; + + let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); + if crv == iana::EllipticCurve::Ed25519.to_i64().into() { + let verifying_key_bytes: &[u8; 32] = x + .as_bytes() + .ok_or(CryptoError::InvalidKey)? + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) + .map_err(|_| CryptoError::InvalidKey)?; + Ok(VerifyingKey { + id: key_id, + inner: VerifyingKeyEnum::Ed25519(verifying_key), + }) + } else { + Err(CryptoError::InvalidKey) + } + } + _ => Err(CryptoError::InvalidKey), + } + } + + /// Verifies the signature of the given data, for the given namespace. + /// This should never be used directly, but only through the `verify` method, to enforce + /// strong domain separation of the signatures. + pub(crate) fn verify( + &self, + namespace: &SigningNamespace, + signature: &Signature, + data: &[u8], + ) -> bool { + let Some(_alg) = &signature.inner().protected.header.alg else { + return false; + }; + + let mut signature_namespace = signature.namespace(); + let Ok(signature_namespace) = signature.namespace() else { + return false; + }; + if signature_namespace != *namespace { + return false; + } + + signature + .inner() + .verify_detached_signature(data, &[], |sig, data| self.verify_raw(sig, data)) + .is_ok() + } + + /// Verifies the signature of the given data, for the given namespace. + /// This should never be used directly, but only through the `verify` method, to enforce + /// strong domain separation of the signatures. + fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<()> { + match &self.inner { + VerifyingKeyEnum::Ed25519(key) => { + let sig = ed25519_dalek::Signature::from_bytes( + signature + .try_into() + .map_err(|_| crate::error::CryptoError::InvalidSignature)?, + ); + key.verify_strict(data, &sig) + .map_err(|_| crate::error::CryptoError::InvalidSignature) + } + } + } +} + +/// A signature cryptographically attests to a (namespace, data) pair. The namespace is included in +/// the signature object, the data is not. One data object can be signed multiple times, with +/// different namespaces / by different signers, depending on the application needs. +#[allow(unused)] +struct Signature(CoseSign1); + +impl From for Signature { + fn from(cose_sign1: CoseSign1) -> Self { + Signature(cose_sign1) + } +} + +#[allow(unused)] +impl Signature { + fn from_bytes(bytes: &[u8]) -> Result { + let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| CryptoError::InvalidSignature)?; + Ok(Signature(cose_sign1)) + } + + fn to_bytes(&self) -> Result> { + self.0 + .clone() + .to_vec() + .map_err(|_| CryptoError::InvalidSignature) + } + + fn inner(&self) -> &CoseSign1 { + &self.0 + } + + fn namespace(&self) -> Result { + let mut namespace = None; + for (key, value) in &self.0.protected.header.rest { + if let Label::Int(key) = key { + if *key == SIGNING_NAMESPACE { + namespace.replace(value); + } + } + } + let Some(namespace) = namespace else { + return Err(CryptoError::InvalidNamespace); + }; + let Some(namespace) = namespace.as_integer() else { + return Err(CryptoError::InvalidNamespace); + }; + let namespace: i128 = namespace.into(); + SigningNamespace::try_from_i64(namespace as i64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sign_roundtrip() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Hello, world!"; + let namespace = SigningNamespace::EncryptionMetadata; + + let signature = signing_key.sign(&namespace, data); + assert!(verifying_key.verify(&namespace, &signature, data)); + } + + #[test] + fn test_changed_signature_fails() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Hello, world!"; + let namespace = SigningNamespace::EncryptionMetadata; + + let signature = signing_key.sign(&namespace, data); + assert!(!verifying_key.verify(&namespace, &signature, b"Goodbye, world!")); + } + + #[test] + fn test_changed_namespace_fails() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Hello, world!"; + let namespace = SigningNamespace::EncryptionMetadata; + let other_namespace = SigningNamespace::Test; + + let signature = signing_key.sign(&namespace, data); + assert!(!verifying_key.verify(&other_namespace, &signature, data)); + } + + #[test] + fn test_cose_roundtrip_encode_signing() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let cose = signing_key.to_cose().unwrap(); + let parsed_key = SigningKey::from_cose(&cose).unwrap(); + + assert_eq!( + signing_key.to_cose().unwrap(), + parsed_key.to_cose().unwrap() + ); + } + + #[test] + fn test_cose_roundtrip_encode_verifying() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let cose = signing_key.to_verifying_key().to_cose().unwrap(); + let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); + + assert_eq!( + signing_key.to_verifying_key().to_cose().unwrap(), + parsed_key.to_cose().unwrap() + ); + } +} diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index d3a0e304d..70ee3aff3 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -31,6 +31,7 @@ pub use wordlist::EFF_LONG_WORD_LIST; mod store; pub use store::{KeyStore, KeyStoreContext}; mod cose; +mod signing; mod traits; mod xchacha20; pub use traits::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds}; diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs new file mode 100644 index 000000000..6f9f21661 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -0,0 +1,27 @@ +use crate::CryptoError; + +/// Signing is domain-separated within bitwarden, to prevent cross protocol attacks. +/// +/// A new signed entity or protocol shall use a new signing namespace. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SigningNamespace { + #[allow(dead_code)] + EncryptionMetadata = 1, + #[cfg(test)] + Test = -1, +} + +impl SigningNamespace { + pub fn as_i64(&self) -> i64 { + *self as i64 + } + + pub fn try_from_i64(value: i64) -> Result { + match value { + 1 => Ok(Self::EncryptionMetadata), + #[cfg(test)] + -1 => Ok(Self::Test), + _ => Err(CryptoError::InvalidNamespace), + } + } +} From 2ad9f598b090c0bf54457a3a5bd8e8752b373360 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 6 May 2025 19:31:04 +0200 Subject: [PATCH 002/112] Remove unused code --- crates/bitwarden-crypto/src/keys/signing_crypto_key.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index a5a3104d3..67e1dff85 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -275,7 +275,6 @@ impl VerifyingKey { return false; }; - let mut signature_namespace = signature.namespace(); let Ok(signature_namespace) = signature.namespace() else { return false; }; From b0c3dae5c267afb18fc6656d53da8daef482dca6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 6 May 2025 19:42:41 +0200 Subject: [PATCH 003/112] Add test vector --- .../src/keys/signing_crypto_key.rs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 67e1dff85..424a15f31 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -320,12 +320,12 @@ impl From for Signature { #[allow(unused)] impl Signature { - fn from_bytes(bytes: &[u8]) -> Result { + fn from_cose(bytes: &[u8]) -> Result { let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| CryptoError::InvalidSignature)?; Ok(Signature(cose_sign1)) } - fn to_bytes(&self) -> Result> { + fn to_cose(&self) -> Result> { self.0 .clone() .to_vec() @@ -360,11 +360,32 @@ impl Signature { mod tests { use super::*; + const SIGNING_KEY: &[u8] = &[167, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 3, 39, 4, 130, 1, 2, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168, 35, 88, 32, 59, 136, 203, 0, 108, 23, 82, 84, 206, 163, 86, 62, 187, 196, 156, 156, 150, 80, 101, 129, 247, 112, 117, 10, 34, 54, 254, 181, 239, 214, 195, 78, 32, 6]; + const VERIFYING_KEY: &[u8] = &[166, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168]; + /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data + const SIGNATURE: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 143, 218, 162, 76, 208, 117, 94, 215, 224, 98, 89, 193, 194, 226, 144, 214, 91, 130, 129, 130, 77, 36, 79, 196, 45, 105, 120, 151, 136, 57, 230, 27, 37, 142, 55, 191, 23, 200, 237, 215, 252, 42, 182, 140, 201, 173, 199, 214, 97, 105, 107, 101, 140, 182, 105, 9, 206, 106, 210, 29, 203, 174, 178, 12]; + + #[test] + fn test_using_test_vectors() { + let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let signature = Signature::from_cose(SIGNATURE).unwrap(); + + let data = b"Test message"; + let namespace = SigningNamespace::EncryptionMetadata; + + assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); + assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); + assert_eq!(signature.to_cose().unwrap(), SIGNATURE); + + assert!(verifying_key.verify(&namespace, &signature, data)); + } + #[test] fn test_sign_roundtrip() { let signing_key = SigningKey::make_ed25519().unwrap(); let verifying_key = signing_key.to_verifying_key(); - let data = b"Hello, world!"; + let data = b"Test message"; let namespace = SigningNamespace::EncryptionMetadata; let signature = signing_key.sign(&namespace, data); @@ -375,7 +396,7 @@ mod tests { fn test_changed_signature_fails() { let signing_key = SigningKey::make_ed25519().unwrap(); let verifying_key = signing_key.to_verifying_key(); - let data = b"Hello, world!"; + let data = b"Test message"; let namespace = SigningNamespace::EncryptionMetadata; let signature = signing_key.sign(&namespace, data); @@ -386,7 +407,7 @@ mod tests { fn test_changed_namespace_fails() { let signing_key = SigningKey::make_ed25519().unwrap(); let verifying_key = signing_key.to_verifying_key(); - let data = b"Hello, world!"; + let data = b"Test message"; let namespace = SigningNamespace::EncryptionMetadata; let other_namespace = SigningNamespace::Test; From 2e8bd92e5213c57a5dbbaa0e1efe1a5ed975dc98 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 6 May 2025 19:44:34 +0200 Subject: [PATCH 004/112] Cargo fmt --- .../src/keys/signing_crypto_key.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 424a15f31..865b3be52 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -360,10 +360,27 @@ impl Signature { mod tests { use super::*; - const SIGNING_KEY: &[u8] = &[167, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 3, 39, 4, 130, 1, 2, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168, 35, 88, 32, 59, 136, 203, 0, 108, 23, 82, 84, 206, 163, 86, 62, 187, 196, 156, 156, 150, 80, 101, 129, 247, 112, 117, 10, 34, 54, 254, 181, 239, 214, 195, 78, 32, 6]; - const VERIFYING_KEY: &[u8] = &[166, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168]; + const SIGNING_KEY: &[u8] = &[ + 167, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, + 3, 39, 4, 130, 1, 2, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, + 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168, + 35, 88, 32, 59, 136, 203, 0, 108, 23, 82, 84, 206, 163, 86, 62, 187, 196, 156, 156, 150, + 80, 101, 129, 247, 112, 117, 10, 34, 54, 254, 181, 239, 214, 195, 78, 32, 6, + ]; + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, + 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, + 168, + ]; /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data - const SIGNATURE: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 143, 218, 162, 76, 208, 117, 94, 215, 224, 98, 89, 193, 194, 226, 144, 214, 91, 130, 129, 130, 77, 36, 79, 196, 45, 105, 120, 151, 136, 57, 230, 27, 37, 142, 55, 191, 23, 200, 237, 215, 252, 42, 182, 140, 201, 173, 199, 214, 97, 105, 107, 101, 140, 182, 105, 9, 206, 106, 210, 29, 203, 174, 178, 12]; + const SIGNATURE: &[u8] = &[ + 132, 88, 27, 163, 1, 39, 4, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, + 20, 115, 137, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 143, 218, 162, 76, 208, 117, 94, 215, + 224, 98, 89, 193, 194, 226, 144, 214, 91, 130, 129, 130, 77, 36, 79, 196, 45, 105, 120, + 151, 136, 57, 230, 27, 37, 142, 55, 191, 23, 200, 237, 215, 252, 42, 182, 140, 201, 173, + 199, 214, 97, 105, 107, 101, 140, 182, 105, 9, 206, 106, 210, 29, 203, 174, 178, 12, + ]; #[test] fn test_using_test_vectors() { From f7f1e2ed775f84ed647674274378be15408df997 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 7 May 2025 13:49:27 +0200 Subject: [PATCH 005/112] Make ed25519 dependency version a range --- crates/bitwarden-crypto/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index cc90f98c8..e6a9fbdbe 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -33,7 +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", features = ["rand_core"] } +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" From 6deb284ce695a0040f935f2e999143238596ba43 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 12 May 2025 16:56:27 +0200 Subject: [PATCH 006/112] Add signed object --- .../src/keys/signing_crypto_key.rs | 180 ++++++++++++++++-- 1 file changed, 169 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 865b3be52..06c27026f 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -143,7 +143,15 @@ impl SigningKey { } } - pub(crate) fn sign(&self, namespace: &SigningNamespace, data: &[u8]) -> Signature { + /// Signs the given payload with the signing key, under a given namespace. + /// This returns a [`Signature`] object, that does not contain the payload. + /// The payload must be stored separately, and needs to be provided when verifying the + /// signature. + /// + /// This should be used when multiple signers are required, or when signatures need to be + /// replaceable without re-uploading the object, or if the signed object should be parseable + /// by the server side, without the use of COSE on the server. + pub(crate) fn sign_detached(&self, namespace: &SigningNamespace, data: &[u8]) -> Signature { Signature::from( coset::CoseSign1Builder::new() .protected( @@ -161,6 +169,31 @@ impl SigningKey { ) } + /// Signs the given payload with the signing key, under a given namespace. + /// This returns a [`SignedObject`] object, that contains the payload. + /// The payload is included in the signature, and does not need to be provided when verifying + /// the signature. + /// + /// This should be used when only one signer is required, so that only one object needs to be + /// kept track of. + pub(crate) fn sign(&self, namespace: &SigningNamespace, data: &[u8]) -> Result { + let cose_sign1 = coset::CoseSign1Builder::new() + .protected( + coset::HeaderBuilder::new() + .algorithm(self.cose_algorithm()) + .key_id((&self.id).into()) + .value( + SIGNING_NAMESPACE, + ciborium::Value::Integer(Integer::from(namespace.as_i64())), + ) + .build(), + ) + .payload(data.to_vec()) + .create_signature(&[], |pt| self.sign_raw(pt)) + .build(); + Ok(SignedObject(cose_sign1)) + } + /// Signs the given byte array with the signing key. /// This should never be used directly, but only through the `sign` method, to enforce /// strong domain separation of the signatures. @@ -265,7 +298,7 @@ impl VerifyingKey { /// Verifies the signature of the given data, for the given namespace. /// This should never be used directly, but only through the `verify` method, to enforce /// strong domain separation of the signatures. - pub(crate) fn verify( + pub(crate) fn verify_signature( &self, namespace: &SigningNamespace, signature: &Signature, @@ -288,6 +321,27 @@ impl VerifyingKey { .is_ok() } + /// Verifies the signature of a signed object, for the given namespace, and returns the payload. + pub(crate) fn get_verified_payload( + &self, + namespace: &SigningNamespace, + signature: &SignedObject, + ) -> Result> { + let Some(_alg) = &signature.inner().protected.header.alg else { + return Err(CryptoError::InvalidSignature); + }; + + let signature_namespace = signature.namespace()?; + if signature_namespace != *namespace { + return Err(CryptoError::InvalidNamespace); + } + + signature + .inner() + .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?; + signature.payload() + } + /// Verifies the signature of the given data, for the given namespace. /// This should never be used directly, but only through the `verify` method, to enforce /// strong domain separation of the signatures. @@ -356,6 +410,63 @@ impl Signature { } } +/// A signed object has a cryptographical attestation to a (namespace, data) pair. The namespace and +/// data are included in the signature object. +#[allow(unused)] +struct SignedObject(CoseSign1); + +impl From for SignedObject { + fn from(cose_sign1: CoseSign1) -> Self { + SignedObject(cose_sign1) + } +} + +#[allow(unused)] +impl SignedObject { + fn from_cose(bytes: &[u8]) -> Result { + let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| CryptoError::InvalidSignature)?; + Ok(SignedObject(cose_sign1)) + } + + fn to_cose(&self) -> Result> { + self.0 + .clone() + .to_vec() + .map_err(|_| CryptoError::InvalidSignature) + } + + fn inner(&self) -> &CoseSign1 { + &self.0 + } + + fn namespace(&self) -> Result { + let mut namespace = None; + for (key, value) in &self.0.protected.header.rest { + if let Label::Int(key) = key { + if *key == SIGNING_NAMESPACE { + namespace.replace(value); + } + } + } + let Some(namespace) = namespace else { + return Err(CryptoError::InvalidNamespace); + }; + let Some(namespace) = namespace.as_integer() else { + return Err(CryptoError::InvalidNamespace); + }; + let namespace: i128 = namespace.into(); + SigningNamespace::try_from_i64(namespace as i64) + } + + fn payload(&self) -> Result> { + self.0 + .payload + .as_ref() + .ok_or(CryptoError::InvalidSignature) + .map(|payload| payload.to_vec()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -395,29 +506,42 @@ mod tests { assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); assert_eq!(signature.to_cose().unwrap(), SIGNATURE); - assert!(verifying_key.verify(&namespace, &signature, data)); + assert!(verifying_key.verify_signature(&namespace, &signature, data)); } #[test] - fn test_sign_roundtrip() { + fn test_sign_detached_roundtrip() { let signing_key = SigningKey::make_ed25519().unwrap(); let verifying_key = signing_key.to_verifying_key(); let data = b"Test message"; let namespace = SigningNamespace::EncryptionMetadata; - let signature = signing_key.sign(&namespace, data); - assert!(verifying_key.verify(&namespace, &signature, data)); + let signature = signing_key.sign_detached(&namespace, data); + assert!(verifying_key.verify_signature(&namespace, &signature, data)); } #[test] - fn test_changed_signature_fails() { + fn test_sign_roundtrip() { let signing_key = SigningKey::make_ed25519().unwrap(); let verifying_key = signing_key.to_verifying_key(); let data = b"Test message"; let namespace = SigningNamespace::EncryptionMetadata; + let signed_object = signing_key.sign(&namespace, data).unwrap(); + let payload = verifying_key + .get_verified_payload(&namespace, &signed_object) + .unwrap(); + assert_eq!(payload, data); + } - let signature = signing_key.sign(&namespace, data); - assert!(!verifying_key.verify(&namespace, &signature, b"Goodbye, world!")); + #[test] + fn test_changed_payload_fails() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Test message"; + let namespace = SigningNamespace::EncryptionMetadata; + + let signature = signing_key.sign_detached(&namespace, data); + assert!(!verifying_key.verify_signature(&namespace, &signature, b"Test message 2")); } #[test] @@ -428,8 +552,42 @@ mod tests { let namespace = SigningNamespace::EncryptionMetadata; let other_namespace = SigningNamespace::Test; - let signature = signing_key.sign(&namespace, data); - assert!(!verifying_key.verify(&other_namespace, &signature, data)); + let signature = signing_key.sign_detached(&namespace, data); + assert!(!verifying_key.verify_signature(&other_namespace, &signature, data)); + } + + #[test] + fn test_changed_namespace_fails_signed_object() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Test message"; + let namespace = SigningNamespace::EncryptionMetadata; + let other_namespace = SigningNamespace::Test; + let signed_object = signing_key.sign(&namespace, data).unwrap(); + assert!(verifying_key + .get_verified_payload(&other_namespace, &signed_object) + .is_err()); + } + + #[test] + fn test_cose_roundtrip_signature() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let cose = + signing_key.sign_detached(&SigningNamespace::EncryptionMetadata, b"Test message"); + let cose = cose.to_cose().unwrap(); + let parsed_cose = Signature::from_cose(&cose).unwrap(); + assert_eq!(cose, parsed_cose.to_cose().unwrap()); + } + + #[test] + fn test_cose_roundtrip_signed_object() { + let signing_key = SigningKey::make_ed25519().unwrap(); + let cose = signing_key + .sign(&SigningNamespace::EncryptionMetadata, b"Test message") + .unwrap(); + let cose = cose.to_cose().unwrap(); + let parsed_cose = SignedObject::from_cose(&cose).unwrap(); + assert_eq!(cose, parsed_cose.to_cose().unwrap()); } #[test] From 835447603fab19183e98722452661c29add68faf Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 13:45:57 +0200 Subject: [PATCH 007/112] Replace magic value --- crates/bitwarden-crypto/src/keys/mod.rs | 1 - crates/bitwarden-crypto/src/keys/signing_crypto_key.rs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 823421e96..73362c3c8 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -27,6 +27,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; mod utils; diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 06c27026f..3c17c93ab 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -8,7 +8,7 @@ use coset::{ use ed25519_dalek::Signer; use rand::rngs::OsRng; -use super::key_id::KeyId; +use super::{key_id::KeyId, KEY_ID_SIZE}; use crate::{cose::SIGNING_NAMESPACE, error::Result, signing::SigningNamespace, CryptoError}; #[allow(unused)] @@ -88,7 +88,7 @@ impl SigningKey { else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; 16] = key_id + let key_id: [u8; KEY_ID_SIZE] = key_id .as_slice() .try_into() .map_err(|_| CryptoError::InvalidKey)?; @@ -242,7 +242,7 @@ impl VerifyingKey { else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; 16] = key_id + let key_id: [u8; KEY_ID_SIZE] = key_id .as_slice() .try_into() .map_err(|_| CryptoError::InvalidKey)?; From 04d532930caecc8f66dc9e7bf593f530ff9c2c93 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 13:50:59 +0200 Subject: [PATCH 008/112] Clean up crate::error reference --- crates/bitwarden-crypto/src/keys/signing_crypto_key.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 3c17c93ab..37f1153bb 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -351,10 +351,10 @@ impl VerifyingKey { let sig = ed25519_dalek::Signature::from_bytes( signature .try_into() - .map_err(|_| crate::error::CryptoError::InvalidSignature)?, + .map_err(|_| CryptoError::InvalidSignature)?, ); key.verify_strict(data, &sig) - .map_err(|_| crate::error::CryptoError::InvalidSignature) + .map_err(|_| CryptoError::InvalidSignature) } } } From efd23cc82a8c4fcb291ff5947c9bad1e8d11c57f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 15:56:23 +0200 Subject: [PATCH 009/112] Add comments to signing and verifying key --- crates/bitwarden-crypto/src/keys/signing_crypto_key.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 37f1153bb..7e9a7bfbc 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -21,12 +21,15 @@ enum VerifyingKeyEnum { Ed25519(ed25519_dalek::VerifyingKey), } +/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be derived from it. #[allow(unused)] struct SigningKey { id: KeyId, inner: SigningCryptoKeyEnum, } +/// A verifying key is a public key used for verifying signatures. It can be published to other users, +/// who can use it to verify that messages were signed by the holder of the corresponding `SigningKey`. #[allow(unused)] struct VerifyingKey { id: KeyId, From d6737cf211fed6b90c36743f835d12c0416f2425 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 16:01:09 +0200 Subject: [PATCH 010/112] Move and clarify comment, drop optional verifying key from signing key --- .../src/keys/signing_crypto_key.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 7e9a7bfbc..ecebd4d75 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -57,23 +57,12 @@ impl SigningKey { coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) .algorithm(Algorithm::EdDSA) - // Note: X does not refer to the X coordinate of the public key curve point, but - // to the verifying 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 - // - // Note: By the standard, the public key is optional (but RECOMMENDED) here, and - // can be derived on the fly. .param( - OkpKeyParameter::X.to_i64(), - Value::Bytes(key.verifying_key().to_bytes().into()), - ) - .param( - OkpKeyParameter::D.to_i64(), + OkpKeyParameter::D.to_i64(), // Signing key Value::Bytes(key.to_bytes().into()), ) .param( - OkpKeyParameter::Crv.to_i64(), + OkpKeyParameter::Crv.to_i64(), // Elliptic curve identifier Value::Integer(Integer::from(EllipticCurve::Ed25519.to_i64())), ) .add_key_op(KeyOperation::Sign) @@ -227,6 +216,10 @@ impl VerifyingKey { OkpKeyParameter::Crv.to_i64(), 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(), Value::Bytes(key.to_bytes().to_vec()), From d2b829b66557d26ed860d8de1584fc087cc0facc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 16:11:14 +0200 Subject: [PATCH 011/112] Change comment into docs --- crates/bitwarden-crypto/src/cose.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index a8769be92..b174cdf99 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -16,9 +16,9 @@ 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. +/// 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 From 9484a64911d5e31d8eb69868a3be800afada2213 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 20 May 2025 09:05:41 +0200 Subject: [PATCH 012/112] Move signature errors to separate enum --- crates/bitwarden-crypto/src/error.rs | 15 ++++--- .../src/keys/signing_crypto_key.rs | 39 +++++++++---------- crates/bitwarden-crypto/src/signing/mod.rs | 4 +- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 9536de1c3..2cb623e3b 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -57,11 +57,8 @@ pub enum CryptoError { #[error("Invalid nonce length")] InvalidNonceLength, - #[error("Invalid signature")] - InvalidSignature, - - #[error("Invalid namespace")] - InvalidNamespace, + #[error("Signature error, {0}")] + SignatureError(#[from] SignatureError), } #[derive(Debug, Error)] @@ -98,5 +95,13 @@ pub enum RsaError { Rsa(#[from] rsa::Error), } +#[derive(Debug, Error)] +pub enum SignatureError { + #[error("Invalid signature")] + InvalidSignature, + #[error("Invalid namespace")] + InvalidNamespace, +} + /// Alias for `Result`. pub(crate) type Result = std::result::Result; diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index ecebd4d75..0a488dbbe 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -9,7 +9,7 @@ use ed25519_dalek::Signer; use rand::rngs::OsRng; use super::{key_id::KeyId, KEY_ID_SIZE}; -use crate::{cose::SIGNING_NAMESPACE, error::Result, signing::SigningNamespace, CryptoError}; +use crate::{cose::SIGNING_NAMESPACE, error::{Result, SignatureError}, signing::SigningNamespace, CryptoError}; #[allow(unused)] enum SigningCryptoKeyEnum { @@ -92,7 +92,7 @@ impl SigningKey { && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => { // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair - let (mut crv, mut x, mut d) = (None, None, None); + let (mut crv, mut d) = (None, None); for (key, value) in &cose_key.params { if let Label::Int(i) = key { let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; @@ -100,9 +100,6 @@ impl SigningKey { OkpKeyParameter::Crv => { crv.replace(value); } - OkpKeyParameter::X => { - x.replace(value); - } OkpKeyParameter::D => { d.replace(value); } @@ -111,7 +108,7 @@ impl SigningKey { } } - let (Some(_x), Some(d), Some(crv)) = (x, d, crv) else { + let (Some(d), Some(crv)) = (d, crv) else { return Err(CryptoError::InvalidKey); }; let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); @@ -324,12 +321,12 @@ impl VerifyingKey { signature: &SignedObject, ) -> Result> { let Some(_alg) = &signature.inner().protected.header.alg else { - return Err(CryptoError::InvalidSignature); + return Err(SignatureError::InvalidSignature.into()); }; let signature_namespace = signature.namespace()?; if signature_namespace != *namespace { - return Err(CryptoError::InvalidNamespace); + return Err(SignatureError::InvalidNamespace.into()); } signature @@ -347,10 +344,10 @@ impl VerifyingKey { let sig = ed25519_dalek::Signature::from_bytes( signature .try_into() - .map_err(|_| CryptoError::InvalidSignature)?, + .map_err(|_| SignatureError::InvalidSignature)?, ); key.verify_strict(data, &sig) - .map_err(|_| CryptoError::InvalidSignature) + .map_err(|_| SignatureError::InvalidSignature.into()) } } } @@ -370,8 +367,8 @@ impl From for Signature { #[allow(unused)] impl Signature { - fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| CryptoError::InvalidSignature)?; + fn from_cose(bytes: &[u8]) -> Result { + let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(Signature(cose_sign1)) } @@ -379,7 +376,7 @@ impl Signature { self.0 .clone() .to_vec() - .map_err(|_| CryptoError::InvalidSignature) + .map_err(|_| SignatureError::InvalidSignature.into()) } fn inner(&self) -> &CoseSign1 { @@ -396,10 +393,10 @@ impl Signature { } } let Some(namespace) = namespace else { - return Err(CryptoError::InvalidNamespace); + return Err(SignatureError::InvalidNamespace.into()); }; let Some(namespace) = namespace.as_integer() else { - return Err(CryptoError::InvalidNamespace); + return Err(SignatureError::InvalidNamespace.into()); }; let namespace: i128 = namespace.into(); SigningNamespace::try_from_i64(namespace as i64) @@ -419,8 +416,8 @@ impl From for SignedObject { #[allow(unused)] impl SignedObject { - fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| CryptoError::InvalidSignature)?; + fn from_cose(bytes: &[u8]) -> Result { + let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(SignedObject(cose_sign1)) } @@ -428,7 +425,7 @@ impl SignedObject { self.0 .clone() .to_vec() - .map_err(|_| CryptoError::InvalidSignature) + .map_err(|_| SignatureError::InvalidSignature.into()) } fn inner(&self) -> &CoseSign1 { @@ -445,10 +442,10 @@ impl SignedObject { } } let Some(namespace) = namespace else { - return Err(CryptoError::InvalidNamespace); + return Err(SignatureError::InvalidNamespace.into()); }; let Some(namespace) = namespace.as_integer() else { - return Err(CryptoError::InvalidNamespace); + return Err(SignatureError::InvalidNamespace.into()); }; let namespace: i128 = namespace.into(); SigningNamespace::try_from_i64(namespace as i64) @@ -458,7 +455,7 @@ impl SignedObject { self.0 .payload .as_ref() - .ok_or(CryptoError::InvalidSignature) + .ok_or(SignatureError::InvalidSignature.into()) .map(|payload| payload.to_vec()) } } diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 6f9f21661..4025a6f44 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -1,4 +1,4 @@ -use crate::CryptoError; +use crate::{error::SignatureError, CryptoError}; /// Signing is domain-separated within bitwarden, to prevent cross protocol attacks. /// @@ -21,7 +21,7 @@ impl SigningNamespace { 1 => Ok(Self::EncryptionMetadata), #[cfg(test)] -1 => Ok(Self::Test), - _ => Err(CryptoError::InvalidNamespace), + _ => Err(SignatureError::InvalidNamespace.into()), } } } From 7e39e9cd8a1bb3038d4fc6ec8bc2208123e0a9f1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 20 May 2025 09:18:54 +0200 Subject: [PATCH 013/112] Update test vectors --- .../src/keys/signing_crypto_key.rs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 0a488dbbe..be570433b 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -463,31 +463,15 @@ impl SignedObject { #[cfg(test)] mod tests { use super::*; - - const SIGNING_KEY: &[u8] = &[ - 167, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, - 3, 39, 4, 130, 1, 2, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, 96, - 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, 168, - 35, 88, 32, 59, 136, 203, 0, 108, 23, 82, 84, 206, 163, 86, 62, 187, 196, 156, 156, 150, - 80, 101, 129, 247, 112, 117, 10, 34, 54, 254, 181, 239, 214, 195, 78, 32, 6, - ]; - const VERIFYING_KEY: &[u8] = &[ - 166, 1, 1, 2, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, 20, 115, 137, - 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 92, 186, 140, 91, 228, 10, 169, 163, 132, 55, 210, 79, - 96, 186, 198, 251, 255, 79, 157, 58, 28, 182, 213, 118, 51, 15, 60, 110, 161, 114, 222, - 168, - ]; + + const SIGNING_KEY: &[u8] = &[166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 3, 39, 4, 130, 1, 2, 35, 88, 32, 31, 72, 18, 5, 81, 182, 75, 229, 106, 91, 174, 171, 136, 48, 87, 10, 231, 220, 24, 134, 42, 189, 54, 217, 51, 206, 23, 49, 140, 165, 23, 125, 32, 6]; + const VERIFYING_KEY: &[u8] = &[166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 40, 62, 139, 254, 182, 152, 40, 135, 232, 175, 93, 191, 16, 31, 208, 54, 5, 136, 208, 14, 159, 199, 204, 209, 11, 161, 171, 213, 128, 101, 224, 160]; /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data - const SIGNATURE: &[u8] = &[ - 132, 88, 27, 163, 1, 39, 4, 80, 222, 105, 244, 28, 22, 106, 70, 109, 171, 83, 154, 97, 23, - 20, 115, 137, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 143, 218, 162, 76, 208, 117, 94, 215, - 224, 98, 89, 193, 194, 226, 144, 214, 91, 130, 129, 130, 77, 36, 79, 196, 45, 105, 120, - 151, 136, 57, 230, 27, 37, 142, 55, 191, 23, 200, 237, 215, 252, 42, 182, 140, 201, 173, - 199, 214, 97, 105, 107, 101, 140, 182, 105, 9, 206, 106, 210, 29, 203, 174, 178, 12, - ]; + const SIGNATURE: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5]; + const SIGNED_OBJECT: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 76, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5]; #[test] - fn test_using_test_vectors() { + fn test_signature_using_test_vectors() { let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); let signature = Signature::from_cose(SIGNATURE).unwrap(); @@ -502,6 +486,25 @@ mod tests { assert!(verifying_key.verify_signature(&namespace, &signature, data)); } + #[test] + fn test_signed_object_using_test_vectors() { + let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + + let data = b"Test message"; + let namespace = SigningNamespace::EncryptionMetadata; + + assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); + assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); + assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); + + let payload = verifying_key + .get_verified_payload(&namespace, &signed_object) + .unwrap(); + assert_eq!(payload, data); + } + #[test] fn test_sign_detached_roundtrip() { let signing_key = SigningKey::make_ed25519().unwrap(); From a37f3e123f77f6f33829a353ebf56ba35568ca8d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 20 May 2025 09:20:51 +0200 Subject: [PATCH 014/112] Format --- .../src/keys/signing_crypto_key.rs | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index be570433b..4b50c084c 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -9,7 +9,12 @@ use ed25519_dalek::Signer; use rand::rngs::OsRng; use super::{key_id::KeyId, KEY_ID_SIZE}; -use crate::{cose::SIGNING_NAMESPACE, error::{Result, SignatureError}, signing::SigningNamespace, CryptoError}; +use crate::{ + cose::SIGNING_NAMESPACE, + error::{Result, SignatureError}, + signing::SigningNamespace, + CryptoError, +}; #[allow(unused)] enum SigningCryptoKeyEnum { @@ -21,15 +26,17 @@ enum VerifyingKeyEnum { Ed25519(ed25519_dalek::VerifyingKey), } -/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be derived from it. +/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be +/// derived from it. #[allow(unused)] struct SigningKey { id: KeyId, inner: SigningCryptoKeyEnum, } -/// 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`. +/// A verifying key is a public key used for verifying signatures. It can be published to other +/// users, who can use it to verify that messages were signed by the holder of the corresponding +/// `SigningKey`. #[allow(unused)] struct VerifyingKey { id: KeyId, @@ -214,9 +221,9 @@ impl VerifyingKey { 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 + // 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(), Value::Bytes(key.to_bytes().to_vec()), @@ -368,7 +375,8 @@ impl From for Signature { #[allow(unused)] impl Signature { fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; + let cose_sign1 = + CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(Signature(cose_sign1)) } @@ -417,7 +425,8 @@ impl From for SignedObject { #[allow(unused)] impl SignedObject { fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; + let cose_sign1 = + CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(SignedObject(cose_sign1)) } @@ -463,12 +472,34 @@ impl SignedObject { #[cfg(test)] mod tests { use super::*; - - const SIGNING_KEY: &[u8] = &[166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 3, 39, 4, 130, 1, 2, 35, 88, 32, 31, 72, 18, 5, 81, 182, 75, 229, 106, 91, 174, 171, 136, 48, 87, 10, 231, 220, 24, 134, 42, 189, 54, 217, 51, 206, 23, 49, 140, 165, 23, 125, 32, 6]; - const VERIFYING_KEY: &[u8] = &[166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 40, 62, 139, 254, 182, 152, 40, 135, 232, 175, 93, 191, 16, 31, 208, 54, 5, 136, 208, 14, 159, 199, 204, 209, 11, 161, 171, 213, 128, 101, 224, 160]; + + const SIGNING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, + 3, 39, 4, 130, 1, 2, 35, 88, 32, 31, 72, 18, 5, 81, 182, 75, 229, 106, 91, 174, 171, 136, + 48, 87, 10, 231, 220, 24, 134, 42, 189, 54, 217, 51, 206, 23, 49, 140, 165, 23, 125, 32, 6, + ]; + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, + 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 40, 62, 139, 254, 182, 152, 40, 135, 232, 175, 93, + 191, 16, 31, 208, 54, 5, 136, 208, 14, 159, 199, 204, 209, 11, 161, 171, 213, 128, 101, + 224, 160, + ]; /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data - const SIGNATURE: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5]; - const SIGNED_OBJECT: &[u8] = &[132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 76, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5]; + const SIGNATURE: &[u8] = &[ + 132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, + 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, + 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, + 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, + 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5, + ]; + const SIGNED_OBJECT: &[u8] = &[ + 132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, + 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 76, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, + 103, 101, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, + 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, + 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, + 73, 36, 74, 34, 132, 5, + ]; #[test] fn test_signature_using_test_vectors() { From e1642b690cfdcd9bcded8e3feb6c9d01bef14b03 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 10:52:18 +0200 Subject: [PATCH 015/112] [PM-20361] Expose signing key generation to mobile and wasm clients & add to key context (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-20361 ## 📔 Objective Adds signing keys to the key context, and adds functions to make signature keys to the sdk. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Matt Gibson Co-authored-by: Daniel García --- Cargo.lock | 35 +- .../api/response/identity_success_response.rs | 3 + .../bitwarden-core/src/auth/auth_request.rs | 10 +- .../bitwarden-core/src/auth/login/api_key.rs | 9 +- .../src/auth/login/auth_request.rs | 1 + .../bitwarden-core/src/auth/login/password.rs | 14 +- .../src/auth/password/validate.rs | 14 +- crates/bitwarden-core/src/auth/pin.rs | 7 +- crates/bitwarden-core/src/auth/tde.rs | 8 +- .../src/client/encryption_settings.rs | 22 +- crates/bitwarden-core/src/client/internal.rs | 15 +- .../src/client/test_accounts.rs | 3 + .../bitwarden-core/src/key_management/mod.rs | 7 +- crates/bitwarden-core/src/mobile/crypto.rs | 91 ++- .../src/mobile/crypto_client.rs | 11 +- .../src/platform/generate_fingerprint.rs | 1 + crates/bitwarden-core/tests/register.rs | 2 + crates/bitwarden-crypto/Cargo.toml | 1 + .../src/enc_string/asymmetric.rs | 2 +- crates/bitwarden-crypto/src/error.rs | 6 + .../src/keys/asymmetric_crypto_key.rs | 27 +- .../bitwarden-crypto/src/keys/fingerprint.rs | 113 ++++ crates/bitwarden-crypto/src/keys/key_id.rs | 8 +- crates/bitwarden-crypto/src/keys/mod.rs | 3 + .../src/keys/signing_crypto_key.rs | 442 ++++--------- .../src/keys/symmetric_crypto_key.rs | 6 + crates/bitwarden-crypto/src/lib.rs | 1 + crates/bitwarden-crypto/src/signing/claims.rs | 100 +++ crates/bitwarden-crypto/src/signing/mod.rs | 33 +- .../bitwarden-crypto/src/signing/namespace.rs | 28 + crates/bitwarden-crypto/src/signing/sign.rs | 588 ++++++++++++++++++ crates/bitwarden-crypto/src/store/context.rs | 151 ++++- crates/bitwarden-crypto/src/store/mod.rs | 11 +- crates/bitwarden-crypto/src/traits/key_id.rs | 23 +- crates/bitwarden-crypto/src/traits/mod.rs | 10 +- crates/bitwarden-wasm-internal/Cargo.toml | 1 + crates/bitwarden-wasm-internal/src/crypto.rs | 8 +- .../src/pure_crypto.rs | 38 +- 38 files changed, 1441 insertions(+), 412 deletions(-) create mode 100644 crates/bitwarden-crypto/src/keys/fingerprint.rs create mode 100644 crates/bitwarden-crypto/src/signing/claims.rs create mode 100644 crates/bitwarden-crypto/src/signing/namespace.rs create mode 100644 crates/bitwarden-crypto/src/signing/sign.rs diff --git a/Cargo.lock b/Cargo.lock index b14833ff9..34d528a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,7 @@ dependencies = [ "serde_json", "serde_qs", "serde_repr", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tsify-next", "uniffi", @@ -439,11 +439,12 @@ dependencies = [ "rsa", "schemars", "serde", + "serde_bytes", "serde_json", "sha1", "sha2", "subtle", - "thiserror 1.0.69", + "thiserror 2.0.12", "tsify-next", "typenum", "uniffi", @@ -477,7 +478,7 @@ dependencies = [ "quote", "serde", "syn", - "thiserror 1.0.69", + "thiserror 2.0.12", "tsify-next", "wasm-bindgen", ] @@ -499,7 +500,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tsify-next", "uniffi", "uuid", @@ -525,7 +526,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "uniffi", "uuid", ] @@ -543,7 +544,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tsify-next", "uniffi", @@ -559,7 +560,7 @@ dependencies = [ "js-sys", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tsify-next", "wasm-bindgen", @@ -577,7 +578,7 @@ dependencies = [ "chrono", "serde", "serde_repr", - "thiserror 1.0.69", + "thiserror 2.0.12", "uniffi", "uuid", "wasm-bindgen", @@ -596,7 +597,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "uuid", "validator", @@ -617,7 +618,7 @@ dependencies = [ "rsa", "serde", "ssh-key", - "thiserror 1.0.69", + "thiserror 2.0.12", "tsify-next", "uniffi", "wasm-bindgen", @@ -645,7 +646,7 @@ dependencies = [ "oslog", "rustls-platform-verifier", "schemars", - "thiserror 1.0.69", + "thiserror 2.0.12", "uniffi", "uuid", ] @@ -670,7 +671,7 @@ dependencies = [ "serde_repr", "sha1", "sha2", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tsify-next", "uniffi", @@ -682,6 +683,7 @@ dependencies = [ name = "bitwarden-wasm-internal" version = "0.1.0" dependencies = [ + "base64", "bitwarden-core", "bitwarden-crypto", "bitwarden-error", @@ -3502,6 +3504,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs index 94ebe9445..0fe7ddff2 100644 --- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs @@ -15,6 +15,8 @@ pub struct IdentityTokenSuccessResponse { pub(crate) private_key: Option, #[serde(alias = "Key")] pub(crate) key: Option, + #[serde(alias = "userKeyEncryptedSigningKey")] + pub(crate) user_key_encrypted_signing_key: Option, #[serde(rename = "twoFactorToken")] two_factor_token: Option, #[serde(alias = "Kdf")] @@ -53,6 +55,7 @@ mod test { refresh_token: Default::default(), token_type: Default::default(), private_key: Default::default(), + user_key_encrypted_signing_key: Default::default(), key: Default::default(), two_factor_token: Default::default(), kdf: KdfType::default(), diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index c265584f4..9033b7ff8 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -162,7 +162,7 @@ mod tests { let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); client .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; @@ -229,7 +229,12 @@ mod tests { existing_device .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key.parse().unwrap()) + .initialize_user_crypto_master_key( + master_key, + user_key, + private_key.parse().unwrap(), + None, + ) .unwrap(); // Initialize a new device which will request to be logged in @@ -246,6 +251,7 @@ mod tests { kdf_params: kdf, email: email.to_owned(), private_key: private_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method: AuthRequestMethod::UserKey { diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 649e448f8..d6d70743a 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -51,9 +51,12 @@ pub(crate) async fn login_api_key( let user_key: EncString = require!(r.key.as_deref()).parse()?; let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - client - .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key)?; + client.internal.initialize_user_crypto_master_key( + master_key, + user_key, + private_key, + None, + )?; } Ok(ApiKeyLoginResponse::process_response(response)) diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 6d2abd203..17ba9a234 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -118,6 +118,7 @@ pub(crate) async fn complete_auth_request( kdf_params: kdf, email: auth_req.email, private_key: require!(r.private_key), + signing_key: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method, diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 9d9390b85..7c4b7fbd1 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -52,10 +52,18 @@ pub(crate) async fn login_password( let user_key: EncString = require!(r.key.as_deref()).parse()?; let private_key: EncString = require!(r.private_key.as_deref()).parse()?; + let signing_key = r + .user_key_encrypted_signing_key + .clone() + .map(|s| s.parse()) + .transpose()?; - client - .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key)?; + client.internal.initialize_user_crypto_master_key( + master_key, + user_key, + private_key, + signing_key, + )?; } Ok(PasswordLoginResponse::process_response(response)) diff --git a/crates/bitwarden-core/src/auth/password/validate.rs b/crates/bitwarden-core/src/auth/password/validate.rs index 39abee276..229c7fde7 100644 --- a/crates/bitwarden-core/src/auth/password/validate.rs +++ b/crates/bitwarden-core/src/auth/password/validate.rs @@ -140,7 +140,12 @@ mod tests { client .internal - .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key) + .initialize_user_crypto_master_key( + master_key, + user_key.parse().unwrap(), + private_key, + None, + ) .unwrap(); let result = @@ -183,7 +188,12 @@ mod tests { client .internal - .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key) + .initialize_user_crypto_master_key( + master_key, + user_key.parse().unwrap(), + private_key, + None, + ) .unwrap(); let result = diff --git a/crates/bitwarden-core/src/auth/pin.rs b/crates/bitwarden-core/src/auth/pin.rs index 93e172f25..c337f9327 100644 --- a/crates/bitwarden-core/src/auth/pin.rs +++ b/crates/bitwarden-core/src/auth/pin.rs @@ -75,7 +75,12 @@ mod tests { client .internal - .initialize_user_crypto_master_key(master_key, user_key.parse().unwrap(), private_key) + .initialize_user_crypto_master_key( + master_key, + user_key.parse().unwrap(), + private_key, + None, + ) .unwrap(); client diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index e8bc8470e..2fd57ea97 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -37,9 +37,11 @@ pub(super) fn make_register_tde_keys( kdf: Kdf::default(), }, )); - client - .internal - .initialize_user_crypto_decrypted_key(user_key.0, key_pair.private.clone())?; + client.internal.initialize_user_crypto_decrypted_key( + user_key.0, + key_pair.private.clone(), + None, + )?; Ok(RegisterTdeKeyResponse { private_key: key_pair.private, diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 97351cb9f..daee0968a 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,12 +1,11 @@ -use bitwarden_crypto::{AsymmetricCryptoKey, KeyStore, SymmetricCryptoKey}; #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, UnsignedSharedKey}; +use bitwarden_crypto::{EncString, KeyStore, SymmetricCryptoKey, UnsignedSharedKey}; use bitwarden_error::bitwarden_error; use thiserror::Error; use uuid::Uuid; use crate::{ - key_management::{AsymmetricKeyId, KeyIds, SymmetricKeyId}, + key_management::{KeyIds, SymmetricKeyId}, MissingPrivateKeyError, VaultLockedError, }; @@ -40,12 +39,13 @@ impl EncryptionSettings { pub(crate) fn new_decrypted_key( user_key: SymmetricCryptoKey, private_key: EncString, + signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::KeyDecryptable; + use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable, SigningKey}; use log::warn; - use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; + use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; let private_key = { let dec: Vec = private_key.decrypt_with_key(&user_key)?; @@ -63,6 +63,12 @@ impl EncryptionSettings { // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, // ) }; + let signing_key = signing_key + .map(|key| { + let dec: Vec = key.decrypt_with_key(&user_key)?; + SigningKey::from_cose(dec.as_slice()) + }) + .transpose()?; // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods #[allow(deprecated)] @@ -72,6 +78,10 @@ impl EncryptionSettings { if let Some(private_key) = private_key { ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; } + + if let Some(signing_key) = signing_key { + ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; + } } Ok(()) @@ -98,6 +108,8 @@ impl EncryptionSettings { org_enc_keys: Vec<(Uuid, UnsignedSharedKey)>, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { + use crate::key_management::AsymmetricKeyId; + let mut ctx = store.context_mut(); // FIXME: [PM-11690] - Early abort to handle private key being corrupt diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 725d95d33..c29e144ef 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -8,13 +8,11 @@ use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey}; use chrono::Utc; use uuid::Uuid; +use super::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use super::login_method::ServiceAccountLoginMethod; use crate::{ - auth::renew::renew_token, - client::{encryption_settings::EncryptionSettings, login_method::LoginMethod}, - key_management::KeyIds, - DeviceType, + auth::renew::renew_token, client::login_method::LoginMethod, key_management::KeyIds, DeviceType, }; #[cfg(feature = "internal")] use crate::{ @@ -178,9 +176,10 @@ impl InternalClient { master_key: MasterKey, user_key: EncString, private_key: EncString, + signing_key: Option, ) -> Result<(), EncryptionSettingsError> { let user_key = master_key.decrypt_user_key(user_key)?; - EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; Ok(()) } @@ -190,8 +189,9 @@ impl InternalClient { &self, user_key: SymmetricCryptoKey, private_key: EncString, + signing_key: Option, ) -> Result<(), EncryptionSettingsError> { - EncryptionSettings::new_decrypted_key(user_key, private_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; Ok(()) } @@ -202,9 +202,10 @@ impl InternalClient { pin_key: PinKey, pin_protected_user_key: EncString, private_key: EncString, + signing_key: Option, ) -> Result<(), EncryptionSettingsError> { let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?; - self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key) + self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key, signing_key) } #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-core/src/client/test_accounts.rs b/crates/bitwarden-core/src/client/test_accounts.rs index 367c36495..f5d1819a7 100644 --- a/crates/bitwarden-core/src/client/test_accounts.rs +++ b/crates/bitwarden-core/src/client/test_accounts.rs @@ -123,6 +123,8 @@ pub fn test_bitwarden_com_account() -> TestAccount { email: "test@bitwarden.com".to_owned(), private_key: "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".to_owned(), + signing_key: None, + method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), user_key: "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".to_owned(), @@ -179,6 +181,7 @@ pub fn test_legacy_user_key_account() -> TestAccount { }, email: "legacy@bitwarden.com".to_owned(), private_key: "2.leBIE5u0aQUeXi++JzAnrA==|P8x+hs00RJx7epw+49qVtBhLJxE/JTL5dEHg6kq5pbZLdUY8ZvWK49v0EqgHbv1r298N9+msoO9hmdSIVIAZyycemYDSoc1rX4S1KpS/ZMA/Vd3VLFb+o13Ts62GFQ5ygHKgQZfzjU6jO5P/B/0igzFoxyJDomhW5NBC1P9+e/5qNRZN8loKvAaWc/7XtpRayPQqWx+AgYc2ntb1GF5hRVrW4M47bG5ZKllbJWtQKg2sXIy2lDBbKLRFWF4RFzNVcXQGMoPdWLY0f3uTwUH01dyGmFFMbOvfBEuYqmZyPdd93ve8zuFOEqkj46Ulpq2CVG8NvZARTwsdKl6XB0wGuHFoTsDJT2SJGl67pBBKsVRGxy059QW+9hAIB+emIV0T/7+0rvdeSXZ4AbG+oXGEXFTkHefwJKfeT0MBTAjYKr7ZRLgqvf7n39+nCEJU4l22kp8FmjcWIU7AgNipdGHC+UT2yfOcYlvgBgWDcMXcbVDMyus9105RgcW6PHozUj7yjbohI/A3XWmAFufP6BSnmEFCKoik78X/ry09xwiH2rN4KVXe/k9LpRNB2QBGIVsfgCrkxjeE8r0nA59Rvwrhny1z5BkvMW/N1KrGuafg/IYgegx72gJNuZPZlFu1Vs7HxySHmzYvm3DPV7bzCaAxxNtvZmQquNIEnsDQfjJO76iL1JCtDqNJVzGLHTMTr7S5hkOcydcH3kfKwZdA1ULVd2qu0SwOUEP/ECjU/cS5INy6WPYzNMAe/g2DISpQjNwBb5K17PIiGOR7/Q/A6E8pVnkHiAXuUFr9aLOYN9BWSu5Z+BPHH65na2FDmssix5WV09I2sUBfvdNCjkrUGdYgo8E+vOTn35x9GJHF45uhmgC1yAn/+/RSpORlrSVJ7NNP11dn3htUpSsIy/b7ituAu8Ry5mhicFU8CXJL4NeMlXThUt8P++wxs4wMkBvJ8J9NJAVKbAOA2o+GOdjbh6Ww3IRegkurWh4oL/dFSx0LpaXJuw6HFT/LzticPlSwHtUP11hZ81seMsXmkSZd8IugRFfwpPl7N6PVRWDOKxLf4gPqcnJ11TvfasXy1uolV2vZCPbrbbVzQMPdVwL/OzwfhqsIgQZI8rsDMK5D2EX8MaT8MDfGcsYcVTL9PmuZYLpOUnnHX0A1opAAa9iPw3d+eWB/GAyLvKPnMTUqVNos8HcCktXckCshihA8QuBJOwg3m0j2LPSZ5Jvf8gbXauBmt9I4IlJq0xfpgquYY1WNnO8IcWE4N9W+ASvOr9gnduA6CkDeAlyMUFmdpkeCjGMcsV741bTCPApSQlL3/TOT1cjK3iejWpz0OaVHXyg02hW2fNkOfYfr81GvnLvlHxIg4Prw89gKuWU+kQk82lFQo6QQpqbCbJC2FleurD8tYoSY0srhuioVInffvTxw2NMF7FQEqUcsK9AMKSEiDqzBi35Um/fiE3JL4XZBFw8Xzl7X3ab5nlg8X+xD5uSZY+oxD3sDVXjLaQ5JUoys+MCm0FkUj85l0zT6rvM4QLhU1RDK1U51T9HJhh8hsFJsqL4abRzwEWG7PSi859zN4UsgyuQfmBJv/n7QAFCbrJhVBlGB1TKLZRzvgmKoxTYTG3cJFkjetLcUTwrwC9naxAQRfF4=|ufHf73IzJ707dx44w4fjkuD7tDa50OwmmkxcypAT9uQ=".to_owned(), + signing_key: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), user_key: "0.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=".to_owned(), diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index dd13ab21c..efcfb86db 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -26,7 +26,12 @@ key_ids! { Local(&'static str), } - pub KeyIds => SymmetricKeyId, AsymmetricKeyId; + #[signing] + pub enum SigningKeyId { + UserSigningKey, + } + + pub KeyIds => SymmetricKeyId, AsymmetricKeyId, SigningKeyId; } /// This is a helper function to create a test KeyStore with a single user key. diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 6e2905ce8..a3c04957d 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -8,16 +8,18 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable, + KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKeyOwnershipClaim, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, - key_management::SymmetricKeyId, + key_management::{AsymmetricKeyId, SymmetricKeyId}, Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError, }; @@ -45,6 +47,10 @@ pub struct InitUserCryptoRequest { pub email: String, /// The user's encrypted private key pub private_key: String, + + /// The user's signing key + pub signing_key: Option, + /// The initialization method to use pub method: InitUserCryptoMethod, } @@ -136,15 +142,18 @@ pub async fn initialize_user_crypto( let user_key: EncString = user_key.parse()?; let master_key = MasterKey::derive(&password, &req.email, &req.kdf_params)?; - client - .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key)?; + client.internal.initialize_user_crypto_master_key( + master_key, + user_key, + private_key, + None, + )?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?; client .internal - .initialize_user_crypto_decrypted_key(user_key, private_key)?; + .initialize_user_crypto_decrypted_key(user_key, private_key, None)?; } InitUserCryptoMethod::Pin { pin, @@ -155,6 +164,7 @@ pub async fn initialize_user_crypto( pin_key, pin_protected_user_key, private_key, + None, )?; } InitUserCryptoMethod::AuthRequest { @@ -176,7 +186,7 @@ pub async fn initialize_user_crypto( }; client .internal - .initialize_user_crypto_decrypted_key(user_key, private_key)?; + .initialize_user_crypto_decrypted_key(user_key, private_key, None)?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -189,7 +199,7 @@ pub async fn initialize_user_crypto( client .internal - .initialize_user_crypto_decrypted_key(user_key, private_key)?; + .initialize_user_crypto_decrypted_key(user_key, private_key, None)?; } InitUserCryptoMethod::KeyConnector { master_key, @@ -201,9 +211,12 @@ pub async fn initialize_user_crypto( let master_key = MasterKey::try_from(master_key_bytes.as_mut_slice())?; let user_key: EncString = user_key.parse()?; - client - .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key)?; + client.internal.initialize_user_crypto_master_key( + master_key, + user_key, + private_key, + None, + )?; } } @@ -542,6 +555,55 @@ pub(super) fn verify_asymmetric_keys( }) } +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct MakeUserSigningKeysResponse { + /// The verifying key + verifying_key: String, + /// Signing key, encrypted with a symmetric key (user key, org key) + signing_key: EncString, + + /// A signed object claiming ownership of a public key. This ties the public key to the + /// signature key + signed_public_key_ownership_claim: String, +} + +/// Makes a new set of signing keys for a user. This also creates a signed public-key ownership +/// claim for the currently used public key. +#[allow(deprecated)] +pub fn make_user_signing_keys(client: &Client) -> Result { + let key_store = client.internal.get_key_store(); + let ctx = key_store.context(); + let public_key = ctx + .dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey) + .map_err(|_| CryptoError::InvalidKey)? + .to_public_der()?; + let public_key = + AsymmetricPublicCryptoKey::from_der(&public_key).map_err(|_| CryptoError::InvalidKey)?; + + let wrapping_key = ctx + .dangerous_get_symmetric_key(SymmetricKeyId::User) + .map_err(|_| CryptoError::InvalidKey)?; + let signature_keypair = + SigningKey::make(SignatureAlgorithm::Ed25519).map_err(|_| CryptoError::InvalidKey)?; + // This needs to be changed to use the correct cose content format before rolling out to real + // accounts + let encrypted_signing_key = signature_keypair.to_cose()?; + let serialized_verifying_key = signature_keypair.to_verifying_key().to_cose()?; + let serialized_verifying_key_b64 = STANDARD.encode(serialized_verifying_key); + let signed_public_key_ownership_claim = + SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signature_keypair)?; + + Ok(MakeUserSigningKeysResponse { + verifying_key: serialized_verifying_key_b64, + signing_key: encrypted_signing_key.encrypt_with_key(wrapping_key)?, + signed_public_key_ownership_claim: STANDARD + .encode(signed_public_key_ownership_claim.as_bytes()), + }) +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; @@ -567,6 +629,7 @@ mod tests { kdf_params: kdf.clone(), email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), @@ -586,6 +649,7 @@ mod tests { kdf_params: kdf.clone(), email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::Password { password: "123412341234".into(), user_key: new_password_response.new_key.to_string(), @@ -643,6 +707,7 @@ mod tests { }, email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), @@ -664,6 +729,7 @@ mod tests { }, email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key: pin_key.pin_protected_user_key, @@ -706,6 +772,7 @@ mod tests { }, email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), + signing_key: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key, @@ -756,7 +823,7 @@ mod tests { let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); client .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; diff --git a/crates/bitwarden-core/src/mobile/crypto_client.rs b/crates/bitwarden-core/src/mobile/crypto_client.rs index 42634ce32..69de145a7 100644 --- a/crates/bitwarden-core/src/mobile/crypto_client.rs +++ b/crates/bitwarden-core/src/mobile/crypto_client.rs @@ -3,9 +3,10 @@ use bitwarden_crypto::CryptoError; use bitwarden_crypto::{EncString, UnsignedSharedKey}; use super::crypto::{ - derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorError, - DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, MakeKeyPairResponse, - MobileCryptoError, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, + derive_key_connector, make_key_pair, make_user_signing_keys, verify_asymmetric_keys, + DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, + MakeKeyPairResponse, MakeUserSigningKeysResponse, MobileCryptoError, + VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }; #[cfg(feature = "internal")] use crate::mobile::crypto::{ @@ -101,6 +102,10 @@ impl CryptoClient { ) -> Result { verify_asymmetric_keys(request) } + + pub fn make_signing_keys(&self) -> Result { + make_user_signing_keys(&self.client) + } } impl Client { diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 21a16e976..4e1bfe009 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -107,6 +107,7 @@ mod tests { master_key, user_key.parse().unwrap(), private_key.parse().unwrap(), + None, ) .unwrap(); diff --git a/crates/bitwarden-core/tests/register.rs b/crates/bitwarden-core/tests/register.rs index 3f01b4763..c8ac264e8 100644 --- a/crates/bitwarden-core/tests/register.rs +++ b/crates/bitwarden-core/tests/register.rs @@ -33,6 +33,8 @@ async fn test_register_initialize_crypto() { email: email.to_owned(), private_key: register_response.keys.private.to_string(), + signing_key: None, + method: InitUserCryptoMethod::Password { password: password.to_owned(), user_key: register_response.encrypted_user_key, diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index e6a9fbdbe..8d5495e65 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -46,6 +46,7 @@ rayon = ">=1.8.1, <2.0" rsa = ">=0.9.2, <0.10" schemars = { workspace = true } serde = { workspace = true } +serde_bytes = ">=0.11.17, <0.12.0" sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" subtle = ">=2.5.0, <3.0" diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index d5d09c53a..c4314bdaf 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -162,7 +162,7 @@ impl UnsignedSharedKey { encapsulation_key: &dyn AsymmetricEncryptable, ) -> Result { let enc = encrypt_rsa2048_oaep_sha1( - encapsulation_key.to_public_key(), + encapsulation_key.to_public_rsa_key(), &encapsulated_key.to_encoded(), )?; Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data: enc }) diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 2cb623e3b..ec5645d08 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -59,6 +59,12 @@ pub enum CryptoError { #[error("Signature error, {0}")] SignatureError(#[from] SignatureError), + + #[error("Cose encoding error")] + CoseEncodingError, + + #[error("Invalid encoding")] + InvalidEncoding, } #[derive(Debug, Error)] diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 284c4ce25..d7adb45a8 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -1,14 +1,14 @@ use std::pin::Pin; -use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; +use rsa::{pkcs8::DecodePublicKey, traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey}; -use super::key_encryptable::CryptoKey; +use super::{fingerprint::FingerprintableKey, key_encryptable::CryptoKey}; use crate::error::{CryptoError, Result}; /// Trait to allow both [`AsymmetricCryptoKey`] and [`AsymmetricPublicCryptoKey`] to be used to /// encrypt [UnsignedSharedKey](crate::UnsignedSharedKey). pub trait AsymmetricEncryptable { - fn to_public_key(&self) -> &RsaPublicKey; + fn to_public_rsa_key(&self) -> &RsaPublicKey; } /// An asymmetric public encryption key. Can only encrypt @@ -29,11 +29,20 @@ impl AsymmetricPublicCryptoKey { } impl AsymmetricEncryptable for AsymmetricPublicCryptoKey { - fn to_public_key(&self) -> &RsaPublicKey { + fn to_public_rsa_key(&self) -> &RsaPublicKey { &self.key } } +impl FingerprintableKey for AsymmetricPublicCryptoKey { + fn fingerprint_parts(&self) -> Vec> { + vec![ + self.key.n().to_bytes_le().as_slice().to_vec(), + self.key.e().to_bytes_le().as_slice().to_vec(), + ] + } +} + /// An asymmetric encryption key. Contains both the public and private key. Can be used to both /// encrypt and decrypt [`UnsignedSharedKey`](crate::UnsignedSharedKey). #[derive(Clone)] @@ -93,16 +102,22 @@ impl AsymmetricCryptoKey { pub fn to_public_der(&self) -> Result> { use rsa::pkcs8::EncodePublicKey; Ok(self - .to_public_key() + .to_public_rsa_key() .to_public_key_der() .map_err(|_| CryptoError::InvalidKey)? .as_bytes() .to_owned()) } + + pub fn to_public_key(&self) -> AsymmetricPublicCryptoKey { + AsymmetricPublicCryptoKey { + key: self.key.to_public_key().clone(), + } + } } impl AsymmetricEncryptable for AsymmetricCryptoKey { - fn to_public_key(&self) -> &RsaPublicKey { + fn to_public_rsa_key(&self) -> &RsaPublicKey { (*self.key).as_ref() } } diff --git a/crates/bitwarden-crypto/src/keys/fingerprint.rs b/crates/bitwarden-crypto/src/keys/fingerprint.rs new file mode 100644 index 000000000..1c7bc451f --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/fingerprint.rs @@ -0,0 +1,113 @@ +//! This module provides functionality to generate a cryptographic fingerprint for a public key. +//! This is based on a set of parts of a public key, for RSA this can be the modulus and exponent, +//! in canonical form. +//! +//! Currently, only SHA256 is supported, but the format is designed to be extensible, to more +//! algorithms in the future, should SHA256 ever not fulfill the required security properties. +use serde::{Deserialize, Serialize}; +use sha2::Digest; + +/// Security assumption: +/// - The hash function has second pre-image resistance +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum PublicKeyFingerprintAlgorithm { + Sha256 = 1, +} + +/// A fingerprint represents a short, canonical representation of a public key. +/// When signing a key, or showing a key to a user, this representation is used. +/// +/// Note: This implies that a key can have multiple fingerprints. Under a given algorithm, +/// the fingerprint is always the same, but under different algorithms, the fingerprint is also +/// different. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct PublicKeyFingerprint { + pub(crate) digest: serde_bytes::ByteBuf, + pub(crate) algorithm: PublicKeyFingerprintAlgorithm, +} + +/// A trait for objects that can have a canonical cryptographic fingerprint derived from them. To +/// implement this trait, the object should implement the `FingerprintableKey` trait. +pub(crate) trait Fingerprintable { + /// Returns a fingerprint for the public key, using the currently recommended algorithm. + fn fingerprint(&self) -> PublicKeyFingerprint; + /// Verify that a fingerprint is valid for the public key + fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool; +} + +pub(crate) trait FingerprintableKey: Fingerprintable { + /// Returns a canonical representation of the public key. + /// The entries of the returned vector should not contain data that is a non-injective mapping + /// of the public key. For instance, for RSA, the modulus and exponent should be returned + /// separately, not concatenated. + fn fingerprint_parts(&self) -> Vec>; +} + +impl Fingerprintable for T { + fn fingerprint(&self) -> PublicKeyFingerprint { + let fingerprint_parts = self.fingerprint_parts(); + derive_fingerprint(fingerprint_parts) + } + + fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool { + let fingerprint_parts = self.fingerprint_parts(); + verify_fingerprint(fingerprint, fingerprint_parts) + } +} + +/// Derives a fingerprint using a currently supported algorithm. +/// Fingerprint_parts must be a canonical set of parts representing the public key. +/// +/// The encoding needs to be canonical. That is, something like DER or PEM does *not* work, +/// because the encoding could differ slightly between implementations. For RSA, using the modulus +/// and exponent directly works. +fn derive_fingerprint(fingerprint_parts: Vec>) -> PublicKeyFingerprint { + derive_fingerprint_from_parts(fingerprint_parts) +} + +/// This function ensures an injective mapping of the inputs to the output hash. +/// Concatenating the inputs does not work. For RSA this could mean that: +/// with data = [N,E], |nnnnnn|ee|, and |nnnnnnn|e| would both be valid interpretations of the +/// concatenation of the bytes, and thus may lead to the same hash for different (N,E) pairs. +/// +/// This function hashes each input separately, concatenates the hashes, and then hashes the result. +/// +/// Assumption: H is a cryptographic hash function, with respect to: +/// - Second pre-image resistance +/// +/// Assumption: H's output has a constant length output HS +/// +/// Specifically, the construction is: +/// H(H(data1)|H(data2)|...|H(dataN)) +/// +/// Given the assumptions above, then hashing each input separately, and concatenating the hashes is +/// an injective mapping. Because there is an injective mapping, and because of collision resistance +/// w.r.t. the final hash functions inputs, this also implies collision resistance w.r.t. data. +fn derive_fingerprint_from_parts(data: Vec>) -> PublicKeyFingerprint { + let hash_set = data + .iter() + .map(|d| derive_fingerprint_single(d)) + .collect::>(); + let concat = hash_set + .iter() + .flat_map(|h| h.digest.clone()) + .collect::>(); + derive_fingerprint_single(&concat) +} + +fn derive_fingerprint_single(data: &[u8]) -> PublicKeyFingerprint { + PublicKeyFingerprint { + digest: sha2::Sha256::digest(data).to_vec().into(), + algorithm: PublicKeyFingerprintAlgorithm::Sha256, + } +} + +/// Verifies a fingerprint for a given public key, represented as a canonical list of parts. +fn verify_fingerprint(fingerprint: &PublicKeyFingerprint, fingerprint_parts: Vec>) -> bool { + match fingerprint.algorithm { + PublicKeyFingerprintAlgorithm::Sha256 => { + let hash = derive_fingerprint_from_parts(fingerprint_parts); + hash.digest == fingerprint.digest + } + } +} diff --git a/crates/bitwarden-crypto/src/keys/key_id.rs b/crates/bitwarden-crypto/src/keys/key_id.rs index 525a74183..cb772d519 100644 --- a/crates/bitwarden-crypto/src/keys/key_id.rs +++ b/crates/bitwarden-crypto/src/keys/key_id.rs @@ -10,7 +10,13 @@ pub(crate) const KEY_ID_SIZE: usize = 16; /// bytes, so something like a user key rotation is replacing the key with ID A with a new key with /// ID B. #[derive(Clone)] -pub(crate) struct KeyId(uuid::Uuid); +pub(crate) struct KeyId(Uuid); + +impl zeroize::Zeroize for KeyId { + fn zeroize(&mut self) { + self.0 = Uuid::nil(); + } +} /// Fixed length identifiers for keys. /// These are intended to be unique and constant per-key. diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 73362c3c8..55e341356 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -15,14 +15,17 @@ pub use asymmetric_crypto_key::{ AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey, }; mod signing_crypto_key; +pub use signing_crypto_key::{SigningKey, *}; mod user_key; pub use user_key::UserKey; mod device_key; pub use device_key::{DeviceKey, TrustDeviceResponse}; mod pin_key; pub use pin_key::PinKey; +mod fingerprint; mod kdf; mod key_id; +pub(crate) use fingerprint::{Fingerprintable, FingerprintableKey, PublicKeyFingerprint}; pub use kdf::{ default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, default_pbkdf2_iterations, Kdf, diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 4b50c084c..222474482 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -1,14 +1,19 @@ -//! This file implements creation and verification of detached signatures - use ciborium::{value::Integer, Value}; use coset::{ - iana::{self, Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, + iana::{ + self, Algorithm, CoapContentFormat, EllipticCurve, EnumI64, KeyOperation, KeyType, + OkpKeyParameter, + }, CborSerializable, CoseKey, CoseSign1, Label, RegisteredLabel, RegisteredLabelWithPrivate, }; -use ed25519_dalek::Signer; use rand::rngs::OsRng; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; +use zeroize::ZeroizeOnDrop; -use super::{key_id::KeyId, KEY_ID_SIZE}; +use super::{key_id::KeyId, CryptoKey, KEY_ID_SIZE}; use crate::{ cose::SIGNING_NAMESPACE, error::{Result, SignatureError}, @@ -16,51 +21,74 @@ use crate::{ CryptoError, }; -#[allow(unused)] -enum SigningCryptoKeyEnum { +/// The type of key / signature scheme used for signing and verifying. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub enum SignatureAlgorithm { + Ed25519, +} + +impl SignatureAlgorithm { + /// Returns the currently accepted safe algorithm for new keys. + pub fn default_algorithm() -> Self { + SignatureAlgorithm::Ed25519 + } +} + +/// A `SigningKey` without the key id. This enum contains a variant for each supported signature +/// scheme. +#[derive(Clone, zeroize::ZeroizeOnDrop)] +pub(crate) enum RawSigningKey { Ed25519(ed25519_dalek::SigningKey), } -#[allow(unused)] -enum VerifyingKeyEnum { +/// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature +/// scheme. +pub(crate) enum RawVerifyingKey { Ed25519(ed25519_dalek::VerifyingKey), } /// A signing key is a private key used for signing data. An associated `VerifyingKey` can be /// derived from it. -#[allow(unused)] -struct SigningKey { - id: KeyId, - inner: SigningCryptoKeyEnum, +#[derive(Clone, ZeroizeOnDrop)] +pub struct SigningKey { + pub(crate) id: KeyId, + pub(crate) inner: RawSigningKey, } +impl CryptoKey for SigningKey {} + /// A verifying key is a public key used for verifying signatures. It can be published to other /// users, who can use it to verify that messages were signed by the holder of the corresponding /// `SigningKey`. -#[allow(unused)] -struct VerifyingKey { +pub struct VerifyingKey { id: KeyId, - inner: VerifyingKeyEnum, + pub(crate) inner: RawVerifyingKey, } -#[allow(unused)] impl SigningKey { - fn make_ed25519() -> Result { - Ok(SigningKey { - id: KeyId::make(), - inner: SigningCryptoKeyEnum::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), - }) + /// Makes a new signing key for the given signature scheme. + pub fn make(key_algorithm: SignatureAlgorithm) -> Result { + match key_algorithm { + SignatureAlgorithm::Ed25519 => Ok(SigningKey { + id: KeyId::make(), + inner: RawSigningKey::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), + }), + } } - fn cose_algorithm(&self) -> Algorithm { + pub(crate) fn cose_algorithm(&self) -> Algorithm { match &self.inner { - SigningCryptoKeyEnum::Ed25519(_) => Algorithm::EdDSA, + RawSigningKey::Ed25519(_) => Algorithm::EdDSA, } } - fn to_cose(&self) -> Result> { + /// Serializes the signing key to a COSE-formatted byte array. + pub fn to_cose(&self) -> Result> { match &self.inner { - SigningCryptoKeyEnum::Ed25519(key) => { + RawSigningKey::Ed25519(key) => { coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) .algorithm(Algorithm::EdDSA) @@ -81,7 +109,8 @@ impl SigningKey { } } - fn from_cose(bytes: &[u8]) -> Result { + /// Deserializes a COSE-formatted byte array into a signing key. + pub fn from_cose(bytes: &[u8]) -> Result { let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) else { @@ -129,7 +158,7 @@ impl SigningKey { let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes); Ok(SigningKey { id: key_id, - inner: SigningCryptoKeyEnum::Ed25519(key), + inner: RawSigningKey::Ed25519(key), }) } else { Err(CryptoError::InvalidKey) @@ -139,81 +168,31 @@ impl SigningKey { } } - /// Signs the given payload with the signing key, under a given namespace. - /// This returns a [`Signature`] object, that does not contain the payload. - /// The payload must be stored separately, and needs to be provided when verifying the - /// signature. - /// - /// This should be used when multiple signers are required, or when signatures need to be - /// replaceable without re-uploading the object, or if the signed object should be parseable - /// by the server side, without the use of COSE on the server. - pub(crate) fn sign_detached(&self, namespace: &SigningNamespace, data: &[u8]) -> Signature { - Signature::from( - coset::CoseSign1Builder::new() - .protected( - coset::HeaderBuilder::new() - .algorithm(self.cose_algorithm()) - .key_id((&self.id).into()) - .value( - SIGNING_NAMESPACE, - ciborium::Value::Integer(Integer::from(namespace.as_i64())), - ) - .build(), - ) - .create_detached_signature(data, &[], |pt| self.sign_raw(pt)) - .build(), - ) - } - - /// Signs the given payload with the signing key, under a given namespace. - /// This returns a [`SignedObject`] object, that contains the payload. - /// The payload is included in the signature, and does not need to be provided when verifying - /// the signature. - /// - /// This should be used when only one signer is required, so that only one object needs to be - /// kept track of. - pub(crate) fn sign(&self, namespace: &SigningNamespace, data: &[u8]) -> Result { - let cose_sign1 = coset::CoseSign1Builder::new() - .protected( - coset::HeaderBuilder::new() - .algorithm(self.cose_algorithm()) - .key_id((&self.id).into()) - .value( - SIGNING_NAMESPACE, - ciborium::Value::Integer(Integer::from(namespace.as_i64())), - ) - .build(), - ) - .payload(data.to_vec()) - .create_signature(&[], |pt| self.sign_raw(pt)) - .build(); - Ok(SignedObject(cose_sign1)) - } - - /// Signs the given byte array with the signing key. - /// This should never be used directly, but only through the `sign` method, to enforce - /// strong domain separation of the signatures. - fn sign_raw(&self, data: &[u8]) -> Vec { + /// Derives the verifying key from the signing key. The key id is the same for the signing and + /// verifying key, since they are a pair. + pub fn to_verifying_key(&self) -> VerifyingKey { match &self.inner { - SigningCryptoKeyEnum::Ed25519(key) => key.sign(data).to_bytes().to_vec(), + RawSigningKey::Ed25519(key) => VerifyingKey { + id: self.id.clone(), + inner: RawVerifyingKey::Ed25519(key.verifying_key()), + }, } } - fn to_verifying_key(&self) -> VerifyingKey { + #[allow(unused)] + fn algorithm(&self) -> SignatureAlgorithm { match &self.inner { - SigningCryptoKeyEnum::Ed25519(key) => VerifyingKey { - id: self.id.clone(), - inner: VerifyingKeyEnum::Ed25519(key.verifying_key()), - }, + RawSigningKey::Ed25519(_) => SignatureAlgorithm::Ed25519, } } } #[allow(unused)] impl VerifyingKey { - fn to_cose(&self) -> Result> { + /// Serializes the verifying key to a COSE-formatted byte array. + pub fn to_cose(&self) -> Result> { match &self.inner { - VerifyingKeyEnum::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() + RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) .algorithm(Algorithm::EdDSA) .param( @@ -235,7 +214,8 @@ impl VerifyingKey { } } - fn from_cose(bytes: &[u8]) -> Result { + /// Deserializes a COSE-formatted byte array into a verifying key. + pub fn from_cose(bytes: &[u8]) -> Result { let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) @@ -285,7 +265,7 @@ impl VerifyingKey { .map_err(|_| CryptoError::InvalidKey)?; Ok(VerifyingKey { id: key_id, - inner: VerifyingKeyEnum::Ed25519(verifying_key), + inner: RawVerifyingKey::Ed25519(verifying_key), }) } else { Err(CryptoError::InvalidKey) @@ -295,67 +275,10 @@ impl VerifyingKey { } } - /// Verifies the signature of the given data, for the given namespace. - /// This should never be used directly, but only through the `verify` method, to enforce - /// strong domain separation of the signatures. - pub(crate) fn verify_signature( - &self, - namespace: &SigningNamespace, - signature: &Signature, - data: &[u8], - ) -> bool { - let Some(_alg) = &signature.inner().protected.header.alg else { - return false; - }; - - let Ok(signature_namespace) = signature.namespace() else { - return false; - }; - if signature_namespace != *namespace { - return false; - } - - signature - .inner() - .verify_detached_signature(data, &[], |sig, data| self.verify_raw(sig, data)) - .is_ok() - } - - /// Verifies the signature of a signed object, for the given namespace, and returns the payload. - pub(crate) fn get_verified_payload( - &self, - namespace: &SigningNamespace, - signature: &SignedObject, - ) -> Result> { - let Some(_alg) = &signature.inner().protected.header.alg else { - return Err(SignatureError::InvalidSignature.into()); - }; - - let signature_namespace = signature.namespace()?; - if signature_namespace != *namespace { - return Err(SignatureError::InvalidNamespace.into()); - } - - signature - .inner() - .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?; - signature.payload() - } - - /// Verifies the signature of the given data, for the given namespace. - /// This should never be used directly, but only through the `verify` method, to enforce - /// strong domain separation of the signatures. - fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<()> { + /// Returns the signature scheme used by the verifying key. + pub fn algorithm(&self) -> SignatureAlgorithm { match &self.inner { - VerifyingKeyEnum::Ed25519(key) => { - let sig = ed25519_dalek::Signature::from_bytes( - signature - .try_into() - .map_err(|_| SignatureError::InvalidSignature)?, - ); - key.verify_strict(data, &sig) - .map_err(|_| SignatureError::InvalidSignature.into()) - } + RawVerifyingKey::Ed25519(_) => SignatureAlgorithm::Ed25519, } } } @@ -363,8 +286,7 @@ impl VerifyingKey { /// A signature cryptographically attests to a (namespace, data) pair. The namespace is included in /// the signature object, the data is not. One data object can be signed multiple times, with /// different namespaces / by different signers, depending on the application needs. -#[allow(unused)] -struct Signature(CoseSign1); +pub struct Signature(CoseSign1); impl From for Signature { fn from(cose_sign1: CoseSign1) -> Self { @@ -374,24 +296,24 @@ impl From for Signature { #[allow(unused)] impl Signature { - fn from_cose(bytes: &[u8]) -> Result { + pub(crate) fn from_cose(bytes: &[u8]) -> Result { let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(Signature(cose_sign1)) } - fn to_cose(&self) -> Result> { + pub(crate) fn to_cose(&self) -> Result> { self.0 .clone() .to_vec() .map_err(|_| SignatureError::InvalidSignature.into()) } - fn inner(&self) -> &CoseSign1 { + pub(crate) fn inner(&self) -> &CoseSign1 { &self.0 } - fn namespace(&self) -> Result { + pub(crate) fn namespace(&self) -> Result { let mut namespace = None; for (key, value) in &self.0.protected.header.rest { if let Label::Int(key) = key { @@ -409,12 +331,26 @@ impl Signature { let namespace: i128 = namespace.into(); SigningNamespace::try_from_i64(namespace as i64) } + + pub(crate) fn content_type(&self) -> Result { + if let RegisteredLabel::Assigned(content_format) = self + .0 + .protected + .header + .content_type + .clone() + .ok_or(CryptoError::from(SignatureError::InvalidSignature))? + { + Ok(content_format) + } else { + Err(SignatureError::InvalidSignature.into()) + } + } } /// A signed object has a cryptographical attestation to a (namespace, data) pair. The namespace and /// data are included in the signature object. -#[allow(unused)] -struct SignedObject(CoseSign1); +pub struct SignedObject(pub(crate) CoseSign1); impl From for SignedObject { fn from(cose_sign1: CoseSign1) -> Self { @@ -422,26 +358,43 @@ impl From for SignedObject { } } +impl SignedObject { + pub fn content_type(&self) -> Result { + if let RegisteredLabel::Assigned(content_format) = self + .0 + .protected + .header + .content_type + .clone() + .ok_or(CryptoError::from(SignatureError::InvalidSignature))? + { + Ok(content_format) + } else { + Err(SignatureError::InvalidSignature.into()) + } + } +} + #[allow(unused)] impl SignedObject { - fn from_cose(bytes: &[u8]) -> Result { + pub(crate) fn from_cose(bytes: &[u8]) -> Result { let cose_sign1 = CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; Ok(SignedObject(cose_sign1)) } - fn to_cose(&self) -> Result> { + pub(crate) fn to_cose(&self) -> Result> { self.0 .clone() .to_vec() .map_err(|_| SignatureError::InvalidSignature.into()) } - fn inner(&self) -> &CoseSign1 { + pub(crate) fn inner(&self) -> &CoseSign1 { &self.0 } - fn namespace(&self) -> Result { + pub(crate) fn namespace(&self) -> Result { let mut namespace = None; for (key, value) in &self.0.protected.header.rest { if let Label::Int(key) = key { @@ -456,11 +409,14 @@ impl SignedObject { let Some(namespace) = namespace.as_integer() else { return Err(SignatureError::InvalidNamespace.into()); }; - let namespace: i128 = namespace.into(); - SigningNamespace::try_from_i64(namespace as i64) + SigningNamespace::try_from_i64( + namespace + .try_into() + .map_err(|_| SignatureError::InvalidNamespace)?, + ) } - fn payload(&self) -> Result> { + pub fn payload(&self) -> Result> { self.0 .payload .as_ref() @@ -471,146 +427,24 @@ impl SignedObject { #[cfg(test)] mod tests { - use super::*; - - const SIGNING_KEY: &[u8] = &[ - 166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, - 3, 39, 4, 130, 1, 2, 35, 88, 32, 31, 72, 18, 5, 81, 182, 75, 229, 106, 91, 174, 171, 136, - 48, 87, 10, 231, 220, 24, 134, 42, 189, 54, 217, 51, 206, 23, 49, 140, 165, 23, 125, 32, 6, - ]; - const VERIFYING_KEY: &[u8] = &[ - 166, 1, 1, 2, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, 52, 227, 197, - 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 40, 62, 139, 254, 182, 152, 40, 135, 232, 175, 93, - 191, 16, 31, 208, 54, 5, 136, 208, 14, 159, 199, 204, 209, 11, 161, 171, 213, 128, 101, - 224, 160, - ]; - /// Uses the ´SigningNamespace::EncryptionMetadata´ namespace, "Test message" as data - const SIGNATURE: &[u8] = &[ - 132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, - 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 246, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, - 179, 178, 83, 190, 102, 200, 225, 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, - 208, 81, 42, 2, 87, 32, 84, 194, 144, 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, - 154, 204, 32, 185, 180, 143, 88, 57, 50, 73, 36, 74, 34, 132, 5, - ]; - const SIGNED_OBJECT: &[u8] = &[ - 132, 88, 27, 163, 1, 39, 4, 80, 46, 133, 42, 0, 247, 84, 68, 139, 178, 110, 111, 186, 249, - 52, 227, 197, 58, 0, 1, 56, 127, 1, 160, 76, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, - 103, 101, 88, 64, 187, 108, 86, 209, 43, 187, 42, 117, 179, 178, 83, 190, 102, 200, 225, - 126, 67, 16, 69, 6, 60, 119, 8, 201, 141, 57, 44, 72, 208, 81, 42, 2, 87, 32, 84, 194, 144, - 84, 0, 33, 47, 67, 64, 21, 200, 222, 33, 123, 50, 154, 204, 32, 185, 180, 143, 88, 57, 50, - 73, 36, 74, 34, 132, 5, - ]; - - #[test] - fn test_signature_using_test_vectors() { - let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let signature = Signature::from_cose(SIGNATURE).unwrap(); - - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - - assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); - assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); - assert_eq!(signature.to_cose().unwrap(), SIGNATURE); - - assert!(verifying_key.verify_signature(&namespace, &signature, data)); - } - - #[test] - fn test_signed_object_using_test_vectors() { - let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); - - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - - assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); - assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); - assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); - - let payload = verifying_key - .get_verified_payload(&namespace, &signed_object) - .unwrap(); - assert_eq!(payload, data); - } + use coset::CoseSign1Builder; - #[test] - fn test_sign_detached_roundtrip() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - - let signature = signing_key.sign_detached(&namespace, data); - assert!(verifying_key.verify_signature(&namespace, &signature, data)); - } - - #[test] - fn test_sign_roundtrip() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - let signed_object = signing_key.sign(&namespace, data).unwrap(); - let payload = verifying_key - .get_verified_payload(&namespace, &signed_object) - .unwrap(); - assert_eq!(payload, data); - } - - #[test] - fn test_changed_payload_fails() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - - let signature = signing_key.sign_detached(&namespace, data); - assert!(!verifying_key.verify_signature(&namespace, &signature, b"Test message 2")); - } - - #[test] - fn test_changed_namespace_fails() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - let other_namespace = SigningNamespace::Test; - - let signature = signing_key.sign_detached(&namespace, data); - assert!(!verifying_key.verify_signature(&other_namespace, &signature, data)); - } - - #[test] - fn test_changed_namespace_fails_signed_object() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::EncryptionMetadata; - let other_namespace = SigningNamespace::Test; - let signed_object = signing_key.sign(&namespace, data).unwrap(); - assert!(verifying_key - .get_verified_payload(&other_namespace, &signed_object) - .is_err()); - } + use super::*; #[test] fn test_cose_roundtrip_signature() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let cose = - signing_key.sign_detached(&SigningNamespace::EncryptionMetadata, b"Test message"); - let cose = cose.to_cose().unwrap(); + let sig = CoseSign1Builder::new().build(); + let signature = Signature(sig.clone()); + let cose = signature.to_cose().unwrap(); let parsed_cose = Signature::from_cose(&cose).unwrap(); assert_eq!(cose, parsed_cose.to_cose().unwrap()); } #[test] fn test_cose_roundtrip_signed_object() { - let signing_key = SigningKey::make_ed25519().unwrap(); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); let cose = signing_key - .sign(&SigningNamespace::EncryptionMetadata, b"Test message") + .sign(&"test", &SigningNamespace::ExampleNamespace) .unwrap(); let cose = cose.to_cose().unwrap(); let parsed_cose = SignedObject::from_cose(&cose).unwrap(); @@ -619,7 +453,7 @@ mod tests { #[test] fn test_cose_roundtrip_encode_signing() { - let signing_key = SigningKey::make_ed25519().unwrap(); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); let cose = signing_key.to_cose().unwrap(); let parsed_key = SigningKey::from_cose(&cose).unwrap(); @@ -628,16 +462,4 @@ mod tests { parsed_key.to_cose().unwrap() ); } - - #[test] - fn test_cose_roundtrip_encode_verifying() { - let signing_key = SigningKey::make_ed25519().unwrap(); - let cose = signing_key.to_verifying_key().to_cose().unwrap(); - let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); - - assert_eq!( - signing_key.to_verifying_key().to_cose().unwrap(), - parsed_key.to_cose().unwrap() - ); - } } diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs index 91e001290..ea616c35a 100644 --- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -298,6 +298,12 @@ impl TryFrom<&mut [u8]> for SymmetricCryptoKey { } } +impl From for SymmetricCryptoKey { + fn from(key: Aes256CbcHmacKey) -> Self { + SymmetricCryptoKey::Aes256CbcHmacKey(key) + } +} + impl CryptoKey for SymmetricCryptoKey {} // We manually implement these to make sure we don't print any sensitive data diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 70ee3aff3..e7c5526ed 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -32,6 +32,7 @@ mod store; pub use store::{KeyStore, KeyStoreContext}; mod cose; mod signing; +pub use signing::*; mod traits; mod xchacha20; pub use traits::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds}; diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs new file mode 100644 index 000000000..c9da88667 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/claims.rs @@ -0,0 +1,100 @@ +use serde::{Deserialize, Serialize}; + +use super::SigningNamespace; +use crate::{ + keys::Fingerprintable, AsymmetricPublicCryptoKey, CryptoError, FingerprintableKey, + PublicKeyFingerprint, SignedObject, SigningKey, VerifyingKey, +}; + +/// The non-serialized version of `PublicKeyOwnershipClaim` +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct PublicKeyOwnershipClaim { + pub(crate) fingerprint: PublicKeyFingerprint, +} + +impl PublicKeyOwnershipClaim { + pub(crate) fn for_public_key(public_key: &impl FingerprintableKey) -> Self { + Self { + fingerprint: public_key.fingerprint(), + } + } +} + +/// A user or org shall only have one long-term cryptographic identity. This is the signing key. A +/// user also needs to receive messages asymmetrically shared to them. Thus, an object tying the +/// signing key to the asymmetric encryption public key is needed. A signed public key ownership +/// claim represents a claim by a signing key that it owns a specific public encryption key. This is +/// used to tie the cryptographic identity (signing) to the encryption receiving identity +/// (asymmetric encryption key). +/// +/// 1. Initially, Alice knows Bob's cryptographic identity (verifying key). +/// 2. Alice wants to send a message to Bob using his public encryption key. +/// 3. Alice gets Bob's public encryption key from the server, along with the +/// [`SignedPublicKeyOwnershipClaim`]. +/// 4. Alice verifies the claim using Bob's verifying key that she trusts. +/// ``` +/// use rand::rngs::OsRng; +/// use bitwarden_crypto::{AsymmetricCryptoKey, CryptoError, SigningKey, VerifyingKey, SignedPublicKeyOwnershipClaim, SignatureAlgorithm}; +/// +/// // Initial setup +/// let bob_signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); +/// let bob_verifying_key = bob_signing_key.to_verifying_key(); +/// let bob_public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key(); +/// +/// // Alice trusts Bob's verifying key - this becomes Bob's cryptographic identity. +/// let bob_claim = SignedPublicKeyOwnershipClaim::make_claim_with_key(&bob_public_key, &bob_signing_key).unwrap(); +/// // Alice downloads Bob's public key from the server. +/// // Alice verifies the claim using Bob's verifying key. +/// assert!(bob_claim.verify_claim(&bob_public_key, &bob_verifying_key).unwrap()); +/// // Alice can now send a message to Bob using his public encryption key. +pub struct SignedPublicKeyOwnershipClaim(Vec); + +impl SignedPublicKeyOwnershipClaim { + /// Creates a new `SignedPublicKeyOwnershipClaim` for the provided public key and signing key. + pub fn make_claim_with_key( + public_key: &AsymmetricPublicCryptoKey, + signing_key: &SigningKey, + ) -> Result { + let claim = PublicKeyOwnershipClaim::for_public_key(public_key); + let signature = signing_key.sign(&claim, &SigningNamespace::PublicKeyOwnershipClaim)?; + Ok(Self(signature.to_cose()?)) + } + + /// Verifies the signed claim using the provided public key and verifying key. + pub fn verify_claim( + &self, + public_key: &AsymmetricPublicCryptoKey, + verifying_key: &VerifyingKey, + ) -> Result { + let signed_object = SignedObject::from_cose(&self.0)?; + let claim: PublicKeyOwnershipClaim = verifying_key + .get_verified_payload(&signed_object, &SigningNamespace::PublicKeyOwnershipClaim)?; + Ok(public_key.verify_fingerprint(&claim.fingerprint)) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self(bytes.to_vec())) + } +} + +#[cfg(test)] +mod tests { + use rand::rngs::OsRng; + + use super::*; + use crate::{AsymmetricCryptoKey, SignatureAlgorithm}; + + #[test] + fn test_public_key_ownership_claim() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key(); + let claim = + SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signing_key).unwrap(); + assert!(claim.verify_claim(&public_key, &verifying_key).unwrap()); + } +} diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 4025a6f44..b9cdfd7fc 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -1,27 +1,8 @@ -use crate::{error::SignatureError, CryptoError}; +//! Note -/// Signing is domain-separated within bitwarden, to prevent cross protocol attacks. -/// -/// A new signed entity or protocol shall use a new signing namespace. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SigningNamespace { - #[allow(dead_code)] - EncryptionMetadata = 1, - #[cfg(test)] - Test = -1, -} - -impl SigningNamespace { - pub fn as_i64(&self) -> i64 { - *self as i64 - } - - pub fn try_from_i64(value: i64) -> Result { - match value { - 1 => Ok(Self::EncryptionMetadata), - #[cfg(test)] - -1 => Ok(Self::Test), - _ => Err(SignatureError::InvalidNamespace.into()), - } - } -} +mod claims; +pub use claims::SignedPublicKeyOwnershipClaim; +mod namespace; +pub use namespace::SigningNamespace; +mod sign; +pub use sign::*; diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs new file mode 100644 index 000000000..b94218475 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -0,0 +1,28 @@ +use crate::{error::SignatureError, CryptoError}; + +/// Signing is domain-separated within bitwarden, to prevent cross protocol attacks. +/// +/// A new signed entity or protocol shall use a new signing namespace. Generally, this means +/// that a signing namespace has exactly one associated valid message struct. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SigningNamespace { + /// The namespace for + /// [`PublicKeyOwnershipClaim`](crate::signing::claims::PublicKeyOwnershipClaim). + PublicKeyOwnershipClaim = 1, + /// This namespace is only used in tests and documentation. + ExampleNamespace = -1, +} + +impl SigningNamespace { + pub fn as_i64(&self) -> i64 { + *self as i64 + } + + pub fn try_from_i64(value: i64) -> Result { + match value { + 1 => Ok(Self::PublicKeyOwnershipClaim), + -1 => Ok(Self::ExampleNamespace), + _ => Err(SignatureError::InvalidNamespace.into()), + } + } +} diff --git a/crates/bitwarden-crypto/src/signing/sign.rs b/crates/bitwarden-crypto/src/signing/sign.rs new file mode 100644 index 000000000..33b07ff3c --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/sign.rs @@ -0,0 +1,588 @@ +//! Signing is used to assert integrity of a message to others or to oneself. +//! +//! Signing and signature verification operations are divided into three layers here: +//! - (public) High-level: Give a struct, namespace, and get a signed object or signature + +//! serialized message. Purpose: Serialization should not be decided by the consumer of this +//! interface, but rather by the signing implementation. Each consumer shouldn't have to make the +//! decision on how to serialize. Further, the serialization format is written to the signature +//! object, and verified. +//! - Mid-level: Give a byte array, content format, namespace, and get a signed object or signature. +//! Purpose: All signatures should be domain-separated, so that any proofs only need to consider +//! the allowed messages under the current namespace, and cross-protocol attacks are not possible. +//! - Low-level: Give a byte array, and get a signature. Purpose: This just implements the signing +//! of byte arrays. Digital signature schemes generally just care about a set of input bytes to +//! sign; and this operation implements that per-supported digital signature scheme. To add +//! support for a new scheme, only this operation needs to be implemented for the new signing key +//! type. +//! +//! Further, there are two kinds of signing operations supported here: +//! - Sign: Create a signed object that contains the payload. Purpose: If only one signature is +//! needed for an object then it is simpler to keep the signature and payload together in one +//! blob, so they cannot be separated. +//! - Sign detached: Create a signature that does not contain the payload; but the serialized +//! payload is returned. Purpose: If multiple signatures are needed for one object, then sign +//! detached can be used. + +use ciborium::value::Integer; +use coset::iana::CoapContentFormat; +use ed25519_dalek::Signer; +use serde::{de::DeserializeOwned, Serialize}; + +use super::SigningNamespace; +use crate::{ + cose::SIGNING_NAMESPACE, error::SignatureError, CryptoError, RawSigningKey, RawVerifyingKey, + Signature, SignedObject, SigningKey, VerifyingKey, +}; + +impl SigningKey { + /// Signs the given payload with the signing key, under a given [`SigningNamespace`]. + /// This returns a [`Signature`] object, that does not contain the payload. + /// The payload must be stored separately, and needs to be provided when verifying the + /// signature. + /// + /// This should be used when multiple signers are required, or when signatures need to be + /// replaceable without re-uploading the object, or if the signed object should be parseable + /// by the server side, without the use of COSE on the server. + /// ``` + /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey}; + /// use serde::{Serialize, Deserialize}; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let (signature, serialized_message) = signing_key.sign_detached(&message, &namespace).unwrap(); + /// // Verification + /// let verifying_key = signing_key.to_verifying_key(); + /// assert!(verifying_key.verify_signature(&serialized_message.as_ref(), &namespace, &signature)); + /// ``` + #[allow(unused)] + pub fn sign_detached( + &self, + message: &Message, + namespace: &SigningNamespace, + ) -> Result<(Signature, SerializedMessage), CryptoError> { + let message = encode_message(message)?; + Ok((self.sign_detached_bytes(&message, namespace), message)) + } + + /// Given a serialized message, signature, this counter-signs the message. That is, if multiple + /// parties want to sign the same message, one party creates the initial message, and the + /// other parties then counter-sign it, and submit their signatures. This can be done as + /// follows: ``` + /// let alice_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// let bob_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let (signature, serialized_message) = alice_key.sign_detached(&message, + /// &namespace).unwrap();\ // Alice shares (signature, serialized_message) with Bob. + /// // Bob verifies the contents of serialized_message using application logic, then signs it: + /// let (bob_signature, serialized_message) = bob_key.counter_sign(&serialized_message, + /// &signature, &namespace).unwrap(); ``` + #[allow(unused)] + pub fn counter_sign_detached( + &self, + serialized_message_bytes: Vec, + 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 { + serialized_message_bytes, + content_type: initial_signature.content_type()?, + }, + namespace, + )) + } + + /// Signs the given payload with the signing key, under a given namespace. + /// This is is the underlying implementation of the `sign_detached` method, and takes + /// a raw byte array as input. + fn sign_detached_bytes( + &self, + message: &SerializedMessage, + namespace: &SigningNamespace, + ) -> Signature { + Signature::from( + coset::CoseSign1Builder::new() + .protected( + coset::HeaderBuilder::new() + .algorithm(self.cose_algorithm()) + .key_id((&self.id).into()) + .content_format(message.content_type) + .value( + SIGNING_NAMESPACE, + ciborium::Value::Integer(Integer::from(namespace.as_i64())), + ) + .build(), + ) + .create_detached_signature(&message.serialized_message_bytes, &[], |pt| { + self.sign_raw(pt) + }) + .build(), + ) + } + + /// Signs the given payload with the signing key, under a given namespace. + /// This returns a [`SignedObject`] object, that contains the payload. + /// The payload is included in the signature, and does not need to be provided when verifying + /// the signature. + /// + /// This should be used when only one signer is required, so that only one object needs to be + /// kept track of. + /// ``` + /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey}; + /// use serde::{Serialize, Deserialize}; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let signed_object = signing_key.sign(&message, &namespace).unwrap(); + /// // The signed object can be verified using the verifying key: + /// let verifying_key = signing_key.to_verifying_key(); + /// let payload: TestMessage = verifying_key.get_verified_payload(&signed_object, &namespace).unwrap(); + /// assert_eq!(payload, message); + /// ``` + pub fn sign( + &self, + message: &Message, + namespace: &SigningNamespace, + ) -> Result { + let message = encode_message(message)?; + self.sign_bytes(&message, namespace) + } + + /// Signs the given payload with the signing key, under a given namespace. + /// This is is the underlying implementation of the `sign` method, and takes + /// a raw byte array as input. + fn sign_bytes( + &self, + serialized_message: &SerializedMessage, + namespace: &SigningNamespace, + ) -> Result { + let cose_sign1 = coset::CoseSign1Builder::new() + .protected( + coset::HeaderBuilder::new() + .algorithm(self.cose_algorithm()) + .key_id((&self.id).into()) + .content_format(serialized_message.content_type) + .value( + SIGNING_NAMESPACE, + ciborium::Value::Integer(Integer::from(namespace.as_i64())), + ) + .build(), + ) + .payload(serialized_message.serialized_message_bytes.clone()) + .create_signature(&[], |pt| self.sign_raw(pt)) + .build(); + Ok(SignedObject(cose_sign1)) + } + + /// Signs the given byte array with the signing key. + /// This should never be used directly, but only through the `sign` method, to enforce + /// strong domain separation of the signatures. + fn sign_raw(&self, data: &[u8]) -> Vec { + match &self.inner { + RawSigningKey::Ed25519(key) => key.sign(data).to_bytes().to_vec(), + } + } +} + +/// A message (struct), serialized to a byte array, along with the content format of the bytes. +pub struct SerializedMessage { + serialized_message_bytes: Vec, + content_type: CoapContentFormat, +} + +impl AsRef<[u8]> for SerializedMessage { + fn as_ref(&self) -> &[u8] { + &self.serialized_message_bytes + } +} + +impl SerializedMessage { + pub fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { + SerializedMessage { + serialized_message_bytes: bytes, + content_type, + } + } +} + +impl VerifyingKey { + /// Verifies the signature of the given serialized message bytes, created by + /// [`SigningKey::sign_detached`], for the given namespace. The namespace must match the one + /// used to create the signature. + #[allow(unused)] + pub fn verify_signature( + &self, + serialized_message_bytes: &[u8], + namespace: &SigningNamespace, + signature: &Signature, + ) -> bool { + let Some(_alg) = &signature.inner().protected.header.alg else { + return false; + }; + + let Ok(signature_namespace) = signature.namespace() else { + return false; + }; + if signature_namespace != *namespace { + return false; + } + + signature + .inner() + .verify_detached_signature(serialized_message_bytes, &[], |sig, data| { + self.verify_raw(sig, data) + }) + .is_ok() + } + + /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given + /// namespace and returns the deserialized payload, if the signature is valid. + pub fn get_verified_payload( + &self, + signed_object: &SignedObject, + namespace: &SigningNamespace, + ) -> Result { + let payload_bytes = self.get_verified_payload_bytes(signed_object, namespace)?; + decode_message(&SerializedMessage { + serialized_message_bytes: payload_bytes, + content_type: signed_object.content_type()?, + }) + } + + /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given + /// namespace and returns the raw payload bytes, if the signature is valid. + fn get_verified_payload_bytes( + &self, + signed_object: &SignedObject, + namespace: &SigningNamespace, + ) -> Result, CryptoError> { + let Some(_alg) = &signed_object.inner().protected.header.alg else { + return Err(SignatureError::InvalidSignature.into()); + }; + + let signature_namespace = signed_object.namespace()?; + if signature_namespace != *namespace { + return Err(SignatureError::InvalidNamespace.into()); + } + + signed_object + .inner() + .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?; + signed_object.payload() + } + + /// Verifies the signature of the given data, for the given namespace. + /// This should never be used directly, but only through the `verify` method, to enforce + /// strong domain separation of the signatures. + fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<(), CryptoError> { + match &self.inner { + RawVerifyingKey::Ed25519(key) => { + let sig = ed25519_dalek::Signature::from_bytes( + signature + .try_into() + .map_err(|_| SignatureError::InvalidSignature)?, + ); + key.verify_strict(data, &sig) + .map_err(|_| SignatureError::InvalidSignature.into()) + } + } + } +} + +fn encode_message(message: &Message) -> Result { + let mut buffer = Vec::new(); + ciborium::ser::into_writer(message, &mut buffer).map_err(|_| CryptoError::CoseEncodingError)?; + Ok(SerializedMessage { + serialized_message_bytes: buffer, + content_type: CoapContentFormat::Cbor, + }) +} + +fn decode_message( + message: &SerializedMessage, +) -> Result { + if message.content_type != CoapContentFormat::Cbor { + return Err(CryptoError::CoseEncodingError); + } + + let decoded = ciborium::de::from_reader(message.serialized_message_bytes.as_slice()) + .map_err(|_| CryptoError::CoseEncodingError)?; + Ok(decoded) +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::SignatureAlgorithm; + + /// The function used to create the test vectors below, and can be used to re-generate them. + /// Once rolled out to user accounts, this function can be removed, because at that point we + /// cannot introduce format-breaking changes anymore. + #[test] + fn make_test_vectors() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let namespace = SigningNamespace::ExampleNamespace; + + let (signature, serialized_message) = signing_key + .sign_detached(&test_message, &namespace) + .unwrap(); + let signed_object = signing_key.sign(&test_message, &namespace).unwrap(); + + println!( + "const SIGNING_KEY: &[u8] = &{:?};", + signing_key.to_cose().unwrap() + ); + println!( + "const VERIFYING_KEY: &[u8] = &{:?};", + verifying_key.to_cose().unwrap() + ); + println!( + "const SIGNATURE: &[u8] = &{:?};", + signature.to_cose().unwrap() + ); + println!( + "const SERIALIZED_MESSAGE: &[u8] = &{:?};", + serialized_message.serialized_message_bytes + ); + println!( + "const SIGNED_OBJECT: &[u8] = &{:?};", + signed_object.to_cose().unwrap() + ); + } + + const SIGNING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155, + 16, 3, 39, 4, 130, 1, 2, 35, 88, 32, 114, 65, 45, 133, 77, 188, 130, 57, 89, 250, 113, 125, + 108, 138, 255, 68, 3, 202, 189, 96, 31, 218, 197, 24, 35, 127, 52, 168, 232, 85, 95, 199, + 32, 6, + ]; + const VERIFYING_KEY: &[u8] = &[ + 166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155, + 16, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 91, 255, 95, 169, 53, 21, 222, 134, 102, 103, 105, + 224, 58, 210, 82, 121, 141, 60, 76, 68, 9, 26, 242, 215, 111, 150, 228, 154, 141, 143, 108, + 38, + ]; + const SIGNATURE: &[u8] = &[ + 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, + 105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 246, 88, 64, 110, 91, 1, 209, 74, + 57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243, 61, 167, + 248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168, 10, 173, + 145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85, 247, 99, + 14, + ]; + const SERIALIZED_MESSAGE: &[u8] = &[ + 161, 102, 102, 105, 101, 108, 100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, + 103, 101, + ]; + const SIGNED_OBJECT: &[u8] = &[ + 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, + 105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 85, 161, 102, 102, 105, 101, 108, + 100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 88, 64, 110, 91, 1, + 209, 74, 57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243, + 61, 167, 248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168, + 10, 173, 145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85, + 247, 99, 14, + ]; + + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct TestMessage { + field1: String, + } + + #[test] + fn test_vectors() { + let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let signature = Signature::from_cose(SIGNATURE).unwrap(); + let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); + + assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); + assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); + assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); + + assert_eq!( + signature.namespace().unwrap(), + SigningNamespace::ExampleNamespace + ); + assert_eq!(signature.content_type().unwrap(), CoapContentFormat::Cbor); + assert_eq!(signature.to_cose().unwrap(), SIGNATURE); + + assert_eq!(signed_object.payload().unwrap(), SERIALIZED_MESSAGE); + assert_eq!( + signed_object.namespace().unwrap(), + SigningNamespace::ExampleNamespace + ); + assert_eq!( + signed_object.content_type().unwrap(), + CoapContentFormat::Cbor + ); + assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); + + let verified_payload: TestMessage = verifying_key + .get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace) + .unwrap(); + assert_eq!( + verified_payload, + TestMessage { + field1: "Test message".to_string() + } + ); + assert!(verifying_key.verify_signature( + SERIALIZED_MESSAGE, + &SigningNamespace::ExampleNamespace, + &signature + )); + } + + #[test] + fn test_sign_detached_roundtrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = TestMessage { + field1: "Test message".to_string(), + }; + let namespace = SigningNamespace::ExampleNamespace; + let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); + assert!(verifying_key.verify_signature( + &serialized_message.serialized_message_bytes, + &namespace, + &signature + )); + let decoded_message: TestMessage = decode_message(&serialized_message).unwrap(); + assert_eq!(decoded_message, data); + } + + #[test] + fn test_sign_roundtrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = "Test message".to_string(); + let namespace = SigningNamespace::ExampleNamespace; + let signed_object = signing_key.sign(&data, &namespace).unwrap(); + let payload: String = verifying_key + .get_verified_payload(&signed_object, &namespace) + .unwrap(); + assert_eq!(payload, data); + } + + #[test] + fn test_countersign_roundtrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = "Test message".to_string(); + let namespace = SigningNamespace::ExampleNamespace; + let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); + let countersignature = signing_key + .counter_sign_detached( + serialized_message.serialized_message_bytes.clone(), + &signature, + &namespace, + ) + .unwrap(); + assert!(verifying_key.verify_signature( + &serialized_message.serialized_message_bytes, + &namespace, + &countersignature + )); + } + + #[test] + fn test_changed_payload_fails() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = "Test message".to_string(); + let namespace = SigningNamespace::ExampleNamespace; + + let (signature, mut serialized_message) = + signing_key.sign_detached(&data, &namespace).unwrap(); + let modified_message = serialized_message + .serialized_message_bytes + .get_mut(0) + .unwrap(); + *modified_message = 0xFF; + assert!(!verifying_key.verify_signature( + &serialized_message.serialized_message_bytes, + &namespace, + &signature + )); + } + + #[test] + fn test_changed_namespace_fails() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Test message"; + let namespace = SigningNamespace::ExampleNamespace; + let other_namespace = SigningNamespace::PublicKeyOwnershipClaim; + + let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); + assert!(!verifying_key.verify_signature( + &serialized_message.serialized_message_bytes, + &other_namespace, + &signature + )); + assert!(verifying_key.verify_signature( + &serialized_message.serialized_message_bytes, + &namespace, + &signature + )); + } + + #[test] + fn test_changed_namespace_fails_signed_object() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let data = b"Test message"; + let namespace = SigningNamespace::ExampleNamespace; + let other_namespace = SigningNamespace::PublicKeyOwnershipClaim; + let signed_object = signing_key.sign(data, &namespace).unwrap(); + assert!(verifying_key + .get_verified_payload::>(&signed_object, &other_namespace) + .is_err()); + assert!(verifying_key + .get_verified_payload::>(&signed_object, &namespace) + .is_ok()); + } + + #[test] + fn test_encode_decode_message() { + let message = TestMessage { + field1: "Hello".to_string(), + }; + let encoded = encode_message(&message).unwrap(); + let decoded = decode_message(&encoded).unwrap(); + assert_eq!(message, decoded); + } +} diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 261067af8..7d2f7d321 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -3,13 +3,14 @@ use std::{ sync::{RwLockReadGuard, RwLockWriteGuard}, }; +use serde::Serialize; use zeroize::Zeroizing; use super::KeyStoreInner; use crate::{ derive_shareable_key, error::UnsupportedOperation, store::backend::StoreBackend, - AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, SymmetricCryptoKey, - UnsignedSharedKey, + AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature, + SignatureAlgorithm, SignedObject, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] @@ -39,7 +40,11 @@ use crate::{ /// # pub enum AsymmKeyId { /// # UserPrivate, /// # } -/// # pub Ids => SymmKeyId, AsymmKeyId; +/// # #[signing] +/// # pub enum SigningKeyId { +/// # UserSigning, +/// # } +/// # pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId; /// # } /// struct Data { /// key: EncString, @@ -66,6 +71,7 @@ pub struct KeyStoreContext<'a, Ids: KeyIds> { pub(super) local_symmetric_keys: Box>, 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 +110,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 @@ -244,6 +251,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 +264,14 @@ 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 @@ -287,6 +307,11 @@ impl KeyStoreContext<'_, Ids> { self.get_asymmetric_key(key_id) } + #[deprecated(note = "This function should ideally never be used outside this crate")] + pub fn dangerous_get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { + self.get_signing_key(key_id) + } + fn get_symmetric_key(&self, key_id: Ids::Symmetric) -> Result<&SymmetricCryptoKey> { if key_id.is_local() { self.local_symmetric_keys.get(key_id) @@ -305,6 +330,16 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } + #[allow(unused)] + fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { + if key_id.is_local() { + self.local_signing_keys.get(key_id) + } else { + self.global_keys.get().signing_keys.get(key_id) + } + .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) + } + #[deprecated(note = "This function should ideally never be used outside this crate")] pub fn set_symmetric_key( &mut self, @@ -339,6 +374,16 @@ impl KeyStoreContext<'_, Ids> { Ok(()) } + #[deprecated(note = "This function should ideally never be used outside this crate")] + pub fn set_signing_key(&mut self, key_id: Ids::Signing, key: SigningKey) -> Result<()> { + if key_id.is_local() { + self.local_signing_keys.upsert(key_id, key); + } else { + self.global_keys.get_mut()?.signing_keys.upsert(key_id, key); + } + Ok(()) + } + pub(crate) fn decrypt_data_with_symmetric_key( &self, key: Ids::Symmetric, @@ -374,17 +419,62 @@ impl 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 { + let key = self.get_signing_key(key)?; + key.sign(message, namespace) + } + + /// Signs the given data using the specified signing key, for the given + /// [crate::SigningNamespace] and returns the signature and the serialized message. See + /// [crate::SigningKey::sign_detached] + #[allow(unused)] + pub(crate) fn sign_detached( + &self, + key: Ids::Signing, + message: &Message, + namespace: &crate::SigningNamespace, + ) -> Result<(Signature, Vec)> { + let key = self.get_signing_key(key)?; + let (signature, serialized_message) = key.sign_detached(message, namespace)?; + Ok((signature, serialized_message.as_ref().to_vec())) + } } #[cfg(test)] #[allow(deprecated)] mod tests { + use serde::{Deserialize, Serialize}; + use crate::{ store::{tests::DataView, KeyStore}, - traits::tests::{TestIds, TestSymmKey}, - Decryptable, Encryptable, SymmetricCryptoKey, + traits::tests::{TestIds, TestSigningKey, TestSymmKey}, + CryptoError, Decryptable, Encryptable, SignatureAlgorithm, SigningKey, SigningNamespace, + SymmetricCryptoKey, }; + #[test] + fn test_set_signing_key() { + let store: KeyStore = KeyStore::default(); + + // Generate and insert a key + let key_a0_id = TestSigningKey::A(0); + let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + store + .context_mut() + .set_signing_key(key_a0_id, key_a0) + .unwrap(); + } + #[test] fn test_set_keys_for_encryption() { let store: KeyStore = KeyStore::default(); @@ -448,4 +538,55 @@ mod tests { // Assert that the decrypted data is the same assert_eq!(decrypted1.0, decrypted2.0); } + + #[test] + fn test_signing() { + let store: KeyStore = KeyStore::default(); + + // Generate and insert a key + let key_a0_id = TestSigningKey::A(0); + let key_a0 = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = key_a0.to_verifying_key(); + store + .context_mut() + .set_signing_key(key_a0_id, key_a0) + .unwrap(); + + assert!(store.context().has_signing_key(key_a0_id)); + + // Sign some data with the key + #[derive(Serialize, Deserialize)] + struct TestData { + data: String, + } + let signed_object = store + .context() + .sign( + key_a0_id, + &TestData { + data: "Hello".to_string(), + }, + &SigningNamespace::ExampleNamespace, + ) + .unwrap(); + let payload: Result = + verifying_key.get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace); + assert!(payload.is_ok()); + + let (signature, serialized_message) = store + .context() + .sign_detached( + key_a0_id, + &TestData { + data: "Hello".to_string(), + }, + &SigningNamespace::ExampleNamespace, + ) + .unwrap(); + assert!(verifying_key.verify_signature( + &serialized_message, + &SigningNamespace::ExampleNamespace, + &signature + )); + } } diff --git a/crates/bitwarden-crypto/src/store/mod.rs b/crates/bitwarden-crypto/src/store/mod.rs index f447f58b2..ff133a9c4 100644 --- a/crates/bitwarden-crypto/src/store/mod.rs +++ b/crates/bitwarden-crypto/src/store/mod.rs @@ -58,7 +58,11 @@ pub use context::KeyStoreContext; /// pub enum AsymmKeyId { /// UserPrivate, /// } -/// pub Ids => SymmKeyId, AsymmKeyId; +/// #[signing] +/// pub enum SigningKeyId { +/// UserSigning, +/// } +/// pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId; /// } /// /// // Initialize the store and insert a test key @@ -101,6 +105,7 @@ impl 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/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs index ba997a5b0..854149424 100644 --- a/crates/bitwarden-crypto/src/traits/key_id.rs +++ b/crates/bitwarden-crypto/src/traits/key_id.rs @@ -2,7 +2,7 @@ use std::{fmt::Debug, hash::Hash}; use zeroize::ZeroizeOnDrop; -use crate::{AsymmetricCryptoKey, CryptoKey, SymmetricCryptoKey}; +use crate::{AsymmetricCryptoKey, CryptoKey, SigningKey, SymmetricCryptoKey}; /// Represents a key identifier that can be used to identify cryptographic keys in the /// key store. It is used to avoid exposing the key material directly in the public API. @@ -30,6 +30,7 @@ pub trait KeyId: pub trait KeyIds { type Symmetric: KeyId; type Asymmetric: KeyId; + type Signing: KeyId; } /// Just a small derive_like macro that can be used to generate the key identifier enums. @@ -49,7 +50,13 @@ pub trait KeyIds { /// pub enum AsymmKeyId { /// PrivateKey, /// } -/// pub Ids => SymmKeyId, AsymmKeyId; +/// +/// #[signing] +/// pub enum SigningKeyId { +/// SigningKey, +/// } +/// +/// pub Ids => SymmKeyId, AsymmKeyId, SigningKeyId; /// } #[macro_export] macro_rules! key_ids { @@ -63,7 +70,7 @@ macro_rules! key_ids { $(,)? } )+ - $ids_vis:vis $ids_name:ident => $symm_name:ident, $asymm_name:ident; + $ids_vis:vis $ids_name:ident => $symm_name:ident, $asymm_name:ident, $signing_name:ident; ) => { $( #[derive(std::fmt::Debug, Clone, Copy, std::hash::Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -88,11 +95,15 @@ macro_rules! key_ids { impl $crate::KeyIds for $ids_name { type Symmetric = $symm_name; type Asymmetric = $asymm_name; + type Signing = $signing_name; } }; ( @key_type symmetric ) => { $crate::SymmetricCryptoKey }; ( @key_type asymmetric ) => { $crate::AsymmetricCryptoKey }; + ( @key_type signing ) => { $crate::SigningKey }; + + ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant ( _ ) }; ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant (_) }; ( @variant_match $variant:ident ) => { $variant }; @@ -104,7 +115,7 @@ macro_rules! key_ids { #[cfg(test)] pub(crate) mod tests { use crate::{ - traits::tests::{TestAsymmKey, TestSymmKey}, + traits::tests::{TestAsymmKey, TestSigningKey, TestSymmKey}, KeyId, }; @@ -117,5 +128,9 @@ pub(crate) mod tests { assert!(!TestAsymmKey::A(0).is_local()); assert!(!TestAsymmKey::B.is_local()); assert!(TestAsymmKey::C("test").is_local()); + + assert!(!TestSigningKey::A(0).is_local()); + assert!(!TestSigningKey::B.is_local()); + assert!(TestSigningKey::C("test").is_local()); } } diff --git a/crates/bitwarden-crypto/src/traits/mod.rs b/crates/bitwarden-crypto/src/traits/mod.rs index 28b811e36..9110a7508 100644 --- a/crates/bitwarden-crypto/src/traits/mod.rs +++ b/crates/bitwarden-crypto/src/traits/mod.rs @@ -36,6 +36,14 @@ pub(crate) mod tests { C(&'static str), } - pub TestIds => TestSymmKey, TestAsymmKey; + #[signing] + pub enum TestSigningKey { + A(u8), + B, + #[local] + C(&'static str), + } + + pub TestIds => TestSymmKey, TestAsymmKey, TestSigningKey; } } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 46713b9eb..e3e2c2bb2 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -25,6 +25,7 @@ bitwarden-ipc = { workspace = true, features = ["wasm"] } bitwarden-ssh = { workspace = true, features = ["wasm"] } bitwarden-vault = { workspace = true, features = ["wasm"] } chrono = { workspace = true } +base64 = ">=0.22.1, <0.23.0" console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } js-sys = "0.3.68" diff --git a/crates/bitwarden-wasm-internal/src/crypto.rs b/crates/bitwarden-wasm-internal/src/crypto.rs index 044ffbcec..8bae18955 100644 --- a/crates/bitwarden-wasm-internal/src/crypto.rs +++ b/crates/bitwarden-wasm-internal/src/crypto.rs @@ -2,7 +2,7 @@ use bitwarden_core::{ client::encryption_settings::EncryptionSettingsError, mobile::crypto::{ InitOrgCryptoRequest, InitUserCryptoRequest, MakeKeyPairResponse, - VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, + MakeUserSigningKeysResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }, }; use bitwarden_crypto::CryptoError; @@ -52,4 +52,10 @@ impl CryptoClient { ) -> Result { self.0.verify_asymmetric_keys(request) } + + /// Generates a new signing key pair and encrypts the signing key with the provided symmetric + /// key. Crypto initialization not required. + pub fn make_signing_keys(&self) -> Result { + self.0.make_signing_keys() + } } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 85a34b49b..8c19efccc 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, Decryptable, EncString, - Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, SymmetricCryptoKey, - UnsignedSharedKey, + Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, SignatureAlgorithm, + SignedPublicKeyOwnershipClaim, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -265,6 +265,40 @@ impl PureCrypto { )?)? .to_encoded()) } + + pub fn verifying_key_for_signing_key( + signing_key: String, + wrapping_key: Vec, + ) -> 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(); + 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_public_key_ownership_claim( + claim: Vec, + public_key: Vec, + verifying_key: Vec, + ) -> Result { + let claim = SignedPublicKeyOwnershipClaim::from_bytes(claim.as_slice())?; + let public_key = AsymmetricPublicCryptoKey::from_der(public_key.as_slice())?; + let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?; + claim.verify_claim(&public_key, &verifying_key) + } } #[cfg(test)] From 6c725d27ed05df6f41e732f00c5dadaae8a7666b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:09:48 +0200 Subject: [PATCH 016/112] Remove incorrect login response change --- .../src/auth/api/response/identity_success_response.rs | 2 -- crates/bitwarden-core/src/auth/login/password.rs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs index 0fe7ddff2..07f9363f8 100644 --- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs @@ -15,8 +15,6 @@ pub struct IdentityTokenSuccessResponse { pub(crate) private_key: Option, #[serde(alias = "Key")] pub(crate) key: Option, - #[serde(alias = "userKeyEncryptedSigningKey")] - pub(crate) user_key_encrypted_signing_key: Option, #[serde(rename = "twoFactorToken")] two_factor_token: Option, #[serde(alias = "Kdf")] diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 7c4b7fbd1..4923a641f 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -52,17 +52,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()?; - let signing_key = r - .user_key_encrypted_signing_key - .clone() - .map(|s| s.parse()) - .transpose()?; client.internal.initialize_user_crypto_master_key( master_key, user_key, private_key, - signing_key, + None )?; } From fc895d87a615363d44872a3fd7191fa9a009b01a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:12:02 +0200 Subject: [PATCH 017/112] Fix comment --- crates/bitwarden-crypto/src/cose.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index b174cdf99..1da1e10a1 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -16,8 +16,8 @@ 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 -/// +// Labels +// /// The label used for the namespace ensuring strong domain separation when using signatures. pub(crate) const SIGNING_NAMESPACE: i64 = -80000; From 0e33fee414966d0f11d7d00c9f8d783e1eb9c0c9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:21:40 +0200 Subject: [PATCH 018/112] Cleanup --- .../api/response/identity_success_response.rs | 1 - .../src/keys/signing_crypto_key.rs | 35 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs index 07f9363f8..94ebe9445 100644 --- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs @@ -53,7 +53,6 @@ mod test { refresh_token: Default::default(), token_type: Default::default(), private_key: Default::default(), - user_key_encrypted_signing_key: Default::default(), key: Default::default(), two_factor_token: Default::default(), kdf: KdfType::default(), diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 222474482..1d2425691 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -252,24 +252,25 @@ impl VerifyingKey { return Err(CryptoError::InvalidKey); }; - let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); - if crv == iana::EllipticCurve::Ed25519.to_i64().into() { - let verifying_key_bytes: &[u8; 32] = x - .as_bytes() - .ok_or(CryptoError::InvalidKey)? - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let verifying_key = - ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) - .map_err(|_| CryptoError::InvalidKey)?; - Ok(VerifyingKey { - id: key_id, - inner: RawVerifyingKey::Ed25519(verifying_key), - }) - } else { - Err(CryptoError::InvalidKey) + if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) != + EllipticCurve::Ed25519.to_i64().into() + { + return Err(CryptoError::InvalidKey); } + + let verifying_key_bytes: &[u8; 32] = x + .as_bytes() + .ok_or(CryptoError::InvalidKey)? + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) + .map_err(|_| CryptoError::InvalidKey)?; + Ok(VerifyingKey { + id: key_id, + inner: RawVerifyingKey::Ed25519(verifying_key), + }) } _ => Err(CryptoError::InvalidKey), } From 6308cc57e2f9eb358cd9a1e33f1495bf066c00ce Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:24:54 +0200 Subject: [PATCH 019/112] Add comments --- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 8c19efccc..fcc6b2246 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -266,6 +266,8 @@ 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, From 971eb174278d379a0f6e7dec63a890010caa00a3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:26:16 +0200 Subject: [PATCH 020/112] Cleanup --- crates/bitwarden-crypto/src/signing/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index b9cdfd7fc..171efd52c 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -1,5 +1,3 @@ -//! Note - mod claims; pub use claims::SignedPublicKeyOwnershipClaim; mod namespace; From 12387f8ba106bd7a29a16673f287081f1223660e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:27:18 +0200 Subject: [PATCH 021/112] Remove unrelated change --- crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs index ea616c35a..91e001290 100644 --- a/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs @@ -298,12 +298,6 @@ impl TryFrom<&mut [u8]> for SymmetricCryptoKey { } } -impl From for SymmetricCryptoKey { - fn from(key: Aes256CbcHmacKey) -> Self { - SymmetricCryptoKey::Aes256CbcHmacKey(key) - } -} - impl CryptoKey for SymmetricCryptoKey {} // We manually implement these to make sure we don't print any sensitive data From f3eb5257dd6f92824d93bf4682a7977b50b31428 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:27:31 +0200 Subject: [PATCH 022/112] Cleanup --- crates/bitwarden-crypto/src/keys/signing_crypto_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 1d2425691..cf6d2af2a 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -1,7 +1,7 @@ use ciborium::{value::Integer, Value}; use coset::{ iana::{ - self, Algorithm, CoapContentFormat, EllipticCurve, EnumI64, KeyOperation, KeyType, + Algorithm, CoapContentFormat, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter, }, CborSerializable, CoseKey, CoseSign1, Label, RegisteredLabel, RegisteredLabelWithPrivate, From d6bbae0ed97ea97e3c77cd86657e8fb0a6265df6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 11:43:07 +0200 Subject: [PATCH 023/112] Fix init --- crates/bitwarden-core/src/mobile/crypto.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index a3c04957d..787e80686 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -136,6 +136,7 @@ pub async fn initialize_user_crypto( use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; let private_key: EncString = req.private_key.parse()?; + let signing_key: Option = req.signing_key.map(|s| s.parse()).transpose()?; match req.method { InitUserCryptoMethod::Password { password, user_key } => { @@ -146,7 +147,7 @@ pub async fn initialize_user_crypto( master_key, user_key, private_key, - None, + signing_key, )?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { @@ -164,7 +165,7 @@ pub async fn initialize_user_crypto( pin_key, pin_protected_user_key, private_key, - None, + signing_key, )?; } InitUserCryptoMethod::AuthRequest { @@ -186,7 +187,7 @@ pub async fn initialize_user_crypto( }; client .internal - .initialize_user_crypto_decrypted_key(user_key, private_key, None)?; + .initialize_user_crypto_decrypted_key(user_key, private_key, signing_key)?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -199,7 +200,7 @@ pub async fn initialize_user_crypto( client .internal - .initialize_user_crypto_decrypted_key(user_key, private_key, None)?; + .initialize_user_crypto_decrypted_key(user_key, private_key, signing_key)?; } InitUserCryptoMethod::KeyConnector { master_key, @@ -215,7 +216,7 @@ pub async fn initialize_user_crypto( master_key, user_key, private_key, - None, + signing_key, )?; } } From 2ab6620babec3e7c9975b76e6a0880bd0693a918 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:09:28 +0200 Subject: [PATCH 024/112] Cleanup --- .../bitwarden-core/src/auth/login/password.rs | 2 +- crates/bitwarden-core/src/mobile/crypto.rs | 16 ++-- .../src/keys/signing_crypto_key.rs | 93 ++++++++++--------- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 4923a641f..ae046524b 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -57,7 +57,7 @@ pub(crate) async fn login_password( master_key, user_key, private_key, - None + None, )?; } diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 787e80686..b62f4e99e 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -185,9 +185,11 @@ pub async fn initialize_user_crypto( auth_request_key, )?, }; - client - .internal - .initialize_user_crypto_decrypted_key(user_key, private_key, signing_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + private_key, + signing_key, + )?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -198,9 +200,11 @@ pub 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, private_key, signing_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + private_key, + signing_key, + )?; } InitUserCryptoMethod::KeyConnector { master_key, diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index cf6d2af2a..6adc3156b 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -112,17 +112,17 @@ impl SigningKey { /// Deserializes a COSE-formatted byte array into a signing key. pub fn from_cose(bytes: &[u8]) -> Result { let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) - else { + + let Some(algorithm) = cose_key.alg else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; KEY_ID_SIZE] = key_id + let key_id: [u8; KEY_ID_SIZE] = cose_key + .key_id .as_slice() .try_into() .map_err(|_| CryptoError::InvalidKey)?; let key_id: KeyId = key_id.into(); - - match (key_type, algorithm) { + match (cose_key.kty, algorithm) { (kty, alg) if kty == RegisteredLabel::Assigned(KeyType::OKP) && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => @@ -218,17 +218,16 @@ impl VerifyingKey { pub fn from_cose(bytes: &[u8]) -> Result { let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - let (key_id, Some(algorithm), key_type) = (cose_key.key_id, cose_key.alg, cose_key.kty) - else { + let Some(algorithm) = cose_key.alg else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; KEY_ID_SIZE] = key_id + let key_id: [u8; 16] = cose_key + .key_id .as_slice() .try_into() .map_err(|_| CryptoError::InvalidKey)?; let key_id: KeyId = key_id.into(); - - match (key_type, algorithm) { + match (cose_key.kty, algorithm) { (kty, alg) if kty == RegisteredLabel::Assigned(KeyType::OKP) && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => @@ -252,8 +251,8 @@ impl VerifyingKey { return Err(CryptoError::InvalidKey); }; - if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) != - EllipticCurve::Ed25519.to_i64().into() + if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) + != EllipticCurve::Ed25519.to_i64().into() { return Err(CryptoError::InvalidKey); } @@ -264,9 +263,8 @@ impl VerifyingKey { .as_slice() .try_into() .map_err(|_| CryptoError::InvalidKey)?; - let verifying_key = - ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) - .map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) + .map_err(|_| CryptoError::InvalidKey)?; Ok(VerifyingKey { id: key_id, inner: RawVerifyingKey::Ed25519(verifying_key), @@ -315,22 +313,27 @@ impl Signature { } pub(crate) fn namespace(&self) -> Result { - let mut namespace = None; - for (key, value) in &self.0.protected.header.rest { - if let Label::Int(key) = key { - if *key == SIGNING_NAMESPACE { - namespace.replace(value); + let namespace = self + .0 + .protected + .header + .rest + .iter() + .find_map(|(key, value)| { + if let Label::Int(key) = key { + if *key == SIGNING_NAMESPACE { + return value.as_integer(); + } } - } - } - let Some(namespace) = namespace else { - return Err(SignatureError::InvalidNamespace.into()); - }; - let Some(namespace) = namespace.as_integer() else { - return Err(SignatureError::InvalidNamespace.into()); - }; - let namespace: i128 = namespace.into(); - SigningNamespace::try_from_i64(namespace as i64) + None + }) + .ok_or(SignatureError::InvalidNamespace)?; + + SigningNamespace::try_from_i64( + i128::from(namespace) + .try_into() + .map_err(|_| SignatureError::InvalidNamespace)?, + ) } pub(crate) fn content_type(&self) -> Result { @@ -396,22 +399,24 @@ impl SignedObject { } pub(crate) fn namespace(&self) -> Result { - let mut namespace = None; - for (key, value) in &self.0.protected.header.rest { - if let Label::Int(key) = key { - if *key == SIGNING_NAMESPACE { - namespace.replace(value); + let namespace = self + .0 + .protected + .header + .rest + .iter() + .find_map(|(key, value)| { + if let Label::Int(key) = key { + if *key == SIGNING_NAMESPACE { + return value.as_integer(); + } } - } - } - let Some(namespace) = namespace else { - return Err(SignatureError::InvalidNamespace.into()); - }; - let Some(namespace) = namespace.as_integer() else { - return Err(SignatureError::InvalidNamespace.into()); - }; + None + }) + .ok_or(SignatureError::InvalidNamespace)?; + SigningNamespace::try_from_i64( - namespace + i128::from(namespace) .try_into() .map_err(|_| SignatureError::InvalidNamespace)?, ) From 17e0a07117354e9e7d491ab5df631af2be242eb2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:12:47 +0200 Subject: [PATCH 025/112] Fix sorting of dependencies --- crates/bitwarden-wasm-internal/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index e3e2c2bb2..d8bb9f603 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 } @@ -25,7 +26,6 @@ bitwarden-ipc = { workspace = true, features = ["wasm"] } bitwarden-ssh = { workspace = true, features = ["wasm"] } bitwarden-vault = { workspace = true, features = ["wasm"] } chrono = { workspace = true } -base64 = ">=0.22.1, <0.23.0" console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } js-sys = "0.3.68" From 8c9f00f4d18f17038a215312f7b7fd3debea7a5e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:37:01 +0200 Subject: [PATCH 026/112] Attempt to fix build --- .../src/main/java/com/bitwarden/myapplication/MainActivity.kt | 3 +++ crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift | 1 + 2 files changed, 4 insertions(+) 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 c6efcbeb0..e2facdc1b 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 @@ -253,6 +253,7 @@ class MainActivity : FragmentActivity() { kdfParams = kdf, email = EMAIL, privateKey = loginBody.PrivateKey, + signingKey = null, method = InitUserCryptoMethod.Password( password = PASSWORD, userKey = loginBody.Key ) @@ -337,6 +338,7 @@ class MainActivity : FragmentActivity() { kdfParams = kdf, email = EMAIL, privateKey = privateKey!!, + signingKey = null, method = InitUserCryptoMethod.DecryptedKey(decryptedUserKey = key) ) ) @@ -374,6 +376,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 7b642bce2..ee95b9986 100644 --- a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift +++ b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift @@ -192,6 +192,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: loginData.PrivateKey, + signingKey: nil, method: InitUserCryptoMethod.password( password: PASSWORD, userKey: loginData.Key From 06b26ba3ca5692b0329af847c756568358f3adb9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:44:24 +0200 Subject: [PATCH 027/112] Add more default nil values for signing key to fix ios build --- crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift index ee95b9986..ed0b1793b 100644 --- a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift +++ b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift @@ -250,6 +250,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: privateKey, + signingKey: nil, method: InitUserCryptoMethod.decryptedKey( decryptedUserKey: key ) @@ -276,6 +277,7 @@ struct ContentView: View { kdfParams: kdf, email: EMAIL, privateKey: privateKey, + signingKey: nil, method: InitUserCryptoMethod.pin(pin: PIN, pinProtectedUserKey: pinProtectedUserKey) )) } From a2e95e7fad5ae80e2e8854b01d46fc82268e6e95 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:45:53 +0200 Subject: [PATCH 028/112] Fix docs --- crates/bitwarden-crypto/src/keys/fingerprint.rs | 2 +- crates/bitwarden-crypto/src/signing/claims.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/fingerprint.rs b/crates/bitwarden-crypto/src/keys/fingerprint.rs index 1c7bc451f..baa1a12ca 100644 --- a/crates/bitwarden-crypto/src/keys/fingerprint.rs +++ b/crates/bitwarden-crypto/src/keys/fingerprint.rs @@ -67,7 +67,7 @@ fn derive_fingerprint(fingerprint_parts: Vec>) -> PublicKeyFingerprint { /// This function ensures an injective mapping of the inputs to the output hash. /// Concatenating the inputs does not work. For RSA this could mean that: -/// with data = [N,E], |nnnnnn|ee|, and |nnnnnnn|e| would both be valid interpretations of the +/// with data = \[N,E\], |nnnnnn|ee|, and |nnnnnnn|e| would both be valid interpretations of the /// concatenation of the bytes, and thus may lead to the same hash for different (N,E) pairs. /// /// This function hashes each input separately, concatenates the hashes, and then hashes the result. diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs index c9da88667..ffb197a8e 100644 --- a/crates/bitwarden-crypto/src/signing/claims.rs +++ b/crates/bitwarden-crypto/src/signing/claims.rs @@ -1,13 +1,10 @@ -use serde::{Deserialize, Serialize}; - use super::SigningNamespace; use crate::{ keys::Fingerprintable, AsymmetricPublicCryptoKey, CryptoError, FingerprintableKey, PublicKeyFingerprint, SignedObject, SigningKey, VerifyingKey, }; -/// The non-serialized version of `PublicKeyOwnershipClaim` -#[derive(Serialize, Deserialize, Debug)] +/// The non-serialized version of `SignedPublicKeyOwnershipClaim` pub(crate) struct PublicKeyOwnershipClaim { pub(crate) fingerprint: PublicKeyFingerprint, } From e6ab5ef5c9e7afee78fd7ea1c0a9141e307457f6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 12:54:51 +0200 Subject: [PATCH 029/112] Fix build --- crates/bitwarden-crypto/src/signing/claims.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs index ffb197a8e..2f1da5379 100644 --- a/crates/bitwarden-crypto/src/signing/claims.rs +++ b/crates/bitwarden-crypto/src/signing/claims.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use super::SigningNamespace; use crate::{ keys::Fingerprintable, AsymmetricPublicCryptoKey, CryptoError, FingerprintableKey, @@ -5,6 +7,7 @@ use crate::{ }; /// The non-serialized version of `SignedPublicKeyOwnershipClaim` +#[derive(Serialize, Deserialize)] pub(crate) struct PublicKeyOwnershipClaim { pub(crate) fingerprint: PublicKeyFingerprint, } From 48423092b2905bb92226f75e51cf9ceb28ebc71c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 13:03:02 +0200 Subject: [PATCH 030/112] Fix doc --- crates/bitwarden-crypto/src/signing/namespace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index b94218475..3ac8f2e41 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -7,7 +7,7 @@ use crate::{error::SignatureError, CryptoError}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SigningNamespace { /// The namespace for - /// [`PublicKeyOwnershipClaim`](crate::signing::claims::PublicKeyOwnershipClaim). + /// [`SignedPublicKeyOwnershipClaim`](crate::signing::claims::SignedPublicKeyOwnershipClaim). PublicKeyOwnershipClaim = 1, /// This namespace is only used in tests and documentation. ExampleNamespace = -1, From d6e6e99ffbdd3ab8ee2e5e9007f2dff8cf7b5510 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 13:51:17 +0200 Subject: [PATCH 031/112] Add tests to purecrypto --- .../bitwarden-core/src/auth/auth_request.rs | 4 +- .../src/keys/asymmetric_crypto_key.rs | 7 +- .../bitwarden-crypto/src/keys/device_key.rs | 3 +- .../src/keys/signing_crypto_key.rs | 2 +- crates/bitwarden-crypto/src/signing/claims.rs | 4 +- .../src/traits/encryptable.rs | 2 +- .../src/pure_crypto.rs | 117 ++++++++++++++++++ 7 files changed, 128 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 9033b7ff8..7435c52a3 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -31,9 +31,7 @@ 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::generate(&mut rng); + let key = AsymmetricCryptoKey::make(); let spki = key.to_public_der()?; diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index d7adb45a8..2a1a6f8b0 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -67,7 +67,12 @@ impl zeroize::ZeroizeOnDrop for AsymmetricCryptoKey {} impl AsymmetricCryptoKey { /// Generate a random AsymmetricCryptoKey (RSA-2048). - pub fn generate(rng: &mut R) -> Self { + pub fn make() -> Self { + use rand::rngs::OsRng; + Self::make_internal(&mut OsRng) + } + + fn make_internal(rng: &mut R) -> Self { let bits = 2048; Self { diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index 6574ea91b..f2f68ef9a 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -29,10 +29,9 @@ 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(); let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned(user_key, &device_private_key)?; diff --git a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs index 6adc3156b..89d239c7e 100644 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs @@ -22,7 +22,7 @@ use crate::{ }; /// The type of key / signature scheme used for signing and verifying. -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[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))] diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs index 2f1da5379..7dc1e5808 100644 --- a/crates/bitwarden-crypto/src/signing/claims.rs +++ b/crates/bitwarden-crypto/src/signing/claims.rs @@ -83,8 +83,6 @@ impl SignedPublicKeyOwnershipClaim { #[cfg(test)] mod tests { - use rand::rngs::OsRng; - use super::*; use crate::{AsymmetricCryptoKey, SignatureAlgorithm}; @@ -92,7 +90,7 @@ mod tests { fn test_public_key_ownership_claim() { let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); let verifying_key = signing_key.to_verifying_key(); - let public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key(); + let public_key = AsymmetricCryptoKey::make().to_public_key(); let claim = SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signing_key).unwrap(); assert!(claim.verify_claim(&public_key, &verifying_key).unwrap()); diff --git a/crates/bitwarden-crypto/src/traits/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index 84d4b7bdc..d6c0c562e 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -84,7 +84,7 @@ mod tests { 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(); #[allow(deprecated)] store diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index fcc6b2246..93c1cdc1f 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -357,6 +357,53 @@ 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 WRAPPED_SIGNING_KEY: &str = "2.TNBR4qijqxOxD7BQgePL1g==|42N+N/oif73CPb+7w9zfEo409i4iI7FJscsKjzhy2iLO8HRiF3avTYJhoVrpgd4dZz1+Ibj6FaPeZPEfO7U8ozKqMUPjkih1l+h5+YNh+3s=|wZsJJLfgNlkhIjDAfBiyLqq4LcbjYwOD+d3/2hTcI34="; + 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_OWNERSHIP_CLAIM: &[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, 88, 72, 161, 107, 102, 105, 110, 103, + 101, 114, 112, 114, 105, 110, 116, 162, 102, 100, 105, 103, 101, 115, 116, 88, 32, 157, + 225, 74, 231, 216, 192, 213, 240, 234, 67, 3, 221, 30, 3, 145, 141, 17, 73, 71, 233, 20, 4, + 102, 134, 195, 186, 11, 109, 142, 59, 25, 59, 105, 97, 108, 103, 111, 114, 105, 116, 104, + 109, 102, 83, 104, 97, 50, 53, 54, 88, 64, 19, 244, 252, 60, 39, 88, 200, 62, 208, 147, + 106, 200, 57, 125, 189, 6, 253, 109, 197, 164, 207, 193, 15, 242, 195, 241, 4, 229, 235, + 178, 207, 61, 157, 51, 178, 6, 151, 49, 129, 21, 206, 105, 158, 174, 88, 206, 11, 149, 138, + 27, 103, 15, 251, 110, 251, 148, 233, 124, 129, 29, 41, 250, 47, 10, + ]; + 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, + ]; + #[test] fn test_symmetric_decrypt() { let enc_string = EncString::from_str(ENCRYPTED).unwrap(); @@ -505,4 +552,74 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= .unwrap(); assert_eq!(shared_key, unwrapped_key); } + + #[test] + fn make_test_vectors() { + let key = PureCrypto::make_user_key_aes256_cbc_hmac(); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let verifying_key_cose = verifying_key.to_cose().unwrap(); + let signing_key_cose = signing_key.to_cose().unwrap(); + let wrapped_signing_key = + PureCrypto::symmetric_encrypt_bytes(signing_key_cose.clone(), key.clone()).unwrap(); + let asymmetric_crypto_key = AsymmetricCryptoKey::make(); + let pubkey = asymmetric_crypto_key.to_public_key(); + let public_key_ownership_claim = + SignedPublicKeyOwnershipClaim::make_claim_with_key(&pubkey, &signing_key).unwrap(); + + println!("const SIGNING_KEY_WRAPPING_KEY: &[u8] = {:?}", key); + println!( + "const WRAPPED_SIGNING_KEY: &str = {:?}", + wrapped_signing_key + ); + println!("const SIGNING_KEY: &[u8] = {:?}", signing_key_cose); + println!("const VERIFYING_KEY: &[u8] = {:?}", verifying_key_cose); + println!( + "const PUBLIC_KEY_OWNERSHIP_CLAIM: &[u8] = {:?}", + public_key_ownership_claim.as_bytes() + ); + println!( + "const PUBLIC_KEY: &[u8] = {:?}", + asymmetric_crypto_key.to_public_der() + ); + } + + #[test] + fn test_verify_public_key_ownership_claim() { + let public_key = AsymmetricPublicCryptoKey::from_der(PUBLIC_KEY).unwrap(); + let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); + let claim = SignedPublicKeyOwnershipClaim::from_bytes(PUBLIC_KEY_OWNERSHIP_CLAIM).unwrap(); + let result = claim.verify_claim(&public_key, &verifying_key); + assert!(result.is_ok()); + assert!(result.unwrap()); + } + + #[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()).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().unwrap(), + verifying_key_derived.to_cose().unwrap() + ); + } } From 05787430e0390f7fab70ecc47966de47779aa2d1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 13:51:50 +0200 Subject: [PATCH 032/112] Remove code to make test vectors --- .../src/pure_crypto.rs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 93c1cdc1f..6e4e74dcd 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -553,37 +553,6 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= assert_eq!(shared_key, unwrapped_key); } - #[test] - fn make_test_vectors() { - let key = PureCrypto::make_user_key_aes256_cbc_hmac(); - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let verifying_key_cose = verifying_key.to_cose().unwrap(); - let signing_key_cose = signing_key.to_cose().unwrap(); - let wrapped_signing_key = - PureCrypto::symmetric_encrypt_bytes(signing_key_cose.clone(), key.clone()).unwrap(); - let asymmetric_crypto_key = AsymmetricCryptoKey::make(); - let pubkey = asymmetric_crypto_key.to_public_key(); - let public_key_ownership_claim = - SignedPublicKeyOwnershipClaim::make_claim_with_key(&pubkey, &signing_key).unwrap(); - - println!("const SIGNING_KEY_WRAPPING_KEY: &[u8] = {:?}", key); - println!( - "const WRAPPED_SIGNING_KEY: &str = {:?}", - wrapped_signing_key - ); - println!("const SIGNING_KEY: &[u8] = {:?}", signing_key_cose); - println!("const VERIFYING_KEY: &[u8] = {:?}", verifying_key_cose); - println!( - "const PUBLIC_KEY_OWNERSHIP_CLAIM: &[u8] = {:?}", - public_key_ownership_claim.as_bytes() - ); - println!( - "const PUBLIC_KEY: &[u8] = {:?}", - asymmetric_crypto_key.to_public_der() - ); - } - #[test] fn test_verify_public_key_ownership_claim() { let public_key = AsymmetricPublicCryptoKey::from_der(PUBLIC_KEY).unwrap(); From 38ec3eb80181ea012acc82ca30821615a2dccdca Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 13:52:48 +0200 Subject: [PATCH 033/112] Cleanup --- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 6e4e74dcd..2e30c2d0c 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -363,7 +363,6 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 224, 90, 10, 191, 72, 22, 227, 171, 105, 107, 139, 24, 49, 9, 150, 103, 139, 151, 204, 165, 121, 165, 71, ]; - const WRAPPED_SIGNING_KEY: &str = "2.TNBR4qijqxOxD7BQgePL1g==|42N+N/oif73CPb+7w9zfEo409i4iI7FJscsKjzhy2iLO8HRiF3avTYJhoVrpgd4dZz1+Ibj6FaPeZPEfO7U8ozKqMUPjkih1l+h5+YNh+3s=|wZsJJLfgNlkhIjDAfBiyLqq4LcbjYwOD+d3/2hTcI34="; 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, From 4467b0dbb95532d7ed7c34f346e35238b1db3125 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 21 May 2025 13:55:38 +0200 Subject: [PATCH 034/112] Fix docs --- crates/bitwarden-crypto/src/signing/claims.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs index 7dc1e5808..f422ae334 100644 --- a/crates/bitwarden-crypto/src/signing/claims.rs +++ b/crates/bitwarden-crypto/src/signing/claims.rs @@ -39,7 +39,7 @@ impl PublicKeyOwnershipClaim { /// // Initial setup /// let bob_signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); /// let bob_verifying_key = bob_signing_key.to_verifying_key(); -/// let bob_public_key = AsymmetricCryptoKey::generate(&mut OsRng).to_public_key(); +/// let bob_public_key = AsymmetricCryptoKey::make().to_public_key(); /// /// // Alice trusts Bob's verifying key - this becomes Bob's cryptographic identity. /// let bob_claim = SignedPublicKeyOwnershipClaim::make_claim_with_key(&bob_public_key, &bob_signing_key).unwrap(); From d17399defdb8c5f53f4445e750c047ed7c5bdb66 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 13:56:37 +0200 Subject: [PATCH 035/112] Split up and simplify --- .../bitwarden-core/src/auth/auth_request.rs | 18 +- crates/bitwarden-core/src/auth/tde.rs | 4 +- .../src/client/encryption_settings.rs | 4 +- crates/bitwarden-core/src/mobile/crypto.rs | 55 +- .../src/platform/generate_fingerprint.rs | 2 +- crates/bitwarden-crypto/src/cose.rs | 6 + .../src/enc_string/asymmetric.rs | 71 ++- .../src/keys/asymmetric_crypto_key.rs | 161 ++--- .../bitwarden-crypto/src/keys/device_key.rs | 18 +- .../bitwarden-crypto/src/keys/fingerprint.rs | 113 ---- crates/bitwarden-crypto/src/keys/mod.rs | 13 +- .../src/keys/signed_public_key.rs | 112 ++++ .../src/keys/signing_crypto_key.rs | 471 -------------- crates/bitwarden-crypto/src/lib.rs | 1 + crates/bitwarden-crypto/src/signing/claims.rs | 98 --- .../bitwarden-crypto/src/signing/message.rs | 107 ++++ crates/bitwarden-crypto/src/signing/mod.rs | 118 +++- .../bitwarden-crypto/src/signing/namespace.rs | 13 +- crates/bitwarden-crypto/src/signing/sign.rs | 588 ------------------ .../bitwarden-crypto/src/signing/signature.rs | 311 +++++++++ .../src/signing/signed_object.rs | 266 ++++++++ .../src/signing/signing_key.rs | 183 ++++++ .../src/signing/verifying_key.rs | 192 ++++++ crates/bitwarden-crypto/src/store/context.rs | 58 +- .../src/traits/encryptable.rs | 6 +- crates/bitwarden-crypto/src/traits/key_id.rs | 6 +- .../src/pure_crypto.rs | 57 +- crates/memory-testing/src/main.rs | 2 +- 28 files changed, 1533 insertions(+), 1521 deletions(-) delete mode 100644 crates/bitwarden-crypto/src/keys/fingerprint.rs create mode 100644 crates/bitwarden-crypto/src/keys/signed_public_key.rs delete mode 100644 crates/bitwarden-crypto/src/keys/signing_crypto_key.rs delete mode 100644 crates/bitwarden-crypto/src/signing/claims.rs create mode 100644 crates/bitwarden-crypto/src/signing/message.rs delete mode 100644 crates/bitwarden-crypto/src/signing/sign.rs create mode 100644 crates/bitwarden-crypto/src/signing/signature.rs create mode 100644 crates/bitwarden-crypto/src/signing/signed_object.rs create mode 100644 crates/bitwarden-crypto/src/signing/signing_key.rs create mode 100644 crates/bitwarden-crypto/src/signing/verifying_key.rs diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 7435c52a3..e56381f00 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, + fingerprint, generate_random_alphanumeric, CryptoError, PrivateKey, PublicKey, + PublicKeyEncryptionAlgorithm, UnsignedSharedKey, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, SymmetricCryptoKey}; @@ -31,9 +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 key = AsymmetricCryptoKey::make(); + let key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); - let spki = key.to_public_der()?; + let spki = key.to_public_key().to_der()?; let fingerprint = fingerprint(email, &spki)?; let b64 = STANDARD.encode(&spki); @@ -52,7 +52,7 @@ pub(crate) fn auth_request_decrypt_user_key( private_key: String, user_key: UnsignedSharedKey, ) -> Result { - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let key = PrivateKey::from_der(&STANDARD.decode(private_key)?)?; let key: SymmetricCryptoKey = user_key.decapsulate_key_unsigned(&key)?; Ok(key) } @@ -66,7 +66,7 @@ pub(crate) fn auth_request_decrypt_master_key( ) -> Result { use bitwarden_crypto::MasterKey; - let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let key = PrivateKey::from_der(&STANDARD.decode(private_key)?)?; let master_key: SymmetricCryptoKey = master_key.decapsulate_key_unsigned(&key)?; let master_key = MasterKey::try_from(&master_key)?; @@ -91,7 +91,7 @@ pub(crate) fn approve_auth_request( client: &Client, public_key: String, ) -> Result { - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = PublicKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); @@ -118,11 +118,11 @@ fn test_auth_request() { ]; let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); + PrivateKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(secret.clone()).unwrap(), - &private_key, + &private_key.to_public_key(), ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 2fd57ea97..6bcbdea42 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SymmetricCryptoKey, TrustDeviceResponse, + DeviceKey, EncString, Kdf, PublicKey, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; @@ -15,7 +15,7 @@ pub(super) fn make_register_tde_keys( org_public_key: String, remember_device: bool, ) -> Result { - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(org_public_key)?)?; + let public_key = PublicKey::from_der(&STANDARD.decode(org_public_key)?)?; let user_key = UserKey::new(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); let key_pair = user_key.make_key_pair()?; diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index daee0968a..919c14faa 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -42,7 +42,7 @@ impl EncryptionSettings { signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable, SigningKey}; + use bitwarden_crypto::{CoseSerializable, KeyDecryptable, PrivateKey, SigningKey}; use log::warn; use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; @@ -52,7 +52,7 @@ impl EncryptionSettings { // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a recovery // process in place. - AsymmetricCryptoKey::from_der(&dec) + PrivateKey::from_der(&dec) .map_err(|_| { warn!("Invalid private key"); }) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index b62f4e99e..8a776fbde 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -8,9 +8,8 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKeyOwnershipClaim, SigningKey, - SymmetricCryptoKey, UnsignedSharedKey, UserKey, + CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, + PrivateKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -19,7 +18,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, - key_management::{AsymmetricKeyId, SymmetricKeyId}, + key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}, Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError, }; @@ -405,9 +404,9 @@ pub(super) fn enroll_admin_password_reset( public_key: String, ) -> Result { use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::AsymmetricPublicCryptoKey; + use bitwarden_crypto::PublicKey; - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = PublicKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); // FIXME: [PM-18110] This should be removed once the key store can handle public key encryption @@ -527,11 +526,12 @@ pub(super) fn verify_asymmetric_keys( .decrypt_with_key(user_key) .map_err(VerifyError::DecryptFailed)?; - let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key) - .map_err(VerifyError::ParseFailed)?; + let private_key = + PrivateKey::from_der(&decrypted_private_key).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); @@ -572,7 +572,7 @@ pub struct MakeUserSigningKeysResponse { /// A signed object claiming ownership of a public key. This ties the public key to the /// signature key - signed_public_key_ownership_claim: String, + signed_public_key: String, } /// Makes a new set of signing keys for a user. This also creates a signed public-key ownership @@ -581,31 +581,24 @@ pub struct MakeUserSigningKeysResponse { pub fn make_user_signing_keys(client: &Client) -> Result { let key_store = client.internal.get_key_store(); let ctx = key_store.context(); - let public_key = ctx - .dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey) - .map_err(|_| CryptoError::InvalidKey)? - .to_public_der()?; - let public_key = - AsymmetricPublicCryptoKey::from_der(&public_key).map_err(|_| CryptoError::InvalidKey)?; let wrapping_key = ctx .dangerous_get_symmetric_key(SymmetricKeyId::User) .map_err(|_| CryptoError::InvalidKey)?; - let signature_keypair = - SigningKey::make(SignatureAlgorithm::Ed25519).map_err(|_| CryptoError::InvalidKey)?; - // This needs to be changed to use the correct cose content format before rolling out to real - // accounts - let encrypted_signing_key = signature_keypair.to_cose()?; - let serialized_verifying_key = signature_keypair.to_verifying_key().to_cose()?; - let serialized_verifying_key_b64 = STANDARD.encode(serialized_verifying_key); - let signed_public_key_ownership_claim = - SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signature_keypair)?; + + // Make new keypair and sign the public key with it + let signature_keypair = SigningKey::make(SignatureAlgorithm::Ed25519) + .map_err(|_| CryptoError::InvalidKey)?; + let signed_public_key: Vec = ctx.make_signed_public_key( + AsymmetricKeyId::UserPrivateKey, + SigningKeyId::UserSigningKey, + )?.try_into()?; Ok(MakeUserSigningKeysResponse { - verifying_key: serialized_verifying_key_b64, - signing_key: encrypted_signing_key.encrypt_with_key(wrapping_key)?, - signed_public_key_ownership_claim: STANDARD - .encode(signed_public_key_ownership_claim.as_bytes()), + 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 + signing_key: signature_keypair.to_cose()?.encrypt_with_key(wrapping_key)?, + signed_public_key: STANDARD.encode(&signed_public_key), }) } @@ -811,7 +804,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); @@ -836,8 +828,7 @@ mod tests { let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; - let private_key = - AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); + let private_key = PrivateKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); let decrypted: SymmetricCryptoKey = encrypted.decapsulate_key_unsigned(&private_key).unwrap(); diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index 4e1bfe009..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) diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index 1da1e10a1..7ed503c39 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -117,6 +117,12 @@ impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey { } } +pub trait CoseSerializable { + fn to_cose(&self) -> Result, CryptoError>; + 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 c4314bdaf..01011dc91 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,7 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - AsymmetricCryptoKey, AsymmetricEncryptable, SymmetricCryptoKey, + PrivateKey, PublicKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion @@ -159,13 +159,18 @@ impl UnsignedSharedKey { /// and thus does not guarantee sender authenticity. pub fn encapsulate_key_unsigned( encapsulated_key: &SymmetricCryptoKey, - encapsulation_key: &dyn AsymmetricEncryptable, + encapsulation_key: &PublicKey, ) -> Result { - let enc = encrypt_rsa2048_oaep_sha1( - encapsulation_key.to_public_rsa_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]. @@ -187,27 +192,31 @@ impl UnsignedSharedKey { /// guaranteed. pub fn decapsulate_key_unsigned( &self, - decapsulation_key: &AsymmetricCryptoKey, + decapsulation_key: &PrivateKey, ) -> 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()) } } @@ -228,8 +237,8 @@ impl schemars::JsonSchema for UnsignedSharedKey { mod tests { use schemars::schema_for; - use super::{AsymmetricCryptoKey, UnsignedSharedKey}; - use crate::SymmetricCryptoKey; + use super::UnsignedSharedKey; + use crate::{PrivateKey, SymmetricCryptoKey}; const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS @@ -262,7 +271,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha256_b64() { - let key_pair = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let key_pair = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); @@ -275,7 +284,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha1_b64() { - let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); @@ -288,7 +297,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() { - let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 2a1a6f8b0..5867258ae 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -1,57 +1,62 @@ use std::pin::Pin; -use rsa::{pkcs8::DecodePublicKey, traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey}; +use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; -use super::{fingerprint::FingerprintableKey, key_encryptable::CryptoKey}; +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 { - fn to_public_rsa_key(&self) -> &RsaPublicKey; +pub enum PublicKeyEncryptionAlgorithm { + RsaOaepSha1, } -/// An asymmetric public encryption key. Can only encrypt -/// [UnsignedSharedKey](crate::UnsignedSharedKey), usually accompanied by a -/// [AsymmetricCryptoKey] -pub struct AsymmetricPublicCryptoKey { - key: RsaPublicKey, +#[derive(Clone)] +pub(crate) enum RawPublicKey { + RsaOaepSha1(RsaPublicKey), +} + +#[derive(Clone)] +pub struct PublicKey { + inner: RawPublicKey, } -impl AsymmetricPublicCryptoKey { +impl PublicKey { + 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(PublicKey { + inner: RawPublicKey::RsaOaepSha1( + RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?, + ), }) } -} - -impl AsymmetricEncryptable for AsymmetricPublicCryptoKey { - fn to_public_rsa_key(&self) -> &RsaPublicKey { - &self.key - } -} -impl FingerprintableKey for AsymmetricPublicCryptoKey { - fn fingerprint_parts(&self) -> Vec> { - vec![ - self.key.n().to_bytes_le().as_slice().to_vec(), - self.key.e().to_bytes_le().as_slice().to_vec(), - ] + 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>), +} + +#[derive(Clone)] +pub struct PrivateKey { + inner: RawPrivateKey, } // Note that RsaPrivateKey already implements ZeroizeOnDrop, so we don't need to do anything @@ -62,75 +67,75 @@ const _: () = { assert_zeroize_on_drop::(); } }; +impl zeroize::ZeroizeOnDrop for PrivateKey {} +impl CryptoKey for PrivateKey {} -impl zeroize::ZeroizeOnDrop for AsymmetricCryptoKey {} - -impl AsymmetricCryptoKey { +impl PrivateKey { /// Generate a random AsymmetricCryptoKey (RSA-2048). - pub fn make() -> Self { + pub fn make(algorithm: PublicKeyEncryptionAlgorithm) -> Self { use rand::rngs::OsRng; - Self::make_internal(&mut OsRng) + Self::make_internal(algorithm, &mut OsRng) } - fn make_internal(rng: &mut R) -> Self { - let bits = 2048; - - 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"), + )), + }, } } 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)?, + )), }) } 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)?, + )), }) } pub fn to_der(&self) -> Result> { - use rsa::pkcs8::EncodePrivateKey; - Ok(self - .key - .to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_owned()) - } - - pub fn to_public_der(&self) -> Result> { - use rsa::pkcs8::EncodePublicKey; - Ok(self - .to_public_rsa_key() - .to_public_key_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()) + } + } } - pub fn to_public_key(&self) -> AsymmetricPublicCryptoKey { - AsymmetricPublicCryptoKey { - key: self.key.to_public_key().clone(), + pub fn to_public_key(&self) -> PublicKey { + match &self.inner { + RawPrivateKey::RsaOaepSha1(private_key) => PublicKey { + inner: RawPublicKey::RsaOaepSha1(private_key.to_public_key()), + }, } } -} -impl AsymmetricEncryptable for AsymmetricCryptoKey { - fn to_public_rsa_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 { +impl std::fmt::Debug for PrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AsymmetricCryptoKey").finish() } @@ -140,9 +145,7 @@ impl std::fmt::Debug for AsymmetricCryptoKey { mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; - use crate::{ - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey, - }; + use crate::{PrivateKey, PublicKey, SymmetricCryptoKey, UnsignedSharedKey}; #[test] fn test_asymmetric_crypto_key() { @@ -178,9 +181,9 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let der_key_vec = STANDARD.decode("MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYcafeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4CwmqqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyvb0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZwP7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2famrEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKiszJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx0d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+8vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVRjB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKachvGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI1u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KRJ30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQTjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9ByeKvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiNwEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZUZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEAkY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7Wpt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwNZy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLiCVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzupPFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnfDnqOsltgPomWZ7xVfMkm9niL2OA=").unwrap(); // 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); + let pem_key = PrivateKey::from_pem(pem_key_str).unwrap(); + let der_key = PrivateKey::from_der(&der_key_vec).unwrap(); + 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); @@ -232,8 +235,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= )) .unwrap(); - let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); - let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); + let private_key = PrivateKey::from_der(&private_key).unwrap(); + let public_key = PublicKey::from_der(&public_key).unwrap(); let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index f2f68ef9a..dcc425e50 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::{PrivateKey, PublicKeyEncryptionAlgorithm}; use crate::{ - error::Result, AsymmetricCryptoKey, CryptoError, EncString, KeyDecryptable, KeyEncryptable, - SymmetricCryptoKey, UnsignedSharedKey, + error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, + UnsignedSharedKey, }; /// Device Key @@ -31,13 +32,16 @@ impl DeviceKey { pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); - let device_private_key = AsymmetricCryptoKey::make(); + let device_private_key = PrivateKey::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 @@ -59,7 +63,7 @@ impl DeviceKey { protected_user_key: UnsignedSharedKey, ) -> Result { let device_private_key: Vec = protected_device_private_key.decrypt_with_key(&self.0)?; - let device_private_key = AsymmetricCryptoKey::from_der(&device_private_key)?; + let device_private_key = PrivateKey::from_der(&device_private_key)?; let user_key: SymmetricCryptoKey = protected_user_key.decapsulate_key_unsigned(&device_private_key)?; diff --git a/crates/bitwarden-crypto/src/keys/fingerprint.rs b/crates/bitwarden-crypto/src/keys/fingerprint.rs deleted file mode 100644 index baa1a12ca..000000000 --- a/crates/bitwarden-crypto/src/keys/fingerprint.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! This module provides functionality to generate a cryptographic fingerprint for a public key. -//! This is based on a set of parts of a public key, for RSA this can be the modulus and exponent, -//! in canonical form. -//! -//! Currently, only SHA256 is supported, but the format is designed to be extensible, to more -//! algorithms in the future, should SHA256 ever not fulfill the required security properties. -use serde::{Deserialize, Serialize}; -use sha2::Digest; - -/// Security assumption: -/// - The hash function has second pre-image resistance -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) enum PublicKeyFingerprintAlgorithm { - Sha256 = 1, -} - -/// A fingerprint represents a short, canonical representation of a public key. -/// When signing a key, or showing a key to a user, this representation is used. -/// -/// Note: This implies that a key can have multiple fingerprints. Under a given algorithm, -/// the fingerprint is always the same, but under different algorithms, the fingerprint is also -/// different. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct PublicKeyFingerprint { - pub(crate) digest: serde_bytes::ByteBuf, - pub(crate) algorithm: PublicKeyFingerprintAlgorithm, -} - -/// A trait for objects that can have a canonical cryptographic fingerprint derived from them. To -/// implement this trait, the object should implement the `FingerprintableKey` trait. -pub(crate) trait Fingerprintable { - /// Returns a fingerprint for the public key, using the currently recommended algorithm. - fn fingerprint(&self) -> PublicKeyFingerprint; - /// Verify that a fingerprint is valid for the public key - fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool; -} - -pub(crate) trait FingerprintableKey: Fingerprintable { - /// Returns a canonical representation of the public key. - /// The entries of the returned vector should not contain data that is a non-injective mapping - /// of the public key. For instance, for RSA, the modulus and exponent should be returned - /// separately, not concatenated. - fn fingerprint_parts(&self) -> Vec>; -} - -impl Fingerprintable for T { - fn fingerprint(&self) -> PublicKeyFingerprint { - let fingerprint_parts = self.fingerprint_parts(); - derive_fingerprint(fingerprint_parts) - } - - fn verify_fingerprint(&self, fingerprint: &PublicKeyFingerprint) -> bool { - let fingerprint_parts = self.fingerprint_parts(); - verify_fingerprint(fingerprint, fingerprint_parts) - } -} - -/// Derives a fingerprint using a currently supported algorithm. -/// Fingerprint_parts must be a canonical set of parts representing the public key. -/// -/// The encoding needs to be canonical. That is, something like DER or PEM does *not* work, -/// because the encoding could differ slightly between implementations. For RSA, using the modulus -/// and exponent directly works. -fn derive_fingerprint(fingerprint_parts: Vec>) -> PublicKeyFingerprint { - derive_fingerprint_from_parts(fingerprint_parts) -} - -/// This function ensures an injective mapping of the inputs to the output hash. -/// Concatenating the inputs does not work. For RSA this could mean that: -/// with data = \[N,E\], |nnnnnn|ee|, and |nnnnnnn|e| would both be valid interpretations of the -/// concatenation of the bytes, and thus may lead to the same hash for different (N,E) pairs. -/// -/// This function hashes each input separately, concatenates the hashes, and then hashes the result. -/// -/// Assumption: H is a cryptographic hash function, with respect to: -/// - Second pre-image resistance -/// -/// Assumption: H's output has a constant length output HS -/// -/// Specifically, the construction is: -/// H(H(data1)|H(data2)|...|H(dataN)) -/// -/// Given the assumptions above, then hashing each input separately, and concatenating the hashes is -/// an injective mapping. Because there is an injective mapping, and because of collision resistance -/// w.r.t. the final hash functions inputs, this also implies collision resistance w.r.t. data. -fn derive_fingerprint_from_parts(data: Vec>) -> PublicKeyFingerprint { - let hash_set = data - .iter() - .map(|d| derive_fingerprint_single(d)) - .collect::>(); - let concat = hash_set - .iter() - .flat_map(|h| h.digest.clone()) - .collect::>(); - derive_fingerprint_single(&concat) -} - -fn derive_fingerprint_single(data: &[u8]) -> PublicKeyFingerprint { - PublicKeyFingerprint { - digest: sha2::Sha256::digest(data).to_vec().into(), - algorithm: PublicKeyFingerprintAlgorithm::Sha256, - } -} - -/// Verifies a fingerprint for a given public key, represented as a canonical list of parts. -fn verify_fingerprint(fingerprint: &PublicKeyFingerprint, fingerprint_parts: Vec>) -> bool { - match fingerprint.algorithm { - PublicKeyFingerprintAlgorithm::Sha256 => { - let hash = derive_fingerprint_from_parts(fingerprint_parts); - hash.digest == fingerprint.digest - } - } -} diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 55e341356..451da0db6 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -11,24 +11,21 @@ pub use symmetric_crypto_key::{ Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key, }; mod asymmetric_crypto_key; -pub use asymmetric_crypto_key::{ - AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey, -}; -mod signing_crypto_key; -pub use signing_crypto_key::{SigningKey, *}; +pub use asymmetric_crypto_key::{PrivateKey, PublicKey, 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; pub use device_key::{DeviceKey, TrustDeviceResponse}; mod pin_key; pub use pin_key::PinKey; -mod fingerprint; mod kdf; mod key_id; -pub(crate) use fingerprint::{Fingerprintable, FingerprintableKey, PublicKeyFingerprint}; pub use kdf::{ default_argon2_iterations, default_argon2_memory, default_argon2_parallelism, default_pbkdf2_iterations, Kdf, }; -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..0c5766360 --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -0,0 +1,112 @@ +//! 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 serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +use super::PublicKey; +use crate::{ + cose::CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, +}; + +/// `PublicKeyEncryptionAlgorithm` defines the algorithms used for asymmetric encryption. +/// Currently, only RSA with OAEP and SHA-1 keys are used. +#[derive(Serialize, Deserialize)] +enum PublicKeyEncryptionAlgorithms { + RsaOaepSha1 = 0, +} + +/// `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, Deserialize)] +enum PublicKeyFormat { + Spki = 0, +} + +/// `SignedAsymmetricPublicKeyMessage` is a message that once signed, makes a claim towards owning a +/// public encryption key. +#[derive(Serialize, Deserialize)] +pub struct SignedPublicKeyMessage { + /// The algorithm/crypto system used with this public key. + algorithm: PublicKeyEncryptionAlgorithms, + /// The format of the public key. + content_format: PublicKeyFormat, + /// The public key, serialized and formatted in the content format specified in + /// `content_format`. + public_key: ByteBuf, +} + +impl SignedPublicKeyMessage { + pub fn from_public_key(public_key: &PublicKey) -> Result { + Ok(SignedPublicKeyMessage { + algorithm: PublicKeyEncryptionAlgorithms::RsaOaepSha1, + content_format: PublicKeyFormat::Spki, + public_key: ByteBuf::from(public_key.to_der()?), + }) + } + + pub(crate) 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. +pub struct SignedPublicKey(pub(crate) SignedObject); + +impl TryInto> for SignedPublicKey { + type Error = CryptoError; + fn try_into(self) -> Result, CryptoError> { + self.0.to_cose() + } +} + +impl TryFrom> for SignedPublicKey { + type Error = CryptoError; + fn try_from(bytes: Vec) -> Result { + Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) + } +} + +impl SignedPublicKey { + 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, + ) { + (PublicKeyEncryptionAlgorithms::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( + PublicKey::from_der(&public_key_message.public_key.into_vec()) + .map_err(|_| CryptoError::InvalidKey)?, + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{PrivateKey, PublicKeyEncryptionAlgorithm, SignatureAlgorithm}; + + #[test] + fn test_signed_asymmetric_public_key() { + let public_key = + PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1).to_public_key(); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + 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/keys/signing_crypto_key.rs b/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs deleted file mode 100644 index 89d239c7e..000000000 --- a/crates/bitwarden-crypto/src/keys/signing_crypto_key.rs +++ /dev/null @@ -1,471 +0,0 @@ -use ciborium::{value::Integer, Value}; -use coset::{ - iana::{ - Algorithm, CoapContentFormat, EllipticCurve, EnumI64, KeyOperation, KeyType, - OkpKeyParameter, - }, - CborSerializable, CoseKey, CoseSign1, Label, RegisteredLabel, RegisteredLabelWithPrivate, -}; -use rand::rngs::OsRng; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use tsify_next::Tsify; -use zeroize::ZeroizeOnDrop; - -use super::{key_id::KeyId, CryptoKey, KEY_ID_SIZE}; -use crate::{ - cose::SIGNING_NAMESPACE, - error::{Result, SignatureError}, - signing::SigningNamespace, - CryptoError, -}; - -/// 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, -} - -impl SignatureAlgorithm { - /// Returns the currently accepted safe algorithm for new keys. - pub fn default_algorithm() -> Self { - SignatureAlgorithm::Ed25519 - } -} - -/// A `SigningKey` without the key id. This enum contains a variant for each supported signature -/// scheme. -#[derive(Clone, zeroize::ZeroizeOnDrop)] -pub(crate) enum RawSigningKey { - Ed25519(ed25519_dalek::SigningKey), -} - -/// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature -/// scheme. -pub(crate) enum RawVerifyingKey { - Ed25519(ed25519_dalek::VerifyingKey), -} - -/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be -/// derived from it. -#[derive(Clone, ZeroizeOnDrop)] -pub struct SigningKey { - pub(crate) id: KeyId, - pub(crate) inner: RawSigningKey, -} - -impl CryptoKey for SigningKey {} - -/// A verifying key is a public key used for verifying signatures. It can be published to other -/// users, who can use it to verify that messages were signed by the holder of the corresponding -/// `SigningKey`. -pub struct VerifyingKey { - id: KeyId, - pub(crate) inner: RawVerifyingKey, -} - -impl SigningKey { - /// Makes a new signing key for the given signature scheme. - pub fn make(key_algorithm: SignatureAlgorithm) -> Result { - match key_algorithm { - SignatureAlgorithm::Ed25519 => Ok(SigningKey { - id: KeyId::make(), - inner: RawSigningKey::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), - }), - } - } - - pub(crate) fn cose_algorithm(&self) -> Algorithm { - match &self.inner { - RawSigningKey::Ed25519(_) => Algorithm::EdDSA, - } - } - - /// Serializes the signing key to a COSE-formatted byte array. - pub fn to_cose(&self) -> Result> { - 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() - .map_err(|_| CryptoError::InvalidKey) - } - } - } - - /// Deserializes a COSE-formatted byte array into a signing key. - pub fn from_cose(bytes: &[u8]) -> Result { - let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - - let Some(algorithm) = cose_key.alg else { - return Err(CryptoError::InvalidKey); - }; - let key_id: [u8; KEY_ID_SIZE] = cose_key - .key_id - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key_id: KeyId = key_id.into(); - match (cose_key.kty, algorithm) { - (kty, alg) - if kty == RegisteredLabel::Assigned(KeyType::OKP) - && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => - { - // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair - let (mut crv, mut d) = (None, None); - for (key, value) in &cose_key.params { - if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; - match key { - OkpKeyParameter::Crv => { - crv.replace(value); - } - OkpKeyParameter::D => { - d.replace(value); - } - _ => (), - } - } - } - - let (Some(d), Some(crv)) = (d, crv) else { - return Err(CryptoError::InvalidKey); - }; - let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); - if crv == EllipticCurve::Ed25519.to_i64().into() { - let secret_key_bytes: &[u8; 32] = d - .as_bytes() - .ok_or(CryptoError::InvalidKey)? - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes); - Ok(SigningKey { - id: key_id, - inner: RawSigningKey::Ed25519(key), - }) - } else { - Err(CryptoError::InvalidKey) - } - } - _ => Err(CryptoError::InvalidKey), - } - } - - /// 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()), - }, - } - } - - #[allow(unused)] - fn algorithm(&self) -> SignatureAlgorithm { - match &self.inner { - RawSigningKey::Ed25519(_) => SignatureAlgorithm::Ed25519, - } - } -} - -#[allow(unused)] -impl VerifyingKey { - /// Serializes the verifying key to a COSE-formatted byte array. - pub fn to_cose(&self) -> Result> { - match &self.inner { - RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() - .key_id((&self.id).into()) - .algorithm(Algorithm::EdDSA) - .param( - OkpKeyParameter::Crv.to_i64(), - 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(), - Value::Bytes(key.to_bytes().to_vec()), - ) - .add_key_op(KeyOperation::Verify) - .build() - .to_vec() - .map_err(|_| CryptoError::InvalidKey), - } - } - - /// Deserializes a COSE-formatted byte array into a verifying key. - pub fn from_cose(bytes: &[u8]) -> Result { - let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - - let Some(algorithm) = cose_key.alg else { - return Err(CryptoError::InvalidKey); - }; - let key_id: [u8; 16] = cose_key - .key_id - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key_id: KeyId = key_id.into(); - match (cose_key.kty, algorithm) { - (kty, alg) - if kty == RegisteredLabel::Assigned(KeyType::OKP) - && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => - { - let (mut crv, mut x) = (None, None); - for (key, value) in &cose_key.params { - if let coset::Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; - match key { - OkpKeyParameter::Crv => { - crv.replace(value); - } - OkpKeyParameter::X => { - x.replace(value); - } - _ => (), - } - } - } - let (Some(x), Some(crv)) = (x, crv) else { - return Err(CryptoError::InvalidKey); - }; - - if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) - != EllipticCurve::Ed25519.to_i64().into() - { - return Err(CryptoError::InvalidKey); - } - - let verifying_key_bytes: &[u8; 32] = x - .as_bytes() - .ok_or(CryptoError::InvalidKey)? - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) - .map_err(|_| CryptoError::InvalidKey)?; - Ok(VerifyingKey { - id: key_id, - inner: RawVerifyingKey::Ed25519(verifying_key), - }) - } - _ => Err(CryptoError::InvalidKey), - } - } - - /// Returns the signature scheme used by the verifying key. - pub fn algorithm(&self) -> SignatureAlgorithm { - match &self.inner { - RawVerifyingKey::Ed25519(_) => SignatureAlgorithm::Ed25519, - } - } -} - -/// 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) - } -} - -#[allow(unused)] -impl Signature { - pub(crate) fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = - CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; - Ok(Signature(cose_sign1)) - } - - pub(crate) fn to_cose(&self) -> Result> { - self.0 - .clone() - .to_vec() - .map_err(|_| SignatureError::InvalidSignature.into()) - } - - pub(crate) fn inner(&self) -> &CoseSign1 { - &self.0 - } - - pub(crate) fn namespace(&self) -> Result { - let namespace = self - .0 - .protected - .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_i64( - i128::from(namespace) - .try_into() - .map_err(|_| SignatureError::InvalidNamespace)?, - ) - } - - pub(crate) fn content_type(&self) -> Result { - if let RegisteredLabel::Assigned(content_format) = self - .0 - .protected - .header - .content_type - .clone() - .ok_or(CryptoError::from(SignatureError::InvalidSignature))? - { - Ok(content_format) - } else { - Err(SignatureError::InvalidSignature.into()) - } - } -} - -/// A signed object has a cryptographical attestation to a (namespace, data) pair. The namespace and -/// data are included in the signature object. -pub struct SignedObject(pub(crate) CoseSign1); - -impl From for SignedObject { - fn from(cose_sign1: CoseSign1) -> Self { - SignedObject(cose_sign1) - } -} - -impl SignedObject { - pub fn content_type(&self) -> Result { - if let RegisteredLabel::Assigned(content_format) = self - .0 - .protected - .header - .content_type - .clone() - .ok_or(CryptoError::from(SignatureError::InvalidSignature))? - { - Ok(content_format) - } else { - Err(SignatureError::InvalidSignature.into()) - } - } -} - -#[allow(unused)] -impl SignedObject { - pub(crate) fn from_cose(bytes: &[u8]) -> Result { - let cose_sign1 = - CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; - Ok(SignedObject(cose_sign1)) - } - - pub(crate) fn to_cose(&self) -> Result> { - self.0 - .clone() - .to_vec() - .map_err(|_| SignatureError::InvalidSignature.into()) - } - - pub(crate) fn inner(&self) -> &CoseSign1 { - &self.0 - } - - pub(crate) fn namespace(&self) -> Result { - let namespace = self - .0 - .protected - .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_i64( - i128::from(namespace) - .try_into() - .map_err(|_| SignatureError::InvalidNamespace)?, - ) - } - - pub fn payload(&self) -> Result> { - self.0 - .payload - .as_ref() - .ok_or(SignatureError::InvalidSignature.into()) - .map(|payload| payload.to_vec()) - } -} - -#[cfg(test)] -mod tests { - use coset::CoseSign1Builder; - - use super::*; - - #[test] - fn test_cose_roundtrip_signature() { - let sig = CoseSign1Builder::new().build(); - let signature = Signature(sig.clone()); - let cose = signature.to_cose().unwrap(); - let parsed_cose = Signature::from_cose(&cose).unwrap(); - assert_eq!(cose, parsed_cose.to_cose().unwrap()); - } - - #[test] - fn test_cose_roundtrip_signed_object() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let cose = signing_key - .sign(&"test", &SigningNamespace::ExampleNamespace) - .unwrap(); - let cose = cose.to_cose().unwrap(); - let parsed_cose = SignedObject::from_cose(&cose).unwrap(); - assert_eq!(cose, parsed_cose.to_cose().unwrap()); - } - - #[test] - fn test_cose_roundtrip_encode_signing() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let cose = signing_key.to_cose().unwrap(); - let parsed_key = SigningKey::from_cose(&cose).unwrap(); - - assert_eq!( - signing_key.to_cose().unwrap(), - parsed_key.to_cose().unwrap() - ); - } -} diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index e7c5526ed..6db9df997 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -31,6 +31,7 @@ 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; diff --git a/crates/bitwarden-crypto/src/signing/claims.rs b/crates/bitwarden-crypto/src/signing/claims.rs deleted file mode 100644 index f422ae334..000000000 --- a/crates/bitwarden-crypto/src/signing/claims.rs +++ /dev/null @@ -1,98 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::SigningNamespace; -use crate::{ - keys::Fingerprintable, AsymmetricPublicCryptoKey, CryptoError, FingerprintableKey, - PublicKeyFingerprint, SignedObject, SigningKey, VerifyingKey, -}; - -/// The non-serialized version of `SignedPublicKeyOwnershipClaim` -#[derive(Serialize, Deserialize)] -pub(crate) struct PublicKeyOwnershipClaim { - pub(crate) fingerprint: PublicKeyFingerprint, -} - -impl PublicKeyOwnershipClaim { - pub(crate) fn for_public_key(public_key: &impl FingerprintableKey) -> Self { - Self { - fingerprint: public_key.fingerprint(), - } - } -} - -/// A user or org shall only have one long-term cryptographic identity. This is the signing key. A -/// user also needs to receive messages asymmetrically shared to them. Thus, an object tying the -/// signing key to the asymmetric encryption public key is needed. A signed public key ownership -/// claim represents a claim by a signing key that it owns a specific public encryption key. This is -/// used to tie the cryptographic identity (signing) to the encryption receiving identity -/// (asymmetric encryption key). -/// -/// 1. Initially, Alice knows Bob's cryptographic identity (verifying key). -/// 2. Alice wants to send a message to Bob using his public encryption key. -/// 3. Alice gets Bob's public encryption key from the server, along with the -/// [`SignedPublicKeyOwnershipClaim`]. -/// 4. Alice verifies the claim using Bob's verifying key that she trusts. -/// ``` -/// use rand::rngs::OsRng; -/// use bitwarden_crypto::{AsymmetricCryptoKey, CryptoError, SigningKey, VerifyingKey, SignedPublicKeyOwnershipClaim, SignatureAlgorithm}; -/// -/// // Initial setup -/// let bob_signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); -/// let bob_verifying_key = bob_signing_key.to_verifying_key(); -/// let bob_public_key = AsymmetricCryptoKey::make().to_public_key(); -/// -/// // Alice trusts Bob's verifying key - this becomes Bob's cryptographic identity. -/// let bob_claim = SignedPublicKeyOwnershipClaim::make_claim_with_key(&bob_public_key, &bob_signing_key).unwrap(); -/// // Alice downloads Bob's public key from the server. -/// // Alice verifies the claim using Bob's verifying key. -/// assert!(bob_claim.verify_claim(&bob_public_key, &bob_verifying_key).unwrap()); -/// // Alice can now send a message to Bob using his public encryption key. -pub struct SignedPublicKeyOwnershipClaim(Vec); - -impl SignedPublicKeyOwnershipClaim { - /// Creates a new `SignedPublicKeyOwnershipClaim` for the provided public key and signing key. - pub fn make_claim_with_key( - public_key: &AsymmetricPublicCryptoKey, - signing_key: &SigningKey, - ) -> Result { - let claim = PublicKeyOwnershipClaim::for_public_key(public_key); - let signature = signing_key.sign(&claim, &SigningNamespace::PublicKeyOwnershipClaim)?; - Ok(Self(signature.to_cose()?)) - } - - /// Verifies the signed claim using the provided public key and verifying key. - pub fn verify_claim( - &self, - public_key: &AsymmetricPublicCryptoKey, - verifying_key: &VerifyingKey, - ) -> Result { - let signed_object = SignedObject::from_cose(&self.0)?; - let claim: PublicKeyOwnershipClaim = verifying_key - .get_verified_payload(&signed_object, &SigningNamespace::PublicKeyOwnershipClaim)?; - Ok(public_key.verify_fingerprint(&claim.fingerprint)) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Ok(Self(bytes.to_vec())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{AsymmetricCryptoKey, SignatureAlgorithm}; - - #[test] - fn test_public_key_ownership_claim() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let public_key = AsymmetricCryptoKey::make().to_public_key(); - let claim = - SignedPublicKeyOwnershipClaim::make_claim_with_key(&public_key, &signing_key).unwrap(); - assert!(claim.verify_claim(&public_key, &verifying_key).unwrap()); - } -} diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs new file mode 100644 index 000000000..25c1eb3fa --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -0,0 +1,107 @@ +//! 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. +//! +//! Currently, only CBOR is implemented, since it is compact and is what COSE already uses. + +use coset::iana::CoapContentFormat; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::CryptoError; + +/// 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 { + pub(super) fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { + SerializedMessage { + serialized_message_bytes: bytes, + content_type, + } + } + + 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(|_| CryptoError::CoseEncodingError)?; + 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(super) fn decode(&self) -> Result { + if self.content_type != CoapContentFormat::Cbor { + return Err(CryptoError::CoseEncodingError); + } + + ciborium::de::from_reader(self.serialized_message_bytes.as_slice()) + .map_err(|_| CryptoError::CoseEncodingError) + } +} + +#[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 index 171efd52c..fa31d43d3 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -1,6 +1,116 @@ -mod claims; -pub use claims::SignedPublicKeyOwnershipClaim; +//! 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 namespace; pub use namespace::SigningNamespace; -mod sign; -pub use sign::*; +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, +} + +impl SignatureAlgorithm { + /// Returns the currently accepted safe algorithm for new keys. + pub fn default_algorithm() -> Self { + SignatureAlgorithm::Ed25519 + } +} + +#[cfg(test)] +#[derive(Deserialize, Debug, PartialEq, Serialize)] +pub(super) struct TestMessage { + field1: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CoseSerializable; + + /// The function used to create the test vectors below, and can be used to re-generate them. + /// Once rolled out to user accounts, this function can be removed, because at that point we + /// cannot introduce format-breaking changes anymore. + #[test] + fn make_test_vectors() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let verifying_key = signing_key.to_verifying_key(); + let test_message = TestMessage { + field1: "Test message".to_string(), + }; + let (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().unwrap() + ); + println!( + "const VERIFYING_KEY: &[u8] = &{:?};", + verifying_key.to_cose().unwrap() + ); + println!( + "const SIGNATURE: &[u8] = &{:?};", + signature.to_cose().unwrap() + ); + println!( + "const SERIALIZED_MESSAGE: &[u8] = &{:?};", + serialized_message.as_bytes() + ); + println!( + "const SIGNED_OBJECT: &[u8] = &{:?};", + signed_object.to_cose().unwrap() + ); + 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 index 3ac8f2e41..5f9f3bd0d 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -4,13 +4,19 @@ use crate::{error::SignatureError, CryptoError}; /// /// 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 - /// [`SignedPublicKeyOwnershipClaim`](crate::signing::claims::SignedPublicKeyOwnershipClaim). - PublicKeyOwnershipClaim = 1, + /// [`SignedPublicKey`](crate::signing::SignedPublicKey). + SignedPublicKey = 1, + /// This namespace is only used in tests and documentation. ExampleNamespace = -1, + /// This namespace is only used in tests and documentation. + ExampleNamespace2 = -2, } impl SigningNamespace { @@ -20,8 +26,9 @@ impl SigningNamespace { pub fn try_from_i64(value: i64) -> Result { match value { - 1 => Ok(Self::PublicKeyOwnershipClaim), + 1 => Ok(Self::SignedPublicKey), -1 => Ok(Self::ExampleNamespace), + -2 => Ok(Self::ExampleNamespace2), _ => Err(SignatureError::InvalidNamespace.into()), } } diff --git a/crates/bitwarden-crypto/src/signing/sign.rs b/crates/bitwarden-crypto/src/signing/sign.rs deleted file mode 100644 index 33b07ff3c..000000000 --- a/crates/bitwarden-crypto/src/signing/sign.rs +++ /dev/null @@ -1,588 +0,0 @@ -//! Signing is used to assert integrity of a message to others or to oneself. -//! -//! Signing and signature verification operations are divided into three layers here: -//! - (public) High-level: Give a struct, namespace, and get a signed object or signature + -//! serialized message. Purpose: Serialization should not be decided by the consumer of this -//! interface, but rather by the signing implementation. Each consumer shouldn't have to make the -//! decision on how to serialize. Further, the serialization format is written to the signature -//! object, and verified. -//! - Mid-level: Give a byte array, content format, namespace, and get a signed object or signature. -//! Purpose: All signatures should be domain-separated, so that any proofs only need to consider -//! the allowed messages under the current namespace, and cross-protocol attacks are not possible. -//! - Low-level: Give a byte array, and get a signature. Purpose: This just implements the signing -//! of byte arrays. Digital signature schemes generally just care about a set of input bytes to -//! sign; and this operation implements that per-supported digital signature scheme. To add -//! support for a new scheme, only this operation needs to be implemented for the new signing key -//! type. -//! -//! Further, there are two kinds of signing operations supported here: -//! - Sign: Create a signed object that contains the payload. Purpose: If only one signature is -//! needed for an object then it is simpler to keep the signature and payload together in one -//! blob, so they cannot be separated. -//! - Sign detached: Create a signature that does not contain the payload; but the serialized -//! payload is returned. Purpose: If multiple signatures are needed for one object, then sign -//! detached can be used. - -use ciborium::value::Integer; -use coset::iana::CoapContentFormat; -use ed25519_dalek::Signer; -use serde::{de::DeserializeOwned, Serialize}; - -use super::SigningNamespace; -use crate::{ - cose::SIGNING_NAMESPACE, error::SignatureError, CryptoError, RawSigningKey, RawVerifyingKey, - Signature, SignedObject, SigningKey, VerifyingKey, -}; - -impl SigningKey { - /// Signs the given payload with the signing key, under a given [`SigningNamespace`]. - /// This returns a [`Signature`] object, that does not contain the payload. - /// The payload must be stored separately, and needs to be provided when verifying the - /// signature. - /// - /// This should be used when multiple signers are required, or when signatures need to be - /// replaceable without re-uploading the object, or if the signed object should be parseable - /// by the server side, without the use of COSE on the server. - /// ``` - /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey}; - /// use serde::{Serialize, Deserialize}; - /// - /// #[derive(Serialize, Deserialize, Debug, PartialEq)] - /// struct TestMessage { - /// field1: String, - /// } - /// - /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - /// let message = TestMessage { - /// field1: "Test message".to_string(), - /// }; - /// let namespace = SigningNamespace::ExampleNamespace; - /// let (signature, serialized_message) = signing_key.sign_detached(&message, &namespace).unwrap(); - /// // Verification - /// let verifying_key = signing_key.to_verifying_key(); - /// assert!(verifying_key.verify_signature(&serialized_message.as_ref(), &namespace, &signature)); - /// ``` - #[allow(unused)] - pub fn sign_detached( - &self, - message: &Message, - namespace: &SigningNamespace, - ) -> Result<(Signature, SerializedMessage), CryptoError> { - let message = encode_message(message)?; - Ok((self.sign_detached_bytes(&message, namespace), message)) - } - - /// Given a serialized message, signature, this counter-signs the message. That is, if multiple - /// parties want to sign the same message, one party creates the initial message, and the - /// other parties then counter-sign it, and submit their signatures. This can be done as - /// follows: ``` - /// let alice_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - /// let bob_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - /// - /// let message = TestMessage { - /// field1: "Test message".to_string(), - /// }; - /// let namespace = SigningNamespace::ExampleNamespace; - /// let (signature, serialized_message) = alice_key.sign_detached(&message, - /// &namespace).unwrap();\ // Alice shares (signature, serialized_message) with Bob. - /// // Bob verifies the contents of serialized_message using application logic, then signs it: - /// let (bob_signature, serialized_message) = bob_key.counter_sign(&serialized_message, - /// &signature, &namespace).unwrap(); ``` - #[allow(unused)] - pub fn counter_sign_detached( - &self, - serialized_message_bytes: Vec, - 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 { - serialized_message_bytes, - content_type: initial_signature.content_type()?, - }, - namespace, - )) - } - - /// Signs the given payload with the signing key, under a given namespace. - /// This is is the underlying implementation of the `sign_detached` method, and takes - /// a raw byte array as input. - fn sign_detached_bytes( - &self, - message: &SerializedMessage, - namespace: &SigningNamespace, - ) -> Signature { - Signature::from( - coset::CoseSign1Builder::new() - .protected( - coset::HeaderBuilder::new() - .algorithm(self.cose_algorithm()) - .key_id((&self.id).into()) - .content_format(message.content_type) - .value( - SIGNING_NAMESPACE, - ciborium::Value::Integer(Integer::from(namespace.as_i64())), - ) - .build(), - ) - .create_detached_signature(&message.serialized_message_bytes, &[], |pt| { - self.sign_raw(pt) - }) - .build(), - ) - } - - /// Signs the given payload with the signing key, under a given namespace. - /// This returns a [`SignedObject`] object, that contains the payload. - /// The payload is included in the signature, and does not need to be provided when verifying - /// the signature. - /// - /// This should be used when only one signer is required, so that only one object needs to be - /// kept track of. - /// ``` - /// use bitwarden_crypto::{SigningNamespace, SignatureAlgorithm, SigningKey}; - /// use serde::{Serialize, Deserialize}; - /// - /// #[derive(Serialize, Deserialize, Debug, PartialEq)] - /// struct TestMessage { - /// field1: String, - /// } - /// - /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - /// let message = TestMessage { - /// field1: "Test message".to_string(), - /// }; - /// let namespace = SigningNamespace::ExampleNamespace; - /// let signed_object = signing_key.sign(&message, &namespace).unwrap(); - /// // The signed object can be verified using the verifying key: - /// let verifying_key = signing_key.to_verifying_key(); - /// let payload: TestMessage = verifying_key.get_verified_payload(&signed_object, &namespace).unwrap(); - /// assert_eq!(payload, message); - /// ``` - pub fn sign( - &self, - message: &Message, - namespace: &SigningNamespace, - ) -> Result { - let message = encode_message(message)?; - self.sign_bytes(&message, namespace) - } - - /// Signs the given payload with the signing key, under a given namespace. - /// This is is the underlying implementation of the `sign` method, and takes - /// a raw byte array as input. - fn sign_bytes( - &self, - serialized_message: &SerializedMessage, - namespace: &SigningNamespace, - ) -> Result { - let cose_sign1 = coset::CoseSign1Builder::new() - .protected( - coset::HeaderBuilder::new() - .algorithm(self.cose_algorithm()) - .key_id((&self.id).into()) - .content_format(serialized_message.content_type) - .value( - SIGNING_NAMESPACE, - ciborium::Value::Integer(Integer::from(namespace.as_i64())), - ) - .build(), - ) - .payload(serialized_message.serialized_message_bytes.clone()) - .create_signature(&[], |pt| self.sign_raw(pt)) - .build(); - Ok(SignedObject(cose_sign1)) - } - - /// Signs the given byte array with the signing key. - /// This should never be used directly, but only through the `sign` method, to enforce - /// strong domain separation of the signatures. - fn sign_raw(&self, data: &[u8]) -> Vec { - match &self.inner { - RawSigningKey::Ed25519(key) => key.sign(data).to_bytes().to_vec(), - } - } -} - -/// A message (struct), serialized to a byte array, along with the content format of the bytes. -pub struct SerializedMessage { - serialized_message_bytes: Vec, - content_type: CoapContentFormat, -} - -impl AsRef<[u8]> for SerializedMessage { - fn as_ref(&self) -> &[u8] { - &self.serialized_message_bytes - } -} - -impl SerializedMessage { - pub fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { - SerializedMessage { - serialized_message_bytes: bytes, - content_type, - } - } -} - -impl VerifyingKey { - /// Verifies the signature of the given serialized message bytes, created by - /// [`SigningKey::sign_detached`], for the given namespace. The namespace must match the one - /// used to create the signature. - #[allow(unused)] - pub fn verify_signature( - &self, - serialized_message_bytes: &[u8], - namespace: &SigningNamespace, - signature: &Signature, - ) -> bool { - let Some(_alg) = &signature.inner().protected.header.alg else { - return false; - }; - - let Ok(signature_namespace) = signature.namespace() else { - return false; - }; - if signature_namespace != *namespace { - return false; - } - - signature - .inner() - .verify_detached_signature(serialized_message_bytes, &[], |sig, data| { - self.verify_raw(sig, data) - }) - .is_ok() - } - - /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given - /// namespace and returns the deserialized payload, if the signature is valid. - pub fn get_verified_payload( - &self, - signed_object: &SignedObject, - namespace: &SigningNamespace, - ) -> Result { - let payload_bytes = self.get_verified_payload_bytes(signed_object, namespace)?; - decode_message(&SerializedMessage { - serialized_message_bytes: payload_bytes, - content_type: signed_object.content_type()?, - }) - } - - /// Verifies the signature of a signed object, created by [`SigningKey::sign`], for the given - /// namespace and returns the raw payload bytes, if the signature is valid. - fn get_verified_payload_bytes( - &self, - signed_object: &SignedObject, - namespace: &SigningNamespace, - ) -> Result, CryptoError> { - let Some(_alg) = &signed_object.inner().protected.header.alg else { - return Err(SignatureError::InvalidSignature.into()); - }; - - let signature_namespace = signed_object.namespace()?; - if signature_namespace != *namespace { - return Err(SignatureError::InvalidNamespace.into()); - } - - signed_object - .inner() - .verify_signature(&[], |sig, data| self.verify_raw(sig, data))?; - signed_object.payload() - } - - /// Verifies the signature of the given data, for the given namespace. - /// This should never be used directly, but only through the `verify` method, to enforce - /// strong domain separation of the signatures. - fn verify_raw(&self, signature: &[u8], data: &[u8]) -> Result<(), CryptoError> { - match &self.inner { - RawVerifyingKey::Ed25519(key) => { - let sig = ed25519_dalek::Signature::from_bytes( - signature - .try_into() - .map_err(|_| SignatureError::InvalidSignature)?, - ); - key.verify_strict(data, &sig) - .map_err(|_| SignatureError::InvalidSignature.into()) - } - } - } -} - -fn encode_message(message: &Message) -> Result { - let mut buffer = Vec::new(); - ciborium::ser::into_writer(message, &mut buffer).map_err(|_| CryptoError::CoseEncodingError)?; - Ok(SerializedMessage { - serialized_message_bytes: buffer, - content_type: CoapContentFormat::Cbor, - }) -} - -fn decode_message( - message: &SerializedMessage, -) -> Result { - if message.content_type != CoapContentFormat::Cbor { - return Err(CryptoError::CoseEncodingError); - } - - let decoded = ciborium::de::from_reader(message.serialized_message_bytes.as_slice()) - .map_err(|_| CryptoError::CoseEncodingError)?; - Ok(decoded) -} - -#[cfg(test)] -mod tests { - use serde::Deserialize; - - use super::*; - use crate::SignatureAlgorithm; - - /// The function used to create the test vectors below, and can be used to re-generate them. - /// Once rolled out to user accounts, this function can be removed, because at that point we - /// cannot introduce format-breaking changes anymore. - #[test] - fn make_test_vectors() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let test_message = TestMessage { - field1: "Test message".to_string(), - }; - let namespace = SigningNamespace::ExampleNamespace; - - let (signature, serialized_message) = signing_key - .sign_detached(&test_message, &namespace) - .unwrap(); - let signed_object = signing_key.sign(&test_message, &namespace).unwrap(); - - println!( - "const SIGNING_KEY: &[u8] = &{:?};", - signing_key.to_cose().unwrap() - ); - println!( - "const VERIFYING_KEY: &[u8] = &{:?};", - verifying_key.to_cose().unwrap() - ); - println!( - "const SIGNATURE: &[u8] = &{:?};", - signature.to_cose().unwrap() - ); - println!( - "const SERIALIZED_MESSAGE: &[u8] = &{:?};", - serialized_message.serialized_message_bytes - ); - println!( - "const SIGNED_OBJECT: &[u8] = &{:?};", - signed_object.to_cose().unwrap() - ); - } - - const SIGNING_KEY: &[u8] = &[ - 166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155, - 16, 3, 39, 4, 130, 1, 2, 35, 88, 32, 114, 65, 45, 133, 77, 188, 130, 57, 89, 250, 113, 125, - 108, 138, 255, 68, 3, 202, 189, 96, 31, 218, 197, 24, 35, 127, 52, 168, 232, 85, 95, 199, - 32, 6, - ]; - const VERIFYING_KEY: &[u8] = &[ - 166, 1, 1, 2, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, 105, 158, 216, 103, 155, - 16, 3, 39, 4, 129, 2, 32, 6, 33, 88, 32, 91, 255, 95, 169, 53, 21, 222, 134, 102, 103, 105, - 224, 58, 210, 82, 121, 141, 60, 76, 68, 9, 26, 242, 215, 111, 150, 228, 154, 141, 143, 108, - 38, - ]; - const SIGNATURE: &[u8] = &[ - 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, - 105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 246, 88, 64, 110, 91, 1, 209, 74, - 57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243, 61, 167, - 248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168, 10, 173, - 145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85, 247, 99, - 14, - ]; - const SERIALIZED_MESSAGE: &[u8] = &[ - 161, 102, 102, 105, 101, 108, 100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, - 103, 101, - ]; - const SIGNED_OBJECT: &[u8] = &[ - 132, 88, 30, 164, 1, 39, 3, 24, 60, 4, 80, 91, 154, 106, 83, 253, 98, 76, 188, 129, 226, - 105, 158, 216, 103, 155, 16, 58, 0, 1, 56, 127, 32, 160, 85, 161, 102, 102, 105, 101, 108, - 100, 49, 108, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 88, 64, 110, 91, 1, - 209, 74, 57, 108, 168, 211, 218, 58, 247, 112, 21, 205, 127, 120, 156, 192, 98, 81, 243, - 61, 167, 248, 236, 19, 115, 168, 62, 57, 170, 232, 138, 219, 159, 68, 193, 144, 100, 168, - 10, 173, 145, 72, 179, 236, 78, 94, 9, 135, 117, 153, 135, 126, 30, 70, 111, 109, 235, 85, - 247, 99, 14, - ]; - - #[derive(Serialize, Deserialize, Debug, PartialEq)] - struct TestMessage { - field1: String, - } - - #[test] - fn test_vectors() { - let signing_key = SigningKey::from_cose(SIGNING_KEY).unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let signature = Signature::from_cose(SIGNATURE).unwrap(); - let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); - - assert_eq!(signing_key.to_cose().unwrap(), SIGNING_KEY); - assert_eq!(verifying_key.to_cose().unwrap(), VERIFYING_KEY); - assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); - - assert_eq!( - signature.namespace().unwrap(), - SigningNamespace::ExampleNamespace - ); - assert_eq!(signature.content_type().unwrap(), CoapContentFormat::Cbor); - assert_eq!(signature.to_cose().unwrap(), SIGNATURE); - - assert_eq!(signed_object.payload().unwrap(), SERIALIZED_MESSAGE); - assert_eq!( - signed_object.namespace().unwrap(), - SigningNamespace::ExampleNamespace - ); - assert_eq!( - signed_object.content_type().unwrap(), - CoapContentFormat::Cbor - ); - assert_eq!(signed_object.to_cose().unwrap(), SIGNED_OBJECT); - - let verified_payload: TestMessage = verifying_key - .get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace) - .unwrap(); - assert_eq!( - verified_payload, - TestMessage { - field1: "Test message".to_string() - } - ); - assert!(verifying_key.verify_signature( - SERIALIZED_MESSAGE, - &SigningNamespace::ExampleNamespace, - &signature - )); - } - - #[test] - fn test_sign_detached_roundtrip() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = TestMessage { - field1: "Test message".to_string(), - }; - let namespace = SigningNamespace::ExampleNamespace; - let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); - assert!(verifying_key.verify_signature( - &serialized_message.serialized_message_bytes, - &namespace, - &signature - )); - let decoded_message: TestMessage = decode_message(&serialized_message).unwrap(); - assert_eq!(decoded_message, data); - } - - #[test] - fn test_sign_roundtrip() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = "Test message".to_string(); - let namespace = SigningNamespace::ExampleNamespace; - let signed_object = signing_key.sign(&data, &namespace).unwrap(); - let payload: String = verifying_key - .get_verified_payload(&signed_object, &namespace) - .unwrap(); - assert_eq!(payload, data); - } - - #[test] - fn test_countersign_roundtrip() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = "Test message".to_string(); - let namespace = SigningNamespace::ExampleNamespace; - let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); - let countersignature = signing_key - .counter_sign_detached( - serialized_message.serialized_message_bytes.clone(), - &signature, - &namespace, - ) - .unwrap(); - assert!(verifying_key.verify_signature( - &serialized_message.serialized_message_bytes, - &namespace, - &countersignature - )); - } - - #[test] - fn test_changed_payload_fails() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = "Test message".to_string(); - let namespace = SigningNamespace::ExampleNamespace; - - let (signature, mut serialized_message) = - signing_key.sign_detached(&data, &namespace).unwrap(); - let modified_message = serialized_message - .serialized_message_bytes - .get_mut(0) - .unwrap(); - *modified_message = 0xFF; - assert!(!verifying_key.verify_signature( - &serialized_message.serialized_message_bytes, - &namespace, - &signature - )); - } - - #[test] - fn test_changed_namespace_fails() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::ExampleNamespace; - let other_namespace = SigningNamespace::PublicKeyOwnershipClaim; - - let (signature, serialized_message) = signing_key.sign_detached(&data, &namespace).unwrap(); - assert!(!verifying_key.verify_signature( - &serialized_message.serialized_message_bytes, - &other_namespace, - &signature - )); - assert!(verifying_key.verify_signature( - &serialized_message.serialized_message_bytes, - &namespace, - &signature - )); - } - - #[test] - fn test_changed_namespace_fails_signed_object() { - let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let verifying_key = signing_key.to_verifying_key(); - let data = b"Test message"; - let namespace = SigningNamespace::ExampleNamespace; - let other_namespace = SigningNamespace::PublicKeyOwnershipClaim; - let signed_object = signing_key.sign(data, &namespace).unwrap(); - assert!(verifying_key - .get_verified_payload::>(&signed_object, &other_namespace) - .is_err()); - assert!(verifying_key - .get_verified_payload::>(&signed_object, &namespace) - .is_ok()); - } - - #[test] - fn test_encode_decode_message() { - let message = TestMessage { - field1: "Hello".to_string(), - }; - let encoded = encode_message(&message).unwrap(); - let decoded = decode_message(&encoded).unwrap(); - assert_eq!(message, decoded); - } -} diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs new file mode 100644 index 000000000..23c1b344a --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -0,0 +1,311 @@ +use ciborium::value::Integer; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1, Label, RegisteredLabel}; +use serde::Serialize; + +use super::{message::SerializedMessage, signing_key::SigningKey, SigningNamespace, VerifyingKey}; +use crate::{ + cose::{CoseSerializable, SIGNING_NAMESPACE}, + error::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 + } + + pub(self) fn namespace(&self) -> Result { + let namespace = self + .0 + .protected + .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_i64( + i128::from(namespace) + .try_into() + .map_err(|_| SignatureError::InvalidNamespace)?, + ) + } + + pub(super) fn content_type(&self) -> Result { + if let RegisteredLabel::Assigned(content_format) = self + .0 + .protected + .header + .content_type + .clone() + .ok_or(CryptoError::from(SignatureError::InvalidSignature))? + { + Ok(content_format) + } else { + Err(SignatureError::InvalidSignature.into()) + } + } + + /// Verifies the signature of the given serialized message bytes, created by + /// [`SigningKey::sign_detached`], for the given namespace. The namespace must match the one + /// used to create the signature. + #[allow(unused)] + pub fn verify( + &self, + serialized_message_bytes: &[u8], + verifying_key: &VerifyingKey, + namespace: &SigningNamespace, + ) -> bool { + let Some(_alg) = &self.inner().protected.header.alg else { + return false; + }; + + let Ok(signature_namespace) = self.namespace() else { + return false; + }; + if signature_namespace != *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}; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let (signature, serialized_message) = signing_key.sign_detached(&message, &namespace).unwrap(); + /// // Verification + /// let verifying_key = signing_key.to_verifying_key(); + /// assert!(signature.verify(&serialized_message.as_bytes(), &verifying_key, &namespace)); + /// ``` + #[allow(unused)] + 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).unwrap(); + /// let bob_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let (signature, serialized_message) = alice_key.sign_detached(&message, + /// &namespace).unwrap();\ // Alice shares (signature, serialized_message) with Bob. + /// // Bob verifies the contents of serialized_message using application logic, then signs it: + /// let (bob_signature, serialized_message) = bob_key.counter_sign(&serialized_message, + /// &signature, &namespace).unwrap(); ``` + 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(|_| SignatureError::InvalidSignature)?; + Ok(Signature(cose_sign1)) + } + + fn to_cose(&self) -> Result, CryptoError> { + self.0 + .clone() + .to_vec() + .map_err(|_| SignatureError::InvalidSignature.into()) + } +} + +#[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().unwrap(); + 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).unwrap(); + 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).unwrap(); + 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).unwrap(); + 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..9eb1eaff5 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -0,0 +1,266 @@ +use ciborium::value::Integer; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1, Label, RegisteredLabel}; +use serde::{de::DeserializeOwned, Serialize}; + +use super::{ + message::SerializedMessage, signing_key::SigningKey, verifying_key::VerifyingKey, + SigningNamespace, +}; +use crate::{ + cose::{CoseSerializable, SIGNING_NAMESPACE}, + error::SignatureError, + CryptoError, +}; + +pub struct SignedObject(pub(crate) CoseSign1); + +impl From for SignedObject { + fn from(cose_sign1: CoseSign1) -> Self { + SignedObject(cose_sign1) + } +} + +impl SignedObject { + pub fn content_type(&self) -> Result { + if let RegisteredLabel::Assigned(content_format) = self + .0 + .protected + .header + .content_type + .clone() + .ok_or(CryptoError::from(SignatureError::InvalidSignature))? + { + Ok(content_format) + } else { + Err(SignatureError::InvalidSignature.into()) + } + } + + pub(crate) fn inner(&self) -> &CoseSign1 { + &self.0 + } + + pub(crate) fn namespace(&self) -> Result { + let namespace = self + .0 + .protected + .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_i64( + i128::from(namespace) + .try_into() + .map_err(|_| SignatureError::InvalidNamespace)?, + ) + } + + 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() + } + + /// 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> { + let Some(_alg) = &self.inner().protected.header.alg else { + return Err(SignatureError::InvalidSignature.into()); + }; + + let signature_namespace = self.namespace()?; + if signature_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}; + /// + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct TestMessage { + /// field1: String, + /// } + /// + /// let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + /// let message = TestMessage { + /// field1: "Test message".to_string(), + /// }; + /// let namespace = SigningNamespace::ExampleNamespace; + /// let signed_object = signing_key.sign(&message, &namespace).unwrap(); + /// // The signed object can be verified using the verifying key: + /// let verifying_key = signing_key.to_verifying_key(); + /// let payload: TestMessage = 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(|_| SignatureError::InvalidSignature)?, + )) + } + + fn to_cose(&self) -> Result, CryptoError> { + self.0 + .clone() + .to_vec() + .map_err(|_| SignatureError::InvalidSignature.into()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, TestMessage, 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, + ]; + + #[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().unwrap(); + 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(crate::SignatureAlgorithm::Ed25519).unwrap(); + 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(crate::SignatureAlgorithm::Ed25519).unwrap(); + 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..90c375160 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -0,0 +1,183 @@ +use ciborium::{value::Integer, Value}; +use coset::{ + iana::{Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, + CborSerializable, CoseKey, Label, RegisteredLabel, RegisteredLabelWithPrivate, +}; +use ed25519_dalek::Signer; +use rand::rngs::OsRng; +use zeroize::ZeroizeOnDrop; + +use super::{ + verifying_key::{RawVerifyingKey, VerifyingKey}, + SignatureAlgorithm, +}; +use crate::{ + cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey, KEY_ID_SIZE, +}; + +/// A `SigningKey` without the key id. This enum contains a variant for each supported signature +/// scheme. +#[derive(Clone, zeroize::ZeroizeOnDrop)] +enum RawSigningKey { + Ed25519(ed25519_dalek::SigningKey), +} + +/// A signing key is a private key used for signing data. An associated `VerifyingKey` can be +/// derived from it. +#[derive(Clone, ZeroizeOnDrop)] +pub struct SigningKey { + pub(super) id: KeyId, + inner: RawSigningKey, +} + +impl CryptoKey for SigningKey {} + +impl SigningKey { + /// Makes a new signing key for the given signature scheme. + pub fn make(algorithm: SignatureAlgorithm) -> Result { + match algorithm { + SignatureAlgorithm::Ed25519 => Ok(SigningKey { + id: KeyId::make(), + inner: RawSigningKey::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), + }), + } + } + + 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 never be used directly by anything other than the second layer defined in + /// [super::message]. + pub 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) -> Result> { + 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() + .map_err(|_| CryptoError::InvalidKey) + } + } + } + + /// 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(|_| CryptoError::InvalidKey)?; + + let Some(algorithm) = cose_key.alg else { + return Err(CryptoError::InvalidKey); + }; + let key_id: [u8; KEY_ID_SIZE] = cose_key + .key_id + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key_id: KeyId = key_id.into(); + match (cose_key.kty, algorithm) { + (kty, alg) + if kty == RegisteredLabel::Assigned(KeyType::OKP) + && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let (mut crv, mut d) = (None, None); + for (key, value) in &cose_key.params { + if let Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + match key { + OkpKeyParameter::Crv => { + crv.replace(value); + } + OkpKeyParameter::D => { + d.replace(value); + } + _ => (), + } + } + } + + let (Some(d), Some(crv)) = (d, crv) else { + return Err(CryptoError::InvalidKey); + }; + let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); + if crv == EllipticCurve::Ed25519.to_i64().into() { + let secret_key_bytes: &[u8; 32] = d + .as_bytes() + .ok_or(CryptoError::InvalidKey)? + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes); + Ok(SigningKey { + id: key_id, + inner: RawSigningKey::Ed25519(key), + }) + } else { + Err(CryptoError::InvalidKey) + } + } + _ => Err(CryptoError::InvalidKey), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cose_roundtrip_encode_signing() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + let cose = signing_key.to_cose().unwrap(); + let parsed_key = SigningKey::from_cose(&cose).unwrap(); + + assert_eq!( + signing_key.to_cose().unwrap(), + parsed_key.to_cose().unwrap() + ); + } + + #[test] + fn test_sign_rountrip() { + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); + 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..db99a4596 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -0,0 +1,192 @@ +//! 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::SignatureAlgorithm; +use crate::{cose::CoseSerializable, error::SignatureError, keys::KeyId, CryptoError, KEY_ID_SIZE}; + +/// 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) -> Result, CryptoError> { + match &self.inner { + RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() + .key_id((&self.id).into()) + .algorithm(Algorithm::EdDSA) + .param( + OkpKeyParameter::Crv.to_i64(), + 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(), + Value::Bytes(key.to_bytes().to_vec()), + ) + .add_key_op(KeyOperation::Verify) + .build() + .to_vec() + .map_err(|_| CryptoError::InvalidKey), + } + } + + fn from_cose(bytes: &[u8]) -> Result + where + Self: Sized, + { + let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; + + let Some(algorithm) = cose_key.alg else { + return Err(CryptoError::InvalidKey); + }; + let key_id: [u8; KEY_ID_SIZE] = cose_key + .key_id + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let key_id: KeyId = key_id.into(); + match (cose_key.kty, algorithm) { + (kty, alg) + if kty == RegisteredLabel::Assigned(KeyType::OKP) + && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + { + let (mut crv, mut x) = (None, None); + for (key, value) in &cose_key.params { + if let coset::Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + match key { + OkpKeyParameter::Crv => { + crv.replace(value); + } + OkpKeyParameter::X => { + x.replace(value); + } + _ => (), + } + } + } + let (Some(x), Some(crv)) = (x, crv) else { + return Err(CryptoError::InvalidKey); + }; + + if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) + != EllipticCurve::Ed25519.to_i64().into() + { + return Err(CryptoError::InvalidKey); + } + + let verifying_key_bytes: &[u8; 32] = x + .as_bytes() + .ok_or(CryptoError::InvalidKey)? + .as_slice() + .try_into() + .map_err(|_| CryptoError::InvalidKey)?; + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) + .map_err(|_| CryptoError::InvalidKey)?; + Ok(VerifyingKey { + id: key_id, + inner: RawVerifyingKey::Ed25519(verifying_key), + }) + } + _ => Err(CryptoError::InvalidKey), + } + } +} + +#[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().unwrap(); + let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); + + assert_eq!( + verifying_key.to_cose().unwrap(), + parsed_key.to_cose().unwrap() + ); + } + + #[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 7d2f7d321..2ac5838f3 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -8,9 +8,10 @@ use zeroize::Zeroizing; use super::KeyStoreInner; use crate::{ - derive_shareable_key, error::UnsupportedOperation, store::backend::StoreBackend, - AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature, - SignatureAlgorithm, SignedObject, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, + derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, + CryptoError, EncString, KeyId, KeyIds, PrivateKey, Result, Signature, SignatureAlgorithm, + SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, + UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] @@ -237,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(), ) } @@ -300,13 +301,25 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn dangerous_get_asymmetric_key( - &self, - key_id: Ids::Asymmetric, - ) -> Result<&AsymmetricCryptoKey> { + pub fn dangerous_get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&PrivateKey> { 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) + } + #[deprecated(note = "This function should ideally never be used outside this crate")] pub fn dangerous_get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { self.get_signing_key(key_id) @@ -321,7 +334,7 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { + fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&PrivateKey> { if key_id.is_local() { self.local_asymmetric_keys.get(key_id) } else { @@ -358,11 +371,7 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn set_asymmetric_key( - &mut self, - key_id: Ids::Asymmetric, - key: AsymmetricCryptoKey, - ) -> Result<()> { + pub fn set_asymmetric_key(&mut self, key_id: Ids::Asymmetric, key: PrivateKey) -> Result<()> { if key_id.is_local() { self.local_asymmetric_keys.upsert(key_id, key); } else { @@ -430,8 +439,7 @@ impl KeyStoreContext<'_, Ids> { message: &Message, namespace: &crate::SigningNamespace, ) -> Result { - let key = self.get_signing_key(key)?; - key.sign(message, namespace) + self.get_signing_key(key)?.sign(message, namespace) } /// Signs the given data using the specified signing key, for the given @@ -443,10 +451,8 @@ impl KeyStoreContext<'_, Ids> { key: Ids::Signing, message: &Message, namespace: &crate::SigningNamespace, - ) -> Result<(Signature, Vec)> { - let key = self.get_signing_key(key)?; - let (signature, serialized_message) = key.sign_detached(message, namespace)?; - Ok((signature, serialized_message.as_ref().to_vec())) + ) -> Result<(Signature, signing::SerializedMessage)> { + self.get_signing_key(key)?.sign_detached(message, namespace) } } @@ -570,7 +576,7 @@ mod tests { ) .unwrap(); let payload: Result = - verifying_key.get_verified_payload(&signed_object, &SigningNamespace::ExampleNamespace); + signed_object.verify_and_unwrap(&verifying_key, &SigningNamespace::ExampleNamespace); assert!(payload.is_ok()); let (signature, serialized_message) = store @@ -583,10 +589,10 @@ mod tests { &SigningNamespace::ExampleNamespace, ) .unwrap(); - assert!(verifying_key.verify_signature( - &serialized_message, - &SigningNamespace::ExampleNamespace, - &signature - )); + assert!(signature.verify( + serialized_message.as_bytes(), + &verifying_key, + &SigningNamespace::ExampleNamespace + )) } } diff --git a/crates/bitwarden-crypto/src/traits/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index d6c0c562e..ccccf4eb3 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -76,15 +76,15 @@ impl, Output> #[cfg(test)] mod tests { use crate::{ - traits::tests::*, AsymmetricCryptoKey, Decryptable, Encryptable, KeyStore, - SymmetricCryptoKey, + traits::tests::*, Decryptable, Encryptable, KeyStore, PrivateKey, + PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, }; fn test_store() -> KeyStore { let store = KeyStore::::default(); let symm_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); - let asymm_key = AsymmetricCryptoKey::make(); + let asymm_key = PrivateKey::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 854149424..6400afdc2 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, SigningKey, SymmetricCryptoKey}; +use crate::{CryptoKey, PrivateKey, 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. @@ -29,7 +29,7 @@ pub trait KeyId: /// At the moment it's just symmetric and asymmetric keys. pub trait KeyIds { type Symmetric: KeyId; - type Asymmetric: KeyId; + type Asymmetric: KeyId; type Signing: KeyId; } @@ -100,7 +100,7 @@ macro_rules! key_ids { }; ( @key_type symmetric ) => { $crate::SymmetricCryptoKey }; - ( @key_type asymmetric ) => { $crate::AsymmetricCryptoKey }; + ( @key_type asymmetric ) => { $crate::PrivateKey }; ( @key_type signing ) => { $crate::SigningKey }; ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant ( _ ) }; diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 2e30c2d0c..78fb85820 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -2,9 +2,9 @@ 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, SignatureAlgorithm, - SignedPublicKeyOwnershipClaim, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, + CoseSerializable, CryptoError, Decryptable, EncString, Encryptable, Kdf, KeyDecryptable, + KeyEncryptable, KeyStore, MasterKey, PrivateKey, PublicKey, SignatureAlgorithm, + SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -244,7 +244,7 @@ impl PureCrypto { shared_key: Vec, encapsulation_key: Vec, ) -> Result { - let encapsulation_key = AsymmetricPublicCryptoKey::from_der(encapsulation_key.as_slice())?; + let encapsulation_key = PublicKey::from_der(encapsulation_key.as_slice())?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(shared_key)?, &encapsulation_key, @@ -260,9 +260,7 @@ impl PureCrypto { decapsulation_key: Vec, ) -> Result, CryptoError> { Ok(UnsignedSharedKey::from_str(encapsulated_key.as_str())? - .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der( - decapsulation_key.as_slice(), - )?)? + .decapsulate_key_unsigned(&PrivateKey::from_der(decapsulation_key.as_slice())?)? .to_encoded()) } @@ -291,15 +289,15 @@ impl PureCrypto { /// identity claimed ownership of the public key. This is a one-sided claim and merely shows /// that the signing identity has the intent to receive messages encrypted to the public /// key. - pub fn verify_public_key_ownership_claim( - claim: Vec, - public_key: Vec, + pub fn verify_and_unwrap_signed_public_key( + signed_public_key: Vec, verifying_key: Vec, - ) -> Result { - let claim = SignedPublicKeyOwnershipClaim::from_bytes(claim.as_slice())?; - let public_key = AsymmetricPublicCryptoKey::from_der(public_key.as_slice())?; + ) -> Result, CryptoError> { + let signed_public_key = SignedPublicKey::try_from(signed_public_key)?; let verifying_key = VerifyingKey::from_cose(verifying_key.as_slice())?; - claim.verify_claim(&public_key, &verifying_key) + signed_public_key + .verify_and_unwrap(&verifying_key) + .map(|public_key| public_key.to_der())? } } @@ -374,17 +372,6 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 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_OWNERSHIP_CLAIM: &[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, 88, 72, 161, 107, 102, 105, 110, 103, - 101, 114, 112, 114, 105, 110, 116, 162, 102, 100, 105, 103, 101, 115, 116, 88, 32, 157, - 225, 74, 231, 216, 192, 213, 240, 234, 67, 3, 221, 30, 3, 145, 141, 17, 73, 71, 233, 20, 4, - 102, 134, 195, 186, 11, 109, 142, 59, 25, 59, 105, 97, 108, 103, 111, 114, 105, 116, 104, - 109, 102, 83, 104, 97, 50, 53, 54, 88, 64, 19, 244, 252, 60, 39, 88, 200, 62, 208, 147, - 106, 200, 57, 125, 189, 6, 253, 109, 197, 164, 207, 193, 15, 242, 195, 241, 4, 229, 235, - 178, 207, 61, 157, 51, 178, 6, 151, 49, 129, 21, 206, 105, 158, 174, 88, 206, 11, 149, 138, - 27, 103, 15, 251, 110, 251, 148, 233, 124, 129, 29, 41, 250, 47, 10, - ]; 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, @@ -511,8 +498,8 @@ 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 decapsulation_key = PrivateKey::from_pem(PEM_KEY).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()) @@ -524,7 +511,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_wrap_decapsulation_key() { - let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); + let decapsulation_key = PrivateKey::from_pem(PEM_KEY).unwrap(); let wrapping_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let wrapped_key = PureCrypto::wrap_decapsulation_key( decapsulation_key.to_der().unwrap(), @@ -539,8 +526,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] 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 decapsulation_key = PrivateKey::from_pem(PEM_KEY).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(); @@ -552,16 +539,6 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= assert_eq!(shared_key, unwrapped_key); } - #[test] - fn test_verify_public_key_ownership_claim() { - let public_key = AsymmetricPublicCryptoKey::from_der(PUBLIC_KEY).unwrap(); - let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let claim = SignedPublicKeyOwnershipClaim::from_bytes(PUBLIC_KEY_OWNERSHIP_CLAIM).unwrap(); - let result = claim.verify_claim(&public_key, &verifying_key); - assert!(result.is_ok()); - assert!(result.unwrap()); - } - #[test] fn test_key_algorithm_for_verifying_key() { let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); diff --git a/crates/memory-testing/src/main.rs b/crates/memory-testing/src/main.rs index 67f38b0e8..e2877ddcc 100644 --- a/crates/memory-testing/src/main.rs +++ b/crates/memory-testing/src/main.rs @@ -31,7 +31,7 @@ fn main() { symmetric_keys.push((key.to_encoded(), key)); } memory_testing::CaseCommand::AsymmetricKey { private_key } => { - let key = bitwarden_crypto::AsymmetricCryptoKey::from_pem(&private_key).unwrap(); + let key = bitwarden_crypto::PrivateKey::from_pem(&private_key).unwrap(); asymmetric_keys.push(key); } memory_testing::CaseCommand::MasterKey { From 26bf715ac63c40fe636b44b0de40d6c9a7f58d4e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 13:58:48 +0200 Subject: [PATCH 036/112] Undo renames --- .../bitwarden-core/src/auth/auth_request.rs | 12 ++++---- crates/bitwarden-core/src/auth/tde.rs | 4 +-- .../src/client/encryption_settings.rs | 4 +-- crates/bitwarden-core/src/mobile/crypto.rs | 10 +++---- .../src/enc_string/asymmetric.rs | 14 ++++----- .../src/keys/asymmetric_crypto_key.rs | 30 +++++++++---------- .../bitwarden-crypto/src/keys/device_key.rs | 6 ++-- crates/bitwarden-crypto/src/keys/mod.rs | 2 +- .../src/keys/signed_public_key.rs | 12 ++++---- crates/bitwarden-crypto/src/store/context.rs | 8 ++--- .../src/traits/encryptable.rs | 4 +-- crates/bitwarden-crypto/src/traits/key_id.rs | 4 +-- .../src/pure_crypto.rs | 12 ++++---- crates/memory-testing/src/main.rs | 2 +- 14 files changed, 62 insertions(+), 62 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index e56381f00..8bd3d9008 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - fingerprint, generate_random_alphanumeric, CryptoError, PrivateKey, PublicKey, + fingerprint, generate_random_alphanumeric, CryptoError, AsymmetricCryptoKey, AsymmetricCryptoPublicKey, PublicKeyEncryptionAlgorithm, UnsignedSharedKey, }; #[cfg(feature = "internal")] @@ -31,7 +31,7 @@ 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 key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + let key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); let spki = key.to_public_key().to_der()?; @@ -52,7 +52,7 @@ pub(crate) fn auth_request_decrypt_user_key( private_key: String, user_key: UnsignedSharedKey, ) -> Result { - let key = PrivateKey::from_der(&STANDARD.decode(private_key)?)?; + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; let key: SymmetricCryptoKey = user_key.decapsulate_key_unsigned(&key)?; Ok(key) } @@ -66,7 +66,7 @@ pub(crate) fn auth_request_decrypt_master_key( ) -> Result { use bitwarden_crypto::MasterKey; - let key = PrivateKey::from_der(&STANDARD.decode(private_key)?)?; + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; let master_key: SymmetricCryptoKey = master_key.decapsulate_key_unsigned(&key)?; let master_key = MasterKey::try_from(&master_key)?; @@ -91,7 +91,7 @@ pub(crate) fn approve_auth_request( client: &Client, public_key: String, ) -> Result { - let public_key = PublicKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); @@ -118,7 +118,7 @@ fn test_auth_request() { ]; let private_key = - PrivateKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); + AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(secret.clone()).unwrap(), diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 6bcbdea42..82fae9670 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - DeviceKey, EncString, Kdf, PublicKey, SymmetricCryptoKey, TrustDeviceResponse, + DeviceKey, EncString, Kdf, AsymmetricCryptoPublicKey, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; @@ -15,7 +15,7 @@ pub(super) fn make_register_tde_keys( org_public_key: String, remember_device: bool, ) -> Result { - let public_key = PublicKey::from_der(&STANDARD.decode(org_public_key)?)?; + let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(org_public_key)?)?; let user_key = UserKey::new(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); let key_pair = user_key.make_key_pair()?; diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 919c14faa..6fa773077 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -42,7 +42,7 @@ impl EncryptionSettings { signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{CoseSerializable, KeyDecryptable, PrivateKey, SigningKey}; + use bitwarden_crypto::{CoseSerializable, KeyDecryptable, AsymmetricCryptoKey, SigningKey}; use log::warn; use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; @@ -52,7 +52,7 @@ impl EncryptionSettings { // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a recovery // process in place. - PrivateKey::from_der(&dec) + AsymmetricCryptoKey::from_der(&dec) .map_err(|_| { warn!("Invalid private key"); }) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 8a776fbde..155b1b8aa 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, - PrivateKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + AsymmetricCryptoKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -404,9 +404,9 @@ pub(super) fn enroll_admin_password_reset( public_key: String, ) -> Result { use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::PublicKey; + use bitwarden_crypto::AsymmetricCryptoPublicKey; - let public_key = PublicKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); // FIXME: [PM-18110] This should be removed once the key store can handle public key encryption @@ -527,7 +527,7 @@ pub(super) fn verify_asymmetric_keys( .map_err(VerifyError::DecryptFailed)?; let private_key = - PrivateKey::from_der(&decrypted_private_key).map_err(VerifyError::ParseFailed)?; + AsymmetricCryptoKey::from_der(&decrypted_private_key).map_err(VerifyError::ParseFailed)?; let derived_public_key_vec = private_key .to_public_key() @@ -828,7 +828,7 @@ mod tests { let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; - let private_key = PrivateKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); + let private_key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); let decrypted: SymmetricCryptoKey = encrypted.decapsulate_key_unsigned(&private_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index 01011dc91..f9d611a8a 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,7 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - PrivateKey, PublicKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, + AsymmetricCryptoKey, AsymmetricCryptoPublicKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion @@ -159,7 +159,7 @@ impl UnsignedSharedKey { /// and thus does not guarantee sender authenticity. pub fn encapsulate_key_unsigned( encapsulated_key: &SymmetricCryptoKey, - encapsulation_key: &PublicKey, + encapsulation_key: &AsymmetricCryptoPublicKey, ) -> Result { match encapsulation_key.inner() { RawPublicKey::RsaOaepSha1(rsa_public_key) => { @@ -192,7 +192,7 @@ impl UnsignedSharedKey { /// guaranteed. pub fn decapsulate_key_unsigned( &self, - decapsulation_key: &PrivateKey, + decapsulation_key: &AsymmetricCryptoKey, ) -> Result { match decapsulation_key.inner() { RawPrivateKey::RsaOaepSha1(rsa_private_key) => { @@ -238,7 +238,7 @@ mod tests { use schemars::schema_for; use super::UnsignedSharedKey; - use crate::{PrivateKey, SymmetricCryptoKey}; + use crate::{AsymmetricCryptoKey, SymmetricCryptoKey}; const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS @@ -271,7 +271,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha256_b64() { - let key_pair = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let key_pair = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); @@ -284,7 +284,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha1_b64() { - let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); @@ -297,7 +297,7 @@ XKZBokBGnjFnTnKcs7nv/O8= #[test] fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() { - let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA=="; let enc_string: UnsignedSharedKey = enc_str.parse().unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 5867258ae..c79a42d46 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -15,18 +15,18 @@ pub(crate) enum RawPublicKey { } #[derive(Clone)] -pub struct PublicKey { +pub struct AsymmetricCryptoPublicKey { inner: RawPublicKey, } -impl PublicKey { +impl AsymmetricCryptoPublicKey { pub(crate) fn inner(&self) -> &RawPublicKey { &self.inner } /// Build a public key from the SubjectPublicKeyInfo DER. pub fn from_der(der: &[u8]) -> Result { - Ok(PublicKey { + Ok(AsymmetricCryptoPublicKey { inner: RawPublicKey::RsaOaepSha1( RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?, ), @@ -55,7 +55,7 @@ pub(crate) enum RawPrivateKey { } #[derive(Clone)] -pub struct PrivateKey { +pub struct AsymmetricCryptoKey { inner: RawPrivateKey, } @@ -67,10 +67,10 @@ const _: () = { assert_zeroize_on_drop::(); } }; -impl zeroize::ZeroizeOnDrop for PrivateKey {} -impl CryptoKey for PrivateKey {} +impl zeroize::ZeroizeOnDrop for AsymmetricCryptoKey {} +impl CryptoKey for AsymmetricCryptoKey {} -impl PrivateKey { +impl AsymmetricCryptoKey { /// Generate a random AsymmetricCryptoKey (RSA-2048). pub fn make(algorithm: PublicKeyEncryptionAlgorithm) -> Self { use rand::rngs::OsRng; @@ -121,9 +121,9 @@ impl PrivateKey { } } - pub fn to_public_key(&self) -> PublicKey { + pub fn to_public_key(&self) -> AsymmetricCryptoPublicKey { match &self.inner { - RawPrivateKey::RsaOaepSha1(private_key) => PublicKey { + RawPrivateKey::RsaOaepSha1(private_key) => AsymmetricCryptoPublicKey { inner: RawPublicKey::RsaOaepSha1(private_key.to_public_key()), }, } @@ -135,7 +135,7 @@ impl PrivateKey { } // We manually implement these to make sure we don't print any sensitive data -impl std::fmt::Debug for PrivateKey { +impl std::fmt::Debug for AsymmetricCryptoKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AsymmetricCryptoKey").finish() } @@ -145,7 +145,7 @@ impl std::fmt::Debug for PrivateKey { mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; - use crate::{PrivateKey, PublicKey, SymmetricCryptoKey, UnsignedSharedKey}; + use crate::{AsymmetricCryptoKey, AsymmetricCryptoPublicKey, SymmetricCryptoKey, UnsignedSharedKey}; #[test] fn test_asymmetric_crypto_key() { @@ -181,8 +181,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let der_key_vec = STANDARD.decode("MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYcafeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4CwmqqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyvb0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZwP7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2famrEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKiszJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx0d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+8vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVRjB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKachvGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI1u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KRJ30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQTjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9ByeKvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiNwEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZUZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEAkY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7Wpt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwNZy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLiCVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzupPFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnfDnqOsltgPomWZ7xVfMkm9niL2OA=").unwrap(); // Load the two different formats and check they are the same key - let pem_key = PrivateKey::from_pem(pem_key_str).unwrap(); - let der_key = PrivateKey::from_der(&der_key_vec).unwrap(); + let pem_key = AsymmetricCryptoKey::from_pem(pem_key_str).unwrap(); + let der_key = AsymmetricCryptoKey::from_der(&der_key_vec).unwrap(); assert_eq!(pem_key.to_der().unwrap(), der_key.to_der().unwrap()); // Check that the keys can be converted back to DER @@ -235,8 +235,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= )) .unwrap(); - let private_key = PrivateKey::from_der(&private_key).unwrap(); - let public_key = PublicKey::from_der(&public_key).unwrap(); + let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); + let public_key = AsymmetricCryptoPublicKey::from_der(&public_key).unwrap(); let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index dcc425e50..0274f5a8f 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,4 +1,4 @@ -use super::{PrivateKey, PublicKeyEncryptionAlgorithm}; +use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm}; use crate::{ error::Result, CryptoError, EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey, UnsignedSharedKey, @@ -32,7 +32,7 @@ impl DeviceKey { pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); - let device_private_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + let device_private_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned( user_key, @@ -63,7 +63,7 @@ impl DeviceKey { protected_user_key: UnsignedSharedKey, ) -> Result { let device_private_key: Vec = protected_device_private_key.decrypt_with_key(&self.0)?; - let device_private_key = PrivateKey::from_der(&device_private_key)?; + let device_private_key = AsymmetricCryptoKey::from_der(&device_private_key)?; let user_key: SymmetricCryptoKey = protected_user_key.decapsulate_key_unsigned(&device_private_key)?; diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 451da0db6..e2225d380 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -11,7 +11,7 @@ pub use symmetric_crypto_key::{ Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key, }; mod asymmetric_crypto_key; -pub use asymmetric_crypto_key::{PrivateKey, PublicKey, PublicKeyEncryptionAlgorithm}; +pub use asymmetric_crypto_key::{AsymmetricCryptoKey, AsymmetricCryptoPublicKey, PublicKeyEncryptionAlgorithm}; pub(crate) use asymmetric_crypto_key::{RawPrivateKey, RawPublicKey}; mod signed_public_key; pub use signed_public_key::{SignedPublicKey, SignedPublicKeyMessage}; diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 0c5766360..192c3eb86 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; -use super::PublicKey; +use super::AsymmetricCryptoPublicKey; use crate::{ cose::CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; @@ -39,7 +39,7 @@ pub struct SignedPublicKeyMessage { } impl SignedPublicKeyMessage { - pub fn from_public_key(public_key: &PublicKey) -> Result { + pub fn from_public_key(public_key: &AsymmetricCryptoPublicKey) -> Result { Ok(SignedPublicKeyMessage { algorithm: PublicKeyEncryptionAlgorithms::RsaOaepSha1, content_format: PublicKeyFormat::Spki, @@ -74,7 +74,7 @@ impl TryFrom> for SignedPublicKey { } impl SignedPublicKey { - pub fn verify_and_unwrap(self, verifying_key: &VerifyingKey) -> Result { + pub fn verify_and_unwrap(self, verifying_key: &VerifyingKey) -> Result { let public_key_message: SignedPublicKeyMessage = self .0 .verify_and_unwrap(verifying_key, &SigningNamespace::SignedPublicKey)?; @@ -83,7 +83,7 @@ impl SignedPublicKey { public_key_message.content_format, ) { (PublicKeyEncryptionAlgorithms::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( - PublicKey::from_der(&public_key_message.public_key.into_vec()) + AsymmetricCryptoPublicKey::from_der(&public_key_message.public_key.into_vec()) .map_err(|_| CryptoError::InvalidKey)?, ), } @@ -93,12 +93,12 @@ impl SignedPublicKey { #[cfg(test)] mod tests { use super::*; - use crate::{PrivateKey, PublicKeyEncryptionAlgorithm, SignatureAlgorithm}; + use crate::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm, SignatureAlgorithm}; #[test] fn test_signed_asymmetric_public_key() { let public_key = - PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1).to_public_key(); + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1).to_public_key(); let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); let message = SignedPublicKeyMessage::from_public_key(&public_key).unwrap(); let signed_public_key = message.sign(&signing_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 2ac5838f3..afdb9fc38 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -9,7 +9,7 @@ use zeroize::Zeroizing; use super::KeyStoreInner; use crate::{ derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, - CryptoError, EncString, KeyId, KeyIds, PrivateKey, Result, Signature, SignatureAlgorithm, + CryptoError, EncString, KeyId, KeyIds, AsymmetricCryptoKey, Result, Signature, SignatureAlgorithm, SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, }; @@ -301,7 +301,7 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn dangerous_get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&PrivateKey> { + pub fn dangerous_get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { self.get_asymmetric_key(key_id) } @@ -334,7 +334,7 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&PrivateKey> { + fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { if key_id.is_local() { self.local_asymmetric_keys.get(key_id) } else { @@ -371,7 +371,7 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn set_asymmetric_key(&mut self, key_id: Ids::Asymmetric, key: PrivateKey) -> Result<()> { + pub fn set_asymmetric_key(&mut self, key_id: Ids::Asymmetric, key: AsymmetricCryptoKey) -> Result<()> { if key_id.is_local() { self.local_asymmetric_keys.upsert(key_id, key); } else { diff --git a/crates/bitwarden-crypto/src/traits/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index ccccf4eb3..9a6760ec4 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -76,7 +76,7 @@ impl, Output> #[cfg(test)] mod tests { use crate::{ - traits::tests::*, Decryptable, Encryptable, KeyStore, PrivateKey, + traits::tests::*, Decryptable, Encryptable, KeyStore, AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, }; @@ -84,7 +84,7 @@ mod tests { let store = KeyStore::::default(); let symm_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); - let asymm_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + 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 6400afdc2..c4cc2b650 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::{CryptoKey, PrivateKey, SigningKey, SymmetricCryptoKey}; +use crate::{CryptoKey, AsymmetricCryptoKey, 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. @@ -29,7 +29,7 @@ pub trait KeyId: /// At the moment it's just symmetric and asymmetric keys. pub trait KeyIds { type Symmetric: KeyId; - type Asymmetric: KeyId; + type Asymmetric: KeyId; type Signing: KeyId; } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 78fb85820..0bf9abd5e 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ CoseSerializable, CryptoError, Decryptable, EncString, Encryptable, Kdf, KeyDecryptable, - KeyEncryptable, KeyStore, MasterKey, PrivateKey, PublicKey, SignatureAlgorithm, + KeyEncryptable, KeyStore, MasterKey, AsymmetricCryptoKey, AsymmetricCryptoPublicKey, SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -244,7 +244,7 @@ impl PureCrypto { shared_key: Vec, encapsulation_key: Vec, ) -> Result { - let encapsulation_key = PublicKey::from_der(encapsulation_key.as_slice())?; + let encapsulation_key = AsymmetricCryptoPublicKey::from_der(encapsulation_key.as_slice())?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(shared_key)?, &encapsulation_key, @@ -260,7 +260,7 @@ impl PureCrypto { decapsulation_key: Vec, ) -> Result, CryptoError> { Ok(UnsignedSharedKey::from_str(encapsulated_key.as_str())? - .decapsulate_key_unsigned(&PrivateKey::from_der(decapsulation_key.as_slice())?)? + .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der(decapsulation_key.as_slice())?)? .to_encoded()) } @@ -498,7 +498,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_wrap_encapsulation_key() { - let decapsulation_key = PrivateKey::from_pem(PEM_KEY).unwrap(); + let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).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 = @@ -511,7 +511,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_wrap_decapsulation_key() { - let decapsulation_key = PrivateKey::from_pem(PEM_KEY).unwrap(); + let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).unwrap(); let wrapping_key = PureCrypto::make_user_key_aes256_cbc_hmac(); let wrapped_key = PureCrypto::wrap_decapsulation_key( decapsulation_key.to_der().unwrap(), @@ -526,7 +526,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= #[test] fn test_encapsulate_key_unsigned() { let shared_key = PureCrypto::make_user_key_aes256_cbc_hmac(); - let decapsulation_key = PrivateKey::from_pem(PEM_KEY).unwrap(); + let decapsulation_key = AsymmetricCryptoKey::from_pem(PEM_KEY).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()) diff --git a/crates/memory-testing/src/main.rs b/crates/memory-testing/src/main.rs index e2877ddcc..67f38b0e8 100644 --- a/crates/memory-testing/src/main.rs +++ b/crates/memory-testing/src/main.rs @@ -31,7 +31,7 @@ fn main() { symmetric_keys.push((key.to_encoded(), key)); } memory_testing::CaseCommand::AsymmetricKey { private_key } => { - let key = bitwarden_crypto::PrivateKey::from_pem(&private_key).unwrap(); + let key = bitwarden_crypto::AsymmetricCryptoKey::from_pem(&private_key).unwrap(); asymmetric_keys.push(key); } memory_testing::CaseCommand::MasterKey { From 5846dbc4df6a0d595591b4b0ed1f081eacb4de54 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:00:22 +0200 Subject: [PATCH 037/112] Undo changes --- crates/bitwarden-core/src/auth/auth_request.rs | 4 ++-- crates/bitwarden-core/src/auth/tde.rs | 4 ++-- crates/bitwarden-core/src/mobile/crypto.rs | 4 ++-- .../bitwarden-crypto/src/enc_string/asymmetric.rs | 4 ++-- .../src/keys/asymmetric_crypto_key.rs | 14 +++++++------- crates/bitwarden-crypto/src/keys/mod.rs | 2 +- .../bitwarden-crypto/src/keys/signed_public_key.rs | 8 ++++---- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 8bd3d9008..2670c16f0 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - fingerprint, generate_random_alphanumeric, CryptoError, AsymmetricCryptoKey, AsymmetricCryptoPublicKey, + fingerprint, generate_random_alphanumeric, CryptoError, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm, UnsignedSharedKey, }; #[cfg(feature = "internal")] @@ -91,7 +91,7 @@ pub(crate) fn approve_auth_request( client: &Client, public_key: String, ) -> Result { - let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 82fae9670..a24069cef 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - DeviceKey, EncString, Kdf, AsymmetricCryptoPublicKey, SymmetricCryptoKey, TrustDeviceResponse, + DeviceKey, EncString, Kdf, AsymmetricPublicCryptoKey, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; @@ -15,7 +15,7 @@ pub(super) fn make_register_tde_keys( org_public_key: String, remember_device: bool, ) -> Result { - let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(org_public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(org_public_key)?)?; let user_key = UserKey::new(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); let key_pair = user_key.make_key_pair()?; diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 155b1b8aa..6f0845cb2 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -404,9 +404,9 @@ pub(super) fn enroll_admin_password_reset( public_key: String, ) -> Result { use base64::{engine::general_purpose::STANDARD, Engine}; - use bitwarden_crypto::AsymmetricCryptoPublicKey; + use bitwarden_crypto::AsymmetricPublicCryptoKey; - let public_key = AsymmetricCryptoPublicKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); // FIXME: [PM-18110] This should be removed once the key store can handle public key encryption diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index f9d611a8a..830771dd5 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,7 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - AsymmetricCryptoKey, AsymmetricCryptoPublicKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion @@ -159,7 +159,7 @@ impl UnsignedSharedKey { /// and thus does not guarantee sender authenticity. pub fn encapsulate_key_unsigned( encapsulated_key: &SymmetricCryptoKey, - encapsulation_key: &AsymmetricCryptoPublicKey, + encapsulation_key: &AsymmetricPublicCryptoKey, ) -> Result { match encapsulation_key.inner() { RawPublicKey::RsaOaepSha1(rsa_public_key) => { diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index c79a42d46..42e83e0b0 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -15,18 +15,18 @@ pub(crate) enum RawPublicKey { } #[derive(Clone)] -pub struct AsymmetricCryptoPublicKey { +pub struct AsymmetricPublicCryptoKey { inner: RawPublicKey, } -impl AsymmetricCryptoPublicKey { +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(AsymmetricCryptoPublicKey { + Ok(AsymmetricPublicCryptoKey { inner: RawPublicKey::RsaOaepSha1( RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?, ), @@ -121,9 +121,9 @@ impl AsymmetricCryptoKey { } } - pub fn to_public_key(&self) -> AsymmetricCryptoPublicKey { + pub fn to_public_key(&self) -> AsymmetricPublicCryptoKey { match &self.inner { - RawPrivateKey::RsaOaepSha1(private_key) => AsymmetricCryptoPublicKey { + RawPrivateKey::RsaOaepSha1(private_key) => AsymmetricPublicCryptoKey { inner: RawPublicKey::RsaOaepSha1(private_key.to_public_key()), }, } @@ -145,7 +145,7 @@ impl std::fmt::Debug for AsymmetricCryptoKey { mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; - use crate::{AsymmetricCryptoKey, AsymmetricCryptoPublicKey, SymmetricCryptoKey, UnsignedSharedKey}; + use crate::{AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey}; #[test] fn test_asymmetric_crypto_key() { @@ -236,7 +236,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= .unwrap(); let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); - let public_key = AsymmetricCryptoPublicKey::from_der(&public_key).unwrap(); + let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index e2225d380..8a476faa0 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -11,7 +11,7 @@ pub use symmetric_crypto_key::{ Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key, }; mod asymmetric_crypto_key; -pub use asymmetric_crypto_key::{AsymmetricCryptoKey, AsymmetricCryptoPublicKey, PublicKeyEncryptionAlgorithm}; +pub use asymmetric_crypto_key::{AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm}; pub(crate) use asymmetric_crypto_key::{RawPrivateKey, RawPublicKey}; mod signed_public_key; pub use signed_public_key::{SignedPublicKey, SignedPublicKeyMessage}; diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 192c3eb86..42a13dd11 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; -use super::AsymmetricCryptoPublicKey; +use super::AsymmetricPublicCryptoKey; use crate::{ cose::CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; @@ -39,7 +39,7 @@ pub struct SignedPublicKeyMessage { } impl SignedPublicKeyMessage { - pub fn from_public_key(public_key: &AsymmetricCryptoPublicKey) -> Result { + pub fn from_public_key(public_key: &AsymmetricPublicCryptoKey) -> Result { Ok(SignedPublicKeyMessage { algorithm: PublicKeyEncryptionAlgorithms::RsaOaepSha1, content_format: PublicKeyFormat::Spki, @@ -74,7 +74,7 @@ impl TryFrom> for SignedPublicKey { } impl SignedPublicKey { - pub fn verify_and_unwrap(self, verifying_key: &VerifyingKey) -> Result { + pub fn verify_and_unwrap(self, verifying_key: &VerifyingKey) -> Result { let public_key_message: SignedPublicKeyMessage = self .0 .verify_and_unwrap(verifying_key, &SigningNamespace::SignedPublicKey)?; @@ -83,7 +83,7 @@ impl SignedPublicKey { public_key_message.content_format, ) { (PublicKeyEncryptionAlgorithms::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( - AsymmetricCryptoPublicKey::from_der(&public_key_message.public_key.into_vec()) + AsymmetricPublicCryptoKey::from_der(&public_key_message.public_key.into_vec()) .map_err(|_| CryptoError::InvalidKey)?, ), } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 0bf9abd5e..aa82635a7 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; use bitwarden_crypto::{ CoseSerializable, CryptoError, Decryptable, EncString, Encryptable, Kdf, KeyDecryptable, - KeyEncryptable, KeyStore, MasterKey, AsymmetricCryptoKey, AsymmetricCryptoPublicKey, SignatureAlgorithm, + KeyEncryptable, KeyStore, MasterKey, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -244,7 +244,7 @@ impl PureCrypto { shared_key: Vec, encapsulation_key: Vec, ) -> Result { - let encapsulation_key = AsymmetricCryptoPublicKey::from_der(encapsulation_key.as_slice())?; + let encapsulation_key = AsymmetricPublicCryptoKey::from_der(encapsulation_key.as_slice())?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(shared_key)?, &encapsulation_key, From d466ec739ff7d7285360d9f77dd9735c3bbbbfe2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:28:45 +0200 Subject: [PATCH 038/112] Add examples --- crates/bitwarden-crypto/examples/signature.rs | 112 ++++++++++++++++++ .../examples/signed_object.rs | 61 ++++++++++ .../bitwarden-crypto/src/signing/message.rs | 4 +- .../bitwarden-crypto/src/signing/signature.rs | 2 +- 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 crates/bitwarden-crypto/examples/signature.rs create mode 100644 crates/bitwarden-crypto/examples/signed_object.rs diff --git a/crates/bitwarden-crypto/examples/signature.rs b/crates/bitwarden-crypto/examples/signature.rs new file mode 100644 index 000000000..5abc416b1 --- /dev/null +++ b/crates/bitwarden-crypto/examples/signature.rs @@ -0,0 +1,112 @@ +use bitwarden_crypto::CoseSerializable; +use serde::{Deserialize, Serialize}; + +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) + .expect("Failed to create signing key for Alice"); + let alice_verifying_key = alice_signature_key.to_verifying_key(); + let bob_signature_key = bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519) + .expect("Failed to create signing key for Bob"); + 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. + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ).expect("Failed to sign message"); + + // Alice sends the signed object to Bob + mock_server.upload("signature", signature.to_cose().expect("Failed to serialize signature")); + 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().to_vec(), + &alice_verifying_key, + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) { + 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, &bitwarden_crypto::SigningNamespace::ExampleNamespace) + .expect("Failed to counter sign message"); + // Bob sends the counter signature to Charlie + mock_server.upload("bobs_signature", bobs_signature.to_cose().expect("Failed to serialize Bob's signature")); + + // 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().to_vec(), + &alice_verifying_key, + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) { + panic!("Alice's signature verification failed"); + } + // Charlie verifies Bob's signature + if !retrieved_bobs_signature.verify( + &retrieved_serialized_message.as_bytes().to_vec(), + &bob_verifying_key, + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) { + 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) + } +} \ No newline at end of file diff --git a/crates/bitwarden-crypto/examples/signed_object.rs b/crates/bitwarden-crypto/examples/signed_object.rs new file mode 100644 index 000000000..107875fe2 --- /dev/null +++ b/crates/bitwarden-crypto/examples/signed_object.rs @@ -0,0 +1,61 @@ +use bitwarden_crypto::{CoseSerializable, SignedObject}; +use serde::{Deserialize, Serialize}; + +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) + .expect("Failed to create signing key for Alice"); + 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. + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ).expect("Failed to sign message"); + + // Alice sends the signed object to Bob + mock_server.upload("signed_object", signed_object.to_cose().expect("Failed to serialize signed object")); + + // 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, &bitwarden_crypto::SigningNamespace::ExampleNamespace) + .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) + } +} \ No newline at end of file diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs index 25c1eb3fa..b3f9aed11 100644 --- a/crates/bitwarden-crypto/src/signing/message.rs +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -25,7 +25,7 @@ impl AsRef<[u8]> for SerializedMessage { } impl SerializedMessage { - pub(super) fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { + pub fn from_bytes(bytes: Vec, content_type: CoapContentFormat) -> Self { SerializedMessage { serialized_message_bytes: bytes, content_type, @@ -53,7 +53,7 @@ impl SerializedMessage { /// 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(super) fn decode(&self) -> Result { + pub fn decode(&self) -> Result { if self.content_type != CoapContentFormat::Cbor { return Err(CryptoError::CoseEncodingError); } diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 23c1b344a..45d602e65 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -49,7 +49,7 @@ impl Signature { ) } - pub(super) fn content_type(&self) -> Result { + pub fn content_type(&self) -> Result { if let RegisteredLabel::Assigned(content_format) = self .0 .protected From b27ddf410c5ea36d630bd3f37b5450fa2b89744e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:29:02 +0200 Subject: [PATCH 039/112] Cargo fmt --- .../bitwarden-core/src/auth/auth_request.rs | 4 +- crates/bitwarden-core/src/auth/tde.rs | 2 +- .../src/client/encryption_settings.rs | 2 +- crates/bitwarden-core/src/mobile/crypto.rs | 35 +++--- crates/bitwarden-crypto/examples/signature.rs | 101 ++++++++++++------ .../examples/signed_object.rs | 57 ++++++---- .../src/enc_string/asymmetric.rs | 3 +- .../src/keys/asymmetric_crypto_key.rs | 4 +- .../bitwarden-crypto/src/keys/device_key.rs | 3 +- crates/bitwarden-crypto/src/keys/mod.rs | 4 +- .../src/keys/signed_public_key.rs | 5 +- .../src/signing/signed_object.rs | 3 +- crates/bitwarden-crypto/src/store/context.rs | 17 ++- .../src/traits/encryptable.rs | 2 +- crates/bitwarden-crypto/src/traits/key_id.rs | 2 +- .../src/pure_crypto.rs | 11 +- 16 files changed, 171 insertions(+), 84 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 2670c16f0..f8959cc04 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, CryptoError, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, - PublicKeyEncryptionAlgorithm, UnsignedSharedKey, + fingerprint, generate_random_alphanumeric, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, + CryptoError, PublicKeyEncryptionAlgorithm, UnsignedSharedKey, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, SymmetricCryptoKey}; diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index a24069cef..2fd57ea97 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,6 +1,6 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - DeviceKey, EncString, Kdf, AsymmetricPublicCryptoKey, SymmetricCryptoKey, TrustDeviceResponse, + AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 6fa773077..aba248f34 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -42,7 +42,7 @@ impl EncryptionSettings { signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{CoseSerializable, KeyDecryptable, AsymmetricCryptoKey, SigningKey}; + use bitwarden_crypto::{AsymmetricCryptoKey, CoseSerializable, KeyDecryptable, SigningKey}; use log::warn; use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 6f0845cb2..1b2d7af23 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -8,8 +8,9 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, - AsymmetricCryptoKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, + KeyEncryptable, MasterKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -526,8 +527,8 @@ pub(super) fn verify_asymmetric_keys( .decrypt_with_key(user_key) .map_err(VerifyError::DecryptFailed)?; - let private_key = - AsymmetricCryptoKey::from_der(&decrypted_private_key).map_err(VerifyError::ParseFailed)?; + let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key) + .map_err(VerifyError::ParseFailed)?; let derived_public_key_vec = private_key .to_public_key() @@ -587,17 +588,22 @@ pub fn make_user_signing_keys(client: &Client) -> Result = ctx.make_signed_public_key( - AsymmetricKeyId::UserPrivateKey, - SigningKeyId::UserSigningKey, - )?.try_into()?; + let signature_keypair = + SigningKey::make(SignatureAlgorithm::Ed25519).map_err(|_| CryptoError::InvalidKey)?; + let signed_public_key: Vec = ctx + .make_signed_public_key( + AsymmetricKeyId::UserPrivateKey, + SigningKeyId::UserSigningKey, + )? + .try_into()?; 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 - signing_key: signature_keypair.to_cose()?.encrypt_with_key(wrapping_key)?, + 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 + signing_key: signature_keypair + .to_cose()? + .encrypt_with_key(wrapping_key)?, signed_public_key: STANDARD.encode(&signed_public_key), }) } @@ -828,7 +834,8 @@ mod tests { let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; - let private_key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); + let private_key = + AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key).unwrap()).unwrap(); let decrypted: SymmetricCryptoKey = encrypted.decapsulate_key_unsigned(&private_key).unwrap(); diff --git a/crates/bitwarden-crypto/examples/signature.rs b/crates/bitwarden-crypto/examples/signature.rs index 5abc416b1..13f5f973d 100644 --- a/crates/bitwarden-crypto/examples/signature.rs +++ b/crates/bitwarden-crypto/examples/signature.rs @@ -2,43 +2,60 @@ use bitwarden_crypto::CoseSerializable; use serde::{Deserialize, Serialize}; 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. + // 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) - .expect("Failed to create signing key for Alice"); + let alice_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519) + .expect("Failed to create signing key for Alice"); let alice_verifying_key = alice_signature_key.to_verifying_key(); - let bob_signature_key = bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519) - .expect("Failed to create signing key for Bob"); + let bob_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519) + .expect("Failed to create signing key for Bob"); 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. - + // 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. - &bitwarden_crypto::SigningNamespace::ExampleNamespace, - ).expect("Failed to sign message"); + 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. + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) + .expect("Failed to sign message"); // Alice sends the signed object to Bob - mock_server.upload("signature", signature.to_cose().expect("Failed to serialize signature")); + mock_server.upload( + "signature", + signature.to_cose().expect("Failed to serialize signature"), + ); 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"); + &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") + 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 @@ -51,22 +68,43 @@ fn main() { } // Bob signs the message for Charlie - let bobs_signature = bob_signature_key.counter_sign_detached(retrieved_serialized_message.as_bytes().to_vec(), &retrieved_signature, &bitwarden_crypto::SigningNamespace::ExampleNamespace) + let bobs_signature = bob_signature_key + .counter_sign_detached( + retrieved_serialized_message.as_bytes().to_vec(), + &retrieved_signature, + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) .expect("Failed to counter sign message"); // Bob sends the counter signature to Charlie - mock_server.upload("bobs_signature", bobs_signature.to_cose().expect("Failed to serialize Bob's signature")); + mock_server.upload( + "bobs_signature", + bobs_signature + .to_cose() + .expect("Failed to serialize Bob's signature"), + ); // 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") + 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"); + &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"); + &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( @@ -88,7 +126,10 @@ fn main() { 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); + println!( + "Charlie received a message from Alice and Bob: {}", + verified_message.content + ); } pub(crate) struct MockServer { @@ -109,4 +150,4 @@ impl MockServer { pub(crate) fn download(&self, key: &str) -> Option<&Vec> { self.map.get(key) } -} \ No newline at end of file +} diff --git a/crates/bitwarden-crypto/examples/signed_object.rs b/crates/bitwarden-crypto/examples/signed_object.rs index 107875fe2..a1db9a603 100644 --- a/crates/bitwarden-crypto/examples/signed_object.rs +++ b/crates/bitwarden-crypto/examples/signed_object.rs @@ -2,42 +2,61 @@ use bitwarden_crypto::{CoseSerializable, SignedObject}; use serde::{Deserialize, Serialize}; 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. + // 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) - .expect("Failed to create signing key for Alice"); + let alice_signature_key = + bitwarden_crypto::SigningKey::make(bitwarden_crypto::SignatureAlgorithm::Ed25519) + .expect("Failed to create signing key for Alice"); 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. - + // 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. - &bitwarden_crypto::SigningNamespace::ExampleNamespace, - ).expect("Failed to sign message"); + 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. + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) + .expect("Failed to sign message"); // Alice sends the signed object to Bob - mock_server.upload("signed_object", signed_object.to_cose().expect("Failed to serialize signed object")); + mock_server.upload( + "signed_object", + signed_object + .to_cose() + .expect("Failed to serialize signed object"), + ); // 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"); + &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, &bitwarden_crypto::SigningNamespace::ExampleNamespace) + .verify_and_unwrap( + &alice_verifying_key, + &bitwarden_crypto::SigningNamespace::ExampleNamespace, + ) .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); + println!( + "Bob received a message from Alice: {}", + verified_message.content + ); } pub(crate) struct MockServer { @@ -58,4 +77,4 @@ impl MockServer { pub(crate) fn download(&self, key: &str) -> Option<&Vec> { self.map.get(key) } -} \ No newline at end of file +} diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index 830771dd5..b047b041b 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,7 +9,8 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, + SymmetricCryptoKey, }; // This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop // macro expansion diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 42e83e0b0..fdbf1ab07 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -145,7 +145,9 @@ impl std::fmt::Debug for AsymmetricCryptoKey { mod tests { use base64::{engine::general_purpose::STANDARD, Engine}; - use crate::{AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey}; + use crate::{ + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SymmetricCryptoKey, UnsignedSharedKey, + }; #[test] fn test_asymmetric_crypto_key() { diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index 0274f5a8f..7445decc8 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -32,7 +32,8 @@ impl DeviceKey { pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); - let device_private_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + let device_private_key = + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned( user_key, diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 8a476faa0..4327d20c0 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -11,7 +11,9 @@ pub use symmetric_crypto_key::{ Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key, }; mod asymmetric_crypto_key; -pub use asymmetric_crypto_key::{AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm}; +pub use asymmetric_crypto_key::{ + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, PublicKeyEncryptionAlgorithm, +}; pub(crate) use asymmetric_crypto_key::{RawPrivateKey, RawPublicKey}; mod signed_public_key; pub use signed_public_key::{SignedPublicKey, SignedPublicKeyMessage}; diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 42a13dd11..1beca6a4e 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -74,7 +74,10 @@ impl TryFrom> for SignedPublicKey { } impl SignedPublicKey { - pub fn verify_and_unwrap(self, verifying_key: &VerifyingKey) -> Result { + pub fn verify_and_unwrap( + self, + verifying_key: &VerifyingKey, + ) -> Result { let public_key_message: SignedPublicKeyMessage = self .0 .verify_and_unwrap(verifying_key, &SigningNamespace::SignedPublicKey)?; diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 9eb1eaff5..58e01a8d6 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -189,7 +189,8 @@ impl CoseSerializable for SignedObject { #[cfg(test)] mod tests { use crate::{ - CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, TestMessage, VerifyingKey + CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, TestMessage, + VerifyingKey, }; const VERIFYING_KEY: &[u8] = &[ diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index afdb9fc38..93ec97ed3 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -9,9 +9,9 @@ use zeroize::Zeroizing; use super::KeyStoreInner; use crate::{ derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, - CryptoError, EncString, KeyId, KeyIds, AsymmetricCryptoKey, Result, Signature, SignatureAlgorithm, - SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, - UnsignedSharedKey, + AsymmetricCryptoKey, CryptoError, EncString, KeyId, KeyIds, Result, Signature, + SignatureAlgorithm, SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, + SymmetricCryptoKey, UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] @@ -301,7 +301,10 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn dangerous_get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { + pub fn dangerous_get_asymmetric_key( + &self, + key_id: Ids::Asymmetric, + ) -> Result<&AsymmetricCryptoKey> { self.get_asymmetric_key(key_id) } @@ -371,7 +374,11 @@ impl KeyStoreContext<'_, Ids> { } #[deprecated(note = "This function should ideally never be used outside this crate")] - pub fn set_asymmetric_key(&mut self, key_id: Ids::Asymmetric, key: AsymmetricCryptoKey) -> Result<()> { + pub fn set_asymmetric_key( + &mut self, + key_id: Ids::Asymmetric, + key: AsymmetricCryptoKey, + ) -> Result<()> { if key_id.is_local() { self.local_asymmetric_keys.upsert(key_id, key); } else { diff --git a/crates/bitwarden-crypto/src/traits/encryptable.rs b/crates/bitwarden-crypto/src/traits/encryptable.rs index 9a6760ec4..d9b8d600b 100644 --- a/crates/bitwarden-crypto/src/traits/encryptable.rs +++ b/crates/bitwarden-crypto/src/traits/encryptable.rs @@ -76,7 +76,7 @@ impl, Output> #[cfg(test)] mod tests { use crate::{ - traits::tests::*, Decryptable, Encryptable, KeyStore, AsymmetricCryptoKey, + traits::tests::*, AsymmetricCryptoKey, Decryptable, Encryptable, KeyStore, PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, }; diff --git a/crates/bitwarden-crypto/src/traits/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs index c4cc2b650..3a7863236 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::{CryptoKey, AsymmetricCryptoKey, SigningKey, 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. diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index aa82635a7..bd640658f 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::{ - CoseSerializable, CryptoError, Decryptable, EncString, Encryptable, Kdf, KeyDecryptable, - KeyEncryptable, KeyStore, MasterKey, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, SignatureAlgorithm, - SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, VerifyingKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseSerializable, CryptoError, Decryptable, + EncString, Encryptable, Kdf, KeyDecryptable, KeyEncryptable, KeyStore, MasterKey, + SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, + VerifyingKey, }; use wasm_bindgen::prelude::*; @@ -260,7 +261,9 @@ impl PureCrypto { decapsulation_key: Vec, ) -> Result, CryptoError> { Ok(UnsignedSharedKey::from_str(encapsulated_key.as_str())? - .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der(decapsulation_key.as_slice())?)? + .decapsulate_key_unsigned(&AsymmetricCryptoKey::from_der( + decapsulation_key.as_slice(), + )?)? .to_encoded()) } From 5733baa6ecac56e817ff53e130dd02ac0b020c3c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:30:10 +0200 Subject: [PATCH 040/112] Fix build --- crates/bitwarden-crypto/src/traits/key_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/traits/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs index 3a7863236..854149424 100644 --- a/crates/bitwarden-crypto/src/traits/key_id.rs +++ b/crates/bitwarden-crypto/src/traits/key_id.rs @@ -100,7 +100,7 @@ macro_rules! key_ids { }; ( @key_type symmetric ) => { $crate::SymmetricCryptoKey }; - ( @key_type asymmetric ) => { $crate::PrivateKey }; + ( @key_type asymmetric ) => { $crate::AsymmetricCryptoKey }; ( @key_type signing ) => { $crate::SigningKey }; ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant ( _ ) }; From beb1251e171013ad46de971bdd3e52fc9a61b419 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:34:05 +0200 Subject: [PATCH 041/112] Add comment --- crates/bitwarden-core/src/auth/tde.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 2fd57ea97..4af72baed 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -40,6 +40,7 @@ pub(super) fn make_register_tde_keys( 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, )?; From af72a87e85e3237f7f11810cdda329bc51c6b469 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:37:22 +0200 Subject: [PATCH 042/112] Address formatting issue --- crates/bitwarden-core/src/mobile/crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 1b2d7af23..d5551ec91 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -570,7 +570,6 @@ pub struct MakeUserSigningKeysResponse { verifying_key: String, /// Signing key, encrypted with a symmetric key (user key, org key) signing_key: EncString, - /// A signed object claiming ownership of a public key. This ties the public key to the /// signature key signed_public_key: String, From f18512c85de1e898eff9cb2a20344aafaf57db80 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:47:31 +0200 Subject: [PATCH 043/112] Fix build --- crates/bitwarden-core/src/client/encryption_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index b96955ca4..9e004b7a0 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::{ error::UserIdAlreadySetError, - key_management::{AsymmetricKeyId, KeyIds, SymmetricKeyId}, + key_management::{KeyIds, SymmetricKeyId}, MissingPrivateKeyError, VaultLockedError, }; From 17f8d09de76f0cb59be8272ef6dfdfb0caea7dea Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 14:56:24 +0200 Subject: [PATCH 044/112] Add signed public key test --- .../src/keys/signed_public_key.rs | 2 +- .../src/pure_crypto.rs | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 1beca6a4e..806e624b0 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -47,7 +47,7 @@ impl SignedPublicKeyMessage { }) } - pub(crate) fn sign(&self, signing_key: &SigningKey) -> Result { + pub fn sign(&self, signing_key: &SigningKey) -> Result { Ok(SignedPublicKey( signing_key.sign(self, &SigningNamespace::SignedPublicKey)?, )) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index bd640658f..8abca3de6 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -308,7 +308,7 @@ impl PureCrypto { mod tests { use std::{num::NonZero, str::FromStr}; - use bitwarden_crypto::EncString; + use bitwarden_crypto::{EncString, SignedPublicKeyMessage}; use super::*; @@ -393,6 +393,32 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 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, 95, 163, 105, 97, 108, 103, + 111, 114, 105, 116, 104, 109, 107, 82, 115, 97, 79, 97, 101, 112, 83, 104, 97, 49, 110, 99, + 111, 110, 116, 101, 110, 116, 95, 102, 111, 114, 109, 97, 116, 100, 83, 112, 107, 105, 106, + 112, 117, 98, 108, 105, 99, 95, 107, 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, 108, 94, 172, 226, 255, 54, 239, 122, 106, 80, 218, 178, + 189, 194, 7, 178, 23, 147, 254, 164, 240, 86, 232, 60, 54, 116, 70, 179, 190, 81, 222, 56, + 51, 225, 180, 123, 44, 82, 157, 117, 21, 49, 29, 84, 38, 73, 92, 197, 34, 108, 211, 97, + 191, 24, 187, 123, 5, 10, 158, 232, 177, 45, 186, 0, + ]; + #[test] fn test_symmetric_decrypt() { let enc_string = EncString::from_str(ENCRYPTED).unwrap(); @@ -570,4 +596,14 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= verifying_key_derived.to_cose().unwrap() ); } + + #[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); + } } From a8c6fa528724f40e2b81fcad26f87360b9c082c0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 15:01:59 +0200 Subject: [PATCH 045/112] Cleanup --- .../src/keys/signed_public_key.rs | 5 ++ .../src/pure_crypto.rs | 46 +++++++++---------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 806e624b0..46cd85369 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -14,6 +14,7 @@ use crate::{ /// Currently, only RSA with OAEP and SHA-1 keys are used. #[derive(Serialize, Deserialize)] enum PublicKeyEncryptionAlgorithms { + #[serde(rename = "0")] RsaOaepSha1 = 0, } @@ -22,6 +23,7 @@ enum PublicKeyEncryptionAlgorithms { /// option in the future. #[derive(Serialize, Deserialize)] enum PublicKeyFormat { + #[serde(rename = "0")] Spki = 0, } @@ -30,11 +32,14 @@ enum PublicKeyFormat { #[derive(Serialize, Deserialize)] pub struct SignedPublicKeyMessage { /// The algorithm/crypto system used with this public key. + #[serde(rename = "alg")] algorithm: PublicKeyEncryptionAlgorithms, /// The format of the public key. + #[serde(rename = "format")] content_format: PublicKeyFormat, /// The public key, serialized and formatted in the content format specified in /// `content_format`. + #[serde(rename = "key")] public_key: ByteBuf, } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 8abca3de6..9591d9178 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -308,7 +308,7 @@ impl PureCrypto { mod tests { use std::{num::NonZero, str::FromStr}; - use bitwarden_crypto::{EncString, SignedPublicKeyMessage}; + use bitwarden_crypto::EncString; use super::*; @@ -395,28 +395,26 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 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, 95, 163, 105, 97, 108, 103, - 111, 114, 105, 116, 104, 109, 107, 82, 115, 97, 79, 97, 101, 112, 83, 104, 97, 49, 110, 99, - 111, 110, 116, 101, 110, 116, 95, 102, 111, 114, 109, 97, 116, 100, 83, 112, 107, 105, 106, - 112, 117, 98, 108, 105, 99, 95, 107, 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, 108, 94, 172, 226, 255, 54, 239, 122, 106, 80, 218, 178, - 189, 194, 7, 178, 23, 147, 254, 164, 240, 86, 232, 60, 54, 116, 70, 179, 190, 81, 222, 56, - 51, 225, 180, 123, 44, 82, 157, 117, 21, 49, 29, 84, 38, 73, 92, 197, 34, 108, 211, 97, - 191, 24, 187, 123, 5, 10, 158, 232, 177, 45, 186, 0, + 219, 193, 50, 30, 21, 43, 58, 0, 1, 56, 127, 1, 160, 89, 1, 61, 163, 99, 97, 108, 103, 97, + 48, 102, 102, 111, 114, 109, 97, 116, 97, 48, 99, 107, 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, 92, 149, 155, 77, 244, 168, 111, 175, 145, 222, 0, + 110, 243, 119, 10, 40, 86, 232, 153, 163, 175, 146, 45, 173, 64, 132, 217, 205, 244, 165, + 92, 57, 43, 150, 208, 69, 143, 169, 50, 240, 204, 77, 199, 190, 191, 167, 27, 214, 108, + 233, 41, 59, 177, 85, 61, 44, 218, 230, 136, 112, 204, 64, 192, 3, ]; #[test] @@ -598,7 +596,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= } #[test] - fn test_verify_and_unwrap_signed_public_key() { + fn test_verify_aend_unwrap_signed_public_key() { let public_key = PureCrypto::verify_and_unwrap_signed_public_key( SIGNED_PUBLIC_KEY.to_vec(), VERIFYING_KEY.to_vec(), From 55ddd0bdc6db43cca9169de78d7ac626b21c40b0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 15:06:26 +0200 Subject: [PATCH 046/112] Add comment --- crates/bitwarden-crypto/src/keys/signed_public_key.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 46cd85369..71074b8f6 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -79,6 +79,8 @@ impl TryFrom> for SignedPublicKey { } 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, From 053d8947083e1aced049e57f9f70ba0b8c14f26f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 15:09:29 +0200 Subject: [PATCH 047/112] Cargo fmt --- crates/bitwarden-crypto/src/keys/signed_public_key.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 71074b8f6..2b1d70903 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -79,8 +79,8 @@ impl TryFrom> for SignedPublicKey { } impl SignedPublicKey { - /// Verifies the signature of the public key against the provided `VerifyingKey`, and returns the - /// `AsymmetricPublicCryptoKey` if the verification is successful. + /// 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, From c520046bd456a38be725777bf0d97afa14ffb6d2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 28 May 2025 18:20:59 +0200 Subject: [PATCH 048/112] Cleanup --- crates/bitwarden-crypto/src/signing/namespace.rs | 2 +- crates/bitwarden-crypto/src/signing/signing_key.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index 5f9f3bd0d..6314f9884 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -10,7 +10,7 @@ use crate::{error::SignatureError, CryptoError}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SigningNamespace { /// The namespace for - /// [`SignedPublicKey`](crate::signing::SignedPublicKey). + /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, /// This namespace is only used in tests and documentation. diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 90c375160..fb9d7baab 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -61,9 +61,9 @@ impl SigningKey { } /// Signs the given byte array with the signing key. - /// This should never be used directly by anything other than the second layer defined in - /// [super::message]. - pub fn sign_raw(&self, data: &[u8]) -> Vec { + /// 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(), } From 740584a84273dac1ed307fda03ab943da268a62d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:15:17 +0200 Subject: [PATCH 049/112] Cleanup --- crates/bitwarden-crypto/src/signing/cose.rs | 137 ++++++++++++++++++ crates/bitwarden-crypto/src/signing/mod.rs | 2 + .../bitwarden-crypto/src/signing/signature.rs | 42 +----- .../src/signing/signed_object.rs | 41 +----- .../src/signing/signing_key.rs | 60 ++------ .../src/signing/verifying_key.rs | 55 +------ 6 files changed, 171 insertions(+), 166 deletions(-) create mode 100644 crates/bitwarden-crypto/src/signing/cose.rs diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs new file mode 100644 index 000000000..5a79d4fd8 --- /dev/null +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -0,0 +1,137 @@ +//! 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::SignatureError, keys::KeyId, CryptoError, KEY_ID_SIZE, +}; + +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_i64( + i128::from(namespace) + .try_into() + .map_err(|_| SignatureError::InvalidNamespace)?, + ) +} + +pub(super) fn content_type( + protected_header: &ProtectedHeader, +) -> Result { + if let RegisteredLabel::Assigned(content_format) = protected_header + .header + .content_type + .clone() + .ok_or(CryptoError::from(SignatureError::InvalidSignature))? + { + Ok(content_format) + } else { + Err(SignatureError::InvalidSignature.into()) + } +} + +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(|_| CryptoError::InvalidKey)?; + let key_id: KeyId = key_id.into(); + Ok(key_id) +} + +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(|_| CryptoError::InvalidKey)?, + )) + } else { + Err(CryptoError::InvalidKey) + } +} + +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() { + Ok(ed25519_dalek::VerifyingKey::from_bytes( + x.try_into().map_err(|_| CryptoError::InvalidKey)?, + ) + .map_err(|_| CryptoError::InvalidKey)?) + } else { + Err(CryptoError::InvalidKey) + } +} + +fn okp_d(cose_key: &CoseKey) -> Result<&[u8], CryptoError> { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let mut d = None; + for (key, value) in &cose_key.params { + if let Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + if key == OkpKeyParameter::D { + d.replace(value); + } + } + } + let d = d.ok_or(CryptoError::InvalidKey)?; + Ok(d.as_bytes().ok_or(CryptoError::InvalidKey)?.as_slice()) +} + +fn okp_x(cose_key: &CoseKey) -> Result<&[u8], CryptoError> { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let mut x = None; + for (key, value) in &cose_key.params { + if let Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + if key == OkpKeyParameter::X { + x.replace(value); + } + } + } + let x = x.ok_or(CryptoError::InvalidKey)?; + Ok(x.as_bytes().ok_or(CryptoError::InvalidKey)?.as_slice()) +} + +fn okp_curve(cose_key: &CoseKey) -> Result { + // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair + let mut crv = None; + for (key, value) in &cose_key.params { + if let Label::Int(i) = key { + let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + if key == OkpKeyParameter::Crv { + crv.replace(value); + } + } + } + + let crv = crv.ok_or(CryptoError::InvalidKey)?; + Ok(crv.as_integer().ok_or(CryptoError::InvalidKey)?.into()) +} diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index fa31d43d3..a1f794c12 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -27,6 +27,8 @@ //! serialized payload is returned. Purpose: If multiple signatures are needed for one object, //! then sign detached can be used. +mod cose; +pub(self) use cose::*; mod namespace; pub use namespace::SigningNamespace; mod signed_object; diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 45d602e65..6d395e932 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -1,8 +1,11 @@ use ciborium::value::Integer; -use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1, Label, RegisteredLabel}; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1}; use serde::Serialize; -use super::{message::SerializedMessage, signing_key::SigningKey, SigningNamespace, VerifyingKey}; +use super::{ + content_type, message::SerializedMessage, namespace, signing_key::SigningKey, SigningNamespace, + VerifyingKey, +}; use crate::{ cose::{CoseSerializable, SIGNING_NAMESPACE}, error::SignatureError, @@ -26,42 +29,11 @@ impl Signature { } pub(self) fn namespace(&self) -> Result { - let namespace = self - .0 - .protected - .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_i64( - i128::from(namespace) - .try_into() - .map_err(|_| SignatureError::InvalidNamespace)?, - ) + namespace(&self.0.protected) } pub fn content_type(&self) -> Result { - if let RegisteredLabel::Assigned(content_format) = self - .0 - .protected - .header - .content_type - .clone() - .ok_or(CryptoError::from(SignatureError::InvalidSignature))? - { - Ok(content_format) - } else { - Err(SignatureError::InvalidSignature.into()) - } + content_type(&self.0.protected) } /// Verifies the signature of the given serialized message bytes, created by diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 58e01a8d6..f712138c5 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -1,10 +1,10 @@ use ciborium::value::Integer; -use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1, Label, RegisteredLabel}; +use coset::{iana::CoapContentFormat, CborSerializable, CoseSign1}; use serde::{de::DeserializeOwned, Serialize}; use super::{ - message::SerializedMessage, signing_key::SigningKey, verifying_key::VerifyingKey, - SigningNamespace, + content_type, message::SerializedMessage, namespace, signing_key::SigningKey, + verifying_key::VerifyingKey, SigningNamespace, }; use crate::{ cose::{CoseSerializable, SIGNING_NAMESPACE}, @@ -22,18 +22,7 @@ impl From for SignedObject { impl SignedObject { pub fn content_type(&self) -> Result { - if let RegisteredLabel::Assigned(content_format) = self - .0 - .protected - .header - .content_type - .clone() - .ok_or(CryptoError::from(SignatureError::InvalidSignature))? - { - Ok(content_format) - } else { - Err(SignatureError::InvalidSignature.into()) - } + content_type(&self.0.protected) } pub(crate) fn inner(&self) -> &CoseSign1 { @@ -41,27 +30,7 @@ impl SignedObject { } pub(crate) fn namespace(&self) -> Result { - let namespace = self - .0 - .protected - .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_i64( - i128::from(namespace) - .try_into() - .map_err(|_| SignatureError::InvalidNamespace)?, - ) + namespace(&self.0.protected) } fn payload(&self) -> Result, CryptoError> { diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index fb9d7baab..6b8f55794 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -1,18 +1,19 @@ use ciborium::{value::Integer, Value}; use coset::{ iana::{Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, - CborSerializable, CoseKey, Label, RegisteredLabel, RegisteredLabelWithPrivate, + CborSerializable, CoseKey, RegisteredLabel, RegisteredLabelWithPrivate, }; use ed25519_dalek::Signer; use rand::rngs::OsRng; use zeroize::ZeroizeOnDrop; use super::{ + ed25519_signing_key, key_id, verifying_key::{RawVerifyingKey, VerifyingKey}, SignatureAlgorithm, }; use crate::{ - cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey, KEY_ID_SIZE, + cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey, }; /// A `SigningKey` without the key id. This enum contains a variant for each supported signature @@ -99,56 +100,19 @@ impl CoseSerializable for SigningKey { fn from_cose(bytes: &[u8]) -> Result { let cose_key = CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - let Some(algorithm) = cose_key.alg else { + let Some(ref algorithm) = cose_key.alg else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; KEY_ID_SIZE] = cose_key - .key_id - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key_id: KeyId = key_id.into(); - match (cose_key.kty, algorithm) { + match (&cose_key.kty, algorithm) { (kty, alg) - if kty == RegisteredLabel::Assigned(KeyType::OKP) - && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + if *kty == RegisteredLabel::Assigned(KeyType::OKP) + && *alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => { - // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair - let (mut crv, mut d) = (None, None); - for (key, value) in &cose_key.params { - if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; - match key { - OkpKeyParameter::Crv => { - crv.replace(value); - } - OkpKeyParameter::D => { - d.replace(value); - } - _ => (), - } - } - } - - let (Some(d), Some(crv)) = (d, crv) else { - return Err(CryptoError::InvalidKey); - }; - let crv: i128 = crv.as_integer().ok_or(CryptoError::InvalidKey)?.into(); - if crv == EllipticCurve::Ed25519.to_i64().into() { - let secret_key_bytes: &[u8; 32] = d - .as_bytes() - .ok_or(CryptoError::InvalidKey)? - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key = ed25519_dalek::SigningKey::from_bytes(secret_key_bytes); - Ok(SigningKey { - id: key_id, - inner: RawSigningKey::Ed25519(key), - }) - } else { - Err(CryptoError::InvalidKey) - } + let key = ed25519_signing_key(&cose_key)?; + Ok(SigningKey { + id: key_id(&cose_key)?, + inner: RawSigningKey::Ed25519(key), + }) } _ => Err(CryptoError::InvalidKey), } diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index db99a4596..5b2eaf819 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -9,8 +9,8 @@ use coset::{ CborSerializable, RegisteredLabel, RegisteredLabelWithPrivate, }; -use super::SignatureAlgorithm; -use crate::{cose::CoseSerializable, error::SignatureError, keys::KeyId, CryptoError, KEY_ID_SIZE}; +use super::{ed25519_verifying_key, key_id, SignatureAlgorithm}; +use crate::{cose::CoseSerializable, error::SignatureError, keys::KeyId, CryptoError}; /// A `VerifyingKey` without the key id. This enum contains a variant for each supported signature /// scheme. @@ -83,56 +83,17 @@ impl CoseSerializable for VerifyingKey { { let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; - let Some(algorithm) = cose_key.alg else { + let Some(ref algorithm) = cose_key.alg else { return Err(CryptoError::InvalidKey); }; - let key_id: [u8; KEY_ID_SIZE] = cose_key - .key_id - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let key_id: KeyId = key_id.into(); - match (cose_key.kty, algorithm) { + match (&cose_key.kty, algorithm) { (kty, alg) - if kty == RegisteredLabel::Assigned(KeyType::OKP) - && alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => + if *kty == RegisteredLabel::Assigned(KeyType::OKP) + && *alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => { - let (mut crv, mut x) = (None, None); - for (key, value) in &cose_key.params { - if let coset::Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; - match key { - OkpKeyParameter::Crv => { - crv.replace(value); - } - OkpKeyParameter::X => { - x.replace(value); - } - _ => (), - } - } - } - let (Some(x), Some(crv)) = (x, crv) else { - return Err(CryptoError::InvalidKey); - }; - - if i128::from(crv.as_integer().ok_or(CryptoError::InvalidKey)?) - != EllipticCurve::Ed25519.to_i64().into() - { - return Err(CryptoError::InvalidKey); - } - - let verifying_key_bytes: &[u8; 32] = x - .as_bytes() - .ok_or(CryptoError::InvalidKey)? - .as_slice() - .try_into() - .map_err(|_| CryptoError::InvalidKey)?; - let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(verifying_key_bytes) - .map_err(|_| CryptoError::InvalidKey)?; Ok(VerifyingKey { - id: key_id, - inner: RawVerifyingKey::Ed25519(verifying_key), + id: key_id(&cose_key)?, + inner: RawVerifyingKey::Ed25519(ed25519_verifying_key(&cose_key)?), }) } _ => Err(CryptoError::InvalidKey), From 621374dcd605a92dd0e44f650db8a6403f52196a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:16:57 +0200 Subject: [PATCH 050/112] Move in deprecated annotation --- crates/bitwarden-core/src/mobile/crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index e5fb4aed1..69e699b19 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -582,11 +582,11 @@ pub struct MakeUserSigningKeysResponse { /// Makes a new set of signing keys for a user. This also creates a signed public-key ownership /// claim for the currently used public key. -#[allow(deprecated)] pub fn make_user_signing_keys(client: &Client) -> Result { let key_store = client.internal.get_key_store(); let ctx = key_store.context(); + #[allow(deprecated)] let wrapping_key = ctx .dangerous_get_symmetric_key(SymmetricKeyId::User) .map_err(|_| CryptoError::InvalidKey)?; From d9c224bff969dabd9334451531e37a049b15d71f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:20:59 +0200 Subject: [PATCH 051/112] Remove allow --- crates/bitwarden-crypto/src/store/context.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 93ec97ed3..2779dd171 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -346,7 +346,6 @@ impl KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - #[allow(unused)] fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { if key_id.is_local() { self.local_signing_keys.get(key_id) From 77176333518f67e6b1c08c853693a23c799bc577 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:22:00 +0200 Subject: [PATCH 052/112] Cleanup --- crates/bitwarden-core/tests/register.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bitwarden-core/tests/register.rs b/crates/bitwarden-core/tests/register.rs index 905b3a39d..dd336766c 100644 --- a/crates/bitwarden-core/tests/register.rs +++ b/crates/bitwarden-core/tests/register.rs @@ -33,9 +33,7 @@ async fn test_register_initialize_crypto() { kdf_params: kdf, email: email.to_owned(), private_key: register_response.keys.private.to_string(), - signing_key: None, - method: InitUserCryptoMethod::Password { password: password.to_owned(), user_key: register_response.encrypted_user_key, From 6d8db7c2267a4d280a5d4c7c5fa1c8e93089c4e1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:29:11 +0200 Subject: [PATCH 053/112] Clean up comment --- crates/bitwarden-core/src/mobile/crypto.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 69e699b19..1f01b68a6 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -575,13 +575,12 @@ pub struct MakeUserSigningKeysResponse { verifying_key: String, /// Signing key, encrypted with a symmetric key (user key, org key) signing_key: EncString, - /// A signed object claiming ownership of a public key. This ties the public key to the - /// signature key + /// The user's public key, signed by the signing key signed_public_key: String, } -/// Makes a new set of signing keys for a user. This also creates a signed public-key ownership -/// claim for the currently used public key. +/// Makes a new set of signing keys for a user. This also signs the public key with the signing key +/// and returns the signed public key. pub fn make_user_signing_keys(client: &Client) -> Result { let key_store = client.internal.get_key_store(); let ctx = key_store.context(); From 12364266f7bf94129d4d7aafed707e0a6fe962b0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 10:42:40 +0200 Subject: [PATCH 054/112] Cargo fmt --- crates/bitwarden-crypto/src/signing/mod.rs | 2 +- crates/bitwarden-crypto/src/signing/signing_key.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index a1f794c12..6b1dd5ac0 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -28,7 +28,7 @@ //! then sign detached can be used. mod cose; -pub(self) use cose::*; +pub(self) use cose::*; mod namespace; pub use namespace::SigningNamespace; mod signed_object; diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 6b8f55794..6cd85e622 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -12,9 +12,7 @@ use super::{ verifying_key::{RawVerifyingKey, VerifyingKey}, SignatureAlgorithm, }; -use crate::{ - cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey, -}; +use crate::{cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey}; /// A `SigningKey` without the key id. This enum contains a variant for each supported signature /// scheme. From ec2b153cd6088896aed4715922c4191aa8226abc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 12:10:50 +0200 Subject: [PATCH 055/112] Remove zeroize and pin signing key --- crates/bitwarden-crypto/src/keys/key_id.rs | 6 ------ crates/bitwarden-crypto/src/signing/signing_key.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/key_id.rs b/crates/bitwarden-crypto/src/keys/key_id.rs index cb772d519..2184027a8 100644 --- a/crates/bitwarden-crypto/src/keys/key_id.rs +++ b/crates/bitwarden-crypto/src/keys/key_id.rs @@ -12,12 +12,6 @@ pub(crate) const KEY_ID_SIZE: usize = 16; #[derive(Clone)] pub(crate) struct KeyId(Uuid); -impl zeroize::Zeroize for KeyId { - fn zeroize(&mut self) { - self.0 = Uuid::nil(); - } -} - /// Fixed length identifiers for keys. /// These are intended to be unique and constant per-key. /// diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 6cd85e622..7283acae2 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -1,3 +1,5 @@ +use std::pin::Pin; + use ciborium::{value::Integer, Value}; use coset::{ iana::{Algorithm, EllipticCurve, EnumI64, KeyOperation, KeyType, OkpKeyParameter}, @@ -5,7 +7,6 @@ use coset::{ }; use ed25519_dalek::Signer; use rand::rngs::OsRng; -use zeroize::ZeroizeOnDrop; use super::{ ed25519_signing_key, key_id, @@ -16,14 +17,14 @@ use crate::{cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, Cry /// A `SigningKey` without the key id. This enum contains a variant for each supported signature /// scheme. -#[derive(Clone, zeroize::ZeroizeOnDrop)] +#[derive(Clone)] enum RawSigningKey { - Ed25519(ed25519_dalek::SigningKey), + Ed25519(Pin>), } /// A signing key is a private key used for signing data. An associated `VerifyingKey` can be /// derived from it. -#[derive(Clone, ZeroizeOnDrop)] +#[derive(Clone)] pub struct SigningKey { pub(super) id: KeyId, inner: RawSigningKey, @@ -37,7 +38,7 @@ impl SigningKey { match algorithm { SignatureAlgorithm::Ed25519 => Ok(SigningKey { id: KeyId::make(), - inner: RawSigningKey::Ed25519(ed25519_dalek::SigningKey::generate(&mut OsRng)), + inner: RawSigningKey::Ed25519(Box::pin(ed25519_dalek::SigningKey::generate(&mut OsRng))), }), } } @@ -106,10 +107,9 @@ impl CoseSerializable for SigningKey { if *kty == RegisteredLabel::Assigned(KeyType::OKP) && *alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => { - let key = ed25519_signing_key(&cose_key)?; Ok(SigningKey { id: key_id(&cose_key)?, - inner: RawSigningKey::Ed25519(key), + inner: RawSigningKey::Ed25519(Box::pin(ed25519_signing_key(&cose_key)?)), }) } _ => Err(CryptoError::InvalidKey), From b7388346583819f8de778332482522d5755e06f1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 12:24:00 +0200 Subject: [PATCH 056/112] Fix build and impl zeroizeOnDrop for signingkey --- crates/bitwarden-crypto/src/signing/signing_key.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 7283acae2..50ae3faa3 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -30,6 +30,16 @@ pub struct SigningKey { 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 { @@ -38,7 +48,9 @@ impl SigningKey { match algorithm { SignatureAlgorithm::Ed25519 => Ok(SigningKey { id: KeyId::make(), - inner: RawSigningKey::Ed25519(Box::pin(ed25519_dalek::SigningKey::generate(&mut OsRng))), + inner: RawSigningKey::Ed25519(Box::pin(ed25519_dalek::SigningKey::generate( + &mut OsRng, + ))), }), } } From 863ba337b954f87ba36f06e3e7af194dc85ee38a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 12:30:42 +0200 Subject: [PATCH 057/112] Remove unused error --- crates/bitwarden-crypto/src/error.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index ec5645d08..a40fab3b7 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -62,9 +62,6 @@ pub enum CryptoError { #[error("Cose encoding error")] CoseEncodingError, - - #[error("Invalid encoding")] - InvalidEncoding, } #[derive(Debug, Error)] From 8e61453582e4d215e105a8abd1fa169c2aafce8c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 29 May 2025 12:34:17 +0200 Subject: [PATCH 058/112] Add comment with link to follow-up task --- crates/bitwarden-core/src/mobile/crypto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 1f01b68a6..41f602061 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -602,8 +602,8 @@ pub fn make_user_signing_keys(client: &Client) -> Result Date: Thu, 29 May 2025 13:06:36 +0200 Subject: [PATCH 059/112] Remove unnecessary pub(self) --- crates/bitwarden-crypto/src/signing/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 6b1dd5ac0..1fc83f736 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -28,7 +28,7 @@ //! then sign detached can be used. mod cose; -pub(self) use cose::*; +use cose::*; mod namespace; pub use namespace::SigningNamespace; mod signed_object; From 677541582464e68ff0b5241cca0313160e16d2cc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 10:38:23 +0200 Subject: [PATCH 060/112] Remove empty line --- crates/bitwarden-core/src/mobile/crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 41f602061..d38eb5342 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -48,7 +48,6 @@ pub struct InitUserCryptoRequest { pub email: String, /// The user's encrypted private key pub private_key: String, - /// The user's signing key pub signing_key: Option, From 82c7090580505b3e318ffd0d4bcc4ba6138b099d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 10:51:07 +0200 Subject: [PATCH 061/112] Remove another newline --- crates/bitwarden-core/src/mobile/crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index d38eb5342..c589e5a37 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -50,7 +50,6 @@ pub struct InitUserCryptoRequest { pub private_key: String, /// The user's signing key pub signing_key: Option, - /// The initialization method to use pub method: InitUserCryptoMethod, } From e705d282c1291a9fc922e05f84087c7645c76db9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 11:10:28 +0200 Subject: [PATCH 062/112] Replace OsRng with threadrng --- crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index fdbf1ab07..b6bab857c 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -73,8 +73,7 @@ impl CryptoKey for AsymmetricCryptoKey {} impl AsymmetricCryptoKey { /// Generate a random AsymmetricCryptoKey (RSA-2048). pub fn make(algorithm: PublicKeyEncryptionAlgorithm) -> Self { - use rand::rngs::OsRng; - Self::make_internal(algorithm, &mut OsRng) + Self::make_internal(algorithm, &mut rand::thread_rng()) } fn make_internal( From 632f74e7d7d31a07b6a0ff08f82219792e92fe14 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 11:13:43 +0200 Subject: [PATCH 063/112] Add match to signed public key --- .../src/keys/signed_public_key.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 2b1d70903..9d2b7e03f 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -7,7 +7,8 @@ use serde_bytes::ByteBuf; use super::AsymmetricPublicCryptoKey; use crate::{ - cose::CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, + cose::CoseSerializable, CryptoError, RawPublicKey, SignedObject, SigningKey, SigningNamespace, + VerifyingKey, }; /// `PublicKeyEncryptionAlgorithm` defines the algorithms used for asymmetric encryption. @@ -45,11 +46,13 @@ pub struct SignedPublicKeyMessage { impl SignedPublicKeyMessage { pub fn from_public_key(public_key: &AsymmetricPublicCryptoKey) -> Result { - Ok(SignedPublicKeyMessage { - algorithm: PublicKeyEncryptionAlgorithms::RsaOaepSha1, - content_format: PublicKeyFormat::Spki, - public_key: ByteBuf::from(public_key.to_der()?), - }) + match public_key.inner() { + RawPublicKey::RsaOaepSha1(_) => Ok(SignedPublicKeyMessage { + algorithm: PublicKeyEncryptionAlgorithms::RsaOaepSha1, + content_format: PublicKeyFormat::Spki, + public_key: ByteBuf::from(public_key.to_der()?), + }), + } } pub fn sign(&self, signing_key: &SigningKey) -> Result { From 3b567bcf9a3949b80d7529566e8d83c688de3c4b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 12:36:07 +0200 Subject: [PATCH 064/112] Replace OsRng with threadrng --- crates/bitwarden-crypto/src/signing/signing_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 50ae3faa3..3b3c5a221 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -49,7 +49,7 @@ impl SigningKey { SignatureAlgorithm::Ed25519 => Ok(SigningKey { id: KeyId::make(), inner: RawSigningKey::Ed25519(Box::pin(ed25519_dalek::SigningKey::generate( - &mut OsRng, + &mut rand::thread_rng(), ))), }), } From 7a0b35891855cb76c8b78a0c9d9245320c0abd7e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 13:13:25 +0200 Subject: [PATCH 065/112] Add finegrained encoding errors --- .../src/client/encryption_settings.rs | 6 +- crates/bitwarden-crypto/src/cose.rs | 7 ++- crates/bitwarden-crypto/src/error.rs | 18 +++++- .../src/keys/signed_public_key.rs | 14 ++--- crates/bitwarden-crypto/src/signing/cose.rs | 59 ++++++++++++------- .../bitwarden-crypto/src/signing/message.rs | 12 ++-- .../bitwarden-crypto/src/signing/signature.rs | 10 ++-- .../src/signing/signed_object.rs | 13 ++-- .../src/signing/signing_key.rs | 23 +++++--- .../src/signing/verifying_key.rs | 22 ++++--- .../src/pure_crypto.rs | 2 +- 11 files changed, 117 insertions(+), 69 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 9e004b7a0..9d2d3479f 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -46,7 +46,9 @@ impl EncryptionSettings { signing_key: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{AsymmetricCryptoKey, CoseSerializable, KeyDecryptable, SigningKey}; + use bitwarden_crypto::{ + AsymmetricCryptoKey, CoseSerializable, CryptoError, KeyDecryptable, SigningKey, + }; use log::warn; use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; @@ -70,7 +72,7 @@ impl EncryptionSettings { let signing_key = signing_key .map(|key| { let dec: Vec = key.decrypt_with_key(&user_key)?; - SigningKey::from_cose(dec.as_slice()) + SigningKey::from_cose(dec.as_slice()).map_err(Into::::into) }) .transpose()?; diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index 7ed503c39..f42398174 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 @@ -118,8 +119,8 @@ impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey { } pub trait CoseSerializable { - fn to_cose(&self) -> Result, CryptoError>; - fn from_cose(bytes: &[u8]) -> Result + fn to_cose(&self) -> Result, EncodingError>; + fn from_cose(bytes: &[u8]) -> Result where Self: Sized; } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index a40fab3b7..867ba071f 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -60,8 +60,8 @@ pub enum CryptoError { #[error("Signature error, {0}")] SignatureError(#[from] SignatureError), - #[error("Cose encoding error")] - CoseEncodingError, + #[error("Encoding error, {0}")] + EncodingError(#[from] EncodingError), } #[derive(Debug, Error)] @@ -106,5 +106,19 @@ pub enum SignatureError { 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/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 9d2b7e03f..fdac3cfc6 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -7,8 +7,8 @@ use serde_bytes::ByteBuf; use super::AsymmetricPublicCryptoKey; use crate::{ - cose::CoseSerializable, CryptoError, RawPublicKey, SignedObject, SigningKey, SigningNamespace, - VerifyingKey, + cose::CoseSerializable, error::EncodingError, CryptoError, RawPublicKey, SignedObject, + SigningKey, SigningNamespace, VerifyingKey, }; /// `PublicKeyEncryptionAlgorithm` defines the algorithms used for asymmetric encryption. @@ -68,15 +68,15 @@ impl SignedPublicKeyMessage { pub struct SignedPublicKey(pub(crate) SignedObject); impl TryInto> for SignedPublicKey { - type Error = CryptoError; - fn try_into(self) -> Result, CryptoError> { + type Error = EncodingError; + fn try_into(self) -> Result, EncodingError> { self.0.to_cose() } } impl TryFrom> for SignedPublicKey { - type Error = CryptoError; - fn try_from(bytes: Vec) -> Result { + type Error = EncodingError; + fn try_from(bytes: Vec) -> Result { Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) } } @@ -97,7 +97,7 @@ impl SignedPublicKey { ) { (PublicKeyEncryptionAlgorithms::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( AsymmetricPublicCryptoKey::from_der(&public_key_message.public_key.into_vec()) - .map_err(|_| CryptoError::InvalidKey)?, + .map_err(|_| EncodingError::InvalidValue("public key"))?, ), } } diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 5a79d4fd8..8333a0576 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -7,7 +7,10 @@ use coset::{ use super::SigningNamespace; use crate::{ - cose::SIGNING_NAMESPACE, error::SignatureError, keys::KeyId, CryptoError, KEY_ID_SIZE, + cose::SIGNING_NAMESPACE, + error::{EncodingError, SignatureError}, + keys::KeyId, + CryptoError, KEY_ID_SIZE, }; pub(super) fn namespace( @@ -49,89 +52,101 @@ pub(super) fn content_type( } } -pub(super) fn key_id(cose_key: &CoseKey) -> Result { +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(|_| CryptoError::InvalidKey)?; + .map_err(|_| EncodingError::InvalidValue("key id length"))?; let key_id: KeyId = key_id.into(); Ok(key_id) } pub(super) fn ed25519_signing_key( cose_key: &CoseKey, -) -> Result { +) -> 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(|_| CryptoError::InvalidKey)?, + d.try_into() + .map_err(|_| EncodingError::InvalidCoseEncoding)?, )) } else { - Err(CryptoError::InvalidKey) + Err(EncodingError::UnsupportedValue("OKP curve").into()) } } pub(super) fn ed25519_verifying_key( cose_key: &CoseKey, -) -> Result { +) -> 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() { Ok(ed25519_dalek::VerifyingKey::from_bytes( - x.try_into().map_err(|_| CryptoError::InvalidKey)?, + x.try_into() + .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key"))?, ) - .map_err(|_| CryptoError::InvalidKey)?) + .map_err(|_| EncodingError::InvalidValue("ed25519 verifying key"))?) } else { - Err(CryptoError::InvalidKey) + Err(EncodingError::UnsupportedValue("OKP curve").into()) } } -fn okp_d(cose_key: &CoseKey) -> Result<&[u8], CryptoError> { +fn okp_d(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair let mut d = None; for (key, value) in &cose_key.params { if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + let key = OkpKeyParameter::from_i64(*i) + .ok_or(EncodingError::MissingValue("OKP private key"))?; if key == OkpKeyParameter::D { d.replace(value); } } } - let d = d.ok_or(CryptoError::InvalidKey)?; - Ok(d.as_bytes().ok_or(CryptoError::InvalidKey)?.as_slice()) + let d = d.ok_or(EncodingError::MissingValue("OKP private key"))?; + Ok(d.as_bytes() + .ok_or(EncodingError::InvalidValue("OKP private key"))? + .as_slice()) } -fn okp_x(cose_key: &CoseKey) -> Result<&[u8], CryptoError> { +fn okp_x(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair let mut x = None; for (key, value) in &cose_key.params { if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + let key = OkpKeyParameter::from_i64(*i) + .ok_or(EncodingError::MissingValue("OKP public key"))?; if key == OkpKeyParameter::X { x.replace(value); } } } - let x = x.ok_or(CryptoError::InvalidKey)?; - Ok(x.as_bytes().ok_or(CryptoError::InvalidKey)?.as_slice()) + let x = x.ok_or(EncodingError::MissingValue("OKP public key"))?; + Ok(x.as_bytes() + .ok_or(EncodingError::InvalidValue("OKP public key"))? + .as_slice()) } -fn okp_curve(cose_key: &CoseKey) -> Result { +fn okp_curve(cose_key: &CoseKey) -> Result { // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair let mut crv = None; for (key, value) in &cose_key.params { if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i).ok_or(CryptoError::InvalidKey)?; + let key = + OkpKeyParameter::from_i64(*i).ok_or(EncodingError::InvalidValue("OKP curve"))?; if key == OkpKeyParameter::Crv { crv.replace(value); } } } - let crv = crv.ok_or(CryptoError::InvalidKey)?; - Ok(crv.as_integer().ok_or(CryptoError::InvalidKey)?.into()) + let crv = crv.ok_or(EncodingError::MissingValue("OKP curve"))?; + Ok(crv + .as_integer() + .ok_or(EncodingError::InvalidValue("OKP curve"))? + .into()) } diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs index b3f9aed11..3a69defbb 100644 --- a/crates/bitwarden-crypto/src/signing/message.rs +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -9,7 +9,7 @@ use coset::iana::CoapContentFormat; use serde::{de::DeserializeOwned, Serialize}; -use crate::CryptoError; +use crate::error::EncodingError; /// A message (struct) to be signed, serialized to a byte array, along with the content format of /// the bytes. @@ -41,10 +41,10 @@ impl SerializedMessage { } /// Encodes a message into a `SerializedMessage` using CBOR serialization. - pub(super) fn encode(message: &Message) -> Result { + pub(super) fn encode(message: &Message) -> Result { let mut buffer = Vec::new(); ciborium::ser::into_writer(message, &mut buffer) - .map_err(|_| CryptoError::CoseEncodingError)?; + .map_err(|_| EncodingError::InvalidCborSerialization)?; Ok(SerializedMessage { serialized_message_bytes: buffer, content_type: CoapContentFormat::Cbor, @@ -53,13 +53,13 @@ impl SerializedMessage { /// 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 { + pub fn decode(&self) -> Result { if self.content_type != CoapContentFormat::Cbor { - return Err(CryptoError::CoseEncodingError); + return Err(EncodingError::InvalidValue("Unsupported content type")); } ciborium::de::from_reader(self.serialized_message_bytes.as_slice()) - .map_err(|_| CryptoError::CoseEncodingError) + .map_err(|_| EncodingError::InvalidCborSerialization) } } diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 6d395e932..36600a26f 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::{ cose::{CoseSerializable, SIGNING_NAMESPACE}, - error::SignatureError, + error::{EncodingError, SignatureError}, CryptoError, }; @@ -171,17 +171,17 @@ impl SigningKey { } impl CoseSerializable for Signature { - fn from_cose(bytes: &[u8]) -> Result { + fn from_cose(bytes: &[u8]) -> Result { let cose_sign1 = - CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?; + CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; Ok(Signature(cose_sign1)) } - fn to_cose(&self) -> Result, CryptoError> { + fn to_cose(&self) -> Result, EncodingError> { self.0 .clone() .to_vec() - .map_err(|_| SignatureError::InvalidSignature.into()) + .map_err(|_| EncodingError::InvalidCoseEncoding) } } diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index f712138c5..91c0d82a5 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::{ cose::{CoseSerializable, SIGNING_NAMESPACE}, - error::SignatureError, + error::{EncodingError, SignatureError}, CryptoError, }; @@ -50,9 +50,10 @@ impl SignedObject { ) -> Result { SerializedMessage::from_bytes( self.verify_and_unwrap_bytes(verifying_key, namespace)?, - self.content_type()?, + self.content_type().map_err(Into::::into)?, ) .decode() + .map_err(Into::into) } /// Verifies the signature of the signed object and returns the payload as raw bytes, if the @@ -141,17 +142,17 @@ impl SigningKey { } impl CoseSerializable for SignedObject { - fn from_cose(bytes: &[u8]) -> Result { + fn from_cose(bytes: &[u8]) -> Result { Ok(SignedObject( - CoseSign1::from_slice(bytes).map_err(|_| SignatureError::InvalidSignature)?, + CoseSign1::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?, )) } - fn to_cose(&self) -> Result, CryptoError> { + fn to_cose(&self) -> Result, EncodingError> { self.0 .clone() .to_vec() - .map_err(|_| SignatureError::InvalidSignature.into()) + .map_err(|_| EncodingError::InvalidCoseEncoding) } } diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 3b3c5a221..5a96ddd9c 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -6,14 +6,18 @@ use coset::{ CborSerializable, CoseKey, RegisteredLabel, RegisteredLabelWithPrivate, }; use ed25519_dalek::Signer; -use rand::rngs::OsRng; use super::{ ed25519_signing_key, key_id, verifying_key::{RawVerifyingKey, VerifyingKey}, SignatureAlgorithm, }; -use crate::{cose::CoseSerializable, error::Result, keys::KeyId, CryptoError, CryptoKey}; +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. @@ -84,7 +88,7 @@ impl SigningKey { impl CoseSerializable for SigningKey { /// Serializes the signing key to a COSE-formatted byte array. - fn to_cose(&self) -> Result> { + fn to_cose(&self) -> Result, EncodingError> { match &self.inner { RawSigningKey::Ed25519(key) => { coset::CoseKeyBuilder::new_okp_key() @@ -102,17 +106,18 @@ impl CoseSerializable for SigningKey { .add_key_op(KeyOperation::Verify) .build() .to_vec() - .map_err(|_| CryptoError::InvalidKey) + .map_err(|_| EncodingError::InvalidCoseEncoding) } } } /// 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(|_| CryptoError::InvalidKey)?; + fn from_cose(bytes: &[u8]) -> Result { + let cose_key = + CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; let Some(ref algorithm) = cose_key.alg else { - return Err(CryptoError::InvalidKey); + return Err(EncodingError::MissingValue("cose key algorithm")); }; match (&cose_key.kty, algorithm) { (kty, alg) @@ -124,7 +129,9 @@ impl CoseSerializable for SigningKey { inner: RawSigningKey::Ed25519(Box::pin(ed25519_signing_key(&cose_key)?)), }) } - _ => Err(CryptoError::InvalidKey), + _ => Err(EncodingError::UnsupportedValue( + "cose key type or algorithm", + )), } } } diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 5b2eaf819..be7d0c786 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -10,7 +10,12 @@ use coset::{ }; use super::{ed25519_verifying_key, key_id, SignatureAlgorithm}; -use crate::{cose::CoseSerializable, error::SignatureError, keys::KeyId, CryptoError}; +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. @@ -53,7 +58,7 @@ impl VerifyingKey { } impl CoseSerializable for VerifyingKey { - fn to_cose(&self) -> Result, CryptoError> { + fn to_cose(&self) -> Result, EncodingError> { match &self.inner { RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) @@ -73,18 +78,19 @@ impl CoseSerializable for VerifyingKey { .add_key_op(KeyOperation::Verify) .build() .to_vec() - .map_err(|_| CryptoError::InvalidKey), + .map_err(|_| EncodingError::InvalidCoseEncoding), } } - fn from_cose(bytes: &[u8]) -> Result + fn from_cose(bytes: &[u8]) -> Result where Self: Sized, { - let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| CryptoError::InvalidKey)?; + let cose_key = + coset::CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; let Some(ref algorithm) = cose_key.alg else { - return Err(CryptoError::InvalidKey); + return Err(EncodingError::MissingValue("Cose key algorithm")); }; match (&cose_key.kty, algorithm) { (kty, alg) @@ -96,7 +102,9 @@ impl CoseSerializable for VerifyingKey { inner: RawVerifyingKey::Ed25519(ed25519_verifying_key(&cose_key)?), }) } - _ => Err(CryptoError::InvalidKey), + _ => Err(EncodingError::UnsupportedValue( + "unsupported COSE key type or algorithm", + )), } } } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 9591d9178..824b4d3bc 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -276,7 +276,7 @@ impl PureCrypto { let bytes = Self::symmetric_decrypt_bytes(signing_key, wrapping_key)?; let signing_key = SigningKey::from_cose(&bytes)?; let verifying_key = signing_key.to_verifying_key(); - verifying_key.to_cose() + verifying_key.to_cose().map_err(Into::::into) } /// Returns the algorithm used for the given verifying key. From 36df567d6e312e3716ab457672dd14cbe40fc2ab Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 13:14:53 +0200 Subject: [PATCH 066/112] Apply clippy fixes --- crates/bitwarden-crypto/src/signing/cose.rs | 4 ++-- crates/bitwarden-crypto/src/signing/signed_object.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 8333a0576..f18e44122 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -74,7 +74,7 @@ pub(super) fn ed25519_signing_key( .map_err(|_| EncodingError::InvalidCoseEncoding)?, )) } else { - Err(EncodingError::UnsupportedValue("OKP curve").into()) + Err(EncodingError::UnsupportedValue("OKP curve")) } } @@ -91,7 +91,7 @@ pub(super) fn ed25519_verifying_key( ) .map_err(|_| EncodingError::InvalidValue("ed25519 verifying key"))?) } else { - Err(EncodingError::UnsupportedValue("OKP curve").into()) + Err(EncodingError::UnsupportedValue("OKP curve")) } } diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 91c0d82a5..f971270d4 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -50,7 +50,7 @@ impl SignedObject { ) -> Result { SerializedMessage::from_bytes( self.verify_and_unwrap_bytes(verifying_key, namespace)?, - self.content_type().map_err(Into::::into)?, + self.content_type()?, ) .decode() .map_err(Into::into) From d378933cf2b48959120008fb36ba4e2ccc8a98b1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 2 Jun 2025 13:16:02 +0200 Subject: [PATCH 067/112] Clean up error --- crates/bitwarden-crypto/src/signing/verifying_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index be7d0c786..24998ac7d 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -103,7 +103,7 @@ impl CoseSerializable for VerifyingKey { }) } _ => Err(EncodingError::UnsupportedValue( - "unsupported COSE key type or algorithm", + "COSE key type or algorithm", )), } } From 0c042be1facb3f2147d5a2cb7019fa298feecae8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:00:51 +0200 Subject: [PATCH 068/112] Update crates/bitwarden-crypto/src/signing/verifying_key.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/verifying_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 24998ac7d..007b01e4e 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -72,7 +72,7 @@ impl CoseSerializable for VerifyingKey { // 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(), + OkpKeyParameter::X.to_i64(), // Verifying key (digital signature public key) Value::Bytes(key.to_bytes().to_vec()), ) .add_key_op(KeyOperation::Verify) From 37436d478b1995af7f324323672f8c26aa4cc31c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:00:58 +0200 Subject: [PATCH 069/112] Update crates/bitwarden-crypto/src/signing/verifying_key.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/verifying_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 007b01e4e..46db73ebe 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -64,7 +64,7 @@ impl CoseSerializable for VerifyingKey { .key_id((&self.id).into()) .algorithm(Algorithm::EdDSA) .param( - OkpKeyParameter::Crv.to_i64(), + 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 From 594f0f129b15c5416788f0197b3fb8980d28fe56 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:01:49 +0200 Subject: [PATCH 070/112] Update crates/bitwarden-crypto/src/keys/signed_public_key.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/keys/signed_public_key.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index fdac3cfc6..9a54a230d 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -13,9 +13,9 @@ use crate::{ /// `PublicKeyEncryptionAlgorithm` defines the algorithms used for asymmetric encryption. /// Currently, only RSA with OAEP and SHA-1 keys are used. -#[derive(Serialize, Deserialize)] +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] enum PublicKeyEncryptionAlgorithms { - #[serde(rename = "0")] RsaOaepSha1 = 0, } From 67b9757c45bdc14a67ffdb6016235db8dfeda6d4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:12:03 +0200 Subject: [PATCH 071/112] Add docs --- crates/bitwarden-crypto/src/signing/namespace.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index 6314f9884..70d07da36 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -20,10 +20,13 @@ pub enum SigningNamespace { } impl SigningNamespace { + /// Returns the numeric value of the namespace. pub fn as_i64(&self) -> i64 { *self as i64 } + /// Converts an i64 value to a `SigningNamespace`, and fails if there is no corresponding + /// namespace for the value. pub fn try_from_i64(value: i64) -> Result { match value { 1 => Ok(Self::SignedPublicKey), From bf91effe8ad2ac5851259710c76e124886ed5e2a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:24:04 +0200 Subject: [PATCH 072/112] Move test message into test module --- crates/bitwarden-crypto/src/signing/mod.rs | 11 +++++------ crates/bitwarden-crypto/src/signing/signed_object.rs | 5 +++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 1fc83f736..88c25ad33 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -62,17 +62,16 @@ impl SignatureAlgorithm { } } -#[cfg(test)] -#[derive(Deserialize, Debug, PartialEq, Serialize)] -pub(super) struct TestMessage { - field1: String, -} - #[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. diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index f971270d4..6318dffa5 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -179,6 +179,11 @@ mod tests { 6, ]; + #[derive(Deserialize, Debug, PartialEq, Serialize)] + struct TestMessage { + field1: String, + } + #[test] fn test_roundtrip_cose() { let signed_object = SignedObject::from_cose(SIGNED_OBJECT).unwrap(); From a1a297588766b1b58376bdaa2c73b94b80eb84c1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:28:38 +0200 Subject: [PATCH 073/112] Fix build --- Cargo.lock | 1 + crates/bitwarden-crypto/Cargo.toml | 1 + crates/bitwarden-crypto/src/keys/signed_public_key.rs | 5 +++-- crates/bitwarden-crypto/src/signing/signed_object.rs | 4 +++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0e27ecc7..3e2ed9d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "serde_repr", "sha1", "sha2", "subtle", diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index a2e82bf14..38623b463 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -47,6 +47,7 @@ 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/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 9a54a230d..5e669830e 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; +use serde_repr::{Deserialize_repr, Serialize_repr}; use super::AsymmetricPublicCryptoKey; use crate::{ @@ -22,9 +23,9 @@ enum PublicKeyEncryptionAlgorithms { /// `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, Deserialize)] +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] enum PublicKeyFormat { - #[serde(rename = "0")] Spki = 0, } diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 6318dffa5..fb5c21bc2 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -158,8 +158,10 @@ impl CoseSerializable for SignedObject { #[cfg(test)] mod tests { + use serde::{Deserialize, Serialize}; + use crate::{ - CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, TestMessage, + CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; From cd84ab7aced9c5bb26abb3eca6b83ded3124cb31 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:28:58 +0200 Subject: [PATCH 074/112] Cargo fmt --- crates/bitwarden-crypto/src/signing/signed_object.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index fb5c21bc2..ac8276ff7 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -161,8 +161,7 @@ mod tests { use serde::{Deserialize, Serialize}; use crate::{ - CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, - VerifyingKey, + CoseSerializable, CryptoError, SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; const VERIFYING_KEY: &[u8] = &[ From 4b26c43c671e16ca9b125d789807aa22c6050b2a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:34:46 +0200 Subject: [PATCH 075/112] Add documentation --- crates/bitwarden-crypto/src/signing/signature.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 36600a26f..7113b2d5c 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -32,6 +32,8 @@ impl Signature { 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) } From 3f9e087d885c5f063a3e562e9d7af2696d7d89a6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:35:39 +0200 Subject: [PATCH 076/112] Update crates/bitwarden-crypto/src/signing/signed_object.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/signed_object.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index ac8276ff7..d50ab2710 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -67,8 +67,7 @@ impl SignedObject { return Err(SignatureError::InvalidSignature.into()); }; - let signature_namespace = self.namespace()?; - if signature_namespace != *namespace { + if self.namespace()? != *namespace { return Err(SignatureError::InvalidNamespace.into()); } From 9c008fdeb9187ec20b29c51619d2768c66b5d727 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:35:53 +0200 Subject: [PATCH 077/112] Update crates/bitwarden-core/src/mobile/crypto.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-core/src/mobile/crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index c589e5a37..8fb78c8cd 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -569,7 +569,7 @@ pub(super) fn verify_asymmetric_keys( #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct MakeUserSigningKeysResponse { - /// The verifying key + /// Base64 encoded verifying key verifying_key: String, /// Signing key, encrypted with a symmetric key (user key, org key) signing_key: EncString, From eb5e183e684095dd9a0a92e5125a95a4d55abed2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:38:11 +0200 Subject: [PATCH 078/112] Update crates/bitwarden-crypto/src/signing/signature.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/signature.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index 7113b2d5c..de11142dc 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -52,10 +52,7 @@ impl Signature { return false; }; - let Ok(signature_namespace) = self.namespace() else { - return false; - }; - if signature_namespace != *namespace { + if self.namespace().ok().as_ref() != Some(namespace) { return false; } From fa8f8e6b6514db9ea0c99a865133aeea2ba7d7f6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:38:22 +0200 Subject: [PATCH 079/112] Update crates/bitwarden-crypto/src/signing/signed_object.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/signed_object.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index d50ab2710..225f914e6 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -63,9 +63,9 @@ impl SignedObject { verifying_key: &VerifyingKey, namespace: &SigningNamespace, ) -> Result, CryptoError> { - let Some(_alg) = &self.inner().protected.header.alg else { + if self.inner().protected.header.alg.is_none() { return Err(SignatureError::InvalidSignature.into()); - }; + } if self.namespace()? != *namespace { return Err(SignatureError::InvalidNamespace.into()); From dd6ad41889f6f42e4f06bdbfbef12a4ed3a134d0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:38:45 +0200 Subject: [PATCH 080/112] Update crates/bitwarden-crypto/src/signing/namespace.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/namespace.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index 70d07da36..dd13c3076 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -12,7 +12,6 @@ pub enum SigningNamespace { /// The namespace for /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, - /// This namespace is only used in tests and documentation. ExampleNamespace = -1, /// This namespace is only used in tests and documentation. From 2d7346b51154f4060d00fc4b3875d336275230a7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:41:20 +0200 Subject: [PATCH 081/112] Change signing key to encstring --- crates/bitwarden-core/src/mobile/crypto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index c589e5a37..dc5b40c71 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -49,7 +49,7 @@ pub struct InitUserCryptoRequest { /// The user's encrypted private key pub private_key: String, /// The user's signing key - pub signing_key: Option, + pub signing_key: Option, /// The initialization method to use pub method: InitUserCryptoMethod, } @@ -135,7 +135,7 @@ pub async fn initialize_user_crypto( use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; let private_key: EncString = req.private_key.parse()?; - let signing_key: Option = req.signing_key.map(|s| s.parse()).transpose()?; + let signing_key = req.signing_key; if let Some(user_id) = req.user_id { client.internal.init_user_id(user_id)?; From 6d9e0671c58e66102c0321187879630b0832d177 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:46:03 +0200 Subject: [PATCH 082/112] Add docs --- crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index b6bab857c..35bec42da 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -5,6 +5,7 @@ use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey}; use super::key_encryptable::CryptoKey; use crate::error::{CryptoError, Result}; +/// Algorithm / public key encryption scheme used for encryption/decryption. pub enum PublicKeyEncryptionAlgorithm { RsaOaepSha1, } From 29da0d89f3f95f527bce75013fd002db6ffaee6e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:48:24 +0200 Subject: [PATCH 083/112] Add docs --- crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 35bec42da..6f2d1e348 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -15,6 +15,8 @@ pub(crate) enum RawPublicKey { RsaOaepSha1(RsaPublicKey), } +/// Public key of a key pair used in a public key encryption scheme. It is used for +/// encrypting data. #[derive(Clone)] pub struct AsymmetricPublicCryptoKey { inner: RawPublicKey, @@ -34,6 +36,7 @@ impl AsymmetricPublicCryptoKey { }) } + /// Makes a SubjectPublicKeyInfo DER serialized version of the public key. pub fn to_der(&self) -> Result> { use rsa::pkcs8::EncodePublicKey; match &self.inner { @@ -55,6 +58,8 @@ pub(crate) enum RawPrivateKey { 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, From 7439bb10b5029ef13a20d7c26a9b6dffce4092b5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:49:49 +0200 Subject: [PATCH 084/112] Add docs --- crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 6f2d1e348..5d415ee27 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -126,6 +126,8 @@ impl AsymmetricCryptoKey { } } + /// 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 { From f2c0991b8ced6f7da91dce99444cb900c4e6baa6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 13:51:11 +0200 Subject: [PATCH 085/112] Add docs --- crates/bitwarden-crypto/src/keys/signed_public_key.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 5e669830e..0b67b4abe 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -46,6 +46,8 @@ pub struct SignedPublicKeyMessage { } 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 { @@ -56,6 +58,8 @@ impl SignedPublicKeyMessage { } } + /// 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)?, From 8f6168bc5bcfc81c74709432006655ae019032e5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:01:14 +0200 Subject: [PATCH 086/112] Add docs --- crates/bitwarden-crypto/src/signing/cose.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index f18e44122..1b616216a 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -13,6 +13,8 @@ use crate::{ 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 { @@ -37,6 +39,9 @@ pub(super) fn 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 { @@ -52,6 +57,8 @@ pub(super) fn content_type( } } +/// 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 @@ -62,6 +69,7 @@ pub(super) fn key_id(cose_key: &CoseKey) -> Result { Ok(key_id) } +/// Helper function to parse a ed25519 signing key from a `CoseKey`. pub(super) fn ed25519_signing_key( cose_key: &CoseKey, ) -> Result { @@ -78,6 +86,7 @@ pub(super) fn ed25519_signing_key( } } +/// Helper function to parse a ed25519 verifying key from a `CoseKey`. pub(super) fn ed25519_verifying_key( cose_key: &CoseKey, ) -> Result { @@ -95,6 +104,7 @@ pub(super) fn ed25519_verifying_key( } } +/// 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 let mut d = None; @@ -113,6 +123,7 @@ fn okp_d(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { .as_slice()) } +/// 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 let mut x = None; @@ -131,6 +142,7 @@ fn okp_x(cose_key: &CoseKey) -> Result<&[u8], EncodingError> { .as_slice()) } +/// 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 let mut crv = None; From 057098207508ceec6f4c6b2c85e720fa384939c9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:01:24 +0200 Subject: [PATCH 087/112] Format --- crates/bitwarden-crypto/src/signing/cose.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 1b616216a..37af7d1d1 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -13,8 +13,8 @@ use crate::{ 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. +/// 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 { @@ -39,9 +39,9 @@ pub(super) fn 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. +/// 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 { From 351ccf44a20ccab9fe9516816da8240ffb5c4653 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:02:10 +0200 Subject: [PATCH 088/112] Update crates/bitwarden-crypto/src/signing/signature.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/signature.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index de11142dc..fb6f5b2a7 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -48,9 +48,9 @@ impl Signature { verifying_key: &VerifyingKey, namespace: &SigningNamespace, ) -> bool { - let Some(_alg) = &self.inner().protected.header.alg else { + if self.inner().protected.header.alg.is_none() { return false; - }; + } if self.namespace().ok().as_ref() != Some(namespace) { return false; From aaacfbc22af4a5cdc655cc85255ee71348b5f875 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:06:46 +0200 Subject: [PATCH 089/112] Add docs --- crates/bitwarden-crypto/src/signing/message.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs index 3a69defbb..6a83087f6 100644 --- a/crates/bitwarden-crypto/src/signing/message.rs +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -25,6 +25,7 @@ impl AsRef<[u8]> for SerializedMessage { } 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, @@ -32,6 +33,8 @@ impl SerializedMessage { } } + /// 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 } From 4f1c50581d6c81ef87417e36317906d4478963d2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:10:38 +0200 Subject: [PATCH 090/112] Add docs --- crates/bitwarden-crypto/src/signing/signed_object.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index 225f914e6..eed0c95db 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -12,6 +12,10 @@ use crate::{ 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. pub struct SignedObject(pub(crate) CoseSign1); impl From for SignedObject { From b1a11762ce68b229b254c20e07948cef5bbaa3b8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:16:18 +0200 Subject: [PATCH 091/112] Add signed org memberships / emergency access --- crates/bitwarden-crypto/src/signing/signature.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index fb6f5b2a7..c1d554949 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -41,6 +41,9 @@ impl Signature { /// 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: + /// https://bitwarden.atlassian.net/browse/PM-17458 #[allow(unused)] pub fn verify( &self, From 88145cf36b0638b513da550e45530079a9d5162a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:17:58 +0200 Subject: [PATCH 092/112] Add docs --- crates/bitwarden-crypto/src/signing/signed_object.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index eed0c95db..b8aff2425 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -25,6 +25,8 @@ impl From for SignedObject { } 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) } From 4635bc62cd522554af8fdf08c09b7d435f8d8d59 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:23:01 +0200 Subject: [PATCH 093/112] Apply suggestion to clean up from_cose --- .../src/signing/signing_key.rs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 5a96ddd9c..287744e8b 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -116,21 +116,16 @@ impl CoseSerializable for SigningKey { let cose_key = CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; - let Some(ref algorithm) = cose_key.alg else { - return Err(EncodingError::MissingValue("cose key algorithm")); - }; - match (&cose_key.kty, algorithm) { - (kty, alg) - if *kty == RegisteredLabel::Assigned(KeyType::OKP) - && *alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => - { - Ok(SigningKey { - id: key_id(&cose_key)?, - inner: RawSigningKey::Ed25519(Box::pin(ed25519_signing_key(&cose_key)?)), - }) - } + 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", + "COSE key type or algorithm", )), } } From ff59f807dd61dc453ef4304ce3ab8d970e2b9c5c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:24:49 +0200 Subject: [PATCH 094/112] Apply suggestion to rewrite from_cose in verifying_key --- .../src/signing/verifying_key.rs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 46db73ebe..39597ae6d 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -89,19 +89,17 @@ impl CoseSerializable for VerifyingKey { let cose_key = coset::CoseKey::from_slice(bytes).map_err(|_| EncodingError::InvalidCoseEncoding)?; - let Some(ref algorithm) = cose_key.alg else { - return Err(EncodingError::MissingValue("Cose key algorithm")); - }; + let algorithm = cose_key.alg.as_ref().ok_or(EncodingError::MissingValue( + "COSE key algorithm", + ))?; match (&cose_key.kty, algorithm) { - (kty, alg) - if *kty == RegisteredLabel::Assigned(KeyType::OKP) - && *alg == RegisteredLabelWithPrivate::Assigned(Algorithm::EdDSA) => - { - Ok(VerifyingKey { - id: key_id(&cose_key)?, - inner: RawVerifyingKey::Ed25519(ed25519_verifying_key(&cose_key)?), - }) - } + ( + 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", )), From b726fb476ad428edc837dab24fe210a4c475191b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 14:30:15 +0200 Subject: [PATCH 095/112] Fix build --- .../bitwarden-core/src/auth/auth_request.rs | 7 +---- .../src/client/encryption_settings.rs | 4 +-- crates/bitwarden-core/src/client/internal.rs | 2 -- crates/bitwarden-core/src/mobile/crypto.rs | 26 ++++++++++++------- .../src/signing/verifying_key.rs | 7 ++--- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 396990681..4e9abb26a 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -227,12 +227,7 @@ mod tests { existing_device .internal - .initialize_user_crypto_master_key( - master_key, - user_key, - private_key.clone(), - None, - ) + .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 diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 6167803f0..5dc506350 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}; diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index d09e4a3bc..fcd22ea30 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -11,8 +11,6 @@ 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, diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index dd0d1ae43..1d3340f74 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -150,9 +150,11 @@ pub async fn initialize_user_crypto( } 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, req.signing_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::Pin { pin, @@ -163,7 +165,7 @@ pub async fn initialize_user_crypto( pin_key, pin_protected_user_key, req.private_key, - req.signing_key + req.signing_key, )?; } InitUserCryptoMethod::AuthRequest { @@ -183,9 +185,11 @@ pub async fn initialize_user_crypto( auth_request_key, )?, }; - client - .internal - .initialize_user_crypto_decrypted_key(user_key, req.private_key, req.signing_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -196,9 +200,11 @@ pub 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, req.signing_key)?; + client.internal.initialize_user_crypto_decrypted_key( + user_key, + req.private_key, + req.signing_key, + )?; } InitUserCryptoMethod::KeyConnector { master_key, diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 39597ae6d..53d890e43 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -89,9 +89,10 @@ impl CoseSerializable for VerifyingKey { 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", - ))?; + let algorithm = cose_key + .alg + .as_ref() + .ok_or(EncodingError::MissingValue("COSE key algorithm"))?; match (&cose_key.kty, algorithm) { ( RegisteredLabel::Assigned(KeyType::OKP), From 36cb017f4c3a31270c71aa4dfb2d69a362be09c1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 17:11:49 +0200 Subject: [PATCH 096/112] Fix build --- crates/bitwarden-core/src/auth/auth_request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 4e9abb26a..d2afb11a4 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -244,7 +244,7 @@ mod tests { user_id: Some(uuid::Uuid::new_v4()), kdf_params: kdf, email: email.to_owned(), - private_key: private_key, + private_key, signing_key: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, From f64037487acaccbfaf25660f3a030d55fe665869 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 17:23:29 +0200 Subject: [PATCH 097/112] Fix doc formatting --- crates/bitwarden-crypto/src/signing/signature.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index c1d554949..e3cef85e7 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -43,7 +43,7 @@ impl Signature { /// used to create the signature. /// /// The first anticipated consumer will be signed org memberships / emergency access: - /// https://bitwarden.atlassian.net/browse/PM-17458 + /// #[allow(unused)] pub fn verify( &self, From d73818c084a3ad2a722a1d3b66f7adc5202c99da Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 18:08:04 +0200 Subject: [PATCH 098/112] Fix typo --- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 824b4d3bc..f2e16ffcf 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -596,7 +596,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= } #[test] - fn test_verify_aend_unwrap_signed_public_key() { + 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(), From 626557fe4335dd8166a74befc4ee58f9a7431c25 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 18:18:05 +0200 Subject: [PATCH 099/112] Fix testvector --- .../src/pure_crypto.rs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index f2e16ffcf..bb8d1ff27 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -308,7 +308,7 @@ impl PureCrypto { mod tests { use std::{num::NonZero, str::FromStr}; - use bitwarden_crypto::EncString; + use bitwarden_crypto::{EncString, SignedPublicKeyMessage}; use super::*; @@ -395,26 +395,26 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 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, 61, 163, 99, 97, 108, 103, 97, - 48, 102, 102, 111, 114, 109, 97, 116, 97, 48, 99, 107, 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, 92, 149, 155, 77, 244, 168, 111, 175, 145, 222, 0, - 110, 243, 119, 10, 40, 86, 232, 153, 163, 175, 146, 45, 173, 64, 132, 217, 205, 244, 165, - 92, 57, 43, 150, 208, 69, 143, 169, 50, 240, 204, 77, 199, 190, 191, 167, 27, 214, 108, - 233, 41, 59, 177, 85, 61, 44, 218, 230, 136, 112, 204, 64, 192, 3, + 219, 193, 50, 30, 21, 43, 58, 0, 1, 56, 127, 1, 160, 89, 1, 59, 163, 99, 97, 108, 103, 0, + 102, 102, 111, 114, 109, 97, 116, 0, 99, 107, 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, 245, 62, 154, 222, 121, 29, 101, 75, 129, 125, 252, + 225, 90, 52, 91, 41, 162, 208, 202, 185, 197, 178, 151, 126, 247, 217, 122, 34, 42, 209, + 90, 235, 0, 165, 101, 225, 245, 113, 23, 35, 26, 183, 175, 46, 174, 197, 224, 200, 218, 74, + 226, 65, 133, 94, 205, 15, 136, 79, 29, 176, 226, 72, 135, 4, ]; #[test] From e11ff9a0a0b1a2b892244672cb6f68ed6a707431 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 18:21:21 +0200 Subject: [PATCH 100/112] Fix clippy error --- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index bb8d1ff27..c4a3cfdb0 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -308,7 +308,7 @@ impl PureCrypto { mod tests { use std::{num::NonZero, str::FromStr}; - use bitwarden_crypto::{EncString, SignedPublicKeyMessage}; + use bitwarden_crypto::EncString; use super::*; From ac72887f643e7b6feb1d5ee6eafc4b9a1ea5d48e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 18:46:35 +0200 Subject: [PATCH 101/112] Update crates/bitwarden-crypto/src/signing/cose.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-crypto/src/signing/cose.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 37af7d1d1..25ea98ecf 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -45,16 +45,15 @@ pub(super) fn namespace( pub(super) fn content_type( protected_header: &ProtectedHeader, ) -> Result { - if let RegisteredLabel::Assigned(content_format) = protected_header + protected_header .header .content_type - .clone() - .ok_or(CryptoError::from(SignatureError::InvalidSignature))? - { - Ok(content_format) - } else { - Err(SignatureError::InvalidSignature.into()) - } + .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 From 91723f78736958b85fa2b54d617a3baa7b44ed67 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 19:00:59 +0200 Subject: [PATCH 102/112] Cleanup and expose SignedPublicKey object publically --- crates/bitwarden-core/src/mobile/crypto.rs | 6 +- crates/bitwarden-crypto/examples/signature.rs | 12 +--- .../examples/signed_object.rs | 7 +- crates/bitwarden-crypto/src/cose.rs | 2 +- .../src/keys/signed_public_key.rs | 29 +++++++- crates/bitwarden-crypto/src/signing/cose.rs | 72 ++++++++----------- crates/bitwarden-crypto/src/signing/mod.rs | 14 ++-- .../bitwarden-crypto/src/signing/signature.rs | 6 +- .../src/signing/signed_object.rs | 6 +- .../src/signing/signing_key.rs | 11 ++- .../src/signing/verifying_key.rs | 11 ++- crates/bitwarden-crypto/src/uniffi_support.rs | 11 ++- 12 files changed, 89 insertions(+), 98 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 1d3340f74..39ce0eeea 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -595,12 +595,10 @@ pub fn make_user_signing_keys(client: &Client) -> Result for SymmetricCryptoKey { } pub trait CoseSerializable { - fn to_cose(&self) -> Result, EncodingError>; + fn to_cose(&self) -> Vec; fn from_cose(bytes: &[u8]) -> Result where Self: Sized; diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 0b67b4abe..da5514b8e 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -2,6 +2,7 @@ //! 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 base64::{engine::general_purpose::STANDARD, Engine}; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -12,6 +13,12 @@ use crate::{ 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; +"#; + /// `PublicKeyEncryptionAlgorithm` defines the algorithms used for asymmetric encryption. /// Currently, only RSA with OAEP and SHA-1 keys are used. #[derive(Serialize_repr, Deserialize_repr)] @@ -72,9 +79,8 @@ impl SignedPublicKeyMessage { /// the Signer before they can use the public key for encryption. pub struct SignedPublicKey(pub(crate) SignedObject); -impl TryInto> for SignedPublicKey { - type Error = EncodingError; - fn try_into(self) -> Result, EncodingError> { +impl Into> for SignedPublicKey { + fn into(self) -> Vec { self.0.to_cose() } } @@ -86,6 +92,23 @@ impl TryFrom> for SignedPublicKey { } } +impl Into for SignedPublicKey { + fn into(self) -> String { + let bytes: Vec = self.into(); + STANDARD.encode(&bytes) + } +} + +impl TryFrom for SignedPublicKey { + type Error = EncodingError; + fn try_from(encoded: String) -> Result { + let bytes = STANDARD + .decode(encoded) + .map_err(|_| EncodingError::InvalidCborSerialization)?; + Self::try_from(bytes) + } +} + impl SignedPublicKey { /// Verifies the signature of the public key against the provided `VerifyingKey`, and returns /// the `AsymmetricPublicCryptoKey` if the verification is successful. diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 37af7d1d1..3a83356e2 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -94,11 +94,11 @@ pub(super) fn ed25519_verifying_key( let x = okp_x(cose_key)?; let crv = okp_curve(cose_key)?; if crv == EllipticCurve::Ed25519.to_i64().into() { - Ok(ed25519_dalek::VerifyingKey::from_bytes( + ed25519_dalek::VerifyingKey::from_bytes( x.try_into() .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key"))?, ) - .map_err(|_| EncodingError::InvalidValue("ed25519 verifying key"))?) + .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key")) } else { Err(EncodingError::UnsupportedValue("OKP curve")) } @@ -107,58 +107,44 @@ pub(super) fn ed25519_verifying_key( /// 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 - let mut d = None; - for (key, value) in &cose_key.params { - if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i) - .ok_or(EncodingError::MissingValue("OKP private key"))?; - if key == OkpKeyParameter::D { - d.replace(value); + 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()) } - } - } - let d = d.ok_or(EncodingError::MissingValue("OKP private key"))?; - Ok(d.as_bytes() - .ok_or(EncodingError::InvalidValue("OKP private key"))? - .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 - let mut x = None; - for (key, value) in &cose_key.params { - if let Label::Int(i) = key { - let key = OkpKeyParameter::from_i64(*i) - .ok_or(EncodingError::MissingValue("OKP public key"))?; - if key == OkpKeyParameter::X { - x.replace(value); + 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()) } - } - } - let x = x.ok_or(EncodingError::MissingValue("OKP public key"))?; - Ok(x.as_bytes() - .ok_or(EncodingError::InvalidValue("OKP public key"))? - .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 - let mut crv = None; - for (key, value) in &cose_key.params { - if let Label::Int(i) = key { - let key = - OkpKeyParameter::from_i64(*i).ok_or(EncodingError::InvalidValue("OKP curve"))?; - if key == OkpKeyParameter::Crv { - crv.replace(value); + 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(|v| i128::from(v)) } - } - } - - let crv = crv.ok_or(EncodingError::MissingValue("OKP curve"))?; - Ok(crv - .as_integer() - .ok_or(EncodingError::InvalidValue("OKP curve"))? - .into()) + _ => None, + }) + .ok_or(EncodingError::MissingValue("OKP curve")) } diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 88c25ad33..38c19a621 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -89,25 +89,19 @@ mod tests { .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().unwrap() - ); + println!("const SIGNING_KEY: &[u8] = &{:?};", signing_key.to_cose()); println!( "const VERIFYING_KEY: &[u8] = &{:?};", - verifying_key.to_cose().unwrap() - ); - println!( - "const SIGNATURE: &[u8] = &{:?};", - signature.to_cose().unwrap() + 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().unwrap() + signed_object.to_cose() ); println!( "const SIGNED_OBJECT_RAW: &[u8] = &{:?};", diff --git a/crates/bitwarden-crypto/src/signing/signature.rs b/crates/bitwarden-crypto/src/signing/signature.rs index e3cef85e7..675db3bb7 100644 --- a/crates/bitwarden-crypto/src/signing/signature.rs +++ b/crates/bitwarden-crypto/src/signing/signature.rs @@ -179,11 +179,11 @@ impl CoseSerializable for Signature { Ok(Signature(cose_sign1)) } - fn to_cose(&self) -> Result, EncodingError> { + fn to_cose(&self) -> Vec { self.0 .clone() .to_vec() - .map_err(|_| EncodingError::InvalidCoseEncoding) + .expect("Signature is always serializable") } } @@ -213,7 +213,7 @@ mod tests { #[test] fn test_cose_roundtrip_encode_signature() { let signature = Signature::from_cose(SIGNATURE).unwrap(); - let cose_bytes = signature.to_cose().unwrap(); + let cose_bytes = signature.to_cose(); let decoded_signature = Signature::from_cose(&cose_bytes).unwrap(); assert_eq!(signature.inner(), decoded_signature.inner()); } diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index b8aff2425..d85b0afd5 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -153,11 +153,11 @@ impl CoseSerializable for SignedObject { )) } - fn to_cose(&self) -> Result, EncodingError> { + fn to_cose(&self) -> Vec { self.0 .clone() .to_vec() - .map_err(|_| EncodingError::InvalidCoseEncoding) + .expect("SignedObject is always serializable") } } @@ -197,7 +197,7 @@ mod tests { signed_object.content_type().unwrap(), coset::iana::CoapContentFormat::Cbor ); - let cose_bytes = signed_object.to_cose().unwrap(); + let cose_bytes = signed_object.to_cose(); assert_eq!(cose_bytes, SIGNED_OBJECT); } diff --git a/crates/bitwarden-crypto/src/signing/signing_key.rs b/crates/bitwarden-crypto/src/signing/signing_key.rs index 287744e8b..475e1544d 100644 --- a/crates/bitwarden-crypto/src/signing/signing_key.rs +++ b/crates/bitwarden-crypto/src/signing/signing_key.rs @@ -88,7 +88,7 @@ impl SigningKey { impl CoseSerializable for SigningKey { /// Serializes the signing key to a COSE-formatted byte array. - fn to_cose(&self) -> Result, EncodingError> { + fn to_cose(&self) -> Vec { match &self.inner { RawSigningKey::Ed25519(key) => { coset::CoseKeyBuilder::new_okp_key() @@ -106,7 +106,7 @@ impl CoseSerializable for SigningKey { .add_key_op(KeyOperation::Verify) .build() .to_vec() - .map_err(|_| EncodingError::InvalidCoseEncoding) + .expect("Signing key is always serializable") } } } @@ -138,13 +138,10 @@ mod tests { #[test] fn test_cose_roundtrip_encode_signing() { let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519).unwrap(); - let cose = signing_key.to_cose().unwrap(); + let cose = signing_key.to_cose(); let parsed_key = SigningKey::from_cose(&cose).unwrap(); - assert_eq!( - signing_key.to_cose().unwrap(), - parsed_key.to_cose().unwrap() - ); + assert_eq!(signing_key.to_cose(), parsed_key.to_cose()); } #[test] diff --git a/crates/bitwarden-crypto/src/signing/verifying_key.rs b/crates/bitwarden-crypto/src/signing/verifying_key.rs index 53d890e43..372fa0cd7 100644 --- a/crates/bitwarden-crypto/src/signing/verifying_key.rs +++ b/crates/bitwarden-crypto/src/signing/verifying_key.rs @@ -58,7 +58,7 @@ impl VerifyingKey { } impl CoseSerializable for VerifyingKey { - fn to_cose(&self) -> Result, EncodingError> { + fn to_cose(&self) -> Vec { match &self.inner { RawVerifyingKey::Ed25519(key) => coset::CoseKeyBuilder::new_okp_key() .key_id((&self.id).into()) @@ -78,7 +78,7 @@ impl CoseSerializable for VerifyingKey { .add_key_op(KeyOperation::Verify) .build() .to_vec() - .map_err(|_| EncodingError::InvalidCoseEncoding), + .expect("Verifying key is always serializable"), } } @@ -128,13 +128,10 @@ mod tests { #[test] fn test_cose_roundtrip_encode_verifying() { let verifying_key = VerifyingKey::from_cose(VERIFYING_KEY).unwrap(); - let cose = verifying_key.to_cose().unwrap(); + let cose = verifying_key.to_cose(); let parsed_key = VerifyingKey::from_cose(&cose).unwrap(); - assert_eq!( - verifying_key.to_cose().unwrap(), - parsed_key.to_cose().unwrap() - ); + assert_eq!(verifying_key.to_cose(), parsed_key.to_cose()); } #[test] diff --git a/crates/bitwarden-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 99b77d400..df8abd262 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| { + SignedPublicKey::try_from(val).map_err(|e| { + CryptoError::EncodingError(e).into() + }) + }, + lower: |obj| obj.into(), +}); From 893624f5716b76b1fea46f2d33f3ee4e540f8f76 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 19:11:39 +0200 Subject: [PATCH 103/112] Fix build --- crates/bitwarden-core/src/mobile/crypto.rs | 2 +- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 39ce0eeea..720887954 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -592,7 +592,7 @@ pub fn make_user_signing_keys(client: &Client) -> Result::into) + Ok(verifying_key.to_cose()) } /// Returns the algorithm used for the given verifying key. From ca2ab02b970e87d65a78187c431edb73e24653d9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 19:20:17 +0200 Subject: [PATCH 104/112] Fix build --- crates/bitwarden-wasm-internal/src/pure_crypto.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 617dc6384..e0dd760db 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -570,7 +570,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= 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()).unwrap(); + PureCrypto::key_algorithm_for_verifying_key(verifying_key.to_cose()).unwrap(); assert_eq!(algorithm, SignatureAlgorithm::Ed25519); } @@ -590,8 +590,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let verifying_key_derived = VerifyingKey::from_cose(verifying_key_derived.as_slice()).unwrap(); assert_eq!( - verifying_key.to_cose().unwrap(), - verifying_key_derived.to_cose().unwrap() + verifying_key.to_cose(), + verifying_key_derived.to_cose() ); } From 072bbf6ce4e83858a5f467957df8e86ebc4e4f87 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 19:39:03 +0200 Subject: [PATCH 105/112] Expose signed public key publically --- crates/bitwarden-core/src/mobile/crypto.rs | 16 +++---- .../src/enc_string/asymmetric.rs | 3 +- crates/bitwarden-crypto/src/enc_string/mod.rs | 26 ----------- .../src/enc_string/symmetric.rs | 3 +- .../src/keys/signed_public_key.rs | 44 ++++++++++++++++++- .../src/signing/signed_object.rs | 1 + crates/bitwarden-crypto/src/util.rs | 26 ++++++++++- .../src/pure_crypto.rs | 5 +-- 8 files changed, 80 insertions(+), 44 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 720887954..7001dacb3 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, SignatureAlgorithm, SigningKey, SymmetricCryptoKey, + KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use schemars::JsonSchema; @@ -570,7 +570,7 @@ pub struct MakeUserSigningKeysResponse { /// Signing key, encrypted with a symmetric key (user key, org key) signing_key: EncString, /// The user's public key, signed by the signing key - signed_public_key: String, + signed_public_key: SignedPublicKey, } /// Makes a new set of signing keys for a user. This also signs the public key with the signing key @@ -587,19 +587,17 @@ pub fn make_user_signing_keys(client: &Client) -> Result = ctx - .make_signed_public_key( - AsymmetricKeyId::UserPrivateKey, - SigningKeyId::UserSigningKey, - )? - .into(); + 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_with_key(wrapping_key)?, - signed_public_key: STANDARD.encode(&signed_public_key), + signed_public_key, }) } diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index b047b041b..e1169411b 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -9,6 +9,7 @@ use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, rsa::encrypt_rsa2048_oaep_sha1, + util::FromStrVisitor, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, RawPrivateKey, RawPublicKey, SymmetricCryptoKey, }; @@ -141,7 +142,7 @@ impl<'de> Deserialize<'de> for UnsignedSharedKey { where D: serde::Deserializer<'de>, { - deserializer.deserialize_str(super::FromStrVisitor::new()) + deserializer.deserialize_str(FromStrVisitor::new()) } } 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 33d287e58..d9090ad04 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, }; @@ -232,7 +233,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/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index da5514b8e..7df08f32e 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -2,6 +2,8 @@ //! 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; @@ -9,8 +11,8 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::AsymmetricPublicCryptoKey; use crate::{ - cose::CoseSerializable, error::EncodingError, CryptoError, RawPublicKey, SignedObject, - SigningKey, SigningNamespace, VerifyingKey, + cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CryptoError, RawPublicKey, + SignedObject, SigningKey, SigningNamespace, VerifyingKey, }; #[cfg(feature = "wasm")] @@ -77,6 +79,7 @@ impl SignedPublicKeyMessage { /// `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 Into> for SignedPublicKey { @@ -131,6 +134,43 @@ impl SignedPublicKey { } } +impl FromStr for SignedPublicKey { + type Err = EncodingError; + + fn from_str(s: &str) -> Result { + Self::try_from(s.to_string()) + } +} + +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::*; diff --git a/crates/bitwarden-crypto/src/signing/signed_object.rs b/crates/bitwarden-crypto/src/signing/signed_object.rs index d85b0afd5..860a0751a 100644 --- a/crates/bitwarden-crypto/src/signing/signed_object.rs +++ b/crates/bitwarden-crypto/src/signing/signed_object.rs @@ -16,6 +16,7 @@ use crate::{ /// 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 { 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-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index e0dd760db..179b719d4 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -589,10 +589,7 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= .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() - ); + assert_eq!(verifying_key.to_cose(), verifying_key_derived.to_cose()); } #[test] From 68debadf5c44a3cd63a1adbdf9d5a75555c4909a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 3 Jun 2025 19:52:46 +0200 Subject: [PATCH 106/112] Clippy cleanup --- .../bitwarden-crypto/src/keys/signed_public_key.rs | 12 ++++++------ crates/bitwarden-crypto/src/signing/cose.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 7df08f32e..907672f74 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -82,9 +82,9 @@ impl SignedPublicKeyMessage { #[derive(Clone, Debug)] pub struct SignedPublicKey(pub(crate) SignedObject); -impl Into> for SignedPublicKey { - fn into(self) -> Vec { - self.0.to_cose() +impl From for Vec { + fn from(val: SignedPublicKey) -> Self { + val.0.to_cose() } } @@ -95,9 +95,9 @@ impl TryFrom> for SignedPublicKey { } } -impl Into for SignedPublicKey { - fn into(self) -> String { - let bytes: Vec = self.into(); +impl From for String { + fn from(val: SignedPublicKey) -> Self { + let bytes: Vec = val.into(); STANDARD.encode(&bytes) } } diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index d5edc9bb2..5dea9c7c4 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -141,7 +141,7 @@ fn okp_curve(cose_key: &CoseKey) -> Result { .iter() .find_map(|(key, value)| match key { Label::Int(i) if OkpKeyParameter::from_i64(*i) == Some(OkpKeyParameter::Crv) => { - value.as_integer().map(|v| i128::from(v)) + value.as_integer().map(i128::from) } _ => None, }) From 830de67723dbd196c0f008894d37acd9574b4a39 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 4 Jun 2025 11:05:43 +0200 Subject: [PATCH 107/112] Improve message.rs comment --- crates/bitwarden-crypto/src/signing/message.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/message.rs b/crates/bitwarden-crypto/src/signing/message.rs index 6a83087f6..6556a7a67 100644 --- a/crates/bitwarden-crypto/src/signing/message.rs +++ b/crates/bitwarden-crypto/src/signing/message.rs @@ -1,10 +1,18 @@ +//! 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. //! -//! Currently, only CBOR is implemented, since it is compact and is what COSE already uses. +//! 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}; From 03a67ff4c3db62e6c57982fc88af12ad4365fb83 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Jun 2025 15:50:44 +0200 Subject: [PATCH 108/112] Rename to make_user_signing_keys_for_enrollment --- crates/bitwarden-core/src/mobile/crypto.rs | 7 +++++-- crates/bitwarden-core/src/mobile/crypto_client.rs | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index 7001dacb3..0972a0950 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -573,9 +573,12 @@ pub struct MakeUserSigningKeysResponse { signed_public_key: SignedPublicKey, } -/// Makes a new set of signing keys for a user. This also signs the public key with the signing key +/// 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(client: &Client) -> Result { +pub fn make_user_signing_keys_for_enrollment( + client: &Client, +) -> Result { let key_store = client.internal.get_key_store(); let ctx = key_store.context(); diff --git a/crates/bitwarden-core/src/mobile/crypto_client.rs b/crates/bitwarden-core/src/mobile/crypto_client.rs index 69de145a7..e2a8d4da4 100644 --- a/crates/bitwarden-core/src/mobile/crypto_client.rs +++ b/crates/bitwarden-core/src/mobile/crypto_client.rs @@ -3,10 +3,10 @@ use bitwarden_crypto::CryptoError; use bitwarden_crypto::{EncString, UnsignedSharedKey}; use super::crypto::{ - derive_key_connector, make_key_pair, make_user_signing_keys, verify_asymmetric_keys, - DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, - MakeKeyPairResponse, MakeUserSigningKeysResponse, MobileCryptoError, - VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, + derive_key_connector, make_key_pair, make_user_signing_keys_for_enrollment, + verify_asymmetric_keys, DeriveKeyConnectorError, DeriveKeyConnectorRequest, + EnrollAdminPasswordResetError, MakeKeyPairResponse, MakeUserSigningKeysResponse, + MobileCryptoError, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }; #[cfg(feature = "internal")] use crate::mobile::crypto::{ @@ -104,7 +104,7 @@ impl CryptoClient { } pub fn make_signing_keys(&self) -> Result { - make_user_signing_keys(&self.client) + make_user_signing_keys_for_enrollment(&self.client) } } From 4f414fd94718911036c3d29a130ffc76ad868a49 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Jun 2025 16:02:12 +0200 Subject: [PATCH 109/112] Impl try from i128 --- crates/bitwarden-crypto/src/signing/cose.rs | 6 +---- .../bitwarden-crypto/src/signing/namespace.rs | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/cose.rs b/crates/bitwarden-crypto/src/signing/cose.rs index 5dea9c7c4..d34d359d0 100644 --- a/crates/bitwarden-crypto/src/signing/cose.rs +++ b/crates/bitwarden-crypto/src/signing/cose.rs @@ -32,11 +32,7 @@ pub(super) fn namespace( }) .ok_or(SignatureError::InvalidNamespace)?; - SigningNamespace::try_from_i64( - i128::from(namespace) - .try_into() - .map_err(|_| SignatureError::InvalidNamespace)?, - ) + SigningNamespace::try_from(i128::from(namespace)) } /// Helper function to extract the content type from a `ProtectedHeader`. The content type is a diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index dd13c3076..a44719339 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -23,15 +23,28 @@ impl SigningNamespace { pub fn as_i64(&self) -> i64 { *self as i64 } +} + +impl TryFrom for SigningNamespace { + type Error = CryptoError; - /// Converts an i64 value to a `SigningNamespace`, and fails if there is no corresponding - /// namespace for the value. - pub fn try_from_i64(value: i64) -> Result { + fn try_from(value: i64) -> Result { match value { - 1 => Ok(Self::SignedPublicKey), - -1 => Ok(Self::ExampleNamespace), - -2 => Ok(Self::ExampleNamespace2), + 1 => Ok(SigningNamespace::SignedPublicKey), + -1 => Ok(SigningNamespace::ExampleNamespace), + -2 => Ok(SigningNamespace::ExampleNamespace2), _ => Err(SignatureError::InvalidNamespace.into()), } } } + +impl TryFrom for SigningNamespace { + type Error = CryptoError; + + fn try_from(value: i128) -> Result { + if value < i64::MIN as i128 || value > i64::MAX as i128 { + return Err(SignatureError::InvalidNamespace.into()); + } + Self::try_from(value as i64) + } +} From 670610983e4d063a15eddd22f3cb72acdb1fb792 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Jun 2025 16:18:37 +0200 Subject: [PATCH 110/112] Cleanup --- crates/bitwarden-core/src/mobile/crypto.rs | 1 + crates/bitwarden-core/src/mobile/crypto_client.rs | 3 ++- crates/bitwarden-crypto/src/cose.rs | 3 +++ crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs | 1 + crates/bitwarden-crypto/src/store/context.rs | 2 +- crates/bitwarden-wasm-internal/src/crypto.rs | 4 ++-- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto.rs b/crates/bitwarden-core/src/mobile/crypto.rs index e70d99ba9..28964cbc9 100644 --- a/crates/bitwarden-core/src/mobile/crypto.rs +++ b/crates/bitwarden-core/src/mobile/crypto.rs @@ -561,6 +561,7 @@ 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))] diff --git a/crates/bitwarden-core/src/mobile/crypto_client.rs b/crates/bitwarden-core/src/mobile/crypto_client.rs index e2a8d4da4..c61e48908 100644 --- a/crates/bitwarden-core/src/mobile/crypto_client.rs +++ b/crates/bitwarden-core/src/mobile/crypto_client.rs @@ -103,7 +103,8 @@ impl CryptoClient { verify_asymmetric_keys(request) } - pub fn make_signing_keys(&self) -> Result { + /// 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) } } diff --git a/crates/bitwarden-crypto/src/cose.rs b/crates/bitwarden-crypto/src/cose.rs index 0f8de6f82..05b50ee6e 100644 --- a/crates/bitwarden-crypto/src/cose.rs +++ b/crates/bitwarden-crypto/src/cose.rs @@ -118,8 +118,11 @@ 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; diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 5ce6d998b..94c252907 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -7,6 +7,7 @@ use crate::error::{CryptoError, Result}; /// Algorithm / public key encryption scheme used for encryption/decryption. pub enum PublicKeyEncryptionAlgorithm { + /// RSA with OAEP padding and SHA-1 hashing. RsaOaepSha1, } diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index a5869030e..7d49b3932 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -265,7 +265,7 @@ impl KeyStoreContext<'_, Ids> { Ok(key_id) } - // Generate a new signature key using the current default algorithm, and store it in the context + /// 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)] diff --git a/crates/bitwarden-wasm-internal/src/crypto.rs b/crates/bitwarden-wasm-internal/src/crypto.rs index b940876d5..54ff2b07e 100644 --- a/crates/bitwarden-wasm-internal/src/crypto.rs +++ b/crates/bitwarden-wasm-internal/src/crypto.rs @@ -57,7 +57,7 @@ impl CryptoClient { /// Generates a new signing key pair and encrypts the signing key with the provided symmetric /// key. Crypto initialization not required. - pub fn make_signing_keys(&self) -> Result { - self.0.make_signing_keys() + pub fn make_signing_keys_for_enrollment(&self) -> Result { + self.0.make_user_signing_keys_for_enrollment() } } From 1bc09fb233caa97ca6940b3f4460ccf2dfbf699c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Jun 2025 16:19:54 +0200 Subject: [PATCH 111/112] Cargo fmt --- crates/bitwarden-core/src/mobile/crypto_client.rs | 4 +++- crates/bitwarden-crypto/src/store/context.rs | 3 ++- crates/bitwarden-wasm-internal/src/crypto.rs | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-core/src/mobile/crypto_client.rs b/crates/bitwarden-core/src/mobile/crypto_client.rs index c61e48908..31c6a9ac2 100644 --- a/crates/bitwarden-core/src/mobile/crypto_client.rs +++ b/crates/bitwarden-core/src/mobile/crypto_client.rs @@ -104,7 +104,9 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_user_signing_keys_for_enrollment(&self) -> Result { + pub fn make_user_signing_keys_for_enrollment( + &self, + ) -> Result { make_user_signing_keys_for_enrollment(&self.client) } } diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 7d49b3932..3b4d6d683 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -265,7 +265,8 @@ impl KeyStoreContext<'_, Ids> { Ok(key_id) } - /// Generate a new signature key using the current default algorithm, and store it in the context + /// 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)] diff --git a/crates/bitwarden-wasm-internal/src/crypto.rs b/crates/bitwarden-wasm-internal/src/crypto.rs index 54ff2b07e..ea8f11608 100644 --- a/crates/bitwarden-wasm-internal/src/crypto.rs +++ b/crates/bitwarden-wasm-internal/src/crypto.rs @@ -57,7 +57,9 @@ impl CryptoClient { /// Generates a new signing key pair and encrypts the signing key with the provided symmetric /// key. Crypto initialization not required. - pub fn make_signing_keys_for_enrollment(&self) -> Result { + pub fn make_signing_keys_for_enrollment( + &self, + ) -> Result { self.0.make_user_signing_keys_for_enrollment() } } From cedf2e41da027477a19d2f7abc7c60ef51599ded Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Jun 2025 16:48:26 +0200 Subject: [PATCH 112/112] Cleanup --- crates/bitwarden-crypto/src/signing/mod.rs | 1 + crates/bitwarden-crypto/src/store/context.rs | 4 +++- crates/bitwarden-crypto/src/traits/key_id.rs | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/signing/mod.rs b/crates/bitwarden-crypto/src/signing/mod.rs index 38c19a621..b3c5ac581 100644 --- a/crates/bitwarden-crypto/src/signing/mod.rs +++ b/crates/bitwarden-crypto/src/signing/mod.rs @@ -52,6 +52,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; #[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, } diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 3b4d6d683..6929cd494 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -252,7 +252,7 @@ impl KeyStoreContext<'_, Ids> { self.get_asymmetric_key(key_id).is_ok() } - // Returns `true` if the context has a signing key with the given identifier + /// 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() } @@ -326,6 +326,7 @@ impl KeyStoreContext<'_, Ids> { Ok(signed_public_key) } + /// Returns a signing key from the context #[deprecated(note = "This function should ideally never be used outside this crate")] pub fn dangerous_get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { self.get_signing_key(key_id) @@ -394,6 +395,7 @@ 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() { diff --git a/crates/bitwarden-crypto/src/traits/key_id.rs b/crates/bitwarden-crypto/src/traits/key_id.rs index 55298c922..f6ed82208 100644 --- a/crates/bitwarden-crypto/src/traits/key_id.rs +++ b/crates/bitwarden-crypto/src/traits/key_id.rs @@ -33,6 +33,7 @@ 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; }