From 9827fd3cab2f77a26222bddc3b765f962d127920 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 4 Dec 2015 06:16:23 -0800 Subject: [PATCH 01/98] ruma-signatures --- .gitignore | 2 + Cargo.toml | 21 +++++++++ LICENSE | 19 ++++++++ README.md | 11 +++++ src/lib.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fa8d85ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..b7491644 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["Jimmy Cuadra "] +description = "Digital signatures according to the Matrix specification." +documentation = "http://ruma.github.io/ruma-signatures/ruma-signatures" +homepage = "https://github.com/ruma/ruma-signatures" +keywords = ["matrix", "matrix.org", "chat", "messaging", "ruma"] +license = "MIT" +name = "ruma-signatures" +readme = "README.md" +repository = "https://github.com/ruma/ruma-signatures" +version = "0.1.0" + +[lib] +# name = "etcd" +doctest = false + +[dependencies] +lazy_static = "0.1.15" +rustc-serialize = "0.3.16" +serde_json = "0.6.0" +sodiumoxide = "0.0.9" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..825ea9f9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..1e9a8a21 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# ruma-signatures + +ruma-signatures provides functionality for creating digital signatures according to the [Matrix](https://matrix.org/) specification. + +## Status + +This project is currently experimental and is very likely to change drastically. + +## License + +[MIT](http://opensource.org/licenses/MIT) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..d5b0d0a2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,131 @@ +//! ruma-signatures provides functionality for creating digital signatures according to the +//! [Matrix](https://matrix.org/) specification. + +#[macro_use] +extern crate lazy_static; +extern crate rustc_serialize; +extern crate serde_json; +extern crate sodiumoxide; + +use std::error::Error; +use std::fmt::Display; + +use rustc_serialize::base64::{CharacterSet, Config, Newline, ToBase64}; +use serde_json::{Value, to_string}; +use sodiumoxide::init; +use sodiumoxide::crypto::sign::{SecretKey, Signature, sign_detached}; + +lazy_static! { + static ref _LIBSODIUM_INIT: bool = init(); +} + +static BASE64_CONFIG: Config = Config { + char_set: CharacterSet::Standard, + newline: Newline::CRLF, + pad: false, + line_length: None, +}; + +/// An error produced when signing data fails. +#[derive(Debug)] +pub struct SigningError { + message: String, +} + +impl SigningError { + pub fn new(message: T) -> Self where T: Into { + SigningError { + message: message.into(), + } + } +} + +impl Error for SigningError { + fn description(&self) -> &str { + self.message.as_ref() + } +} + +impl Display for SigningError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +/// The algorithm used for signing. +#[derive(Debug)] +pub enum SigningAlgorithm { + Ed25519, +} + +/// A signing key, consisting of an algoritm, a secret key, and a key version. +#[derive(Debug)] +pub struct SigningKey { + algorithm: SigningAlgorithm, + key: SecretKey, + version: String, +} + +impl SigningKey { + /// Initialize a new signing key. + pub fn new(algorithm: SigningAlgorithm, key: [u8; 64], version: String) -> Self { + SigningKey { + algorithm: algorithm, + key: SecretKey(key), + version: version, + } + } + + /// Sign a JSON object. + pub fn sign(&self, value: &Value) -> Result { + if !value.is_object() { + return Err(SigningError::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + { + let mut hash = owned_value.as_object_mut().unwrap(); // Safe since we checked above. + hash.remove("signatures"); + hash.remove("unsigned"); + } + + let json = match to_string(&owned_value) { + Ok(json) => json, + Err(error) => return Err(SigningError::new(error.description())), + }; + + Ok(sign_detached(json.as_bytes(), &self.key)) + } + + /// Sign and Base64 encode a JSON object. + pub fn sign_and_base64_encode(&self, value: &Value) -> Result { + let signature = try!(self.sign(value)); + + Ok(signature.as_ref().to_base64(BASE64_CONFIG)) + } +} + +#[cfg(test)] +mod test { + use rustc_serialize::base64::FromBase64; + use serde_json::from_str; + use sodiumoxide::crypto::sign::{SecretKey, Seed, keypair_from_seed}; + + use super::{SigningAlgorithm, SigningKey}; + + #[test] + fn empty_json() { + let seed_vec = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1".from_base64().unwrap(); + let seed = Seed::from_slice(&seed_vec[..]).unwrap(); + let (_pubkey, seckey) = keypair_from_seed(&seed); + let SecretKey(raw_seckey) = seckey; + let signing_key = SigningKey::new(SigningAlgorithm::Ed25519, raw_seckey, "1".to_owned()); + let value = from_str("{}").unwrap(); + let actual = signing_key.sign_and_base64_encode(&value).unwrap(); + assert_eq!( + actual, + "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" + ); + } +} From 2b358f8cc71d78a7220892a834d3bf6aa4fd5da2 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 2 Oct 2016 17:18:47 -0700 Subject: [PATCH 02/98] Update dependencies and Cargo metadata. --- Cargo.toml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b7491644..83bcb4d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,20 @@ [package] authors = ["Jimmy Cuadra "] description = "Digital signatures according to the Matrix specification." -documentation = "http://ruma.github.io/ruma-signatures/ruma-signatures" +documentation = "https://docs.rs/ruma-signatures" homepage = "https://github.com/ruma/ruma-signatures" -keywords = ["matrix", "matrix.org", "chat", "messaging", "ruma"] +keywords = ["matrix", "chat", "messaging", "ruma"] license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" version = "0.1.0" -[lib] -# name = "etcd" -doctest = false - [dependencies] -lazy_static = "0.1.15" -rustc-serialize = "0.3.16" -serde_json = "0.6.0" -sodiumoxide = "0.0.9" +lazy_static = "0.2.1" +rustc-serialize = "0.3.19" +serde_json = "0.8.2" +sodiumoxide = "0.0.12" + +[lib] +doctest = false From 68fccbddd661c34700218ffe6b171660f3cbd2e7 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 9 Oct 2016 08:16:53 -0700 Subject: [PATCH 03/98] Use a SigningKey to produce a Signature. --- src/lib.rs | 98 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d5b0d0a2..725a992f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,13 +7,14 @@ extern crate rustc_serialize; extern crate serde_json; extern crate sodiumoxide; -use std::error::Error; -use std::fmt::Display; +use std::collections::HashSet; +use std::error::Error as StdError; +use std::fmt::{Display, Formatter, Result as FmtResult}; use rustc_serialize::base64::{CharacterSet, Config, Newline, ToBase64}; use serde_json::{Value, to_string}; use sodiumoxide::init; -use sodiumoxide::crypto::sign::{SecretKey, Signature, sign_detached}; +use sodiumoxide::crypto::sign::{SecretKey, Signature as SodiumSignature, sign_detached}; lazy_static! { static ref _LIBSODIUM_INIT: bool = init(); @@ -28,37 +29,51 @@ static BASE64_CONFIG: Config = Config { /// An error produced when signing data fails. #[derive(Debug)] -pub struct SigningError { +pub struct Error { message: String, } -impl SigningError { +impl Error { pub fn new(message: T) -> Self where T: Into { - SigningError { + Error { message: message.into(), } } } -impl Error for SigningError { +impl StdError for Error { fn description(&self) -> &str { - self.message.as_ref() + &self.message } } -impl Display for SigningError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> FmtResult { write!(f, "{}", self.message) } } -/// The algorithm used for signing. +/// A single digital signature. +/// +/// Generated from `SigningKey`. #[derive(Debug)] +pub struct Signature { + algorithm: SigningAlgorithm, + signature: SodiumSignature, + version: String, +} + +/// A set of signatures created by a single homeserver. +pub type SignatureSet = HashSet; + +/// The algorithm used for signing. +#[derive(Clone, Copy, Debug)] pub enum SigningAlgorithm { + /// The Ed25519 digital signature algorithm. Ed25519, } -/// A signing key, consisting of an algoritm, a secret key, and a key version. +/// A signing key, consisting of an algorithm, a secret key, and a key version. #[derive(Debug)] pub struct SigningKey { algorithm: SigningAlgorithm, @@ -66,8 +81,46 @@ pub struct SigningKey { version: String, } +impl Signature { + /// The algorithm used to generate the signature. + pub fn algorithm(&self) -> SigningAlgorithm { + self.algorithm + } + + /// The raw bytes of the signature. + pub fn as_bytes(&self) -> &[u8] { + self.signature.as_ref() + } + + /// A Base64 encoding of the signature. + pub fn base64(&self) -> String { + self.signature.as_ref().to_base64(BASE64_CONFIG) + } + + /// A string containing the signature algorithm and the key "version" separated by a colon. + pub fn id(&self) -> String { + format!("ed25519:{}", 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 + } +} + impl SigningKey { /// Initialize a new signing key. + /// + /// # Parameters + /// + /// * algorithm: The digital signature algorithm to use. + /// * key: A 64-byte secret key. + /// * 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 new(algorithm: SigningAlgorithm, key: [u8; 64], version: String) -> Self { SigningKey { algorithm: algorithm, @@ -77,9 +130,9 @@ impl SigningKey { } /// Sign a JSON object. - pub fn sign(&self, value: &Value) -> Result { + pub fn sign(&self, value: &Value) -> Result { if !value.is_object() { - return Err(SigningError::new("JSON value must be a JSON object")); + return Err(Error::new("JSON value must be a JSON object")); } let mut owned_value = value.clone(); @@ -92,17 +145,14 @@ impl SigningKey { let json = match to_string(&owned_value) { Ok(json) => json, - Err(error) => return Err(SigningError::new(error.description())), + Err(error) => return Err(Error::new(error.description())), }; - Ok(sign_detached(json.as_bytes(), &self.key)) - } - - /// Sign and Base64 encode a JSON object. - pub fn sign_and_base64_encode(&self, value: &Value) -> Result { - let signature = try!(self.sign(value)); - - Ok(signature.as_ref().to_base64(BASE64_CONFIG)) + Ok(Signature { + algorithm: self.algorithm, + signature: sign_detached(json.as_bytes(), &self.key), + version: self.version.clone(), + }) } } @@ -122,7 +172,7 @@ mod test { let SecretKey(raw_seckey) = seckey; let signing_key = SigningKey::new(SigningAlgorithm::Ed25519, raw_seckey, "1".to_owned()); let value = from_str("{}").unwrap(); - let actual = signing_key.sign_and_base64_encode(&value).unwrap(); + let actual = signing_key.sign(&value).unwrap().base64(); assert_eq!( actual, "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" From e6b7179c28de47ff7e281f21f0067b0484883234 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 9 Oct 2016 14:15:58 -0700 Subject: [PATCH 04/98] Add SignatureSet. --- Cargo.toml | 1 + src/lib.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83bcb4d1..4278c76b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ version = "0.1.0" [dependencies] lazy_static = "0.2.1" rustc-serialize = "0.3.19" +serde = "0.8.12" serde_json = "0.8.2" sodiumoxide = "0.0.12" diff --git a/src/lib.rs b/src/lib.rs index 725a992f..9901b56d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #[macro_use] extern crate lazy_static; extern crate rustc_serialize; +extern crate serde; extern crate serde_json; extern crate sodiumoxide; @@ -11,7 +12,9 @@ use std::collections::HashSet; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; -use rustc_serialize::base64::{CharacterSet, Config, Newline, ToBase64}; +use rustc_serialize::base64::{CharacterSet, Config, FromBase64, Newline, ToBase64}; +use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; +use serde::de::{MapVisitor, Visitor}; use serde_json::{Value, to_string}; use sodiumoxide::init; use sodiumoxide::crypto::sign::{SecretKey, Signature as SodiumSignature, sign_detached}; @@ -56,7 +59,7 @@ impl Display for Error { /// A single digital signature. /// /// Generated from `SigningKey`. -#[derive(Debug)] +#[derive(Debug, Eq, Hash, PartialEq)] pub struct Signature { algorithm: SigningAlgorithm, signature: SodiumSignature, @@ -64,10 +67,15 @@ pub struct Signature { } /// A set of signatures created by a single homeserver. -pub type SignatureSet = HashSet; +pub struct SignatureSet { + set: HashSet, +} + +/// Serde Visitor for deserializing `SignatureSet`. +struct SignatureSetVisitor; /// The algorithm used for signing. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SigningAlgorithm { /// The Ed25519 digital signature algorithm. Ed25519, @@ -111,6 +119,104 @@ impl Signature { } } +impl SignatureSet { + /// Initialize a new empty SignatureSet. + pub fn new() -> Self { + SignatureSet { + set: HashSet::new(), + } + } + + /// Initialize a new empty SignatureSet with room for a specific number of signatures. + pub fn with_capacity(capacity: usize) -> Self { + SignatureSet { + set: HashSet::with_capacity(capacity), + } + } + + /// Adds a signature to the set. + /// + /// The boolean return value indicates whether or not the value was actually inserted, since + /// subsequent inserts of the same signature have no effect. + pub fn insert(&mut self, signature: Signature) -> bool { + self.set.insert(signature) + } + + /// The number of signatures in the set. + pub fn len(&self) -> usize { + self.set.len() + } +} + +impl Deserialize for SignatureSet { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize_map(SignatureSetVisitor) + } +} + +impl Serialize for SignatureSet { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + let mut state = try!(serializer.serialize_map(Some(self.len()))); + + for signature in self.set.iter() { + try!(serializer.serialize_map_key(&mut state, signature.id())); + try!(serializer.serialize_map_value(&mut state, signature.base64())); + } + + serializer.serialize_map_end(state) + } +} + +impl Visitor for SignatureSetVisitor { + type Value = SignatureSet; + + fn visit_map(&mut self, mut visitor: M) -> Result + where M: MapVisitor { + const SIGNATURE_ID_LENGTH: usize = 2; + + let mut signature_set = SignatureSet::with_capacity(visitor.size_hint().0); + + while let Some((key, value)) = try!(visitor.visit::()) { + let signature_id: Vec<&str> = key.split(':').collect(); + + let signature_id_length = signature_id.len(); + + if signature_id_length != SIGNATURE_ID_LENGTH { + return Err(M::Error::invalid_length(signature_id_length)); + } + + let algorithm_input = signature_id[0]; + + let algorithm = match algorithm_input { + "ed25519" => SigningAlgorithm::Ed25519, + _ => return Err(M::Error::invalid_value(algorithm_input)), + }; + + let raw_signature: Vec = match value.from_base64() { + Ok(raw) => raw, + Err(error) => return Err(M::Error::custom(error.description())), + }; + + let sodium_signature = match SodiumSignature::from_slice(&raw_signature) { + Some(s) => s, + None => return Err(M::Error::invalid_value("invalid Ed25519 signature")), + }; + + let signature = Signature { + algorithm: algorithm, + signature: sodium_signature, + version: signature_id[1].to_string(), + }; + + signature_set.insert(signature); + } + + try!(visitor.end()); + + Ok(signature_set) + } +} + impl SigningKey { /// Initialize a new signing key. /// From 9f78427a04212946b5c076d663d9581d9a4bca00 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 9 Oct 2016 14:18:21 -0700 Subject: [PATCH 05/98] Add Travis CI configuration. --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4e2e913d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: "rust" +notifications: + email: false + irc: + channels: + - "chat.freenode.net#ruma" + use_notice: true +rust: + - "nightly" From 6460312c79261a30fb22b0657aca5feac32fffe6 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 9 Oct 2016 14:33:30 -0700 Subject: [PATCH 06/98] Install libsodium on Travis. --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4e2e913d..448915b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ +before_install: + - "curl -LO https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz" + - "tar -zxvf libsodium-1.0.11.tar.gz" + - "cd libsodium-1.0.11" + - "./configure" + - "make" + - "sudo make install" language: "rust" notifications: email: false From b080a934fb2c0359381ca12d00d0ae7eda8fbf36 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 10 Dec 2016 04:02:22 -0800 Subject: [PATCH 07/98] Switch to *ring* for crypto and flesh out the API. --- .travis.yml | 9 -- Cargo.toml | 13 +- src/lib.rs | 416 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 296 insertions(+), 142 deletions(-) diff --git a/.travis.yml b/.travis.yml index 448915b9..1e78660a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,3 @@ -before_install: - - "curl -LO https://github.com/jedisct1/libsodium/releases/download/1.0.11/libsodium-1.0.11.tar.gz" - - "tar -zxvf libsodium-1.0.11.tar.gz" - - "cd libsodium-1.0.11" - - "./configure" - - "make" - - "sudo make install" language: "rust" notifications: email: false @@ -12,5 +5,3 @@ notifications: channels: - "chat.freenode.net#ruma" use_notice: true -rust: - - "nightly" diff --git a/Cargo.toml b/Cargo.toml index 4278c76b..3261fc9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,8 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.1.0" [dependencies] -lazy_static = "0.2.1" -rustc-serialize = "0.3.19" -serde = "0.8.12" -serde_json = "0.8.2" -sodiumoxide = "0.0.12" - -[lib] -doctest = false +ring = "0.6.0-alpha1" +rustc-serialize = "0.3.22" +serde = "0.8.19" +serde_json = "0.8.4" +untrusted = "0.3.2" diff --git a/src/lib.rs b/src/lib.rs index 9901b56d..043bff6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,106 @@ -//! ruma-signatures provides functionality for creating digital signatures according to the +//! 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. Each homeserver has one or more signing key pairs which it uses to sign all +//! events. Matrix clients and other Matrix homeservers can ask the homeserver for its public keys +//! and use those keys to verify the signed events. +//! +//! 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. +//! +//! # Signing JSON +//! +//! A homeserver signs JSON with a key pair: +//! +//! ```rust,no_run +//! # extern crate ruma_signatures; +//! # extern crate serde_json; +//! # fn main() { +//! # use ruma_signatures::KeyPair; +//! # let public_key = [0; 32]; +//! # let private_key = [0; 32]; +//! // Create an Ed25519 key pair. +//! let key_pair = ruma_signatures::Ed25519KeyPair::new( +//! &public_key, // &[u8] +//! &private_key, // &[u8] +//! "1".to_string(), // The "version" of the key. +//! ).unwrap(); +//! let value = serde_json::from_str("{}").unwrap(); // An empty JSON object. +//! let signature = key_pair.sign(&value).unwrap(); // Creates a `Signature`. +//! # } +//! ``` +//! +//! # Verifying signatures +//! +//! A client application or another homeserver can verify a signature: +//! +//! ```rust,no_run +//! # extern crate ruma_signatures; +//! # extern crate serde_json; +//! # fn main() { +//! # let public_key = [0; 32]; +//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! let value = serde_json::from_str("{}").unwrap(); // The same empty JSON object. +//! assert!(signature.verify(&public_key, &value).is_ok()); +//! # } +//! ``` +//! +//! # Signature sets +//! +//! Signatures that a homeserver has added to an event are stored in a JSON object under the +//! "signatures" key in the event's JSON representation: +//! +//! ```json +//! { +//! "content": {}, +//! "event_type": "not.a.real.event", +//! "signatures": { +//! "example.com": { +//! "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" +//! } +//! } +//! } +//! ``` +//! +//! The keys inside the "signatures" object are the hostnames of homeservers that have added +//! signatures. Within each of those objects are a set of signatures, keyed by the signing key +//! pair's identifier. +//! +//! This inner object can be created by serializing a `SignatureSet`: +//! +//! ```rust,no_run +//! # extern crate ruma_signatures; +//! # extern crate serde; +//! # extern crate serde_json; +//! # fn main() { +//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! let mut signature_set = ruma_signatures::SignatureSet::new(); +//! signature_set.insert(signature); +//! let json = serde_json::to_string(&signature_set).unwrap(); +//! # } +//! ``` +//! +//! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, +//! a `SignatureSet` can be produced by deserializing JSON that follows this form. -#[macro_use] -extern crate lazy_static; +extern crate ring; extern crate rustc_serialize; extern crate serde; extern crate serde_json; -extern crate sodiumoxide; +extern crate untrusted; use std::collections::HashSet; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; +use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; use rustc_serialize::base64::{CharacterSet, Config, FromBase64, Newline, ToBase64}; use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; use serde::de::{MapVisitor, Visitor}; use serde_json::{Value, to_string}; -use sodiumoxide::init; -use sodiumoxide::crypto::sign::{SecretKey, Signature as SodiumSignature, sign_detached}; - -lazy_static! { - static ref _LIBSODIUM_INIT: bool = init(); -} +use untrusted::Input; static BASE64_CONFIG: Config = Config { char_set: CharacterSet::Standard, @@ -30,12 +109,138 @@ static BASE64_CONFIG: Config = Config { line_length: None, }; -/// An error produced when signing data fails. +fn signable_json(value: &Value) -> Result { + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + { + let mut hash = owned_value.as_object_mut().unwrap(); // Safe since we checked above. + hash.remove("signatures"); + hash.remove("unsigned"); + } + + to_string(&owned_value).map_err(|error| Error::new(error.description())) +} + +enum SplitError<'a> { + InvalidLength(usize), + UnknownAlgorithm(&'a str), +} + +fn split_id(id: &str) -> Result<(Algorithm, String), SplitError> { + 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 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())) +} + +/// An Ed25519 key pair. +pub struct Ed25519KeyPair { + ring_key_pair: RingEd25519KeyPair, + version: String, +} + +/// An error produced during signing or verification. #[derive(Debug)] pub struct Error { message: String, } +/// A cryptographic key pair for digitally signing data. +pub trait KeyPair: Sized { + /// Initializes a new key pair. + /// + /// # Parameters + /// + /// * public_key: The public key of the key pair. + /// * private_key: The private key of the key pair. + /// * 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. + fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result; + + /// Signs a JSON object. + /// + /// # Parameters + /// + /// * value: A JSON value to be signed according to the Matrix specification. + /// + /// # Errors + /// + /// Returns an error if the JSON value is not a JSON object. + fn sign(&self, value: &Value) -> Result; +} + +/// A single digital signature. +/// +/// Signatures are originally generated from a `KeyPair`. +/// For verifying a signature, a `Signature` can be constructed from bytes using `new`. +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct Signature { + algorithm: Algorithm, + signature: Vec, + version: String, +} + +/// A set of signatures created by a single homeserver. +pub struct SignatureSet { + set: HashSet, +} + +/// Serde Visitor for deserializing `SignatureSet`. +struct SignatureSetVisitor; + +/// The algorithm used for signing data. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Algorithm { + /// The Ed25519 digital signature algorithm. + Ed25519, +} + +impl KeyPair for Ed25519KeyPair { + fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { + Ok(Ed25519KeyPair { + ring_key_pair: RingEd25519KeyPair::from_bytes( + private_key, + public_key, + ).map_err(|_| Error::new("invalid key pair"))?, + version: version, + }) + } + + fn sign(&self, value: &Value) -> Result { + let json = signable_json(value)?; + + Ok(Signature { + algorithm: Algorithm::Ed25519, + signature: self.ring_key_pair.sign(json.as_bytes()).as_slice().to_vec(), + version: self.version.clone(), + }) + } +} + impl Error { pub fn new(message: T) -> Self where T: Into { Error { @@ -56,58 +261,57 @@ impl Display for Error { } } -/// A single digital signature. -/// -/// Generated from `SigningKey`. -#[derive(Debug, Eq, Hash, PartialEq)] -pub struct Signature { - algorithm: SigningAlgorithm, - signature: SodiumSignature, - version: String, -} - -/// A set of signatures created by a single homeserver. -pub struct SignatureSet { - set: HashSet, -} - -/// Serde Visitor for deserializing `SignatureSet`. -struct SignatureSetVisitor; - -/// The algorithm used for signing. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum SigningAlgorithm { - /// The Ed25519 digital signature algorithm. - Ed25519, -} - -/// A signing key, consisting of an algorithm, a secret key, and a key version. -#[derive(Debug)] -pub struct SigningKey { - algorithm: SigningAlgorithm, - key: SecretKey, - version: String, -} - impl Signature { + /// Creates a signature from raw bytes. + pub fn new(id: &str, bytes: &[u8]) -> Result { + let (algorithm, version) = split_id(id).map_err(|split_error| { + match split_error { + SplitError::InvalidLength(_) => Error::new("malformed signature ID"), + SplitError::UnknownAlgorithm(algorithm) => { + Error::new(format!("unknown algorithm: {}", algorithm)) + } + } + })?; + + Ok(Signature { + algorithm: algorithm, + signature: bytes.to_vec(), + version: version, + }) + } + /// The algorithm used to generate the signature. - pub fn algorithm(&self) -> SigningAlgorithm { + pub fn algorithm(&self) -> Algorithm { self.algorithm } /// The raw bytes of the signature. pub fn as_bytes(&self) -> &[u8] { - self.signature.as_ref() + self.signature.as_slice() } /// A Base64 encoding of the signature. pub fn base64(&self) -> String { - self.signature.as_ref().to_base64(BASE64_CONFIG) + self.signature.as_slice().to_base64(BASE64_CONFIG) } /// A string containing the signature algorithm and the key "version" separated by a colon. pub fn id(&self) -> String { - format!("ed25519:{}", self.version()) + format!("{}:{}", self.algorithm, self.version) + } + + /// Use the public key to verify the signature against the JSON object that was signed. + pub fn verify(&self, public_key: &[u8], value: &Value) -> Result<(), Error> { + match self.algorithm { + Algorithm::Ed25519 => { + verify( + &ED25519, + Input::from(public_key), + Input::from(signable_json(value)?.as_bytes()), + Input::from(self.as_bytes()), + ).map_err(|_| Error::new("signature verification failed")) + } + } } /// The "version" of the key used for this signature. @@ -172,40 +376,25 @@ impl Visitor for SignatureSetVisitor { fn visit_map(&mut self, mut visitor: M) -> Result where M: MapVisitor { - const SIGNATURE_ID_LENGTH: usize = 2; - let mut signature_set = SignatureSet::with_capacity(visitor.size_hint().0); while let Some((key, value)) = try!(visitor.visit::()) { - let signature_id: Vec<&str> = key.split(':').collect(); + let (algorithm, version) = split_id(&key).map_err(|split_error| { + match split_error { + SplitError::InvalidLength(length) => M::Error::invalid_length(length), + SplitError::UnknownAlgorithm(algorithm) => M::Error::invalid_value(algorithm), + } + })?; - let signature_id_length = signature_id.len(); - - if signature_id_length != SIGNATURE_ID_LENGTH { - return Err(M::Error::invalid_length(signature_id_length)); - } - - let algorithm_input = signature_id[0]; - - let algorithm = match algorithm_input { - "ed25519" => SigningAlgorithm::Ed25519, - _ => return Err(M::Error::invalid_value(algorithm_input)), - }; - - let raw_signature: Vec = match value.from_base64() { + let signature_bytes: Vec = match value.from_base64() { Ok(raw) => raw, Err(error) => return Err(M::Error::custom(error.description())), }; - let sodium_signature = match SodiumSignature::from_slice(&raw_signature) { - Some(s) => s, - None => return Err(M::Error::invalid_value("invalid Ed25519 signature")), - }; - let signature = Signature { algorithm: algorithm, - signature: sodium_signature, - version: signature_id[1].to_string(), + signature: signature_bytes, + version: version, }; signature_set.insert(signature); @@ -217,48 +406,13 @@ impl Visitor for SignatureSetVisitor { } } -impl SigningKey { - /// Initialize a new signing key. - /// - /// # Parameters - /// - /// * algorithm: The digital signature algorithm to use. - /// * key: A 64-byte secret key. - /// * 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 new(algorithm: SigningAlgorithm, key: [u8; 64], version: String) -> Self { - SigningKey { - algorithm: algorithm, - key: SecretKey(key), - version: version, - } - } - - /// Sign a JSON object. - pub fn sign(&self, value: &Value) -> Result { - if !value.is_object() { - return Err(Error::new("JSON value must be a JSON object")); - } - - let mut owned_value = value.clone(); - - { - let mut hash = owned_value.as_object_mut().unwrap(); // Safe since we checked above. - hash.remove("signatures"); - hash.remove("unsigned"); - } - - let json = match to_string(&owned_value) { - Ok(json) => json, - Err(error) => return Err(Error::new(error.description())), +impl Display for Algorithm { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let name = match *self { + Algorithm::Ed25519 => "ed25519", }; - Ok(Signature { - algorithm: self.algorithm, - signature: sign_detached(json.as_bytes(), &self.key), - version: self.version.clone(), - }) + write!(f, "{}", name) } } @@ -266,22 +420,34 @@ impl SigningKey { mod test { use rustc_serialize::base64::FromBase64; use serde_json::from_str; - use sodiumoxide::crypto::sign::{SecretKey, Seed, keypair_from_seed}; - use super::{SigningAlgorithm, SigningKey}; + use super::{Ed25519KeyPair, KeyPair, Signature}; + + const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; + const PRIVATE_KEY: &'static str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; + + const EMPTY_JSON_SIGNATURE: &'static str = + "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; #[test] - fn empty_json() { - let seed_vec = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1".from_base64().unwrap(); - let seed = Seed::from_slice(&seed_vec[..]).unwrap(); - let (_pubkey, seckey) = keypair_from_seed(&seed); - let SecretKey(raw_seckey) = seckey; - let signing_key = SigningKey::new(SigningAlgorithm::Ed25519, raw_seckey, "1".to_owned()); + fn sign_empty_json() { + let key_pair = Ed25519KeyPair::new( + &PUBLIC_KEY.from_base64().unwrap(), + &PRIVATE_KEY.from_base64().unwrap(), + "1".to_string(), + ).unwrap(); let value = from_str("{}").unwrap(); - let actual = signing_key.sign(&value).unwrap().base64(); - assert_eq!( - actual, - "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" - ); + let signature = key_pair.sign(&value).unwrap(); + assert_eq!(signature.base64(), EMPTY_JSON_SIGNATURE); + } + + #[test] + fn verify_empty_json() { + let signature = Signature::new( + "ed25519:1", + &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + ).unwrap(); + let value = from_str("{}").unwrap(); + assert!(signature.verify(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); } } From 348b6e2941cc1041a4ba6050585fad29acaf2fb8 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 10 Dec 2016 10:47:12 -0800 Subject: [PATCH 08/98] Move signing to a top-level function. --- src/lib.rs | 80 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 043bff6b..c1141e5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,10 @@ //! [Matrix](https://matrix.org/) specification. //! //! Digital signatures are used by Matrix homeservers to verify the authenticity of events in the -//! Matrix system. Each homeserver has one or more signing key pairs which it uses to sign all -//! events. Matrix clients and other Matrix homeservers can ask the homeserver for its public keys -//! and use those keys to verify the signed events. +//! Matrix system, as well as requests between homeservers for federation. Each homeserver has one +//! or more signing key pairs 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 @@ -28,13 +29,18 @@ //! "1".to_string(), // The "version" of the key. //! ).unwrap(); //! let value = serde_json::from_str("{}").unwrap(); // An empty JSON object. -//! let signature = key_pair.sign(&value).unwrap(); // Creates a `Signature`. +//! let signature = ruma_signatures::sign_json(&key_pair, &value).unwrap(); // `Signature` //! # } //! ``` //! +//! # Signing Matrix events +//! +//! Signing an event uses a more involved process than signing arbitrary JSON. +//! Event signing is not yet implemented by ruma_signatures. +//! //! # Verifying signatures //! -//! A client application or another homeserver can verify a signature: +//! A client application or another homeserver can verify a signature on arbitrary JSON: //! //! ```rust,no_run //! # extern crate ruma_signatures; @@ -43,10 +49,12 @@ //! # let public_key = [0; 32]; //! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); //! let value = serde_json::from_str("{}").unwrap(); // The same empty JSON object. -//! assert!(signature.verify(&public_key, &value).is_ok()); +//! assert!(signature.verify_json(&public_key, &value).is_ok()); //! # } //! ``` //! +//! Verifying signatures of Matrix events is not yet implemented by ruma_signatures. +//! //! # Signature sets //! //! Signatures that a homeserver has added to an event are stored in a JSON object under the @@ -109,7 +117,28 @@ static BASE64_CONFIG: Config = Config { line_length: None, }; -fn signable_json(value: &Value) -> Result { +/// Signs an arbitrary JSON object. +/// +/// # Parameters +/// +/// * key_pair: A cryptographic key pair used to sign the JSON. +/// * value: A JSON object to be signed according to the Matrix specification. +/// +/// # Errors +/// +/// Returns an error if the JSON value is not a JSON object. +pub fn sign_json(key_pair: &K, value: &Value) -> Result where K: KeyPair { + let json = to_canonical_json(value)?; + + Ok(key_pair.sign(json.as_bytes())) +} + +/// Converts a JSON object into the "canonical" form, suitable for signing. +/// +/// # Errors +/// +/// Returns an error if the provided JSON value is not a JSON object. +pub fn to_canonical_json(value: &Value) -> Result { if !value.is_object() { return Err(Error::new("JSON value must be a JSON object")); } @@ -117,9 +146,9 @@ fn signable_json(value: &Value) -> Result { let mut owned_value = value.clone(); { - let mut hash = owned_value.as_object_mut().unwrap(); // Safe since we checked above. - hash.remove("signatures"); - hash.remove("unsigned"); + let mut object = owned_value.as_object_mut().unwrap(); // Safe since we checked above. + object.remove("signatures"); + object.remove("unsigned"); } to_string(&owned_value).map_err(|error| Error::new(error.description())) @@ -185,18 +214,11 @@ pub trait KeyPair: Sized { /// /// # Parameters /// - /// * value: A JSON value to be signed according to the Matrix specification. - /// - /// # Errors - /// - /// Returns an error if the JSON value is not a JSON object. - fn sign(&self, value: &Value) -> Result; + /// * message: An arbitrary binary value to sign. + fn sign(&self, message: &[u8]) -> Signature; } /// A single digital signature. -/// -/// Signatures are originally generated from a `KeyPair`. -/// For verifying a signature, a `Signature` can be constructed from bytes using `new`. #[derive(Debug, Eq, Hash, PartialEq)] pub struct Signature { algorithm: Algorithm, @@ -230,14 +252,12 @@ impl KeyPair for Ed25519KeyPair { }) } - fn sign(&self, value: &Value) -> Result { - let json = signable_json(value)?; - - Ok(Signature { + fn sign(&self, message: &[u8]) -> Signature { + Signature { algorithm: Algorithm::Ed25519, - signature: self.ring_key_pair.sign(json.as_bytes()).as_slice().to_vec(), + signature: self.ring_key_pair.sign(message).as_slice().to_vec(), version: self.version.clone(), - }) + } } } @@ -301,13 +321,13 @@ impl Signature { } /// Use the public key to verify the signature against the JSON object that was signed. - pub fn verify(&self, public_key: &[u8], value: &Value) -> Result<(), Error> { + pub fn verify_json(&self, public_key: &[u8], value: &Value) -> Result<(), Error> { match self.algorithm { Algorithm::Ed25519 => { verify( &ED25519, Input::from(public_key), - Input::from(signable_json(value)?.as_bytes()), + Input::from(to_canonical_json(value)?.as_bytes()), Input::from(self.as_bytes()), ).map_err(|_| Error::new("signature verification failed")) } @@ -421,7 +441,7 @@ mod test { use rustc_serialize::base64::FromBase64; use serde_json::from_str; - use super::{Ed25519KeyPair, KeyPair, Signature}; + use super::{Ed25519KeyPair, KeyPair, Signature, sign_json}; const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; const PRIVATE_KEY: &'static str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; @@ -437,7 +457,7 @@ mod test { "1".to_string(), ).unwrap(); let value = from_str("{}").unwrap(); - let signature = key_pair.sign(&value).unwrap(); + let signature = sign_json(&key_pair, &value).unwrap(); assert_eq!(signature.base64(), EMPTY_JSON_SIGNATURE); } @@ -448,6 +468,6 @@ mod test { &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), ).unwrap(); let value = from_str("{}").unwrap(); - assert!(signature.verify(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); + assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); } } From 1c0379b59c3570760e08d104d0d8d7203ae0ea43 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 10 Dec 2016 11:29:58 -0800 Subject: [PATCH 09/98] Add `Signatures` type. --- Cargo.toml | 1 + src/lib.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3261fc9a..a7d94b14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ rustc-serialize = "0.3.22" serde = "0.8.19" serde_json = "0.8.4" untrusted = "0.3.2" +url = "1.2.3" diff --git a/src/lib.rs b/src/lib.rs index c1141e5c..6f24f4d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,14 +92,34 @@ //! //! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, //! a `SignatureSet` can be produced by deserializing JSON that follows this form. +//! +//! The outer object (the map of server names to signature sets) is a `Signatures` value and +//! created like this: +//! +//! ```rust,no_run +//! # extern crate ruma_signatures; +//! # extern crate serde; +//! # extern crate serde_json; +//! # fn main() { +//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! let mut signature_set = ruma_signatures::SignatureSet::new(); +//! signature_set.insert(signature); +//! let mut signatures = ruma_signatures::Signatures::new(); +//! signatures.insert("example.com", &signature_set); +//! let json = serde_json::to_string(&signatures).unwrap(); +//! # } +//! ``` +//! +//! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. extern crate ring; extern crate rustc_serialize; extern crate serde; extern crate serde_json; extern crate untrusted; +extern crate url; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -109,6 +129,9 @@ use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serialize use serde::de::{MapVisitor, Visitor}; use serde_json::{Value, to_string}; use untrusted::Input; +use url::Url; + +pub use url::Host; static BASE64_CONFIG: Config = Config { char_set: CharacterSet::Standard, @@ -187,7 +210,7 @@ pub struct Ed25519KeyPair { } /// An error produced during signing or verification. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Error { message: String, } @@ -219,14 +242,24 @@ pub trait KeyPair: Sized { } /// A single digital signature. -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Signature { algorithm: Algorithm, signature: Vec, version: String, } +/// A map of server names to sets of signatures created by that server. +#[derive(Clone, Debug)] +pub struct Signatures { + map: HashMap +} + +/// Serde Visitor for deserializing `Signatures`. +struct SignaturesVisitor; + /// A set of signatures created by a single homeserver. +#[derive(Clone, Debug)] pub struct SignatureSet { set: HashSet, } @@ -343,15 +376,97 @@ impl Signature { } } +impl Signatures { + /// Initializes a new empty Signatures. + pub fn new() -> Self { + Signatures { + map: HashMap::new(), + } + } + + /// Initializes a new empty Signatures with room for a specific number of servers. + pub fn with_capacity(capacity: usize) -> Self { + Signatures { + map: HashMap::with_capacity(capacity), + } + } + + /// Adds a signature set for a server. + /// + /// If no signature set for the given server existed in the collection, `None` is returned. + /// Otherwise, the signature set is returned. + /// + /// # Errors + /// + /// Returns an error if the given server name cannot be parsed as a valid host. + pub fn insert(&mut self, server_name: &str, signature_set: &SignatureSet) + -> Result, Error> { + let url_string = format!("https://{}", server_name); + let url = Url::parse(&url_string).map_err(|_| { + Error::new(format!("invalid server name: {}", server_name)) + })?; + + let host = match url.host() { + Some(host) => host.to_owned(), + None => return Err(Error::new(format!("invalid server name: {}", server_name))), + }; + + Ok(self.map.insert(host, signature_set.clone())) + } + + /// The number of servers in the collection. + pub fn len(&self) -> usize { + self.map.len() + } +} + +impl Deserialize for Signatures { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize_map(SignaturesVisitor) + } +} + +impl Serialize for Signatures { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + let mut state = try!(serializer.serialize_map(Some(self.len()))); + + for (host, signature_set) in self.map.iter() { + try!(serializer.serialize_map_key(&mut state, host.to_string())); + try!(serializer.serialize_map_value(&mut state, signature_set)); + } + + serializer.serialize_map_end(state) + } +} + +impl Visitor for SignaturesVisitor { + type Value = Signatures; + + fn visit_map(&mut self, mut visitor: M) -> Result + where M: MapVisitor { + let mut signatures = Signatures::with_capacity(visitor.size_hint().0); + + while let Some((server_name, signature_set)) = try!(visitor.visit::()) { + if let Err(_) = signatures.insert(&server_name, &signature_set) { + return Err(M::Error::invalid_value(&server_name)); + } + } + + try!(visitor.end()); + + Ok(signatures) + } +} + impl SignatureSet { - /// Initialize a new empty SignatureSet. + /// Initializes a new empty SignatureSet. pub fn new() -> Self { SignatureSet { set: HashSet::new(), } } - /// Initialize a new empty SignatureSet with room for a specific number of signatures. + /// Initializes a new empty SignatureSet with room for a specific number of signatures. pub fn with_capacity(capacity: usize) -> Self { SignatureSet { set: HashSet::with_capacity(capacity), From c4665ade9ed60bd43f90d36592a98ff0b9b3677b Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 10 Dec 2016 17:27:58 -0800 Subject: [PATCH 10/98] Add more tests. --- .travis.yml | 2 + Cargo.toml | 3 + src/lib.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 159 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1e78660a..4e2e913d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,5 @@ notifications: channels: - "chat.freenode.net#ruma" use_notice: true +rust: + - "nightly" diff --git a/Cargo.toml b/Cargo.toml index a7d94b14..75c29b16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ serde = "0.8.19" serde_json = "0.8.4" untrusted = "0.3.2" url = "1.2.3" + +[dev-dependencies] +serde_derive = "0.8.19" diff --git a/src/lib.rs b/src/lib.rs index 6f24f4d5..21dfd434 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,16 +105,21 @@ //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! let mut signatures = ruma_signatures::Signatures::new(); -//! signatures.insert("example.com", &signature_set); +//! signatures.insert("example.com", signature_set); //! let json = serde_json::to_string(&signatures).unwrap(); //! # } //! ``` //! //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. +#![cfg_attr(test, feature(proc_macro))] + extern crate ring; extern crate rustc_serialize; extern crate serde; +#[cfg(test)] +#[macro_use] +extern crate serde_derive; extern crate serde_json; extern crate untrusted; extern crate url; @@ -399,7 +404,7 @@ impl Signatures { /// # Errors /// /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn insert(&mut self, server_name: &str, signature_set: &SignatureSet) + pub fn insert(&mut self, server_name: &str, signature_set: SignatureSet) -> Result, Error> { let url_string = format!("https://{}", server_name); let url = Url::parse(&url_string).map_err(|_| { @@ -411,7 +416,7 @@ impl Signatures { None => return Err(Error::new(format!("invalid server name: {}", server_name))), }; - Ok(self.map.insert(host, signature_set.clone())) + Ok(self.map.insert(host, signature_set)) } /// The number of servers in the collection. @@ -447,7 +452,7 @@ impl Visitor for SignaturesVisitor { let mut signatures = Signatures::with_capacity(visitor.size_hint().0); while let Some((server_name, signature_set)) = try!(visitor.visit::()) { - if let Err(_) = signatures.insert(&server_name, &signature_set) { + if let Err(_) = signatures.insert(&server_name, signature_set) { return Err(M::Error::invalid_value(&server_name)); } } @@ -554,15 +559,17 @@ impl Display for Algorithm { #[cfg(test)] mod test { use rustc_serialize::base64::FromBase64; - use serde_json::from_str; + use serde_json::{from_str, to_string, to_value}; - use super::{Ed25519KeyPair, KeyPair, Signature, sign_json}; + use super::{Ed25519KeyPair, KeyPair, Signature, Signatures, SignatureSet, sign_json}; const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; const PRIVATE_KEY: &'static str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; const EMPTY_JSON_SIGNATURE: &'static str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; + const MINIMAL_JSON_SIGNATURE: &'static str = + "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; #[test] fn sign_empty_json() { @@ -571,8 +578,11 @@ mod test { &PRIVATE_KEY.from_base64().unwrap(), "1".to_string(), ).unwrap(); + let value = from_str("{}").unwrap(); + let signature = sign_json(&key_pair, &value).unwrap(); + assert_eq!(signature.base64(), EMPTY_JSON_SIGNATURE); } @@ -582,7 +592,145 @@ mod test { "ed25519:1", &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), ).unwrap(); + let value = from_str("{}").unwrap(); + assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); } + + #[test] + fn signatures_empty_json() { + #[derive(Serialize)] + struct EmptyWithSignatures { + signatures: Signatures, + } + + let signature = Signature::new( + "ed25519:1", + &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + ).unwrap(); + + let mut signature_set = SignatureSet::with_capacity(1); + signature_set.insert(signature); + + let mut signatures = Signatures::with_capacity(1); + signatures.insert("domain", signature_set).ok(); + + let empty = EmptyWithSignatures { + signatures: signatures, + }; + + let json = to_string(&empty).unwrap(); + + assert_eq!( + json, + r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# + ); + } + + #[test] + fn sign_minimal_json() { + #[derive(Serialize)] + struct Alpha { + one: u8, + two: String, + } + + #[derive(Serialize)] + struct ReverseAlpha { + two: String, + one: u8, + } + + let key_pair = Ed25519KeyPair::new( + &PUBLIC_KEY.from_base64().unwrap(), + &PRIVATE_KEY.from_base64().unwrap(), + "1".to_string(), + ).unwrap(); + + let alpha = Alpha { + one: 1, + two: "Two".to_string(), + }; + + let reverse_alpha = ReverseAlpha { + two: "Two".to_string(), + one: 1, + }; + + let alpha_value = to_value(alpha); + let alpha_signature = sign_json(&key_pair, &alpha_value).unwrap(); + + assert_eq!(alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); + + let reverse_alpha_value = to_value(reverse_alpha); + let reverse_alpha_signature = sign_json(&key_pair, &reverse_alpha_value).unwrap(); + + assert_eq!(reverse_alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); + } + + #[test] + fn verify_minimal_json() { + let signature = Signature::new( + "ed25519:1", + &MINIMAL_JSON_SIGNATURE.from_base64().unwrap(), + ).unwrap(); + + let value = from_str( + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + ).unwrap(); + + assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); + + let reverse_value = from_str( + r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# + ).unwrap(); + + assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &reverse_value).is_ok()); + } + + #[test] + fn signatures_minimal_json() { + #[derive(Serialize)] + struct MinimalWithSignatures { + one: u8, + signatures: Signatures, + two: String, + } + + let signature = Signature::new( + "ed25519:1", + &MINIMAL_JSON_SIGNATURE.from_base64().unwrap(), + ).unwrap(); + + let mut signature_set = SignatureSet::with_capacity(1); + signature_set.insert(signature); + + let mut signatures = Signatures::with_capacity(1); + signatures.insert("domain", signature_set).ok(); + + let minimal = MinimalWithSignatures { + one: 1, + signatures: signatures.clone(), + two: "Two".to_string(), + }; + + let json = to_string(&minimal).unwrap(); + assert_eq!( + json, + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + ); + } + + #[test] + fn fail_verify() { + let signature = Signature::new( + "ed25519:1", + &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + ).unwrap(); + + let value = from_str(r#"{"not":"empty"}"#).unwrap(); + + assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_err()); + } } From e2d1bc2cf1e0cf2f4b3707c23f6ca29a1797a004 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 15:54:48 -0800 Subject: [PATCH 11/98] Verify JSON via a top level function that delegates to a Verifier. --- src/lib.rs | 185 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 35 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 21dfd434..cc969df1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,9 +47,11 @@ //! # extern crate serde_json; //! # fn main() { //! # let public_key = [0; 32]; -//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! # let signature_bytes = [0, 32]; +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let value = serde_json::from_str("{}").unwrap(); // The same empty JSON object. -//! assert!(signature.verify_json(&public_key, &value).is_ok()); +//! let verifier = ruma_signatures::Ed25519Verifier::new(); +//! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! # } //! ``` //! @@ -83,7 +85,8 @@ //! # extern crate serde; //! # extern crate serde_json; //! # fn main() { -//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! # let signature_bytes = [0, 32]; +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! let json = serde_json::to_string(&signature_set).unwrap(); @@ -101,7 +104,8 @@ //! # extern crate serde; //! # extern crate serde_json; //! # fn main() { -//! # let signature = ruma_signatures::Signature::new("ed25519:1", &[0; 32]).unwrap(); +//! # let signature_bytes = [0, 32]; +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! let mut signatures = ruma_signatures::Signatures::new(); @@ -113,6 +117,7 @@ //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. #![cfg_attr(test, feature(proc_macro))] +#![deny(missing_docs)] extern crate ring; extern crate rustc_serialize; @@ -161,7 +166,11 @@ pub fn sign_json(key_pair: &K, value: &Value) -> Result whe Ok(key_pair.sign(json.as_bytes())) } -/// Converts a JSON object into the "canonical" form, suitable for signing. +/// Converts a JSON object into the "canonical" string form, suitable for signing. +/// +/// # Parameters +/// +/// * value: The `serde_json::Value` (JSON value) to convert. /// /// # Errors /// @@ -182,11 +191,31 @@ pub fn to_canonical_json(value: &Value) -> Result { to_string(&owned_value).map_err(|error| Error::new(error.description())) } +/// Use a public key to verify a signature of a JSON object. +/// +/// # Parameters +/// +/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. +/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. +/// * signature: The `Signature` to verify. +/// * value: The `serde_json::Value` (JSON value) that was signed. +/// +/// # Errors +/// +/// Returns an error if verification fails. +pub fn verify_json(verifier: &V, public_key: &[u8], signature: &Signature, value: &Value) +-> Result<(), Error> where V: Verifier { + verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) +} + +/// An error when trying to extract the algorithm and version from a key identifier. +#[derive(Debug)] enum SplitError<'a> { InvalidLength(usize), UnknownAlgorithm(&'a str), } +/// Extract the algorithm and version from a key identifier. fn split_id(id: &str) -> Result<(Algorithm, String), SplitError> { const SIGNATURE_ID_LENGTH: usize = 2; @@ -208,13 +237,24 @@ fn split_id(id: &str) -> Result<(Algorithm, String), SplitError> { Ok((algorithm, signature_id[1].to_string())) } +/// The algorithm used for signing data. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Algorithm { + /// The Ed25519 digital signature algorithm. + Ed25519, +} + /// An Ed25519 key pair. pub struct Ed25519KeyPair { ring_key_pair: RingEd25519KeyPair, version: String, } -/// An error produced during signing or verification. +/// A verifier for Ed25519 digital signatures. +#[derive(Clone, Copy, Debug)] +pub struct Ed25519Verifier; + +/// An error produced when ruma_signatures operations fail. #[derive(Clone, Debug)] pub struct Error { message: String, @@ -242,11 +282,11 @@ pub trait KeyPair: Sized { /// /// # Parameters /// - /// * message: An arbitrary binary value to sign. + /// * message: An arbitrary series of bytes to sign. fn sign(&self, message: &[u8]) -> Signature; } -/// A single digital signature. +/// A digital signature. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Signature { algorithm: Algorithm, @@ -254,7 +294,7 @@ pub struct Signature { version: String, } -/// A map of server names to sets of signatures created by that server. +/// A map of server names to sets of digital signatures created by that server. #[derive(Clone, Debug)] pub struct Signatures { map: HashMap @@ -263,7 +303,7 @@ pub struct Signatures { /// Serde Visitor for deserializing `Signatures`. struct SignaturesVisitor; -/// A set of signatures created by a single homeserver. +/// A set of digital signatures created by a single homeserver. #[derive(Clone, Debug)] pub struct SignatureSet { set: HashSet, @@ -272,11 +312,21 @@ pub struct SignatureSet { /// Serde Visitor for deserializing `SignatureSet`. struct SignatureSetVisitor; -/// The algorithm used for signing data. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Algorithm { - /// The Ed25519 digital signature algorithm. - Ed25519, +/// 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 public key of the key pair used to sign the message. + /// * signature: The `Signature` to verify. + /// * message: The message that was signed. + /// + /// # Errors + /// + /// Returns an error if verification fails. + fn verify_json(&self, public_key: &[u8], signature: &Signature, message: &[u8]) + -> Result<(), Error>; } impl KeyPair for Ed25519KeyPair { @@ -299,7 +349,31 @@ impl KeyPair for Ed25519KeyPair { } } +impl Ed25519Verifier { + /// Creates a new `Ed25519Verifier`. + pub fn new() -> Self { + Ed25519Verifier + } +} + +impl Verifier for Ed25519Verifier { + fn verify_json(&self, public_key: &[u8], signature: &Signature, message: &[u8]) + -> Result<(), Error> { + verify( + &ED25519, + Input::from(public_key), + Input::from(message), + Input::from(signature.as_bytes()), + ).map_err(|_| Error::new("signature verification failed")) + } +} + impl Error { + /// Creates a new error. + /// + /// # Parameters + /// + /// * message: The error message. pub fn new(message: T) -> Self where T: Into { Error { message: message.into(), @@ -321,6 +395,15 @@ impl Display for Error { impl Signature { /// Creates a signature from raw bytes. + /// + /// # 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 identifier is invalid. pub fn new(id: &str, bytes: &[u8]) -> Result { let (algorithm, version) = split_id(id).map_err(|split_error| { match split_error { @@ -353,25 +436,12 @@ impl Signature { self.signature.as_slice().to_base64(BASE64_CONFIG) } - /// A string containing the signature algorithm and the key "version" separated by a colon. + /// 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) } - /// Use the public key to verify the signature against the JSON object that was signed. - pub fn verify_json(&self, public_key: &[u8], value: &Value) -> Result<(), Error> { - match self.algorithm { - Algorithm::Ed25519 => { - verify( - &ED25519, - Input::from(public_key), - Input::from(to_canonical_json(value)?.as_bytes()), - Input::from(self.as_bytes()), - ).map_err(|_| Error::new("signature verification failed")) - } - } - } - /// The "version" of the key used for this signature. /// /// Versions are used as an identifier to distinguish signatures generated from different keys @@ -390,6 +460,10 @@ impl Signatures { } /// Initializes a new empty Signatures with room for a specific number of servers. + /// + /// # Parameters + /// + /// * capacity: The number of items to allocate memory for. pub fn with_capacity(capacity: usize) -> Self { Signatures { map: HashMap::with_capacity(capacity), @@ -401,6 +475,11 @@ impl Signatures { /// If no signature set for the given server existed in the collection, `None` is returned. /// Otherwise, the signature set is returned. /// + /// # Parameters + /// + /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. + /// * signature_set: The `SignatureSet` containing the digital signatures made by the server. + /// /// # Errors /// /// Returns an error if the given server name cannot be parsed as a valid host. @@ -472,6 +551,10 @@ impl SignatureSet { } /// Initializes a new empty SignatureSet with room for a specific number of signatures. + /// + /// # Parameters + /// + /// * capacity: The number of items to allocate memory for. pub fn with_capacity(capacity: usize) -> Self { SignatureSet { set: HashSet::with_capacity(capacity), @@ -482,6 +565,10 @@ impl SignatureSet { /// /// The boolean return value indicates whether or not the value was actually inserted, since /// subsequent inserts of the same signature have no effect. + /// + /// # Parameters + /// + /// * signature: A `Signature` to insert into the set. pub fn insert(&mut self, signature: Signature) -> bool { self.set.insert(signature) } @@ -561,7 +648,16 @@ mod test { use rustc_serialize::base64::FromBase64; use serde_json::{from_str, to_string, to_value}; - use super::{Ed25519KeyPair, KeyPair, Signature, Signatures, SignatureSet, sign_json}; + use super::{ + Ed25519KeyPair, + Ed25519Verifier, + KeyPair, + Signature, + Signatures, + SignatureSet, + sign_json, + verify_json, + }; const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; const PRIVATE_KEY: &'static str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; @@ -595,7 +691,11 @@ mod test { let value = from_str("{}").unwrap(); - assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); + let verifier = Ed25519Verifier::new(); + + assert!( + verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_ok() + ); } #[test] @@ -680,13 +780,24 @@ mod test { r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); - assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_ok()); + let verifier = Ed25519Verifier::new(); + + assert!( + verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_ok() + ); let reverse_value = from_str( r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); - assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &reverse_value).is_ok()); + assert!( + verify_json( + &verifier, + &PUBLIC_KEY.from_base64().unwrap(), + &signature, + &reverse_value, + ).is_ok() + ); } #[test] @@ -731,6 +842,10 @@ mod test { let value = from_str(r#"{"not":"empty"}"#).unwrap(); - assert!(signature.verify_json(&PUBLIC_KEY.from_base64().unwrap(), &value).is_err()); + let verifier = Ed25519Verifier::new(); + + assert!( + verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_err() + ); } } From 8db6b94af345fdb2dc37fff9a111239c455536fa Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 16:01:59 -0800 Subject: [PATCH 12/98] Add keywords to crate manifest. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 75c29b16..c9d5d982 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Jimmy Cuadra "] description = "Digital signatures according to the Matrix specification." documentation = "https://docs.rs/ruma-signatures" homepage = "https://github.com/ruma/ruma-signatures" -keywords = ["matrix", "chat", "messaging", "ruma"] +keywords = ["matrix", "chat", "messaging", "ruma", "crypto", "cryptography"] license = "MIT" name = "ruma-signatures" readme = "README.md" From 97df65e3d7c7baa497123f6429c2c3ab3bc1625a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 16:02:55 -0800 Subject: [PATCH 13/98] Remove a crate keyword, only 5 are allowed. [ci skip] --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c9d5d982..b9f4ca6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Jimmy Cuadra "] description = "Digital signatures according to the Matrix specification." documentation = "https://docs.rs/ruma-signatures" homepage = "https://github.com/ruma/ruma-signatures" -keywords = ["matrix", "chat", "messaging", "ruma", "crypto", "cryptography"] +keywords = ["matrix", "chat", "messaging", "ruma", "cryptography"] license = "MIT" name = "ruma-signatures" readme = "README.md" From 9b0bd2511fea1b2198f6cc9cfea468b53e429923 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 16:15:50 -0800 Subject: [PATCH 14/98] Update README for docs and more stable status. [ci skip] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1e9a8a21..a642afa6 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ruma-signatures provides functionality for creating digital signatures according to the [Matrix](https://matrix.org/) specification. -## Status +## Documentation -This project is currently experimental and is very likely to change drastically. +ruma-signatures has [comprehensive documentation](https://docs.rs/ruma-signatures) available on docs.rs. ## License From 2a7f43a8056b010fe0a35fe819c391b6a3652464 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 23:23:27 -0800 Subject: [PATCH 15/98] Encrypt IRC channel name for Travis notifications. See https://github.com/travis-ci/travis-ci/issues/1094 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4e2e913d..3dc5acbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ notifications: email: false irc: channels: - - "chat.freenode.net#ruma" + - secure: "bI2kShW50RpX6ptnZQEzv8BSnejCskATLMvhKndXsTEJhrBIxC/IgOyWi9WTwmYWXYgupuKYRe+z5dk/BEWBa+JfKh+UYOGtwE5Y5T9S4JV2rDQjxb7xSI/vwLTqlDHhzBvqK+9JxfsqitS7FFVGtmNEXFc27f2NwC3/uW/CAM1iPD+K+Xe9ZoqAT0+03tdJtPY9kVE8+h3k5V/KAo4ENx8MqQ7g31r8ch8twFUjuknMhR9Les5trNoMUJk46YluhThUgSAvcjo0+n43cxfv2t0Qx7NSzTc0myLfK0nRMRAuFEMeSnWJWnSLWLKSeluEsYN8RPdCQGNlAzoftDSBPAgDBtWS8FYqQZVbkxBJepdczjy+PFAgjjBr+eqMRjPz1t83ThElefPxkQUshzQU3DVifUwSryNhF58tmZ/4iiCyw0fIPZL1m7n0OivpBNkTLFqeSHTZgGiCBThc3MFarL6qOcBkFZWynG19lNQ+s9BzdVeN/veFWRp6m1lOymPSCVVDt7KMoloEK2ie1wkVzvc6IlFRXJgDWU+C4fy+nue2ghdAv4ZQ1D+JDX2M4DCrtfWBZsdTR+UsQL8S8fY0zOcQw0y39qh4AsoKeArvjUfWxfDPx5oPSqpwM8Mvmdp/OAC5knUeSxBtjpnnPPxcucE2oQ07QYq3/SHnjeEqhrA=" use_notice: true rust: - "nightly" From ca1aeace5bbd95dc302490b3aa061a5a02abcb72 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 26 Jan 2017 00:46:26 -0800 Subject: [PATCH 16/98] Update to serde 0.9, ring 0.6.2, and url 1.4. --- Cargo.toml | 10 +++--- src/lib.rs | 90 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9f4ca6b..988eda32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,12 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.1.0" [dependencies] -ring = "0.6.0-alpha1" +ring = "0.6.2" rustc-serialize = "0.3.22" -serde = "0.8.19" -serde_json = "0.8.4" +serde = "0.9.1" +serde_json = "0.9.1" untrusted = "0.3.2" -url = "1.2.3" +url = "1.4.0" [dev-dependencies] -serde_derive = "0.8.19" +serde_derive = "0.9.1" diff --git a/src/lib.rs b/src/lib.rs index cc969df1..8a4af2d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,9 +27,9 @@ //! &public_key, // &[u8] //! &private_key, // &[u8] //! "1".to_string(), // The "version" of the key. -//! ).unwrap(); -//! let value = serde_json::from_str("{}").unwrap(); // An empty JSON object. -//! let signature = ruma_signatures::sign_json(&key_pair, &value).unwrap(); // `Signature` +//! ).expect("the provided keys should be suitable for Ed25519"); +//! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); +//! ruma_signatures::sign_json(&key_pair, &value).expect("value is a a JSON object"); // `Signature` //! # } //! ``` //! @@ -48,8 +48,10 @@ //! # fn main() { //! # let public_key = [0; 32]; //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); -//! let value = serde_json::from_str("{}").unwrap(); // The same empty JSON object. +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( +//! "key identifier should be valid" +//! ); +//! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); //! let verifier = ruma_signatures::Ed25519Verifier::new(); //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! # } @@ -86,10 +88,12 @@ //! # extern crate serde_json; //! # fn main() { //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( +//! "key identifier should be valid" +//! ); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); -//! let json = serde_json::to_string(&signature_set).unwrap(); +//! serde_json::to_string(&signature_set).expect("signature_set should serialize"); //! # } //! ``` //! @@ -105,18 +109,19 @@ //! # extern crate serde_json; //! # fn main() { //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( +//! "key identifier should be valid" +//! ); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! let mut signatures = ruma_signatures::Signatures::new(); -//! signatures.insert("example.com", signature_set); -//! let json = serde_json::to_string(&signatures).unwrap(); +//! signatures.insert("example.com", signature_set).expect("example.com is a valid server name"); +//! serde_json::to_string(&signatures).expect("signatures should serialize"); //! # } //! ``` //! //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. -#![cfg_attr(test, feature(proc_macro))] #![deny(missing_docs)] extern crate ring; @@ -135,8 +140,9 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; use rustc_serialize::base64::{CharacterSet, Config, FromBase64, Newline, ToBase64}; -use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; -use serde::de::{MapVisitor, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::de::{Error as SerdeError, MapVisitor, Unexpected, Visitor}; +use serde::ser::SerializeMap; use serde_json::{Value, to_string}; use untrusted::Input; use url::Url; @@ -183,7 +189,7 @@ pub fn to_canonical_json(value: &Value) -> Result { let mut owned_value = value.clone(); { - let mut object = owned_value.as_object_mut().unwrap(); // Safe since we checked above. + let mut object = owned_value.as_object_mut().expect("safe since we checked above"); object.remove("signatures"); object.remove("unsigned"); } @@ -505,39 +511,41 @@ impl Signatures { } impl Deserialize for Signatures { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize_map(SignaturesVisitor) } } impl Serialize for Signatures { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { - let mut state = try!(serializer.serialize_map(Some(self.len()))); + fn serialize(&self, serializer: S) -> Result where S: Serializer { + let mut map_serializer = serializer.serialize_map(Some(self.len()))?; for (host, signature_set) in self.map.iter() { - try!(serializer.serialize_map_key(&mut state, host.to_string())); - try!(serializer.serialize_map_value(&mut state, signature_set)); + map_serializer.serialize_key(&host.to_string())?; + map_serializer.serialize_value(signature_set)?; } - serializer.serialize_map_end(state) + map_serializer.end() } } impl Visitor for SignaturesVisitor { type Value = Signatures; - fn visit_map(&mut self, mut visitor: M) -> Result + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "digital signatures") + } + + fn visit_map(self, mut visitor: M) -> Result where M: MapVisitor { let mut signatures = Signatures::with_capacity(visitor.size_hint().0); - while let Some((server_name, signature_set)) = try!(visitor.visit::()) { + while let Some((server_name, signature_set)) = visitor.visit::()? { if let Err(_) = signatures.insert(&server_name, signature_set) { - return Err(M::Error::invalid_value(&server_name)); + return Err(M::Error::invalid_value(Unexpected::Str(&server_name), &self)); } } - try!(visitor.end()); - Ok(signatures) } } @@ -580,36 +588,42 @@ impl SignatureSet { } impl Deserialize for SignatureSet { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize_map(SignatureSetVisitor) } } impl Serialize for SignatureSet { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { - let mut state = try!(serializer.serialize_map(Some(self.len()))); + fn serialize(&self, serializer: S) -> Result where S: Serializer { + let mut map_serializer = serializer.serialize_map(Some(self.len()))?; for signature in self.set.iter() { - try!(serializer.serialize_map_key(&mut state, signature.id())); - try!(serializer.serialize_map_value(&mut state, signature.base64())); + map_serializer.serialize_key(&signature.id())?; + map_serializer.serialize_value(&signature.base64())?; } - serializer.serialize_map_end(state) + map_serializer.end() } } impl Visitor for SignatureSetVisitor { type Value = SignatureSet; - fn visit_map(&mut self, mut visitor: M) -> Result + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a set of digital signatures") + } + + fn visit_map(self, mut visitor: M) -> Result where M: MapVisitor { let mut signature_set = SignatureSet::with_capacity(visitor.size_hint().0); - while let Some((key, value)) = try!(visitor.visit::()) { + while let Some((key, value)) = visitor.visit::()? { let (algorithm, version) = split_id(&key).map_err(|split_error| { match split_error { - SplitError::InvalidLength(length) => M::Error::invalid_length(length), - SplitError::UnknownAlgorithm(algorithm) => M::Error::invalid_value(algorithm), + SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), + SplitError::UnknownAlgorithm(algorithm) => { + M::Error::invalid_value(Unexpected::Str(algorithm), &self) + } } })?; @@ -627,8 +641,6 @@ impl Visitor for SignatureSetVisitor { signature_set.insert(signature); } - try!(visitor.end()); - Ok(signature_set) } } @@ -758,12 +770,12 @@ mod test { one: 1, }; - let alpha_value = to_value(alpha); + let alpha_value = to_value(alpha).expect("alpha should serialize"); let alpha_signature = sign_json(&key_pair, &alpha_value).unwrap(); assert_eq!(alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); - let reverse_alpha_value = to_value(reverse_alpha); + let reverse_alpha_value = to_value(reverse_alpha).expect("reverse_alpha should serialize"); let reverse_alpha_signature = sign_json(&key_pair, &reverse_alpha_value).unwrap(); assert_eq!(reverse_alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); From 0bfcf340d9a010038bd9ca26c958f392e97a58ec Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 26 Jan 2017 00:52:59 -0800 Subject: [PATCH 17/98] Bump version to 0.2.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 988eda32..f5ee29b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.1.0" +version = "0.2.0" [dependencies] ring = "0.6.2" From b581715ffbd9fb2be7793687cd1619be14615736 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 26 Jan 2017 23:10:55 +0100 Subject: [PATCH 18/98] Update ring to fix build error on latest nightly See https://github.com/briansmith/ring/pull/430 for details --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f5ee29b5..ff4d7d88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.2.0" [dependencies] -ring = "0.6.2" +ring = "0.6.3" rustc-serialize = "0.3.22" serde = "0.9.1" serde_json = "0.9.1" From 0590867dc132ee950326fab097105f40ead3c956 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 27 Jan 2017 01:50:00 -0800 Subject: [PATCH 19/98] Bump version to 0.2.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ff4d7d88..a9221de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.2.0" +version = "0.2.1" [dependencies] ring = "0.6.3" From e63b0fbcd5ae9921ee3fcca8f5904d051e4b9817 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Apr 2017 22:41:59 -0700 Subject: [PATCH 20/98] Update ring to 0.7 and serde to 1.0. --- Cargo.toml | 14 +++++++------- src/lib.rs | 34 +++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9221de9..28160f2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,12 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.2.1" [dependencies] -ring = "0.6.3" -rustc-serialize = "0.3.22" -serde = "0.9.1" -serde_json = "0.9.1" -untrusted = "0.3.2" -url = "1.4.0" +ring = "0.7" +rustc-serialize = "0.3" +serde = "1.0" +serde_json = "1.0" +untrusted = "0.3" +url = "1.4" [dev-dependencies] -serde_derive = "0.9.1" +serde_derive = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 8a4af2d8..4e92c1aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,7 +141,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; use rustc_serialize::base64::{CharacterSet, Config, FromBase64, Newline, ToBase64}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde::de::{Error as SerdeError, MapVisitor, Unexpected, Visitor}; +use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::ser::SerializeMap; use serde_json::{Value, to_string}; use untrusted::Input; @@ -510,8 +510,8 @@ impl Signatures { } } -impl Deserialize for Signatures { - fn deserialize(deserializer: D) -> Result where D: Deserializer { +impl<'de> Deserialize<'de> for Signatures { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { deserializer.deserialize_map(SignaturesVisitor) } } @@ -529,18 +529,20 @@ impl Serialize for Signatures { } } -impl Visitor for SignaturesVisitor { +impl<'de> Visitor<'de> for SignaturesVisitor { type Value = Signatures; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { write!(formatter, "digital signatures") } - fn visit_map(self, mut visitor: M) -> Result - where M: MapVisitor { - let mut signatures = Signatures::with_capacity(visitor.size_hint().0); + fn visit_map(self, mut visitor: M) -> Result where M: MapAccess<'de> { + let mut signatures = match visitor.size_hint() { + Some(capacity) => Signatures::with_capacity(capacity), + None => Signatures::new(), + }; - while let Some((server_name, signature_set)) = visitor.visit::()? { + while let Some((server_name, signature_set)) = visitor.next_entry::()? { if let Err(_) = signatures.insert(&server_name, signature_set) { return Err(M::Error::invalid_value(Unexpected::Str(&server_name), &self)); } @@ -587,8 +589,8 @@ impl SignatureSet { } } -impl Deserialize for SignatureSet { - fn deserialize(deserializer: D) -> Result where D: Deserializer { +impl<'de> Deserialize<'de> for SignatureSet { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { deserializer.deserialize_map(SignatureSetVisitor) } } @@ -606,18 +608,20 @@ impl Serialize for SignatureSet { } } -impl Visitor for SignatureSetVisitor { +impl<'de> Visitor<'de> for SignatureSetVisitor { type Value = SignatureSet; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { write!(formatter, "a set of digital signatures") } - fn visit_map(self, mut visitor: M) -> Result - where M: MapVisitor { - let mut signature_set = SignatureSet::with_capacity(visitor.size_hint().0); + fn visit_map(self, mut visitor: M) -> Result where M: MapAccess<'de> { + let mut signature_set = match visitor.size_hint() { + Some(capacity) => SignatureSet::with_capacity(capacity), + None => SignatureSet::new(), + }; - while let Some((key, value)) = visitor.visit::()? { + while let Some((key, value)) = visitor.next_entry::()? { let (algorithm, version) = split_id(&key).map_err(|split_error| { match split_error { SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), From 26f618cb2d6a5b9eb01a2a11b5ed73221148affe Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Apr 2017 22:42:39 -0700 Subject: [PATCH 21/98] Bump version to 0.3.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 28160f2d..551ffe45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.2.1" +version = "0.3.0" [dependencies] ring = "0.7" From fbc1960ab902deae28aa21a86873e900130af1e9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 24 Apr 2017 00:13:57 -0700 Subject: [PATCH 22/98] Replace rustc-serialize with base64. Fixes #2. --- Cargo.toml | 3 ++- src/lib.rs | 68 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 551ffe45..dc17e861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,9 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.3.0" [dependencies] +base64 = "0.5.0" +lazy_static = "0.2.8" ring = "0.7" -rustc-serialize = "0.3" serde = "1.0" serde_json = "1.0" untrusted = "0.3" diff --git a/src/lib.rs b/src/lib.rs index 4e92c1aa..9305da39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,8 +124,10 @@ #![deny(missing_docs)] +extern crate base64; +#[macro_use] +extern crate lazy_static; extern crate ring; -extern crate rustc_serialize; extern crate serde; #[cfg(test)] #[macro_use] @@ -138,8 +140,8 @@ use std::collections::{HashMap, HashSet}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; +use base64::{CharacterSet, Config, LineWrap, decode_config, encode_config}; use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; -use rustc_serialize::base64::{CharacterSet, Config, FromBase64, Newline, ToBase64}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::ser::SerializeMap; @@ -149,12 +151,14 @@ use url::Url; pub use url::Host; -static BASE64_CONFIG: Config = Config { - char_set: CharacterSet::Standard, - newline: Newline::CRLF, - pad: false, - line_length: None, -}; +lazy_static! { + static ref BASE64_CONFIG: Config = Config::new( + CharacterSet::Standard, + false, + false, + LineWrap::NoWrap, + ); +} /// Signs an arbitrary JSON object. /// @@ -439,7 +443,7 @@ impl Signature { /// A Base64 encoding of the signature. pub fn base64(&self) -> String { - self.signature.as_slice().to_base64(BASE64_CONFIG) + encode_config(self.signature.as_slice(), *BASE64_CONFIG) } /// The key identifier, a string containing the signature algorithm and the key "version" @@ -631,7 +635,7 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { } })?; - let signature_bytes: Vec = match value.from_base64() { + let signature_bytes: Vec = match decode_config(&value, *BASE64_CONFIG) { Ok(raw) => raw, Err(error) => return Err(M::Error::custom(error.description())), }; @@ -661,10 +665,11 @@ impl Display for Algorithm { #[cfg(test)] mod test { - use rustc_serialize::base64::FromBase64; + use base64::decode_config; use serde_json::{from_str, to_string, to_value}; use super::{ + BASE64_CONFIG, Ed25519KeyPair, Ed25519Verifier, KeyPair, @@ -686,8 +691,8 @@ mod test { #[test] fn sign_empty_json() { let key_pair = Ed25519KeyPair::new( - &PUBLIC_KEY.from_base64().unwrap(), - &PRIVATE_KEY.from_base64().unwrap(), + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), + decode_config(&PRIVATE_KEY, *BASE64_CONFIG).unwrap().as_slice(), "1".to_string(), ).unwrap(); @@ -702,7 +707,7 @@ mod test { fn verify_empty_json() { let signature = Signature::new( "ed25519:1", - &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), ).unwrap(); let value = from_str("{}").unwrap(); @@ -710,7 +715,12 @@ mod test { let verifier = Ed25519Verifier::new(); assert!( - verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_ok() + verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), + &signature, + &value, + ).is_ok() ); } @@ -723,7 +733,7 @@ mod test { let signature = Signature::new( "ed25519:1", - &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), ).unwrap(); let mut signature_set = SignatureSet::with_capacity(1); @@ -759,8 +769,8 @@ mod test { } let key_pair = Ed25519KeyPair::new( - &PUBLIC_KEY.from_base64().unwrap(), - &PRIVATE_KEY.from_base64().unwrap(), + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), + decode_config(&PRIVATE_KEY, *BASE64_CONFIG).unwrap().as_slice(), "1".to_string(), ).unwrap(); @@ -789,7 +799,7 @@ mod test { fn verify_minimal_json() { let signature = Signature::new( "ed25519:1", - &MINIMAL_JSON_SIGNATURE.from_base64().unwrap(), + decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), ).unwrap(); let value = from_str( @@ -799,7 +809,12 @@ mod test { let verifier = Ed25519Verifier::new(); assert!( - verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_ok() + verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), + &signature, + &value, + ).is_ok() ); let reverse_value = from_str( @@ -809,7 +824,7 @@ mod test { assert!( verify_json( &verifier, - &PUBLIC_KEY.from_base64().unwrap(), + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), &signature, &reverse_value, ).is_ok() @@ -827,7 +842,7 @@ mod test { let signature = Signature::new( "ed25519:1", - &MINIMAL_JSON_SIGNATURE.from_base64().unwrap(), + decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), ).unwrap(); let mut signature_set = SignatureSet::with_capacity(1); @@ -853,7 +868,7 @@ mod test { fn fail_verify() { let signature = Signature::new( "ed25519:1", - &EMPTY_JSON_SIGNATURE.from_base64().unwrap(), + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), ).unwrap(); let value = from_str(r#"{"not":"empty"}"#).unwrap(); @@ -861,7 +876,12 @@ mod test { let verifier = Ed25519Verifier::new(); assert!( - verify_json(&verifier, &PUBLIC_KEY.from_base64().unwrap(), &signature, &value).is_err() + verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), + &signature, + &value, + ).is_err() ); } } From 23e01211254a81c9124740bcbb439b845cbacfc7 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 8 May 2017 17:38:03 -1000 Subject: [PATCH 23/98] Update to *ring* 0.9.4. ' --- Cargo.toml | 6 +++--- src/lib.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc17e861..9c03a0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,15 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.3.0" +version = "0.4.0" [dependencies] base64 = "0.5.0" lazy_static = "0.2.8" -ring = "0.7" +ring = "0.9.4" serde = "1.0" serde_json = "1.0" -untrusted = "0.3" +untrusted = "0.5" url = "1.4" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 9305da39..62b6e557 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -342,9 +342,9 @@ pub trait Verifier { impl KeyPair for Ed25519KeyPair { fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { Ok(Ed25519KeyPair { - ring_key_pair: RingEd25519KeyPair::from_bytes( - private_key, - public_key, + ring_key_pair: RingEd25519KeyPair::from_seed_and_public_key( + untrusted::Input::from(private_key), + untrusted::Input::from(public_key), ).map_err(|_| Error::new("invalid key pair"))?, version: version, }) @@ -353,7 +353,7 @@ impl KeyPair for Ed25519KeyPair { fn sign(&self, message: &[u8]) -> Signature { Signature { algorithm: Algorithm::Ed25519, - signature: self.ring_key_pair.sign(message).as_slice().to_vec(), + signature: self.ring_key_pair.sign(message).as_ref().to_vec(), version: self.version.clone(), } } From 86135ddeb085416ac5a9cc86cc9c643dd00f031f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 9 Nov 2018 11:20:56 +0100 Subject: [PATCH 24/98] Update base64 to latest version --- Cargo.toml | 2 +- src/lib.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c03a0e1..3bd44cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.4.0" [dependencies] -base64 = "0.5.0" +base64 = "0.10.0" lazy_static = "0.2.8" ring = "0.9.4" serde = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 62b6e557..441ac25b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,7 +140,7 @@ use std::collections::{HashMap, HashSet}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; -use base64::{CharacterSet, Config, LineWrap, decode_config, encode_config}; +use base64::{CharacterSet, Config, decode_config, encode_config}; use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; @@ -155,8 +155,6 @@ lazy_static! { static ref BASE64_CONFIG: Config = Config::new( CharacterSet::Standard, false, - false, - LineWrap::NoWrap, ); } From bd23e42cfca20964b69039dcccd1f7302bfac38c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 9 Nov 2018 11:24:35 +0100 Subject: [PATCH 25/98] Update lazy_static, url, ring, untrusted --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3bd44cf1..338553ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ version = "0.4.0" [dependencies] base64 = "0.10.0" -lazy_static = "0.2.8" -ring = "0.9.4" +lazy_static = "1.2" +ring = "0.13.3" serde = "1.0" serde_json = "1.0" -untrusted = "0.5" -url = "1.4" +untrusted = "0.6.2" +url = "1.7" [dev-dependencies] serde_derive = "1.0" From 1334fc37e5b33725ab62935ee7b66f1ac1bb56aa Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 9 Nov 2018 11:28:36 +0100 Subject: [PATCH 26/98] Remove unused mut --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 441ac25b..d2c4f9b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,7 @@ pub fn to_canonical_json(value: &Value) -> Result { let mut owned_value = value.clone(); { - let mut object = owned_value.as_object_mut().expect("safe since we checked above"); + let object = owned_value.as_object_mut().expect("safe since we checked above"); object.remove("signatures"); object.remove("unsigned"); } From 0c299b6226ac92af6c0da8d5f2e24045cc766161 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Dec 2018 18:50:58 -0800 Subject: [PATCH 27/98] Update dependencies. --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 338553ed..410c901b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ version = "0.4.0" [dependencies] base64 = "0.10.0" -lazy_static = "1.2" -ring = "0.13.3" -serde = "1.0" -serde_json = "1.0" +lazy_static = "1.2.0" +ring = "0.13.5" +serde = "1.0.80" +serde_json = "1.0.33" untrusted = "0.6.2" -url = "1.7" +url = "1.7.2" [dev-dependencies] -serde_derive = "1.0" +serde_derive = "1.0.80" From e3883bfbedc048d46c9131ee054ae6e85bba3a74 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Dec 2018 18:56:08 -0800 Subject: [PATCH 28/98] Bump version to 0.4.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 410c901b..3d28424e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.4.0" +version = "0.4.1" [dependencies] base64 = "0.10.0" From df1fc1739b0825b756e2766b7046400c6eb19b19 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 7 Dec 2018 17:16:47 -0800 Subject: [PATCH 29/98] Use edition 2018. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 3d28424e..971a3ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ authors = ["Jimmy Cuadra "] 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" From 9f478a1d5104ee2805f4406db7d44257f1f55044 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 7 Dec 2018 17:20:59 -0800 Subject: [PATCH 30/98] Use Rust 2018 edition idioms. --- src/lib.rs | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d2c4f9b0..4cccb92f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,25 +122,14 @@ //! //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. -#![deny(missing_docs)] - -extern crate base64; -#[macro_use] -extern crate lazy_static; -extern crate ring; -extern crate serde; -#[cfg(test)] -#[macro_use] -extern crate serde_derive; -extern crate serde_json; -extern crate untrusted; -extern crate url; +#![deny(missing_docs, warnings)] use std::collections::{HashMap, HashSet}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; use base64::{CharacterSet, Config, decode_config, encode_config}; +use lazy_static::lazy_static; use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; @@ -224,7 +213,7 @@ enum SplitError<'a> { } /// Extract the algorithm and version from a key identifier. -fn split_id(id: &str) -> Result<(Algorithm, String), SplitError> { +fn split_id(id: &str) -> Result<(Algorithm, String), SplitError<'_>> { const SIGNATURE_ID_LENGTH: usize = 2; let signature_id: Vec<&str> = id.split(':').collect(); @@ -341,8 +330,8 @@ impl KeyPair for Ed25519KeyPair { fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { Ok(Ed25519KeyPair { ring_key_pair: RingEd25519KeyPair::from_seed_and_public_key( - untrusted::Input::from(private_key), - untrusted::Input::from(public_key), + Input::from(private_key), + Input::from(public_key), ).map_err(|_| Error::new("invalid key pair"))?, version: version, }) @@ -396,7 +385,7 @@ impl StdError for Error { } impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, "{}", self.message) } } @@ -534,7 +523,7 @@ impl Serialize for Signatures { impl<'de> Visitor<'de> for SignaturesVisitor { type Value = Signatures; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "digital signatures") } @@ -613,7 +602,7 @@ impl Serialize for SignatureSet { impl<'de> Visitor<'de> for SignatureSetVisitor { type Value = SignatureSet; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a set of digital signatures") } @@ -652,7 +641,7 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { } impl Display for Algorithm { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let name = match *self { Algorithm::Ed25519 => "ed25519", }; @@ -724,7 +713,7 @@ mod test { #[test] fn signatures_empty_json() { - #[derive(Serialize)] + #[derive(serde_derive::Serialize)] struct EmptyWithSignatures { signatures: Signatures, } @@ -754,13 +743,13 @@ mod test { #[test] fn sign_minimal_json() { - #[derive(Serialize)] + #[derive(serde_derive::Serialize)] struct Alpha { one: u8, two: String, } - #[derive(Serialize)] + #[derive(serde_derive::Serialize)] struct ReverseAlpha { two: String, one: u8, @@ -831,7 +820,7 @@ mod test { #[test] fn signatures_minimal_json() { - #[derive(Serialize)] + #[derive(serde_derive::Serialize)] struct MinimalWithSignatures { one: u8, signatures: Signatures, From 6ff12a74d2de6d584682b8ac3e6cf06000fd6bd3 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 7 Dec 2018 17:29:14 -0800 Subject: [PATCH 31/98] Run rustfmt and add it to CI runs. --- .travis.yml | 6 ++ rustfmt.toml | 2 + src/lib.rs | 266 ++++++++++++++++++++++++++++++++------------------- 3 files changed, 177 insertions(+), 97 deletions(-) create mode 100644 rustfmt.toml diff --git a/.travis.yml b/.travis.yml index 3dc5acbe..d9dea07d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,10 @@ language: "rust" +before_script: + - "rustup component add rustfmt" +script: + - "cargo fmt --all -- --check" + - "cargo build --verbose" + - "cargo test --verbose" notifications: email: false irc: diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..84896d4e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +imports_indent = "Block" +imports_layout = "HorizontalVertical" diff --git a/src/lib.rs b/src/lib.rs index 4cccb92f..5e985e29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,23 +128,20 @@ use std::collections::{HashMap, HashSet}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; -use base64::{CharacterSet, Config, decode_config, encode_config}; +use base64::{decode_config, encode_config, CharacterSet, Config}; use lazy_static::lazy_static; -use ring::signature::{ED25519, Ed25519KeyPair as RingEd25519KeyPair, verify}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use ring::signature::{verify, Ed25519KeyPair as RingEd25519KeyPair, ED25519}; use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::ser::SerializeMap; -use serde_json::{Value, to_string}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{to_string, Value}; use untrusted::Input; use url::Url; pub use url::Host; lazy_static! { - static ref BASE64_CONFIG: Config = Config::new( - CharacterSet::Standard, - false, - ); + static ref BASE64_CONFIG: Config = Config::new(CharacterSet::Standard, false); } /// Signs an arbitrary JSON object. @@ -157,7 +154,10 @@ lazy_static! { /// # Errors /// /// Returns an error if the JSON value is not a JSON object. -pub fn sign_json(key_pair: &K, value: &Value) -> Result where K: KeyPair { +pub fn sign_json(key_pair: &K, value: &Value) -> Result +where + K: KeyPair, +{ let json = to_canonical_json(value)?; Ok(key_pair.sign(json.as_bytes())) @@ -180,7 +180,9 @@ pub fn to_canonical_json(value: &Value) -> Result { let mut owned_value = value.clone(); { - let object = owned_value.as_object_mut().expect("safe since we checked above"); + let object = owned_value + .as_object_mut() + .expect("safe since we checked above"); object.remove("signatures"); object.remove("unsigned"); } @@ -200,8 +202,15 @@ pub fn to_canonical_json(value: &Value) -> Result { /// # Errors /// /// Returns an error if verification fails. -pub fn verify_json(verifier: &V, public_key: &[u8], signature: &Signature, value: &Value) --> Result<(), Error> where V: Verifier { +pub fn verify_json( + verifier: &V, + public_key: &[u8], + signature: &Signature, + value: &Value, +) -> Result<(), Error> +where + V: Verifier, +{ verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) } @@ -294,7 +303,7 @@ pub struct Signature { /// A map of server names to sets of digital signatures created by that server. #[derive(Clone, Debug)] pub struct Signatures { - map: HashMap + map: HashMap, } /// Serde Visitor for deserializing `Signatures`. @@ -322,8 +331,12 @@ pub trait Verifier { /// # Errors /// /// Returns an error if verification fails. - fn verify_json(&self, public_key: &[u8], signature: &Signature, message: &[u8]) - -> Result<(), Error>; + fn verify_json( + &self, + public_key: &[u8], + signature: &Signature, + message: &[u8], + ) -> Result<(), Error>; } impl KeyPair for Ed25519KeyPair { @@ -332,7 +345,8 @@ impl KeyPair for Ed25519KeyPair { ring_key_pair: RingEd25519KeyPair::from_seed_and_public_key( Input::from(private_key), Input::from(public_key), - ).map_err(|_| Error::new("invalid key pair"))?, + ) + .map_err(|_| Error::new("invalid key pair"))?, version: version, }) } @@ -354,14 +368,19 @@ impl Ed25519Verifier { } impl Verifier for Ed25519Verifier { - fn verify_json(&self, public_key: &[u8], signature: &Signature, message: &[u8]) - -> Result<(), Error> { + fn verify_json( + &self, + public_key: &[u8], + signature: &Signature, + message: &[u8], + ) -> Result<(), Error> { verify( &ED25519, Input::from(public_key), Input::from(message), Input::from(signature.as_bytes()), - ).map_err(|_| Error::new("signature verification failed")) + ) + .map_err(|_| Error::new("signature verification failed")) } } @@ -371,7 +390,10 @@ impl Error { /// # Parameters /// /// * message: The error message. - pub fn new(message: T) -> Self where T: Into { + pub fn new(message: T) -> Self + where + T: Into, + { Error { message: message.into(), } @@ -402,12 +424,10 @@ impl Signature { /// /// Returns an error if the key identifier is invalid. pub fn new(id: &str, bytes: &[u8]) -> Result { - let (algorithm, version) = split_id(id).map_err(|split_error| { - match split_error { - SplitError::InvalidLength(_) => Error::new("malformed signature ID"), - SplitError::UnknownAlgorithm(algorithm) => { - Error::new(format!("unknown algorithm: {}", algorithm)) - } + let (algorithm, version) = split_id(id).map_err(|split_error| match split_error { + SplitError::InvalidLength(_) => Error::new("malformed signature ID"), + SplitError::UnknownAlgorithm(algorithm) => { + Error::new(format!("unknown algorithm: {}", algorithm)) } })?; @@ -480,12 +500,14 @@ impl Signatures { /// # Errors /// /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn insert(&mut self, server_name: &str, signature_set: SignatureSet) - -> Result, Error> { + pub fn insert( + &mut self, + server_name: &str, + signature_set: SignatureSet, + ) -> Result, Error> { let url_string = format!("https://{}", server_name); - let url = Url::parse(&url_string).map_err(|_| { - Error::new(format!("invalid server name: {}", server_name)) - })?; + let url = Url::parse(&url_string) + .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; let host = match url.host() { Some(host) => host.to_owned(), @@ -502,13 +524,19 @@ impl Signatures { } impl<'de> Deserialize<'de> for Signatures { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_map(SignaturesVisitor) } } impl Serialize for Signatures { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { let mut map_serializer = serializer.serialize_map(Some(self.len()))?; for (host, signature_set) in self.map.iter() { @@ -527,15 +555,23 @@ impl<'de> Visitor<'de> for SignaturesVisitor { write!(formatter, "digital signatures") } - fn visit_map(self, mut visitor: M) -> Result where M: MapAccess<'de> { + fn visit_map(self, mut visitor: M) -> Result + where + M: MapAccess<'de>, + { let mut signatures = match visitor.size_hint() { Some(capacity) => Signatures::with_capacity(capacity), None => Signatures::new(), }; - while let Some((server_name, signature_set)) = visitor.next_entry::()? { + while let Some((server_name, signature_set)) = + visitor.next_entry::()? + { if let Err(_) = signatures.insert(&server_name, signature_set) { - return Err(M::Error::invalid_value(Unexpected::Str(&server_name), &self)); + return Err(M::Error::invalid_value( + Unexpected::Str(&server_name), + &self, + )); } } @@ -581,13 +617,19 @@ impl SignatureSet { } impl<'de> Deserialize<'de> for SignatureSet { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_map(SignatureSetVisitor) } } impl Serialize for SignatureSet { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { let mut map_serializer = serializer.serialize_map(Some(self.len()))?; for signature in self.set.iter() { @@ -606,19 +648,20 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { write!(formatter, "a set of digital signatures") } - fn visit_map(self, mut visitor: M) -> Result where M: MapAccess<'de> { + fn visit_map(self, mut visitor: M) -> Result + where + M: MapAccess<'de>, + { let mut signature_set = match visitor.size_hint() { Some(capacity) => SignatureSet::with_capacity(capacity), None => SignatureSet::new(), }; while let Some((key, value)) = visitor.next_entry::()? { - let (algorithm, version) = split_id(&key).map_err(|split_error| { - match split_error { - SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), - SplitError::UnknownAlgorithm(algorithm) => { - M::Error::invalid_value(Unexpected::Str(algorithm), &self) - } + let (algorithm, version) = split_id(&key).map_err(|split_error| match split_error { + SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), + SplitError::UnknownAlgorithm(algorithm) => { + M::Error::invalid_value(Unexpected::Str(algorithm), &self) } })?; @@ -656,15 +699,15 @@ mod test { use serde_json::{from_str, to_string, to_value}; use super::{ - BASE64_CONFIG, + sign_json, + verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, - Signatures, SignatureSet, - sign_json, - verify_json, + Signatures, + BASE64_CONFIG, }; const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -678,10 +721,15 @@ mod test { #[test] fn sign_empty_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - decode_config(&PRIVATE_KEY, *BASE64_CONFIG).unwrap().as_slice(), + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + decode_config(&PRIVATE_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), "1".to_string(), - ).unwrap(); + ) + .unwrap(); let value = from_str("{}").unwrap(); @@ -694,21 +742,25 @@ mod test { fn verify_empty_json() { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), - ).unwrap(); + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + .unwrap() + .as_slice(), + ) + .unwrap(); let value = from_str("{}").unwrap(); let verifier = Ed25519Verifier::new(); - assert!( - verify_json( - &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - &signature, - &value, - ).is_ok() - ); + assert!(verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + &signature, + &value, + ) + .is_ok()); } #[test] @@ -720,8 +772,11 @@ mod test { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), - ).unwrap(); + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + .unwrap() + .as_slice(), + ) + .unwrap(); let mut signature_set = SignatureSet::with_capacity(1); signature_set.insert(signature); @@ -756,10 +811,15 @@ mod test { } let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - decode_config(&PRIVATE_KEY, *BASE64_CONFIG).unwrap().as_slice(), + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + decode_config(&PRIVATE_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), "1".to_string(), - ).unwrap(); + ) + .unwrap(); let alpha = Alpha { one: 1, @@ -786,8 +846,11 @@ mod test { fn verify_minimal_json() { let signature = Signature::new( "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), - ).unwrap(); + decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG) + .unwrap() + .as_slice(), + ) + .unwrap(); let value = from_str( r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# @@ -795,27 +858,29 @@ mod test { let verifier = Ed25519Verifier::new(); - assert!( - verify_json( - &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - &signature, - &value, - ).is_ok() - ); + assert!(verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + &signature, + &value, + ) + .is_ok()); let reverse_value = from_str( r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); - assert!( - verify_json( - &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - &signature, - &reverse_value, - ).is_ok() - ); + assert!(verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + &signature, + &reverse_value, + ) + .is_ok()); } #[test] @@ -829,8 +894,11 @@ mod test { let signature = Signature::new( "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), - ).unwrap(); + decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG) + .unwrap() + .as_slice(), + ) + .unwrap(); let mut signature_set = SignatureSet::with_capacity(1); signature_set.insert(signature); @@ -855,20 +923,24 @@ mod test { fn fail_verify() { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG).unwrap().as_slice(), - ).unwrap(); + decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + .unwrap() + .as_slice(), + ) + .unwrap(); let value = from_str(r#"{"not":"empty"}"#).unwrap(); let verifier = Ed25519Verifier::new(); - assert!( - verify_json( - &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG).unwrap().as_slice(), - &signature, - &value, - ).is_err() - ); + assert!(verify_json( + &verifier, + decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + .unwrap() + .as_slice(), + &signature, + &value, + ) + .is_err()); } } From 61e1ebb4cbc3c734cf5756126130d9e5bf965fc3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 8 Jan 2019 19:59:52 +0100 Subject: [PATCH 32/98] Configure rustfmt for nested imports, re-run 'cargo fmt' --- .rustfmt.toml | 1 + src/lib.rs | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..7d2cf549 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +merge_imports = true diff --git a/src/lib.rs b/src/lib.rs index 5e985e29..d4a58c2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,16 +124,20 @@ #![deny(missing_docs, warnings)] -use std::collections::{HashMap, HashSet}; -use std::error::Error as StdError; -use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::{ + collections::{HashMap, HashSet}, + error::Error as StdError, + fmt::{Display, Formatter, Result as FmtResult}, +}; use base64::{decode_config, encode_config, CharacterSet, Config}; use lazy_static::lazy_static; use ring::signature::{verify, Ed25519KeyPair as RingEd25519KeyPair, ED25519}; -use serde::de::{Error as SerdeError, MapAccess, Unexpected, Visitor}; -use serde::ser::SerializeMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ + de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; use serde_json::{to_string, Value}; use untrusted::Input; use url::Url; @@ -699,15 +703,8 @@ mod test { use serde_json::{from_str, to_string, to_value}; use super::{ - sign_json, - verify_json, - Ed25519KeyPair, - Ed25519Verifier, - KeyPair, - Signature, - SignatureSet, - Signatures, - BASE64_CONFIG, + sign_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, SignatureSet, + Signatures, BASE64_CONFIG, }; const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; From 163a555a17b6556d84f50c6d8fa930a7bbad5057 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sun, 13 Jan 2019 22:30:12 +0100 Subject: [PATCH 33/98] Update doc tests to Rust 2018, remove unnecessary main declaration from them --- src/lib.rs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d4a58c2a..219f48bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,10 +16,8 @@ //! A homeserver signs JSON with a key pair: //! //! ```rust,no_run -//! # extern crate ruma_signatures; -//! # extern crate serde_json; -//! # fn main() { -//! # use ruma_signatures::KeyPair; +//! # use ruma_signatures::{self, KeyPair}; +//! # use serde_json; //! # let public_key = [0; 32]; //! # let private_key = [0; 32]; //! // Create an Ed25519 key pair. @@ -30,7 +28,6 @@ //! ).expect("the provided keys should be suitable for Ed25519"); //! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); //! ruma_signatures::sign_json(&key_pair, &value).expect("value is a a JSON object"); // `Signature` -//! # } //! ``` //! //! # Signing Matrix events @@ -43,9 +40,8 @@ //! A client application or another homeserver can verify a signature on arbitrary JSON: //! //! ```rust,no_run -//! # extern crate ruma_signatures; -//! # extern crate serde_json; -//! # fn main() { +//! # use ruma_signatures; +//! # use serde_json; //! # let public_key = [0; 32]; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( @@ -54,7 +50,6 @@ //! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); //! let verifier = ruma_signatures::Ed25519Verifier::new(); //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); -//! # } //! ``` //! //! Verifying signatures of Matrix events is not yet implemented by ruma_signatures. @@ -83,10 +78,9 @@ //! This inner object can be created by serializing a `SignatureSet`: //! //! ```rust,no_run -//! # extern crate ruma_signatures; -//! # extern crate serde; -//! # extern crate serde_json; -//! # fn main() { +//! # use ruma_signatures; +//! # use serde; +//! # use serde_json; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( //! "key identifier should be valid" @@ -94,7 +88,6 @@ //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! serde_json::to_string(&signature_set).expect("signature_set should serialize"); -//! # } //! ``` //! //! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, @@ -104,10 +97,9 @@ //! created like this: //! //! ```rust,no_run -//! # extern crate ruma_signatures; -//! # extern crate serde; -//! # extern crate serde_json; -//! # fn main() { +//! # use ruma_signatures; +//! # use serde; +//! # use serde_json; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( //! "key identifier should be valid" @@ -117,7 +109,6 @@ //! let mut signatures = ruma_signatures::Signatures::new(); //! signatures.insert("example.com", signature_set).expect("example.com is a valid server name"); //! serde_json::to_string(&signatures).expect("signatures should serialize"); -//! # } //! ``` //! //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. From b15f7a1c07aad45f744d2caad8fee8d9c7f042ce Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Apr 2019 16:52:20 -0700 Subject: [PATCH 34/98] Update dependencies. --- Cargo.toml | 14 +++++++------- src/lib.rs | 9 +++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 971a3ae1..8cbbfee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,13 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.4.1" [dependencies] -base64 = "0.10.0" -lazy_static = "1.2.0" -ring = "0.13.5" -serde = "1.0.80" -serde_json = "1.0.33" +base64 = "0.10.1" +lazy_static = "1.3.0" +ring = "0.14.6" +serde_json = "1.0.39" untrusted = "0.6.2" url = "1.7.2" -[dev-dependencies] -serde_derive = "1.0.80" +[dependencies.serde] +version = "1.0.90" +features = ["derive"] diff --git a/src/lib.rs b/src/lib.rs index 219f48bd..e2c84e82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -691,6 +691,7 @@ impl Display for Algorithm { #[cfg(test)] mod test { use base64::decode_config; + use serde::Serialize; use serde_json::{from_str, to_string, to_value}; use super::{ @@ -753,7 +754,7 @@ mod test { #[test] fn signatures_empty_json() { - #[derive(serde_derive::Serialize)] + #[derive(Serialize)] struct EmptyWithSignatures { signatures: Signatures, } @@ -786,13 +787,13 @@ mod test { #[test] fn sign_minimal_json() { - #[derive(serde_derive::Serialize)] + #[derive(Serialize)] struct Alpha { one: u8, two: String, } - #[derive(serde_derive::Serialize)] + #[derive(Serialize)] struct ReverseAlpha { two: String, one: u8, @@ -873,7 +874,7 @@ mod test { #[test] fn signatures_minimal_json() { - #[derive(serde_derive::Serialize)] + #[derive(Serialize)] struct MinimalWithSignatures { one: u8, signatures: Signatures, From e55f0a324e0e9445f25eec5c4a6ad8295d804a67 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 11 Apr 2019 17:43:51 -0700 Subject: [PATCH 35/98] Bump version to 0.4.2. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8cbbfee3..78b593e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.4.1" +version = "0.4.2" [dependencies] base64 = "0.10.1" From 2e0376a8a4bb6b7dfa80c33761d910783669cfc2 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 2 Jun 2019 09:14:55 -0700 Subject: [PATCH 36/98] Use stable Rust on Travis. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d9dea07d..c5b1d431 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,3 @@ notifications: channels: - secure: "bI2kShW50RpX6ptnZQEzv8BSnejCskATLMvhKndXsTEJhrBIxC/IgOyWi9WTwmYWXYgupuKYRe+z5dk/BEWBa+JfKh+UYOGtwE5Y5T9S4JV2rDQjxb7xSI/vwLTqlDHhzBvqK+9JxfsqitS7FFVGtmNEXFc27f2NwC3/uW/CAM1iPD+K+Xe9ZoqAT0+03tdJtPY9kVE8+h3k5V/KAo4ENx8MqQ7g31r8ch8twFUjuknMhR9Les5trNoMUJk46YluhThUgSAvcjo0+n43cxfv2t0Qx7NSzTc0myLfK0nRMRAuFEMeSnWJWnSLWLKSeluEsYN8RPdCQGNlAzoftDSBPAgDBtWS8FYqQZVbkxBJepdczjy+PFAgjjBr+eqMRjPz1t83ThElefPxkQUshzQU3DVifUwSryNhF58tmZ/4iiCyw0fIPZL1m7n0OivpBNkTLFqeSHTZgGiCBThc3MFarL6qOcBkFZWynG19lNQ+s9BzdVeN/veFWRp6m1lOymPSCVVDt7KMoloEK2ie1wkVzvc6IlFRXJgDWU+C4fy+nue2ghdAv4ZQ1D+JDX2M4DCrtfWBZsdTR+UsQL8S8fY0zOcQw0y39qh4AsoKeArvjUfWxfDPx5oPSqpwM8Mvmdp/OAC5knUeSxBtjpnnPPxcucE2oQ07QYq3/SHnjeEqhrA=" use_notice: true -rust: - - "nightly" From e3b453c46862b82db8837439b17c2bb21f2ba953 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 2 Jun 2019 19:12:05 -0700 Subject: [PATCH 37/98] Add rustfmt and clippy to CI and address clippy warnings. --- .rustfmt.toml | 1 - .travis.yml | 2 ++ rustfmt.toml | 2 -- src/lib.rs | 86 ++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 67 insertions(+), 24 deletions(-) delete mode 100644 .rustfmt.toml delete mode 100644 rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index 7d2cf549..00000000 --- a/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -merge_imports = true diff --git a/.travis.yml b/.travis.yml index c5b1d431..91661b79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: "rust" before_script: - "rustup component add rustfmt" + - "rustup component add clippy" script: - "cargo fmt --all -- --check" + - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" - "cargo test --verbose" notifications: diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 84896d4e..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -imports_indent = "Block" -imports_layout = "HorizontalVertical" diff --git a/src/lib.rs b/src/lib.rs index e2c84e82..bc8a0b42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,31 @@ //! //! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. -#![deny(missing_docs, warnings)] +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + warnings +)] +#![warn( + clippy::empty_line_after_outer_attr, + clippy::expl_impl_clone_on_copy, + clippy::if_not_else, + clippy::items_after_statements, + clippy::match_same_arms, + clippy::mem_forget, + clippy::missing_docs_in_private_items, + clippy::multiple_inherent_impl, + clippy::mut_mut, + clippy::needless_borrow, + clippy::needless_continue, + clippy::single_match_else, + clippy::unicode_not_nfc, + clippy::use_self, + clippy::used_underscore_binding, + clippy::wrong_pub_self_convention, + clippy::wrong_self_convention +)] use std::{ collections::{HashMap, HashSet}, @@ -212,12 +236,15 @@ where /// An error when trying to extract the algorithm and version from a key identifier. #[derive(Debug)] enum SplitError<'a> { + /// The signature's ID has an invalid length. InvalidLength(usize), + /// 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(); @@ -246,18 +273,22 @@ pub enum Algorithm { } /// An Ed25519 key pair. +#[derive(Debug)] pub struct Ed25519KeyPair { + /// The *ring* key pair. ring_key_pair: RingEd25519KeyPair, + /// The version of the key pair. version: String, } /// A verifier for Ed25519 digital signatures. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct Ed25519Verifier; -/// An error produced when ruma_signatures operations fail. +/// An error produced when `ruma_signatures` operations fail. #[derive(Clone, Debug)] pub struct Error { + /// A human-readable description of the error. message: String, } @@ -290,14 +321,18 @@ pub trait KeyPair: Sized { /// A digital signature. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Signature { + /// The cryptographic algorithm to use. algorithm: Algorithm, + /// The signature data. signature: Vec, + /// The version of the signature. version: String, } /// A map of server names to sets of digital signatures created by that server. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Signatures { + /// A map of homeservers to sets of signatures for the homeserver. map: HashMap, } @@ -305,8 +340,9 @@ pub struct Signatures { struct SignaturesVisitor; /// A set of digital signatures created by a single homeserver. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct SignatureSet { + /// A set of signatures for a homeserver. set: HashSet, } @@ -336,13 +372,13 @@ pub trait Verifier { impl KeyPair for Ed25519KeyPair { fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { - Ok(Ed25519KeyPair { + Ok(Self { ring_key_pair: RingEd25519KeyPair::from_seed_and_public_key( Input::from(private_key), Input::from(public_key), ) .map_err(|_| Error::new("invalid key pair"))?, - version: version, + version, }) } @@ -389,7 +425,7 @@ impl Error { where T: Into, { - Error { + Self { message: message.into(), } } @@ -426,10 +462,10 @@ impl Signature { } })?; - Ok(Signature { - algorithm: algorithm, + Ok(Self { + algorithm, signature: bytes.to_vec(), - version: version, + version, }) } @@ -466,7 +502,7 @@ impl Signature { impl Signatures { /// Initializes a new empty Signatures. pub fn new() -> Self { - Signatures { + Self { map: HashMap::new(), } } @@ -477,7 +513,7 @@ impl Signatures { /// /// * capacity: The number of items to allocate memory for. pub fn with_capacity(capacity: usize) -> Self { - Signatures { + Self { map: HashMap::with_capacity(capacity), } } @@ -516,6 +552,11 @@ impl Signatures { pub fn len(&self) -> usize { self.map.len() } + + /// Whether or not the collection of signatures is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl<'de> Deserialize<'de> for Signatures { @@ -562,7 +603,7 @@ impl<'de> Visitor<'de> for SignaturesVisitor { while let Some((server_name, signature_set)) = visitor.next_entry::()? { - if let Err(_) = signatures.insert(&server_name, signature_set) { + if signatures.insert(&server_name, signature_set).is_err() { return Err(M::Error::invalid_value( Unexpected::Str(&server_name), &self, @@ -577,7 +618,7 @@ impl<'de> Visitor<'de> for SignaturesVisitor { impl SignatureSet { /// Initializes a new empty SignatureSet. pub fn new() -> Self { - SignatureSet { + Self { set: HashSet::new(), } } @@ -588,7 +629,7 @@ impl SignatureSet { /// /// * capacity: The number of items to allocate memory for. pub fn with_capacity(capacity: usize) -> Self { - SignatureSet { + Self { set: HashSet::with_capacity(capacity), } } @@ -609,6 +650,11 @@ impl SignatureSet { pub fn len(&self) -> usize { self.set.len() } + + /// Whether or not the set of signatures is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl<'de> Deserialize<'de> for SignatureSet { @@ -666,9 +712,9 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { }; let signature = Signature { - algorithm: algorithm, + algorithm, signature: signature_bytes, - version: version, + version, }; signature_set.insert(signature); @@ -773,9 +819,7 @@ mod test { let mut signatures = Signatures::with_capacity(1); signatures.insert("domain", signature_set).ok(); - let empty = EmptyWithSignatures { - signatures: signatures, - }; + let empty = EmptyWithSignatures { signatures }; let json = to_string(&empty).unwrap(); From aa9d546b1b2237cdf3ddb2bfd3a1bdb74301003a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 16 Jun 2019 16:42:46 -0700 Subject: [PATCH 38/98] Add crates.io categories. [ci skip] --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 78b593e8..26e83888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [package] authors = ["Jimmy Cuadra "] +categories = ["api-bindings", "cryptography"] description = "Digital signatures according to the Matrix specification." documentation = "https://docs.rs/ruma-signatures" edition = "2018" From c0d10881a2f78c09261e0508d1c812f16cb24862 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 18:47:11 -0700 Subject: [PATCH 39/98] Implement `Clone` and `PartialEq` for all types. See https://github.com/briansmith/ring/issues/859 for background. --- src/lib.rs | 76 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bc8a0b42..f36df81e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Crate **ruma_signatures** implements digital signatures according to the +//! 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 @@ -27,13 +27,16 @@ //! "1".to_string(), // The "version" of the key. //! ).expect("the provided keys should be suitable for Ed25519"); //! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); -//! ruma_signatures::sign_json(&key_pair, &value).expect("value is a a JSON object"); // `Signature` +//! let signature = ruma_signatures::sign_json( +//! &key_pair, +//! &value, +//! ).expect("`value` must be a JSON object"); //! ``` //! //! # Signing Matrix events //! //! Signing an event uses a more involved process than signing arbitrary JSON. -//! Event signing is not yet implemented by ruma_signatures. +//! Event signing is not yet implemented by ruma-signatures. //! //! # Verifying signatures //! @@ -52,7 +55,7 @@ //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! ``` //! -//! Verifying signatures of Matrix events is not yet implemented by ruma_signatures. +//! Verifying signatures of Matrix events is not yet implemented by ruma-signatures. //! //! # Signature sets //! @@ -142,7 +145,7 @@ use std::{ collections::{HashMap, HashSet}, error::Error as StdError, - fmt::{Display, Formatter, Result as FmtResult}, + fmt::{Debug, Display, Formatter, Result as FmtResult}, }; use base64::{decode_config, encode_config, CharacterSet, Config}; @@ -234,7 +237,7 @@ where } /// An error when trying to extract the algorithm and version from a key identifier. -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] enum SplitError<'a> { /// The signature's ID has an invalid length. InvalidLength(usize), @@ -273,20 +276,24 @@ pub enum Algorithm { } /// An Ed25519 key pair. -#[derive(Debug)] +#[derive(Clone, PartialEq)] pub struct Ed25519KeyPair { - /// The *ring* key pair. - ring_key_pair: RingEd25519KeyPair, + /// The public key. + public_key: Vec, + + /// The private key. + private_key: Vec, + /// The version of the key pair. version: String, } /// A verifier for Ed25519 digital signatures. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Ed25519Verifier; -/// An error produced when `ruma_signatures` operations fail. -#[derive(Clone, Debug)] +/// An error produced when ruma-signatures operations fail. +#[derive(Clone, Debug, PartialEq)] pub struct Error { /// A human-readable description of the error. message: String, @@ -330,7 +337,7 @@ pub struct Signature { } /// A map of server names to sets of digital signatures created by that server. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Signatures { /// A map of homeservers to sets of signatures for the homeserver. map: HashMap, @@ -340,7 +347,7 @@ pub struct Signatures { struct SignaturesVisitor; /// A set of digital signatures created by a single homeserver. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct SignatureSet { /// A set of signatures for a homeserver. set: HashSet, @@ -372,25 +379,46 @@ pub trait Verifier { impl KeyPair for Ed25519KeyPair { fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { + if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key( + Input::from(private_key), + Input::from(public_key), + ) { + return Err(Error::new(error.to_string())); + } + Ok(Self { - ring_key_pair: RingEd25519KeyPair::from_seed_and_public_key( - Input::from(private_key), - Input::from(public_key), - ) - .map_err(|_| Error::new("invalid key pair"))?, + public_key: public_key.to_owned(), + private_key: private_key.to_owned(), version, }) } fn sign(&self, message: &[u8]) -> Signature { + // Okay to unwrap because we verified the input in the `new`. + let ring_key_pair = RingEd25519KeyPair::from_seed_and_public_key( + Input::from(&self.private_key), + Input::from(&self.public_key), + ) + .unwrap(); + Signature { algorithm: Algorithm::Ed25519, - signature: self.ring_key_pair.sign(message).as_ref().to_vec(), + signature: ring_key_pair.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.public_key) + .field("version", &self.version) + .finish() + } +} + impl Ed25519Verifier { /// Creates a new `Ed25519Verifier`. pub fn new() -> Self { @@ -745,12 +773,12 @@ mod test { Signatures, BASE64_CONFIG, }; - const PUBLIC_KEY: &'static str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; - const PRIVATE_KEY: &'static str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; + const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; + const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; - const EMPTY_JSON_SIGNATURE: &'static str = + const EMPTY_JSON_SIGNATURE: &str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; - const MINIMAL_JSON_SIGNATURE: &'static str = + const MINIMAL_JSON_SIGNATURE: &str = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; #[test] From 6ffd73b3120234b1694a14099a7da185b8d018d4 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 18:51:15 -0700 Subject: [PATCH 40/98] Make Error::new private to the crate. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f36df81e..ab6c9d6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -449,7 +449,7 @@ impl Error { /// # Parameters /// /// * message: The error message. - pub fn new(message: T) -> Self + pub(crate) fn new(message: T) -> Self where T: Into, { From 302e401a80795c6acd30b2935e66e1d87ace96c3 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 18:53:29 -0700 Subject: [PATCH 41/98] Remove unnecessary constructor for `Ed25519Verifier`. --- src/lib.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ab6c9d6e..388e2dc6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ //! "key identifier should be valid" //! ); //! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); -//! let verifier = ruma_signatures::Ed25519Verifier::new(); +//! let verifier = ruma_signatures::Ed25519Verifier; //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! ``` //! @@ -419,13 +419,6 @@ impl Debug for Ed25519KeyPair { } } -impl Ed25519Verifier { - /// Creates a new `Ed25519Verifier`. - pub fn new() -> Self { - Ed25519Verifier - } -} - impl Verifier for Ed25519Verifier { fn verify_json( &self, @@ -813,7 +806,7 @@ mod test { let value = from_str("{}").unwrap(); - let verifier = Ed25519Verifier::new(); + let verifier = Ed25519Verifier; assert!(verify_json( &verifier, @@ -917,7 +910,7 @@ mod test { r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); - let verifier = Ed25519Verifier::new(); + let verifier = Ed25519Verifier; assert!(verify_json( &verifier, @@ -992,7 +985,7 @@ mod test { let value = from_str(r#"{"not":"empty"}"#).unwrap(); - let verifier = Ed25519Verifier::new(); + let verifier = Ed25519Verifier; assert!(verify_json( &verifier, From 03fcf7281d4d0c3c064e10da38902307d1c3aa6c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 20:45:27 -0700 Subject: [PATCH 42/98] Use constants from base64. --- Cargo.toml | 1 - src/lib.rs | 43 ++++++++++++++++++++----------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26e83888..137b8052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ version = "0.4.2" [dependencies] base64 = "0.10.1" -lazy_static = "1.3.0" ring = "0.14.6" serde_json = "1.0.39" untrusted = "0.6.2" diff --git a/src/lib.rs b/src/lib.rs index 388e2dc6..4f030932 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,8 +148,7 @@ use std::{ fmt::{Debug, Display, Formatter, Result as FmtResult}, }; -use base64::{decode_config, encode_config, CharacterSet, Config}; -use lazy_static::lazy_static; +use base64::{decode_config, encode_config, STANDARD_NO_PAD}; use ring::signature::{verify, Ed25519KeyPair as RingEd25519KeyPair, ED25519}; use serde::{ de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, @@ -162,10 +161,6 @@ use url::Url; pub use url::Host; -lazy_static! { - static ref BASE64_CONFIG: Config = Config::new(CharacterSet::Standard, false); -} - /// Signs an arbitrary JSON object. /// /// # Parameters @@ -501,8 +496,10 @@ impl Signature { } /// 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(), *BASE64_CONFIG) + encode_config(self.signature.as_slice(), STANDARD_NO_PAD) } /// The key identifier, a string containing the signature algorithm and the key "version" @@ -727,7 +724,7 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { } })?; - let signature_bytes: Vec = match decode_config(&value, *BASE64_CONFIG) { + let signature_bytes: Vec = match decode_config(&value, STANDARD_NO_PAD) { Ok(raw) => raw, Err(error) => return Err(M::Error::custom(error.description())), }; @@ -757,13 +754,13 @@ impl Display for Algorithm { #[cfg(test)] mod test { - use base64::decode_config; + use base64::{decode_config, STANDARD_NO_PAD}; use serde::Serialize; use serde_json::{from_str, to_string, to_value}; use super::{ sign_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, SignatureSet, - Signatures, BASE64_CONFIG, + Signatures, }; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -777,10 +774,10 @@ mod test { #[test] fn sign_empty_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), - decode_config(&PRIVATE_KEY, *BASE64_CONFIG) + decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), "1".to_string(), @@ -798,7 +795,7 @@ mod test { fn verify_empty_json() { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) .unwrap() .as_slice(), ) @@ -810,7 +807,7 @@ mod test { assert!(verify_json( &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), &signature, @@ -828,7 +825,7 @@ mod test { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) .unwrap() .as_slice(), ) @@ -865,10 +862,10 @@ mod test { } let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), - decode_config(&PRIVATE_KEY, *BASE64_CONFIG) + decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), "1".to_string(), @@ -900,7 +897,7 @@ mod test { fn verify_minimal_json() { let signature = Signature::new( "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG) + decode_config(&MINIMAL_JSON_SIGNATURE, STANDARD_NO_PAD) .unwrap() .as_slice(), ) @@ -914,7 +911,7 @@ mod test { assert!(verify_json( &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), &signature, @@ -928,7 +925,7 @@ mod test { assert!(verify_json( &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), &signature, @@ -948,7 +945,7 @@ mod test { let signature = Signature::new( "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, *BASE64_CONFIG) + decode_config(&MINIMAL_JSON_SIGNATURE, STANDARD_NO_PAD) .unwrap() .as_slice(), ) @@ -977,7 +974,7 @@ mod test { fn fail_verify() { let signature = Signature::new( "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, *BASE64_CONFIG) + decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) .unwrap() .as_slice(), ) @@ -989,7 +986,7 @@ mod test { assert!(verify_json( &verifier, - decode_config(&PUBLIC_KEY, *BASE64_CONFIG) + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() .as_slice(), &signature, From 97ee073e116a2886b963340e71f159180b1cd03f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 21:10:50 -0700 Subject: [PATCH 43/98] Split the library into modules. --- src/keys.rs | 89 ++++++++ src/lib.rs | 499 ++------------------------------------------ src/signatures.rs | 365 ++++++++++++++++++++++++++++++++ src/verification.rs | 48 +++++ 4 files changed, 516 insertions(+), 485 deletions(-) create mode 100644 src/keys.rs create mode 100644 src/signatures.rs create mode 100644 src/verification.rs diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 00000000..a8e1c833 --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,89 @@ +//! Public and private key pairs. + +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use ring::signature::Ed25519KeyPair as RingEd25519KeyPair; +use untrusted::Input; + +use crate::{signatures::Signature, Algorithm, Error}; + +/// A cryptographic key pair for digitally signing data. +pub trait KeyPair: Sized { + /// Initializes a new key pair. + /// + /// # Parameters + /// + /// * public_key: The public key of the key pair. + /// * private_key: The private key of the key pair. + /// * 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. + fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result; + + /// Signs a JSON object. + /// + /// # Parameters + /// + /// * message: An arbitrary series of bytes to sign. + fn sign(&self, message: &[u8]) -> Signature; +} + +/// An Ed25519 key pair. +#[derive(Clone, PartialEq)] +pub struct Ed25519KeyPair { + /// The public key. + public_key: Vec, + + /// The private key. + private_key: Vec, + + /// The version of the key pair. + version: String, +} + +impl KeyPair for Ed25519KeyPair { + fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { + if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key( + Input::from(private_key), + Input::from(public_key), + ) { + return Err(Error::new(error.to_string())); + } + + Ok(Self { + public_key: public_key.to_owned(), + private_key: private_key.to_owned(), + version, + }) + } + + fn sign(&self, message: &[u8]) -> Signature { + // Okay to unwrap because we verified the input in the `new`. + let ring_key_pair = RingEd25519KeyPair::from_seed_and_public_key( + Input::from(&self.private_key), + Input::from(&self.public_key), + ) + .unwrap(); + + Signature { + algorithm: Algorithm::Ed25519, + signature: ring_key_pair.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.public_key) + .field("version", &self.version) + .finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 4f030932..c051e682 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,24 +143,22 @@ )] use std::{ - collections::{HashMap, HashSet}, error::Error as StdError, - fmt::{Debug, Display, Formatter, Result as FmtResult}, + fmt::{Display, Formatter, Result as FmtResult}, }; -use base64::{decode_config, encode_config, STANDARD_NO_PAD}; -use ring::signature::{verify, Ed25519KeyPair as RingEd25519KeyPair, ED25519}; -use serde::{ - de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, - ser::SerializeMap, - Deserialize, Deserializer, Serialize, Serializer, -}; use serde_json::{to_string, Value}; -use untrusted::Input; -use url::Url; pub use url::Host; +pub use keys::{Ed25519KeyPair, KeyPair}; +pub use signatures::{Signature, SignatureSet, Signatures}; +pub use verification::{Ed25519Verifier, Verifier}; + +mod keys; +mod signatures; +mod verification; + /// Signs an arbitrary JSON object. /// /// # Parameters @@ -231,62 +229,6 @@ where verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) } -/// 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 has an invalid length. - InvalidLength(usize), - /// 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 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())) -} - -/// The algorithm used for signing data. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Algorithm { - /// The Ed25519 digital signature algorithm. - Ed25519, -} - -/// An Ed25519 key pair. -#[derive(Clone, PartialEq)] -pub struct Ed25519KeyPair { - /// The public key. - public_key: Vec, - - /// The private key. - private_key: Vec, - - /// The version of the key pair. - version: String, -} - -/// A verifier for Ed25519 digital signatures. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Ed25519Verifier; - /// An error produced when ruma-signatures operations fail. #[derive(Clone, Debug, PartialEq)] pub struct Error { @@ -294,143 +236,6 @@ pub struct Error { message: String, } -/// A cryptographic key pair for digitally signing data. -pub trait KeyPair: Sized { - /// Initializes a new key pair. - /// - /// # Parameters - /// - /// * public_key: The public key of the key pair. - /// * private_key: The private key of the key pair. - /// * 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. - fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result; - - /// Signs a JSON object. - /// - /// # Parameters - /// - /// * message: An arbitrary series of bytes to sign. - fn sign(&self, message: &[u8]) -> Signature; -} - -/// A digital signature. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct Signature { - /// The cryptographic algorithm to use. - algorithm: Algorithm, - /// The signature data. - signature: Vec, - /// The version of the signature. - version: String, -} - -/// A map of server names to sets of digital signatures created by that server. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Signatures { - /// A map of homeservers to sets of signatures for the homeserver. - map: HashMap, -} - -/// Serde Visitor for deserializing `Signatures`. -struct SignaturesVisitor; - -/// A set of digital signatures created by a single homeserver. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct SignatureSet { - /// A set of signatures for a homeserver. - set: HashSet, -} - -/// Serde Visitor for deserializing `SignatureSet`. -struct SignatureSetVisitor; - -/// 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 public key of the key pair used to sign the message. - /// * signature: The `Signature` to verify. - /// * message: The message that was signed. - /// - /// # Errors - /// - /// Returns an error if verification fails. - fn verify_json( - &self, - public_key: &[u8], - signature: &Signature, - message: &[u8], - ) -> Result<(), Error>; -} - -impl KeyPair for Ed25519KeyPair { - fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { - if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key( - Input::from(private_key), - Input::from(public_key), - ) { - return Err(Error::new(error.to_string())); - } - - Ok(Self { - public_key: public_key.to_owned(), - private_key: private_key.to_owned(), - version, - }) - } - - fn sign(&self, message: &[u8]) -> Signature { - // Okay to unwrap because we verified the input in the `new`. - let ring_key_pair = RingEd25519KeyPair::from_seed_and_public_key( - Input::from(&self.private_key), - Input::from(&self.public_key), - ) - .unwrap(); - - Signature { - algorithm: Algorithm::Ed25519, - signature: ring_key_pair.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.public_key) - .field("version", &self.version) - .finish() - } -} - -impl Verifier for Ed25519Verifier { - fn verify_json( - &self, - public_key: &[u8], - signature: &Signature, - message: &[u8], - ) -> Result<(), Error> { - verify( - &ED25519, - Input::from(public_key), - Input::from(message), - Input::from(signature.as_bytes()), - ) - .map_err(|_| Error::new("signature verification failed")) - } -} - impl Error { /// Creates a new error. /// @@ -459,287 +264,11 @@ impl Display for Error { } } -impl Signature { - /// Creates a signature from raw bytes. - /// - /// # 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 identifier is invalid. - pub fn new(id: &str, bytes: &[u8]) -> Result { - let (algorithm, version) = split_id(id).map_err(|split_error| match split_error { - SplitError::InvalidLength(_) => Error::new("malformed signature ID"), - 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 - } -} - -impl Signatures { - /// Initializes a new empty Signatures. - pub fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - /// Initializes a new empty Signatures with room for a specific number of servers. - /// - /// # Parameters - /// - /// * capacity: The number of items to allocate memory for. - pub fn with_capacity(capacity: usize) -> Self { - Self { - map: HashMap::with_capacity(capacity), - } - } - - /// Adds a signature set for a server. - /// - /// If no signature set for the given server existed in the collection, `None` is returned. - /// Otherwise, the signature set is returned. - /// - /// # Parameters - /// - /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. - /// * signature_set: The `SignatureSet` containing the digital signatures made by the server. - /// - /// # Errors - /// - /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn insert( - &mut self, - server_name: &str, - signature_set: SignatureSet, - ) -> Result, Error> { - let url_string = format!("https://{}", server_name); - let url = Url::parse(&url_string) - .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; - - let host = match url.host() { - Some(host) => host.to_owned(), - None => return Err(Error::new(format!("invalid server name: {}", server_name))), - }; - - Ok(self.map.insert(host, signature_set)) - } - - /// The number of servers in the collection. - pub fn len(&self) -> usize { - self.map.len() - } - - /// Whether or not the collection of signatures is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl<'de> Deserialize<'de> for Signatures { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(SignaturesVisitor) - } -} - -impl Serialize for Signatures { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map_serializer = serializer.serialize_map(Some(self.len()))?; - - for (host, signature_set) in self.map.iter() { - map_serializer.serialize_key(&host.to_string())?; - map_serializer.serialize_value(signature_set)?; - } - - map_serializer.end() - } -} - -impl<'de> Visitor<'de> for SignaturesVisitor { - type Value = Signatures; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "digital signatures") - } - - fn visit_map(self, mut visitor: M) -> Result - where - M: MapAccess<'de>, - { - let mut signatures = match visitor.size_hint() { - Some(capacity) => Signatures::with_capacity(capacity), - None => Signatures::new(), - }; - - while let Some((server_name, signature_set)) = - visitor.next_entry::()? - { - if signatures.insert(&server_name, signature_set).is_err() { - return Err(M::Error::invalid_value( - Unexpected::Str(&server_name), - &self, - )); - } - } - - Ok(signatures) - } -} - -impl SignatureSet { - /// Initializes a new empty SignatureSet. - pub fn new() -> Self { - Self { - set: HashSet::new(), - } - } - - /// Initializes a new empty SignatureSet with room for a specific number of signatures. - /// - /// # Parameters - /// - /// * capacity: The number of items to allocate memory for. - pub fn with_capacity(capacity: usize) -> Self { - Self { - set: HashSet::with_capacity(capacity), - } - } - - /// Adds a signature to the set. - /// - /// The boolean return value indicates whether or not the value was actually inserted, since - /// subsequent inserts of the same signature have no effect. - /// - /// # Parameters - /// - /// * signature: A `Signature` to insert into the set. - pub fn insert(&mut self, signature: Signature) -> bool { - self.set.insert(signature) - } - - /// The number of signatures in the set. - pub fn len(&self) -> usize { - self.set.len() - } - - /// Whether or not the set of signatures is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl<'de> Deserialize<'de> for SignatureSet { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(SignatureSetVisitor) - } -} - -impl Serialize for SignatureSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map_serializer = serializer.serialize_map(Some(self.len()))?; - - for signature in self.set.iter() { - map_serializer.serialize_key(&signature.id())?; - map_serializer.serialize_value(&signature.base64())?; - } - - map_serializer.end() - } -} - -impl<'de> Visitor<'de> for SignatureSetVisitor { - type Value = SignatureSet; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a set of digital signatures") - } - - fn visit_map(self, mut visitor: M) -> Result - where - M: MapAccess<'de>, - { - let mut signature_set = match visitor.size_hint() { - Some(capacity) => SignatureSet::with_capacity(capacity), - None => SignatureSet::new(), - }; - - while let Some((key, value)) = visitor.next_entry::()? { - let (algorithm, version) = split_id(&key).map_err(|split_error| match split_error { - SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), - SplitError::UnknownAlgorithm(algorithm) => { - M::Error::invalid_value(Unexpected::Str(algorithm), &self) - } - })?; - - let signature_bytes: Vec = match decode_config(&value, STANDARD_NO_PAD) { - Ok(raw) => raw, - Err(error) => return Err(M::Error::custom(error.description())), - }; - - let signature = Signature { - algorithm, - signature: signature_bytes, - version, - }; - - signature_set.insert(signature); - } - - Ok(signature_set) - } +/// 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 { diff --git a/src/signatures.rs b/src/signatures.rs new file mode 100644 index 00000000..16ea3ad0 --- /dev/null +++ b/src/signatures.rs @@ -0,0 +1,365 @@ +//! Digital signatures and collections of signatures. + +use std::{ + collections::{HashMap, HashSet}, + error::Error as _, + fmt::{Formatter, Result as FmtResult}, +}; + +use base64::{decode_config, encode_config, STANDARD_NO_PAD}; +use serde::{ + de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, + ser::SerializeMap, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::{Host, Url}; + +use crate::{Algorithm, Error}; + +/// A digital signature. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Signature { + /// The cryptographic algorithm to use. + pub(crate) algorithm: Algorithm, + + /// The signature data. + pub(crate) signature: Vec, + + /// The version of the signature. + pub(crate) version: String, +} + +impl Signature { + /// Creates a signature from raw bytes. + /// + /// # 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 identifier is invalid. + pub fn new(id: &str, bytes: &[u8]) -> Result { + let (algorithm, version) = split_id(id).map_err(|split_error| match split_error { + SplitError::InvalidLength(_) => Error::new("malformed signature ID"), + 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 of server names to sets of digital signatures created by that server. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Signatures { + /// A map of homeservers to sets of signatures for the homeserver. + map: HashMap, +} + +impl Signatures { + /// Initializes a new empty Signatures. + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } + + /// Initializes a new empty Signatures with room for a specific number of servers. + /// + /// # Parameters + /// + /// * capacity: The number of items to allocate memory for. + pub fn with_capacity(capacity: usize) -> Self { + Self { + map: HashMap::with_capacity(capacity), + } + } + + /// Adds a signature set for a server. + /// + /// If no signature set for the given server existed in the collection, `None` is returned. + /// Otherwise, the signature set is returned. + /// + /// # Parameters + /// + /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. + /// * signature_set: The `SignatureSet` containing the digital signatures made by the server. + /// + /// # Errors + /// + /// Returns an error if the given server name cannot be parsed as a valid host. + pub fn insert( + &mut self, + server_name: &str, + signature_set: SignatureSet, + ) -> Result, Error> { + let url_string = format!("https://{}", server_name); + let url = Url::parse(&url_string) + .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; + + let host = match url.host() { + Some(host) => host.to_owned(), + None => return Err(Error::new(format!("invalid server name: {}", server_name))), + }; + + Ok(self.map.insert(host, signature_set)) + } + + /// The number of servers in the collection. + pub fn len(&self) -> usize { + self.map.len() + } + + /// Whether or not the collection of signatures is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Serialize for Signatures { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_serializer = serializer.serialize_map(Some(self.len()))?; + + for (host, signature_set) in self.map.iter() { + map_serializer.serialize_key(&host.to_string())?; + map_serializer.serialize_value(signature_set)?; + } + + map_serializer.end() + } +} + +impl<'de> Deserialize<'de> for Signatures { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(SignaturesVisitor) + } +} + +/// Serde Visitor for deserializing `Signatures`. +struct SignaturesVisitor; + +impl<'de> Visitor<'de> for SignaturesVisitor { + type Value = Signatures; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "digital signatures") + } + + fn visit_map(self, mut visitor: M) -> Result + where + M: MapAccess<'de>, + { + let mut signatures = match visitor.size_hint() { + Some(capacity) => Signatures::with_capacity(capacity), + None => Signatures::new(), + }; + + while let Some((server_name, signature_set)) = + visitor.next_entry::()? + { + if signatures.insert(&server_name, signature_set).is_err() { + return Err(M::Error::invalid_value( + Unexpected::Str(&server_name), + &self, + )); + } + } + + Ok(signatures) + } +} + +/// A set of digital signatures created by a single homeserver. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct SignatureSet { + /// A set of signatures for a homeserver. + set: HashSet, +} + +impl SignatureSet { + /// Initializes a new empty SignatureSet. + pub fn new() -> Self { + Self { + set: HashSet::new(), + } + } + + /// Initializes a new empty SignatureSet with room for a specific number of signatures. + /// + /// # Parameters + /// + /// * capacity: The number of items to allocate memory for. + pub fn with_capacity(capacity: usize) -> Self { + Self { + set: HashSet::with_capacity(capacity), + } + } + + /// Adds a signature to the set. + /// + /// The boolean return value indicates whether or not the value was actually inserted, since + /// subsequent inserts of the same signature have no effect. + /// + /// # Parameters + /// + /// * signature: A `Signature` to insert into the set. + pub fn insert(&mut self, signature: Signature) -> bool { + self.set.insert(signature) + } + + /// The number of signatures in the set. + pub fn len(&self) -> usize { + self.set.len() + } + + /// Whether or not the set of signatures is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Serialize for SignatureSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_serializer = serializer.serialize_map(Some(self.len()))?; + + for signature in self.set.iter() { + map_serializer.serialize_key(&signature.id())?; + map_serializer.serialize_value(&signature.base64())?; + } + + map_serializer.end() + } +} + +impl<'de> Deserialize<'de> for SignatureSet { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(SignatureSetVisitor) + } +} + +/// Serde Visitor for deserializing `SignatureSet`. +struct SignatureSetVisitor; + +impl<'de> Visitor<'de> for SignatureSetVisitor { + type Value = SignatureSet; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a set of digital signatures") + } + + fn visit_map(self, mut visitor: M) -> Result + where + M: MapAccess<'de>, + { + let mut signature_set = match visitor.size_hint() { + Some(capacity) => SignatureSet::with_capacity(capacity), + None => SignatureSet::new(), + }; + + while let Some((key, value)) = visitor.next_entry::()? { + let (algorithm, version) = split_id(&key).map_err(|split_error| match split_error { + SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), + SplitError::UnknownAlgorithm(algorithm) => { + M::Error::invalid_value(Unexpected::Str(algorithm), &self) + } + })?; + + let signature_bytes: Vec = match decode_config(&value, STANDARD_NO_PAD) { + Ok(raw) => raw, + Err(error) => return Err(M::Error::custom(error.description())), + }; + + let signature = Signature { + algorithm, + signature: signature_bytes, + version, + }; + + signature_set.insert(signature); + } + + Ok(signature_set) + } +} + +/// 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 has an invalid length. + InvalidLength(usize), + /// 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 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())) +} diff --git a/src/verification.rs b/src/verification.rs new file mode 100644 index 00000000..664be611 --- /dev/null +++ b/src/verification.rs @@ -0,0 +1,48 @@ +//! Verification of digital signatures. + +use ring::signature::{verify, ED25519}; +use untrusted::Input; + +use crate::{signatures::Signature, 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 public key of the key pair used to sign the message. + /// * signature: The `Signature` to verify. + /// * message: The message that was signed. + /// + /// # Errors + /// + /// Returns an error if verification fails. + fn verify_json( + &self, + public_key: &[u8], + signature: &Signature, + 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: &Signature, + message: &[u8], + ) -> Result<(), Error> { + verify( + &ED25519, + Input::from(public_key), + Input::from(message), + Input::from(signature.as_bytes()), + ) + .map_err(|_| Error::new("signature verification failed")) + } +} From 07295b11bb03c92437b76197ef6abbe3201ffa53 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 21:22:17 -0700 Subject: [PATCH 44/98] Rename `Signatures` `SignatureMap`. --- src/lib.rs | 47 +++++++++++++++++++++++++---------------------- src/signatures.rs | 26 +++++++++++++------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c051e682..30f81271 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,7 @@ //! //! Verifying signatures of Matrix events is not yet implemented by ruma-signatures. //! -//! # Signature sets +//! # Signature sets and maps //! //! Signatures that a homeserver has added to an event are stored in a JSON object under the //! "signatures" key in the event's JSON representation: @@ -96,7 +96,7 @@ //! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, //! a `SignatureSet` can be produced by deserializing JSON that follows this form. //! -//! The outer object (the map of server names to signature sets) is a `Signatures` value and +//! The outer object (the map of server names to signature sets) is a `SignatureMap` value and //! created like this: //! //! ```rust,no_run @@ -109,12 +109,13 @@ //! ); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); -//! let mut signatures = ruma_signatures::Signatures::new(); -//! signatures.insert("example.com", signature_set).expect("example.com is a valid server name"); -//! serde_json::to_string(&signatures).expect("signatures should serialize"); +//! let mut signature_map = ruma_signatures::SignatureMap::new(); +//! signature_map.insert("example.com", signature_set).expect("example.com is a valid server name"); +//! serde_json::to_string(&signature_map).expect("signature_map should serialize"); //! ``` //! -//! Just like the `SignatureSet` itself, the `Signatures` value can also be deserialized from JSON. +//! Just like the `SignatureSet` itself, the `SignatureMap` value can also be deserialized from +//! JSON. #![deny( missing_copy_implementations, @@ -152,7 +153,7 @@ use serde_json::{to_string, Value}; pub use url::Host; pub use keys::{Ed25519KeyPair, KeyPair}; -pub use signatures::{Signature, SignatureSet, Signatures}; +pub use signatures::{Signature, SignatureMap, SignatureSet}; pub use verification::{Ed25519Verifier, Verifier}; mod keys; @@ -288,8 +289,8 @@ mod test { use serde_json::{from_str, to_string, to_value}; use super::{ - sign_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, SignatureSet, - Signatures, + sign_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, SignatureMap, + SignatureSet, }; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -346,10 +347,10 @@ mod test { } #[test] - fn signatures_empty_json() { + fn signature_map_empty_json() { #[derive(Serialize)] - struct EmptyWithSignatures { - signatures: Signatures, + struct EmptyWithSignatureMap { + signatures: SignatureMap, } let signature = Signature::new( @@ -363,10 +364,12 @@ mod test { let mut signature_set = SignatureSet::with_capacity(1); signature_set.insert(signature); - let mut signatures = Signatures::with_capacity(1); - signatures.insert("domain", signature_set).ok(); + let mut signature_map = SignatureMap::with_capacity(1); + signature_map.insert("domain", signature_set).ok(); - let empty = EmptyWithSignatures { signatures }; + let empty = EmptyWithSignatureMap { + signatures: signature_map, + }; let json = to_string(&empty).unwrap(); @@ -464,11 +467,11 @@ mod test { } #[test] - fn signatures_minimal_json() { + fn signature_map_minimal_json() { #[derive(Serialize)] - struct MinimalWithSignatures { + struct MinimalWithSignatureMap { one: u8, - signatures: Signatures, + signatures: SignatureMap, two: String, } @@ -483,12 +486,12 @@ mod test { let mut signature_set = SignatureSet::with_capacity(1); signature_set.insert(signature); - let mut signatures = Signatures::with_capacity(1); - signatures.insert("domain", signature_set).ok(); + let mut signature_map = SignatureMap::with_capacity(1); + signature_map.insert("domain", signature_set).ok(); - let minimal = MinimalWithSignatures { + let minimal = MinimalWithSignatureMap { one: 1, - signatures: signatures.clone(), + signatures: signature_map.clone(), two: "Two".to_string(), }; diff --git a/src/signatures.rs b/src/signatures.rs index 16ea3ad0..5bcf581f 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -89,20 +89,20 @@ impl Signature { /// A map of server names to sets of digital signatures created by that server. #[derive(Clone, Debug, Default, PartialEq)] -pub struct Signatures { +pub struct SignatureMap { /// A map of homeservers to sets of signatures for the homeserver. map: HashMap, } -impl Signatures { - /// Initializes a new empty Signatures. +impl SignatureMap { + /// Initializes a new empty `SignatureMap`. pub fn new() -> Self { Self { map: HashMap::new(), } } - /// Initializes a new empty Signatures with room for a specific number of servers. + /// Initializes a new empty `SignatureMap` with room for a specific number of servers. /// /// # Parameters /// @@ -154,7 +154,7 @@ impl Signatures { } } -impl Serialize for Signatures { +impl Serialize for SignatureMap { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -170,20 +170,20 @@ impl Serialize for Signatures { } } -impl<'de> Deserialize<'de> for Signatures { +impl<'de> Deserialize<'de> for SignatureMap { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - deserializer.deserialize_map(SignaturesVisitor) + deserializer.deserialize_map(SignatureMapVisitor) } } -/// Serde Visitor for deserializing `Signatures`. -struct SignaturesVisitor; +/// Serde Visitor for deserializing `SignatureMap`. +struct SignatureMapVisitor; -impl<'de> Visitor<'de> for SignaturesVisitor { - type Value = Signatures; +impl<'de> Visitor<'de> for SignatureMapVisitor { + type Value = SignatureMap; fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "digital signatures") @@ -194,8 +194,8 @@ impl<'de> Visitor<'de> for SignaturesVisitor { M: MapAccess<'de>, { let mut signatures = match visitor.size_hint() { - Some(capacity) => Signatures::with_capacity(capacity), - None => Signatures::new(), + Some(capacity) => SignatureMap::with_capacity(capacity), + None => SignatureMap::new(), }; while let Some((server_name, signature_set)) = From 9a56e2b0a6fef53effd949b4b8470dea587dac46 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 22:01:15 -0700 Subject: [PATCH 45/98] Add tests for canonical JSON. --- src/lib.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 30f81271..c9689677 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -286,11 +286,11 @@ impl Display for Algorithm { mod test { use base64::{decode_config, STANDARD_NO_PAD}; use serde::Serialize; - use serde_json::{from_str, to_string, to_value}; + use serde_json::{from_str, to_string, to_value, Value}; use super::{ - sign_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, SignatureMap, - SignatureSet, + sign_json, to_canonical_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, + Signature, SignatureMap, SignatureSet, }; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -301,6 +301,102 @@ mod test { const MINIMAL_JSON_SIGNATURE: &str = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; + fn test_canonical_json(input: &str) -> String { + let value = from_str::(input).unwrap(); + + to_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( From 202b3ed402c938727de76aae684871b34ada9663 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Jul 2019 23:35:09 -0700 Subject: [PATCH 46/98] Add content_hash and reference_hash functions and move all functions into a module. --- src/functions.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 74 +--------------- 2 files changed, 225 insertions(+), 72 deletions(-) create mode 100644 src/functions.rs diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 00000000..47046642 --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,223 @@ +//! Functions for signing and verifying JSON and events. + +use std::error::Error as _; + +use base64::{encode_config, STANDARD_NO_PAD}; +use ring::digest::{digest, SHA256}; +use serde_json::{to_string, Value}; + +use crate::{keys::KeyPair, signatures::Signature, verification::Verifier, Error}; + +/// The fields that are allowed to remain in an event during redaction. +static ALLOWED_KEYS: &[&str] = &[ + "event_id", + "type", + "room_id", + "sender", + "state_key", + "content", + "hashes", + "signatures", + "depth", + "prev_events", + "prev_state", + "auth_events", + "origin", + "origin_server_ts", + "membership", +]; + +/// The fields of an *m.room.power_levels* event's `content` key that are allowed to remain in an +/// event during redaction. +static ALLOWED_POWER_LEVELS_KEYS: &[&str] = &[ + "ban", + "events", + "events_default", + "kick", + "redact", + "state_default", + "users", + "users_default", +]; + +/// The fields to remove from a JSON object when converting JSON into the "canonical" form. +static CANONICAL_JSON_FIELDS_TO_REMOVE: &[&str] = &["signatures", "unsigned"]; + +/// The fields to remove from a JSON object when creating a content hash of an event. +static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsigned"]; + +/// The fields to remove from a JSON object when creating a reference hash of an event. +static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "unsigned"]; + +/// Signs an arbitrary JSON object. +/// +/// # Parameters +/// +/// * key_pair: A cryptographic key pair used to sign the JSON. +/// * value: A JSON object to be signed according to the Matrix specification. +/// +/// # Errors +/// +/// Returns an error if the JSON value is not a JSON object. +pub fn sign_json(key_pair: &K, value: &Value) -> Result +where + K: KeyPair, +{ + let json = to_canonical_json(value)?; + + Ok(key_pair.sign(json.as_bytes())) +} + +/// Converts a JSON object into the "canonical" string form, suitable for signing. +/// +/// # Parameters +/// +/// * value: The `serde_json::Value` (JSON value) to convert. +/// +/// # Errors +/// +/// Returns an error if the provided JSON value is not a JSON object. +pub fn to_canonical_json(value: &Value) -> Result { + to_canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) +} + +/// Use a public key to verify a signature of a JSON object. +/// +/// # Parameters +/// +/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. +/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. +/// * signature: The `Signature` to verify. +/// * value: The `serde_json::Value` (JSON value) that was signed. +/// +/// # Errors +/// +/// Returns an error if verification fails. +pub fn verify_json( + verifier: &V, + public_key: &[u8], + signature: &Signature, + value: &Value, +) -> Result<(), Error> +where + V: Verifier, +{ + verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) +} + +/// Creates a *content hash* for the JSON representation of an event. +/// +/// The content hash of an event covers the complete event including the unredacted contents. It is +/// used during federation and is described in the Matrix server-server specification. +pub fn content_hash(value: &Value) -> Result { + let json = to_canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?; + + let hash = digest(&SHA256, json.as_bytes()); + + Ok(encode_config(&hash, STANDARD_NO_PAD)) +} + +/// Creates a *reference hash* for the JSON representation of an event. +/// +/// The reference hash of an event covers the essential fields of an event, including content +/// hashes. It is used during federation and is described in the Matrix server-server +/// specification. +pub fn reference_hash(value: &Value) -> Result { + let redacted_value = redact(value)?; + + let json = + to_canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE)?; + + let hash = digest(&SHA256, json.as_bytes()); + + Ok(encode_config(&hash, STANDARD_NO_PAD)) +} + +/// Internal implementation detail of the canonical JSON algorithm. Allows customization of the +/// fields that will be removed before serializing. +fn to_canonical_json_with_fields_to_remove( + value: &Value, + fields: &[&str], +) -> Result { + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + { + let object = owned_value + .as_object_mut() + .expect("safe since we checked above"); + + for field in fields { + object.remove(*field); + } + } + + to_string(&owned_value).map_err(|error| Error::new(error.description())) +} + +/// Redact the JSON representation of an event using the rules specified in the Matrix +/// client-server specification. +/// +/// This is part of the process of signing an event. +fn redact(value: &Value) -> Result { + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + let event = owned_value + .as_object_mut() + .expect("safe since we checked above"); + + let event_type_value = match event.get("type") { + Some(event_type_value) => event_type_value, + None => return Err(Error::new("Field `type` in JSON value must be present")), + }; + + let event_type = match event_type_value.as_str() { + Some(event_type) => event_type.to_string(), + None => { + return Err(Error::new( + "Field `type` in JSON value must be a JSON string", + )) + } + }; + + if let Some(content_value) = event.get_mut("content") { + if !content_value.is_object() { + return Err(Error::new( + "Field `content` in JSON value must be a JSON object", + )); + } + + let content = content_value + .as_object_mut() + .expect("safe since we checked above"); + + for key in content.clone().keys() { + match event_type.as_ref() { + "m.room.member" if key != "membership" => content.remove(key), + "m.room.create" if key != "creator" => content.remove(key), + "m.room.join_rules" if key != "join_rules" => content.remove(key), + "m.room.power_levels" if !ALLOWED_POWER_LEVELS_KEYS.contains(&key.as_ref()) => { + content.remove(key) + } + "m.room.aliases" if key != "aliases" => content.remove(key), + "m.room.history_visibility" if key != "history_visibility" => content.remove(key), + _ => content.remove(key), + }; + } + } + + for key in event.clone().keys() { + if !ALLOWED_KEYS.contains(&key.as_ref()) { + event.remove(key); + } + } + + Ok(owned_value) +} diff --git a/src/lib.rs b/src/lib.rs index c9689677..36d3c8a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,88 +148,18 @@ use std::{ fmt::{Display, Formatter, Result as FmtResult}, }; -use serde_json::{to_string, Value}; - pub use url::Host; +pub use functions::{content_hash, reference_hash, sign_json, to_canonical_json, verify_json}; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; pub use verification::{Ed25519Verifier, Verifier}; +mod functions; mod keys; mod signatures; mod verification; -/// Signs an arbitrary JSON object. -/// -/// # Parameters -/// -/// * key_pair: A cryptographic key pair used to sign the JSON. -/// * value: A JSON object to be signed according to the Matrix specification. -/// -/// # Errors -/// -/// Returns an error if the JSON value is not a JSON object. -pub fn sign_json(key_pair: &K, value: &Value) -> Result -where - K: KeyPair, -{ - let json = to_canonical_json(value)?; - - Ok(key_pair.sign(json.as_bytes())) -} - -/// Converts a JSON object into the "canonical" string form, suitable for signing. -/// -/// # Parameters -/// -/// * value: The `serde_json::Value` (JSON value) to convert. -/// -/// # Errors -/// -/// Returns an error if the provided JSON value is not a JSON object. -pub fn to_canonical_json(value: &Value) -> Result { - if !value.is_object() { - return Err(Error::new("JSON value must be a JSON object")); - } - - let mut owned_value = value.clone(); - - { - let object = owned_value - .as_object_mut() - .expect("safe since we checked above"); - object.remove("signatures"); - object.remove("unsigned"); - } - - to_string(&owned_value).map_err(|error| Error::new(error.description())) -} - -/// Use a public key to verify a signature of a JSON object. -/// -/// # Parameters -/// -/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. -/// * signature: The `Signature` to verify. -/// * value: The `serde_json::Value` (JSON value) that was signed. -/// -/// # Errors -/// -/// Returns an error if verification fails. -pub fn verify_json( - verifier: &V, - public_key: &[u8], - signature: &Signature, - value: &Value, -) -> Result<(), Error> -where - V: Verifier, -{ - verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) -} - /// An error produced when ruma-signatures operations fail. #[derive(Clone, Debug, PartialEq)] pub struct Error { From 47c6de7a1ba40986bcab8082e46a9549837cccf7 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 00:05:01 -0700 Subject: [PATCH 47/98] Add hash_and_sign_event function. --- src/functions.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 4 ++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 47046642..1c659a92 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -4,9 +4,14 @@ use std::error::Error as _; use base64::{encode_config, STANDARD_NO_PAD}; use ring::digest::{digest, SHA256}; -use serde_json::{to_string, Value}; +use serde_json::{json, to_string, to_value, Value}; -use crate::{keys::KeyPair, signatures::Signature, verification::Verifier, Error}; +use crate::{ + keys::KeyPair, + signatures::{Signature, SignatureMap, SignatureSet}, + verification::Verifier, + Error, +}; /// The fields that are allowed to remain in an event during redaction. static ALLOWED_KEYS: &[&str] = &[ @@ -133,6 +138,62 @@ pub fn reference_hash(value: &Value) -> Result { Ok(encode_config(&hash, STANDARD_NO_PAD)) } +/// Hashes and signs the JSON representation of an event. +/// +/// # Parameters +/// +/// * server_name: The hostname or IP of the 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. +pub fn hash_and_sign_event( + server_name: &str, + key_pair: &K, + value: &Value, +) -> Result +where + K: KeyPair, +{ + let hash = content_hash(value)?; + + if !value.is_object() { + return Err(Error::new("JSON value must be a JSON object")); + } + + let mut owned_value = value.clone(); + + // Limit the scope of the mutable borrow so `owned_value` can be passed immutably to `redact` + // below. + { + let object = owned_value + .as_object_mut() + .expect("safe since we checked above"); + + let hashes = json!({ "sha256": hash }); + + object.insert("hashes".to_string(), hashes); + } + + let redacted = redact(&owned_value)?; + + let signature = sign_json(key_pair, &redacted)?; + + let mut signature_set = SignatureSet::with_capacity(1); + signature_set.insert(signature); + + let mut signature_map = SignatureMap::with_capacity(1); + signature_map.insert(server_name, signature_set)?; + + let signature_map_value = + to_value(signature_map).map_err(|error| Error::new(error.to_string()))?; + + let object = owned_value + .as_object_mut() + .expect("safe since we checked above"); + object.insert("signatures".to_string(), signature_map_value); + + Ok(owned_value) +} + /// Internal implementation detail of the canonical JSON algorithm. Allows customization of the /// fields that will be removed before serializing. fn to_canonical_json_with_fields_to_remove( diff --git a/src/lib.rs b/src/lib.rs index 36d3c8a3..9e48dac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,7 +150,9 @@ use std::{ pub use url::Host; -pub use functions::{content_hash, reference_hash, sign_json, to_canonical_json, verify_json}; +pub use functions::{ + content_hash, hash_and_sign_event, reference_hash, sign_json, to_canonical_json, verify_json, +}; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; pub use verification::{Ed25519Verifier, Verifier}; From d7d098ba35b597d24df2a94977392caa1894bf8c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 00:13:58 -0700 Subject: [PATCH 48/98] Clarify what reference hashes are used for. --- src/functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.rs b/src/functions.rs index 1c659a92..129c0c03 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -125,7 +125,7 @@ pub fn content_hash(value: &Value) -> Result { /// Creates a *reference hash* for the JSON representation of an event. /// /// The reference hash of an event covers the essential fields of an event, including content -/// hashes. It is used during federation and is described in the Matrix server-server +/// hashes. It is used to generate event identifiers and is described in the Matrix server-server /// specification. pub fn reference_hash(value: &Value) -> Result { let redacted_value = redact(value)?; From 49f3208e7f2ed45aabb730fb78a1cb06814d7556 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 01:52:10 -0700 Subject: [PATCH 49/98] Add get and get_mut methods to SignatureMap. --- src/signatures.rs | 51 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/signatures.rs b/src/signatures.rs index 5bcf581f..efacc138 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -131,18 +131,41 @@ impl SignatureMap { server_name: &str, signature_set: SignatureSet, ) -> Result, Error> { - let url_string = format!("https://{}", server_name); - let url = Url::parse(&url_string) - .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; - - let host = match url.host() { - Some(host) => host.to_owned(), - None => return Err(Error::new(format!("invalid server name: {}", server_name))), - }; + let host = server_name_to_host(server_name)?; Ok(self.map.insert(host, signature_set)) } + /// Gets a reference to the signature set for the given server, if any. + /// + /// # Parameters + /// + /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. + /// + /// # Errors + /// + /// Returns an error if the given server name cannot be parsed as a valid host. + pub fn get(&self, server_name: &str) -> Result, Error> { + let host = server_name_to_host(server_name)?; + + Ok(self.map.get(&host)) + } + + /// Gets a mutable reference to the signature set for the given server, if any. + /// + /// # Parameters + /// + /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. + /// + /// # Errors + /// + /// Returns an error if the given server name cannot be parsed as a valid host. + pub fn get_mut(&mut self, server_name: &str) -> Result, Error> { + let host = server_name_to_host(server_name)?; + + Ok(self.map.get_mut(&host)) + } + /// The number of servers in the collection. pub fn len(&self) -> usize { self.map.len() @@ -363,3 +386,15 @@ fn split_id(id: &str) -> Result<(Algorithm, String), SplitError<'_>> { Ok((algorithm, signature_id[1].to_string())) } + +/// Attempts to convert a server name as a string into a `url::Host`. +fn server_name_to_host(server_name: &str) -> Result { + let url_string = format!("https://{}", server_name); + let url = Url::parse(&url_string) + .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; + + match url.host() { + Some(host) => Ok(host.to_owned()), + None => Err(Error::new(format!("invalid server name: {}", server_name))), + } +} From fb222b8e8203e5786301b1c35dda51dac44ae041 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 02:49:43 -0700 Subject: [PATCH 50/98] Add entry method to SignatureMap. --- src/signatures.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/signatures.rs b/src/signatures.rs index efacc138..e3bd1434 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -1,7 +1,7 @@ //! Digital signatures and collections of signatures. use std::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, error::Error as _, fmt::{Formatter, Result as FmtResult}, }; @@ -136,6 +136,13 @@ impl SignatureMap { Ok(self.map.insert(host, signature_set)) } + /// Gets the given server's corresponding signature set for in-place manipulation. + pub fn entry(&mut self, server_name: &str) -> Result, Error> { + let host = server_name_to_host(server_name)?; + + Ok(self.map.entry(host)) + } + /// Gets a reference to the signature set for the given server, if any. /// /// # Parameters From ad3b4401f0db16c0b9b3475049426968aac58b2e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 02:50:37 -0700 Subject: [PATCH 51/98] Rework `sign_json` to mutate the JSON the signature will be added to. --- src/functions.rs | 83 +++++++++++++++++++++++++++++++++++------------- src/lib.rs | 33 ++++++++++--------- 2 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 129c0c03..6e358379 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,10 +1,8 @@ //! Functions for signing and verifying JSON and events. -use std::error::Error as _; - use base64::{encode_config, STANDARD_NO_PAD}; use ring::digest::{digest, SHA256}; -use serde_json::{json, to_string, to_value, Value}; +use serde_json::{from_str, json, to_string, to_value, Value}; use crate::{ keys::KeyPair, @@ -54,26 +52,72 @@ static CONTENT_HASH_FIELDS_TO_REMOVE: &[&str] = &["hashes", "signatures", "unsig /// The fields to remove from a JSON object when creating a reference hash of an event. static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "unsigned"]; -/// Signs an arbitrary JSON object. +/// 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 /// +/// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. /// * key_pair: A cryptographic key pair used to sign the JSON. -/// * value: A JSON object to be signed according to the Matrix specification. +/// * value: A JSON object to sign according and append a signature to. /// /// # Errors /// /// Returns an error if the JSON value is not a JSON object. -pub fn sign_json(key_pair: &K, value: &Value) -> Result +pub fn sign_json(server_name: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> where K: KeyPair, { - let json = to_canonical_json(value)?; + let mut signature_map; + let maybe_unsigned; - Ok(key_pair.sign(json.as_bytes())) + // 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_str(&to_string(signatures)?)?, + None => return Err(Error::new("Field `signatures` must be a JSON object")), + }, + None => SignatureMap::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(server_name)? + .or_insert_with(|| SignatureSet::with_capacity(1)); + + signature_set.insert(signature); + + // 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" string form, suitable for signing. +/// Converts a JSON object into the "canonical" string form. /// /// # Parameters /// @@ -112,6 +156,8 @@ where /// Creates a *content hash* for the JSON representation of an event. /// +/// Returns the hash as a Base64-encoded string 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. pub fn content_hash(value: &Value) -> Result { @@ -124,6 +170,8 @@ pub fn content_hash(value: &Value) -> Result { /// Creates a *reference hash* for the JSON representation of an event. /// +/// Returns the hash as a Base64-encoded string 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. @@ -173,23 +221,14 @@ where object.insert("hashes".to_string(), hashes); } - let redacted = redact(&owned_value)?; + let mut redacted = redact(&owned_value)?; - let signature = sign_json(key_pair, &redacted)?; - - let mut signature_set = SignatureSet::with_capacity(1); - signature_set.insert(signature); - - let mut signature_map = SignatureMap::with_capacity(1); - signature_map.insert(server_name, signature_set)?; - - let signature_map_value = - to_value(signature_map).map_err(|error| Error::new(error.to_string()))?; + sign_json(server_name, key_pair, &mut redacted)?; let object = owned_value .as_object_mut() .expect("safe since we checked above"); - object.insert("signatures".to_string(), signature_map_value); + object.insert("signatures".to_string(), redacted["signatures"].take()); Ok(owned_value) } @@ -216,7 +255,7 @@ fn to_canonical_json_with_fields_to_remove( } } - to_string(&owned_value).map_err(|error| Error::new(error.description())) + to_string(&owned_value).map_err(Error::from) } /// Redact the JSON representation of an event using the rules specified in the Matrix diff --git a/src/lib.rs b/src/lib.rs index 9e48dac8..c3e8d161 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,12 +25,9 @@ //! &public_key, // &[u8] //! &private_key, // &[u8] //! "1".to_string(), // The "version" of the key. -//! ).expect("the provided keys should be suitable for Ed25519"); -//! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); -//! let signature = ruma_signatures::sign_json( -//! &key_pair, -//! &value, -//! ).expect("`value` must be a JSON object"); +//! ).unwrap(); +//! let mut value = serde_json::from_str("{}").unwrap(); +//! ruma_signatures::sign_json("example.com", &key_pair, &mut value).unwrap() //! ``` //! //! # Signing Matrix events @@ -197,6 +194,12 @@ impl Display for Error { } } +impl From 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 { @@ -342,11 +345,11 @@ mod test { ) .unwrap(); - let value = from_str("{}").unwrap(); + let mut value = from_str("{}").unwrap(); - let signature = sign_json(&key_pair, &value).unwrap(); + sign_json("example.com", &key_pair, &mut value).unwrap(); - assert_eq!(signature.base64(), EMPTY_JSON_SIGNATURE); + assert_eq!(value.pointer("/signatures/example.com/ed25519:1").unwrap(), EMPTY_JSON_SIGNATURE); } #[test] @@ -442,15 +445,15 @@ mod test { one: 1, }; - let alpha_value = to_value(alpha).expect("alpha should serialize"); - let alpha_signature = sign_json(&key_pair, &alpha_value).unwrap(); + let mut alpha_value = to_value(alpha).expect("alpha should serialize"); + sign_json("example.com", &key_pair, &mut alpha_value).unwrap(); - assert_eq!(alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); + assert_eq!(alpha_value.pointer("/signatures/example.com/ed25519:1").unwrap(), MINIMAL_JSON_SIGNATURE); - let reverse_alpha_value = to_value(reverse_alpha).expect("reverse_alpha should serialize"); - let reverse_alpha_signature = sign_json(&key_pair, &reverse_alpha_value).unwrap(); + let mut reverse_alpha_value = to_value(reverse_alpha).expect("reverse_alpha should serialize"); + sign_json("example.com", &key_pair, &mut reverse_alpha_value).unwrap(); - assert_eq!(reverse_alpha_signature.base64(), MINIMAL_JSON_SIGNATURE); + assert_eq!(reverse_alpha_value.pointer("/signatures/example.com/ed25519:1").unwrap(), MINIMAL_JSON_SIGNATURE); } #[test] From 66f35cd12ad4456118cdb6b1e0679344e29ad3fa Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 03:01:36 -0700 Subject: [PATCH 52/98] Update tests to verify new API. --- src/lib.rs | 103 ++++++++++++----------------------------------------- 1 file changed, 22 insertions(+), 81 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c3e8d161..23a1c7cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,17 +225,17 @@ mod test { use super::{ sign_json, to_canonical_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, - Signature, SignatureMap, SignatureSet, + Signature, }; - - const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; - const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; - const EMPTY_JSON_SIGNATURE: &str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; const MINIMAL_JSON_SIGNATURE: &str = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; + const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; + const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; + + /// Convenience for converting a string of JSON into its canonical form. fn test_canonical_json(input: &str) -> String { let value = from_str::(input).unwrap(); @@ -349,7 +349,10 @@ mod test { sign_json("example.com", &key_pair, &mut value).unwrap(); - assert_eq!(value.pointer("/signatures/example.com/ed25519:1").unwrap(), EMPTY_JSON_SIGNATURE); + assert_eq!( + to_string(&value).unwrap(), + r#"{"signatures":{"example.com":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# + ); } #[test] @@ -377,39 +380,6 @@ mod test { .is_ok()); } - #[test] - fn signature_map_empty_json() { - #[derive(Serialize)] - struct EmptyWithSignatureMap { - signatures: SignatureMap, - } - - let signature = Signature::new( - "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - ) - .unwrap(); - - let mut signature_set = SignatureSet::with_capacity(1); - signature_set.insert(signature); - - let mut signature_map = SignatureMap::with_capacity(1); - signature_map.insert("domain", signature_set).ok(); - - let empty = EmptyWithSignatureMap { - signatures: signature_map, - }; - - let json = to_string(&empty).unwrap(); - - assert_eq!( - json, - r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# - ); - } - #[test] fn sign_minimal_json() { #[derive(Serialize)] @@ -448,12 +418,19 @@ mod test { let mut alpha_value = to_value(alpha).expect("alpha should serialize"); sign_json("example.com", &key_pair, &mut alpha_value).unwrap(); - assert_eq!(alpha_value.pointer("/signatures/example.com/ed25519:1").unwrap(), MINIMAL_JSON_SIGNATURE); + assert_eq!( + to_string(&alpha_value).unwrap(), + r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + ); - let mut reverse_alpha_value = to_value(reverse_alpha).expect("reverse_alpha should serialize"); + let mut reverse_alpha_value = + to_value(reverse_alpha).expect("reverse_alpha should serialize"); sign_json("example.com", &key_pair, &mut reverse_alpha_value).unwrap(); - assert_eq!(reverse_alpha_value.pointer("/signatures/example.com/ed25519:1").unwrap(), MINIMAL_JSON_SIGNATURE); + assert_eq!( + to_string(&reverse_alpha_value).unwrap(), + r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + ); } #[test] @@ -467,7 +444,7 @@ mod test { .unwrap(); let value = from_str( - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); let verifier = Ed25519Verifier; @@ -483,7 +460,7 @@ mod test { .is_ok()); let reverse_value = from_str( - r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# + r#"{"two":"Two","signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); assert!(verify_json( @@ -498,43 +475,7 @@ mod test { } #[test] - fn signature_map_minimal_json() { - #[derive(Serialize)] - struct MinimalWithSignatureMap { - one: u8, - signatures: SignatureMap, - two: String, - } - - let signature = Signature::new( - "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - ) - .unwrap(); - - let mut signature_set = SignatureSet::with_capacity(1); - signature_set.insert(signature); - - let mut signature_map = SignatureMap::with_capacity(1); - signature_map.insert("domain", signature_set).ok(); - - let minimal = MinimalWithSignatureMap { - one: 1, - signatures: signature_map.clone(), - two: "Two".to_string(), - }; - - let json = to_string(&minimal).unwrap(); - assert_eq!( - json, - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# - ); - } - - #[test] - fn fail_verify() { + fn fail_verify_json() { let signature = Signature::new( "ed25519:1", decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) From 0d26b740513caafe3bd7283d4bb87fdd61c4c7e8 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 03:34:44 -0700 Subject: [PATCH 53/98] Rework `hash_and_sign_event` to mutate JSON. --- src/functions.rs | 49 +++++++++++++++++++++++++----------------------- src/lib.rs | 43 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 6e358379..cec427ad 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -2,7 +2,7 @@ use base64::{encode_config, STANDARD_NO_PAD}; use ring::digest::{digest, SHA256}; -use serde_json::{from_str, json, to_string, to_value, Value}; +use serde_json::{from_str, map::Map, to_string, to_value, Value}; use crate::{ keys::KeyPair, @@ -186,7 +186,11 @@ pub fn reference_hash(value: &Value) -> Result { Ok(encode_config(&hash, STANDARD_NO_PAD)) } -/// Hashes and signs the JSON representation of an event. +/// 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 /// @@ -196,41 +200,40 @@ pub fn reference_hash(value: &Value) -> Result { pub fn hash_and_sign_event( server_name: &str, key_pair: &K, - value: &Value, -) -> Result + value: &mut Value, +) -> Result<(), Error> where K: KeyPair, { let hash = content_hash(value)?; - if !value.is_object() { - return Err(Error::new("JSON value must be a JSON object")); - } - - let mut owned_value = value.clone(); - - // Limit the scope of the mutable borrow so `owned_value` can be passed immutably to `redact` - // below. + // Limit the scope of the mutable borrow so `value` can be passed immutably to `redact` below. { - let object = owned_value - .as_object_mut() - .expect("safe since we checked above"); + let map = match value { + Value::Object(ref mut map) => map, + _ => return Err(Error::new("JSON value must be a JSON object")), + }; - let hashes = json!({ "sha256": hash }); + let hashes_value = map + .entry("hashes") + .or_insert_with(|| Value::Object(Map::with_capacity(1))); - object.insert("hashes".to_string(), hashes); + 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(&owned_value)?; + let mut redacted = redact(value)?; sign_json(server_name, key_pair, &mut redacted)?; - let object = owned_value - .as_object_mut() - .expect("safe since we checked above"); - object.insert("signatures".to_string(), redacted["signatures"].take()); + // Safe to unwrap because we did this exact check at the beginning of the function. + let map = value.as_object_mut().unwrap(); - Ok(owned_value) + map.insert("signatures".to_string(), redacted["signatures"].take()); + + Ok(()) } /// Internal implementation detail of the canonical JSON algorithm. Allows customization of the diff --git a/src/lib.rs b/src/lib.rs index 23a1c7cf..2ae5930b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,8 +224,8 @@ mod test { use serde_json::{from_str, to_string, to_value, Value}; use super::{ - sign_json, to_canonical_json, verify_json, Ed25519KeyPair, Ed25519Verifier, KeyPair, - Signature, + hash_and_sign_event, sign_json, to_canonical_json, verify_json, Ed25519KeyPair, + Ed25519Verifier, KeyPair, Signature, }; const EMPTY_JSON_SIGNATURE: &str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; @@ -498,4 +498,43 @@ mod test { ) .is_err()); } + + #[test] + fn sign_minimal_event() { + let key_pair = Ed25519KeyPair::new( + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) + .unwrap() + .as_slice(), + decode_config(&PRIVATE_KEY, 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::(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":"KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg"}},"type":"X","unsigned":{"age_ts":1000000}}"# + ); + } } From 4e7bc994ebdefde44a6f831701fd81903a325a42 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 03:39:57 -0700 Subject: [PATCH 54/98] Add a test for signing a redacted event. --- src/lib.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2ae5930b..2ca54c80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -537,4 +537,42 @@ mod test { 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}}"# ); } + + #[test] + fn sign_redacted_event() { + let key_pair = Ed25519KeyPair::new( + decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) + .unwrap() + .as_slice(), + decode_config(&PRIVATE_KEY, 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::(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":"Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUwu6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# + ); + } } From 935407c8053e98e65293c720332218c59950cc7f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 03:55:20 -0700 Subject: [PATCH 55/98] Document parameters and return conditions. --- src/functions.rs | 33 ++++++++++++++++++++++++++++++++- src/signatures.rs | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/functions.rs b/src/functions.rs index cec427ad..35071fc9 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -64,7 +64,11 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// /// # Errors /// -/// Returns an error if the JSON value is not a JSON object. +/// Returns an error if: +/// +/// * `value` is not a JSON object. +/// * `value` contains a field called `signatures` that is not a JSON object. +/// * `server_name` cannot be parsed as a valid host. pub fn sign_json(server_name: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> where K: KeyPair, @@ -160,6 +164,14 @@ where /// /// 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 { let json = to_canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?; @@ -175,6 +187,14 @@ pub fn content_hash(value: &Value) -> Result { /// 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 { let redacted_value = redact(value)?; @@ -197,6 +217,17 @@ pub fn reference_hash(value: &Value) -> Result { /// * server_name: The hostname or IP of the 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. +/// * `server_name` cannot be parsed as a valid host. pub fn hash_and_sign_event( server_name: &str, key_pair: &K, diff --git a/src/signatures.rs b/src/signatures.rs index e3bd1434..bb5dd306 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -137,6 +137,14 @@ impl SignatureMap { } /// Gets the given server's corresponding signature set for in-place manipulation. + /// + /// # Parameters + /// + /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. + /// + /// # Errors + /// + /// Returns an error if the given server name cannot be parsed as a valid host. pub fn entry(&mut self, server_name: &str) -> Result, Error> { let host = server_name_to_host(server_name)?; From 152458707cf28d24e27b16df14cbd92bd9c7696c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 04:00:21 -0700 Subject: [PATCH 56/98] Simplify extraction of JSON object from event content. --- src/functions.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 35071fc9..dc2aaf2f 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -322,27 +322,26 @@ fn redact(value: &Value) -> Result { }; if let Some(content_value) = event.get_mut("content") { - if !content_value.is_object() { - return Err(Error::new( - "Field `content` in JSON value must be a JSON object", - )); - } + let map = match content_value { + Value::Object(ref mut map) => map, + _ => { + return Err(Error::new( + "Field `content` in JSON value must be a JSON object", + )) + } + }; - let content = content_value - .as_object_mut() - .expect("safe since we checked above"); - - for key in content.clone().keys() { + for key in map.clone().keys() { match event_type.as_ref() { - "m.room.member" if key != "membership" => content.remove(key), - "m.room.create" if key != "creator" => content.remove(key), - "m.room.join_rules" if key != "join_rules" => content.remove(key), + "m.room.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()) => { - content.remove(key) + map.remove(key) } - "m.room.aliases" if key != "aliases" => content.remove(key), - "m.room.history_visibility" if key != "history_visibility" => content.remove(key), - _ => content.remove(key), + "m.room.aliases" if key != "aliases" => map.remove(key), + "m.room.history_visibility" if key != "history_visibility" => map.remove(key), + _ => map.remove(key), }; } } From 14ede0c1458ccd991ad9614221673f22940981c3 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 04:06:40 -0700 Subject: [PATCH 57/98] Use unwrap in docs to make examples more succinct. --- src/lib.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2ca54c80..b8fcae74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,10 +44,8 @@ //! # use serde_json; //! # let public_key = [0; 32]; //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( -//! "key identifier should be valid" -//! ); -//! let value = serde_json::from_str("{}").expect("an empty JSON object should deserialize"); +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! let value = serde_json::from_str("{}").unwrap(); //! let verifier = ruma_signatures::Ed25519Verifier; //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! ``` @@ -82,12 +80,10 @@ //! # use serde; //! # use serde_json; //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( -//! "key identifier should be valid" -//! ); +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); -//! serde_json::to_string(&signature_set).expect("signature_set should serialize"); +//! serde_json::to_string(&signature_set).unwrap(); //! ``` //! //! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, @@ -101,14 +97,12 @@ //! # use serde; //! # use serde_json; //! # let signature_bytes = [0, 32]; -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).expect( -//! "key identifier should be valid" -//! ); +//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); //! let mut signature_map = ruma_signatures::SignatureMap::new(); -//! signature_map.insert("example.com", signature_set).expect("example.com is a valid server name"); -//! serde_json::to_string(&signature_map).expect("signature_map should serialize"); +//! signature_map.insert("example.com", signature_set).unwrap(); +//! serde_json::to_string(&signature_map).unwrap(); //! ``` //! //! Just like the `SignatureSet` itself, the `SignatureMap` value can also be deserialized from From 8c102d11bbd409ef9c5ae04d5c0e2140d2aafbef Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 04:20:43 -0700 Subject: [PATCH 58/98] Update doc examples for new APIs. --- src/lib.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b8fcae74..0e57a544 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,10 +30,87 @@ //! ruma_signatures::sign_json("example.com", &key_pair, &mut value).unwrap() //! ``` //! -//! # Signing Matrix events +//! This will modify the JSON from an empty object to a structure like this: //! -//! Signing an event uses a more involved process than signing arbitrary JSON. -//! Event signing is not yet implemented by ruma-signatures. +//! ```json +//! { +//! "signatures": { +//! "example.com": { +//! "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" +//! } +//! } +//! } +//! ``` +//! +//! # Hashing and signing Matrix events +//! +//! Signing an event uses a more involved 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 and sign 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: +//! +//! ```rust,no_run +//! # use ruma_signatures::{self, KeyPair}; +//! # use serde_json; +//! # let public_key = [0; 32]; +//! # let private_key = [0; 32]; +//! // Create an Ed25519 key pair. +//! let key_pair = ruma_signatures::Ed25519KeyPair::new( +//! &public_key, // &[u8] +//! &private_key, // &[u8] +//! "1".to_string(), // The "version" of the key. +//! ).unwrap(); +//! 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(); +//! ruma_signatures::hash_and_sign_event("example.com", &key_pair, &mut value).unwrap() +//! ``` +//! +//! 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`. //! //! # Verifying signatures //! From 30f830bd5ee71bfbf4b56bd4b735ba98f7059c94 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 15:33:03 -0700 Subject: [PATCH 59/98] Be more clear about the Base64 encoding of signatures and hashes. --- src/functions.rs | 4 ++-- src/lib.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index dc2aaf2f..f6401697 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -160,7 +160,7 @@ where /// Creates a *content hash* for the JSON representation of an event. /// -/// Returns the hash as a Base64-encoded string without padding. +/// 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. @@ -182,7 +182,7 @@ pub fn content_hash(value: &Value) -> Result { /// Creates a *reference hash* for the JSON representation of an event. /// -/// Returns the hash as a Base64-encoded string without padding. +/// 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 diff --git a/src/lib.rs b/src/lib.rs index 0e57a544..28d506f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,9 @@ //! 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. //! +//! In JSON representations, signatures and hashes appear as Base64-encoded strings, using the +//! standard character set, without padding. +//! //! # Signing JSON //! //! A homeserver signs JSON with a key pair: From 8c756141e1c27a42c74da01b54a867285678e21e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 16:29:45 -0700 Subject: [PATCH 60/98] Ensure key ID version contains only valid characters. --- src/signatures.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/signatures.rs b/src/signatures.rs index bb5dd306..01628d5e 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -42,7 +42,8 @@ impl Signature { /// Returns an error if the key identifier is invalid. pub fn new(id: &str, bytes: &[u8]) -> Result { let (algorithm, version) = split_id(id).map_err(|split_error| match split_error { - SplitError::InvalidLength(_) => Error::new("malformed signature ID"), + 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)) } @@ -347,6 +348,9 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { while let Some((key, value)) = visitor.next_entry::()? { let (algorithm, version) = split_id(&key).map_err(|split_error| match split_error { SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), + SplitError::InvalidVersion(version) => { + M::Error::invalid_value(Unexpected::Str(version), &self) + } SplitError::UnknownAlgorithm(algorithm) => { M::Error::invalid_value(Unexpected::Str(algorithm), &self) } @@ -373,8 +377,10 @@ impl<'de> Visitor<'de> for SignatureSetVisitor { /// 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 has an invalid length. + /// 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), } @@ -392,6 +398,19 @@ fn split_id(id: &str) -> Result<(Algorithm, String), SplitError<'_>> { 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 { @@ -413,3 +432,28 @@ fn server_name_to_host(server_name: &str) -> Result { None => Err(Error::new(format!("invalid server name: {}", server_name))), } } + +#[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()); + } +} From eaeb1a66c7cff8640b248c3a5eb3f29aaef9866a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 16:33:31 -0700 Subject: [PATCH 61/98] Remove an unnecessary layer of JSON conversion. --- src/functions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index f6401697..f1b7823a 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -2,7 +2,7 @@ use base64::{encode_config, STANDARD_NO_PAD}; use ring::digest::{digest, SHA256}; -use serde_json::{from_str, map::Map, to_string, to_value, Value}; +use serde_json::{from_value, map::Map, to_string, to_value, Value}; use crate::{ keys::KeyPair, @@ -86,7 +86,7 @@ where signature_map = match map.remove("signatures") { Some(signatures_value) => match signatures_value.as_object() { - Some(signatures) => from_str(&to_string(signatures)?)?, + Some(signatures) => from_value(Value::Object(signatures.clone()))?, None => return Err(Error::new("Field `signatures` must be a JSON object")), }, None => SignatureMap::with_capacity(1), From b85d0fc22c831f1849b2f24a6d511ea23602e4d9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 17:01:57 -0700 Subject: [PATCH 62/98] Reimplement SignatureSet as a map so signatures can be efficiently looked up by key ID. --- src/signatures.rs | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/signatures.rs b/src/signatures.rs index 01628d5e..9a1718f4 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -1,7 +1,7 @@ //! Digital signatures and collections of signatures. use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, + collections::{hash_map::Entry, HashMap}, error::Error as _, fmt::{Formatter, Result as FmtResult}, }; @@ -256,14 +256,14 @@ impl<'de> Visitor<'de> for SignatureMapVisitor { #[derive(Clone, Debug, Default, PartialEq)] pub struct SignatureSet { /// A set of signatures for a homeserver. - set: HashSet, + map: HashMap, } impl SignatureSet { /// Initializes a new empty SignatureSet. pub fn new() -> Self { Self { - set: HashSet::new(), + map: HashMap::new(), } } @@ -274,25 +274,43 @@ impl SignatureSet { /// * capacity: The number of items to allocate memory for. pub fn with_capacity(capacity: usize) -> Self { Self { - set: HashSet::with_capacity(capacity), + map: HashMap::with_capacity(capacity), } } /// Adds a signature to the set. /// - /// The boolean return value indicates whether or not the value was actually inserted, since - /// subsequent inserts of the same signature have no effect. + /// If no signature with the given key ID existed in the collection, `None` is returned. + /// Otherwise, the signature is returned. /// /// # Parameters /// /// * signature: A `Signature` to insert into the set. - pub fn insert(&mut self, signature: Signature) -> bool { - self.set.insert(signature) + pub fn insert(&mut self, signature: Signature) -> Option { + self.map.insert(signature.id(), signature) + } + + /// Gets a reference to the signature with the given key ID, if any. + /// + /// # Parameters + /// + /// * key_id: The identifier of the public key (e.g. "ed25519:1") for the desired signature. + pub fn get(&self, key_id: &str) -> Option<&Signature> { + self.map.get(key_id) + } + + /// Gets a mutable reference to the signature with the given ID, if any. + /// + /// # Parameters + /// + /// * key_id: The identifier of the public key (e.g. "ed25519:1") for the desired signature. + pub fn get_mut(&mut self, key_id: &str) -> Option<&mut Signature> { + self.map.get_mut(key_id) } /// The number of signatures in the set. pub fn len(&self) -> usize { - self.set.len() + self.map.len() } /// Whether or not the set of signatures is empty. @@ -308,7 +326,7 @@ impl Serialize for SignatureSet { { let mut map_serializer = serializer.serialize_map(Some(self.len()))?; - for signature in self.set.iter() { + for signature in self.map.values() { map_serializer.serialize_key(&signature.id())?; map_serializer.serialize_value(&signature.base64())?; } From 490ac1c4d399df1ef8d528bdc9e27d2a99b26430 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 9 Jul 2019 23:38:51 -0700 Subject: [PATCH 63/98] Add verify_event function. --- src/functions.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 36 +++++++++++-------- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index f1b7823a..019bfa20 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,8 +1,10 @@ //! Functions for signing and verifying JSON and events. +use std::{collections::HashMap, hash::BuildHasher}; + use base64::{encode_config, STANDARD_NO_PAD}; use ring::digest::{digest, SHA256}; -use serde_json::{from_value, map::Map, to_string, to_value, Value}; +use serde_json::{from_str, from_value, map::Map, to_string, to_value, Value}; use crate::{ keys::KeyPair, @@ -134,7 +136,7 @@ pub fn to_canonical_json(value: &Value) -> Result { to_canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) } -/// Use a public key to verify a signature of a JSON object. +/// Uses a public key to verify a signature of a JSON object. /// /// # Parameters /// @@ -267,6 +269,94 @@ where 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 of 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. +/// +/// # Parameters +/// +/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. +/// * verify_key_map: A map of server names to a map of key identifiers to public keys. Server +/// names are the hostname or IP 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. +pub fn verify_event( + verifier: &V, + verify_key_map: HashMap<&str, HashMap<&str, &[u8], S>, S>, + value: &Value, +) -> Result<(), Error> +where + V: Verifier, + S: BuildHasher, +{ + 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 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 (server_name, verify_keys) in verify_key_map { + let signature_set = match signature_map.get(server_name)? { + Some(set) => set, + None => { + return Err(Error::new(format!( + "no signatures found for server `{}`", + server_name + ))) + } + }; + + let mut maybe_signature = None; + let mut maybe_verify_key = None; + + for (key_id, verify_key) in verify_keys { + if let Some(signature) = signature_set.get(key_id) { + maybe_signature = Some(signature); + maybe_verify_key = Some(verify_key); + + break; + } + } + + let signature = match maybe_signature { + Some(signature) => signature, + None => { + return Err(Error::new( + "event is not signed with any of the given verify keys", + )) + } + }; + + let verify_key = match maybe_verify_key { + Some(verify_key) => verify_key, + None => { + return Err(Error::new( + "event is not signed with any of the given verify keys", + )) + } + }; + + let canonical_json = from_str(&to_canonical_json(&redacted)?)?; + + verify_json(verifier, verify_key, signature, &canonical_json)?; + } + + Ok(()) +} + /// Internal implementation detail of the canonical JSON algorithm. Allows customization of the /// fields that will be removed before serializing. fn to_canonical_json_with_fields_to_remove( diff --git a/src/lib.rs b/src/lib.rs index 28d506f3..5add97fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,7 @@ //! A homeserver signs JSON with a key pair: //! //! ```rust,no_run -//! # use ruma_signatures::{self, KeyPair}; -//! # use serde_json; +//! # use ruma_signatures::KeyPair; //! # let public_key = [0; 32]; //! # let private_key = [0; 32]; //! // Create an Ed25519 key pair. @@ -55,8 +54,7 @@ //! same: //! //! ```rust,no_run -//! # use ruma_signatures::{self, KeyPair}; -//! # use serde_json; +//! # use ruma_signatures::KeyPair; //! # let public_key = [0; 32]; //! # let private_key = [0; 32]; //! // Create an Ed25519 key pair. @@ -120,8 +118,6 @@ //! A client application or another homeserver can verify a signature on arbitrary JSON: //! //! ```rust,no_run -//! # use ruma_signatures; -//! # use serde_json; //! # let public_key = [0; 32]; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); @@ -130,7 +126,24 @@ //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! ``` //! -//! Verifying signatures of Matrix events is not yet implemented by ruma-signatures. +//! Verifying the signatures on a Matrix event are slightly different: +//! +//! ```rust,no_run +//! # use std::collections::HashMap; +//! # let public_key_vec = Vec::new(); +//! # let public_key = public_key_vec.as_slice(); +//! # let event_json = ""; +//! let mut verify_key_map = HashMap::new(); +//! let mut example_server_keys = HashMap::new(); +//! example_server_keys.insert("ed25519:1", public_key); // public_key: &[u8] +//! verify_key_map.insert("example.com", example_server_keys); +//! let value = serde_json::from_str(event_json).unwrap(); +//! let verifier = ruma_signatures::Ed25519Verifier; +//! assert!(ruma_signatures::verify_event(&verifier, verify_key_map, &value).is_ok()); +//! ``` +//! +//! See the documentation for `verify_event` for details on what verification entails and the +//! `verify_key_map` parameter. //! //! # Signature sets and maps //! @@ -156,9 +169,6 @@ //! This inner object can be created by serializing a `SignatureSet`: //! //! ```rust,no_run -//! # use ruma_signatures; -//! # use serde; -//! # use serde_json; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); @@ -173,9 +183,6 @@ //! created like this: //! //! ```rust,no_run -//! # use ruma_signatures; -//! # use serde; -//! # use serde_json; //! # let signature_bytes = [0, 32]; //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); //! let mut signature_set = ruma_signatures::SignatureSet::new(); @@ -222,7 +229,8 @@ use std::{ pub use url::Host; pub use functions::{ - content_hash, hash_and_sign_event, reference_hash, sign_json, to_canonical_json, verify_json, + content_hash, hash_and_sign_event, reference_hash, sign_json, to_canonical_json, verify_event, + verify_json, }; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; From c7aca45094ba96f5a67a527441322c00e6af6d25 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 10 Jul 2019 00:17:24 -0700 Subject: [PATCH 64/98] Make all the doc examples runnable. --- src/lib.rs | 148 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5add97fe..6f2e4362 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,18 +18,27 @@ //! //! A homeserver signs JSON with a key pair: //! -//! ```rust,no_run -//! # use ruma_signatures::KeyPair; -//! # let public_key = [0; 32]; -//! # let private_key = [0; 32]; +//! ```rust +//! use ruma_signatures::KeyPair as _; +//! +//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +//! const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +//! +//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! //! // Create an Ed25519 key pair. //! let key_pair = ruma_signatures::Ed25519KeyPair::new( -//! &public_key, // &[u8] -//! &private_key, // &[u8] +//! &public_key, +//! &private_key, //! "1".to_string(), // The "version" of the key. //! ).unwrap(); +//! +//! // Deserialize some JSON. //! let mut value = serde_json::from_str("{}").unwrap(); -//! ruma_signatures::sign_json("example.com", &key_pair, &mut value).unwrap() +//! +//! // Sign the JSON with the key pair. +//! assert!(ruma_signatures::sign_json("example.com", &key_pair, &mut value).is_ok()); //! ``` //! //! This will modify the JSON from an empty object to a structure like this: @@ -53,16 +62,23 @@ //! complicated than for signing arbitrary JSON, the interface to a user of ruma-signatures is the //! same: //! -//! ```rust,no_run -//! # use ruma_signatures::KeyPair; -//! # let public_key = [0; 32]; -//! # let private_key = [0; 32]; +//! ```rust +//! use ruma_signatures::KeyPair as _; +//! +//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +//! const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +//! +//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! //! // Create an Ed25519 key pair. //! let key_pair = ruma_signatures::Ed25519KeyPair::new( -//! &public_key, // &[u8] -//! &private_key, // &[u8] +//! &public_key, +//! &private_key, //! "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", @@ -81,7 +97,9 @@ //! } //! }"# //! ).unwrap(); -//! ruma_signatures::hash_and_sign_event("example.com", &key_pair, &mut value).unwrap() +//! +//! // Hash and sign the JSON with the key pair. +//! assert!(ruma_signatures::hash_and_sign_event("example.com", &key_pair, &mut value).is_ok()); //! ``` //! //! This will modify the JSON from the structure shown to a structure like this: @@ -117,28 +135,76 @@ //! //! A client application or another homeserver can verify a signature on arbitrary JSON: //! -//! ```rust,no_run -//! # let public_key = [0; 32]; -//! # let signature_bytes = [0, 32]; +//! ```rust +//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +//! const SIGNATURE_BYTES: &str = +//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +//! +//! // Decode the public key used to generate the signature into raw bytes. +//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! +//! // Create a `Signature` from the raw bytes of the signature. +//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! +//! // Deserialize the signed JSON. //! let value = serde_json::from_str("{}").unwrap(); +//! +//! // Create the verifier for the Ed25519 algorithm. //! let verifier = ruma_signatures::Ed25519Verifier; +//! +//! // Verify the signature. //! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); //! ``` //! //! Verifying the signatures on a Matrix event are slightly different: //! -//! ```rust,no_run -//! # use std::collections::HashMap; -//! # let public_key_vec = Vec::new(); -//! # let public_key = public_key_vec.as_slice(); -//! # let event_json = ""; -//! let mut verify_key_map = HashMap::new(); +//! ```rust +//! use std::collections::HashMap; +//! +//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +//! +//! // Decode the public key used to generate the signature into raw bytes. +//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +//! +//! // Create a map of key ID to public key. //! let mut example_server_keys = HashMap::new(); -//! example_server_keys.insert("ed25519:1", public_key); // public_key: &[u8] -//! verify_key_map.insert("example.com", example_server_keys); -//! let value = serde_json::from_str(event_json).unwrap(); +//! example_server_keys.insert("ed25519:1", public_key.as_slice()); +//! +//! // Insert the public keys into a map keyed by server name. +//! let mut verify_key_map = HashMap::new(); +//! verify_key_map.insert("domain", example_server_keys); +//! +//! // 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 verifier for the Ed25519 algorithm. //! let verifier = ruma_signatures::Ed25519Verifier; +//! +//! // Verify at least one signature for each server in `verify_key_map`. //! assert!(ruma_signatures::verify_event(&verifier, verify_key_map, &value).is_ok()); //! ``` //! @@ -168,12 +234,20 @@ //! //! This inner object can be created by serializing a `SignatureSet`: //! -//! ```rust,no_run -//! # let signature_bytes = [0, 32]; +//! ```rust +//! const SIGNATURE_BYTES: &str = +//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +//! +//! // Create a `Signature` from the raw bytes of the signature. +//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! +//! // Create a `SignatureSet` and insert the signature into it. //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); -//! serde_json::to_string(&signature_set).unwrap(); +//! +//! // Serialize the set to JSON. +//! assert!(serde_json::to_string(&signature_set).is_ok()); //! ``` //! //! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, @@ -182,14 +256,24 @@ //! The outer object (the map of server names to signature sets) is a `SignatureMap` value and //! created like this: //! -//! ```rust,no_run -//! # let signature_bytes = [0, 32]; +//! ```rust +//! const SIGNATURE_BYTES: &str = +//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +//! +//! // Create a `Signature` from the raw bytes of the signature. +//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); //! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! +//! // Create a `SignatureSet` and insert the signature into it. //! let mut signature_set = ruma_signatures::SignatureSet::new(); //! signature_set.insert(signature); +//! +//! // Create a `SignatureMap` and insert the set into it, keyed by the homeserver name. //! let mut signature_map = ruma_signatures::SignatureMap::new(); //! signature_map.insert("example.com", signature_set).unwrap(); -//! serde_json::to_string(&signature_map).unwrap(); +//! +//! // Serialize the map to JSON. +//! assert!(serde_json::to_string(&signature_map).is_ok()); //! ``` //! //! Just like the `SignatureSet` itself, the `SignatureMap` value can also be deserialized from From bca31bfbd4e8e36986742ec13516d1b131f68c3e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 10 Jul 2019 00:24:03 -0700 Subject: [PATCH 65/98] Add test for verifying an event. --- src/lib.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6f2e4362..f8aa115a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -385,13 +385,15 @@ impl Display for Algorithm { #[cfg(test)] mod test { + use std::collections::HashMap; + use base64::{decode_config, STANDARD_NO_PAD}; use serde::Serialize; use serde_json::{from_str, to_string, to_value, Value}; use super::{ - hash_and_sign_event, sign_json, to_canonical_json, verify_json, Ed25519KeyPair, - Ed25519Verifier, KeyPair, Signature, + hash_and_sign_event, sign_json, to_canonical_json, verify_event, verify_json, + Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, }; const EMPTY_JSON_SIGNATURE: &str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; @@ -741,4 +743,44 @@ mod test { 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":"Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUwu6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# ); } + + #[test] + fn verify_minimal_event() { + let public_key = decode_config(&PUBLIC_KEY, STANDARD_NO_PAD).unwrap(); + + let mut example_server_keys = HashMap::new(); + example_server_keys.insert("ed25519:1", public_key.as_slice()); + + let mut verify_key_map = HashMap::new(); + verify_key_map.insert("domain", example_server_keys); + + 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": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg" + } + }, + "type": "X", + "unsigned": { + "age_ts": 1000000 + } + }"# + ).unwrap(); + + let verifier = Ed25519Verifier; + + assert!(verify_event(&verifier, verify_key_map, &value).is_ok()); + } } From f1d9eba728c19aa69739ead20ab0c5c4e47f8c1f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 10 Jul 2019 01:04:38 -0700 Subject: [PATCH 66/98] Move code examples onto relevant functions/types and rewrite the crate documentation to be a more general overview. --- src/functions.rs | 189 +++++++++++++++++++++++++++++- src/lib.rs | 284 ++++++---------------------------------------- src/signatures.rs | 49 +++++++- 3 files changed, 271 insertions(+), 251 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 019bfa20..d46e3612 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -71,6 +71,45 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// * `value` is not a JSON object. /// * `value` contains a field called `signatures` that is not a JSON object. /// * `server_name` cannot be parsed as a valid host. +/// +/// # Examples +/// +/// A homeserver signs JSON with a key pair: +/// +/// ```rust +/// use ruma_signatures::KeyPair as _; +/// +/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// +/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// +/// // Create an Ed25519 key pair. +/// let key_pair = ruma_signatures::Ed25519KeyPair::new( +/// &public_key, +/// &private_key, +/// "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("example.com", &key_pair, &mut value).is_ok()); +/// ``` +/// +/// This will modify the JSON from an empty object to a structure like this: +/// +/// ```json +/// { +/// "signatures": { +/// "example.com": { +/// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" +/// } +/// } +/// } +/// ``` pub fn sign_json(server_name: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> where K: KeyPair, @@ -148,6 +187,30 @@ pub fn to_canonical_json(value: &Value) -> Result { /// # Errors /// /// Returns an error if verification fails. +/// +/// # Examples +/// +/// ```rust +/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +/// const SIGNATURE_BYTES: &str = +/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +/// +/// // Decode the public key used to generate the signature into raw bytes. +/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// +/// // Create a `Signature` from the raw bytes of the signature. +/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); +/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +/// +/// // Deserialize the signed JSON. +/// let value = serde_json::from_str("{}").unwrap(); +/// +/// // Create the verifier for the Ed25519 algorithm. +/// let verifier = ruma_signatures::Ed25519Verifier; +/// +/// // Verify the signature. +/// assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); +/// ``` pub fn verify_json( verifier: &V, public_key: &[u8], @@ -230,6 +293,77 @@ pub fn reference_hash(value: &Value) -> Result { /// * `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. /// * `server_name` cannot be parsed as a valid host. +/// +/// # Examples +/// +/// ```rust +/// use ruma_signatures::KeyPair as _; +/// +/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; +/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// +/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// +/// // Create an Ed25519 key pair. +/// let key_pair = ruma_signatures::Ed25519KeyPair::new( +/// &public_key, +/// &private_key, +/// "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("example.com", &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( server_name: &str, key_pair: &K, @@ -272,18 +406,69 @@ where /// 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 of servers to sets of public keys. For each homeserver present in the map, this function +/// 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. /// /// # Parameters /// /// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * verify_key_map: A map of server names to a map of key identifiers to public keys. Server +/// * verify_key_map: A map from server names to a map from key identifiers to public keys. Server /// names are the hostname or IP 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"; +/// +/// // Decode the public key used to generate the signature into raw bytes. +/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// +/// // Create a map from key ID to public key. +/// let mut example_server_keys = HashMap::new(); +/// example_server_keys.insert("ed25519:1", public_key.as_slice()); +/// +/// // Insert the public keys into a map keyed by server name. +/// let mut verify_key_map = HashMap::new(); +/// verify_key_map.insert("domain", example_server_keys); +/// +/// // 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 verifier for the Ed25519 algorithm. +/// let verifier = ruma_signatures::Ed25519Verifier; +/// +/// // Verify at least one signature for each server in `verify_key_map`. +/// assert!(ruma_signatures::verify_event(&verifier, verify_key_map, &value).is_ok()); +/// ``` pub fn verify_event( verifier: &V, verify_key_map: HashMap<&str, HashMap<&str, &[u8], S>, S>, diff --git a/src/lib.rs b/src/lib.rs index f8aa115a..6343bd15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,218 +3,30 @@ //! //! 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 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. +//! 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. //! -//! In JSON representations, signatures and hashes appear as Base64-encoded strings, using the +//! 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 JSON +//! # Signatures, maps, and sets //! -//! A homeserver signs JSON with a key pair: +//! An individual signature is represented in ruma-signatures by the `Signature` type. This type +//! encapsulates the raw bytes of the signature, the identifier for the signing key used, and the +//! algorithm used to create the signature. //! -//! ```rust -//! use ruma_signatures::KeyPair as _; -//! -//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -//! const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; -//! -//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! -//! // Create an Ed25519 key pair. -//! let key_pair = ruma_signatures::Ed25519KeyPair::new( -//! &public_key, -//! &private_key, -//! "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("example.com", &key_pair, &mut value).is_ok()); -//! ``` -//! -//! This will modify the JSON from an empty object to a structure like this: -//! -//! ```json -//! { -//! "signatures": { -//! "example.com": { -//! "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" -//! } -//! } -//! } -//! ``` -//! -//! # Hashing and signing Matrix events -//! -//! Signing an event uses a more involved 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 and sign 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: -//! -//! ```rust -//! use ruma_signatures::KeyPair as _; -//! -//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -//! const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; -//! -//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! -//! // Create an Ed25519 key pair. -//! let key_pair = ruma_signatures::Ed25519KeyPair::new( -//! &public_key, -//! &private_key, -//! "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("example.com", &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`. -//! -//! # Verifying signatures -//! -//! A client application or another homeserver can verify a signature on arbitrary JSON: -//! -//! ```rust -//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -//! const SIGNATURE_BYTES: &str = -//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; -//! -//! // Decode the public key used to generate the signature into raw bytes. -//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! -//! // Create a `Signature` from the raw bytes of the signature. -//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); -//! -//! // Deserialize the signed JSON. -//! let value = serde_json::from_str("{}").unwrap(); -//! -//! // Create the verifier for the Ed25519 algorithm. -//! let verifier = ruma_signatures::Ed25519Verifier; -//! -//! // Verify the signature. -//! assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); -//! ``` -//! -//! Verifying the signatures on a Matrix event are slightly different: -//! -//! ```rust -//! use std::collections::HashMap; -//! -//! const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -//! -//! // Decode the public key used to generate the signature into raw bytes. -//! let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -//! -//! // Create a map of key ID to public key. -//! let mut example_server_keys = HashMap::new(); -//! example_server_keys.insert("ed25519:1", public_key.as_slice()); -//! -//! // Insert the public keys into a map keyed by server name. -//! let mut verify_key_map = HashMap::new(); -//! verify_key_map.insert("domain", example_server_keys); -//! -//! // 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 verifier for the Ed25519 algorithm. -//! let verifier = ruma_signatures::Ed25519Verifier; -//! -//! // Verify at least one signature for each server in `verify_key_map`. -//! assert!(ruma_signatures::verify_event(&verifier, verify_key_map, &value).is_ok()); -//! ``` -//! -//! See the documentation for `verify_event` for details on what verification entails and the -//! `verify_key_map` parameter. -//! -//! # Signature sets and maps -//! -//! Signatures that a homeserver has added to an event are stored in a JSON object under the -//! "signatures" key in the event's JSON representation: +//! As mentioned, signatures that a homeserver has added to an event are stored in a JSON object +//! under the `signatures` key in the event's JSON representation: //! //! ```json //! { @@ -228,56 +40,34 @@ //! } //! ``` //! -//! The keys inside the "signatures" object are the hostnames of homeservers that have added -//! signatures. Within each of those objects are a set of signatures, keyed by the signing key -//! pair's identifier. +//! The value of the the `signatures` key is represented in ruma-signatures by the `SignatureMap` +//! type. This type maps the name of a homeserver to a set of its signatures for the containing +//! data. The set of signatures for each homeserver (which appears as a map from key ID to signature +//! in the JSON representation) is represented in ruma-signatures by the `SignatureSet` type. Both +//! `SignatureMap`s and `SignatureSet`s can be serialized and deserialized with +//! [https://serde.rs/](Serde). //! -//! This inner object can be created by serializing a `SignatureSet`: +//! # Signing and hashing //! -//! ```rust -//! const SIGNATURE_BYTES: &str = -//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +//! 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. //! -//! // Create a `Signature` from the raw bytes of the signature. -//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +//! 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. //! -//! // Create a `SignatureSet` and insert the signature into it. -//! let mut signature_set = ruma_signatures::SignatureSet::new(); -//! signature_set.insert(signature); +//! # Verifying signatures and hashes //! -//! // Serialize the set to JSON. -//! assert!(serde_json::to_string(&signature_set).is_ok()); -//! ``` +//! 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. //! -//! This code produces the object under the "example.com" key in the preceeding JSON. Similarly, -//! a `SignatureSet` can be produced by deserializing JSON that follows this form. -//! -//! The outer object (the map of server names to signature sets) is a `SignatureMap` value and -//! created like this: -//! -//! ```rust -//! const SIGNATURE_BYTES: &str = -//! "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; -//! -//! // Create a `Signature` from the raw bytes of the signature. -//! let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -//! let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); -//! -//! // Create a `SignatureSet` and insert the signature into it. -//! let mut signature_set = ruma_signatures::SignatureSet::new(); -//! signature_set.insert(signature); -//! -//! // Create a `SignatureMap` and insert the set into it, keyed by the homeserver name. -//! let mut signature_map = ruma_signatures::SignatureMap::new(); -//! signature_map.insert("example.com", signature_set).unwrap(); -//! -//! // Serialize the map to JSON. -//! assert!(serde_json::to_string(&signature_map).is_ok()); -//! ``` -//! -//! Just like the `SignatureSet` itself, the `SignatureMap` value can also be deserialized from -//! JSON. +//! 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. #![deny( missing_copy_implementations, diff --git a/src/signatures.rs b/src/signatures.rs index 9a1718f4..64fe2925 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -88,10 +88,35 @@ impl Signature { } } -/// A map of server names to sets of digital signatures created by that server. +/// A map from server names to sets of digital signatures created by that server. +/// +/// # Examples +/// +/// Creating and serializing a `SignatureMap`: +/// +/// ```rust +/// const SIGNATURE_BYTES: &str = +/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +/// +/// // Create a `Signature` from the raw bytes of the signature. +/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); +/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +/// +/// // Create a `SignatureSet` and insert the signature into it. +/// let mut signature_set = ruma_signatures::SignatureSet::new(); +/// signature_set.insert(signature); +/// +/// // Create a `SignatureMap` and insert the set into it, keyed by the homeserver name. +/// let mut signature_map = ruma_signatures::SignatureMap::new(); +/// signature_map.insert("example.com", signature_set).unwrap(); +/// +/// // Serialize the map to JSON. +/// assert!(serde_json::to_string(&signature_map).is_ok()); +/// ``` +/// #[derive(Clone, Debug, Default, PartialEq)] pub struct SignatureMap { - /// A map of homeservers to sets of signatures for the homeserver. + /// A map from homeservers to sets of signatures for the homeserver. map: HashMap, } @@ -253,6 +278,26 @@ impl<'de> Visitor<'de> for SignatureMapVisitor { } /// A set of digital signatures created by a single homeserver. +/// +/// # Examples +/// +/// Creating and serializing a `SignatureSet`: +/// +/// ```rust +/// const SIGNATURE_BYTES: &str = +/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; +/// +/// // Create a `Signature` from the raw bytes of the signature. +/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); +/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); +/// +/// // Create a `SignatureSet` and insert the signature into it. +/// let mut signature_set = ruma_signatures::SignatureSet::new(); +/// signature_set.insert(signature); +/// +/// // Serialize the set to JSON. +/// assert!(serde_json::to_string(&signature_set).is_ok()); +/// ``` #[derive(Clone, Debug, Default, PartialEq)] pub struct SignatureSet { /// A set of signatures for a homeserver. From efbea86fe5df8d1ba1204556130c33aa180c822d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 10 Jul 2019 03:53:38 -0700 Subject: [PATCH 67/98] Move the constructor for key pairs out of the trait. --- src/functions.rs | 4 ---- src/keys.rs | 38 +++++++++++++++++++------------------- src/lib.rs | 2 +- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index d46e3612..699b2711 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -77,8 +77,6 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// A homeserver signs JSON with a key pair: /// /// ```rust -/// use ruma_signatures::KeyPair as _; -/// /// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; /// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; /// @@ -297,8 +295,6 @@ pub fn reference_hash(value: &Value) -> Result { /// # Examples /// /// ```rust -/// use ruma_signatures::KeyPair as _; -/// /// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; /// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; /// diff --git a/src/keys.rs b/src/keys.rs index a8e1c833..0fcd374d 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -9,22 +9,6 @@ use crate::{signatures::Signature, Algorithm, Error}; /// A cryptographic key pair for digitally signing data. pub trait KeyPair: Sized { - /// Initializes a new key pair. - /// - /// # Parameters - /// - /// * public_key: The public key of the key pair. - /// * private_key: The private key of the key pair. - /// * 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. - fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result; - /// Signs a JSON object. /// /// # Parameters @@ -46,8 +30,22 @@ pub struct Ed25519KeyPair { version: String, } -impl KeyPair for Ed25519KeyPair { - fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { +impl Ed25519KeyPair { + /// Initializes a new key pair. + /// + /// # Parameters + /// + /// * public_key: The public key of the key pair. + /// * private_key: The private key of the key pair. + /// * 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(public_key: &[u8], private_key: &[u8], version: String) -> Result { if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key( Input::from(private_key), Input::from(public_key), @@ -61,9 +59,11 @@ impl KeyPair for Ed25519KeyPair { version, }) } +} +impl KeyPair for Ed25519KeyPair { fn sign(&self, message: &[u8]) -> Signature { - // Okay to unwrap because we verified the input in the `new`. + // Okay to unwrap because we verified the input in `new`. let ring_key_pair = RingEd25519KeyPair::from_seed_and_public_key( Input::from(&self.private_key), Input::from(&self.public_key), diff --git a/src/lib.rs b/src/lib.rs index 6343bd15..3bb26d6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -183,7 +183,7 @@ mod test { use super::{ hash_and_sign_event, sign_json, to_canonical_json, verify_event, verify_json, - Ed25519KeyPair, Ed25519Verifier, KeyPair, Signature, + Ed25519KeyPair, Ed25519Verifier, Signature, }; const EMPTY_JSON_SIGNATURE: &str = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; From e64df47d23545d76c78141d3bfc79b7143db240f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 10 Jul 2019 19:55:30 -0700 Subject: [PATCH 68/98] Make SignatureMap and SignatureSet type aliases for HashMap. --- src/functions.rs | 24 ++- src/signatures.rs | 394 +++------------------------------------------- 2 files changed, 37 insertions(+), 381 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 699b2711..b2570504 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -2,13 +2,13 @@ use std::{collections::HashMap, hash::BuildHasher}; -use base64::{encode_config, STANDARD_NO_PAD}; +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, - signatures::{Signature, SignatureMap, SignatureSet}, + signatures::{Signature, SignatureMap}, verification::Verifier, Error, }; @@ -128,7 +128,7 @@ where Some(signatures) => from_value(Value::Object(signatures.clone()))?, None => return Err(Error::new("Field `signatures` must be a JSON object")), }, - None => SignatureMap::with_capacity(1), + None => HashMap::with_capacity(1), }; maybe_unsigned = map.remove("unsigned"); @@ -142,10 +142,10 @@ where // Insert the new signature in the map we pulled out (or created) previously. let signature_set = signature_map - .entry(server_name)? - .or_insert_with(|| SignatureSet::with_capacity(1)); + .entry(server_name.to_string()) + .or_insert_with(|| HashMap::with_capacity(1)); - signature_set.insert(signature); + 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(); @@ -490,7 +490,7 @@ where }; for (server_name, verify_keys) in verify_key_map { - let signature_set = match signature_map.get(server_name)? { + let signature_set = match signature_map.get(server_name) { Some(set) => set, None => { return Err(Error::new(format!( @@ -532,7 +532,15 @@ where let canonical_json = from_str(&to_canonical_json(&redacted)?)?; - verify_json(verifier, verify_key, signature, &canonical_json)?; + verify_json( + verifier, + verify_key, + &Signature::new( + "ed25519:fixme", + &decode_config(signature, STANDARD_NO_PAD).expect("FIXME"), + )?, + &canonical_json, + )?; } Ok(()) diff --git a/src/signatures.rs b/src/signatures.rs index 64fe2925..ac4d9c3c 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -1,37 +1,36 @@ //! Digital signatures and collections of signatures. -use std::{ - collections::{hash_map::Entry, HashMap}, - error::Error as _, - fmt::{Formatter, Result as FmtResult}, -}; +use std::collections::HashMap; -use base64::{decode_config, encode_config, STANDARD_NO_PAD}; -use serde::{ - de::{Error as SerdeError, MapAccess, Unexpected, Visitor}, - ser::SerializeMap, - Deserialize, Deserializer, Serialize, Serializer, -}; -use url::{Host, Url}; +use base64::{encode_config, STANDARD_NO_PAD}; use crate::{Algorithm, Error}; /// A digital signature. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Signature { - /// The cryptographic algorithm to use. - pub(crate) algorithm: Algorithm, + /// The cryptographic algorithm that generated this signature. + pub algorithm: Algorithm, /// The signature data. - pub(crate) signature: Vec, + pub signature: Vec, - /// The version of the signature. - pub(crate) version: String, + /// 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". @@ -88,354 +87,15 @@ impl Signature { } } -/// A map from server names to sets of digital signatures created by that server. +/// A map from entity names to sets of digital signatures created by that entity. /// -/// # Examples -/// -/// Creating and serializing a `SignatureMap`: -/// -/// ```rust -/// const SIGNATURE_BYTES: &str = -/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; -/// -/// // Create a `Signature` from the raw bytes of the signature. -/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); -/// -/// // Create a `SignatureSet` and insert the signature into it. -/// let mut signature_set = ruma_signatures::SignatureSet::new(); -/// signature_set.insert(signature); -/// -/// // Create a `SignatureMap` and insert the set into it, keyed by the homeserver name. -/// let mut signature_map = ruma_signatures::SignatureMap::new(); -/// signature_map.insert("example.com", signature_set).unwrap(); -/// -/// // Serialize the map to JSON. -/// assert!(serde_json::to_string(&signature_map).is_ok()); -/// ``` -/// -#[derive(Clone, Debug, Default, PartialEq)] -pub struct SignatureMap { - /// A map from homeservers to sets of signatures for the homeserver. - map: HashMap, -} - -impl SignatureMap { - /// Initializes a new empty `SignatureMap`. - pub fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - /// Initializes a new empty `SignatureMap` with room for a specific number of servers. - /// - /// # Parameters - /// - /// * capacity: The number of items to allocate memory for. - pub fn with_capacity(capacity: usize) -> Self { - Self { - map: HashMap::with_capacity(capacity), - } - } - - /// Adds a signature set for a server. - /// - /// If no signature set for the given server existed in the collection, `None` is returned. - /// Otherwise, the signature set is returned. - /// - /// # Parameters - /// - /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. - /// * signature_set: The `SignatureSet` containing the digital signatures made by the server. - /// - /// # Errors - /// - /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn insert( - &mut self, - server_name: &str, - signature_set: SignatureSet, - ) -> Result, Error> { - let host = server_name_to_host(server_name)?; - - Ok(self.map.insert(host, signature_set)) - } - - /// Gets the given server's corresponding signature set for in-place manipulation. - /// - /// # Parameters - /// - /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. - /// - /// # Errors - /// - /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn entry(&mut self, server_name: &str) -> Result, Error> { - let host = server_name_to_host(server_name)?; - - Ok(self.map.entry(host)) - } - - /// Gets a reference to the signature set for the given server, if any. - /// - /// # Parameters - /// - /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. - /// - /// # Errors - /// - /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn get(&self, server_name: &str) -> Result, Error> { - let host = server_name_to_host(server_name)?; - - Ok(self.map.get(&host)) - } - - /// Gets a mutable reference to the signature set for the given server, if any. - /// - /// # Parameters - /// - /// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. - /// - /// # Errors - /// - /// Returns an error if the given server name cannot be parsed as a valid host. - pub fn get_mut(&mut self, server_name: &str) -> Result, Error> { - let host = server_name_to_host(server_name)?; - - Ok(self.map.get_mut(&host)) - } - - /// The number of servers in the collection. - pub fn len(&self) -> usize { - self.map.len() - } - - /// Whether or not the collection of signatures is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl Serialize for SignatureMap { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map_serializer = serializer.serialize_map(Some(self.len()))?; - - for (host, signature_set) in self.map.iter() { - map_serializer.serialize_key(&host.to_string())?; - map_serializer.serialize_value(signature_set)?; - } - - map_serializer.end() - } -} - -impl<'de> Deserialize<'de> for SignatureMap { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(SignatureMapVisitor) - } -} - -/// Serde Visitor for deserializing `SignatureMap`. -struct SignatureMapVisitor; - -impl<'de> Visitor<'de> for SignatureMapVisitor { - type Value = SignatureMap; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "digital signatures") - } - - fn visit_map(self, mut visitor: M) -> Result - where - M: MapAccess<'de>, - { - let mut signatures = match visitor.size_hint() { - Some(capacity) => SignatureMap::with_capacity(capacity), - None => SignatureMap::new(), - }; - - while let Some((server_name, signature_set)) = - visitor.next_entry::()? - { - if signatures.insert(&server_name, signature_set).is_err() { - return Err(M::Error::invalid_value( - Unexpected::Str(&server_name), - &self, - )); - } - } - - Ok(signatures) - } -} +/// "Entity" is currently always a homeserver, e.g. "example.com". +pub type SignatureMap = HashMap; /// A set of digital signatures created by a single homeserver. /// -/// # Examples -/// -/// Creating and serializing a `SignatureSet`: -/// -/// ```rust -/// const SIGNATURE_BYTES: &str = -/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; -/// -/// // Create a `Signature` from the raw bytes of the signature. -/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); -/// -/// // Create a `SignatureSet` and insert the signature into it. -/// let mut signature_set = ruma_signatures::SignatureSet::new(); -/// signature_set.insert(signature); -/// -/// // Serialize the set to JSON. -/// assert!(serde_json::to_string(&signature_set).is_ok()); -/// ``` -#[derive(Clone, Debug, Default, PartialEq)] -pub struct SignatureSet { - /// A set of signatures for a homeserver. - map: HashMap, -} - -impl SignatureSet { - /// Initializes a new empty SignatureSet. - pub fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - /// Initializes a new empty SignatureSet with room for a specific number of signatures. - /// - /// # Parameters - /// - /// * capacity: The number of items to allocate memory for. - pub fn with_capacity(capacity: usize) -> Self { - Self { - map: HashMap::with_capacity(capacity), - } - } - - /// Adds a signature to the set. - /// - /// If no signature with the given key ID existed in the collection, `None` is returned. - /// Otherwise, the signature is returned. - /// - /// # Parameters - /// - /// * signature: A `Signature` to insert into the set. - pub fn insert(&mut self, signature: Signature) -> Option { - self.map.insert(signature.id(), signature) - } - - /// Gets a reference to the signature with the given key ID, if any. - /// - /// # Parameters - /// - /// * key_id: The identifier of the public key (e.g. "ed25519:1") for the desired signature. - pub fn get(&self, key_id: &str) -> Option<&Signature> { - self.map.get(key_id) - } - - /// Gets a mutable reference to the signature with the given ID, if any. - /// - /// # Parameters - /// - /// * key_id: The identifier of the public key (e.g. "ed25519:1") for the desired signature. - pub fn get_mut(&mut self, key_id: &str) -> Option<&mut Signature> { - self.map.get_mut(key_id) - } - - /// The number of signatures in the set. - pub fn len(&self) -> usize { - self.map.len() - } - - /// Whether or not the set of signatures is empty. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl Serialize for SignatureSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map_serializer = serializer.serialize_map(Some(self.len()))?; - - for signature in self.map.values() { - map_serializer.serialize_key(&signature.id())?; - map_serializer.serialize_value(&signature.base64())?; - } - - map_serializer.end() - } -} - -impl<'de> Deserialize<'de> for SignatureSet { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_map(SignatureSetVisitor) - } -} - -/// Serde Visitor for deserializing `SignatureSet`. -struct SignatureSetVisitor; - -impl<'de> Visitor<'de> for SignatureSetVisitor { - type Value = SignatureSet; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a set of digital signatures") - } - - fn visit_map(self, mut visitor: M) -> Result - where - M: MapAccess<'de>, - { - let mut signature_set = match visitor.size_hint() { - Some(capacity) => SignatureSet::with_capacity(capacity), - None => SignatureSet::new(), - }; - - while let Some((key, value)) = visitor.next_entry::()? { - let (algorithm, version) = split_id(&key).map_err(|split_error| match split_error { - SplitError::InvalidLength(length) => M::Error::invalid_length(length, &self), - SplitError::InvalidVersion(version) => { - M::Error::invalid_value(Unexpected::Str(version), &self) - } - SplitError::UnknownAlgorithm(algorithm) => { - M::Error::invalid_value(Unexpected::Str(algorithm), &self) - } - })?; - - let signature_bytes: Vec = match decode_config(&value, STANDARD_NO_PAD) { - Ok(raw) => raw, - Err(error) => return Err(M::Error::custom(error.description())), - }; - - let signature = Signature { - algorithm, - signature: signature_bytes, - version, - }; - - signature_set.insert(signature); - } - - Ok(signature_set) - } -} +/// This is represented as a map from signing key ID to Base64-encoded signature. +pub type SignatureSet = HashMap; /// An error when trying to extract the algorithm and version from a key identifier. #[derive(Clone, Debug, PartialEq)] @@ -484,18 +144,6 @@ fn split_id(id: &str) -> Result<(Algorithm, String), SplitError<'_>> { Ok((algorithm, signature_id[1].to_string())) } -/// Attempts to convert a server name as a string into a `url::Host`. -fn server_name_to_host(server_name: &str) -> Result { - let url_string = format!("https://{}", server_name); - let url = Url::parse(&url_string) - .map_err(|_| Error::new(format!("invalid server name: {}", server_name)))?; - - match url.host() { - Some(host) => Ok(host.to_owned()), - None => Err(Error::new(format!("invalid server name: {}", server_name))), - } -} - #[cfg(test)] mod tests { use super::Signature; From fe90cdabf9c0a2e2a4635e33cc6d30fdd1c54ccd Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 11 Jul 2019 01:08:34 -0700 Subject: [PATCH 69/98] Change `verify_json` to use the same interface as `verify_event`. --- Cargo.toml | 1 - src/functions.rs | 159 +++++++++++++++++++++++++++++++++++--------- src/lib.rs | 110 ++++++++++-------------------- src/verification.rs | 20 +++--- 4 files changed, 170 insertions(+), 120 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 137b8052..3c20ed84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ base64 = "0.10.1" ring = "0.14.6" serde_json = "1.0.39" untrusted = "0.6.2" -url = "1.7.2" [dependencies.serde] version = "1.0.90" diff --git a/src/functions.rs b/src/functions.rs index b2570504..3306e32a 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,17 +1,12 @@ //! Functions for signing and verifying JSON and events. -use std::{collections::HashMap, hash::BuildHasher}; +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, - signatures::{Signature, SignatureMap}, - verification::Verifier, - Error, -}; +use crate::{keys::KeyPair, signatures::SignatureMap, verification::Verifier, Error}; /// The fields that are allowed to remain in an event during redaction. static ALLOWED_KEYS: &[&str] = &[ @@ -189,6 +184,8 @@ pub fn to_canonical_json(value: &Value) -> Result { /// # Examples /// /// ```rust +/// use std::collections::HashMap; +/// /// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; /// const SIGNATURE_BYTES: &str = /// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; @@ -201,18 +198,119 @@ pub fn to_canonical_json(value: &Value) -> Result { /// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); /// /// // Deserialize the signed JSON. -/// let value = serde_json::from_str("{}").unwrap(); +/// let value = serde_json::from_str( +/// r#"{ +/// "signatures": { +/// "example.com": { +/// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" +/// } +/// } +/// }"# +/// ).unwrap(); /// /// // Create the verifier for the Ed25519 algorithm. /// let verifier = ruma_signatures::Ed25519Verifier; /// -/// // Verify the signature. -/// assert!(ruma_signatures::verify_json(&verifier, &public_key, &signature, &value).is_ok()); +/// // Create the `SignatureMap` that will inform `verify_json` which signatures to verify. +/// let mut signature_set = HashMap::new(); +/// signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); +/// let mut verify_key_map = HashMap::new(); +/// verify_key_map.insert("example.com".to_string(), signature_set); +/// +/// // Verify at least one signature for each server in `verify_key_map`. +/// assert!(ruma_signatures::verify_json(&verifier, &verify_key_map, &value).is_ok()); /// ``` pub fn verify_json( + verifier: &V, + verify_key_map: &SignatureMap, + value: &Value, +) -> Result<(), Error> +where + V: Verifier, +{ + 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 (server_name, verify_keys) in verify_key_map { + let signature_set = match signature_map.get(server_name) { + Some(set) => set, + None => { + return Err(Error::new(format!( + "no signatures found for server `{}`", + server_name + ))) + } + }; + + let mut maybe_signature = None; + let mut maybe_verify_key = None; + + for (key_id, verify_key) in verify_keys { + if let Some(signature) = signature_set.get(key_id) { + maybe_signature = Some(signature); + maybe_verify_key = Some(verify_key); + + break; + } + } + + let signature = match maybe_signature { + Some(signature) => signature, + None => { + return Err(Error::new( + "event is not signed with any of the given verify keys", + )) + } + }; + + let verify_key = match maybe_verify_key { + Some(verify_key) => verify_key, + None => { + return Err(Error::new( + "event is not signed with any of the given verify keys", + )) + } + }; + + let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?; + + let verify_key_bytes = decode_config(&verify_key, STANDARD_NO_PAD)?; + + verify_json_with(verifier, &verify_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. +pub fn verify_json_with( verifier: &V, public_key: &[u8], - signature: &Signature, + signature: &[u8], value: &Value, ) -> Result<(), Error> where @@ -422,17 +520,6 @@ where /// /// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; /// -/// // Decode the public key used to generate the signature into raw bytes. -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// -/// // Create a map from key ID to public key. -/// let mut example_server_keys = HashMap::new(); -/// example_server_keys.insert("ed25519:1", public_key.as_slice()); -/// -/// // Insert the public keys into a map keyed by server name. -/// let mut verify_key_map = HashMap::new(); -/// verify_key_map.insert("domain", example_server_keys); -/// /// // Deserialize an event from JSON. /// let value = serde_json::from_str( /// r#"{ @@ -462,17 +549,24 @@ where /// // Create the verifier for the Ed25519 algorithm. /// let verifier = ruma_signatures::Ed25519Verifier; /// +/// // Create a map from key ID to public key. +/// let mut example_server_keys = HashMap::new(); +/// example_server_keys.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); +/// +/// // Insert the public keys into a map keyed by server name. +/// let mut verify_key_map = HashMap::new(); +/// verify_key_map.insert("domain".to_string(), example_server_keys); +/// /// // Verify at least one signature for each server in `verify_key_map`. -/// assert!(ruma_signatures::verify_event(&verifier, verify_key_map, &value).is_ok()); +/// assert!(ruma_signatures::verify_event(&verifier, &verify_key_map, &value).is_ok()); /// ``` -pub fn verify_event( +pub fn verify_event( verifier: &V, - verify_key_map: HashMap<&str, HashMap<&str, &[u8], S>, S>, + verify_key_map: &SignatureMap, value: &Value, ) -> Result<(), Error> where V: Verifier, - S: BuildHasher, { let redacted = redact(value)?; @@ -532,13 +626,14 @@ where let canonical_json = from_str(&to_canonical_json(&redacted)?)?; - verify_json( + let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?; + + let verify_key_bytes = decode_config(&verify_key, STANDARD_NO_PAD)?; + + verify_json_with( verifier, - verify_key, - &Signature::new( - "ed25519:fixme", - &decode_config(signature, STANDARD_NO_PAD).expect("FIXME"), - )?, + &verify_key_bytes, + &signature_bytes, &canonical_json, )?; } diff --git a/src/lib.rs b/src/lib.rs index 3bb26d6a..07a36b65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,8 +100,6 @@ use std::{ fmt::{Display, Formatter, Result as FmtResult}, }; -pub use url::Host; - pub use functions::{ content_hash, hash_and_sign_event, reference_hash, sign_json, to_canonical_json, verify_event, verify_json, @@ -150,6 +148,12 @@ impl Display for Error { } } +impl From for Error { + fn from(error: base64::DecodeError) -> Self { + Self::new(error.to_string()) + } +} + impl From for Error { fn from(error: serde_json::Error) -> Self { Self::new(error.to_string()) @@ -183,12 +187,8 @@ mod test { use super::{ hash_and_sign_event, sign_json, to_canonical_json, verify_event, verify_json, - Ed25519KeyPair, Ed25519Verifier, Signature, + Ed25519KeyPair, Ed25519Verifier, }; - const EMPTY_JSON_SIGNATURE: &str = - "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; - const MINIMAL_JSON_SIGNATURE: &str = - "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; @@ -315,27 +315,17 @@ mod test { #[test] fn verify_empty_json() { - let signature = Signature::new( - "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - ) - .unwrap(); - - let value = from_str("{}").unwrap(); + let value = from_str(r#"{"signatures":{"example.com":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); let verifier = Ed25519Verifier; - assert!(verify_json( - &verifier, - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - &signature, - &value, - ) - .is_ok()); + let mut signature_set = HashMap::new(); + signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + + let mut verify_key_map = HashMap::new(); + verify_key_map.insert("example.com".to_string(), signature_set); + + assert!(verify_json(&verifier, &verify_key_map, &value).is_ok()); } #[test] @@ -393,68 +383,40 @@ mod test { #[test] fn verify_minimal_json() { - let signature = Signature::new( - "ed25519:1", - decode_config(&MINIMAL_JSON_SIGNATURE, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - ) - .unwrap(); - let value = from_str( r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); let verifier = Ed25519Verifier; - assert!(verify_json( - &verifier, - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - &signature, - &value, - ) - .is_ok()); + let mut signature_set = HashMap::new(); + signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + + let mut verify_key_map = HashMap::new(); + verify_key_map.insert("example.com".to_string(), signature_set); + + assert!(verify_json(&verifier, &verify_key_map, &value).is_ok()); let reverse_value = from_str( r#"{"two":"Two","signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); - assert!(verify_json( - &verifier, - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - &signature, - &reverse_value, - ) - .is_ok()); + assert!(verify_json(&verifier, &verify_key_map, &reverse_value).is_ok()); } #[test] fn fail_verify_json() { - let signature = Signature::new( - "ed25519:1", - decode_config(&EMPTY_JSON_SIGNATURE, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - ) - .unwrap(); - - let value = from_str(r#"{"not":"empty"}"#).unwrap(); + let value = from_str(r#"{"not":"empty","signatures":{"example.com":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); let verifier = Ed25519Verifier; - assert!(verify_json( - &verifier, - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - &signature, - &value, - ) - .is_err()); + let mut signature_set = HashMap::new(); + signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + + let mut verify_key_map = HashMap::new(); + verify_key_map.insert("example.com".to_string(), signature_set); + + assert!(verify_json(&verifier, &verify_key_map, &value).is_err()); } #[test] @@ -536,13 +498,11 @@ mod test { #[test] fn verify_minimal_event() { - let public_key = decode_config(&PUBLIC_KEY, STANDARD_NO_PAD).unwrap(); - - let mut example_server_keys = HashMap::new(); - example_server_keys.insert("ed25519:1", public_key.as_slice()); + let mut signature_set = HashMap::new(); + signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut verify_key_map = HashMap::new(); - verify_key_map.insert("domain", example_server_keys); + verify_key_map.insert("domain".to_string(), signature_set); let value = from_str( r#"{ @@ -571,6 +531,6 @@ mod test { let verifier = Ed25519Verifier; - assert!(verify_event(&verifier, verify_key_map, &value).is_ok()); + assert!(verify_event(&verifier, &verify_key_map, &value).is_ok()); } } diff --git a/src/verification.rs b/src/verification.rs index 664be611..6da72a7e 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -3,7 +3,7 @@ use ring::signature::{verify, ED25519}; use untrusted::Input; -use crate::{signatures::Signature, Error}; +use crate::Error; /// A digital signature verifier. pub trait Verifier { @@ -11,19 +11,15 @@ pub trait Verifier { /// /// # Parameters /// - /// * public_key: The public key of the key pair used to sign the message. - /// * signature: The `Signature` to verify. - /// * message: The message that was signed. + /// * 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: &Signature, - message: &[u8], - ) -> Result<(), Error>; + fn verify_json(&self, public_key: &[u8], signature: &[u8], message: &[u8]) + -> Result<(), Error>; } /// A verifier for Ed25519 digital signatures. @@ -34,14 +30,14 @@ impl Verifier for Ed25519Verifier { fn verify_json( &self, public_key: &[u8], - signature: &Signature, + signature: &[u8], message: &[u8], ) -> Result<(), Error> { verify( &ED25519, Input::from(public_key), Input::from(message), - Input::from(signature.as_bytes()), + Input::from(signature), ) .map_err(|_| Error::new("signature verification failed")) } From a8b8e70c2d621f763e14fa5646a37590a6067f17 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 02:16:26 -0700 Subject: [PATCH 70/98] Check content hash during event verification. --- src/functions.rs | 62 ++++++++++++++++++++++++++++++++++++--------- src/lib.rs | 6 ++--- src/verification.rs | 18 +++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 3306e32a..92abbcf6 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -6,7 +6,12 @@ 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, signatures::SignatureMap, verification::Verifier, Error}; +use crate::{ + keys::KeyPair, + signatures::SignatureMap, + verification::{Verified, Verifier}, + Error, +}; /// The fields that are allowed to remain in an event during redaction. static ALLOWED_KEYS: &[&str] = &[ @@ -121,7 +126,7 @@ where 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 => return Err(Error::new("field `signatures` must be a JSON object")), }, None => HashMap::with_capacity(1), }; @@ -236,7 +241,7 @@ where 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("field `signatures` must be a JSON object")), }, None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; @@ -481,7 +486,7 @@ where 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")), + None => return Err(Error::new("field `hashes` must be a JSON object")), }; } @@ -504,6 +509,10 @@ where /// 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 /// /// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. @@ -564,7 +573,7 @@ pub fn verify_event( verifier: &V, verify_key_map: &SignatureMap, value: &Value, -) -> Result<(), Error> +) -> Result where V: Verifier, { @@ -575,10 +584,24 @@ where _ => 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("field `signatures` must be a JSON object")), }, None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; @@ -638,7 +661,13 @@ where )?; } - Ok(()) + 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 @@ -666,11 +695,20 @@ fn to_canonical_json_with_fields_to_remove( to_string(&owned_value).map_err(Error::from) } -/// Redact the JSON representation of an event using the rules specified in the Matrix +/// Redacts the JSON representation of an event using the rules specified in the Matrix /// client-server specification. /// /// This is part of the process of signing an event. -fn redact(value: &Value) -> Result { +/// +/// 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. +pub fn redact(value: &Value) -> Result { if !value.is_object() { return Err(Error::new("JSON value must be a JSON object")); } @@ -683,14 +721,14 @@ fn redact(value: &Value) -> Result { 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")), + 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", + "field `type` in JSON value must be a JSON string", )) } }; @@ -700,7 +738,7 @@ fn redact(value: &Value) -> Result { Value::Object(ref mut map) => map, _ => { return Err(Error::new( - "Field `content` in JSON value must be a JSON object", + "field `content` in JSON value must be a JSON object", )) } }; diff --git a/src/lib.rs b/src/lib.rs index 07a36b65..5eadf6fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,12 +101,12 @@ use std::{ }; pub use functions::{ - content_hash, hash_and_sign_event, reference_hash, sign_json, to_canonical_json, verify_event, - verify_json, + content_hash, hash_and_sign_event, redact, reference_hash, sign_json, to_canonical_json, + verify_event, verify_json, }; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; -pub use verification::{Ed25519Verifier, Verifier}; +pub use verification::{Ed25519Verifier, Verified, Verifier}; mod functions; mod keys; diff --git a/src/verification.rs b/src/verification.rs index 6da72a7e..62773b1c 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -42,3 +42,21 @@ impl Verifier for Ed25519Verifier { .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, +} From d578292471c5db9ec3fa84960a8c353f73d34837 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 02:53:21 -0700 Subject: [PATCH 71/98] Rename to_canonical_json to canonical_json. --- src/functions.rs | 22 ++++++++++------------ src/lib.rs | 8 ++++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 92abbcf6..7390a566 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -160,7 +160,8 @@ where Ok(()) } -/// Converts a JSON object into the "canonical" string form. +/// Converts a JSON object into the +/// [canonical](https://matrix.org/docs/spec/appendices#canonical-json) string form. /// /// # Parameters /// @@ -169,11 +170,11 @@ where /// # Errors /// /// Returns an error if the provided JSON value is not a JSON object. -pub fn to_canonical_json(value: &Value) -> Result { - to_canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) +pub fn canonical_json(value: &Value) -> Result { + canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) } -/// Uses a public key to verify a signature of a JSON object. +/// Uses a set of public keys to verify a signed JSON object. /// /// # Parameters /// @@ -321,7 +322,7 @@ pub fn verify_json_with( where V: Verifier, { - verifier.verify_json(public_key, signature, to_canonical_json(value)?.as_bytes()) + verifier.verify_json(public_key, signature, canonical_json(value)?.as_bytes()) } /// Creates a *content hash* for the JSON representation of an event. @@ -339,7 +340,7 @@ where /// /// Returns an error if the provided JSON value is not a JSON object. pub fn content_hash(value: &Value) -> Result { - let json = to_canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?; + let json = canonical_json_with_fields_to_remove(value, CONTENT_HASH_FIELDS_TO_REMOVE)?; let hash = digest(&SHA256, json.as_bytes()); @@ -365,7 +366,7 @@ pub fn reference_hash(value: &Value) -> Result { let redacted_value = redact(value)?; let json = - to_canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE)?; + canonical_json_with_fields_to_remove(&redacted_value, REFERENCE_HASH_FIELDS_TO_REMOVE)?; let hash = digest(&SHA256, json.as_bytes()); @@ -647,7 +648,7 @@ where } }; - let canonical_json = from_str(&to_canonical_json(&redacted)?)?; + let canonical_json = from_str(&canonical_json(&redacted)?)?; let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?; @@ -672,10 +673,7 @@ where /// Internal implementation detail of the canonical JSON algorithm. Allows customization of the /// fields that will be removed before serializing. -fn to_canonical_json_with_fields_to_remove( - value: &Value, - fields: &[&str], -) -> Result { +fn canonical_json_with_fields_to_remove(value: &Value, fields: &[&str]) -> Result { if !value.is_object() { return Err(Error::new("JSON value must be a JSON object")); } diff --git a/src/lib.rs b/src/lib.rs index 5eadf6fd..9c834df5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,7 @@ use std::{ }; pub use functions::{ - content_hash, hash_and_sign_event, redact, reference_hash, sign_json, to_canonical_json, + canonical_json, content_hash, hash_and_sign_event, redact, reference_hash, sign_json, verify_event, verify_json, }; pub use keys::{Ed25519KeyPair, KeyPair}; @@ -186,8 +186,8 @@ mod test { use serde_json::{from_str, to_string, to_value, Value}; use super::{ - hash_and_sign_event, sign_json, to_canonical_json, verify_event, verify_json, - Ed25519KeyPair, Ed25519Verifier, + canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair, + Ed25519Verifier, }; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -197,7 +197,7 @@ mod test { fn test_canonical_json(input: &str) -> String { let value = from_str::(input).unwrap(); - to_canonical_json(&value).unwrap() + canonical_json(&value).unwrap() } #[test] From 972388a01903b693795004f7720f43df8231fa46 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 02:55:49 -0700 Subject: [PATCH 72/98] Document error conditions for `redact`. --- src/functions.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/functions.rs b/src/functions.rs index 7390a566..b0279309 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -706,6 +706,16 @@ fn canonical_json_with_fields_to_remove(value: &Value, fields: &[&str]) -> Resul /// # 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 { if !value.is_object() { return Err(Error::new("JSON value must be a JSON object")); From 04004547da2a9df031f65700a5a27d955639e0d2 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 03:05:13 -0700 Subject: [PATCH 73/98] Remove references to "server names." --- src/functions.rs | 46 +++++++++++++++++++++++----------------------- src/signatures.rs | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index b0279309..a0299fe8 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -60,7 +60,8 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// /// # Parameters /// -/// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. +/// * 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. /// @@ -70,7 +71,6 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// /// * `value` is not a JSON object. /// * `value` contains a field called `signatures` that is not a JSON object. -/// * `server_name` cannot be parsed as a valid host. /// /// # Examples /// @@ -108,7 +108,7 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// } /// } /// ``` -pub fn sign_json(server_name: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> +pub fn sign_json(entity_id: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> where K: KeyPair, { @@ -142,7 +142,7 @@ where // Insert the new signature in the map we pulled out (or created) previously. let signature_set = signature_map - .entry(server_name.to_string()) + .entry(entity_id.to_string()) .or_insert_with(|| HashMap::with_capacity(1)); signature_set.insert(signature.id(), signature.base64()); @@ -223,7 +223,7 @@ pub fn canonical_json(value: &Value) -> Result { /// let mut verify_key_map = HashMap::new(); /// verify_key_map.insert("example.com".to_string(), signature_set); /// -/// // Verify at least one signature for each server in `verify_key_map`. +/// // Verify at least one signature for each entity in `verify_key_map`. /// assert!(ruma_signatures::verify_json(&verifier, &verify_key_map, &value).is_ok()); /// ``` pub fn verify_json( @@ -247,13 +247,13 @@ where None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; - for (server_name, verify_keys) in verify_key_map { - let signature_set = match signature_map.get(server_name) { + for (entity_id, verify_keys) in verify_key_map { + let signature_set = match signature_map.get(entity_id) { Some(set) => set, None => { return Err(Error::new(format!( - "no signatures found for server `{}`", - server_name + "no signatures found for entity `{}`", + entity_id ))) } }; @@ -381,7 +381,8 @@ pub fn reference_hash(value: &Value) -> Result { /// /// # Parameters /// -/// * server_name: The hostname or IP of the homeserver, e.g. `example.com`. +/// * 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. /// @@ -394,7 +395,6 @@ pub fn reference_hash(value: &Value) -> Result { /// * `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. -/// * `server_name` cannot be parsed as a valid host. /// /// # Examples /// @@ -465,7 +465,7 @@ pub fn reference_hash(value: &Value) -> Result { /// /// Notice the addition of `hashes` and `signatures`. pub fn hash_and_sign_event( - server_name: &str, + entity_id: &str, key_pair: &K, value: &mut Value, ) -> Result<(), Error> @@ -493,7 +493,7 @@ where let mut redacted = redact(value)?; - sign_json(server_name, key_pair, &mut redacted)?; + sign_json(entity_id, key_pair, &mut redacted)?; // Safe to unwrap because we did this exact check at the beginning of the function. let map = value.as_object_mut().unwrap(); @@ -517,10 +517,10 @@ where /// # Parameters /// /// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * verify_key_map: A map from server names to a map from key identifiers to public keys. Server -/// names are the hostname or IP 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. +/// * verify_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 @@ -563,11 +563,11 @@ where /// let mut example_server_keys = HashMap::new(); /// example_server_keys.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); /// -/// // Insert the public keys into a map keyed by server name. +/// // Insert the public keys into a map keyed by entity ID. /// let mut verify_key_map = HashMap::new(); /// verify_key_map.insert("domain".to_string(), example_server_keys); /// -/// // Verify at least one signature for each server in `verify_key_map`. +/// // Verify at least one signature for each entity in `verify_key_map`. /// assert!(ruma_signatures::verify_event(&verifier, &verify_key_map, &value).is_ok()); /// ``` pub fn verify_event( @@ -607,13 +607,13 @@ where None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; - for (server_name, verify_keys) in verify_key_map { - let signature_set = match signature_map.get(server_name) { + for (entity_id, verify_keys) in verify_key_map { + let signature_set = match signature_map.get(entity_id) { Some(set) => set, None => { return Err(Error::new(format!( - "no signatures found for server `{}`", - server_name + "no signatures found for entity `{}`", + entity_id ))) } }; diff --git a/src/signatures.rs b/src/signatures.rs index ac4d9c3c..54698181 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -89,7 +89,7 @@ impl Signature { /// A map from entity names to sets of digital signatures created by that entity. /// -/// "Entity" is currently always a homeserver, e.g. "example.com". +/// "Entity" is generally a homeserver, e.g. "example.com". pub type SignatureMap = HashMap; /// A set of digital signatures created by a single homeserver. From 8da921cffa4e34bd9125ca82e161ffa36916a41e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 03:09:44 -0700 Subject: [PATCH 74/98] Always use the term "public key" instead of "verify key." --- src/functions.rs | 76 +++++++++++++++++++++++------------------------- src/lib.rs | 26 ++++++++--------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index a0299fe8..d1e92c3e 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -179,8 +179,10 @@ pub fn canonical_json(value: &Value) -> Result { /// # Parameters /// /// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * public_key: The public key of the key pair used to sign the JSON, as a series of bytes. -/// * signature: The `Signature` to verify. +/// * 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 @@ -220,15 +222,15 @@ pub fn canonical_json(value: &Value) -> Result { /// // Create the `SignatureMap` that will inform `verify_json` which signatures to verify. /// let mut signature_set = HashMap::new(); /// signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); -/// let mut verify_key_map = HashMap::new(); -/// verify_key_map.insert("example.com".to_string(), signature_set); +/// let mut public_key_map = HashMap::new(); +/// public_key_map.insert("example.com".to_string(), signature_set); /// -/// // Verify at least one signature for each entity in `verify_key_map`. -/// assert!(ruma_signatures::verify_json(&verifier, &verify_key_map, &value).is_ok()); +/// // Verify at least one signature for each entity in `public_key_map`. +/// assert!(ruma_signatures::verify_json(&verifier, &public_key_map, &value).is_ok()); /// ``` pub fn verify_json( verifier: &V, - verify_key_map: &SignatureMap, + public_key_map: &SignatureMap, value: &Value, ) -> Result<(), Error> where @@ -247,7 +249,7 @@ where None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; - for (entity_id, verify_keys) in verify_key_map { + for (entity_id, public_keys) in public_key_map { let signature_set = match signature_map.get(entity_id) { Some(set) => set, None => { @@ -259,12 +261,12 @@ where }; let mut maybe_signature = None; - let mut maybe_verify_key = None; + let mut maybe_public_key = None; - for (key_id, verify_key) in verify_keys { + for (key_id, public_key) in public_keys { if let Some(signature) = signature_set.get(key_id) { maybe_signature = Some(signature); - maybe_verify_key = Some(verify_key); + maybe_public_key = Some(public_key); break; } @@ -274,25 +276,25 @@ where Some(signature) => signature, None => { return Err(Error::new( - "event is not signed with any of the given verify keys", + "event is not signed with any of the given public keys", )) } }; - let verify_key = match maybe_verify_key { - Some(verify_key) => verify_key, + 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 verify keys", + "event is not signed with any of the given public keys", )) } }; let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?; - let verify_key_bytes = decode_config(&verify_key, STANDARD_NO_PAD)?; + let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?; - verify_json_with(verifier, &verify_key_bytes, &signature_bytes, value)?; + verify_json_with(verifier, &public_key_bytes, &signature_bytes, value)?; } Ok(()) @@ -313,7 +315,7 @@ where /// /// * The provided JSON value is not a JSON object. /// * Verification fails. -pub fn verify_json_with( +fn verify_json_with( verifier: &V, public_key: &[u8], signature: &[u8], @@ -464,11 +466,7 @@ pub fn reference_hash(value: &Value) -> Result { /// ``` /// /// Notice the addition of `hashes` and `signatures`. -pub fn hash_and_sign_event( - entity_id: &str, - key_pair: &K, - value: &mut Value, -) -> Result<(), Error> +pub fn hash_and_sign_event(entity_id: &str, key_pair: &K, value: &mut Value) -> Result<(), Error> where K: KeyPair, { @@ -517,7 +515,7 @@ where /// # Parameters /// /// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. -/// * verify_key_map: A map from entity identifiers to a map from key identifiers to public keys. +/// * 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. @@ -564,15 +562,15 @@ where /// example_server_keys.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); /// /// // Insert the public keys into a map keyed by entity ID. -/// let mut verify_key_map = HashMap::new(); -/// verify_key_map.insert("domain".to_string(), example_server_keys); +/// let mut public_key_map = HashMap::new(); +/// public_key_map.insert("domain".to_string(), example_server_keys); /// -/// // Verify at least one signature for each entity in `verify_key_map`. -/// assert!(ruma_signatures::verify_event(&verifier, &verify_key_map, &value).is_ok()); +/// // Verify at least one signature for each entity in `public_key_map`. +/// assert!(ruma_signatures::verify_event(&verifier, &public_key_map, &value).is_ok()); /// ``` pub fn verify_event( verifier: &V, - verify_key_map: &SignatureMap, + public_key_map: &SignatureMap, value: &Value, ) -> Result where @@ -607,7 +605,7 @@ where None => return Err(Error::new("JSON object must contain a `signatures` field.")), }; - for (entity_id, verify_keys) in verify_key_map { + for (entity_id, public_keys) in public_key_map { let signature_set = match signature_map.get(entity_id) { Some(set) => set, None => { @@ -619,12 +617,12 @@ where }; let mut maybe_signature = None; - let mut maybe_verify_key = None; + let mut maybe_public_key = None; - for (key_id, verify_key) in verify_keys { + for (key_id, public_key) in public_keys { if let Some(signature) = signature_set.get(key_id) { maybe_signature = Some(signature); - maybe_verify_key = Some(verify_key); + maybe_public_key = Some(public_key); break; } @@ -634,16 +632,16 @@ where Some(signature) => signature, None => { return Err(Error::new( - "event is not signed with any of the given verify keys", + "event is not signed with any of the given public keys", )) } }; - let verify_key = match maybe_verify_key { - Some(verify_key) => verify_key, + 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 verify keys", + "event is not signed with any of the given public keys", )) } }; @@ -652,11 +650,11 @@ where let signature_bytes = decode_config(signature, STANDARD_NO_PAD)?; - let verify_key_bytes = decode_config(&verify_key, STANDARD_NO_PAD)?; + let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?; verify_json_with( verifier, - &verify_key_bytes, + &public_key_bytes, &signature_bytes, &canonical_json, )?; diff --git a/src/lib.rs b/src/lib.rs index 9c834df5..1e6f5963 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -322,10 +322,10 @@ mod test { let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); - let mut verify_key_map = HashMap::new(); - verify_key_map.insert("example.com".to_string(), signature_set); + let mut public_key_map = HashMap::new(); + public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &verify_key_map, &value).is_ok()); + assert!(verify_json(&verifier, &public_key_map, &value).is_ok()); } #[test] @@ -392,16 +392,16 @@ mod test { let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); - let mut verify_key_map = HashMap::new(); - verify_key_map.insert("example.com".to_string(), signature_set); + let mut public_key_map = HashMap::new(); + public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &verify_key_map, &value).is_ok()); + assert!(verify_json(&verifier, &public_key_map, &value).is_ok()); let reverse_value = from_str( r#"{"two":"Two","signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); - assert!(verify_json(&verifier, &verify_key_map, &reverse_value).is_ok()); + assert!(verify_json(&verifier, &public_key_map, &reverse_value).is_ok()); } #[test] @@ -413,10 +413,10 @@ mod test { let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); - let mut verify_key_map = HashMap::new(); - verify_key_map.insert("example.com".to_string(), signature_set); + let mut public_key_map = HashMap::new(); + public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &verify_key_map, &value).is_err()); + assert!(verify_json(&verifier, &public_key_map, &value).is_err()); } #[test] @@ -501,8 +501,8 @@ mod test { let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); - let mut verify_key_map = HashMap::new(); - verify_key_map.insert("domain".to_string(), signature_set); + let mut public_key_map = HashMap::new(); + public_key_map.insert("domain".to_string(), signature_set); let value = from_str( r#"{ @@ -531,6 +531,6 @@ mod test { let verifier = Ed25519Verifier; - assert!(verify_event(&verifier, &verify_key_map, &value).is_ok()); + assert!(verify_event(&verifier, &public_key_map, &value).is_ok()); } } From 593d0e469b528b2e559161c7505475fda64f7543 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 03:23:49 -0700 Subject: [PATCH 75/98] Determine Verifier automatically so verifier types don't need to be public. --- src/functions.rs | 49 ++++++++++++++-------------------- src/lib.rs | 67 ++++++++++++++++++++++++++++++++++++----------- src/signatures.rs | 55 +++++--------------------------------- 3 files changed, 78 insertions(+), 93 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index d1e92c3e..df681a48 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -9,7 +9,8 @@ use serde_json::{from_str, from_value, map::Map, to_string, to_value, Value}; use crate::{ keys::KeyPair, signatures::SignatureMap, - verification::{Verified, Verifier}, + split_id, + verification::{Ed25519Verifier, Verified, Verifier}, Error, }; @@ -178,7 +179,6 @@ pub fn canonical_json(value: &Value) -> Result { /// /// # Parameters /// -/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. /// * 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. @@ -216,9 +216,6 @@ pub fn canonical_json(value: &Value) -> Result { /// }"# /// ).unwrap(); /// -/// // Create the verifier for the Ed25519 algorithm. -/// let verifier = ruma_signatures::Ed25519Verifier; -/// /// // Create the `SignatureMap` that will inform `verify_json` which signatures to verify. /// let mut signature_set = HashMap::new(); /// signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); @@ -226,16 +223,9 @@ pub fn canonical_json(value: &Value) -> Result { /// public_key_map.insert("example.com".to_string(), signature_set); /// /// // Verify at least one signature for each entity in `public_key_map`. -/// assert!(ruma_signatures::verify_json(&verifier, &public_key_map, &value).is_ok()); +/// assert!(ruma_signatures::verify_json(&public_key_map, &value).is_ok()); /// ``` -pub fn verify_json( - verifier: &V, - public_key_map: &SignatureMap, - value: &Value, -) -> Result<(), Error> -where - V: Verifier, -{ +pub fn verify_json(public_key_map: &SignatureMap, value: &Value) -> Result<(), Error> { let map = match value { Value::Object(ref map) => map, _ => return Err(Error::new("JSON value must be a JSON object")), @@ -264,6 +254,12 @@ where 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); @@ -294,7 +290,7 @@ where let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?; - verify_json_with(verifier, &public_key_bytes, &signature_bytes, value)?; + verify_json_with(&Ed25519Verifier, &public_key_bytes, &signature_bytes, value)?; } Ok(()) @@ -514,7 +510,6 @@ where /// /// # Parameters /// -/// * verifier: A `Verifier` appropriate for the digital signature algorithm that was used. /// * 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. @@ -554,9 +549,6 @@ where /// }"# /// ).unwrap(); /// -/// // Create the verifier for the Ed25519 algorithm. -/// let verifier = ruma_signatures::Ed25519Verifier; -/// /// // Create a map from key ID to public key. /// let mut example_server_keys = HashMap::new(); /// example_server_keys.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); @@ -566,16 +558,9 @@ where /// public_key_map.insert("domain".to_string(), example_server_keys); /// /// // Verify at least one signature for each entity in `public_key_map`. -/// assert!(ruma_signatures::verify_event(&verifier, &public_key_map, &value).is_ok()); +/// assert!(ruma_signatures::verify_event(&public_key_map, &value).is_ok()); /// ``` -pub fn verify_event( - verifier: &V, - public_key_map: &SignatureMap, - value: &Value, -) -> Result -where - V: Verifier, -{ +pub fn verify_event(public_key_map: &SignatureMap, value: &Value) -> Result { let redacted = redact(value)?; let map = match redacted { @@ -620,6 +605,12 @@ where 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); @@ -653,7 +644,7 @@ where let public_key_bytes = decode_config(&public_key, STANDARD_NO_PAD)?; verify_json_with( - verifier, + &Ed25519Verifier, &public_key_bytes, &signature_bytes, &canonical_json, diff --git a/src/lib.rs b/src/lib.rs index 1e6f5963..7b097a80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,6 @@ pub use functions::{ }; pub use keys::{Ed25519KeyPair, KeyPair}; pub use signatures::{Signature, SignatureMap, SignatureSet}; -pub use verification::{Ed25519Verifier, Verified, Verifier}; mod functions; mod keys; @@ -177,6 +176,53 @@ impl Display for Algorithm { } } +/// 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; @@ -187,7 +233,6 @@ mod test { use super::{ canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair, - Ed25519Verifier, }; const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; @@ -317,15 +362,13 @@ mod test { fn verify_empty_json() { let value = from_str(r#"{"signatures":{"example.com":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); - let verifier = Ed25519Verifier; - let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &public_key_map, &value).is_ok()); + assert!(verify_json(&public_key_map, &value).is_ok()); } #[test] @@ -387,36 +430,32 @@ mod test { r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); - let verifier = Ed25519Verifier; - let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &public_key_map, &value).is_ok()); + assert!(verify_json(&public_key_map, &value).is_ok()); let reverse_value = from_str( r#"{"two":"Two","signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); - assert!(verify_json(&verifier, &public_key_map, &reverse_value).is_ok()); + assert!(verify_json(&public_key_map, &reverse_value).is_ok()); } #[test] fn fail_verify_json() { let value = from_str(r#"{"not":"empty","signatures":{"example.com":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); - let verifier = Ed25519Verifier; - let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("example.com".to_string(), signature_set); - assert!(verify_json(&verifier, &public_key_map, &value).is_err()); + assert!(verify_json(&public_key_map, &value).is_err()); } #[test] @@ -529,8 +568,6 @@ mod test { }"# ).unwrap(); - let verifier = Ed25519Verifier; - - assert!(verify_event(&verifier, &public_key_map, &value).is_ok()); + assert!(verify_event(&public_key_map, &value).is_ok()); } } diff --git a/src/signatures.rs b/src/signatures.rs index 54698181..f1a03033 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use base64::{encode_config, STANDARD_NO_PAD}; -use crate::{Algorithm, Error}; +use crate::{split_id, Algorithm, Error, SplitError}; /// A digital signature. #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -38,7 +38,11 @@ impl Signature { /// /// # Errors /// - /// Returns an error if the key identifier is invalid. + /// 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 { 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)), @@ -97,53 +101,6 @@ pub type SignatureMap = HashMap; /// This is represented as a map from signing key ID to Base64-encoded signature. pub type SignatureSet = HashMap; -/// 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 tests { use super::Signature; From daf160324c007828376afa83efa3a0a43a4c004f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 03:50:49 -0700 Subject: [PATCH 76/98] Add PublicKey{Map,Set} and make the Signature versions private. --- src/functions.rs | 32 +++++++++++++++----------------- src/keys.rs | 15 ++++++++++++++- src/lib.rs | 30 +++++++++++++++--------------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index df681a48..b96fcb5b 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -7,7 +7,7 @@ use ring::digest::{digest, SHA256}; use serde_json::{from_str, from_value, map::Map, to_string, to_value, Value}; use crate::{ - keys::KeyPair, + keys::{KeyPair, PublicKeyMap}, signatures::SignatureMap, split_id, verification::{Ed25519Verifier, Verified, Verifier}, @@ -95,7 +95,7 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// let mut value = serde_json::from_str("{}").unwrap(); /// /// // Sign the JSON with the key pair. -/// assert!(ruma_signatures::sign_json("example.com", &key_pair, &mut value).is_ok()); +/// 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: @@ -103,7 +103,7 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// ```json /// { /// "signatures": { -/// "example.com": { +/// "domain": { /// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" /// } /// } @@ -209,23 +209,23 @@ pub fn canonical_json(value: &Value) -> Result { /// let value = serde_json::from_str( /// r#"{ /// "signatures": { -/// "example.com": { +/// "domain": { /// "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" /// } /// } /// }"# /// ).unwrap(); /// -/// // Create the `SignatureMap` that will inform `verify_json` which signatures to verify. -/// let mut signature_set = HashMap::new(); -/// signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); +/// // 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("example.com".to_string(), signature_set); +/// 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: &SignatureMap, value: &Value) -> Result<(), Error> { +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")), @@ -431,7 +431,7 @@ pub fn reference_hash(value: &Value) -> Result { /// ).unwrap(); /// /// // Hash and sign the JSON with the key pair. -/// assert!(ruma_signatures::hash_and_sign_event("example.com", &key_pair, &mut value).is_ok()); +/// 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: @@ -549,18 +549,16 @@ where /// }"# /// ).unwrap(); /// -/// // Create a map from key ID to public key. -/// let mut example_server_keys = HashMap::new(); -/// example_server_keys.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); -/// -/// // Insert the public keys into a map keyed by entity ID. +/// // 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(), example_server_keys); +/// 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: &SignatureMap, value: &Value) -> Result { +pub fn verify_event(public_key_map: &PublicKeyMap, value: &Value) -> Result { let redacted = redact(value)?; let map = match redacted { diff --git a/src/keys.rs b/src/keys.rs index 0fcd374d..8afaf11c 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,6 +1,9 @@ //! Public and private key pairs. -use std::fmt::{Debug, Formatter, Result as FmtResult}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter, Result as FmtResult}, +}; use ring::signature::Ed25519KeyPair as RingEd25519KeyPair; use untrusted::Input; @@ -87,3 +90,13 @@ impl Debug for Ed25519KeyPair { .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; + +/// 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; diff --git a/src/lib.rs b/src/lib.rs index 7b097a80..76ebad09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,8 +104,8 @@ 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}; -pub use signatures::{Signature, SignatureMap, SignatureSet}; +pub use keys::{Ed25519KeyPair, KeyPair, PublicKeyMap, PublicKeySet}; +pub use signatures::Signature; mod functions; mod keys; @@ -350,23 +350,23 @@ mod test { let mut value = from_str("{}").unwrap(); - sign_json("example.com", &key_pair, &mut value).unwrap(); + sign_json("domain", &key_pair, &mut value).unwrap(); assert_eq!( to_string(&value).unwrap(), - r#"{"signatures":{"example.com":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# + r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# ); } #[test] fn verify_empty_json() { - let value = from_str(r#"{"signatures":{"example.com":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); + let value = from_str(r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); - public_key_map.insert("example.com".to_string(), signature_set); + public_key_map.insert("domain".to_string(), signature_set); assert!(verify_json(&public_key_map, &value).is_ok()); } @@ -407,39 +407,39 @@ mod test { }; let mut alpha_value = to_value(alpha).expect("alpha should serialize"); - sign_json("example.com", &key_pair, &mut alpha_value).unwrap(); + sign_json("domain", &key_pair, &mut alpha_value).unwrap(); assert_eq!( to_string(&alpha_value).unwrap(), - r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ); let mut reverse_alpha_value = to_value(reverse_alpha).expect("reverse_alpha should serialize"); - sign_json("example.com", &key_pair, &mut reverse_alpha_value).unwrap(); + sign_json("domain", &key_pair, &mut reverse_alpha_value).unwrap(); assert_eq!( to_string(&reverse_alpha_value).unwrap(), - r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ); } #[test] fn verify_minimal_json() { let value = from_str( - r#"{"one":1,"signatures":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# ).unwrap(); let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); - public_key_map.insert("example.com".to_string(), signature_set); + 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":{"example.com":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# + r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# ).unwrap(); assert!(verify_json(&public_key_map, &reverse_value).is_ok()); @@ -447,13 +447,13 @@ mod test { #[test] fn fail_verify_json() { - let value = from_str(r#"{"not":"empty","signatures":{"example.com":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); + let value = from_str(r#"{"not":"empty","signatures":{"domain":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); let mut signature_set = HashMap::new(); signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); let mut public_key_map = HashMap::new(); - public_key_map.insert("example.com".to_string(), signature_set); + public_key_map.insert("domain".to_string(), signature_set); assert!(verify_json(&public_key_map, &value).is_err()); } From b867e3230abb6cec561de02ecbdbe1bb7019497c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 03:56:09 -0700 Subject: [PATCH 77/98] Remove serde dependency. --- Cargo.toml | 4 ---- src/lib.rs | 31 +++++++++---------------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c20ed84..d0646250 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,3 @@ base64 = "0.10.1" ring = "0.14.6" serde_json = "1.0.39" untrusted = "0.6.2" - -[dependencies.serde] -version = "1.0.90" -features = ["derive"] diff --git a/src/lib.rs b/src/lib.rs index 76ebad09..6108ca68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,8 +228,7 @@ mod test { use std::collections::HashMap; use base64::{decode_config, STANDARD_NO_PAD}; - use serde::Serialize; - use serde_json::{from_str, to_string, to_value, Value}; + 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, @@ -373,18 +372,6 @@ mod test { #[test] fn sign_minimal_json() { - #[derive(Serialize)] - struct Alpha { - one: u8, - two: String, - } - - #[derive(Serialize)] - struct ReverseAlpha { - two: String, - one: u8, - } - let key_pair = Ed25519KeyPair::new( decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) .unwrap() @@ -396,15 +383,15 @@ mod test { ) .unwrap(); - let alpha = Alpha { - one: 1, - two: "Two".to_string(), - }; + let alpha = json!({ + "one": 1, + "two": "Two", + }); - let reverse_alpha = ReverseAlpha { - two: "Two".to_string(), - one: 1, - }; + 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(); From 31d5c42a7585960fc3ac73f296e5d13f73007e58 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 04:04:17 -0700 Subject: [PATCH 78/98] Remove docs about signatures, maps, and sets. --- src/lib.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6108ca68..0357be99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,34 +19,6 @@ //! In JSON representations, both signatures and hashes appear as Base64-encoded strings, using the //! standard character set, without padding. //! -//! # Signatures, maps, and sets -//! -//! An individual signature is represented in ruma-signatures by the `Signature` type. This type -//! encapsulates the raw bytes of the signature, the identifier for the signing key used, and the -//! algorithm used to create the signature. -//! -//! As mentioned, signatures that a homeserver has added to an event are stored in a JSON object -//! under the `signatures` key in the event's JSON representation: -//! -//! ```json -//! { -//! "content": {}, -//! "event_type": "not.a.real.event", -//! "signatures": { -//! "example.com": { -//! "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" -//! } -//! } -//! } -//! ``` -//! -//! The value of the the `signatures` key is represented in ruma-signatures by the `SignatureMap` -//! type. This type maps the name of a homeserver to a set of its signatures for the containing -//! data. The set of signatures for each homeserver (which appears as a map from key ID to signature -//! in the JSON representation) is represented in ruma-signatures by the `SignatureSet` type. Both -//! `SignatureMap`s and `SignatureSet`s can be serialized and deserialized with -//! [https://serde.rs/](Serde). -//! //! # Signing and hashing //! //! To sign an arbitrary JSON object, use the `sign_json` function. See the documentation of this From 798a2387cfd0138653599f56ed259d34ef889ba0 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 04:10:44 -0700 Subject: [PATCH 79/98] Add example for `canonical_json`. --- src/functions.rs | 25 ++++++++++++++++--------- src/lib.rs | 26 +++++++++++++------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index b96fcb5b..9978a9c9 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -171,6 +171,22 @@ where /// # 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::(input).unwrap(); +/// +/// let canonical = ruma_signatures::canonical_json(&value).unwrap(); +/// +/// assert_eq!(canonical, r#"{"日":1,"本":2}"#); +/// ``` pub fn canonical_json(value: &Value) -> Result { canonical_json_with_fields_to_remove(value, CANONICAL_JSON_FIELDS_TO_REMOVE) } @@ -195,15 +211,6 @@ pub fn canonical_json(value: &Value) -> Result { /// use std::collections::HashMap; /// /// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -/// const SIGNATURE_BYTES: &str = -/// "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; -/// -/// // Decode the public key used to generate the signature into raw bytes. -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// -/// // Create a `Signature` from the raw bytes of the signature. -/// let signature_bytes = base64::decode_config(&SIGNATURE_BYTES, base64::STANDARD_NO_PAD).unwrap(); -/// let signature = ruma_signatures::Signature::new("ed25519:1", &signature_bytes).unwrap(); /// /// // Deserialize the signed JSON. /// let value = serde_json::from_str( diff --git a/src/lib.rs b/src/lib.rs index 0357be99..163cc08b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,9 +223,9 @@ mod test { assert_eq!( &test_canonical_json( r#"{ - "one": 1, - "two": "Two" - }"# + "one": 1, + "two": "Two" + }"# ), r#"{"one":1,"two":"Two"}"# ); @@ -233,9 +233,9 @@ mod test { assert_eq!( &test_canonical_json( r#"{ - "b": "2", - "a": "1" - }"# + "b": "2", + "a": "1" + }"# ), r#"{"a":"1","b":"2"}"# ); @@ -271,8 +271,8 @@ mod test { assert_eq!( &test_canonical_json( r#"{ - "a": "日本語" - }"# + "a": "日本語" + }"# ), r#"{"a":"日本語"}"# ); @@ -280,9 +280,9 @@ mod test { assert_eq!( &test_canonical_json( r#"{ - "本": 2, - "日": 1 - }"# + "本": 2, + "日": 1 + }"# ), r#"{"日":1,"本":2}"# ); @@ -290,8 +290,8 @@ mod test { assert_eq!( &test_canonical_json( r#"{ - "a": "\u65E5" - }"# + "a": "\u65E5" + }"# ), r#"{"a":"日"}"# ); From c2ad08a9f697a8a111921f74cdd9b837b0fd7bb3 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 04:18:06 -0700 Subject: [PATCH 80/98] Update dependencies. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d0646250..9a96dff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,5 @@ version = "0.4.2" [dependencies] base64 = "0.10.1" ring = "0.14.6" -serde_json = "1.0.39" +serde_json = "1.0.40" untrusted = "0.6.2" From dce3f62e819869e36b6a19547df58a7dc367d990 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Jul 2019 04:19:32 -0700 Subject: [PATCH 81/98] Bump version to 0.5.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9a96dff7..d0a42398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.4.2" +version = "0.5.0" [dependencies] base64 = "0.10.1" From 110fd05d1dc16c5d108ff7875da72ee2f965c3f9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 23 Jul 2019 10:00:34 -0700 Subject: [PATCH 82/98] Run cargo-audit on CI. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 91661b79..9165a6f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,12 @@ language: "rust" +cache: "cargo" before_script: - "rustup component add rustfmt" - "rustup component add clippy" + - "cargo install --force cargo-audit" + - "cargo generate-lockfile" script: + - "cargo audit" - "cargo fmt --all -- --check" - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" From 346eef308138d56c0ffb99d5aabb953a1c7245dc Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 23 Jul 2019 22:08:12 +0200 Subject: [PATCH 83/98] Update ring to 0.16.1, untrusted to 0.7.0 --- Cargo.toml | 4 ++-- src/keys.rs | 14 ++++---------- src/verification.rs | 16 ++++++++-------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d0a42398..688a7b33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,6 @@ version = "0.5.0" [dependencies] base64 = "0.10.1" -ring = "0.14.6" +ring = "0.16.1" serde_json = "1.0.40" -untrusted = "0.6.2" +untrusted = "0.7.0" diff --git a/src/keys.rs b/src/keys.rs index 8afaf11c..1cbc7d10 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -6,7 +6,6 @@ use std::{ }; use ring::signature::Ed25519KeyPair as RingEd25519KeyPair; -use untrusted::Input; use crate::{signatures::Signature, Algorithm, Error}; @@ -49,10 +48,7 @@ impl Ed25519KeyPair { /// Returns an error if the public and private keys provided are invalid for the implementing /// algorithm. pub fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { - if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key( - Input::from(private_key), - Input::from(public_key), - ) { + if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key(private_key, public_key) { return Err(Error::new(error.to_string())); } @@ -67,11 +63,9 @@ impl Ed25519KeyPair { impl KeyPair for Ed25519KeyPair { fn sign(&self, message: &[u8]) -> Signature { // Okay to unwrap because we verified the input in `new`. - let ring_key_pair = RingEd25519KeyPair::from_seed_and_public_key( - Input::from(&self.private_key), - Input::from(&self.public_key), - ) - .unwrap(); + let ring_key_pair = + RingEd25519KeyPair::from_seed_and_public_key(&self.private_key, &self.public_key) + .unwrap(); Signature { algorithm: Algorithm::Ed25519, diff --git a/src/verification.rs b/src/verification.rs index 62773b1c..f8df82be 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -1,6 +1,6 @@ //! Verification of digital signatures. -use ring::signature::{verify, ED25519}; +use ring::signature::{VerificationAlgorithm, ED25519}; use untrusted::Input; use crate::Error; @@ -33,13 +33,13 @@ impl Verifier for Ed25519Verifier { signature: &[u8], message: &[u8], ) -> Result<(), Error> { - verify( - &ED25519, - Input::from(public_key), - Input::from(message), - Input::from(signature), - ) - .map_err(|_| Error::new("signature verification failed")) + ED25519 + .verify( + Input::from(public_key), + Input::from(message), + Input::from(signature), + ) + .map_err(|_| Error::new("signature verification failed")) } } From 09aaf69007167bce3546a20c56d57b9b21900780 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 3 Aug 2019 14:02:53 -0700 Subject: [PATCH 84/98] Only build PRs and the master branch on CI. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9165a6f5..5e18c960 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ script: - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" - "cargo test --verbose" +if: "type != push OR (tag IS blank AND branch = master)" notifications: email: false irc: From 9f143f33837f02c7a2d380abe9d067d75ee7de19 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:10:49 +0100 Subject: [PATCH 85/98] Update dependencies --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 688a7b33..589ec858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.5.0" [dependencies] -base64 = "0.10.1" -ring = "0.16.1" -serde_json = "1.0.40" +base64 = "0.11.0" +ring = "0.16.9" +serde_json = "1.0.41" untrusted = "0.7.0" From a13f2a7eb95267afb1b7ec7839888232e39af4be Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:18:50 +0100 Subject: [PATCH 86/98] Test beta and nightly on CI --- .travis.yml | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e18c960..db27637d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,31 @@ language: "rust" cache: "cargo" +rust: + - stable + - beta + - nightly +jobs: + allow_failures: + - rust: nightly + fast_finish: true + before_script: - - "rustup component add rustfmt" - - "rustup component add clippy" - - "cargo install --force cargo-audit" - - "cargo generate-lockfile" + - rustup component add rustfmt + - rustup component add clippy + - | + if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then + cargo install --force cargo-audit + fi + - cargo generate-lockfile script: - - "cargo audit" - - "cargo fmt --all -- --check" - - "cargo clippy --all-targets --all-features -- -D warnings" - - "cargo build --verbose" - - "cargo test --verbose" + - | + if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then + cargo audit + fi + - cargo fmt -- --check + - cargo clippy --all-targets --all-features -- -D warnings + - cargo build --verbose + - cargo test --verbose if: "type != push OR (tag IS blank AND branch = master)" notifications: email: false From 972022c1758abeba00623be498e65da58cdf6f9e Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:18:59 +0100 Subject: [PATCH 87/98] Add MSRV section to README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a642afa6..92b63566 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ ruma-signatures provides functionality for creating digital signatures according 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/ + ## License [MIT](http://opensource.org/licenses/MIT) From 1cd00e05e9cc5002e94fb0f0fd72d5f84e02ed57 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:21:16 +0100 Subject: [PATCH 88/98] Re-format test --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 163cc08b..5c01f79e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,7 +245,8 @@ mod test { r#"{"a":"1","b":"2"}"# ); - assert_eq!(&test_canonical_json( + assert_eq!( + &test_canonical_json( r#"{ "auth": { "success": true, @@ -264,7 +265,8 @@ mod test { ] } } - }"#), + }"# + ), 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}}"# ); From f88731052450791f6604b5f5c294a788e289e1ae Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:21:26 +0100 Subject: [PATCH 89/98] Apply clippy suggestion --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5c01f79e..9306b58b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,7 +141,7 @@ pub enum Algorithm { impl Display for Algorithm { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let name = match *self { - Algorithm::Ed25519 => "ed25519", + Self::Ed25519 => "ed25519", }; write!(f, "{}", name) From d3e519067011cb8e9d8acde82387f17949147d3a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:21:50 +0100 Subject: [PATCH 90/98] =?UTF-8?q?Remove=20#![deny(warnings)],=20#![warn(cl?= =?UTF-8?q?ippy::=E2=80=A6)]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 56 ++++++++++++++++++------------------------------------ 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9306b58b..8f4e9806 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,27 +44,7 @@ #![deny( missing_copy_implementations, missing_debug_implementations, - missing_docs, - warnings -)] -#![warn( - clippy::empty_line_after_outer_attr, - clippy::expl_impl_clone_on_copy, - clippy::if_not_else, - clippy::items_after_statements, - clippy::match_same_arms, - clippy::mem_forget, - clippy::missing_docs_in_private_items, - clippy::multiple_inherent_impl, - clippy::mut_mut, - clippy::needless_borrow, - clippy::needless_continue, - clippy::single_match_else, - clippy::unicode_not_nfc, - clippy::use_self, - clippy::used_underscore_binding, - clippy::wrong_pub_self_convention, - clippy::wrong_self_convention + missing_docs )] use std::{ @@ -247,24 +227,24 @@ mod test { 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": { + "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}}"# From a08fc01c0bce63f913e1b4b1a673169d59738b63 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 12 Nov 2019 01:26:33 +0100 Subject: [PATCH 91/98] Add #![warn(rust_2018_idioms)], fix warning --- src/keys.rs | 2 +- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keys.rs b/src/keys.rs index 1cbc7d10..8800831c 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -76,7 +76,7 @@ impl KeyPair for Ed25519KeyPair { } impl Debug for Ed25519KeyPair { - fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { formatter .debug_struct("Ed25519KeyPair") .field("public_key", &self.public_key) diff --git a/src/lib.rs b/src/lib.rs index 8f4e9806..6908df9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ //! 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, From 6310f97ad2a1452077cd7fe3a184b472c5688053 Mon Sep 17 00:00:00 2001 From: timokoesters Date: Mon, 30 Mar 2020 16:28:06 +0200 Subject: [PATCH 92/98] update dependencies --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 589ec858..02a7c296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ repository = "https://github.com/ruma/ruma-signatures" version = "0.5.0" [dependencies] -base64 = "0.11.0" -ring = "0.16.9" -serde_json = "1.0.41" +base64 = "0.12.0" +ring = "0.16.12" +serde_json = "1.0.50" untrusted = "0.7.0" From 575001de130d2a708863c1efad9f2752d0cb2eb1 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 30 Mar 2020 20:11:51 +0200 Subject: [PATCH 93/98] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 02a7c296..21c7fbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" name = "ruma-signatures" readme = "README.md" repository = "https://github.com/ruma/ruma-signatures" -version = "0.5.0" +version = "0.6.0-dev.1" [dependencies] base64 = "0.12.0" From 9947e94cb28daea456904197f7cd754a8e48797a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 3 Apr 2020 19:25:13 +0200 Subject: [PATCH 94/98] CI: Disable IRC notifications --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index db27637d..e7f2baac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,9 +27,3 @@ script: - cargo build --verbose - cargo test --verbose if: "type != push OR (tag IS blank AND branch = master)" -notifications: - email: false - irc: - channels: - - secure: "bI2kShW50RpX6ptnZQEzv8BSnejCskATLMvhKndXsTEJhrBIxC/IgOyWi9WTwmYWXYgupuKYRe+z5dk/BEWBa+JfKh+UYOGtwE5Y5T9S4JV2rDQjxb7xSI/vwLTqlDHhzBvqK+9JxfsqitS7FFVGtmNEXFc27f2NwC3/uW/CAM1iPD+K+Xe9ZoqAT0+03tdJtPY9kVE8+h3k5V/KAo4ENx8MqQ7g31r8ch8twFUjuknMhR9Les5trNoMUJk46YluhThUgSAvcjo0+n43cxfv2t0Qx7NSzTc0myLfK0nRMRAuFEMeSnWJWnSLWLKSeluEsYN8RPdCQGNlAzoftDSBPAgDBtWS8FYqQZVbkxBJepdczjy+PFAgjjBr+eqMRjPz1t83ThElefPxkQUshzQU3DVifUwSryNhF58tmZ/4iiCyw0fIPZL1m7n0OivpBNkTLFqeSHTZgGiCBThc3MFarL6qOcBkFZWynG19lNQ+s9BzdVeN/veFWRp6m1lOymPSCVVDt7KMoloEK2ie1wkVzvc6IlFRXJgDWU+C4fy+nue2ghdAv4ZQ1D+JDX2M4DCrtfWBZsdTR+UsQL8S8fY0zOcQw0y39qh4AsoKeArvjUfWxfDPx5oPSqpwM8Mvmdp/OAC5knUeSxBtjpnnPPxcucE2oQ07QYq3/SHnjeEqhrA=" - use_notice: true From ef482071f738690636562bd3433fc582f7984a81 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 10 Apr 2020 14:10:08 +0200 Subject: [PATCH 95/98] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92b63566..41c9016a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # 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/) +[![build status](https://travis-ci.org/ruma/ruma-signatures.svg?branch=master)](https://travis-ci.org/ruma/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 @@ -13,7 +18,3 @@ 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/ - -## License - -[MIT](http://opensource.org/licenses/MIT) From c3f8399c268695464730afd6077c7ce50155b8d5 Mon Sep 17 00:00:00 2001 From: Riley Date: Sun, 19 Apr 2020 12:36:05 -0500 Subject: [PATCH 96/98] Update Ed25519KeyPair representation, add key generation --- src/functions.rs | 18 ++++-------- src/keys.rs | 61 +++++++++++++++++++++++----------------- src/lib.rs | 72 +++++++++++++++++++++--------------------------- 3 files changed, 74 insertions(+), 77 deletions(-) diff --git a/src/functions.rs b/src/functions.rs index 9978a9c9..1f18c23d 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -78,16 +78,13 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// A homeserver signs JSON with a key pair: /// /// ```rust -/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ"; /// -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap(); /// /// // Create an Ed25519 key pair. /// let key_pair = ruma_signatures::Ed25519KeyPair::new( -/// &public_key, -/// &private_key, +/// &document, /// "1".to_string(), // The "version" of the key. /// ).unwrap(); /// @@ -404,16 +401,13 @@ pub fn reference_hash(value: &Value) -> Result { /// # Examples /// /// ```rust -/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ"; /// -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap(); /// /// // Create an Ed25519 key pair. /// let key_pair = ruma_signatures::Ed25519KeyPair::new( -/// &public_key, -/// &private_key, +/// &document, /// "1".to_string(), // The "version" of the key. /// ).unwrap(); /// diff --git a/src/keys.rs b/src/keys.rs index 8800831c..bb652ac7 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -5,7 +5,7 @@ use std::{ fmt::{Debug, Formatter, Result as FmtResult}, }; -use ring::signature::Ed25519KeyPair as RingEd25519KeyPair; +use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _}; use crate::{signatures::Signature, Algorithm, Error}; @@ -20,13 +20,9 @@ pub trait KeyPair: Sized { } /// An Ed25519 key pair. -#[derive(Clone, PartialEq)] pub struct Ed25519KeyPair { - /// The public key. - public_key: Vec, - - /// The private key. - private_key: Vec, + /// Ring's Keypair type + keypair: RingEd25519KeyPair, /// The version of the key pair. version: String, @@ -37,8 +33,7 @@ impl Ed25519KeyPair { /// /// # Parameters /// - /// * public_key: The public key of the key pair. - /// * private_key: The private key of the key pair. + /// * 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. @@ -47,29 +42,35 @@ impl Ed25519KeyPair { /// /// Returns an error if the public and private keys provided are invalid for the implementing /// algorithm. - pub fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { - if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key(private_key, public_key) { - return Err(Error::new(error.to_string())); - } + pub fn new(document: &[u8], version: String) -> Result { + let keypair = RingEd25519KeyPair::from_pkcs8(document) + .map_err(|error| Error::new(error.to_string()))?; - Ok(Self { - public_key: public_key.to_owned(), - private_key: private_key.to_owned(), - version, - }) + Ok(Self { keypair, version }) + } + + /// Generates a new key pair. + /// + /// # Returns + /// + /// Returns a Vec representing a pkcs8-encoded private/public keypair + /// + /// # Errors + /// + /// Returns an error if the generation failed. + pub fn generate() -> Result, Error> { + let document = RingEd25519KeyPair::generate_pkcs8(&ring::rand::SystemRandom::new()) + .map_err(|e| Error::new(e.to_string()))?; + + Ok(document.as_ref().to_vec()) } } impl KeyPair for Ed25519KeyPair { fn sign(&self, message: &[u8]) -> Signature { - // Okay to unwrap because we verified the input in `new`. - let ring_key_pair = - RingEd25519KeyPair::from_seed_and_public_key(&self.private_key, &self.public_key) - .unwrap(); - Signature { algorithm: Algorithm::Ed25519, - signature: ring_key_pair.sign(message).as_ref().to_vec(), + signature: self.keypair.sign(message).as_ref().to_vec(), version: self.version.clone(), } } @@ -79,7 +80,7 @@ impl Debug for Ed25519KeyPair { fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { formatter .debug_struct("Ed25519KeyPair") - .field("public_key", &self.public_key) + .field("public_key", &self.keypair.public_key()) .field("version", &self.version) .finish() } @@ -94,3 +95,13 @@ pub type PublicKeyMap = HashMap; /// /// This is represented as a map from key ID to Base64-encoded signature. pub type PublicKeySet = HashMap; + +#[cfg(test)] +mod tests { + use super::Ed25519KeyPair; + + #[test] + fn generate_key() { + Ed25519KeyPair::generate().unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6908df9b..907e8787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,14 +181,26 @@ 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 PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; - const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; + 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 { @@ -292,12 +304,7 @@ mod test { #[test] fn sign_empty_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -308,16 +315,16 @@ mod test { assert_eq!( to_string(&value).unwrap(), - r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# + r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"# ); } #[test] fn verify_empty_json() { - let value = from_str(r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); + 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.to_string()); + 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); @@ -328,12 +335,7 @@ mod test { #[test] fn sign_minimal_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -353,7 +355,7 @@ mod test { assert_eq!( to_string(&alpha_value).unwrap(), - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"# ); let mut reverse_alpha_value = @@ -362,18 +364,18 @@ mod test { assert_eq!( to_string(&reverse_alpha_value).unwrap(), - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + 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":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + 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.to_string()); + 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); @@ -381,7 +383,7 @@ mod test { assert!(verify_json(&public_key_map, &value).is_ok()); let reverse_value = from_str( - r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# + r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"# ).unwrap(); assert!(verify_json(&public_key_map, &reverse_value).is_ok()); @@ -389,10 +391,10 @@ mod test { #[test] fn fail_verify_json() { - let value = from_str(r#"{"not":"empty","signatures":{"domain":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); + 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.to_string()); + 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); @@ -403,12 +405,7 @@ mod test { #[test] fn sign_minimal_event() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -435,19 +432,14 @@ mod test { 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":"KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg"}},"type":"X","unsigned":{"age_ts":1000000}}"# + r#"{"auth_events":[],"content":{},"depth":3,"hashes":{"sha256":"5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"},"origin":"domain","origin_server_ts":1000000,"prev_events":[],"room_id":"!x:domain","sender":"@a:domain","signatures":{"domain":{"ed25519:1":"PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"}},"type":"X","unsigned":{"age_ts":1000000}}"# ); } #[test] fn sign_redacted_event() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -473,14 +465,14 @@ mod test { 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":"Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUwu6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# + r#"{"content":{"body":"Here is the message content"},"event_id":"$0:domain","hashes":{"sha256":"onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g"},"origin":"domain","origin_server_ts":1000000,"room_id":"!r:domain","sender":"@u:domain","signatures":{"domain":{"ed25519:1":"D2V+qWBJssVuK/pEUJtwaYMdww2q1fP4PRCo226ChlLz8u8AWmQdLKes19NMjs/X0Hv0HIjU0c1TDKFMtGuoCA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# ); } #[test] fn verify_minimal_event() { let mut signature_set = HashMap::new(); - signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + 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); @@ -500,7 +492,7 @@ mod test { "sender": "@a:domain", "signatures": { "domain": { - "ed25519:1": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg" + "ed25519:1": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw" } }, "type": "X", From c94eecac0c56820b88350040982d79305e10c56b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 24 Apr 2020 21:56:02 +0200 Subject: [PATCH 97/98] Switch CI from travis to builds.sr.ht --- .builds/beta.yml | 27 +++++++++++++++++++++++++++ .builds/nightly.yml | 32 ++++++++++++++++++++++++++++++++ .builds/stable.yml | 29 +++++++++++++++++++++++++++++ .travis.yml | 29 ----------------------------- README.md | 1 - 5 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 .builds/beta.yml create mode 100644 .builds/nightly.yml create mode 100644 .builds/stable.yml delete mode 100644 .travis.yml diff --git a/.builds/beta.yml b/.builds/beta.yml new file mode 100644 index 00000000..e9349ff7 --- /dev/null +++ b/.builds/beta.yml @@ -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 )) diff --git a/.builds/nightly.yml b/.builds/nightly.yml new file mode 100644 index 00000000..f2345792 --- /dev/null +++ b/.builds/nightly.yml @@ -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 )) diff --git a/.builds/stable.yml b/.builds/stable.yml new file mode 100644 index 00000000..8d0152dc --- /dev/null +++ b/.builds/stable.yml @@ -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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e7f2baac..00000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: "rust" -cache: "cargo" -rust: - - stable - - beta - - nightly -jobs: - allow_failures: - - rust: nightly - fast_finish: true - -before_script: - - rustup component add rustfmt - - rustup component add clippy - - | - if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then - cargo install --force cargo-audit - fi - - cargo generate-lockfile -script: - - | - if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then - cargo audit - fi - - cargo fmt -- --check - - cargo clippy --all-targets --all-features -- -D warnings - - cargo build --verbose - - cargo test --verbose -if: "type != push OR (tag IS blank AND branch = master)" diff --git a/README.md b/README.md index 41c9016a..720dd575 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![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/) -[![build status](https://travis-ci.org/ruma/ruma-signatures.svg?branch=master)](https://travis-ci.org/ruma/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. From 1ca545cba8dfd43e0fc8e3c18e1311fb73390a97 Mon Sep 17 00:00:00 2001 From: timokoesters Date: Thu, 23 Apr 2020 14:25:15 +0200 Subject: [PATCH 98/98] Add methods to get version and public key of keypairs --- src/keys.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/keys.rs b/src/keys.rs index bb652ac7..a8921230 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -64,6 +64,16 @@ impl Ed25519KeyPair { 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 {