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