signatures: Implement most function in terms of JSON objects
Instead of arbitrary JSON values, since they all error on non-object values anyway.
This commit is contained in:
parent
9b52601808
commit
5d03bd883a
@ -73,6 +73,9 @@ static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsig
|
|||||||
/// The fields to remove from a JSON object when creating a reference hash of an event.
|
/// 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"];
|
static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "unsigned"];
|
||||||
|
|
||||||
|
/// The inner type of `serde_json::Value::Object`.
|
||||||
|
pub type JsonObject = Map<String, Value>;
|
||||||
|
|
||||||
/// Signs an arbitrary JSON object and adds the signature to an object under the key `signatures`.
|
/// Signs an arbitrary JSON object and adds the signature to an object under the key `signatures`.
|
||||||
///
|
///
|
||||||
/// If `signatures` is already present, the new signature will be appended to the existing ones.
|
/// If `signatures` is already present, the new signature will be appended to the existing ones.
|
||||||
@ -82,14 +85,13 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns
|
|||||||
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
|
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
|
||||||
/// homeserver, e.g. "example.com".
|
/// homeserver, e.g. "example.com".
|
||||||
/// * key_pair: A cryptographic key pair used to sign the JSON.
|
/// * key_pair: A cryptographic key pair used to sign the JSON.
|
||||||
/// * value: A JSON object to sign according and append a signature to.
|
/// * object: A JSON object to sign according and append a signature to.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if:
|
/// Returns an error if:
|
||||||
///
|
///
|
||||||
/// * `value` is not a JSON object.
|
/// * `object` contains a field called `signatures` that is not a JSON object.
|
||||||
/// * `value` contains a field called `signatures` that is not a JSON object.
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@ -127,21 +129,15 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn sign_json<K>(entity_id: &str, key_pair: &K, value: &mut Value) -> Result<(), Error>
|
pub fn sign_json<K>(entity_id: &str, key_pair: &K, object: &mut JsonObject) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
K: KeyPair,
|
K: KeyPair,
|
||||||
{
|
{
|
||||||
let mut signature_map;
|
let mut signature_map;
|
||||||
let maybe_unsigned;
|
let maybe_unsigned;
|
||||||
|
|
||||||
// Pull `signatures` and `unsigned` out of the object.
|
|
||||||
let map = match value {
|
|
||||||
Value::Object(map) => map,
|
|
||||||
_ => return Err(Error::new("JSON value must be a JSON object")),
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: Once MSRV >= 1.45.0, use remove_key and don't allocate new `String`s below.
|
// FIXME: Once MSRV >= 1.45.0, use remove_key and don't allocate new `String`s below.
|
||||||
signature_map = match map.remove("signatures") {
|
signature_map = match object.remove("signatures") {
|
||||||
Some(signatures_value) => match signatures_value.as_object() {
|
Some(signatures_value) => match signatures_value.as_object() {
|
||||||
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
||||||
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
||||||
@ -149,10 +145,10 @@ where
|
|||||||
None => BTreeMap::new(),
|
None => BTreeMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
maybe_unsigned = map.remove("unsigned");
|
maybe_unsigned = object.remove("unsigned");
|
||||||
|
|
||||||
// Get the canonical JSON.
|
// Get the canonical JSON.
|
||||||
let json = to_string(map)?;
|
let json = to_string(object)?;
|
||||||
|
|
||||||
// Sign the canonical JSON.
|
// Sign the canonical JSON.
|
||||||
let signature = key_pair.sign(json.as_bytes());
|
let signature = key_pair.sign(json.as_bytes());
|
||||||
@ -163,10 +159,10 @@ where
|
|||||||
signature_set.insert(signature.id(), signature.base64());
|
signature_set.insert(signature.id(), signature.base64());
|
||||||
|
|
||||||
// Put `signatures` and `unsigned` back in.
|
// Put `signatures` and `unsigned` back in.
|
||||||
map.insert("signatures".into(), to_value(signature_map)?);
|
object.insert("signatures".into(), to_value(signature_map)?);
|
||||||
|
|
||||||
if let Some(unsigned) = maybe_unsigned {
|
if let Some(unsigned) = maybe_unsigned {
|
||||||
map.insert("unsigned".into(), unsigned);
|
object.insert("unsigned".into(), unsigned);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -177,11 +173,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// * value: The `serde_json::Value` (JSON value) to convert.
|
/// * object: The JSON object to convert.
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the provided JSON value is not a JSON object.
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@ -192,14 +184,13 @@ where
|
|||||||
/// "日": 1
|
/// "日": 1
|
||||||
/// }"#;
|
/// }"#;
|
||||||
///
|
///
|
||||||
/// let value = serde_json::from_str::<serde_json::Value>(input).unwrap();
|
/// let object = serde_json::from_str(input).unwrap();
|
||||||
///
|
/// let canonical = ruma_signatures::canonical_json(&object);
|
||||||
/// let canonical = ruma_signatures::canonical_json(&value).unwrap();
|
|
||||||
///
|
///
|
||||||
/// assert_eq!(canonical, r#"{"日":1,"本":2}"#);
|
/// assert_eq!(canonical, r#"{"日":1,"本":2}"#);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn canonical_json(value: &Value) -> Result<String, Error> {
|
pub fn canonical_json(object: &JsonObject) -> String {
|
||||||
canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE)
|
canonical_json_with_fields_to_remove(object, CANONICAL_JSON_FIELDS_TO_REMOVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Uses a set of public keys to verify a signed JSON object.
|
/// Uses a set of public keys to verify a signed JSON object.
|
||||||
@ -210,7 +201,7 @@ pub fn canonical_json(value: &Value) -> Result<String, Error> {
|
|||||||
/// Generally, entity identifiers are server names—the host/IP/port of a homeserver (e.g.
|
/// Generally, entity identifiers are server names—the host/IP/port of a homeserver (e.g.
|
||||||
/// "example.com") for which a signature must be verified. Key identifiers for each server (e.g.
|
/// "example.com") for which a signature must be verified. Key identifiers for each server (e.g.
|
||||||
/// "ed25519:1") then map to their respective public keys.
|
/// "ed25519:1") then map to their respective public keys.
|
||||||
/// * value: The `serde_json::Value` (JSON value) that was signed.
|
/// * object: The JSON object that was signed.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
@ -224,7 +215,7 @@ pub fn canonical_json(value: &Value) -> Result<String, Error> {
|
|||||||
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
|
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
|
||||||
///
|
///
|
||||||
/// // Deserialize the signed JSON.
|
/// // Deserialize the signed JSON.
|
||||||
/// let value = serde_json::from_str(
|
/// let object = serde_json::from_str(
|
||||||
/// r#"{
|
/// r#"{
|
||||||
/// "signatures": {
|
/// "signatures": {
|
||||||
/// "domain": {
|
/// "domain": {
|
||||||
@ -241,15 +232,10 @@ pub fn canonical_json(value: &Value) -> Result<String, Error> {
|
|||||||
/// public_key_map.insert("domain".into(), public_key_set);
|
/// public_key_map.insert("domain".into(), public_key_set);
|
||||||
///
|
///
|
||||||
/// // Verify at least one signature for each entity in `public_key_map`.
|
/// // Verify at least one signature for each entity in `public_key_map`.
|
||||||
/// assert!(ruma_signatures::verify_json(&public_key_map, &value).is_ok());
|
/// assert!(ruma_signatures::verify_json(&public_key_map, &object).is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn verify_json(public_key_map: &PublicKeyMap, value: &Value) -> Result<(), Error> {
|
pub fn verify_json(public_key_map: &PublicKeyMap, object: &JsonObject) -> Result<(), Error> {
|
||||||
let map = match value {
|
let signature_map: SignatureMap = match object.get("signatures") {
|
||||||
Value::Object(ref map) => map,
|
|
||||||
_ => return Err(Error::new("JSON value must be a JSON object")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let signature_map: SignatureMap = match map.get("signatures") {
|
|
||||||
Some(signatures_value) => match signatures_value.as_object() {
|
Some(signatures_value) => match signatures_value.as_object() {
|
||||||
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
||||||
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
||||||
@ -301,7 +287,7 @@ pub fn verify_json(public_key_map: &PublicKeyMap, value: &Value) -> Result<(), E
|
|||||||
|
|
||||||
let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?;
|
let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?;
|
||||||
|
|
||||||
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, value)?;
|
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, object)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -314,24 +300,21 @@ pub fn verify_json(public_key_map: &PublicKeyMap, value: &Value) -> Result<(), E
|
|||||||
/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used.
|
/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used.
|
||||||
/// * public_key: The raw bytes of the public key used to sign the JSON.
|
/// * public_key: The raw bytes of the public key used to sign the JSON.
|
||||||
/// * signature: The raw bytes of the signature.
|
/// * signature: The raw bytes of the signature.
|
||||||
/// * value: The `serde_json::Value` (JSON value) that was signed.
|
/// * object: The JSON object that was signed.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if:
|
/// Returns an error if verification fails.
|
||||||
///
|
|
||||||
/// * The provided JSON value is not a JSON object.
|
|
||||||
/// * Verification fails.
|
|
||||||
fn verify_json_with<V>(
|
fn verify_json_with<V>(
|
||||||
verifier: &V,
|
verifier: &V,
|
||||||
public_key: &[u8],
|
public_key: &[u8],
|
||||||
signature: &[u8],
|
signature: &[u8],
|
||||||
value: &Value,
|
object: &JsonObject,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
V: Verifier,
|
V: Verifier,
|
||||||
{
|
{
|
||||||
verifier.verify_json(public_key, signature, canonical_json(value)?.as_bytes())
|
verifier.verify_json(public_key, signature, canonical_json(object).as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a *content hash* for the JSON representation of an event.
|
/// Creates a *content hash* for the JSON representation of an event.
|
||||||
@ -343,17 +326,12 @@ where
|
|||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// value: A JSON object to generate a content hash for.
|
/// object: A JSON object to generate a content hash for.
|
||||||
///
|
pub fn content_hash(object: &JsonObject) -> String {
|
||||||
/// # Errors
|
let json = canonical_json_with_fields_to_remove(object, CONTENT_HASH_FIELDS_TO_REMOVE);
|
||||||
///
|
|
||||||
/// Returns an error if the provided JSON value is not a JSON object.
|
|
||||||
pub fn content_hash(value: &Value) -> Result<String, Error> {
|
|
||||||
let json = canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?;
|
|
||||||
|
|
||||||
let hash = digest(&SHA256, json.as_bytes());
|
let hash = digest(&SHA256, json.as_bytes());
|
||||||
|
|
||||||
Ok(encode_config(&hash, STANDARD_NO_PAD))
|
encode_config(&hash, STANDARD_NO_PAD)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a *reference hash* for the JSON representation of an event.
|
/// Creates a *reference hash* for the JSON representation of an event.
|
||||||
@ -366,16 +344,16 @@ pub fn content_hash(value: &Value) -> Result<String, Error> {
|
|||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// value: A JSON object to generate a reference hash for.
|
/// object: A JSON object to generate a reference hash for.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the provided JSON value is not a JSON object.
|
/// Returns an error if redaction fails.
|
||||||
pub fn reference_hash(value: &Value, version: &RoomVersionId) -> Result<String, Error> {
|
pub fn reference_hash(object: &JsonObject, version: &RoomVersionId) -> Result<String, Error> {
|
||||||
let redacted_value = redact(value, version)?;
|
let redacted_value = redact(object, version)?;
|
||||||
|
|
||||||
let json =
|
let json =
|
||||||
canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE)?;
|
canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE);
|
||||||
|
|
||||||
let hash = digest(&SHA256, json.as_bytes());
|
let hash = digest(&SHA256, json.as_bytes());
|
||||||
|
|
||||||
@ -402,17 +380,16 @@ pub fn reference_hash(value: &Value, version: &RoomVersionId) -> Result<String,
|
|||||||
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
|
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
|
||||||
/// homeserver, e.g. "example.com".
|
/// homeserver, e.g. "example.com".
|
||||||
/// * key_pair: A cryptographic key pair used to sign the event.
|
/// * key_pair: A cryptographic key pair used to sign the event.
|
||||||
/// * value: A JSON object to be hashed and signed according to the Matrix specification.
|
/// * object: A JSON object to be hashed and signed according to the Matrix specification.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if:
|
/// Returns an error if:
|
||||||
///
|
///
|
||||||
/// * `value` is not a JSON object.
|
/// * `object` contains a field called `content` that is not a JSON object.
|
||||||
/// * `value` contains a field called `content` that is not a JSON object.
|
/// * `object` contains a field called `hashes` that is not a JSON object.
|
||||||
/// * `value` contains a field called `hashes` that is not a JSON object.
|
/// * `object` contains a field called `signatures` that is not a JSON object.
|
||||||
/// * `value` contains a field called `signatures` that is not a JSON object.
|
/// * `object` is missing the `type` field or the field is not a JSON string.
|
||||||
/// * `value` is missing the `type` field or the field is not a JSON string.
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@ -434,7 +411,7 @@ pub fn reference_hash(value: &Value, version: &RoomVersionId) -> Result<String,
|
|||||||
/// ).unwrap();
|
/// ).unwrap();
|
||||||
///
|
///
|
||||||
/// // Deserialize an event from JSON.
|
/// // Deserialize an event from JSON.
|
||||||
/// let mut value = serde_json::from_str(
|
/// let mut object = serde_json::from_str(
|
||||||
/// r#"{
|
/// r#"{
|
||||||
/// "room_id": "!x:domain",
|
/// "room_id": "!x:domain",
|
||||||
/// "sender": "@a:domain",
|
/// "sender": "@a:domain",
|
||||||
@ -454,7 +431,7 @@ pub fn reference_hash(value: &Value, version: &RoomVersionId) -> Result<String,
|
|||||||
/// ).unwrap();
|
/// ).unwrap();
|
||||||
///
|
///
|
||||||
/// // Hash and sign the JSON with the key pair.
|
/// // Hash and sign the JSON with the key pair.
|
||||||
/// assert!(hash_and_sign_event("domain", &key_pair, &mut value, &RoomVersionId::Version1).is_ok());
|
/// assert!(hash_and_sign_event("domain", &key_pair, &mut object, &RoomVersionId::Version1).is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This will modify the JSON from the structure shown to a structure like this:
|
/// This will modify the JSON from the structure shown to a structure like this:
|
||||||
@ -488,38 +465,27 @@ pub fn reference_hash(value: &Value, version: &RoomVersionId) -> Result<String,
|
|||||||
pub fn hash_and_sign_event<K>(
|
pub fn hash_and_sign_event<K>(
|
||||||
entity_id: &str,
|
entity_id: &str,
|
||||||
key_pair: &K,
|
key_pair: &K,
|
||||||
value: &mut Value,
|
object: &mut JsonObject,
|
||||||
version: &RoomVersionId,
|
version: &RoomVersionId,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
K: KeyPair,
|
K: KeyPair,
|
||||||
{
|
{
|
||||||
let hash = content_hash(value)?;
|
let hash = content_hash(object);
|
||||||
|
|
||||||
// Limit the scope of the mutable borrow so `value` can be passed immutably to `redact` below.
|
|
||||||
{
|
|
||||||
let map = match value {
|
|
||||||
Value::Object(ref mut map) => map,
|
|
||||||
_ => return Err(Error::new("JSON value must be a JSON object")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let hashes_value =
|
let hashes_value =
|
||||||
map.entry("hashes").or_insert_with(|| Value::Object(Map::with_capacity(1)));
|
object.entry("hashes").or_insert_with(|| Value::Object(Map::with_capacity(1)));
|
||||||
|
|
||||||
match hashes_value.as_object_mut() {
|
match hashes_value.as_object_mut() {
|
||||||
Some(hashes) => hashes.insert("sha256".into(), Value::String(hash)),
|
Some(hashes) => hashes.insert("sha256".into(), Value::String(hash)),
|
||||||
None => return Err(Error::new("field `hashes` must be a JSON object")),
|
None => return Err(Error::new("field `hashes` must be a JSON object")),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let mut redacted = redact(value, version)?;
|
let mut redacted = redact(object, version)?;
|
||||||
|
|
||||||
sign_json(entity_id, key_pair, &mut redacted)?;
|
sign_json(entity_id, key_pair, &mut redacted)?;
|
||||||
|
|
||||||
// Safe to unwrap because we did this exact check at the beginning of the function.
|
object.insert("signatures".into(), redacted["signatures"].take());
|
||||||
let map = value.as_object_mut().unwrap();
|
|
||||||
|
|
||||||
map.insert("signatures".into(), redacted["signatures"].take());
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -541,7 +507,7 @@ where
|
|||||||
/// Generally, entity identifiers are server names—the host/IP/port of a homeserver (e.g.
|
/// Generally, entity identifiers are server names—the host/IP/port of a homeserver (e.g.
|
||||||
/// "example.com") for which a signature must be verified. Key identifiers for each server (e.g.
|
/// "example.com") for which a signature must be verified. Key identifiers for each server (e.g.
|
||||||
/// "ed25519:1") then map to their respective public keys.
|
/// "ed25519:1") then map to their respective public keys.
|
||||||
/// * value: The `serde_json::Value` (JSON value) of the event that was signed.
|
/// * object: The JSON object of the event that was signed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@ -553,7 +519,7 @@ where
|
|||||||
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
|
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
|
||||||
///
|
///
|
||||||
/// // Deserialize an event from JSON.
|
/// // Deserialize an event from JSON.
|
||||||
/// let value = serde_json::from_str(
|
/// let object = serde_json::from_str(
|
||||||
/// r#"{
|
/// r#"{
|
||||||
/// "auth_events": [],
|
/// "auth_events": [],
|
||||||
/// "content": {},
|
/// "content": {},
|
||||||
@ -585,21 +551,16 @@ where
|
|||||||
/// public_key_map.insert("domain".into(), public_key_set);
|
/// public_key_map.insert("domain".into(), public_key_set);
|
||||||
///
|
///
|
||||||
/// // Verify at least one signature for each entity in `public_key_map`.
|
/// // Verify at least one signature for each entity in `public_key_map`.
|
||||||
/// assert!(verify_event(&public_key_map, &value, &RoomVersionId::Version6).is_ok());
|
/// assert!(verify_event(&public_key_map, &object, &RoomVersionId::Version6).is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn verify_event(
|
pub fn verify_event(
|
||||||
public_key_map: &PublicKeyMap,
|
public_key_map: &PublicKeyMap,
|
||||||
value: &Value,
|
object: &JsonObject,
|
||||||
version: &RoomVersionId,
|
version: &RoomVersionId,
|
||||||
) -> Result<Verified, Error> {
|
) -> Result<Verified, Error> {
|
||||||
let redacted = redact(value, version)?;
|
let redacted = redact(object, version)?;
|
||||||
|
|
||||||
let map = match redacted {
|
let hash = match object.get("hashes") {
|
||||||
Value::Object(ref map) => map,
|
|
||||||
_ => return Err(Error::new("JSON value must be a JSON object")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let hash = match map.get("hashes") {
|
|
||||||
Some(hashes_value) => match hashes_value.as_object() {
|
Some(hashes_value) => match hashes_value.as_object() {
|
||||||
Some(hashes) => match hashes.get("sha256") {
|
Some(hashes) => match hashes.get("sha256") {
|
||||||
Some(hash_value) => match hash_value.as_str() {
|
Some(hash_value) => match hash_value.as_str() {
|
||||||
@ -613,7 +574,7 @@ pub fn verify_event(
|
|||||||
None => return Err(Error::new("field `hashes` must be present")),
|
None => return Err(Error::new("field `hashes` must be present")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let signature_map: SignatureMap = match map.get("signatures") {
|
let signature_map: SignatureMap = match object.get("signatures") {
|
||||||
Some(signatures_value) => match signatures_value.as_object() {
|
Some(signatures_value) => match signatures_value.as_object() {
|
||||||
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
|
||||||
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
None => return Err(Error::new("field `signatures` must be a JSON object")),
|
||||||
@ -661,7 +622,7 @@ pub fn verify_event(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let canonical_json = from_str(&canonical_json(&redacted)?)?;
|
let canonical_json = from_str(&canonical_json(&redacted))?;
|
||||||
|
|
||||||
let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?;
|
let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?;
|
||||||
|
|
||||||
@ -670,7 +631,7 @@ pub fn verify_event(
|
|||||||
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, &canonical_json)?;
|
verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, &canonical_json)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let calculated_hash = content_hash(value)?;
|
let calculated_hash = content_hash(object);
|
||||||
|
|
||||||
if hash == calculated_hash {
|
if hash == calculated_hash {
|
||||||
Ok(Verified::All)
|
Ok(Verified::All)
|
||||||
@ -681,22 +642,14 @@ pub fn verify_event(
|
|||||||
|
|
||||||
/// Internal implementation detail of the canonical JSON algorithm. Allows customization of the
|
/// Internal implementation detail of the canonical JSON algorithm. Allows customization of the
|
||||||
/// fields that will be removed before serializing.
|
/// fields that will be removed before serializing.
|
||||||
fn canonical_json_with_fields_to_remove(value: &Value, fields: &[&str]) -> Result<String, Error> {
|
fn canonical_json_with_fields_to_remove(object: &JsonObject, fields: &[&str]) -> String {
|
||||||
if !value.is_object() {
|
let mut owned_object = object.clone();
|
||||||
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 {
|
for field in fields {
|
||||||
object.remove(*field);
|
owned_object.remove(*field);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to_string(&owned_value).map_err(Error::from)
|
to_string(&owned_object).expect("JSON object serialization to succeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redacts the JSON representation of an event using the rules specified in the Matrix
|
/// Redacts the JSON representation of an event using the rules specified in the Matrix
|
||||||
@ -707,29 +660,22 @@ fn canonical_json_with_fields_to_remove(value: &Value, fields: &[&str]) -> Resul
|
|||||||
/// Redaction is also suggested when a verifying an event with `verify_event` returns
|
/// Redaction is also suggested when a verifying an event with `verify_event` returns
|
||||||
/// `Verified::Signatures`. See the documentation for `Verified` for details.
|
/// `Verified::Signatures`. See the documentation for `Verified` for details.
|
||||||
///
|
///
|
||||||
/// Returns a new `serde_json::Value` with all applicable fields redacted.
|
/// Returns a new JSON object with all applicable fields redacted.
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// * value: A JSON object to redact.
|
/// * object: A JSON object to redact.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if:
|
/// Returns an error if:
|
||||||
///
|
///
|
||||||
/// * `value` is not a JSON object.
|
/// * `object` contains a field called `content` that is not a JSON object.
|
||||||
/// * `value` contains a field called `content` that is not a JSON object.
|
/// * `object` contains a field called `hashes` that is not a JSON object.
|
||||||
/// * `value` contains a field called `hashes` that is not a JSON object.
|
/// * `object` contains a field called `signatures` that is not a JSON object.
|
||||||
/// * `value` contains a field called `signatures` that is not a JSON object.
|
/// * `object` is missing the `type` field or the field is not a JSON string.
|
||||||
/// * `value` is missing the `type` field or the field is not a JSON string.
|
pub fn redact(object: &JsonObject, version: &RoomVersionId) -> Result<JsonObject, Error> {
|
||||||
pub fn redact(value: &Value, version: &RoomVersionId) -> Result<Value, Error> {
|
let mut event = object.clone();
|
||||||
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") {
|
let event_type_value = match event.get("type") {
|
||||||
Some(event_type_value) => event_type_value,
|
Some(event_type_value) => event_type_value,
|
||||||
@ -758,7 +704,7 @@ pub fn redact(value: &Value, version: &RoomVersionId) -> Result<Value, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let max_values = cmp::max(event.len(), ALLOWED_KEYS.len());
|
let max_values = cmp::max(event.len(), ALLOWED_KEYS.len());
|
||||||
let mut old_event = mem::replace(event, serde_json::Map::with_capacity(max_values));
|
let mut old_event = mem::replace(&mut event, serde_json::Map::with_capacity(max_values));
|
||||||
|
|
||||||
for &key in ALLOWED_KEYS {
|
for &key in ALLOWED_KEYS {
|
||||||
if let Some(value) = old_event.remove(key) {
|
if let Some(value) = old_event.remove(key) {
|
||||||
@ -766,7 +712,7 @@ pub fn redact(value: &Value, version: &RoomVersionId) -> Result<Value, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(owned_value)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -799,6 +745,6 @@ mod tests {
|
|||||||
|
|
||||||
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 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}}"#;
|
||||||
|
|
||||||
assert_eq!(canonical_json(&data).unwrap(), canonical);
|
assert_eq!(canonical_json(data.as_object().unwrap()), canonical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ use std::{
|
|||||||
|
|
||||||
pub use functions::{
|
pub use functions::{
|
||||||
canonical_json, content_hash, hash_and_sign_event, redact, reference_hash, sign_json,
|
canonical_json, content_hash, hash_and_sign_event, redact, reference_hash, sign_json,
|
||||||
verify_event, verify_json,
|
verify_event, verify_json, JsonObject,
|
||||||
};
|
};
|
||||||
pub use keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet};
|
pub use keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet};
|
||||||
pub use signatures::Signature;
|
pub use signatures::Signature;
|
||||||
@ -177,7 +177,7 @@ mod test {
|
|||||||
use base64::{decode_config, STANDARD_NO_PAD};
|
use base64::{decode_config, STANDARD_NO_PAD};
|
||||||
use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _};
|
use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _};
|
||||||
use ruma_identifiers::RoomVersionId;
|
use ruma_identifiers::RoomVersionId;
|
||||||
use serde_json::{from_str, json, to_string, to_value, Value};
|
use serde_json::{from_str, json, to_string, to_value};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair,
|
canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair,
|
||||||
@ -202,9 +202,8 @@ mod test {
|
|||||||
|
|
||||||
/// Convenience for converting a string of JSON into its canonical form.
|
/// Convenience for converting a string of JSON into its canonical form.
|
||||||
fn test_canonical_json(input: &str) -> String {
|
fn test_canonical_json(input: &str) -> String {
|
||||||
let value = from_str::<Value>(input).unwrap();
|
let object = from_str(input).unwrap();
|
||||||
|
canonical_json(&object)
|
||||||
canonical_json(&value).unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -346,7 +345,7 @@ mod test {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut alpha_value = to_value(alpha).expect("alpha should serialize");
|
let mut alpha_value = to_value(alpha).expect("alpha should serialize");
|
||||||
sign_json("domain", &key_pair, &mut alpha_value).unwrap();
|
sign_json("domain", &key_pair, alpha_value.as_object_mut().unwrap()).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_string(&alpha_value).unwrap(),
|
to_string(&alpha_value).unwrap(),
|
||||||
@ -355,7 +354,7 @@ mod test {
|
|||||||
|
|
||||||
let mut reverse_alpha_value =
|
let mut reverse_alpha_value =
|
||||||
to_value(reverse_alpha).expect("reverse_alpha should serialize");
|
to_value(reverse_alpha).expect("reverse_alpha should serialize");
|
||||||
sign_json("domain", &key_pair, &mut reverse_alpha_value).unwrap();
|
sign_json("domain", &key_pair, reverse_alpha_value.as_object_mut().unwrap()).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_string(&reverse_alpha_value).unwrap(),
|
to_string(&reverse_alpha_value).unwrap(),
|
||||||
@ -422,11 +421,11 @@ mod test {
|
|||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
let mut value = from_str::<Value>(json).unwrap();
|
let mut object = from_str(json).unwrap();
|
||||||
hash_and_sign_event("domain", &key_pair, &mut value, &RoomVersionId::Version5).unwrap();
|
hash_and_sign_event("domain", &key_pair, &mut object, &RoomVersionId::Version5).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_string(&value).unwrap(),
|
to_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}}"#
|
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}}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -455,11 +454,11 @@ mod test {
|
|||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
let mut value = from_str::<Value>(json).unwrap();
|
let mut object = from_str(json).unwrap();
|
||||||
hash_and_sign_event("domain", &key_pair, &mut value, &RoomVersionId::Version5).unwrap();
|
hash_and_sign_event("domain", &key_pair, &mut object, &RoomVersionId::Version5).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_string(&value).unwrap(),
|
to_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}}"#
|
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}}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user