Add 'ruma-signatures/' from commit '1ca545cba8dfd43e0fc8e3c18e1311fb73390a97'

git-subtree-dir: ruma-signatures
git-subtree-mainline: 50b74e2b7633f0c4dddb9a71313dc198c58a3074
git-subtree-split: 1ca545cba8dfd43e0fc8e3c18e1311fb73390a97
This commit is contained in:
Jonas Platte 2020-06-05 17:20:12 +02:00
commit 916586e56a
12 changed files with 1724 additions and 0 deletions

View File

@ -0,0 +1,27 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-signatures
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install beta --profile minimal -c rustfmt -c clippy
rustup default beta
- test: |
cd ruma-signatures
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))

View File

@ -0,0 +1,32 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-signatures
tasks:
- rustup: |
rustup toolchain install nightly --profile minimal
rustup default nightly
# Try installing rustfmt & clippy for nightly, but don't fail the build
# if they are not available
rustup component add rustfmt || true
rustup component add clippy || true
- test: |
cd ruma-signatures
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
if ( rustup component list | grep -q rustfmt ); then
cargo fmt -- --check
fi
fmt_exit=$?
if ( rustup component list | grep -q clippy ); then
cargo clippy --all-targets --all-features -- -D warnings
fi
clippy_exit=$?
exit $(( $fmt_exit || $clippy_exit ))

View File

@ -0,0 +1,29 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-signatures
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install stable --profile minimal -c rustfmt -c clippy
rustup default stable
- test: |
cd ruma-signatures
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))
# TODO: Add audit task once cargo-audit binary releases are available.
# See https://github.com/RustSec/cargo-audit/issues/66

2
ruma-signatures/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock
target

View File

@ -0,0 +1,19 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings", "cryptography"]
description = "Digital signatures according to the Matrix specification."
documentation = "https://docs.rs/ruma-signatures"
edition = "2018"
homepage = "https://github.com/ruma/ruma-signatures"
keywords = ["matrix", "chat", "messaging", "ruma", "cryptography"]
license = "MIT"
name = "ruma-signatures"
readme = "README.md"
repository = "https://github.com/ruma/ruma-signatures"
version = "0.6.0-dev.1"
[dependencies]
base64 = "0.12.0"
ring = "0.16.12"
serde_json = "1.0.50"
untrusted = "0.7.0"

19
ruma-signatures/LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2015 Jimmy Cuadra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

19
ruma-signatures/README.md Normal file
View File

