Skip to main content

ruma_signatures/
sign.rs

1use std::{borrow::Cow, collections::BTreeMap, mem};
2
3use ruma_common::{
4    AnyKeyName, CanonicalJsonObject, CanonicalJsonValue, OwnedSigningKeyId, SigningKeyAlgorithm,
5    canonical_json::{CanonicalJsonType, redact},
6    room_version_rules::RedactionRules,
7    serde::{Base64, base64::Standard},
8};
9use serde_json::to_string as to_json_string;
10
11use crate::{JsonError, content_hash};
12
13/// The fields to remove from a JSON object when serializing it for signing.
14pub(crate) static FIELDS_TO_REMOVE_FOR_SIGNING: &[&str] = &["signatures", "unsigned"];
15
16/// Hashes and signs an event and adds the hash and signature to objects under the keys `hashes` and
17/// `signatures`, respectively.
18///
19/// If `hashes` and/or `signatures` are already present, the new data will be appended to the
20/// existing data.
21///
22/// # Parameters
23///
24/// * `entity_id`: The identifier of the entity creating the signature. Generally this means a
25///   homeserver, e.g. "example.com".
26/// * `key_pair`: A cryptographic key pair used to sign the event.
27/// * `object`: A JSON object to be hashed and signed according to the Matrix specification.
28/// * `redaction_rules`: The redaction rules for the version of the event's room.
29///
30/// # Errors
31///
32/// Returns an error if:
33///
34/// * `object` contains a field called `content` that is not a JSON object.
35/// * `object` contains a field called `hashes` that is not a JSON object.
36/// * `object` contains a field called `signatures` that is not a JSON object.
37/// * `object` is missing the `type` field or the field is not a JSON string.
38///
39/// # Examples
40///
41/// ```rust
42/// # use ruma_common::{RoomVersionId, serde::base64::Base64};
43/// # use ruma_signatures::{hash_and_sign_event, Ed25519KeyPair};
44/// #
45/// const PKCS8: &str = "\
46///     MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
47///     tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k\
48/// ";
49///
50/// let document: Base64 = Base64::parse(PKCS8).unwrap();
51///
52/// // Create an Ed25519 key pair.
53/// let key_pair = Ed25519KeyPair::from_der(
54///     document.as_bytes(),
55///     "1".into(), // The "version" of the key.
56/// )
57/// .unwrap();
58///
59/// // Deserialize an event from JSON.
60/// let mut object = serde_json::from_str(
61///     r#"{
62///         "room_id": "!x:domain",
63///         "sender": "@a:domain",
64///         "origin": "domain",
65///         "origin_server_ts": 1000000,
66///         "signatures": {},
67///         "hashes": {},
68///         "type": "X",
69///         "content": {},
70///         "prev_events": [],
71///         "auth_events": [],
72///         "depth": 3,
73///         "unsigned": {
74///             "age_ts": 1000000
75///         }
76///     }"#,
77/// )
78/// .unwrap();
79///
80/// // Get the rules for the version of the current room.
81/// let rules =
82///     RoomVersionId::V1.rules().expect("The rules should be known for a supported room version");
83///
84/// // Hash and sign the JSON with the key pair.
85/// assert!(hash_and_sign_event("domain", &key_pair, &mut object, &rules.redaction).is_ok());
86/// ```
87///
88/// This will modify the JSON from the structure shown to a structure like this:
89///
90/// ```json
91/// {
92///     "auth_events": [],
93///     "content": {},
94///     "depth": 3,
95///     "hashes": {
96///         "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
97///     },
98///     "origin": "domain",
99///     "origin_server_ts": 1000000,
100///     "prev_events": [],
101///     "room_id": "!x:domain",
102///     "sender": "@a:domain",
103///     "signatures": {
104///         "domain": {
105///             "ed25519:1": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg"
106///         }
107///     },
108///     "type": "X",
109///     "unsigned": {
110///         "age_ts": 1000000
111///     }
112/// }
113/// ```
114///
115/// Notice the addition of `hashes` and `signatures`.
116pub fn hash_and_sign_event<K>(
117    entity_id: &str,
118    key_pair: &K,
119    object: &mut CanonicalJsonObject,
120    redaction_rules: &RedactionRules,
121) -> Result<(), JsonError>
122where
123    K: KeyPair,
124{
125    let hash = content_hash(object)?;
126
127    let hashes_value = object
128        .entry("hashes".to_owned())
129        .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
130
131    match hashes_value {
132        CanonicalJsonValue::Object(hashes) => {
133            hashes.insert("sha256".into(), CanonicalJsonValue::String(hash.encode()))
134        }
135        _ => {
136            return Err(JsonError::InvalidType {
137                path: "hashes".to_owned(),
138                expected: CanonicalJsonType::Object,
139                found: hashes_value.json_type(),
140            });
141        }
142    };
143
144    let mut redacted = redact(object.clone(), redaction_rules, None).map_err(JsonError::from)?;
145
146    sign_json(entity_id, key_pair, &mut redacted)?;
147
148    object.insert("signatures".into(), mem::take(redacted.get_mut("signatures").unwrap()));
149
150    Ok(())
151}
152
153/// Signs an arbitrary JSON object and adds the signature to an object under the key `signatures`.
154///
155/// If `signatures` is already present, the new signature will be appended to the existing ones.
156///
157/// # Parameters
158///
159/// * `entity_id`: The identifier of the entity creating the signature. Generally this means a
160///   homeserver, e.g. `example.com`.
161/// * `key_pair`: A cryptographic key pair used to sign the JSON.
162/// * `object`: A JSON object to sign according and append a signature to.
163///
164/// # Errors
165///
166/// Returns an error if:
167///
168/// * `object` contains a field called `signatures` that is not a JSON object.
169///
170/// # Examples
171///
172/// A homeserver signs JSON with a key pair:
173///
174/// ```rust
175/// # use ruma_common::serde::base64::Base64;
176/// #
177/// const PKCS8: &str = "\
178///     MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
179///     tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k\
180/// ";
181///
182/// let document: Base64 = Base64::parse(PKCS8).unwrap();
183///
184/// // Create an Ed25519 key pair.
185/// let key_pair = ruma_signatures::Ed25519KeyPair::from_der(
186///     document.as_bytes(),
187///     "1".into(), // The "version" of the key.
188/// )
189/// .unwrap();
190///
191/// // Deserialize some JSON.
192/// let mut value = serde_json::from_str("{}").unwrap();
193///
194/// // Sign the JSON with the key pair.
195/// assert!(ruma_signatures::sign_json("domain", &key_pair, &mut value).is_ok());
196/// ```
197///
198/// This will modify the JSON from an empty object to a structure like this:
199///
200/// ```json
201/// {
202///     "signatures": {
203///         "domain": {
204///             "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"
205///         }
206///     }
207/// }
208/// ```
209pub fn sign_json<K>(
210    entity_id: &str,
211    key_pair: &K,
212    object: &mut CanonicalJsonObject,
213) -> Result<(), JsonError>
214where
215    K: KeyPair,
216{
217    let (signatures_key, mut signature_map) = match object.remove_entry("signatures") {
218        Some((key, CanonicalJsonValue::Object(signatures))) => (Cow::Owned(key), signatures),
219        Some((_, value)) => {
220            return Err(JsonError::InvalidType {
221                path: "signatures".to_owned(),
222                expected: CanonicalJsonType::Object,
223                found: value.json_type(),
224            });
225        }
226        None => (Cow::Borrowed("signatures"), BTreeMap::new()),
227    };
228
229    let maybe_unsigned_entry = object.remove_entry("unsigned");
230
231    // Get the canonical JSON string.
232    let json = to_json_string(object).map_err(JsonError::Serde)?;
233
234    // Sign the canonical JSON string.
235    let signature = key_pair.sign(json.as_bytes());
236
237    // Insert the new signature in the map we pulled out (or created) previously.
238    let signature_set = signature_map
239        .entry(entity_id.to_owned())
240        .or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
241
242    let CanonicalJsonValue::Object(signature_set) = signature_set else {
243        return Err(JsonError::InvalidType {
244            path: format!("signatures.{entity_id}"),
245            expected: CanonicalJsonType::Object,
246            found: signature_set.json_type(),
247        });
248    };
249
250    signature_set.insert(signature.id(), CanonicalJsonValue::String(signature.base64()));
251
252    // Put `signatures` and `unsigned` back in.
253    object.insert(signatures_key.into(), CanonicalJsonValue::Object(signature_map));
254
255    if let Some((k, v)) = maybe_unsigned_entry {
256        object.insert(k, v);
257    }
258
259    Ok(())
260}
261
262/// A cryptographic key pair for digitally signing data.
263pub trait KeyPair: Sized {
264    /// Signs a JSON object.
265    ///
266    /// # Parameters
267    ///
268    /// * `message`: An arbitrary series of bytes to sign.
269    fn sign(&self, message: &[u8]) -> Signature;
270}
271
272/// A digital signature.
273#[derive(Clone, Debug, Eq, Hash, PartialEq)]
274pub struct Signature {
275    /// The ID of the key used to generate this signature.
276    pub(crate) key_id: OwnedSigningKeyId<AnyKeyName>,
277
278    /// The signature data.
279    pub(crate) signature: Vec<u8>,
280}
281
282impl Signature {
283    /// Creates a signature from raw bytes.
284    ///
285    /// This constructor will ensure that the key ID has the correct `algorithm:key_name` format.
286    ///
287    /// # Parameters
288    ///
289    /// * `key_id`: A key identifier, e.g. `ed25519:1`.
290    /// * `signature`: The digital signature, as a series of bytes.
291    pub fn new(key_id: OwnedSigningKeyId<AnyKeyName>, signature: Vec<u8>) -> Self {
292        Self { key_id, signature }
293    }
294
295    /// The algorithm used to generate the signature.
296    pub fn algorithm(&self) -> SigningKeyAlgorithm {
297        self.key_id.algorithm()
298    }
299
300    /// The raw bytes of the signature.
301    pub fn as_bytes(&self) -> &[u8] {
302        self.signature.as_slice()
303    }
304
305    /// A base64 encoding of the signature.
306    ///
307    /// Uses the standard character set with no padding.
308    pub fn base64(&self) -> String {
309        Base64::<Standard, _>::new(self.signature.as_slice()).encode()
310    }
311
312    /// The key identifier, a string containing the signature algorithm and the key "version"
313    /// separated by a colon, e.g. `ed25519:1`.
314    pub fn id(&self) -> String {
315        self.key_id.to_string()
316    }
317
318    /// The "version" of the key used for this signature.
319    ///
320    /// Versions are used as an identifier to distinguish signatures generated from different keys
321    /// but using the same algorithm on the same homeserver.
322    pub fn version(&self) -> &str {
323        self.key_id.key_name().as_ref()
324    }
325
326    /// Split this `Signature` into its key identifier and bytes.
327    pub fn into_parts(self) -> (OwnedSigningKeyId<AnyKeyName>, Vec<u8>) {
328        (self.key_id, self.signature)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use ruma_common::SigningKeyAlgorithm;
335
336    use super::Signature;
337
338    #[test]
339    fn valid_key_id() {
340        let signature = Signature::new("ed25519:abcdef".try_into().unwrap(), vec![]);
341        assert_eq!(signature.algorithm(), SigningKeyAlgorithm::Ed25519);
342        assert_eq!(signature.version(), "abcdef");
343    }
344
345    #[test]
346    fn unknown_key_id_algorithm() {
347        let signature = Signature::new("foobar:abcdef".try_into().unwrap(), vec![]);
348        assert_eq!(signature.algorithm().as_str(), "foobar");
349        assert_eq!(signature.version(), "abcdef");
350    }
351}