Skip to main content

ruma_signatures/
lib.rs

1#![doc(html_favicon_url = "https://ruma.dev/favicon.ico")]
2#![doc(html_logo_url = "https://ruma.dev/images/logo.png")]
3//! Digital signatures according to the [Matrix](https://matrix.org/) specification.
4//!
5//! Digital signatures are used in several places in the Matrix specification, here are a few
6//! examples:
7//!
8//! * Homeservers sign events to ensure their authenticity
9//! * Homeservers sign requests to other homeservers to prove their identity
10//! * Identity servers sign third-party invites to ensure their authenticity
11//! * Clients sign user keys to mark other users as verified
12//!
13//! Each signing key pair has an identifier, which consists of the name of the digital signature
14//! algorithm it uses and an opaque string called the "key name", separated by a colon. The key name
15//! is used to distinguish key pairs using the same algorithm from the same entity. How it is
16//! generated depends on the entity that uses it. For example, homeservers use an arbitrary
17//! string called a "version" for their public keys, while cross-signing keys use the public key
18//! encoded as unpadded base64.
19//!
20//! This library focuses on JSON objects signing. The signatures are stored within the JSON object
21//! itself under a `signatures` key. Events are also required to contain hashes of their content,
22//! which are similarly stored within the hashed JSON object under a `hashes` key.
23//!
24//! In JSON representations, both signatures and hashes appear as base64-encoded strings, usually
25//! using the standard character set, without padding.
26//!
27//! # Supported room versions
28//!
29//! Only room versions enforcing [canonical JSON] (introduced with [room version 6]) are supported.
30//!
31//! Room versions 1 through 5 are unsupported because the rules for the JSON encoding of events
32//! before signing or hashing them is unspecified. Homeservers using this crate **should not**
33//! advertise support for those room versions.
34//!
35//! # Signing and hashing
36//!
37//! To sign an arbitrary JSON object, use the [`sign_json()`] function. See the documentation of
38//! this function for more details and a full example of use.
39//!
40//! Signing an event uses a more complicated process than signing arbitrary JSON, because events can
41//! be redacted, and signatures need to remain valid even if data is removed from an event later.
42//! Homeservers are required to generate hashes of event contents as well as signing events before
43//! exchanging them with other homeservers. Although the algorithm for hashing and signing an event
44//! is more complicated than for signing arbitrary JSON, the interface to a user of ruma-signatures
45//! is the same. To hash and sign an event, use the [`hash_and_sign_event()`] function. See the
46//! documentation of this function for more details and a full example of use.
47//!
48//! # Verifying signatures and hashes
49//!
50//! When a homeserver receives data from another homeserver via the federation, it's necessary to
51//! verify the authenticity and integrity of the data by verifying their signatures.
52//!
53//! To verify a signature on arbitrary JSON, use the [`verify_json()`] function. To verify the
54//! signatures and hashes on an event, use the [`verify_event()`] function. See the documentation
55//! for these respective functions for more details and full examples of use.
56//!
57//! [canonical JSON]: https://spec.matrix.org/v1.18/appendices/#canonical-json
58//! [room version 6]: https://spec.matrix.org/v1.18/rooms/v6/
59
60#![warn(missing_docs)]
61
62pub use ruma_common::{IdParseError, SigningKeyAlgorithm};
63
64pub use self::{
65    ed25519::{Ed25519KeyPair, Ed25519KeyPairParseError, Ed25519VerificationError},
66    error::{JsonError, VerificationError},
67    hash::{content_hash, reference_hash},
68    sign::{KeyPair, Signature, hash_and_sign_event, sign_json},
69    verify::{
70        PublicKeyMap, PublicKeySet, Verified, required_server_signatures_to_verify_event,
71        to_canonical_json_string_for_signing, verify_canonical_json_bytes, verify_event,
72        verify_json,
73    },
74};
75
76mod ed25519;
77mod error;
78mod hash;
79mod sign;
80mod verify;
81
82#[cfg(test)]
83mod tests {
84    use std::collections::BTreeMap;
85
86    use pkcs8::{PrivateKeyInfo, der::Decode};
87    use ruma_common::{
88        room_version_rules::{RedactionRules, RoomVersionRules},
89        serde::{Base64, base64::Standard},
90    };
91    use serde_json::{from_str as from_json_str, to_string as to_json_string};
92
93    use super::{
94        Ed25519KeyPair, hash_and_sign_event, sign_json, to_canonical_json_string_for_signing,
95        verify_event, verify_json,
96    };
97
98    fn pkcs8() -> Vec<u8> {
99        const ENCODED: &str = "\
100            MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
101            tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k\
102        ";
103
104        Base64::<Standard>::parse(ENCODED).unwrap().into_inner()
105    }
106
107    /// Convenience method for getting the public key as a string
108    fn public_key_string() -> Base64 {
109        Base64::new(PrivateKeyInfo::from_der(&pkcs8()).unwrap().public_key.unwrap().to_owned())
110    }
111
112    /// Convenience for converting a string of JSON into its canonical form.
113    fn test_canonical_json(input: &str) -> String {
114        let object = from_json_str(input).unwrap();
115        to_canonical_json_string_for_signing(&object).unwrap()
116    }
117
118    #[test]
119    fn canonical_json_examples() {
120        assert_eq!(&test_canonical_json("{}"), "{}");
121
122        assert_eq!(
123            &test_canonical_json(
124                r#"{
125                    "one": 1,
126                    "two": "Two"
127                }"#
128            ),
129            r#"{"one":1,"two":"Two"}"#
130        );
131
132        assert_eq!(
133            &test_canonical_json(
134                r#"{
135                    "b": "2",
136                    "a": "1"
137                }"#
138            ),
139            r#"{"a":"1","b":"2"}"#
140        );
141
142        assert_eq!(&test_canonical_json(r#"{"b":"2","a":"1"}"#), r#"{"a":"1","b":"2"}"#);
143
144        assert_eq!(
145            &test_canonical_json(
146                r#"{
147                    "auth": {
148                        "success": true,
149                        "mxid": "@john.doe:example.com",
150                        "profile": {
151                            "display_name": "John Doe",
152                            "three_pids": [
153                                {
154                                    "medium": "email",
155                                    "address": "john.doe@example.org"
156                                },
157                                {
158                                    "medium": "msisdn",
159                                    "address": "123456789"
160                                }
161                            ]
162                        }
163                    }
164                }"#
165            ),
166            r#"{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}"#
167        );
168
169        assert_eq!(
170            &test_canonical_json(
171                r#"{
172                    "a": "日本語"
173                }"#
174            ),
175            r#"{"a":"日本語"}"#
176        );
177
178        assert_eq!(
179            &test_canonical_json(
180                r#"{
181                    "本": 2,
182                    "日": 1
183                }"#
184            ),
185            r#"{"日":1,"本":2}"#
186        );
187
188        assert_eq!(
189            &test_canonical_json(
190                r#"{
191                    "a": "\u65E5"
192                }"#
193            ),
194            r#"{"a":"日"}"#
195        );
196
197        assert_eq!(
198            &test_canonical_json(
199                r#"{
200                "a": null
201            }"#
202            ),
203            r#"{"a":null}"#
204        );
205    }
206
207    #[test]
208    fn sign_empty_json() {
209        let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
210
211        let mut value = from_json_str("{}").unwrap();
212
213        sign_json("domain", &key_pair, &mut value).unwrap();
214
215        assert_eq!(
216            to_json_string(&value).unwrap(),
217            r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#
218        );
219    }
220
221    #[test]
222    fn verify_empty_json() {
223        let value = from_json_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap();
224
225        let mut signature_set = BTreeMap::new();
226        signature_set.insert("ed25519:1".into(), public_key_string());
227
228        let mut public_key_map = BTreeMap::new();
229        public_key_map.insert("domain".into(), signature_set);
230
231        verify_json(&public_key_map, &value).unwrap();
232    }
233
234    #[test]
235    fn sign_minimal_json() {
236        let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
237
238        let mut alpha_object = from_json_str(r#"{ "one": 1, "two": "Two" }"#).unwrap();
239        sign_json("domain", &key_pair, &mut alpha_object).unwrap();
240
241        assert_eq!(
242            to_json_string(&alpha_object).unwrap(),
243            r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
244        );
245
246        let mut reverse_alpha_object =
247            from_json_str(r#"{ "two": "Two", "one": 1 }"#).expect("reverse_alpha should serialize");
248        sign_json("domain", &key_pair, &mut reverse_alpha_object).unwrap();
249
250        assert_eq!(
251            to_json_string(&reverse_alpha_object).unwrap(),
252            r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
253        );
254    }
255
256    #[test]
257    fn verify_minimal_json() {
258        let value = from_json_str(
259            r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
260        ).unwrap();
261
262        let mut signature_set = BTreeMap::new();
263        signature_set.insert("ed25519:1".into(), public_key_string());
264
265        let mut public_key_map = BTreeMap::new();
266        public_key_map.insert("domain".into(), signature_set);
267
268        verify_json(&public_key_map, &value).unwrap();
269
270        let reverse_value = from_json_str(
271            r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"#
272        ).unwrap();
273
274        verify_json(&public_key_map, &reverse_value).unwrap();
275    }
276
277    #[test]
278    fn fail_verify_json() {
279        let value = from_json_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap();
280
281        let mut signature_set = BTreeMap::new();
282        signature_set.insert("ed25519:1".into(), public_key_string());
283
284        let mut public_key_map = BTreeMap::new();
285        public_key_map.insert("domain".into(), signature_set);
286
287        verify_json(&public_key_map, &value).unwrap_err();
288    }
289
290    #[test]
291    fn sign_minimal_event() {
292        let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
293
294        let json = r#"{
295            "room_id": "!x:domain",
296            "sender": "@a:domain",
297            "origin": "domain",
298            "origin_server_ts": 1000000,
299            "signatures": {},
300            "hashes": {},
301            "type": "X",
302            "content": {},
303            "prev_events": [],
304            "auth_events": [],
305            "depth": 3,
306            "unsigned": {
307                "age_ts": 1000000
308            }
309        }"#;
310
311        let mut object = from_json_str(json).unwrap();
312        hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
313
314        assert_eq!(
315            to_json_string(&object).unwrap(),
316            r#"{"auth_events":[],"content":{},"depth":3,"hashes":{"sha256":"5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"},"origin":"domain","origin_server_ts":1000000,"prev_events":[],"room_id":"!x:domain","sender":"@a:domain","signatures":{"domain":{"ed25519:1":"PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"}},"type":"X","unsigned":{"age_ts":1000000}}"#
317        );
318    }
319
320    #[test]
321    fn sign_redacted_event() {
322        let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
323
324        let json = r#"{
325            "content": {
326                "body": "Here is the message content"
327            },
328            "event_id": "$0:domain",
329            "origin": "domain",
330            "origin_server_ts": 1000000,
331            "type": "m.room.message",
332            "room_id": "!r:domain",
333            "sender": "@u:domain",
334            "signatures": {},
335            "unsigned": {
336                "age_ts": 1000000
337            }
338        }"#;
339
340        let mut object = from_json_str(json).unwrap();
341        hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
342
343        assert_eq!(
344            to_json_string(&object).unwrap(),
345            r#"{"content":{"body":"Here is the message content"},"event_id":"$0:domain","hashes":{"sha256":"onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g"},"origin":"domain","origin_server_ts":1000000,"room_id":"!r:domain","sender":"@u:domain","signatures":{"domain":{"ed25519:1":"D2V+qWBJssVuK/pEUJtwaYMdww2q1fP4PRCo226ChlLz8u8AWmQdLKes19NMjs/X0Hv0HIjU0c1TDKFMtGuoCA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"#
346        );
347    }
348
349    #[test]
350    fn verify_minimal_event() {
351        let mut signature_set = BTreeMap::new();
352        signature_set.insert("ed25519:1".into(), public_key_string());
353
354        let mut public_key_map = BTreeMap::new();
355        public_key_map.insert("domain".into(), signature_set);
356
357        let value = from_json_str(
358            r#"{
359                "auth_events": [],
360                "content": {},
361                "depth": 3,
362                "hashes": {
363                    "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
364                },
365                "origin": "domain",
366                "origin_server_ts": 1000000,
367                "prev_events": [],
368                "room_id": "!x:domain",
369                "sender": "@a:domain",
370                "signatures": {
371                    "domain": {
372                        "ed25519:1": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"
373                    }
374                },
375                "type": "X",
376                "unsigned": {
377                    "age_ts": 1000000
378                }
379            }"#
380        ).unwrap();
381
382        verify_event(&public_key_map, &value, &RoomVersionRules::V5).unwrap();
383    }
384}