@ -0,0 +1,19 @@
# ruma-signatures
[![crates.io page](https://img.shields.io/crates/v/ruma-signatures.svg)](https://crates.io/crates/ruma-signatures)
[![docs.rs page](https://docs.rs/ruma-signatures/badge.svg)](https://docs.rs/ruma-signatures/)
![license: MIT](https://img.shields.io/crates/l/ruma-signatures.svg)
ruma-signatures provides functionality for creating digital signatures according to the [Matrix](https://matrix.org/) specification.
## Documentation
ruma-signatures has [comprehensive documentation](https://docs.rs/ruma-signatures) available on docs.rs.
## Minimum Rust version
ruma-client-api is only guaranteed to work on the latest stable version of Rust.
This support policy is inherited from the dependency on [ring][].
[ring]: https://github.com/briansmith/ring/

View File

@ -0,0 +1,764 @@
//! Functions for signing and verifying JSON and events.
use std::collections::HashMap;
use base64::{decode_config, encode_config, STANDARD_NO_PAD};
use ring::digest::{digest, SHA256};
use serde_json::{from_str, from_value, map::Map, to_string, to_value, Value};
use crate::{
keys::{KeyPair, PublicKeyMap},
signatures::SignatureMap,
split_id,
verification::{Ed25519Verifier, Verified, 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 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.
///
/// # Parameters
///
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
/// homeserver, e.g. "example.com".
/// * key_pair: A cryptographic key pair used to sign the JSON.
/// * value: A JSON object to sign according and append a signature to.
///
/// # Errors
///
/// Returns an error if:
///
/// * `value` is not a JSON object.
/// * `value` contains a field called `signatures` that is not a JSON object.
///
/// # Examples
///
/// A homeserver signs JSON with a key pair:
///
/// ```rust
/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ";
///
/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap();
///
/// // Create an Ed25519 key pair.
/// let key_pair = ruma_signatures::Ed25519KeyPair::new(
/// &document,
/// "1".to_string(), // The "version" of the key.
/// ).unwrap();
///
/// // Deserialize some JSON.
/// let mut value = serde_json::from_str("{}").unwrap();
///
/// // Sign the JSON with the key pair.
/// assert!(ruma_signatures::sign_json("domain", &key_pair, &mut value).is_ok());
/// ```
///
/// This will modify the JSON from an empty object to a structure like this:
///
/// ```json
/// {
/// "signatures": {
/// "domain": {
/// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"
/// }
/// }
/// }
/// ```
pub fn sign_json<K>(entity_id: &str, key_pair: &K, value: &mut Value) -> Result<(), Error>
where
K: KeyPair,
{
let mut signature_map;
let maybe_unsigned;
// Pull `signatures` and `unsigned` out of the object, and limit the scope of the mutable
// borrow of `value` so we can call `to_string` with it below.
{
let map = match value {
Value::Object(ref mut map) => map,
_ => return Err(Error::new("JSON value must be a JSON object")),
};
signature_map = match map.remove("signatures") {
Some(signatures_value) => match signatures_value.as_object() {
Some(signatures) => from_value(Value::Object(signatures.clone()))?,
None => return Err(Error::new("field `signatures` must be a JSON object")),
},
None => HashMap::with_capacity(1),
};
maybe_unsigned = map.remove("unsigned");
}
// Get the canonical JSON.
let json = to_string(&value)?;
// Sign the canonical JSON.
let signature = key_pair.sign(json.as_bytes());
// Insert the new signature in the map we pulled out (or created) previously.
let signature_set = signature_map
.entry(entity_id.to_string())
.or_insert_with(|| HashMap::with_capacity(1));
signature_set.insert(signature.id(), signature.base64());
// Safe to unwrap because we did this exact check at the beginning of the function.
let map = value.as_object_mut().unwrap();
// Put `signatures` and `unsigned` back in.
map.insert("signatures".to_string(), to_value(signature_map)?);
if let Some(unsigned) = maybe_unsigned {
map.insert("unsigned".to_string(), to_value(unsigned)?);
}
Ok(())
}
/// Converts a JSON object into the
/// [canonical](https://matrix.org/docs/spec/appendices#canonical-json) string form.
///
/// # Parameters
///
/// * value: The `serde_json::Value` (JSON value) to convert.
///
/// # Errors
///
/// Returns an error if the provided JSON value is not a JSON object.
///
/// # Examples
///
/// ```rust
/// let input =
/// r#"{
/// "本": 2,
/// "日": 1
/// }"#;
///
/// let value = serde_json::from_str::<serde_json::Value>(input).unwrap();
///
/// let canonical = ruma_signatures::canonical_json(&value).unwrap();
///
/// assert_eq!(canonical, r#"{"日":1,"本":2}"#);
/// ```
pub fn canonical_json(value: &Value) -> Result<String, Error> {
canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE)
}
/// Uses a set of public keys to verify a signed JSON object.
///
/// # Parameters
///
/// * public_key_map: A map from entity identifiers to a map from key identifiers to public keys.
/// 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.
/// "ed25519:1") then map to their respective public keys.
/// * value: The `serde_json::Value` (JSON value) that was signed.
///
/// # Errors
///
/// Returns an error if verification fails.
///
/// # Examples
///
/// ```rust
/// use std::collections::HashMap;
///
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
///
/// // Deserialize the signed JSON.
/// let value = serde_json::from_str(
/// r#"{
/// "signatures": {
/// "domain": {
/// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"
/// }
/// }
/// }"#
/// ).unwrap();
///
/// // Create the `PublicKeyMap` that will inform `verify_json` which signatures to verify.
/// let mut public_key_set = HashMap::new();
/// public_key_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string());
/// let mut public_key_map = HashMap::new();
/// public_key_map.insert("domain".to_string(), public_key_set);
///
/// // Verify at least one signature for each entity in `public_key_map`.
/// assert!(ruma_signatures::verify_json(&public_key_map, &value).is_ok());
/// ```
pub fn verify_json(public_key_map: &PublicKeyMap, value: &Value) -> Result<(), Error> {
let map = match value {
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) => from_value(Value::Object(signatures.clone()))?,
None => 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(set) => set,
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 {
// Since only ed25519 is supported right now, we don't actually need to check what the
// algorithm is. If it split successfully, it's ed25519.
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(signature) => signature,
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, value)?;
}
Ok(())
}
/// Uses a public key to verify a signed JSON object.
///
/// # Parameters
///
/// * 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.
/// * signature: The raw bytes of the signature.
/// * value: The `serde_json::Value` (JSON value) that was signed.
///
/// # Errors
///
/// Returns an error if:
///
/// * The provided JSON value is not a JSON object.
/// * Verification fails.
fn verify_json_with<V>(
verifier: &V,
public_key: &[u8],
signature: &[u8],
value: &Value,
) -> Result<(), Error>
where
V: Verifier,
{
verifier.verify_json(public_key, signature, canonical_json(value)?.as_bytes())
}
/// Creates a *content hash* for the JSON representation of an event.
///
/// Returns the hash as a Base64-encoded string, using the standard character set, without padding.
///
/// 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.
///
/// # Parameters
///
/// value: A JSON object to generate a content hash for.
///
/// # Errors
///
/// 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());
Ok(encode_config(&hash, STANDARD_NO_PAD))
}
/// Creates a *reference hash* for the JSON representation of an event.
///
/// Returns the hash as a Base64-encoded string, using the standard character set, without padding.
///
/// The reference hash of an event covers the essential fields of an event, including content
/// hashes. It is used to generate event identifiers and is described in the Matrix server-server
/// specification.
///
/// # Parameters
///
/// value: A JSON object to generate a reference hash for.
///
/// # Errors
///
/// Returns an error if the provided JSON value is not a JSON object.
pub fn reference_hash(value: &Value) -> Result<String, Error> {
let redacted_value = redact(value)?;
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, STANDARD_NO_PAD))
}
/// Hashes and signs the JSON representation of an event and adds the hash and signature to objects
/// under the keys `hashes` and `signatures`, respectively.
///
/// If `hashes` and/or `signatures` are already present, the new data will be appended to the
/// existing data.
///
/// # Parameters
///
/// * entity_id: The identifier of the entity creating the signature. Generally this means a
/// homeserver, e.g. "example.com".
/// * 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.
///
/// # Errors
///
/// Returns an error if:
///
/// * `value` is not a JSON object.
/// * `value` contains a field called `content` that is not a JSON object.
/// * `value` contains a field called `hashes` that is not a JSON object.
/// * `value` contains a field called `signatures` that is not a JSON object.
/// * `value` is missing the `type` field or the field is not a JSON string.
///
/// # Examples
///
/// ```rust
/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ";
///
/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap();
///
/// // Create an Ed25519 key pair.
/// let key_pair = ruma_signatures::Ed25519KeyPair::new(
/// &document,
/// "1".to_string(), // The "version" of the key.
/// ).unwrap();
///
/// // Deserialize an event from JSON.
/// let mut value = serde_json::from_str(
/// 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
/// }
/// }"#
/// ).unwrap();
///
/// // Hash and sign the JSON with the key pair.
/// assert!(ruma_signatures::hash_and_sign_event("domain", &key_pair, &mut value).is_ok());
/// ```
///
/// This will modify the JSON from the structure shown to a structure like this:
///
/// ```json
/// {
/// "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": "X",
/// "unsigned": {
/// "age_ts": 1000000
/// }
/// }
/// ```
///
/// Notice the addition of `hashes` and `signatures`.
pub fn hash_and_sign_event<K>(entity_id: &str, key_pair: &K, value: &mut Value) -> Result<(), Error>
where
K: KeyPair,
{
let hash = content_hash(value)?;
// 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 = map
.entry("hashes")
.or_insert_with(|| Value::Object(Map::with_capacity(1)));
match hashes_value.as_object_mut() {
Some(hashes) => hashes.insert("sha256".to_string(), Value::String(hash)),
None => return Err(Error::new("field `hashes` must be a JSON object")),
};
}
let mut redacted = redact(value)?;
sign_json(entity_id, key_pair, &mut redacted)?;
// Safe to unwrap because we did this exact check at the beginning of the function.
let map = value.as_object_mut().unwrap();
map.insert("signatures".to_string(), redacted["signatures"].take());
Ok(())
}
/// Uses a set of public keys to verify a signed JSON representation of an event.
///
/// Some room versions may require signatures from multiple homeservers, so this function takes a
/// map from servers to sets of public keys. For each homeserver present in the map, this function
/// will require a valid signature. All known public keys for a homeserver should be provided. The
/// first one found on the given event will be used.
///
/// If the `Ok` variant is returned by this function, it will contain a `Verified` value which
/// distinguishes an event with valid signatures and a matching content hash with an event with
/// only valid signatures. See the documetation for `Verified` for details.
///
/// # Parameters
///
/// * public_key_map: A map from entity identifiers to a map from key identifiers to public keys.
/// 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.
/// "ed25519:1") then map to their respective public keys.
/// * value: The `serde_json::Value` (JSON value) of the event that was signed.
///
/// # Examples
///
/// ```rust
/// use std::collections::HashMap;
///
/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
///
/// // Deserialize an event from JSON.
/// let value = 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": "X",
/// "unsigned": {
/// "age_ts": 1000000
/// }
/// }"#
/// ).unwrap();
///
/// // Create the `PublicKeyMap` that will inform `verify_json` which signatures to verify.
/// let mut public_key_set = HashMap::new();
/// public_key_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string());
/// let mut public_key_map = HashMap::new();
/// public_key_map.insert("domain".to_string(), public_key_set);
///
/// // Verify at least one signature for each entity in `public_key_map`.
/// assert!(ruma_signatures::verify_event(&public_key_map, &value).is_ok());
/// ```
pub fn verify_event(public_key_map: &PublicKeyMap, value: &Value) -> Result<Verified, Error> {
let redacted = redact(value)?;
let map = match redacted {
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) => match hashes.get("sha256") {
Some(hash_value) => match hash_value.as_str() {
Some(hash) => hash,
None => return Err(Error::new("sha256 hash must be a JSON string")),
},
None => return Err(Error::new("field `hashes` must be a JSON object")),
},
None => return Err(Error::new("event missing sha256 hash")),
},
None => return Err(Error::new("field `hashes` must be present")),
};
let signature_map: SignatureMap = match map.get("signatures") {
Some(signatures_value) => match signatures_value.as_object() {
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("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(set) => set,
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 {
// Since only ed25519 is supported right now, we don't actually need to check what the
// algorithm is. If it split successfully, it's ed25519.
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(signature) => signature,
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 canonical_json = from_str(&canonical_json(&redacted)?)?;
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(value)?;
if hash == calculated_hash {
Ok(Verified::All)
} else {
Ok(Verified::Signatures)
}
}
/// Internal implementation detail of the canonical JSON algorithm. Allows customization of the
/// fields that will be removed before serializing.
fn canonical_json_with_fields_to_remove(value: &Value, fields: &[&str]) -> Result<String, Error> {
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::from)
}
/// Redacts 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.
///
/// Redaction is also suggested when a verifying an event with `verify_event` returns
/// `Verified::Signatures`. See the documentation for `Verified` for details.
///
/// Returns a new `serde_json::Value` with all applicable fields redacted.
///
/// # Parameters
///
/// * value: A JSON object to redact.
///
/// # Errors
///
/// Returns an error if:
///
/// * `value` is not a JSON object.
/// * `value` contains a field called `content` that is not a JSON object.
/// * `value` contains a field called `hashes` that is not a JSON object.
/// * `value` contains a field called `signatures` that is not a JSON object.
/// * `value` is missing the `type` field or the field is not a JSON string.
pub fn redact(value: &Value) -> Result<Value, Error> {
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") {
let map = match content_value {
Value::Object(ref mut map) => map,
_ => {
return Err(Error::new(
"field `content` in JSON value must be a JSON object",
))
}
};
for key in map.clone().keys() {
match event_type.as_ref() {
"m.room.member" if key != "membership" => map.remove(key),
"m.room.create" if key != "creator" => map.remove(key),
"m.room.join_rules" if key != "join_rules" => map.remove(key),
"m.room.power_levels" if !ALLOWED_POWER_LEVELS_KEYS.contains(&key.as_ref()) => {
map.remove(key)
}
"m.room.aliases" if key != "aliases" => map.remove(key),
"m.room.history_visibility" if key != "history_visibility" => map.remove(key),
_ => map.remove(key),
};
}
}
for key in event.clone().keys() {
if !ALLOWED_KEYS.contains(&key.as_ref()) {
event.remove(key);
}
}
Ok(owned_value)
}

117
ruma-signatures/src/keys.rs Normal file
View File

@ -0,0 +1,117 @@
//! Public and private key pairs.
use std::{
collections::HashMap,
fmt::{Debug, Formatter, Result as FmtResult},
};
use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _};
use crate::{signatures::Signature, Algorithm, Error};
/// A cryptographic key pair for digitally signing data.
pub trait KeyPair: Sized {
/// Signs a JSON object.
///
/// # Parameters
///
/// * message: An arbitrary series of bytes to sign.
fn sign(&self, message: &[u8]) -> Signature;
}
/// An Ed25519 key pair.
pub struct Ed25519KeyPair {
/// Ring's Keypair type
keypair: RingEd25519KeyPair,
/// The version of the key pair.
version: String,
}
impl Ed25519KeyPair {
/// Initializes a new key pair.
///
/// # Parameters
///
/// * document: PKCS8-formatted bytes containing the private & public keys.
/// * version: The "version" of the key used for this signature.
/// Versions are used as an identifier to distinguish signatures generated from different keys
/// but using the same algorithm on the same homeserver.
///
/// # Errors
///
/// Returns an error if the public and private keys provided are invalid for the implementing
/// algorithm.
pub fn new(document: &[u8], version: String) -> Result<Self, Error> {
let keypair = RingEd25519KeyPair::from_pkcs8(document)
.map_err(|error| Error::new(error.to_string()))?;
Ok(Self { keypair, version })
}
/// Generates a new key pair.
///
/// # Returns
///
/// Returns a Vec<u8> representing a pkcs8-encoded private/public keypair
///
/// # Errors
///
/// Returns an error if the generation failed.
pub fn generate() -> Result<Vec<u8>, Error> {
let document = RingEd25519KeyPair::generate_pkcs8(&ring::rand::SystemRandom::new())
.map_err(|e| Error::new(e.to_string()))?;
Ok(document.as_ref().to_vec())
}
/// Returns the version string for this keypair.
pub fn version(&self) -> &str {
&self.version
}
/// Returns the public key.
pub fn public_key(&self) -> &[u8] {
self.keypair.public_key().as_ref()
}
}
impl KeyPair for Ed25519KeyPair {
fn sign(&self, message: &[u8]) -> Signature {
Signature {
algorithm: Algorithm::Ed25519,
signature: self.keypair.sign(message).as_ref().to_vec(),
version: self.version.clone(),
}
}
}
impl Debug for Ed25519KeyPair {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter
.debug_struct("Ed25519KeyPair")
.field("public_key", &self.keypair.public_key())
.field("version", &self.version)
.finish()
}
}
/// A map from entity names to sets of public keys for that entity.
///
/// "Entity" is generally a homeserver, e.g. "example.com".
pub type PublicKeyMap = HashMap<String, PublicKeySet>;
/// A set of public keys for a single homeserver.
///
/// This is represented as a map from key ID to Base64-encoded signature.
pub type PublicKeySet = HashMap<String, String>;
#[cfg(test)]
mod tests {
use super::Ed25519KeyPair;
#[test]
fn generate_key() {
Ed25519KeyPair::generate().unwrap();
}
}

507
ruma-signatures/src/lib.rs Normal file
View File

@ -0,0 +1,507 @@
//! Crate `ruma_signatures` implements digital signatures according to the
//! [Matrix](https://matrix.org/) specification.
//!
//! Digital signatures are used by Matrix homeservers to verify the authenticity of events in the
//! Matrix system, as well as requests between homeservers for federation. Each homeserver has one
//! or more signing key pairs (sometimes referred to as "verify keys") which it uses to sign all
//! events and federation requests. Matrix clients and other Matrix homeservers can ask the
//! homeserver for its public keys and use those keys to verify the signed data.
//!
//! Each signing key pair has an identifier, which consists of the name of the digital signature
//! algorithm it uses and a "version" string, separated by a colon. The version is an arbitrary
//! identifier used to distinguish key pairs using the same algorithm from the same homeserver.
//!
//! Arbitrary JSON objects can be signed as well as JSON representations of Matrix events. In both
//! cases, the signatures are stored within the JSON object itself under a `signatures` key. Events
//! are also required to contain hashes of their content, which are similarly stored within the
//! hashed JSON object under a `hashes` key.
//!
//! In JSON representations, both signatures and hashes appear as Base64-encoded strings, using the
//! standard character set, without padding.
//!
//! # Signing and hashing
//!
//! To sign an arbitrary JSON object, use the `sign_json` function. See the documentation of this
//! function for more details and a full example of use.
//!
//! Signing an event uses a more complicated process than signing arbitrary JSON, because events can
//! be redacted, and signatures need to remain valid even if data is removed from an event later.
//! Homeservers are required to generate hashes of event contents as well as signing events before
//! exchanging them with other homeservers. Although the algorithm for hashing and signing an event
//! is more complicated than for signing arbitrary JSON, the interface to a user of ruma-signatures
//! is the same. To hash and sign an event, use the `hash_and_sign_event` function. See the
//! documentation of this function for more details and a full example of use.
//!
//! # Verifying signatures and hashes
//!
//! When a homeserver receives data from another homeserver via the federation, it's necessary to
//! verify the authenticity and integrity of the data by verifying their signatures.
//!
//! To verify a signature on arbitrary JSON, use the `verify_json` function. To verify the
//! signatures and hashes on an event, use the `verify_event` function. See the documentation for
//! these respective functions for more details and full examples of use.
#![warn(rust_2018_idioms)]
#![deny(
missing_copy_implementations,
missing_debug_implementations,
missing_docs
)]
use std::{
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult},
};
pub use functions::{
canonical_json, content_hash, hash_and_sign_event, redact, reference_hash, sign_json,
verify_event, verify_json,
};
pub use keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet};
pub use signatures::Signature;
mod functions;
mod keys;
mod signatures;
mod verification;
/// An error produced when ruma-signatures operations fail.
#[derive(Clone, Debug, PartialEq)]
pub struct Error {
/// A human-readable description of the error.
message: String,
}
impl Error {
/// Creates a new error.
///
/// # Parameters
///
/// * message: The error message.
pub(crate) fn new<T>(message: T) -> Self
where
T: Into<String>,
{
Self {
message: message.into(),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
&self.message
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.message)
}
}
impl From<base64::DecodeError> for Error {
fn from(error: base64::DecodeError) -> Self {
Self::new(error.to_string())
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Self::new(error.to_string())
}
}
/// The algorithm used for signing data.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Algorithm {
/// The Ed25519 digital signature algorithm.
Ed25519,
}
impl Display for Algorithm {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let name = match *self {
Self::Ed25519 => "ed25519",
};
write!(f, "{}", name)
}
}
/// An error when trying to extract the algorithm and version from a key identifier.
#[derive(Clone, Debug, PartialEq)]
enum SplitError<'a> {
/// The signature's ID does not have exactly two components separated by a colon.
InvalidLength(usize),
/// The signature's ID contains invalid characters in its version.
InvalidVersion(&'a str),
/// The signature uses an unknown algorithm.
UnknownAlgorithm(&'a str),
}
/// Extract the algorithm and version from a key identifier.
fn split_id(id: &str) -> Result<(Algorithm, String), SplitError<'_>> {
/// The length of a valid signature ID.
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(SplitError::InvalidLength(signature_id_length));
}
let version = signature_id[1];
let invalid_character_index = version.find(|ch| {
!((ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9')
|| ch == '_')
});
if invalid_character_index.is_some() {
return Err(SplitError::InvalidVersion(version));
}
let algorithm_input = signature_id[0];
let algorithm = match algorithm_input {
"ed25519" => Algorithm::Ed25519,
algorithm => return Err(SplitError::UnknownAlgorithm(algorithm)),
};
Ok((algorithm, signature_id[1].to_string()))
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use base64::{decode_config, STANDARD_NO_PAD};
use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _};
use serde_json::{from_str, json, to_string, to_value, Value};
use super::{
canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair,
};
const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ";
/// Convenience method for getting the public key as a string
fn public_key_string() -> String {
base64::encode_config(
&RingEd25519KeyPair::from_pkcs8(
&base64::decode_config(PKCS8, STANDARD_NO_PAD).unwrap(),
)
.unwrap()
.public_key(),
STANDARD_NO_PAD,
)
}
/// Convenience for converting a string of JSON into its canonical form.
fn test_canonical_json(input: &str) -> String {
let value = from_str::<Value>(input).unwrap();
canonical_json(&value).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::new(
decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(),
"1".to_string(),
)
.unwrap();
let mut value = from_str("{}").unwrap();
sign_json("domain", &key_pair, &mut value).unwrap();
assert_eq!(
to_string(&value).unwrap(),
r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#
);
}
#[test]
fn verify_empty_json() {
let value = from_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap();
let mut signature_set = HashMap::new();
signature_set.insert("ed25519:1".to_string(), public_key_string());
let mut public_key_map = HashMap::new();
public_key_map.insert("domain".to_string(), signature_set);
assert!(verify_json(&public_key_map, &value).is_ok());
}
#[test]
fn sign_minimal_json() {
let key_pair = Ed25519KeyPair::new(
decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(),
"1".to_string(),
)
.unwrap();
let alpha = json!({
"one": 1,
"two": "Two",
});
let reverse_alpha = json!({
"two": "Two",
"one": 1,
});
let mut alpha_value = to_value(alpha).expect("alpha should serialize");
sign_json("domain", &key_pair, &mut alpha_value).unwrap();
assert_eq!(
to_string(&alpha_value).unwrap(),
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
);
let mut reverse_alpha_value =
to_value(reverse_alpha).expect("reverse_alpha should serialize");
sign_json("domain", &key_pair, &mut reverse_alpha_value).unwrap();
assert_eq!(
to_string(&reverse_alpha_value).unwrap(),
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
);
}
#[test]
fn verify_minimal_json() {
let value = from_str(
r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"#
).unwrap();
let mut signature_set = HashMap::new();
signature_set.insert("ed25519:1".to_string(), public_key_string());
let mut public_key_map = HashMap::new();
public_key_map.insert("domain".to_string(), signature_set);
assert!(verify_json(&public_key_map, &value).is_ok());
let reverse_value = from_str(
r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"#
).unwrap();
assert!(verify_json(&public_key_map, &reverse_value).is_ok());
}
#[test]
fn fail_verify_json() {
let value = from_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap();
let mut signature_set = HashMap::new();
signature_set.insert("ed25519:1".to_string(), public_key_string());
let mut public_key_map = HashMap::new();
public_key_map.insert("domain".to_string(), signature_set);
assert!(verify_json(&public_key_map, &value).is_err());
}
#[test]
fn sign_minimal_event() {
let key_pair = Ed25519KeyPair::new(
decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(),
"1".to_string(),
)
.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 value = from_str::<Value>(json).unwrap();
hash_and_sign_event("domain", &key_pair, &mut value).unwrap();
assert_eq!(
to_string(&value).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::new(
decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(),
"1".to_string(),
)
.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 value = from_str::<Value>(json).unwrap();
hash_and_sign_event("domain", &key_pair, &mut value).unwrap();
assert_eq!(
to_string(&value).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 = HashMap::new();
signature_set.insert("ed25519:1".to_string(), public_key_string());
let mut public_key_map = HashMap::new();
public_key_map.insert("domain".to_string(), signature_set);
let value = 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": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"
}
},
"type": "X",
"unsigned": {
"age_ts": 1000000
}
}"#
).unwrap();
assert!(verify_event(&public_key_map, &value).is_ok());
}
}

