Skip to main content

arminius_server_client/handlers/
cross_signing.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Implements APIs for device verification.
6//!
7//! Useful specs:
8//! - Device verification section: <https://spec.matrix.org/v1.15/client-server-api/#device-verification>
9//! - Cross-signing: <https://spec.matrix.org/v1.15/client-server-api/#cross-signing>
10
11#![allow(
12	clippy::allow_attributes,
13	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
14)]
15
16use std::collections::BTreeMap;
17
18use arminius_lib_authentication::SessionInfo;
19use arminius_lib_component::{
20	Component, ServerState,
21	error::{
22		BadJson, InputJsonError, InternalServerError, MatrixApiError, ResultExt as _,
23		create_custom_matrix_error,
24	},
25	ruma_requests::ArminiusResponse,
26};
27use arminius_lib_db::{Database as _, TransactedDb};
28use arminius_lib_db_devices::{Device, DeviceQueries as _};
29use arminius_lib_db_keys::UserKeysQueries as _;
30use arminius_lib_db_signatures::LocalSignatureQueries as _;
31use arminius_lib_macros::{FromDbError, FromNatsError};
32use arminius_lib_model::user_keys::DbKeyType;
33use arminius_lib_user_keys::{
34	signatures::{SignatureVerificationError, verify_signatures},
35	traits::SigningKey as _,
36};
37use axum::{Router, extract::State, http::StatusCode, routing::post};
38use ruma::{
39	UserId,
40	api::{
41		client::keys::upload_signing_keys,
42		error::{Error as MatrixError, ErrorKind},
43	},
44	encryption::{CrossSigningKey, KeyUsage},
45	serde::{Base64, base64::Standard},
46};
47
48use crate::client_request::AuthenticatedClientRequest;
49
50/// Thrown when signature verification fails.
51#[derive(Debug, thiserror::Error)]
52#[error("Invalid signature: {0}")]
53pub struct SignatureError(String);
54
55// Manually implemented, because `ErrorKind` does not agree with custom error
56// codes.
57impl MatrixApiError for SignatureError {
58	fn create_matrix_error(&self) -> MatrixError {
59		create_custom_matrix_error(
60			StatusCode::BAD_REQUEST,
61			"M_INVALID_SIGNATURE",
62			&self.to_string(),
63		)
64	}
65}
66
67/// Converts a [`SignatureVerificationError`] into a [`CrossSigningKeysError`]
68/// so that it can be returned as Matrix error.
69impl From<SignatureVerificationError> for CrossSigningKeysError {
70	fn from(signature_error: SignatureVerificationError) -> Self {
71		match signature_error {
72			SignatureVerificationError::BadJson(bad_json) => {
73				Self::InputJsonError(InputJsonError::BadJson(bad_json))
74			}
75			other_error => Self::SignatureVerification(SignatureError(other_error.to_string())),
76		}
77	}
78}
79
80#[allow(
81	missing_docs,
82	clippy::missing_docs_in_private_items,
83	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
84)]
85#[derive(Debug, thiserror::Error, MatrixApiError, FromDbError, FromNatsError)]
86enum CrossSigningKeysError {
87	#[error(transparent)]
88	InternalServerError(#[from] InternalServerError),
89
90	#[error(transparent)]
91	InputJsonError(#[from] InputJsonError),
92
93	#[error(transparent)]
94	SignatureVerification(#[from] SignatureError),
95
96	/// The public key of one of the keys is the same as one of the user's
97	/// device IDs.
98	#[error("Key ID in use")]
99	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
100	SameIdAsDevice,
101
102	/// The requested action was impossible because we do not know any master
103	/// key for that user.
104	#[error("Missing Master key")]
105	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::MissingParam)]
106	MissingMasterKey,
107}
108
109/// Performs various checks on a [`CrossSigningKey`].
110///
111/// # Errors
112///
113/// This function returns a [`InputJsonError`] error for any of the
114/// following cases:
115/// - The key is malformed in some way (e.g missing a field, does not have exactly one entry in its
116///   `keys` field, etc).
117/// - The cross-signing key's id is the same as one of the user's `device_ids` (See <https://spec.matrix.org/v1.16/client-server-api/#key-and-signature-security>).
118/// - The user ID of the key does not match that of the authenticated user making the request.
119fn check_cross_signing_key(
120	expected_user_id: &UserId,
121	expected_usage: &KeyUsage,
122	user_devices: &[Device],
123	key: &CrossSigningKey,
124) -> Result<(), CrossSigningKeysError> {
125	if key.user_id != expected_user_id {
126		return Err(InputJsonError::BadJson(BadJson(
127			"Cross-signing key's `user_id` mismatch".to_owned(),
128		))
129		.into());
130	}
131
132	if !key.usage.contains(expected_usage) {
133		return Err(InputJsonError::BadJson(BadJson(format!(
134			"Missing {expected_usage} in cross-signing key's `usage` field"
135		)))
136		.into());
137	}
138
139	// Make sure the key's ID isn't the same as one of the user's devices ID
140	let key_id = key.key_id().map_err(InputJsonError::BadJson)?;
141	if user_devices.iter().any(|device| device.device_id.as_str() == key_id.as_str()) {
142		return Err(CrossSigningKeysError::SameIdAsDevice);
143	}
144
145	Ok(())
146}
147
148/// Handles a new Master Key upload by:
149/// - Checking its correctness using [`check_cross_signing_key`]
150/// - Verify its optional device key signature
151/// - Store it in the database
152async fn handle_new_master_key(
153	transaction: &mut TransactedDb<'_>,
154	session: &SessionInfo,
155	user_devices: &[Device],
156	key: &mut CrossSigningKey,
157) -> Result<(), CrossSigningKeysError> {
158	check_cross_signing_key(&session.user_id, &KeyUsage::Master, user_devices, key)?;
159
160	// If the master key is not signed, just store it in the database now and
161	// return early
162	if key.signatures.is_empty() {
163		transaction
164			.upsert_cross_signing_key(&session.user_id, DbKeyType::Master, key)
165			.await
166			.unrecoverable_error("Inserting cross-signing key")?;
167
168		return Ok(());
169	}
170
171	// If we have device keys, try checking the master key's signature with it
172	if let Some(device_key) = transaction
173		.fetch_device_keys_for_users(&BTreeMap::from([(
174			session.user_id.clone(),
175			vec![session.device_id.clone()],
176		)]))
177		.await?
178		.get(&session.user_id)
179		.and_then(|user_keys| user_keys.get(&session.device_id))
180	{
181		verify_signatures(
182			vec![
183				&device_key
184					.key
185					.keymap()
186					.unrecoverable_error("Failed to get keymap of stored device keys")?,
187				&key.keymap().map_err(InputJsonError::BadJson)?,
188			],
189			key,
190			None,
191		)?;
192	}
193
194	let signatures = key.signatures.remove(&session.user_id);
195
196	let master_key_id = key.key_id().map_err(InputJsonError::BadJson)?;
197
198	let master_key_nid =
199		transaction.upsert_cross_signing_key(&session.user_id, DbKeyType::Master, key).await?;
200
201	if let Some(signatures) = signatures {
202		for (signing_key_id, signature) in signatures {
203			transaction
204				.upsert_local_signature(
205					&session.user_id,
206					if signing_key_id == master_key_id {
207						DbKeyType::Master
208					} else {
209						DbKeyType::Device
210					},
211					&signing_key_id,
212					master_key_nid,
213					Base64::<Standard>::parse(signature)
214						.unrecoverable_error("Failed to deserialize base64")?
215						.as_bytes(),
216				)
217				.await
218				.unrecoverable_error("Failed to insert signature")?;
219		}
220	}
221
222	Ok(())
223}
224
225/// Handler for `POST /_matrix/client/v3/keys/device_signing/upload` (See
226/// <https://spec.matrix.org/v1.15/client-server-api/#post_matrixclientv3keysdevice_signingupload>).
227#[expect(
228	clippy::too_many_lines,
229	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
230)]
231async fn upload_signing_keys_handler(
232	mut state: State<ServerState>,
233	AuthenticatedClientRequest(claims, request): AuthenticatedClientRequest<
234		upload_signing_keys::v3::Request,
235	>,
236) -> Result<ArminiusResponse<upload_signing_keys::v3::Response>, CrossSigningKeysError> {
237	let mut transaction = state.database.begin_transaction().await?;
238
239	// Uploaded cross-signing keys appear in the `device_lists.changed` field of
240	// `/sync` responses of other users, so we need to notify the sync endpoint
241	// about them after committing.
242	let uploaded_cross_signing_keys = request.master_key.is_some()
243		|| request.self_signing_key.is_some()
244		|| request.user_signing_key.is_some();
245
246	// Get the user devices to check their IDs against the cross-signing keys
247	// ones
248	let user_devices = transaction.fetch_devices_for_user(&claims.user_id).await?;
249
250	let master_key = match request.master_key {
251		Some(new_master_key) => {
252			let mut new_master_key = new_master_key.deserialize().map_err(InputJsonError::from)?;
253
254			handle_new_master_key(&mut transaction, &claims, &user_devices, &mut new_master_key)
255				.await?;
256
257			new_master_key
258		}
259		None => transaction
260			.fetch_single_cross_signing_key_for_user(&claims.user_id, DbKeyType::Master)
261			.await?
262			.ok_or(CrossSigningKeysError::MissingMasterKey)?,
263	};
264
265	// The master key **MUST** have signed the other cross-signing keys.
266	let master_key_id =
267		master_key.key_id().unrecoverable_error("Failed to get key ID from master key")?;
268	let keymap = master_key.keymap().unrecoverable_error("Failed to get keymap")?;
269	let required_signers = BTreeMap::from([(claims.user_id.clone(), vec![master_key_id.clone()])]);
270
271	if let Some(self_signing_key) = request.self_signing_key {
272		let mut self_signing_key = self_signing_key.deserialize().map_err(InputJsonError::from)?;
273
274		// Make sure the self-signing key has a valid signature from the master
275		// key.
276		check_cross_signing_key(
277			&claims.user_id,
278			&KeyUsage::SelfSigning,
279			&user_devices,
280			&self_signing_key,
281		)?;
282
283		verify_signatures(vec![&keymap], &self_signing_key, Some(&required_signers))?;
284
285		let signatures = self_signing_key
286			.signatures
287			.remove(&claims.user_id)
288			.unrecoverable_error("Self-signing keys MUST be signed at least by themselves")?;
289
290		let self_signing_key_id = self_signing_key.key_id().map_err(InputJsonError::BadJson)?;
291		let self_signing_key_nid = transaction
292			.upsert_cross_signing_key(&claims.user_id, DbKeyType::SelfSigning, &self_signing_key)
293			.await?;
294
295		for (signing_key_id, signature) in signatures {
296			// The signature is either from the key itself or from the Master
297			// key
298			let signing_key_type = match &signing_key_id {
299				key_id if *key_id == master_key_id => DbKeyType::Master,
300				key_id if key_id == &self_signing_key_id => DbKeyType::SelfSigning,
301				other_key_id => {
302					tracing::warn!("Ignoring signature on user-signing key from {other_key_id}");
303					continue;
304				}
305			};
306
307			transaction
308				.upsert_local_signature(
309					&claims.user_id,
310					signing_key_type,
311					&signing_key_id,
312					self_signing_key_nid,
313					Base64::<Standard>::parse(signature)
314						.unrecoverable_error("Failed to parse base64")?
315						.as_bytes(),
316				)
317				.await?;
318		}
319	}
320
321	if let Some(user_signing_key) = request.user_signing_key {
322		let mut user_signing_key = user_signing_key.deserialize().map_err(InputJsonError::from)?;
323
324		// Make sure the user-signing key has a valid signature from the master
325		// key.
326		check_cross_signing_key(
327			&claims.user_id,
328			&KeyUsage::UserSigning,
329			&user_devices,
330			&user_signing_key,
331		)?;
332
333		verify_signatures(vec![&keymap], &user_signing_key, Some(&required_signers))?;
334
335		let signatures = user_signing_key
336			.signatures
337			.remove(&claims.user_id)
338			.unrecoverable_error("User signing keys MUST be signed at least by the Master key")?;
339
340		let user_signing_key_id = user_signing_key.key_id().map_err(InputJsonError::BadJson)?;
341		let user_signing_key_nid = transaction
342			.upsert_cross_signing_key(&claims.user_id, DbKeyType::UserSigning, &user_signing_key)
343			.await?;
344
345		for (signing_key_id, signature) in signatures {
346			// The signature is either from the key itself or from the Master
347			// key
348			let signing_key_type = match &signing_key_id {
349				key_id if *key_id == master_key_id => DbKeyType::Master,
350				key_id if key_id == &user_signing_key_id => DbKeyType::UserSigning,
351				other_key_id => {
352					tracing::warn!("Ignoring signature on user-signing key from {other_key_id}");
353					continue;
354				}
355			};
356
357			transaction
358				.upsert_local_signature(
359					&claims.user_id,
360					signing_key_type,
361					&signing_key_id,
362					user_signing_key_nid,
363					Base64::<Standard>::parse(signature)
364						.unrecoverable_error("Failed to parse base64")?
365						.as_bytes(),
366				)
367				.await?;
368		}
369	}
370
371	transaction.commit().await?;
372
373	if uploaded_cross_signing_keys {
374		// Notify possibly listening clients that a user updated their
375		// cross-signing keys
376		state.nats_connection.notify_new_sync_data().await?;
377	}
378
379	Ok(upload_signing_keys::v3::Response::new().into())
380}
381
382/// Component implementing endpoints for user keys
383#[derive(Debug)]
384pub struct CrossSigningKeysComponent;
385
386impl Component for CrossSigningKeysComponent {
387	fn router() -> Router<ServerState> {
388		Router::new().route(
389			"/_matrix/client/v3/keys/device_signing/upload",
390			post(upload_signing_keys_handler),
391		)
392	}
393}