Switch to *ring* for crypto and flesh out the API.
This commit is contained in:
parent
6460312c79
commit
b080a934fb
@ -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"
|
||||
|
13
Cargo.toml
13
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"
|
||||
|
416
src/lib.rs
416
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<String, Error> {
|
||||
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<Self, Error>;
|
||||
|
||||
/// 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<Signature, Error>;
|
||||
}
|
||||
|
||||
/// 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<u8>,
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// A set of signatures created by a single homeserver.
|
||||
pub struct SignatureSet {
|
||||
set: HashSet<Signature>,
|
||||
}
|
||||
|
||||
/// 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<Self, Error> {
|
||||
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<Signature, Error> {
|
||||
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<T>(message: T) -> Self where T: Into<String> {
|
||||
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<Signature>,
|
||||
}
|
||||
|
||||
/// 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<Self, Error> {
|
||||
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<M>(&mut self, mut visitor: M) -> Result<Self::Value, M::Error>
|
||||
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::<String, String>()) {
|
||||
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<u8> = match value.from_base64() {
|
||||
let signature_bytes: Vec<u8> = 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<Signature, Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user