diff --git a/matrix_sdk/examples/cross_signing_bootstrap.rs b/matrix_sdk/examples/cross_signing_bootstrap.rs new file mode 100644 index 00000000..b6a4e69f --- /dev/null +++ b/matrix_sdk/examples/cross_signing_bootstrap.rs @@ -0,0 +1,120 @@ +use std::{ + collections::BTreeMap, + env, io, + process::exit, + sync::atomic::{AtomicBool, Ordering}, +}; + +use serde_json::json; +use url::Url; + +use matrix_sdk::{ + self, api::r0::uiaa::AuthData, identifiers::UserId, Client, ClientConfig, LoopCtrl, + SyncSettings, +}; + +fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> { + let mut auth_parameters = BTreeMap::new(); + let identifier = json!({ + "type": "m.id.user", + "user": user, + }); + + auth_parameters.insert("identifier".to_owned(), identifier); + auth_parameters.insert("password".to_owned(), password.to_owned().into()); + + // This is needed because of https://github.com/matrix-org/synapse/issues/5665 + auth_parameters.insert("user".to_owned(), user.as_str().into()); + + AuthData::DirectRequest { + kind: "m.login.password", + auth_parameters, + session, + } +} + +async fn bootstrap(client: Client, user_id: UserId, password: String) { + println!("Bootstrapping a new cross signing identity, press enter to continue."); + + let mut input = String::new(); + + io::stdin() + .read_line(&mut input) + .expect("error: unable to read user input"); + + if let Err(e) = client.bootstrap_cross_signing(None).await { + if let Some(response) = e.uiaa_response() { + let auth_data = auth_data(&user_id, &password, response.session.as_deref()); + client + .bootstrap_cross_signing(Some(auth_data)) + .await + .expect("Couldn't bootstrap cross signing") + } else { + panic!("Error durign cross signing bootstrap {:#?}", e); + } + } +} + +async fn login( + homeserver_url: String, + username: &str, + password: &str, +) -> Result<(), matrix_sdk::Error> { + let client_config = ClientConfig::new() + .disable_ssl_verification() + .proxy("http://localhost:8080") + .unwrap(); + let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); + let client = Client::new_with_config(homeserver_url, client_config).unwrap(); + + let response = client + .login(username, password, None, Some("rust-sdk")) + .await?; + + let user_id = &response.user_id; + let client_ref = &client; + let asked = AtomicBool::new(false); + let asked_ref = &asked; + + client + .sync_with_callback(SyncSettings::new(), |_| async move { + let asked = asked_ref; + let client = &client_ref; + let user_id = &user_id; + let password = &password; + + // Wait for sync to be done then ask the user to bootstrap. + if !asked.load(Ordering::SeqCst) { + tokio::spawn(bootstrap( + (*client).clone(), + (*user_id).clone(), + password.to_string(), + )); + } + + asked.store(true, Ordering::SeqCst); + LoopCtrl::Continue + }) + .await; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), matrix_sdk::Error> { + tracing_subscriber::fmt::init(); + + let (homeserver_url, username, password) = + match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) { + (Some(a), Some(b), Some(c)) => (a, b, c), + _ => { + eprintln!( + "Usage: {} ", + env::args().next().unwrap() + ); + exit(1) + } + }; + + login(homeserver_url, &username, &password).await +} diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index c5786eff..2f592d73 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -107,7 +107,7 @@ use matrix_sdk_common::{ #[cfg(feature = "encryption")] use matrix_sdk_common::{ api::r0::{ - keys::{get_keys, upload_keys}, + keys::{get_keys, upload_keys, upload_signing_keys::Request as UploadSigningKeysRequest}, to_device::send_event_to_device::{ Request as RumaToDeviceRequest, Response as ToDeviceResponse, }, @@ -1823,6 +1823,33 @@ impl Client { })) } + /// TODO + #[cfg(feature = "encryption")] + #[cfg_attr(feature = "docs", doc(cfg(encryption)))] + pub async fn bootstrap_cross_signing(&self, auth_data: Option>) -> Result<()> { + let olm = self + .base_client + .olm_machine() + .await + .ok_or(Error::AuthenticationRequired)?; + + let (request, signature_request) = olm.bootstrap_cross_signing(false).await?; + + println!("HELLOOO MAKING REQUEST {:#?}", request); + + let request = UploadSigningKeysRequest { + auth: auth_data, + master_key: request.master_key, + self_signing_key: request.self_signing_key, + user_signing_key: request.user_signing_key, + }; + + self.send(request).await?; + self.send(signature_request).await?; + + Ok(()) + } + /// Get a map holding all the devices of an user. /// /// This will always return an empty map if the client hasn't been logged diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index 4a074fb7..10094d0d 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -20,7 +20,7 @@ js_int = "0.1.9" [dependencies.ruma] version = "0.0.1" -git = "https://github.com/ruma/ruma" +path = "/home/poljar/werk/priv/ruma/ruma" rev = "db2f58032953ccb6d8ae712d64d713ebdf412598" features = ["client-api", "unstable-pre-spec", "unstable-exhaustive-types"] diff --git a/matrix_sdk_crypto/src/error.rs b/matrix_sdk_crypto/src/error.rs index 39a554bb..1692bd54 100644 --- a/matrix_sdk_crypto/src/error.rs +++ b/matrix_sdk_crypto/src/error.rs @@ -148,6 +148,9 @@ pub enum SignatureError { #[error("the provided JSON object can't be converted to a canonical representation")] CanonicalJsonError(CjsonError), + #[error(transparent)] + JsonError(#[from] SerdeError), + #[error("the signature didn't match the provided key")] VerificationError, } diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index cb80a008..b3d5589a 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -26,6 +26,7 @@ use matrix_sdk_common::{ claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse}, get_keys::Response as KeysQueryResponse, upload_keys, + upload_signatures::Request as UploadSignaturesRequest, }, sync::sync_events::Response as SyncResponse, }, @@ -95,6 +96,7 @@ pub struct OlmMachine { /// State machine handling public user identities and devices, keeping track /// of when a key query needs to be done and handling one. identity_manager: IdentityManager, + cross_signing_request: Arc>>, } #[cfg(not(tarpaulin_include))] @@ -181,13 +183,14 @@ impl OlmMachine { user_id, device_id, account, + user_identity, store, session_manager, group_session_manager, verification_machine, key_request_machine, identity_manager, - user_identity, + cross_signing_request: Arc::new(Mutex::new(None)), } } @@ -369,12 +372,32 @@ impl OlmMachine { /// devices. /// /// Uploading these keys will require user interactive auth. - pub async fn bootstrap_cross_signing(&self) -> StoreResult { - // TODO should we save the request until we get a response? + pub async fn bootstrap_cross_signing( + &self, + reset: bool, + ) -> StoreResult<(UploadSigningKeysRequest, UploadSignaturesRequest)> { let mut identity = self.user_identity.lock().await; - *identity = PrivateCrossSigningIdentity::new(self.user_id().to_owned()).await; - self.store.save_identity(identity.clone()).await?; - Ok(identity.as_upload_request().await) + + if identity.is_empty().await || reset { + info!("Creating new cross signing identity"); + let (id, signature_request) = self.account.bootstrap_cross_signing().await; + let request = id.as_upload_request().await; + + *identity = id; + + self.store.save_identity(identity.clone()).await?; + Ok((request, signature_request)) + } else { + info!("Trying to upload the existing cross signing identity"); + let request = identity.as_upload_request().await; + let device_keys = self.account.unsigned_device_keys(); + // TODO remove this expect. + let signature_request = identity + .sign_device(device_keys) + .await + .expect("Can't sign device keys"); + Ok((request, signature_request)) + } } /// Should device or one-time keys be uploaded to the server. diff --git a/matrix_sdk_crypto/src/olm/account.rs b/matrix_sdk_crypto/src/olm/account.rs index 422d127f..49ce924f 100644 --- a/matrix_sdk_crypto/src/olm/account.rs +++ b/matrix_sdk_crypto/src/olm/account.rs @@ -30,7 +30,9 @@ use tracing::{debug, trace, warn}; #[cfg(test)] use matrix_sdk_common::events::EventType; use matrix_sdk_common::{ - api::r0::keys::{upload_keys, OneTimeKey, SignedKey}, + api::r0::keys::{ + upload_keys, upload_signatures::Request as SignatureUploadRequest, OneTimeKey, SignedKey, + }, encryption::DeviceKeys, events::{room::encrypted::EncryptedEventContent, AnyToDeviceEvent}, identifiers::{ @@ -50,13 +52,16 @@ use olm_rs::{ }; use crate::{ - error::{EventError, OlmResult, SessionCreationError}, + error::{EventError, OlmResult, SessionCreationError, SignatureError}, identities::ReadOnlyDevice, store::Store, OlmError, }; -use super::{EncryptionSettings, InboundGroupSession, OutboundGroupSession, Session}; +use super::{ + EncryptionSettings, InboundGroupSession, OutboundGroupSession, PrivateCrossSigningIdentity, + Session, +}; #[derive(Debug, Clone)] pub struct Account { @@ -618,9 +623,7 @@ impl ReadOnlyAccount { }) } - /// Sign the device keys of the account and return them so they can be - /// uploaded. - pub(crate) async fn device_keys(&self) -> DeviceKeys { + pub(crate) fn unsigned_device_keys(&self) -> DeviceKeys { let identity_keys = self.identity_keys(); let mut keys = BTreeMap::new(); @@ -634,34 +637,41 @@ impl ReadOnlyAccount { identity_keys.ed25519().to_owned(), ); - let device_keys = json!({ - "user_id": (*self.user_id).clone(), - "device_id": (*self.device_id).clone(), - "algorithms": Self::ALGORITHMS, - "keys": keys, - }); - - let mut signatures = BTreeMap::new(); - - let mut signature = BTreeMap::new(); - signature.insert( - DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &self.device_id), - self.sign_json(&device_keys).await, - ); - signatures.insert((*self.user_id).clone(), signature); - DeviceKeys::new( (*self.user_id).clone(), (*self.device_id).clone(), - vec![ - EventEncryptionAlgorithm::OlmV1Curve25519AesSha2, - EventEncryptionAlgorithm::MegolmV1AesSha2, - ], + Self::ALGORITHMS.iter().map(|a| (&**a).clone()).collect(), keys, - signatures, + BTreeMap::new(), ) } + /// Sign the device keys of the account and return them so they can be + /// uploaded. + pub(crate) async fn device_keys(&self) -> DeviceKeys { + let mut device_keys = self.unsigned_device_keys(); + let jsond_device_keys = serde_json::to_value(&device_keys).unwrap(); + + device_keys + .signatures + .entry(self.user_id().clone()) + .or_insert_with(BTreeMap::new) + .insert( + DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &self.device_id), + self.sign_json(jsond_device_keys) + .await + .expect("Can't sign own device keys"), + ); + + device_keys + } + + pub(crate) async fn bootstrap_cross_signing( + &self, + ) -> (PrivateCrossSigningIdentity, SignatureUploadRequest) { + PrivateCrossSigningIdentity::new_with_account(self).await + } + /// Convert a JSON value to the canonical representation and sign the JSON /// string. /// @@ -673,10 +683,13 @@ impl ReadOnlyAccount { /// # Panic /// /// Panics if the json value can't be serialized. - pub async fn sign_json(&self, json: &Value) -> String { - let canonical_json = cjson::to_string(json) - .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", json))); - self.sign(&canonical_json).await + pub async fn sign_json(&self, mut json: Value) -> Result { + let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?; + let _ = json_object.remove("unsigned"); + let _ = json_object.remove("signatures"); + + let canonical_json = cjson::to_string(&json)?; + Ok(self.sign(&canonical_json).await) } pub(crate) async fn signed_one_time_keys_helper( @@ -690,7 +703,10 @@ impl ReadOnlyAccount { "key": key, }); - let signature = self.sign_json(&key_json).await; + let signature = self + .sign_json(key_json) + .await + .expect("Can't sign own one-time keys"); let mut signature_map = BTreeMap::new(); diff --git a/matrix_sdk_crypto/src/olm/signing.rs b/matrix_sdk_crypto/src/olm/signing.rs index dfde208b..d4332501 100644 --- a/matrix_sdk_crypto/src/olm/signing.rs +++ b/matrix_sdk_crypto/src/olm/signing.rs @@ -20,8 +20,12 @@ use aes_gcm::{ }; use base64::{decode_config, encode_config, DecodeError, URL_SAFE_NO_PAD}; use getrandom::getrandom; +use matrix_sdk_common::{ + encryption::DeviceKeys, + identifiers::{DeviceKeyAlgorithm, DeviceKeyId}, +}; use serde::{Deserialize, Serialize}; -use serde_json::Error as JsonError; +use serde_json::{Error as JsonError, Value}; use std::{ collections::BTreeMap, sync::{ @@ -35,16 +39,22 @@ use zeroize::Zeroizing; use olm_rs::{errors::OlmUtilityError, pk::OlmPkSigning, utility::OlmUtility}; use matrix_sdk_common::{ - api::r0::keys::{CrossSigningKey, KeyUsage}, + api::r0::keys::{ + upload_signatures::Request as SignatureUploadRequest, CrossSigningKey, KeyUsage, + }, identifiers::UserId, locks::Mutex, }; use crate::{ + error::SignatureError, identities::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey}, requests::UploadSigningKeysRequest, + UserIdentity, }; +use crate::ReadOnlyAccount; + const NONCE_SIZE: usize = 12; fn encode>(input: T) -> String { @@ -163,6 +173,10 @@ impl UserSigning { PickledUserSigning { pickle, public_key } } + async fn sign_user(&self, _: &UserIdentity) -> BTreeMap> { + todo!(); + } + fn from_pickle(pickle: PickledUserSigning, pickle_key: &[u8]) -> Result { let inner = Signing::from_pickle(pickle.pickle, pickle_key)?; @@ -180,6 +194,29 @@ impl SelfSigning { PickledSelfSigning { pickle, public_key } } + async fn sign_device_raw(&self, value: Value) -> Result { + self.inner.sign_json(value).await + } + + async fn sign_device(&self, device_keys: &mut DeviceKeys) -> Result<(), SignatureError> { + let json_device = serde_json::to_value(&device_keys)?; + let signature = self.sign_device_raw(json_device).await?; + + device_keys + .signatures + .entry(self.public_key.user_id().to_owned()) + .or_insert_with(BTreeMap::new) + .insert( + DeviceKeyId::from_parts( + DeviceKeyAlgorithm::Ed25519, + self.inner.public_key.as_str().into(), + ), + signature.0, + ); + + Ok(()) + } + fn from_pickle(pickle: PickledSelfSigning, pickle_key: &[u8]) -> Result { let inner = Signing::from_pickle(pickle.pickle, pickle_key)?; @@ -346,6 +383,13 @@ impl Signing { utility.ed25519_verify(self.public_key.as_str(), message, signature.as_str()) } + async fn sign_json(&self, mut json: Value) -> Result { + let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?; + let _ = json_object.remove("signatures"); + let canonical_json = cjson::to_string(json_object)?; + Ok(self.sign(&canonical_json).await) + } + async fn sign(&self, message: &str) -> Signature { Signature(self.inner.lock().await.sign(message)) } @@ -356,6 +400,14 @@ impl PrivateCrossSigningIdentity { &self.user_id } + pub async fn is_empty(&self) -> bool { + let has_master = self.master_key.lock().await.is_some(); + let has_user = self.user_signing_key.lock().await.is_some(); + let has_self = self.self_signing_key.lock().await.is_some(); + + !(has_master && has_user && has_self) + } + pub(crate) fn empty(user_id: UserId) -> Self { Self { user_id: Arc::new(user_id), @@ -366,6 +418,94 @@ impl PrivateCrossSigningIdentity { } } + pub(crate) async fn sign_device( + &self, + mut device_keys: DeviceKeys, + ) -> Result { + self.self_signing_key + .lock() + .await + .as_ref() + .ok_or(SignatureError::MissingSigningKey)? + .sign_device(&mut device_keys) + .await?; + + let mut signed_keys = BTreeMap::new(); + signed_keys + .entry((&*self.user_id).to_owned()) + .or_insert_with(BTreeMap::new) + .insert( + device_keys.device_id.to_string(), + serde_json::to_value(device_keys)?, + ); + + Ok(SignatureUploadRequest { signed_keys }) + } + + pub(crate) async fn new_with_account( + account: &ReadOnlyAccount, + ) -> (Self, SignatureUploadRequest) { + let master = Signing::new(); + + let mut public_key = + master.cross_signing_key(account.user_id().to_owned(), KeyUsage::Master); + let signature = account + .sign_json( + serde_json::to_value(&public_key) + .expect("Can't convert own public master key to json"), + ) + .await + .expect("Can't sign own public master key"); + public_key + .signatures + .entry(account.user_id().to_owned()) + .or_insert_with(BTreeMap::new) + .insert(format!("ed25519:{}", account.device_id()), signature); + + let master = MasterSigning { + inner: master, + public_key: public_key.into(), + }; + + let identity = Self::new_helper(account.user_id(), master).await; + let device_keys = account.unsigned_device_keys(); + let request = identity + .sign_device(device_keys) + .await + .expect("Can't sign own device with new cross signign keys"); + + (identity, request) + } + + async fn new_helper(user_id: &UserId, master: MasterSigning) -> Self { + let user = Signing::new(); + let mut public_key = user.cross_signing_key(user_id.to_owned(), KeyUsage::UserSigning); + master.sign_subkey(&mut public_key).await; + + let user = UserSigning { + inner: user, + public_key: public_key.into(), + }; + + let self_signing = Signing::new(); + let mut public_key = + self_signing.cross_signing_key(user_id.to_owned(), KeyUsage::SelfSigning); + master.sign_subkey(&mut public_key).await; + + let self_signing = SelfSigning { + inner: self_signing, + public_key: public_key.into(), + }; + + Self { + user_id: Arc::new(user_id.to_owned()), + shared: Arc::new(AtomicBool::new(false)), + master_key: Arc::new(Mutex::new(Some(master))), + self_signing_key: Arc::new(Mutex::new(Some(self_signing))), + user_signing_key: Arc::new(Mutex::new(Some(user))), + } + } + pub(crate) async fn new(user_id: UserId) -> Self { let master = Signing::new(); @@ -520,6 +660,8 @@ impl PrivateCrossSigningIdentity { #[cfg(test)] mod test { + use crate::olm::ReadOnlyAccount; + use super::{PrivateCrossSigningIdentity, Signing}; use matrix_sdk_common::identifiers::{user_id, UserId}; @@ -617,4 +759,14 @@ mod test { &*unpickled.self_signing_key.lock().await ); } + + #[async_test] + async fn private_identity_signed_by_accound() { + let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into()); + let (identity, _) = PrivateCrossSigningIdentity::new_with_account(&account).await; + let master = identity.master_key.lock().await; + let master = master.as_ref().unwrap(); + + assert!(!master.public_key.signatures().is_empty()); + } }