crypto: Add support for device keys signing.

master
Damir Jelić 2020-02-25 17:36:11 +01:00
parent 478f0d7784
commit 45890a27f3
5 changed files with 187 additions and 6 deletions

View File

@ -12,12 +12,12 @@ version = "0.1.0"
[features] [features]
default = [] default = []
encryption = ["olm-rs", "serde/derive", "serde_json"] encryption = ["olm-rs", "serde/derive", "serde_json", "cjson"]
[dependencies] [dependencies]
js_int = "0.1.2" js_int = "0.1.2"
futures = "0.3.4" futures = "0.3.4"
reqwest = "0.10.1" reqwest = "0.10.2"
http = "0.2.0" http = "0.2.0"
async-std = "1.5.0" async-std = "1.5.0"
ruma-api = "0.13.0" ruma-api = "0.13.0"
@ -30,7 +30,8 @@ url = "2.1.1"
olm-rs = { git = "https://gitlab.gnome.org/jhaye/olm-rs/", optional = true} olm-rs = { git = "https://gitlab.gnome.org/jhaye/olm-rs/", optional = true}
serde = { version = "1.0.104", optional = true, features = ["derive"] } serde = { version = "1.0.104", optional = true, features = ["derive"] }
serde_json = { version = "*", optional = true } serde_json = { version = "1.0.48", optional = true }
cjson = { version = "0.1.0", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "0.2.11", features = ["full"] } tokio = { version = "0.2.11", features = ["full"] }

47
src/crypto/error.rs Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use cjson::Error as CjsonError;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug)]
pub enum SignatureError {
NotAnObject,
NoSignatureFound,
CanonicalJsonError(CjsonError),
VerificationError,
}
impl Display for SignatureError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let message = match self {
SignatureError::NotAnObject => "The provided JSON value isn't an object.",
SignatureError::NoSignatureFound => {
"The provided JSON object doesn't contain a signatures field."
}
SignatureError::CanonicalJsonError(_) => {
"The provided JSON object can't be converted to a canonical representation."
}
SignatureError::VerificationError => "The signature didn't match the provided key.",
};
write!(f, "{}", message)
}
}
impl From<CjsonError> for SignatureError {
fn from(error: CjsonError) -> Self {
Self::CanonicalJsonError(error)
}
}

View File

