1#![doc(html_favicon_url = "https://ruma.dev/favicon.ico")]
2#![doc(html_logo_url = "https://ruma.dev/images/logo.png")]
3#![warn(missing_docs)]
63
64pub use ruma_common::{IdParseError, SigningKeyAlgorithm};
65
66pub use self::{
67 ed25519::{Ed25519KeyPair, Ed25519KeyPairParseError, Ed25519VerificationError},
68 error::{JsonError, VerificationError},
69 hash::{add_content_hash_to_event, content_hash, reference_hash},
70 sign::{KeyPair, Signature, hash_and_sign_event, sign_event, sign_json},
71 verify::{
72 PublicKeyMap, PublicKeySet, Verified, required_server_signatures_to_verify_event,
73 to_canonical_json_string_for_signing, verify_canonical_json_bytes, verify_event,
74 verify_json, verify_policy_server_signature,
75 },
76};
77
78mod ed25519;
79mod error;
80mod hash;
81mod sign;
82mod verify;
83
84#[cfg(test)]
85mod tests {
86 use std::collections::BTreeMap;
87
88 use pkcs8::{PrivateKeyInfoRef, der::Decode};
89 use ruma_common::{
90 room_version_rules::{RedactionRules, RoomVersionRules},
91 serde::{Base64, base64::Standard},
92 };
93 use serde_json::{from_str as from_json_str, to_string as to_json_string};
94
95 use super::{
96 Ed25519KeyPair, hash_and_sign_event, sign_json, to_canonical_json_string_for_signing,
97 verify_event, verify_json,
98 };
99
100 fn pkcs8() -> Vec<u8> {
101 const ENCODED: &str = "\
102 MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
103 tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k\
104 ";
105
106 Base64::<Standard>::parse(ENCODED).unwrap().into_inner()
107 }
108
109 fn public_key_string() -> Base64 {
111 Base64::new(
112 PrivateKeyInfoRef::from_der(&pkcs8())
113 .unwrap()
114 .public_key
115 .unwrap()
116 .raw_bytes()
117 .to_owned(),
118 )
119 }
120
121 fn test_canonical_json(input: &str) -> String {
123 let object = from_json_str(input).unwrap();
124 to_canonical_json_string_for_signing(&object).unwrap()
125 }
126
127 #[test]
128 fn canonical_json_examples() {
129 assert_eq!(&test_canonical_json("{}"), "{}");
130
131 assert_eq!(
132 &test_canonical_json(
133 r#"{
134 "one": 1,
135 "two": "Two"
136 }"#
137 ),
138 r#"{"one":1,"two":"Two"}"#
139 );
140
141 assert_eq!(
142 &test_canonical_json(
143 r#"{
144 "b": "2",
145 "a": "1"
146 }"#
147 ),
148 r#"{"a":"1","b":"2"}"#
149 );
150
151 assert_eq!(&test_canonical_json(r#"{"b":"2","a":"1"}"#), r#"{"a":"1","b":"2"}"#);
152
153 assert_eq!(
154 &test_canonical_json(
155 r#"{
156 "auth": {
157 "success": true,
158 "mxid": "@john.doe:example.com",
159 "profile": {
160 "display_name": "John Doe",
161 "three_pids": [
162 {
163 "medium": "email",
164 "address": "john.doe@example.org"
165 },
166 {
167 "medium": "msisdn",
168 "address": "123456789"
169 }
170 ]
171 }
172 }
173 }"#
174 ),
175 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}}"#
176 );
177
178 assert_eq!(
179 &test_canonical_json(
180 r#"{
181 "a": "日本語"
182 }"#
183 ),
184 r#"{"a":"日本語"}"#
185 );
186
187 assert_eq!(
188 &test_canonical_json(
189 r#"{
190 "本": 2,
191 "日": 1
192 }"#
193 ),
194 r#"{"日":1,"本":2}"#
195 );
196
197 assert_eq!(
198 &test_canonical_json(
199 r#"{
200 "a": "\u65E5"
201 }"#
202 ),
203 r#"{"a":"日"}"#
204 );
205
206 assert_eq!(
207 &test_canonical_json(
208 r#"{
209 "a": null
210 }"#
211 ),
212 r#"{"a":null}"#
213 );
214 }
215
216 #[test]
217 fn sign_empty_json() {
218 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
219
220 let mut value = from_json_str("{}").unwrap();
221
222 sign_json("domain", &key_pair, &mut value).unwrap();
223
224 assert_eq!(
225 to_json_string(&value).unwrap(),
226 r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#
227 );
228 }
229
230 #[test]
231 fn verify_empty_json() {
232 let value = from_json_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap();
233
234 let mut signature_set = BTreeMap::new();
235 signature_set.insert("ed25519:1".into(), public_key_string());
236
237 let mut public_key_map = BTreeMap::new();
238 public_key_map.insert("domain".into(), signature_set);
239
240 verify_json(&public_key_map, &value).unwrap();
241 }
242
243 #[test]
244 fn sign_minimal_json() {
245 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
246
247 let mut alpha_object = from_json_str(r#"{ "one": 1, "two": "Two" }"#).unwrap();
248 sign_json("domain", &key_pair, &mut alpha_object).unwrap();
249
250 assert_eq!(
251 to_json_string(&alpha_object).unwrap(),
252 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
253 );
254
255 let mut reverse_alpha_object =
256 from_json_str(r#"{ "two": "Two", "one": 1 }"#).expect("reverse_alpha should serialize");
257 sign_json("domain", &key_pair, &mut reverse_alpha_object).unwrap();
258
259 assert_eq!(
260 to_json_string(&reverse_alpha_object).unwrap(),
261 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
262 );
263 }
264
265 #[test]
266 fn verify_minimal_json() {
267 let value = from_json_str(
268 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
269 ).unwrap();
270
271 let mut signature_set = BTreeMap::new();
272 signature_set.insert("ed25519:1".into(), public_key_string());
273
274 let mut public_key_map = BTreeMap::new();
275 public_key_map.insert("domain".into(), signature_set);
276
277 verify_json(&public_key_map, &value).unwrap();
278
279 let reverse_value = from_json_str(
280 r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"#
281 ).unwrap();
282
283 verify_json(&public_key_map, &reverse_value).unwrap();
284 }
285
286 #[test]
287 fn fail_verify_json() {
288 let value = from_json_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap();
289
290 let mut signature_set = BTreeMap::new();
291 signature_set.insert("ed25519:1".into(), public_key_string());
292
293 let mut public_key_map = BTreeMap::new();
294 public_key_map.insert("domain".into(), signature_set);
295
296 verify_json(&public_key_map, &value).unwrap_err();
297 }
298
299 #[test]
300 fn sign_minimal_event() {
301 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
302
303 let json = r#"{
304 "room_id": "!x:domain",
305 "sender": "@a:domain",
306 "origin": "domain",
307 "origin_server_ts": 1000000,
308 "signatures": {},
309 "hashes": {},
310 "type": "X",
311 "content": {},
312 "prev_events": [],
313 "auth_events": [],
314 "depth": 3,
315 "unsigned": {
316 "age_ts": 1000000
317 }
318 }"#;
319
320 let mut object = from_json_str(json).unwrap();
321 hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
322
323 assert_eq!(
324 to_json_string(&object).unwrap(),
325 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}}"#
326 );
327 }
328
329 #[test]
330 fn sign_redacted_event() {
331 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
332
333 let json = r#"{
334 "content": {
335 "body": "Here is the message content"
336 },
337 "event_id": "$0:domain",
338 "origin": "domain",
339 "origin_server_ts": 1000000,
340 "type": "m.room.message",
341 "room_id": "!r:domain",
342 "sender": "@u:domain",
343 "signatures": {},
344 "unsigned": {
345 "age_ts": 1000000
346 }
347 }"#;
348
349 let mut object = from_json_str(json).unwrap();
350 hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
351
352 assert_eq!(
353 to_json_string(&object).unwrap(),
354 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}}"#
355 );
356 }
357
358 #[test]
359 fn verify_minimal_event() {
360 let mut signature_set = BTreeMap::new();
361 signature_set.insert("ed25519:1".into(), public_key_string());
362
363 let mut public_key_map = BTreeMap::new();
364 public_key_map.insert("domain".into(), signature_set);
365
366 let value = from_json_str(
367 r#"{
368 "auth_events": [],
369 "content": {},
370 "depth": 3,
371 "hashes": {
372 "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
373 },
374 "origin": "domain",
375 "origin_server_ts": 1000000,
376 "prev_events": [],
377 "room_id": "!x:domain",
378 "sender": "@a:domain",
379 "signatures": {
380 "domain": {
381 "ed25519:1": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"
382 }
383 },
384 "type": "X",
385 "unsigned": {
386 "age_ts": 1000000
387 }
388 }"#
389 ).unwrap();
390
391 verify_event(&public_key_map, &value, &RoomVersionRules::V5).unwrap();
392 }
393}