#![doc(html_favicon_url = "https://ruma.dev/favicon.ico")]
#![doc(html_logo_url = "https://ruma.dev/images/logo.png")]
#![warn(missing_docs)]
use ruma_common::serde::{AsRefStr, DisplayAsRefStr};
pub use self::{
error::{Error, JsonError, ParseError, VerificationError},
functions::{
canonical_json, content_hash, hash_and_sign_event, reference_hash, sign_json, verify_event,
verify_json,
},
keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet},
signatures::Signature,
verification::Verified,
};
mod error;
mod functions;
mod keys;
mod signatures;
mod verification;
#[derive(Clone, Debug, Eq, Hash, PartialEq, AsRefStr, DisplayAsRefStr)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_enum(rename_all = "snake_case")]
pub enum Algorithm {
Ed25519,
}
fn split_id(id: &str) -> Result<(Algorithm, String), Error> {
const SIGNATURE_ID_LENGTH: usize = 2;
let signature_id: Vec<&str> = id.split(':').collect();
let signature_id_length = signature_id.len();
if signature_id_length != SIGNATURE_ID_LENGTH {
return Err(Error::InvalidLength(signature_id_length));
}
let version = signature_id[1];
#[cfg(feature = "compat-signature-id")]
const EXTRA_ALLOWED: [u8; 3] = [b'_', b'+', b'/'];
#[cfg(not(feature = "compat-signature-id"))]
const EXTRA_ALLOWED: [u8; 1] = [b'_'];
if !version.bytes().all(|ch| ch.is_ascii_alphanumeric() || EXTRA_ALLOWED.contains(&ch)) {
return Err(Error::InvalidVersion(version.into()));
}
let algorithm_input = signature_id[0];
let algorithm = match algorithm_input {
"ed25519" => Algorithm::Ed25519,
algorithm => return Err(Error::UnsupportedAlgorithm(algorithm.into())),
};
Ok((algorithm, signature_id[1].to_owned()))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use pkcs8::{der::Decode, PrivateKeyInfo};
use ruma_common::{
serde::{base64::Standard, Base64},
RoomVersionId,
};
use serde_json::{from_str as from_json_str, to_string as to_json_string};
use super::{
canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair,
};
fn pkcs8() -> Vec<u8> {
const ENCODED: &str = "\
MFECAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/\
tA0T+6tgSEA3TPraTczVkDPTRaX4K+AfUuyx7Mzq1UafTXypnl0t2k\
";
Base64::<Standard>::parse(ENCODED).unwrap().into_inner()
}
fn public_key_string() -> Base64 {
Base64::new(PrivateKeyInfo::from_der(&pkcs8()).unwrap().public_key.unwrap().to_owned())
}
fn test_canonical_json(input: &str) -> String {
let object = from_json_str(input).unwrap();
canonical_json(&object).unwrap()
}
#[test]
fn canonical_json_examples() {
assert_eq!(&test_canonical_json("{}"), "{}");
assert_eq!(
&test_canonical_json(
r#"{
"one": 1,
"two": "Two"
}"#
),
r#"{"one":1,"two":"Two"}"#
);
assert_eq!(
&test_canonical_json(
r#"{
"b": "2",
"a": "1"
}"#
),
r#"{"a":"1","b":"2"}"#
);
assert_eq!(&test_canonical_json(r#"{"b":"2","a":"1"}"#), r#"{"a":"1","b":"2"}"#);
assert_eq!(
&test_canonical_json(
r#"{
"auth": {
"success": true,
"mxid": "@john.doe:example.com",
"profile": {
"display_name": "John Doe",
"three_pids": [
{
"medium": "email",
"address": "john.doe@example.org"
},
{
"medium": "msisdn",
"address": "123456789"
}
]
}
}
}"#
),
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}}"#
);
assert_eq!(
&test_canonical_json(
r#"{
"a": "日本語"
}"#
),
r#"{"a":"日本語"}"#
);
assert_eq!(
&test_canonical_json(
r#"{
"本": 2,
"日": 1
}"#
),
r#"{"日":1,"本":2}"#
);
assert_eq!(
&test_canonical_json(
r#"{
"a": "\u65E5"
}"#
),
r#"{"a":"日"}"#
);
assert_eq!(
&test_canonical_json(
r#"{
"a": null
}"#
),
r#"{"a":null}"#
);
}
#[test]
fn sign_empty_json() {
let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
let mut value = from_json_str("{}").unwrap();
sign_json("domain", &key_pair, &mut value).unwrap();
assert_eq!(
to_json_string(&value).unwrap(),
r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#
);
}
#[test]
fn verify_empty_json() {
let value = from_json_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap();
let mut signature_set = BTreeMap::new();
signature_set.insert("ed25519:1".into(), public_key_string());
let mut public_key_map = BTreeMap::new();
public_key_map.insert("domain".into(), signature_set);
verify_json(&public_key_map, &value).unwrap();
}
#[test]
fn sign_minimal_json() {
let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
let mut alpha_object = from_json_str(r#"{ "one": 1, "two": "Two" }"#).unwrap();
sign_json("domain", &key_pair, &mut alpha_object).unwrap();
assert_eq!(
to_json_string(&alpha_object).unwrap(),
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
);
let mut reverse_alpha_object =
from_json_str(r#"{ "two": "Two", "one": 1 }"#).expect("reverse_alpha should serialize");
sign_json("domain", &key_pair, &mut reverse_alpha_object).unwrap();
assert_eq!(
to_json_string(&reverse_alpha_object).unwrap(),
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
);
}
#[test]
fn verify_minimal_json() {
let value = from_json_str(
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
).unwrap();
let mut signature_set = BTreeMap::new();
signature_set.insert("ed25519:1".into(), public_key_string());
let mut public_key_map = BTreeMap::new();
public_key_map.insert("domain".into(), signature_set);
verify_json(&public_key_map, &value).unwrap();
let reverse_value = from_json_str(
r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"#
).unwrap();
verify_json(&public_key_map, &reverse_value).unwrap();
}
#[test]
fn fail_verify_json() {
let value = from_json_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap();
let mut signature_set = BTreeMap::new();
signature_set.insert("ed25519:1".into(), public_key_string());
let mut public_key_map = BTreeMap::new();
public_key_map.insert("domain".into(), signature_set);
verify_json(&public_key_map, &value).unwrap_err();
}
#[test]
fn sign_minimal_event() {
let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
let json = r#"{
"room_id": "!x:domain",
"sender": "@a:domain",
"origin": "domain",
"origin_server_ts": 1000000,
"signatures": {},
"hashes": {},
"type": "X",
"content": {},
"prev_events": [],
"auth_events": [],
"depth": 3,
"unsigned": {
"age_ts": 1000000
}
}"#;
let mut object = from_json_str(json).unwrap();
hash_and_sign_event("domain", &key_pair, &mut object, &RoomVersionId::V5).unwrap();
assert_eq!(
to_json_string(&object).unwrap(),
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}}"#
);
}
#[test]
fn sign_redacted_event() {
let key_pair = Ed25519KeyPair::from_der(&pkcs8(), "1".into()).unwrap();
let json = r#"{
"content": {
"body": "Here is the message content"
},
"event_id": "$0:domain",
"origin": "domain",
"origin_server_ts": 1000000,
"type": "m.room.message",
"room_id": "!r:domain",
"sender": "@u:domain",
"signatures": {},
"unsigned": {
"age_ts": 1000000
}
}"#;
let mut object = from_json_str(json).unwrap();
hash_and_sign_event("domain", &key_pair, &mut object, &RoomVersionId::V5).unwrap();
assert_eq!(
to_json_string(&object).unwrap(),
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}}"#
);
}
#[test]
fn verify_minimal_event() {
let mut signature_set = BTreeMap::new();
signature_set.insert("ed25519:1".into(), public_key_string());
let mut public_key_map = BTreeMap::new();
public_key_map.insert("domain".into(), signature_set);
let value = from_json_str(
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
}
}"#
).unwrap();
verify_event(&public_key_map, &value, &RoomVersionId::V5).unwrap();
}
}