Skip to main content

arminius_lib_user_keys/
traits.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! This module contains two traits:
6//! - [`SigningKey`]: Any type implementing this trait can be used to verify a [`SignedObject`]'s
7//!   signature.
8//! - [`SignedObject`]: Any type implementing this trait can contain signatures which can be
9//!   verified using a [`SigningKey`].
10
11use std::collections::BTreeMap;
12
13use arminius_lib_component::error::BadJson;
14use itertools::Itertools as _;
15use ruma::{
16	Base64PublicKeyOrDeviceId, CrossSigningOrDeviceSignatures, DeviceKeyAlgorithm, KeyId, KeyName,
17	OwnedBase64PublicKeyOrDeviceId, OwnedSigningKeyId, OwnedUserId, Signatures,
18	SigningKeyAlgorithm,
19	canonical_json::to_canonical_value,
20	encryption::{CrossSigningKey, DeviceKeys},
21	serde::{Base64, base64::Standard},
22	signatures::{PublicKeyMap, PublicKeySet},
23};
24use serde::Serialize;
25
26/// This trait provides helper functions on any signing key.
27pub trait SigningKey<K: KeyName + ?Sized>: Serialize {
28	/// Function that extracts the [`PublicKeyMap`] from `Self`.
29	fn keymap(&self) -> Result<PublicKeyMap, BadJson>;
30
31	/// Function that returns `Self`'s key ID used in signing an object.
32	///
33	/// There is no single implementation because different keys use different
34	/// key ID formats.
35	fn key_id(&self) -> Result<OwnedSigningKeyId<K>, BadJson>;
36
37	/// This converts the key in `self` into its canonical string, but
38	/// **stripped** off of its `signatures` and `unsigned` fields. This is
39	/// useful to compare two objects that were signed and tell if they were
40	/// the same, or in tests to prepare a key for being signed.
41	///
42	/// # Error
43	///
44	/// The function will error out a [`BadJson`] in the
45	/// following cases:
46	/// - Calling [`serde_json::to_value`] on `self` fails (See <https://docs.rs/serde_json/latest/serde_json/fn.to_value.html#errors>).
47	/// - The canonicalization of the object fails (See <https://docs.rs/ruma/latest/ruma/enum.CanonicalJsonError.html>).
48	fn stripped_canonical_string(&self) -> Result<String, BadJson> {
49		let mut as_json = serde_json::to_value(self)
50			.map_err(|_| BadJson("Failed to convert to JSON".to_owned()))?;
51
52		let as_obj = as_json.as_object_mut().ok_or_else(|| {
53			BadJson("Serializable value should have contained a JSON object".to_owned())
54		})?;
55		as_obj.remove("signatures");
56		as_obj.remove("unsigned");
57
58		Ok(to_canonical_value(&as_obj)
59			.map_err(|_| BadJson("Failed to convert to canonical JSON".to_owned()))?
60			.to_string())
61	}
62
63	/// Compares the key in `self` with another one, using their stripped
64	/// canonical strings (See [`Self::stripped_canonical_string`]).
65	///
66	/// This is for example called on every signature upload to compare the
67	/// uploaded signed key with the corresponding stored one.
68	fn compare(&self, other: &Self) -> Result<bool, BadJson> {
69		Ok(self.stripped_canonical_string()? == other.stripped_canonical_string()?)
70	}
71}
72
73impl SigningKey<Base64PublicKeyOrDeviceId> for CrossSigningKey {
74	fn keymap(&self) -> Result<PublicKeyMap, BadJson> {
75		let keyset: PublicKeySet = self
76			.keys
77			.iter()
78			.map(|(key_id, key)| -> Result<(_, _), BadJson> {
79				Ok((
80					key_id.to_string(),
81					Base64::<Standard>::parse(key).map_err(|_| {
82						BadJson(
83							"Field `keys` of cross-signing key contains invalid base64 value"
84								.to_owned(),
85						)
86					})?,
87				))
88			})
89			.collect::<Result<_, BadJson>>()?;
90
91		let mut keymap = PublicKeyMap::new();
92		keymap.insert(self.user_id.to_string(), keyset);
93
94		Ok(keymap)
95	}
96
97	fn key_id(&self) -> Result<OwnedSigningKeyId<Base64PublicKeyOrDeviceId>, BadJson> {
98		let Ok(key_id) = self.keys.keys().exactly_one() else {
99			return Err(BadJson("Cross-signing keys should contain exactly one key".to_owned()));
100		};
101
102		Ok(KeyId::from_parts(
103			key_id.algorithm(),
104			&OwnedBase64PublicKeyOrDeviceId::from(key_id.key_name().to_owned()),
105		))
106	}
107}
108
109impl SigningKey<Base64PublicKeyOrDeviceId> for DeviceKeys {
110	fn keymap(&self) -> Result<PublicKeyMap, BadJson> {
111		let (key_id, b64_key) = self
112			.keys
113			.iter()
114			.find_map(|(key_id, key)| {
115				(key_id.algorithm() == DeviceKeyAlgorithm::Ed25519).then_some((key_id, key))
116			})
117			.ok_or_else(|| BadJson("Device keys should contain a `ed25519` key".to_owned()))?;
118
119		let keyset = BTreeMap::from([(
120			key_id.to_string(),
121			Base64::<Standard>::parse(b64_key).map_err(|_| {
122				BadJson("Field `keys` of device keys contains invalid base64 value".to_owned())
123			})?,
124		)]);
125
126		let mut keymap = PublicKeyMap::new();
127		keymap.insert(self.user_id.to_string(), keyset);
128
129		Ok(keymap)
130	}
131
132	fn key_id(&self) -> Result<OwnedSigningKeyId<Base64PublicKeyOrDeviceId>, BadJson> {
133		let key_name: OwnedBase64PublicKeyOrDeviceId = self.device_id.clone().into();
134		Ok(KeyId::from_parts(SigningKeyAlgorithm::Ed25519, &key_name))
135	}
136}
137
138/// Trait to be implemented on any signed key that you want to verify signatures
139/// on.
140pub trait SignedObject<E: Ord, K: KeyName + ?Sized>: Serialize + std::fmt::Debug {
141	/// Returns the signatures of the signed key. This is a map from User ID to
142	/// Key ID to signature.
143	fn signatures(&self) -> Signatures<E, K>;
144}
145
146impl SignedObject<OwnedUserId, Base64PublicKeyOrDeviceId> for DeviceKeys {
147	fn signatures(&self) -> CrossSigningOrDeviceSignatures {
148		self.signatures.clone()
149	}
150}
151
152impl SignedObject<OwnedUserId, Base64PublicKeyOrDeviceId> for CrossSigningKey {
153	fn signatures(&self) -> CrossSigningOrDeviceSignatures {
154		self.signatures.clone()
155	}
156}
157
158impl SignedObject<OwnedUserId, Base64PublicKeyOrDeviceId> for ruma::encryption::SignedKey {
159	fn signatures(&self) -> CrossSigningOrDeviceSignatures {
160		self.signatures
161			.clone()
162			.into_iter()
163			.map(|(entity, key_id, signature)| {
164				(entity, KeyId::from_parts(key_id.algorithm(), key_id.key_name().into()), signature)
165			})
166			.collect()
167	}
168}