use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
mem,
str::FromStr,
};
use base64::{decode_config, encode_config, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
use ring::digest::{digest, SHA256};
use ruma_identifiers::{EventId, RoomVersionId, ServerNameBox, UserId};
use ruma_serde::{to_canonical_json_string, CanonicalJsonObject, CanonicalJsonValue};
use serde_json::from_str as from_json_str;
use crate::{
keys::{KeyPair, PublicKeyMap},
split_id,
verification::{Ed25519Verifier, Verified, Verifier},
Error,
};
static ALLOWED_KEYS: &[&str] = &[
"event_id",
"type",
"room_id",
"sender",
"state_key",
"content",
"hashes",
"signatures",
"depth",
"prev_events",
"prev_state",
"auth_events",
"origin",
"origin_server_ts",
"membership",
];
fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static [&'static str] {
match event_type {
"m.room.member" => &["membership"],
"m.room.create" => &["creator"],
"m.room.join_rules" => &["join_rule"],
"m.room.power_levels" => &[
"ban",
"events",
"events_default",
"kick",
"redact",
"state_default",
"users",
"users_default",
],
"m.room.aliases" => match version {
RoomVersionId::Version1
| RoomVersionId::Version2
| RoomVersionId::Version3
| RoomVersionId::Version4
| RoomVersionId::Version5 => &["aliases"],
_ => &[],
},
"m.room.history_visibility" => &["history_visibility"],
_ => &[],
}
}
static CANONICAL_JSON_FIELDS_TO_REMOVE: &[&str] = &["signatures", "unsigned"];
static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsigned"];
static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "unsigned"];
pub fn sign_json<K>(
entity_id: &str,
key_pair: &K,
object: &mut CanonicalJsonObject,
) -> Result<(), Error>
where
K: KeyPair,
{
let (signatures_key, mut signature_map) = match object.remove_entry("signatures") {
Some((key, CanonicalJsonValue::Object(signatures))) => (Cow::Owned(key), signatures),
Some(_) => return Err(Error::new("field `signatures` must be a JSON object")),
None => (Cow::Borrowed("signatures"), BTreeMap::new()),
};
let maybe_unsigned_entry = object.remove_entry("unsigned");
let json = to_canonical_json_string(object)?;
let signature = key_pair.sign(json.as_bytes());
let signature_set = signature_map
.entry(entity_id.to_string())
.or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
let signature_set = match signature_set {
CanonicalJsonValue::Object(obj) => obj,
_ => return Err(Error::new("fields in `signatures` must be objects")),
};
signature_set.insert(signature.id(), CanonicalJsonValue::String(signature.base64()));
object.insert(signatures_key.into(), CanonicalJsonValue::Object(signature_map));
if let Some((k, v)) = maybe_unsigned_entry {
object.insert(k, v);
}
Ok(())
}
pub fn canonical_json(object: &CanonicalJsonObject) -> String {
canonical_json_with_fields_to_remove(object, CANONICAL_JSON_FIELDS_TO_REMOVE)
}
pub fn verify_json(
public_key_map: &PublicKeyMap,
object: &CanonicalJsonObject,
) -> Result<(), Error> {
let signature_map = match object.get("signatures") {
Some(CanonicalJsonValue::Object(signatures)) => signatures.clone(),
Some(_) => return Err(Error::new("field `signatures` must be a JSON object")),
None => return Err(Error::new("JSON object must contain a `signatures` field.")),
};
for (entity_id, public_keys) in public_key_map {
let signature_set = match signature_map.get(entity_id) {
Some(CanonicalJsonValue::Object(set)) => set,
Some(_) => return Err(Error::new("signature sets must be JSON objects")),
None => {
return Err(Error::new(format!("no signatures found for entity `{}`", entity_id)))
}
};
let mut maybe_signature = None;
let mut maybe_public_key = None;
for (key_id, public_key) in public_keys {
if split_id(key_id).is_err() {
break;
}
if let Some(signature) = signature_set.get(key_id) {
maybe_signature = Some(signature);
maybe_public_key = Some(public_key);
break;
}
}
let signature = match maybe_signature {
Some(CanonicalJsonValue::String(signature)) => signature,
Some(_) => return Err(Error::new("signature must be a string")),
None => {
return Err(Error::new("event is not signed with any of the given public keys"))
}
};
let public_key = match maybe_public_key {
Some(public_key) => public_key,
None => {
return Err(Error::new("event is not signed with any of the given public keys"))
}
};
let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?;
let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?;
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, object)?;
}
Ok(())
}
fn verify_json_with<V>(
verifier: &V,
public_key: &[u8],
signature: &[u8],
object: &CanonicalJsonObject,
) -> Result<(), Error>
where
V: Verifier,
{
verifier.verify_json(public_key, signature, canonical_json(object).as_bytes())
}
pub fn content_hash(object: &CanonicalJsonObject) -> String {
let json = canonical_json_with_fields_to_remove(object, CONTENT_HASH_FIELDS_TO_REMOVE);
let hash = digest(&SHA256, json.as_bytes());
encode_config(&hash, STANDARD_NO_PAD)
}
pub fn reference_hash(
value: &CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<String, Error> {
let redacted_value = redact(value, version)?;
let json =
canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE);
let hash = digest(&SHA256, json.as_bytes());
Ok(encode_config(
&hash,
match version {
RoomVersionId::Version1 | RoomVersionId::Version2 | RoomVersionId::Version3 => {
STANDARD_NO_PAD
}
_ => URL_SAFE_NO_PAD,
},
))
}
pub fn hash_and_sign_event<K>(
entity_id: &str,
key_pair: &K,
object: &mut CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<(), Error>
where
K: KeyPair,
{
let hash = content_hash(object);
let hashes_value = object
.entry("hashes".to_owned())
.or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
match hashes_value {
CanonicalJsonValue::Object(hashes) => {
hashes.insert("sha256".into(), CanonicalJsonValue::String(hash))
}
_ => return Err(Error::new("field `hashes` must be a JSON object")),
};
let mut redacted = redact(object, version)?;
sign_json(entity_id, key_pair, &mut redacted)?;
object.insert("signatures".into(), mem::take(redacted.get_mut("signatures").unwrap()));
Ok(())
}
pub fn verify_event(
public_key_map: &PublicKeyMap,
object: &CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<Verified, Error> {
let redacted = redact(object, version)?;
let hash = match object.get("hashes") {
Some(hashes_value) => match hashes_value {
CanonicalJsonValue::Object(hashes) => match hashes.get("sha256") {
Some(hash_value) => match hash_value {
CanonicalJsonValue::String(hash) => hash,
_ => return Err(Error::new("sha256 hash must be a JSON string")),
},
None => return Err(Error::new("field `hashes` must be a JSON object")),
},
_ => return Err(Error::new("event missing sha256 hash")),
},
None => return Err(Error::new("field `hashes` must be present")),
};
let signature_map = match object.get("signatures") {
Some(CanonicalJsonValue::Object(signatures)) => signatures,
Some(_) => return Err(Error::new("field `signatures` must be a JSON object")),
None => return Err(Error::new("JSON object must contain a `signatures` field.")),
};
let servers_to_check = servers_to_check_signatures(object, version)?;
let canonical_json = from_json_str(&canonical_json(&redacted))?;
for entity_id in servers_to_check {
let signature_set = match signature_map.get(entity_id.as_str()) {
Some(CanonicalJsonValue::Object(set)) => set,
Some(_) => return Err(Error::new("signatures sets must be JSON objects")),
None => {
return Err(Error::new(format!("no signatures found for entity `{}`", entity_id)))
}
};
let mut maybe_signature = None;
let mut maybe_public_key = None;
let public_keys = public_key_map
.get(entity_id.as_str())
.ok_or_else(|| Error::new(format!("missing public keys for server {}", entity_id)))?;
for (key_id, public_key) in public_keys {
if split_id(key_id).is_err() {
break;
}
if let Some(signature) = signature_set.get(key_id) {
maybe_signature = Some(signature);
maybe_public_key = Some(public_key);
break;
}
}
let signature = match maybe_signature {
Some(CanonicalJsonValue::String(signature)) => signature,
Some(_) => return Err(Error::new("signature must be a string")),
None => {
return Err(Error::new("event is not signed with any of the given public keys"))
}
};
let public_key = match maybe_public_key {
Some(public_key) => public_key,
None => {
return Err(Error::new("event is not signed with any of the given public keys"))
}
};
let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?;
let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?;
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, &canonical_json)?;
}
let calculated_hash = content_hash(object);
if *hash == calculated_hash {
Ok(Verified::All)
} else {
Ok(Verified::Signatures)
}
}
fn canonical_json_with_fields_to_remove(object: &CanonicalJsonObject, fields: &[&str]) -> String {
let mut owned_object = object.clone();
for field in fields {
owned_object.remove(*field);
}
to_canonical_json_string(&owned_object).expect("JSON object serialization to succeed")
}
pub fn redact(
object: &CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<CanonicalJsonObject, Error> {
let mut event = object.clone();
let event_type_value = match event.get("type") {
Some(event_type_value) => event_type_value,
None => return Err(Error::new("field `type` in JSON value must be present")),
};
let allowed_content_keys = match event_type_value {
CanonicalJsonValue::String(event_type) => allowed_content_keys_for(event_type, version),
_ => return Err(Error::new("field `type` in JSON value must be a JSON string")),
};
if let Some(content_value) = event.get_mut("content") {
let content = match content_value {
CanonicalJsonValue::Object(map) => map,
_ => return Err(Error::new("field `content` in JSON value must be a JSON object")),
};
let mut old_content = mem::take(content);
for &key in allowed_content_keys {
if let Some(value) = old_content.remove(key) {
content.insert(key.to_owned(), value);
}
}
}
let mut old_event = mem::take(&mut event);
for &key in ALLOWED_KEYS {
if let Some(value) = old_event.remove(key) {
event.insert(key.to_owned(), value);
}
}
Ok(event)
}
fn servers_to_check_signatures(
object: &CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<BTreeSet<ServerNameBox>, Error> {
let mut servers_to_check = BTreeSet::new();
if !is_third_party_invite(object)? {
match object.get("sender") {
Some(CanonicalJsonValue::String(raw_sender)) => {
let user_id = UserId::from_str(raw_sender)
.map_err(|_| Error::new("could not parse user id"))?;
servers_to_check.insert(user_id.server_name().to_owned());
}
_ => return Err(Error::new("field `sender` must be a JSON string")),
};
}
match version {
RoomVersionId::Version1 | RoomVersionId::Version2 => match object.get("event_id") {
Some(CanonicalJsonValue::String(raw_event_id)) => {
let event_id = EventId::from_str(raw_event_id)
.map_err(|_| Error::new("could not parse event id"))?;
let server_name = event_id
.server_name()
.ok_or_else(|| {
Error::new("Event id should have a server name for the given room version")
})?
.to_owned();
servers_to_check.insert(server_name);
}
_ => {
return Err(Error::new(
"Expected to find a string `event_id` for the given room version",
))
}
},
_ => (),
}
Ok(servers_to_check)
}
fn is_third_party_invite(object: &CanonicalJsonObject) -> Result<bool, Error> {
match object.get("type") {
Some(CanonicalJsonValue::String(raw_type)) => Ok(raw_type == "m.room.third_party_invite"),
_ => Err(Error::new("field `type` must be a JSON string")),
}
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
};
use base64::{encode_config, STANDARD_NO_PAD};
use ruma_identifiers::{RoomVersionId, ServerSigningKeyId, SigningKeyAlgorithm};
use ruma_serde::CanonicalJsonValue;
use serde_json::json;
use super::canonical_json;
use crate::{sign_json, verify_event, Ed25519KeyPair, PublicKeyMap, PublicKeySet, Verified};
#[test]
fn canonical_json_complex() {
let data = json!({
"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"
}
]
}
}
});
let canonical = 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}}"#;
let object = match CanonicalJsonValue::try_from(data).unwrap() {
CanonicalJsonValue::Object(obj) => obj,
_ => unreachable!(),
};
assert_eq!(canonical_json(&object), canonical);
}
#[test]
fn verify_event_does_not_check_signatures_for_third_party_invites() {
let signed_event = serde_json::from_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": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg"
}
},
"type": "m.room.third_party_invite",
"unsigned": {
"age_ts": 1000000
}
}"#
).unwrap();
let public_key_map = BTreeMap::new();
let verification_result =
verify_event(&public_key_map, &signed_event, &RoomVersionId::Version6);
assert!(verification_result.is_ok());
let verification = verification_result.unwrap();
assert!(matches!(verification, Verified::Signatures));
}
#[test]
fn verify_event_check_signatures_for_both_sender_and_event_id() {
let key_pair_sender = generate_key_pair();
let key_pair_event = generate_key_pair();
let mut signed_event = serde_json::from_str(
r#"{
"event_id": "$event_id:domain-event",
"auth_events": [],
"content": {},
"depth": 3,
"hashes": {
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
},
"origin": "domain",
"origin_server_ts": 1000000,
"prev_events": [],
"room_id": "!x:domain",
"sender": "@name:domain-sender",
"type": "X",
"unsigned": {
"age_ts": 1000000
}
}"#,
)
.unwrap();
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
sign_json("domain-event", &key_pair_event, &mut signed_event).unwrap();
let mut public_key_map = BTreeMap::new();
add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender);
add_key_to_map(&mut public_key_map, "domain-event", &key_pair_event);
let verification_result =
verify_event(&public_key_map, &signed_event, &RoomVersionId::Version1);
assert!(verification_result.is_ok());
let verification = verification_result.unwrap();
assert!(matches!(verification, Verified::Signatures));
}
#[test]
fn verification_fails_if_required_keys_are_not_given() {
let key_pair_sender = generate_key_pair();
let mut signed_event = serde_json::from_str(
r#"{
"auth_events": [],
"content": {},
"depth": 3,
"hashes": {
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
},
"origin": "domain",
"origin_server_ts": 1000000,
"prev_events": [],
"room_id": "!x:domain",
"sender": "@name:domain-sender",
"type": "X",
"unsigned": {
"age_ts": 1000000
}
}"#,
)
.unwrap();
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
let public_key_map = BTreeMap::new();
let verification_result =
verify_event(&public_key_map, &signed_event, &RoomVersionId::Version6);
assert!(verification_result.is_err());
let error_msg = verification_result.err().unwrap().message;
assert!(error_msg.contains("missing public keys for server"));
}
#[test]
fn verify_event_fails_if_public_key_is_invalid() {
let key_pair_sender = generate_key_pair();
let mut signed_event = serde_json::from_str(
r#"{
"auth_events": [],
"content": {},
"depth": 3,
"hashes": {
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
},
"origin": "domain",
"origin_server_ts": 1000000,
"prev_events": [],
"room_id": "!x:domain",
"sender": "@name:domain-sender",
"type": "X",
"unsigned": {
"age_ts": 1000000
}
}"#,
)
.unwrap();
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
let mut public_key_map = PublicKeyMap::new();
let mut sender_key_map = PublicKeySet::new();
let newly_generated_key_pair = generate_key_pair();
let encoded_public_key =
encode_config(newly_generated_key_pair.public_key(), STANDARD_NO_PAD);
let version = ServerSigningKeyId::from_parts(
SigningKeyAlgorithm::Ed25519,
key_pair_sender.version().try_into().unwrap(),
);
sender_key_map.insert(version.to_string(), encoded_public_key);
public_key_map.insert("domain-sender".to_string(), sender_key_map);
let verification_result =
verify_event(&public_key_map, &signed_event, &RoomVersionId::Version6);
assert!(verification_result.is_err());
let error_msg = verification_result.err().unwrap().message;
assert!(error_msg.contains("signature verification failed"));
}
fn generate_key_pair() -> Ed25519KeyPair {
let key_content = Ed25519KeyPair::generate().unwrap();
Ed25519KeyPair::new(&key_content, "1".to_string()).unwrap()
}
fn add_key_to_map(public_key_map: &mut PublicKeyMap, name: &str, pair: &Ed25519KeyPair) {
let mut sender_key_map = PublicKeySet::new();
let encoded_public_key = encode_config(pair.public_key(), STANDARD_NO_PAD);
let version = ServerSigningKeyId::from_parts(
SigningKeyAlgorithm::Ed25519,
pair.version().try_into().unwrap(),
);
sender_key_map.insert(version.to_string(), encoded_public_key);
public_key_map.insert(name.to_string(), sender_key_map);
}
}