Add 'ruma-signatures/' from commit '1ca545cba8dfd43e0fc8e3c18e1311fb73390a97'
git-subtree-dir: ruma-signatures git-subtree-mainline: 50b74e2b7633f0c4dddb9a71313dc198c58a3074 git-subtree-split: 1ca545cba8dfd43e0fc8e3c18e1311fb73390a97
This commit is contained in:
commit
916586e56a
27
ruma-signatures/.builds/beta.yml
Normal file
27
ruma-signatures/.builds/beta.yml
Normal 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 ))
|
32
ruma-signatures/.builds/nightly.yml
Normal file
32
ruma-signatures/.builds/nightly.yml
Normal 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 ))
|
29
ruma-signatures/.builds/stable.yml
Normal file
29
ruma-signatures/.builds/stable.yml
Normal 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
2
ruma-signatures/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Cargo.lock
|
||||||
|
target
|
19
ruma-signatures/Cargo.toml
Normal file
19
ruma-signatures/Cargo.toml
Normal 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
19
ruma-signatures/LICENSE
Normal 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
19
ruma-signatures/README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# ruma-signatures
|
||||||
|
|
||||||
|
[](https://crates.io/crates/ruma-signatures)
|
||||||
|
[](https://docs.rs/ruma-signatures/)
|
||||||
|

|
||||||
|
|
||||||
|
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/
|
764
ruma-signatures/src/functions.rs
Normal file
764
ruma-signatures/src/functions.rs
Normal 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
117
ruma-signatures/src/keys.rs
Normal 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
507
ruma-signatures/src/lib.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
127
ruma-signatures/src/signatures.rs
Normal file
127
ruma-signatures/src/signatures.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
62
ruma-signatures/src/verification.rs
Normal file
62
ruma-signatures/src/verification.rs
Normal 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,
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user