Skip to main content

tuwunel_api/client/keys/
upload_signing_keys.rs

1use axum::extract::State;
2use ruma::{
3	UserId,
4	api::client::{
5		keys::upload_signing_keys,
6		uiaa::{AuthFlow, AuthType, UiaaInfo},
7	},
8	encryption::CrossSigningKey,
9	serde::Raw,
10};
11use serde_json::{json, value::to_raw_value};
12use tuwunel_core::{
13	Err, Error, Result, debug, debug_error, err,
14	result::NotFound,
15	utils,
16	utils::{BoolExt, OptionExt},
17};
18use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH, users::parse_master_key};
19
20use crate::{Ruma, router::auth_uiaa};
21
22/// # `POST /_matrix/client/r0/keys/device_signing/upload`
23///
24/// Uploads end-to-end key information for the sender user.
25///
26/// - Requires UIAA to verify password
27/// - For OIDC devices, requires OAuth re-authentication via SSO (MSC4312)
28/// - For appservices with `device_management` enabled, UIAA is skipped even
29///   when cross-signing keys already exist (MSC4190)
30pub(crate) async fn upload_signing_keys_route(
31	State(services): State<crate::State>,
32	body: Ruma<upload_signing_keys::v3::Request>,
33) -> Result<upload_signing_keys::v3::Response> {
34	let sender_user = body.sender_user();
35
36	// Access token is required for this endpoint regardless of conditional UIAA so
37	// we'll always have a sender_user.
38	if let Ok(exists) = check_for_new_keys(
39		&services,
40		sender_user,
41		body.self_signing_key.as_ref(),
42		body.user_signing_key.as_ref(),
43		body.master_key.as_ref(),
44	)
45	.await
46	.inspect_err(|e| debug_error!(?e))
47	{
48		if let Some(result) = exists {
49			// No-op, they tried to reupload the same set of keys
50			// (lost connection for example)
51			return Ok(result);
52		}
53
54		// Some of the keys weren't found, so we let them upload
55		debug!("Skipping UIA as per MSC3967: user had no existing keys");
56		return persist_signing_keys(&services, &body).await;
57	}
58
59	// MSC4190: appservices with device_management may replace existing
60	// cross-signing keys without UIAA.
61	if body
62		.appservice_info
63		.as_ref()
64		.is_some_and(|appservice| appservice.registration.device_management)
65	{
66		debug!(
67			"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
68			 enabled"
69		);
70
71		return persist_signing_keys(&services, &body).await;
72	}
73
74	let is_oidc = body
75		.sender_device()
76		.ok()
77		.map_async(|sender_device| {
78			services
79				.users
80				.is_oidc_device(sender_user, sender_device)
81		})
82		.await
83		.unwrap_or(false);
84
85	// MSC4312: OIDC devices require OAuth re-authentication for cross-signing
86	// reset. If a bypass was granted via SSO re-auth, skip UIAA entirely.
87	if is_oidc
88		&& services
89			.users
90			.can_replace_cross_signing_keys(sender_user)
91			.await
92	{
93		return persist_signing_keys(&services, &body).await;
94	}
95
96	// First attempt from OIDC device: issue m.oauth flow.
97	if is_oidc && body.auth.is_none() {
98		return Err(Error::Uiaa(create_oauth_uiaa(&services, sender_user, &body)?));
99	}
100
101	let authed_user = auth_uiaa(&services, &body).await?;
102
103	assert_eq!(sender_user, authed_user, "Expected UIAA of {sender_user} and not {authed_user}");
104	persist_signing_keys(&services, &body).await
105}
106
107async fn persist_signing_keys(
108	services: &Services,
109	body: &Ruma<upload_signing_keys::v3::Request>,
110) -> Result<upload_signing_keys::v3::Response> {
111	services
112		.users
113		.add_cross_signing_keys(
114			body.sender_user(),
115			&body.master_key,
116			&body.self_signing_key,
117			&body.user_signing_key,
118			true, // notify so that other users see the new keys
119		)
120		.await?;
121
122	Ok(upload_signing_keys::v3::Response {})
123}
124
125fn create_oauth_uiaa(
126	services: &Services,
127	sender_user: &UserId,
128	body: &Ruma<upload_signing_keys::v3::Request>,
129) -> Result<UiaaInfo> {
130	let session = utils::random_string(SESSION_ID_LENGTH);
131	let base = services
132		.config
133		.well_known
134		.client
135		.as_ref()
136		.map(|url| url.to_string().trim_end_matches('/').to_owned())
137		.ok_or_else(|| {
138			err!(Config(
139				"well_known.client",
140				"well_known.client must be set for cross-signing reset"
141			))
142		})?;
143
144	let fallback_url =
145		format!("{base}/_matrix/client/v3/auth/m.login.sso/fallback/web?session={session}");
146
147	let uiaainfo = UiaaInfo {
148		flows: vec![AuthFlow { stages: vec![AuthType::OAuth] }],
149		params: Some(to_raw_value(&json!({"m.oauth": { "url": fallback_url }}))?),
150		session: Some(session),
151		..Default::default()
152	};
153
154	services.uiaa.create(
155		sender_user,
156		body.sender_device()?,
157		&uiaainfo,
158		body.json_body
159			.as_ref()
160			.ok_or_else(|| err!(Request(NotJson("JSON body is not valid"))))?,
161	);
162
163	Ok(uiaainfo)
164}
165
166async fn check_for_new_keys(
167	services: &Services,
168	user_id: &UserId,
169	self_signing_key: Option<&Raw<CrossSigningKey>>,
170	user_signing_key: Option<&Raw<CrossSigningKey>>,
171	master_signing_key: Option<&Raw<CrossSigningKey>>,
172) -> Result<Option<upload_signing_keys::v3::Response>> {
173	debug!("checking for existing keys");
174
175	let empty = match master_signing_key {
176		| Some(new_master) => !master_key_matches(services, user_id, new_master).await?,
177		| None => false,
178	};
179
180	if let Some(new_user_signing) = user_signing_key {
181		let fetched = services.users.get_user_signing_key(user_id).await;
182
183		if fetched.is_not_found() {
184			if !empty {
185				return Err!(Request(Forbidden(
186					"Tried to update an existing user signing key, UIA required"
187				)));
188			}
189		} else if fetched?.deserialize()? != new_user_signing.deserialize()? {
190			return Err!(Request(Forbidden(
191				"Tried to change an existing user signing key, UIA required"
192			)));
193		}
194	}
195
196	if let Some(new_self_signing) = self_signing_key {
197		let fetched = services
198			.users
199			.get_self_signing_key(None, user_id, &|_| true)
200			.await;
201
202		if fetched.is_not_found() {
203			if !empty {
204				return Err!(Request(Forbidden(
205					"Tried to add a new signing key independently from the master key"
206				)));
207			}
208		} else if fetched?.deserialize()? != new_self_signing.deserialize()? {
209			return Err!(Request(Forbidden(
210				"Tried to update an existing self signing key, UIA required"
211			)));
212		}
213	}
214
215	Ok(empty
216		.is_false()
217		.into_option()
218		.map(|()| upload_signing_keys::v3::Response {}))
219}
220
221/// Returns `true` if the user already has a master key matching `new_master`,
222/// `false` if they have no master key. Returns `Err` on mismatch or any other
223/// error.
224async fn master_key_matches(
225	services: &Services,
226	user_id: &UserId,
227	new_master: &Raw<CrossSigningKey>,
228) -> Result<bool> {
229	let (new_id, new_value) = parse_master_key(user_id, new_master)?;
230	let existing = services
231		.users
232		.get_master_key(None, user_id, &|_| true)
233		.await;
234
235	if existing.is_not_found() {
236		return Ok(false);
237	}
238
239	let (existing_id, existing_value) = parse_master_key(user_id, &existing?)?;
240	if existing_id != new_id || existing_value != new_value {
241		return Err!(Request(Forbidden("Tried to change an existing master key, UIA required")));
242	}
243
244	Ok(true)
245}