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 issuer = services.oauth.get_server()?.issuer_url()?;
132	let base = issuer.trim_end_matches('/');
133	let url = format!("{base}/_tuwunel/oidc/account?action=org.matrix.cross_signing_reset");
134
135	let uiaainfo = UiaaInfo {
136		flows: vec![AuthFlow { stages: vec![AuthType::OAuth] }],
137		params: Some(to_raw_value(&json!({"m.oauth": { "url": url }}))?),
138		session: Some(session),
139		..Default::default()
140	};
141
142	services.uiaa.create(
143		sender_user,
144		body.sender_device()?,
145		&uiaainfo,
146		body.json_body
147			.as_ref()
148			.ok_or_else(|| err!(Request(NotJson("JSON body is not valid"))))?,
149	);
150
151	Ok(uiaainfo)
152}
153
154async fn check_for_new_keys(
155	services: &Services,
156	user_id: &UserId,
157	self_signing_key: Option<&Raw<CrossSigningKey>>,
158	user_signing_key: Option<&Raw<CrossSigningKey>>,
159	master_signing_key: Option<&Raw<CrossSigningKey>>,
160) -> Result<Option<upload_signing_keys::v3::Response>> {
161	debug!("checking for existing keys");
162
163	let empty = match master_signing_key {
164		| Some(new_master) => !master_key_matches(services, user_id, new_master).await?,
165		| None => false,
166	};
167
168	if let Some(new_user_signing) = user_signing_key {
169		let fetched = services.users.get_user_signing_key(user_id).await;
170
171		if fetched.is_not_found() {
172			if !empty {
173				return Err!(Request(Forbidden(
174					"Tried to update an existing user signing key, UIA required"
175				)));
176			}
177		} else if fetched?.deserialize()? != new_user_signing.deserialize()? {
178			return Err!(Request(Forbidden(
179				"Tried to change an existing user signing key, UIA required"
180			)));
181		}
182	}
183
184	if let Some(new_self_signing) = self_signing_key {
185		let fetched = services
186			.users
187			.get_self_signing_key(None, user_id, &|_| true)
188			.await;
189
190		if fetched.is_not_found() {
191			if !empty {
192				return Err!(Request(Forbidden(
193					"Tried to add a new signing key independently from the master key"
194				)));
195			}
196		} else if fetched?.deserialize()? != new_self_signing.deserialize()? {
197			return Err!(Request(Forbidden(
198				"Tried to update an existing self signing key, UIA required"
199			)));
200		}
201	}
202
203	Ok(empty
204		.is_false()
205		.into_option()
206		.map(|()| upload_signing_keys::v3::Response {}))
207}
208
209/// Returns `true` if the user already has a master key matching `new_master`,
210/// `false` if they have no master key. Returns `Err` on mismatch or any other
211/// error.
212async fn master_key_matches(
213	services: &Services,
214	user_id: &UserId,
215	new_master: &Raw<CrossSigningKey>,
216) -> Result<bool> {
217	let (new_id, new_value) = parse_master_key(user_id, new_master)?;
218	let existing = services
219		.users
220		.get_master_key(None, user_id, &|_| true)
221		.await;
222
223	if existing.is_not_found() {
224		return Ok(false);
225	}
226
227	let (existing_id, existing_value) = parse_master_key(user_id, &existing?)?;
228	if existing_id != new_id || existing_value != new_value {
229		return Err!(Request(Forbidden("Tried to change an existing master key, UIA required")));
230	}
231
232	Ok(true)
233}