crypto: WIP cross signing bootstrap.

This commit is contained in:
Damir Jelić 2020-10-24 10:32:17 +02:00
parent 8ed1e37cef
commit 5c14910126
7 changed files with 383 additions and 42 deletions

View file

@ -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: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login(homeserver_url, &username, &password).await
}

View file

@ -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<AuthData<'_>>) -> 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

View file

@ -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"]

View file

@ -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,
}

View file

@ -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<Mutex<Option<UploadSignaturesRequest>>>,
}
#[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<UploadSigningKeysRequest> {
// 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.

View file

@ -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<String, SignatureError> {
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();

View file

@ -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<T: AsRef<[u8]>>(input: T) -> String {
@ -163,6 +173,10 @@ impl UserSigning {
PickledUserSigning { pickle, public_key }
}
async fn sign_user(&self, _: &UserIdentity) -> BTreeMap<UserId, BTreeMap<String, Value>> {
todo!();
}
fn from_pickle(pickle: PickledUserSigning, pickle_key: &[u8]) -> Result<Self, SigningError> {
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<Signature, SignatureError> {
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<Self, SigningError> {
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<Signature, SignatureError> {
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<SignatureUploadRequest, SignatureError> {
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());
}
}