View File

@ -0,0 +1,127 @@
//! Digital signatures and collections of signatures.
use std::collections::HashMap;
use base64::{encode_config, STANDARD_NO_PAD};
use crate::{split_id, Algorithm, Error, SplitError};
/// A digital signature.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Signature {
/// The cryptographic algorithm that generated this signature.
pub algorithm: Algorithm,
/// The signature data.
pub signature: Vec<u8>,
/// The "version" of the key identifier for the public key used to generate this signature.
pub version: String,
}
impl Signature {
/// Creates a signature from raw bytes.
///
/// While a signature can be created directly using struct literal syntax, this constructor can
/// be used to automatically determine the algorithm and version from a key identifier in the
/// form *algorithm:version*, e.g. "ed25519:1".
///
/// This constructor will ensure that the version does not contain characters that violate the
/// guidelines in the specification. Because it may be necessary to represent signatures with
/// versions that don't adhere to these guidelines, it's possible to simply use the struct
/// literal syntax to construct a `Signature` with an arbitrary key.
///
/// # Parameters
///
/// * id: A key identifier, e.g. "ed25519:1".
/// * bytes: The digital signature, as a series of bytes.
///
/// # Errors
///
/// Returns an error if:
///
/// * The key ID specifies an unknown algorithm.
/// * The key ID is malformed.
/// * The key ID contains a version with invalid characters.
pub fn new(id: &str, bytes: &[u8]) -> Result<Self, Error> {
let (algorithm, version) = split_id(id).map_err(|split_error| match split_error {
SplitError::InvalidLength(length) => Error::new(format!("malformed signature ID: expected exactly 2 segment separated by a colon, found {}", length)),
SplitError::InvalidVersion(version) => Error::new(format!("malformed signature ID: expected version to contain only characters in the character set `[a-zA-Z0-9_]`, found `{}`", version)),
SplitError::UnknownAlgorithm(algorithm) => {
Error::new(format!("unknown algorithm: {}", algorithm))
}
})?;
Ok(Self {
algorithm,
signature: bytes.to_vec(),
version,
})
}
/// The algorithm used to generate the signature.
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
/// The raw bytes of the signature.
pub fn as_bytes(&self) -> &[u8] {
self.signature.as_slice()
}
/// A Base64 encoding of the signature.
///
/// Uses the standard character set with no padding.
pub fn base64(&self) -> String {
encode_config(self.signature.as_slice(), STANDARD_NO_PAD)
}
/// The key identifier, a string containing the signature algorithm and the key "version"
/// separated by a colon, e.g. "ed25519:1".
pub fn id(&self) -> String {
format!("{}:{}", self.algorithm, self.version)
}
/// The "version" of the key used for this signature.
///
/// Versions are used as an identifier to distinguish signatures generated from different keys
/// but using the same algorithm on the same homeserver.
pub fn version(&self) -> &str {
&self.version
}
}
/// A map from entity names to sets of digital signatures created by that entity.
///
/// "Entity" is generally a homeserver, e.g. "example.com".
pub type SignatureMap = HashMap<String, SignatureSet>;
/// A set of digital signatures created by a single homeserver.
///
/// This is represented as a map from signing key ID to Base64-encoded signature.
pub type SignatureSet = HashMap<String, String>;
#[cfg(test)]
mod tests {
use super::Signature;
#[test]
fn valid_key_id() {
assert!(Signature::new("ed25519:abcdef", &[]).is_ok());
}
#[test]
fn invalid_valid_key_id_length() {
assert!(Signature::new("ed25519:abcdef:123456", &[]).is_err());
}
#[test]
fn invalid_key_id_version() {
assert!(Signature::new("ed25519:abc!def", &[]).is_err());
}
#[test]
fn invalid_key_id_algorithm() {
assert!(Signature::new("foobar:abcdef", &[]).is_err());
}
}