@ -14,11 +14,17 @@
use std::convert::TryInto; use std::convert::TryInto;
use super::error::SignatureError;
use super::olm::Account; use super::olm::Account;
use crate::api; use crate::api;
use api::r0::keys; use api::r0::keys;
use cjson;
use olm_rs::utility::OlmUtility;
use serde_json::json;
use serde_json::value::Value;
struct OlmMachine { struct OlmMachine {
/// The unique user id that owns this account. /// The unique user id that owns this account.
user_id: String, user_id: String,
@ -34,6 +40,14 @@ struct OlmMachine {
} }
impl OlmMachine { impl OlmMachine {
const OLM_V1_ALGORITHM: &'static str = "m.olm.v1.curve25519-aes-sha2";
const MEGOLM_V1_ALGORITHM: &'static str = "m.megolm.v1.aes-sha2";
const ALGORITHMS: &'static [&'static str] = &[
OlmMachine::OLM_V1_ALGORITHM,
OlmMachine::MEGOLM_V1_ALGORITHM,
];
/// Create a new account. /// Create a new account.
pub fn new(user_id: &str, device_id: &str) -> Self { pub fn new(user_id: &str, device_id: &str) -> Self {
OlmMachine { OlmMachine {
@ -62,9 +76,9 @@ impl OlmMachine {
} }
} }
/// Receive a successfull keys upload response. /// Receive a successful keys upload response.
/// ///
/// # Arugments /// # Arguments
/// ///
/// `response` - The keys upload response of the request that the client /// `response` - The keys upload response of the request that the client
/// performed. /// performed.
@ -108,7 +122,104 @@ impl OlmMachine {
} }
} }
fn device_keys() -> () {} /// Sign the device keys and return a JSON Value to upload them.
fn device_keys(&self) -> Value {
let identity_keys = self.account.identity_keys();
let mut device_keys = json!({
"user_id": self.user_id,
"device_id": self.device_id,
"algorithms": OlmMachine::ALGORITHMS,
"keys": {
format!("curve25519:{}", self.device_id): identity_keys.curve25519(),
format!("ed25519:{}", self.device_id): identity_keys.ed25519(),
},
});
let signature = json!({
self.user_id.clone(): {
format!("ed25519:{}", self.device_id): self.sign_json(&device_keys),
}
});
let device_keys_object = device_keys
.as_object_mut()
.expect("Device keys json value isn't an object");
device_keys_object.insert("signatures".to_string(), signature);
device_keys
}
/// Convert a JSON value to the canonical representation and sign the JSON string.
fn sign_json(&self, json: &Value) -> String {
let canonical_json =
cjson::to_string(json).expect(&format!("Can't serialize {} to canonical JSON", json));
println!("HELLO SIGNING {}", canonical_json);
self.account.sign(&canonical_json)
}
/// Verify a signed JSON object.
///
/// The object must have a signatures key associated with an object of the
/// form `user_id: {key_id: signature}`.
///
/// # Arguments
///
/// * `user_id` - The user who signed the JSON object.
/// * `device_id` - The device that signed the JSON object.
/// * `user_key` - The public ed25519 key which was used to sign the JSON
/// object.
/// * `json` - The JSON object that should be verified.
///
/// Returns Ok if the signature was successfully verified, otherwise an
/// SignatureError.
fn verify_json(
&self,
user_id: &str,
device_id: &str,
user_key: &str,
json: &mut Value,
) -> Result<(), SignatureError> {
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
let unsigned = json_object.remove("unsigned");
let signatures = json_object.remove("signatures");
let canonical_json = cjson::to_string(json_object)?;
if let Some(u) = unsigned {
json_object.insert("unsigned".to_string(), u);
}
let key_id = format!("ed25519:{}", device_id);
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
let signature_object = signatures
.as_object()
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature_object
.get(user_id)
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature
.get(key_id)
.ok_or(SignatureError::NoSignatureFound)?;
let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?;
let utility = OlmUtility::new();
let ret = if utility
.ed25519_verify(&user_key, &canonical_json, signature)
.is_ok()
{
Ok(())
} else {
Err(SignatureError::VerificationError)
};
json_object.insert("signatures".to_string(), signatures);
ret
}
} }
#[cfg(test)] #[cfg(test)]
@ -194,4 +305,21 @@ mod test {
machine.receive_keys_upload_response(&response).await; machine.receive_keys_upload_response(&response).await;
assert!(machine.generate_one_time_keys().is_err()); assert!(machine.generate_one_time_keys().is_err());
} }
#[test]
fn test_device_key_signing() {
let machine = OlmMachine::new(USER_ID, DEVICE_ID);
let mut device_keys = machine.device_keys();
let identity_keys = machine.account.identity_keys();
let ed25519_key = identity_keys.ed25519();
let ret = machine.verify_json(
&machine.user_id,
&machine.device_id,
ed25519_key,
&mut device_keys,
);
assert!(ret.is_ok());
}
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// TODO remove this. // TODO remove this.
mod error;
#[allow(dead_code)] #[allow(dead_code)]
mod machine; mod machine;
#[allow(dead_code)] #[allow(dead_code)]

View File

@ -151,6 +151,10 @@ impl Account {
pub fn mark_keys_as_published(&self) { pub fn mark_keys_as_published(&self) {
self.inner.mark_keys_as_published(); self.inner.mark_keys_as_published();
} }
pub fn sign(&self, string: &str) -> String {
self.inner.sign(string)
}
} }
#[cfg(test)] #[cfg(test)]