Skip to main content

ruma_signatures/
sign.rs

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