View File

@ -0,0 +1,62 @@
//! Verification of digital signatures.
use ring::signature::{VerificationAlgorithm, ED25519};
use untrusted::Input;
use crate::Error;
/// A digital signature verifier.
pub trait Verifier {
/// Use a public key to verify a signature against the JSON object that was signed.
///
/// # Parameters
///
/// * public_key: The raw bytes of the public key of the key pair used to sign the message.
/// * signature: The raw bytes of the signature to verify.
/// * message: The raw bytes of the message that was signed.
///
/// # Errors
///
/// Returns an error if verification fails.
fn verify_json(&self, public_key: &[u8], signature: &[u8], message: &[u8])
-> Result<(), Error>;
}
/// A verifier for Ed25519 digital signatures.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Ed25519Verifier;
impl Verifier for Ed25519Verifier {
fn verify_json(
&self,
public_key: &[u8],
signature: &[u8],
message: &[u8],
) -> Result<(), Error> {
ED25519
.verify(
Input::from(public_key),
Input::from(message),
Input::from(signature),
)
.map_err(|_| Error::new("signature verification failed"))
}
}
/// A value returned when an event is successfully verified.
///
/// Event verification involves verifying both signatures and a content hash. It is possible for
/// the signatures on an event to be valid, but for the hash to be different than the one
/// calculated during verification. This is not necessarily an error condition, as it may indicate
/// that the event has been redacted. In this case, receiving homeservers should store a redacted
/// version of the event.
#[derive(Debug, Clone, Copy, Hash, PartialEq)]
pub enum Verified {
/// All signatures are valid and the content hashes match.
All,
/// All signatures are valid but the content hashes don't match.
///
/// This may indicate a redacted event.
Signatures,
}