From 202b3ed402c938727de76aae684871b34ada9663 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 23:35:09 -0700 Subject: [PATCH] Add content_hash and reference_hash functions and move all functions into a module. --- src/functions.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 74 +--------------- 2 files changed, 225 insertions(+), 72 deletions(-) create mode 100644 src/functions.rs diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 00000000..47046642 --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,223 @@ +//! Functions for signing and verifying JSON and events. + +use std::error::Error as _; + +use base64::{encode_config, STANDARD_NO_PAD}; +use ring::digest::{digest, SHA256}; +use serde_json::{to_string, Value}; + +use crate::{keys::KeyPair, signatures::Signature, verification::Verifier, Error}; + +/// The fields that are allowed to remain in an event during redaction. +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", +]; + +/// The fields of an *m.room.power_levels* event's `content` key that are allowed to remain in an +/// event during redaction. +static ALLOWED_POWER_LEVELS_KEYS: &[&str] = &[ + "ban", + "events", + "events_default", + "kick", + "redact", + "state_default", + "users", + "users_default", +]; + +/// The fields to remove from a JSON object when converting JSON into the "canonical" form. +static CANONICAL_JSON_FIELDS_TO_REMOVE: &[&str] = &["signatures", "unsigned"]; + +/// The fields to remove from a JSON object when creating a content hash of an event. +static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsigned"]; + +/// The fields to remove from a JSON object when creating a reference hash of an event. +static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "unsigned"]; + +/// Signs an arbitrary JSON object. +/// +/// # Parameters +/// +/// * key_pair: A cryptographic key pair used to sign the JSON. +/// * value: A JSON object to be signed according to the Matrix specification. +/// +/// # Errors +/// +/// Returns an error if the JSON value is not a JSON object. +pub fn sign_json(key_pair: &K, value: &Value) -> Result +where + K: KeyPair, +{ + let json = to_canonical_json(value)?; + + Ok(key_pair.sign(json.as_bytes())) +} + +/// Converts a JSON object into the "canonical" string form, suitable for signing. +/// +/// # Parameters +/// +/// * value: The `serde_json::Value` (JSON value) to convert. +/// +/// # Errors +/// +/// Returns an error if the provided JSON value is not a JSON object. +pub fn to_canonical_json(value: &Value) -> Result { + to_canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) +} + +/// Use a public key to verify a signature of a JSON object. +/// +/// # Parameters +/// +/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. +/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. +/// * signature: The `Signature` to verify. +/// * value: The `serde_json::Value` (JSON value) that was signed. +/// +/// # Errors +/// +/// Returns an error if verification fails. +pub fn verify_json( + verifier: &V, + public_key: &[u8], + signature: &Signature, + value: &Value, +) -> Result<(), Error> +where + V: Verifier, +{ + verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) +} + +/// Creates a *content hash* for the JSON representation of an event. +/// +/// The content hash of an event covers the complete event including the unredacted contents. It is +/// used during federation and is described in the Matrix server-server specification. +pub fn content_hash(value: &Value) -> Result { + let json = to_canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?; + + let hash = digest(&SHA256, json.as_bytes()); + + Ok(encode_config(&hash, STANDARD_NO_PAD)) +} + +/// Creates a *reference hash* for the JSON representation of an event. +/// +/// The reference hash of an event covers the essential fields of an event, including content +/// hashes. It is used during federation and is described in the Matrix server-server +/// specification. +pub fn reference_hash(value: &Value) -> Result { + let redacted_value = redact(value)?; + + let json = + to_canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE)?; + + let hash = digest(&SHA256, json.as_bytes()); + + Ok(encode_config(&hash, STANDARD_NO_PAD)) +} + +/// Internal implementation detail of the canonical JSON algorithm. Allows customization of the +/// fields that will be removed before serializing. +fn to_canonical_json_with_fields_to_remove( + value: &Value, + fields: &[&str], +) -> Result { + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + { + let object = owned_value + .as_object_mut() + .expect("safe since we checked above"); + + for field in fields { + object.remove(*field); + } + } + + to_string(&owned_value).map_err(|error| Error::new(error.description())) +} + +/// Redact the JSON representation of an event using the rules specified in the Matrix +/// client-server specification. +/// +/// This is part of the process of signing an event. +fn redact(value: &Value) -> Result { + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + let event = owned_value + .as_object_mut() + .expect("safe since we checked above"); + + 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 event_type = match event_type_value.as_str() { + Some(event_type) => event_type.to_string(), + None => { + return Err(Error::new( + "Field `type` in JSON value must be a JSON string", + )) + } + }; + + if let Some(content_value) = event.get_mut("content") { + if !content_value.is_object() { + return Err(Error::new( + "Field `content` in JSON value must be a JSON object", + )); + } + + let content = content_value + .as_object_mut() + .expect("safe since we checked above"); + + for key in content.clone().keys() { + match event_type.as_ref() { + "m.room.member" if key != "membership" => content.remove(key), + "m.room.create" if key != "creator" => content.remove(key), + "m.room.join_rules" if key != "join_rules" => content.remove(key), + "m.room.power_levels" if !ALLOWED_POWER_LEVELS_KEYS.contains(&key.as_ref()) => { + content.remove(key) + } + "m.room.aliases" if key != "aliases" => content.remove(key), + "m.room.history_visibility" if key != "history_visibility" => content.remove(key), + _ => content.remove(key), + }; + } + } + + for key in event.clone().keys() { + if !ALLOWED_KEYS.contains(&key.as_ref()) { + event.remove(key); + } + } + + Ok(owned_value) +} diff --git a/src/lib.rs b/src/lib.rs index c9689677..36d3c8a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,88 +148,18 @@ use std::{ fmt::{Display, Formatter, Result as FmtResult}, }; -use serde_json::{to_string, Value}; - pub use url::Host; +pub use functions::{content_hash, reference_hash, sign_json, to_canonical_json, verify_json}; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; pub use verification::{Ed25519Verifier, Verifier}; +mod functions; mod keys; mod signatures; mod verification; -/// Signs an arbitrary JSON object. -/// -/// # Parameters -/// -/// * key_pair: A cryptographic key pair used to sign the JSON. -/// * value: A JSON object to be signed according to the Matrix specification. -/// -/// # Errors -/// -/// Returns an error if the JSON value is not a JSON object. -pub fn sign_json(key_pair: &K, value: &Value) -> Result -where - K: KeyPair, -{ - let json = to_canonical_json(value)?; - - Ok(key_pair.sign(json.as_bytes())) -} - -/// Converts a JSON object into the "canonical" string form, suitable for signing. -/// -/// # Parameters -/// -/// * value: The `serde_json::Value` (JSON value) to convert. -/// -/// # Errors -/// -/// Returns an error if the provided JSON value is not a JSON object. -pub fn to_canonical_json(value: &Value) -> Result { - if !value.is_object() { - return Err(Error::new("JSON value must be a JSON object")); - } - - let mut owned_value = value.clone(); - - { - let object = owned_value - .as_object_mut() - .expect("safe since we checked above"); - object.remove("signatures"); - object.remove("unsigned"); - } - - to_string(&owned_value).map_err(|error| Error::new(error.description())) -} - -/// Use a public key to verify a signature of a JSON object. -/// -/// # Parameters -/// -/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. -/// * signature: The `Signature` to verify. -/// * value: The `serde_json::Value` (JSON value) that was signed. -/// -/// # Errors -/// -/// Returns an error if verification fails. -pub fn verify_json( - verifier: &V, - public_key: &[u8], - signature: &Signature, - value: &Value, -) -> Result<(), Error> -where - V: Verifier, -{ - verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) -} - /// An error produced when ruma-signatures operations fail. #[derive(Clone, Debug, PartialEq)] pub struct Error {