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,
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::{PrivateKeyInfo, 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(PrivateKeyInfo::from_der(&pkcs8()).unwrap().public_key.unwrap().to_owned())
112 }
113
114 fn test_canonical_json(input: &str) -> String {
116 let object = from_json_str(input).unwrap();
117 to_canonical_json_string_for_signing(&object).unwrap()
118 }
119
120 #[test]
121 fn canonical_json_examples() {
122 assert_eq!(&test_canonical_json("{}"), "{}");
123
124 assert_eq!(
125 &test_canonical_json(
126 r#"{
127 "one": 1,
128 "two": "Two"
129 }"#
130 ),
131 r#"{"one":1,"two":"Two"}"#
132 );
133
134 assert_eq!(
135 &test_canonical_json(
136 r#"{
137 "b": "2",
138 "a": "1"
139 }"#
140 ),
141 r#"{"a":"1","b":"2"}"#
142 );
143
144 assert_eq!(&test_canonical_json(r#"{"b":"2","a":"1"}"#), r#"{"a":"1","b":"2"}"#);
145
146 assert_eq!(
147 &test_canonical_json(
148 r#"{
149 "auth": {
150 "success": true,
151 "mxid": "@john.doe:example.com",
152 "profile": {
153 "display_name": "John Doe",
154 "three_pids": [
155 {
156 "medium": "email",
157 "address": "john.doe@example.org"
158 },
159 {
160 "medium": "msisdn",
161 "address": "123456789"
162 }
163 ]
164 }
165 }
166 }"#
167 ),
168 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}}"#
169 );
170
171 assert_eq!(
172 &test_canonical_json(
173 r#"{
174 "a": "日本語"
175 }"#
176 ),
177 r#"{"a":"日本語"}"#
178 );
179
180 assert_eq!(
181 &test_canonical_json(
182 r#"{
183 "本": 2,
184 "日": 1
185 }"#
186 ),
187 r#"{"日":1,"本":2}"#
188 );
189
190 assert_eq!(
191 &test_canonical_json(
192 r#"{
193 "a": "\u65E5"
194 }"#
195 ),
196 r#"{"a":"日"}"#
197 );
198
199 assert_eq!(
200 &test_canonical_json(
201 r#"{
202 "a": null
203 }"#
204 ),
205 r#"{"a":null}"#
206 );
207 }
208
209 #[test]
210 fn sign_empty_json() {
211 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
212
213 let mut value = from_json_str("{}").unwrap();
214
215 sign_json("domain", &key_pair, &mut value).unwrap();
216
217 assert_eq!(
218 to_json_string(&value).unwrap(),
219 r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#
220 );
221 }
222
223 #[test]
224 fn verify_empty_json() {
225 let value = from_json_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap();
226
227 let mut signature_set = BTreeMap::new();
228 signature_set.insert("ed25519:1".into(), public_key_string());
229
230 let mut public_key_map = BTreeMap::new();
231 public_key_map.insert("domain".into(), signature_set);
232
233 verify_json(&public_key_map, &value).unwrap();
234 }
235
236 #[test]
237 fn sign_minimal_json() {
238 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
239
240 let mut alpha_object = from_json_str(r#"{ "one": 1, "two": "Two" }"#).unwrap();
241 sign_json("domain", &key_pair, &mut alpha_object).unwrap();
242
243 assert_eq!(
244 to_json_string(&alpha_object).unwrap(),
245 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
246 );
247
248 let mut reverse_alpha_object =
249 from_json_str(r#"{ "two": "Two", "one": 1 }"#).expect("reverse_alpha should serialize");
250 sign_json("domain", &key_pair, &mut reverse_alpha_object).unwrap();
251
252 assert_eq!(
253 to_json_string(&reverse_alpha_object).unwrap(),
254 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
255 );
256 }
257
258 #[test]
259 fn verify_minimal_json() {
260 let value = from_json_str(
261 r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
262 ).unwrap();
263
264 let mut signature_set = BTreeMap::new();
265 signature_set.insert("ed25519:1".into(), public_key_string());
266
267 let mut public_key_map = BTreeMap::new();
268 public_key_map.insert("domain".into(), signature_set);
269
270 verify_json(&public_key_map, &value).unwrap();
271
272 let reverse_value = from_json_str(
273 r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"#
274 ).unwrap();
275
276 verify_json(&public_key_map, &reverse_value).unwrap();
277 }
278
279 #[test]
280 fn fail_verify_json() {
281 let value = from_json_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap();
282
283 let mut signature_set = BTreeMap::new();
284 signature_set.insert("ed25519:1".into(), public_key_string());
285
286 let mut public_key_map = BTreeMap::new();
287 public_key_map.insert("domain".into(), signature_set);
288
289 verify_json(&public_key_map, &value).unwrap_err();
290 }
291
292 #[test]
293 fn sign_minimal_event() {
294 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
295
296 let json = r#"{
297 "room_id": "!x:domain",
298 "sender": "@a:domain",
299 "origin": "domain",
300 "origin_server_ts": 1000000,
301 "signatures": {},
302 "hashes": {},
303 "type": "X",
304 "content": {},
305 "prev_events": [],
306 "auth_events": [],
307 "depth": 3,
308 "unsigned": {
309 "age_ts": 1000000
310 }
311 }"#;
312
313 let mut object = from_json_str(json).unwrap();
314 hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
315
316 assert_eq!(
317 to_json_string(&object).unwrap(),
318 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}}"#
319 );
320 }
321
322 #[test]
323 fn sign_redacted_event() {
324 let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
325
326 let json = r#"{
327 "content": {
328 "body": "Here is the message content"
329 },
330 "event_id": "$0:domain",
331 "origin": "domain",
332 "origin_server_ts": 1000000,
333 "type": "m.room.message",
334 "room_id": "!r:domain",
335 "sender": "@u:domain",
336 "signatures": {},
337 "unsigned": {
338 "age_ts": 1000000
339 }
340 }"#;
341
342 let mut object = from_json_str(json).unwrap();
343 hash_and_sign_event("domain", &key_pair, &mut object, &RedactionRules::V1).unwrap();
344
345 assert_eq!(
346 to_json_string(&object).unwrap(),
347 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}}"#
348 );
349 }
350
351 #[test]
352 fn verify_minimal_event() {
353 let mut signature_set = BTreeMap::new();
354 signature_set.insert("ed25519:1".into(), public_key_string());
355
356 let mut public_key_map = BTreeMap::new();
357 public_key_map.insert("domain".into(), signature_set);
358
359 let value = from_json_str(
360 r#"{
361 "auth_events": [],
362 "content": {},
363 "depth": 3,
364 "hashes": {
365 "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
366 },
367 "origin": "domain",
368 "origin_server_ts": 1000000,
369 "prev_events": [],
370 "room_id": "!x:domain",
371 "sender": "@a:domain",
372 "signatures": {
373 "domain": {
374 "ed25519:1": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"
375 }
376 },
377 "type": "X",
378 "unsigned": {
379 "age_ts": 1000000
380 }
381 }"#
382 ).unwrap();
383
384 verify_event(&public_key_map, &value, &RoomVersionRules::V5).unwrap();
385 }
386}