arminius_lib_user_keys/signatures.rs
1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Contains the logic to verify signatures of signed objects.
6
7use std::collections::BTreeMap;
8
9use arminius_lib_component::error::BadJson;
10use ruma::{
11 KeyName, OwnedSigningKeyId,
12 canonical_json::to_canonical_value,
13 signatures::{PublicKeyMap, verify_json},
14};
15
16use crate::traits::SignedObject;
17
18/// The various possible errors that can occur when verifying signatures.
19#[derive(Debug, thiserror::Error)]
20pub enum SignatureVerificationError {
21 /// Error thrown if a JSON is malformed or missing required fields.
22 #[error(transparent)]
23 BadJson(#[from] BadJson),
24
25 /// Error thrown if a signature is cryptographically wrong.
26 #[error(transparent)]
27 InvalidSignature(#[from] ruma::signatures::VerificationError),
28
29 /// Error thrown if an object is missing a required entity's key signature.
30 // FIXME(#1692): We could add the missing key ID in the error to hint which key
31 // should
32 #[error("Missing required signature")]
33 MissingRequiredSignature,
34}
35
36/// This function adds the content of the `other` public keymap into the first
37/// `keymap`. Since a keymap is a nested `BTreeMap`, one can't simply call
38/// `keymap.extend(other)` unfortunately.
39pub fn extend_keymap(keymap: &mut PublicKeyMap, other: PublicKeyMap) {
40 for (entity, keyset) in other {
41 let entry = keymap.entry(entity).or_default();
42 entry.extend(keyset);
43 }
44}
45
46/// This is a helper type when calling [`verify_signatures()`].
47///
48/// # Example
49///
50/// The following example code shows how one could enforce/check that some
51/// device keys are signed by a self-signing key.
52/// ```rust
53/// # use arminius_lib_testing::utils::keys_helpers::{
54/// # generate_test_cross_signing_key, generate_test_device_keys, generate_test_key_pair,
55/// # sign_object,
56/// # };
57/// # use arminius_lib_user_keys::signatures::verify_signatures;
58/// # use arminius_lib_user_keys::traits::SigningKey as _;
59/// # use ruma::{encryption::KeyUsage, owned_device_id, owned_user_id};
60/// # use std::collections::BTreeMap;
61/// let signer_user_id = owned_user_id!("@nina:example.org");
62/// let self_signing_key_pair = generate_test_key_pair();
63///
64/// // Generate a self-signing key, signed by itself.
65/// let mut self_signing_key = generate_test_cross_signing_key(
66/// &signer_user_id,
67/// KeyUsage::SelfSigning,
68/// Some(&self_signing_key_pair),
69/// );
70/// let signing_key_id = self_signing_key.key_id().expect("Failed to extract key id");
71/// sign_object(
72/// &mut self_signing_key,
73/// signer_user_id.as_str(),
74/// &signing_key_id.to_string(),
75/// &self_signing_key_pair,
76/// );
77///
78/// // Generate a device, and additionally sign it with the self-signing key.
79/// let mut device_keys =
80/// generate_test_device_keys(&signer_user_id, &owned_device_id!("syJo6qIkff"), None);
81/// sign_object(
82/// &mut device_keys,
83/// signer_user_id.as_str(),
84/// &signing_key_id.to_string(),
85/// &self_signing_key_pair,
86/// );
87///
88/// // First, extract a PublicKeyMap from the self-signing key.
89/// let keymap = self_signing_key.keymap().expect("Failed to extract keymap");
90///
91/// // Then, configure the required signers to contain that key.
92/// let required_signers = BTreeMap::from([(signer_user_id, vec![signing_key_id])]);
93///
94/// // And finally, call the verification function
95/// assert!(verify_signatures(vec![&keymap], &device_keys,
96/// Some(&required_signers)).is_ok());
97/// ```
98pub type RequiredSigners<E, K> = BTreeMap<E, Vec<OwnedSigningKeyId<K>>>;
99
100/// Main entry point for verifying signatures.
101///
102/// Given a list of [PublicKeyMap] and a `signed_object`, this function verifies
103/// that all the given `required_signers` have their entries in the signed
104/// object signature, and then if the signatures are correct.
105pub fn verify_signatures<E: Ord, K: KeyName + ?Sized>(
106 keymaps: Vec<&PublicKeyMap>,
107 signed_object: &impl SignedObject<E, K>,
108 required_signers: Option<&RequiredSigners<E, K>>,
109) -> Result<(), SignatureVerificationError> {
110 // First check that every required signer is present in the signed object's
111 // signatures
112 if let Some(required_signers) = required_signers {
113 for (required_entity, required_key_ids) in required_signers {
114 let signatures_keys_ids: Vec<OwnedSigningKeyId<K>> = signed_object
115 .signatures()
116 .remove(required_entity)
117 .ok_or(SignatureVerificationError::MissingRequiredSignature)?
118 .into_keys()
119 .collect();
120
121 if required_key_ids
122 .iter()
123 .any(|required_key_id| !signatures_keys_ids.contains(required_key_id))
124 {
125 return Err(SignatureVerificationError::MissingRequiredSignature);
126 }
127 }
128 }
129
130 // Then check that the object is correctly signed.
131 let canonical_value = to_canonical_value(signed_object)
132 .map_err(|_| BadJson("Failed to convert to canonical JSON".to_owned()))?;
133 let canonical_object = canonical_value
134 .as_object()
135 .ok_or_else(|| BadJson("Can only verify signatures of JSON objects".to_owned()))?;
136
137 // Combine all the given keymaps into a single one
138 let keymap = keymaps.into_iter().fold(PublicKeyMap::new(), |mut acc, keymap| {
139 extend_keymap(&mut acc, keymap.clone());
140 acc
141 });
142
143 // The behavior of this function needs to be documented, and we should try
144 // to reach a stage where this is the single place in our entire codebase
145 // where it gets called.
146 //
147 // If a signature is present in the signed object but not in the keymap, it
148 // will fail. If a key is in the keymap but no corresponding signature is
149 // in the signed object, it will not fail.
150 verify_json(&keymap, canonical_object)?;
151
152 Ok(())
153}