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