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)]
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 fn public_key_string() -> Base64 {
109 Base64::new(PrivateKeyInfo::from_der(&pkcs8()).unwrap().public_key.unwrap().to_owned())
110 }
111
112 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}