use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
mem,
};
use base64::{encode_config, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
use ruma_common::{
canonical_json::{redact, JsonType},
serde::{base64::Standard, Base64},
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomVersionId, UserId,
};
use serde_json::{from_str as from_json_str, to_string as to_json_string};
use sha2::{digest::Digest, Sha256};
use crate::{
keys::{KeyPair, PublicKeyMap},
split_id,
verification::{Ed25519Verifier, Verified, Verifier},
Error, JsonError, ParseError, VerificationError,
};
const MAX_PDU_BYTES: usize = 65_535;
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(JsonError::not_of_type("signatures", JsonType::Object)),
None => (Cow::Borrowed("signatures"), BTreeMap::new()),
};
let maybe_unsigned_entry = object.remove_entry("unsigned");
let json = to_json_string(object).map_err(JsonError::Serde)?;
let signature = key_pair.sign(json.as_bytes());
let signature_set = signature_map
.entry(entity_id.to_owned())
.or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::new()));
let signature_set = match signature_set {
CanonicalJsonValue::Object(obj) => obj,
_ => return Err(JsonError::not_multiples_of_type("signatures", JsonType::Object)),
};
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) -> Result<String, Error> {
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(JsonError::not_of_type("signatures", JsonType::Object)),
None => return Err(JsonError::field_missing_from_object("signatures")),
};
for (entity_id, signature_set) in signature_map {
let signature_set = match signature_set {
CanonicalJsonValue::Object(set) => set,
_ => return Err(JsonError::not_multiples_of_type("signature sets", JsonType::Object)),
};
let public_keys = match public_key_map.get(&entity_id) {
Some(keys) => keys,
None => {
return Err(JsonError::key_missing("public_key_map", "public_keys", &entity_id))
}
};
for (key_id, signature) in &signature_set {
let signature = match signature {
CanonicalJsonValue::String(s) => s,
_ => return Err(JsonError::not_of_type("signature", JsonType::String)),
};
let public_key = public_keys.get(key_id).ok_or_else(|| {
JsonError::key_missing(
format!("public_keys of {}", &entity_id),
"signature",
key_id,
)
})?;
let signature = Base64::<Standard>::parse(signature)
.map_err(|e| ParseError::base64("signature", signature, e))?;
verify_json_with(
&Ed25519Verifier,
public_key.as_bytes(),
signature.as_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) -> Result<Base64<Standard, [u8; 32]>, Error> {
let json = canonical_json_with_fields_to_remove(object, CONTENT_HASH_FIELDS_TO_REMOVE)?;
if json.len() > MAX_PDU_BYTES {
return Err(Error::PduSize);
}
let hash = Sha256::digest(json.as_bytes());
Ok(Base64::new(hash.into()))
}
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)?;
if json.len() > MAX_PDU_BYTES {
return Err(Error::PduSize);
}
let hash = Sha256::digest(json.as_bytes());
Ok(encode_config(
hash,
match version {
RoomVersionId::V1 | RoomVersionId::V2 | RoomVersionId::V3 => 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.encode()))
}
_ => return Err(JsonError::not_of_type("hashes", JsonType::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(JsonError::not_of_type("sha256 hash", JsonType::String)),
},
None => return Err(JsonError::not_of_type("hashes", JsonType::Object)),
},
_ => return Err(JsonError::field_missing_from_object("sha256")),
},
None => return Err(JsonError::field_missing_from_object("hashes")),
};
let signature_map = match object.get("signatures") {
Some(CanonicalJsonValue::Object(signatures)) => signatures,
Some(_) => return Err(JsonError::not_of_type("signatures", JsonType::Object)),
None => return Err(JsonError::field_missing_from_object("signatures")),
};
let servers_to_check = servers_to_check_signatures(object, version)?;
let canonical_json = from_json_str(&canonical_json(&redacted)?).map_err(JsonError::from)?;
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(JsonError::not_multiples_of_type("signature sets", JsonType::Object))
}
None => return Err(VerificationError::signature_not_found(entity_id)),
};
let mut maybe_signature_and_public_key = None;
let public_keys = public_key_map
.get(entity_id.as_str())
.ok_or_else(|| VerificationError::public_key_not_found(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_and_public_key = Some(SignatureAndPubkey { signature, public_key });
break;
}
}
let signature_and_pubkey = match maybe_signature_and_public_key {
Some(value) => value,
None => return Err(VerificationError::UnknownPublicKeysForSignature.into()),
};
let signature = match signature_and_pubkey.signature {
CanonicalJsonValue::String(signature) => signature,
_ => return Err(JsonError::not_of_type("signature", JsonType::String)),
};
let public_key = signature_and_pubkey.public_key;
let signature = Base64::<Standard>::parse(signature)
.map_err(|e| ParseError::base64("signature", signature, e))?;
verify_json_with(
&Ed25519Verifier,
public_key.as_bytes(),
signature.as_bytes(),
&canonical_json,
)?;
}
let calculated_hash = content_hash(object)?;
if let Ok(hash) = Base64::<Standard>::parse(hash) {
if hash.as_bytes() == calculated_hash.as_bytes() {
return Ok(Verified::All);
}
}
Ok(Verified::Signatures)
}
struct SignatureAndPubkey<'a> {
signature: &'a CanonicalJsonValue,
public_key: &'a Base64,
}
fn canonical_json_with_fields_to_remove(
object: &CanonicalJsonObject,
fields: &[&str],
) -> Result<String, Error> {
let mut owned_object = object.clone();
for field in fields {
owned_object.remove(*field);
}
to_json_string(&owned_object).map_err(|e| Error::Json(e.into()))
}
fn servers_to_check_signatures(
object: &CanonicalJsonObject,
version: &RoomVersionId,
) -> Result<BTreeSet<OwnedServerName>, 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>::try_from(raw_sender.as_str())
.map_err(|e| Error::from(ParseError::UserId(e)))?;
servers_to_check.insert(user_id.server_name().to_owned());
}
_ => return Err(JsonError::not_of_type("sender", JsonType::String)),
};
}
match version {
RoomVersionId::V1 | RoomVersionId::V2 => match object.get("event_id") {
Some(CanonicalJsonValue::String(raw_event_id)) => {
let event_id: OwnedEventId =
raw_event_id.parse().map_err(|e| Error::from(ParseError::EventId(e)))?;
let server_name = event_id
.server_name()
.ok_or_else(|| ParseError::from_event_id_by_room_version(&event_id, version))?
.to_owned();
servers_to_check.insert(server_name);
}
_ => {
return Err(JsonError::field_missing_from_object("event_id"));
}
},
RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
| RoomVersionId::V7 => {}
RoomVersionId::V8 | RoomVersionId::V9 | RoomVersionId::V10 => {
if let Some(authorized_user) = object
.get("content")
.and_then(|c| c.as_object())
.and_then(|c| c.get("join_authorised_via_users_server"))
{
let authorized_user = authorized_user.as_str().ok_or_else(|| {
JsonError::not_of_type("join_authorised_via_users_server", JsonType::String)
})?;
let authorized_user = <&UserId>::try_from(authorized_user)
.map_err(|e| Error::from(ParseError::UserId(e)))?;
servers_to_check.insert(authorized_user.server_name().to_owned());
}
}
_ => unimplemented!(),
}
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(JsonError::not_of_type("type", JsonType::String)),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use ruma_common::{
serde::Base64, CanonicalJsonValue, RoomVersionId, ServerSigningKeyId, SigningKeyAlgorithm,
};
use serde_json::json;
use super::canonical_json;
use crate::{
sign_json, verify_event, Ed25519KeyPair, Error, PublicKeyMap, PublicKeySet,
VerificationError, 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).unwrap(), 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 =
verify_event(&public_key_map, &signed_event, &RoomVersionId::V6).unwrap();
assert_eq!(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 =
verify_event(&public_key_map, &signed_event, &RoomVersionId::V1).unwrap();
assert_eq!(verification, Verified::Signatures);
}
#[test]
fn verify_event_check_signatures_for_authorized_user() {
let key_pair_sender = generate_key_pair();
let key_pair_authorized = generate_key_pair();
let mut signed_event = serde_json::from_str(
r#"{
"event_id": "$event_id:domain-event",
"auth_events": [],
"content": {"join_authorised_via_users_server": "@authorized:domain-authorized"},
"depth": 3,
"hashes": {
"sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"
},
"origin": "domain",
"origin_server_ts": 1000000,
"prev_events": [],
"room_id": "!x:domain",
"sender": "@name:domain-sender",
"type": "m.room.member",
"unsigned": {
"age_ts": 1000000
}
}"#,
)
.unwrap();
sign_json("domain-sender", &key_pair_sender, &mut signed_event).unwrap();
sign_json("domain-authorized", &key_pair_authorized, &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-authorized", &key_pair_authorized);
let verification =
verify_event(&public_key_map, &signed_event, &RoomVersionId::V9).unwrap();
assert_eq!(verification, Verified::Signatures);
}
#[test]
fn verification_fails_if_missing_signatures_for_authorized_user() {
let key_pair_sender = generate_key_pair();
let mut signed_event = serde_json::from_str(
r#"{
"event_id": "$event_id:domain-event",
"auth_events": [],
"content": {"join_authorised_via_users_server": "@authorized:domain-authorized"},
"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 = BTreeMap::new();
add_key_to_map(&mut public_key_map, "domain-sender", &key_pair_sender);
let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V9);
let server = assert_matches!(
verification_result,
Err(Error::Verification(VerificationError::SignatureNotFound(server))) => server
);
assert_eq!(server, "domain-authorized");
}
#[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::V6);
let entity = assert_matches!(
verification_result,
Err(Error::Verification(VerificationError::PublicKeyNotFound(entity))) => entity
);
assert_eq!(entity, "domain-sender");
}
#[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 = Base64::new(newly_generated_key_pair.public_key().to_owned());
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_owned(), sender_key_map);
let verification_result = verify_event(&public_key_map, &signed_event, &RoomVersionId::V6);
let error = assert_matches!(
verification_result,
Err(Error::Verification(VerificationError::Signature(error))) => error
);
assert!(format!("{error:?}").contains("Some(Verification equation was not satisfied)"));
}
fn generate_key_pair() -> Ed25519KeyPair {
let key_content = Ed25519KeyPair::generate().unwrap();
Ed25519KeyPair::from_der(&key_content, "1".to_owned())
.unwrap_or_else(|_| panic!("{:?}", &key_content))
}
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 = Base64::new(pair.public_key().to_owned());
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_owned(), sender_key_map);
}
}