diff --git a/Cargo.lock b/Cargo.lock index 6c3f03f1c..0f519bc84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,6 +374,7 @@ dependencies = [ "jsonrpc-core 2.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-minimal-sysui 0.1.0 (git+https://github.com/ethcore/parity-dapps-minimal-sysui-rs.git)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "ws 0.4.6 (git+https://github.com/ethcore/ws-rs.git)", ] @@ -940,7 +941,7 @@ dependencies = [ [[package]] name = "parity-minimal-sysui" version = "0.1.0" -source = "git+https://github.com/ethcore/parity-dapps-minimal-sysui-rs.git#bc5d76f9666ce19993e6f7b636a3a7af329ea19e" +source = "git+https://github.com/ethcore/parity-dapps-minimal-sysui-rs.git#cb27ae09ee18773ccca6ba2ac74fa3128047a652" [[package]] name = "phf" diff --git a/parity/cli.rs b/parity/cli.rs index bb67ee5c6..fbaa8bd89 100644 --- a/parity/cli.rs +++ b/parity/cli.rs @@ -26,6 +26,7 @@ Usage: parity account (new | list) [options] parity import [ ] [options] parity export [ ] [options] + parity signer new-token [options] parity [options] Protocol Options: @@ -100,9 +101,11 @@ API and Console Options: [default: $HOME/.parity/dapps] --signer Enable Trusted Signer WebSocket endpoint used by - System UIs. + Signer UIs. --signer-port PORT Specify the port of Trusted Signer server [default: 8180]. + --signer-path PATH Specify directory where Signer UIs tokens should + be stored. [default: $HOME/.parity/signer] Sealing/Mining Options: --force-sealing Force the node to author new blocks as if it were @@ -205,6 +208,8 @@ pub struct Args { pub cmd_list: bool, pub cmd_export: bool, pub cmd_import: bool, + pub cmd_signer: bool, + pub cmd_new_token: bool, pub arg_pid_file: String, pub arg_file: Option, pub flag_chain: String, @@ -244,6 +249,7 @@ pub struct Args { pub flag_dapps_path: String, pub flag_signer: bool, pub flag_signer_port: u16, + pub flag_signer_path: String, pub flag_force_sealing: bool, pub flag_author: String, pub flag_usd_per_tx: String, diff --git a/parity/configuration.rs b/parity/configuration.rs index 30d77c35d..66ca93316 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -41,6 +41,7 @@ pub struct Directories { pub keys: String, pub db: String, pub dapps: String, + pub signer: String, } impl Configuration { @@ -331,11 +332,15 @@ impl Configuration { ::std::fs::create_dir_all(&keys_path).unwrap_or_else(|e| die_with_io_error("main", e)); let dapps_path = Configuration::replace_home(&self.args.flag_dapps_path); ::std::fs::create_dir_all(&dapps_path).unwrap_or_else(|e| die_with_io_error("main", e)); + let signer_path = Configuration::replace_home(&self.args.flag_signer_path); + ::std::fs::create_dir_all(&signer_path).unwrap_or_else(|e| die_with_io_error("main", e)); + Directories { keys: keys_path, db: db_path, dapps: dapps_path, + signer: signer_path, } } diff --git a/parity/main.rs b/parity/main.rs index 74d14bfd6..016b20117 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -93,7 +93,7 @@ use informant::Informant; use die::*; use cli::print_version; use rpc::RpcServer; -use signer::SignerServer; +use signer::{SignerServer, new_token}; use dapps::WebappServer; use io_handler::ClientIoHandler; use configuration::Configuration; @@ -137,6 +137,11 @@ fn execute(conf: Configuration) { return; } + if conf.args.cmd_signer { + execute_signer(conf); + return; + } + execute_client(conf, spec, client_config); } @@ -241,6 +246,7 @@ fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig) let signer_server = signer::start(signer::Configuration { enabled: deps_for_rpc_apis.signer_enabled, port: conf.args.flag_signer_port, + signer_path: conf.directories().signer, }, signer::Dependencies { panic_handler: panic_handler.clone(), apis: deps_for_rpc_apis.clone(), @@ -439,6 +445,17 @@ fn execute_import(conf: Configuration) { client.flush_queue(); } +fn execute_signer(conf: Configuration) { + if !conf.args.cmd_new_token { + die!("Unknown command."); + } + + let path = conf.directories().signer; + new_token(path).unwrap_or_else(|e| { + die!("Error generating token: {:?}", e) + }); +} + fn execute_account_cli(conf: Configuration) { use util::keys::store::SecretStore; use rpassword::read_password; diff --git a/parity/signer.rs b/parity/signer.rs index a7de993fb..f8ff699df 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -14,21 +14,28 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +use std::io; +use std::path::PathBuf; use std::sync::Arc; use util::panics::{PanicHandler, ForwardPanic}; +use util::keys::directory::restrict_permissions_owner; use die::*; use rpc_apis; +const CODES_FILENAME: &'static str = "authcodes"; + #[cfg(feature = "ethcore-signer")] use ethcore_signer as signer; #[cfg(feature = "ethcore-signer")] pub use ethcore_signer::Server as SignerServer; + #[cfg(not(feature = "ethcore-signer"))] pub struct SignerServer; pub struct Configuration { pub enabled: bool, pub port: u16, + pub signer_path: String, } pub struct Dependencies { @@ -44,6 +51,25 @@ pub fn start(conf: Configuration, deps: Dependencies) -> Option { } } +fn codes_path(path: String) -> PathBuf { + let mut p = PathBuf::from(path); + p.push(CODES_FILENAME); + let _ = restrict_permissions_owner(&p); + p +} + + +#[cfg(feature = "ethcore-signer")] +pub fn new_token(path: String) -> io::Result<()> { + let path = codes_path(path); + let mut codes = try!(signer::AuthCodes::from_file(&path)); + let code = try!(codes.generate_new()); + try!(codes.to_file(&path)); + println!("New token has been generated. Copy the code below to your Signer UI:"); + println!("{}", code); + Ok(()) +} + #[cfg(feature = "ethcore-signer")] fn do_start(conf: Configuration, deps: Dependencies) -> SignerServer { let addr = format!("127.0.0.1:{}", conf.port).parse().unwrap_or_else(|_| { @@ -51,7 +77,10 @@ fn do_start(conf: Configuration, deps: Dependencies) -> SignerServer { }); let start_result = { - let server = signer::ServerBuilder::new(deps.apis.signer_queue.clone()); + let server = signer::ServerBuilder::new( + deps.apis.signer_queue.clone(), + codes_path(conf.signer_path), + ); let server = rpc_apis::setup_rpc(server, deps.apis, rpc_apis::ApiSet::SafeContext); server.start(addr) }; @@ -67,8 +96,12 @@ fn do_start(conf: Configuration, deps: Dependencies) -> SignerServer { } #[cfg(not(feature = "ethcore-signer"))] -fn do_start(conf: Configuration) -> ! { +fn do_start(_conf: Configuration) -> ! { die!("Your Parity version has been compiled without Trusted Signer support.") } +#[cfg(not(feature = "ethcore-signer"))] +pub fn new_token(_path: String) -> ! { + die!("Your Parity version has been compiled without Trusted Signer support.") +} diff --git a/rpc/src/v1/types/transaction_request.rs b/rpc/src/v1/types/transaction_request.rs index 93d6a479b..e7237b2c6 100644 --- a/rpc/src/v1/types/transaction_request.rs +++ b/rpc/src/v1/types/transaction_request.rs @@ -49,7 +49,7 @@ pub struct TransactionConfirmation { pub transaction: TransactionRequest, } -/// Possible modifications to the confirmed transaction sent by SystemUI +/// Possible modifications to the confirmed transaction sent by `SignerUI` #[derive(Debug, PartialEq, Deserialize)] pub struct TransactionModification { /// Modified gas price diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 22bc58e20..ae5f4b42a 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -11,6 +11,7 @@ build = "build.rs" rustc_version = "0.1" [dependencies] +rand = "0.3.14" jsonrpc-core = "2.0" log = "0.3" env_logger = "0.3" diff --git a/signer/src/authcode_store.rs b/signer/src/authcode_store.rs new file mode 100644 index 000000000..92e86a73e --- /dev/null +++ b/signer/src/authcode_store.rs @@ -0,0 +1,187 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use rand::Rng; +use rand::os::OsRng; +use std::io; +use std::io::{Read, Write}; +use std::fs; +use std::path::Path; +use std::time; +use util::{H256, Hashable}; + +/// Providing current time in seconds +pub trait TimeProvider { + /// Returns timestamp (in seconds since epoch) + fn now(&self) -> u64; +} + +impl u64> TimeProvider for F { + fn now(&self) -> u64 { + self() + } +} + +/// Default implementation of `TimeProvider` using system time. +#[derive(Default)] +pub struct DefaultTimeProvider; + +impl TimeProvider for DefaultTimeProvider { + fn now(&self) -> u64 { + time::UNIX_EPOCH.elapsed().expect("Valid time has to be set in your system.").as_secs() + } +} + +/// No of seconds the hash is valid +const TIME_THRESHOLD: u64 = 2; +const TOKEN_LENGTH: usize = 16; + +/// Manages authorization codes for `SignerUIs` +pub struct AuthCodes { + codes: Vec, + now: T, +} + +impl AuthCodes { + + /// Reads `AuthCodes` from file and creates new instance using `DefaultTimeProvider`. + pub fn from_file(file: &Path) -> io::Result { + let content = { + if let Ok(mut file) = fs::File::open(file) { + let mut s = String::new(); + let _ = try!(file.read_to_string(&mut s)); + s + } else { + "".into() + } + }; + let codes = content.lines() + .filter(|f| f.len() >= TOKEN_LENGTH) + .map(String::from) + .collect(); + Ok(AuthCodes { + codes: codes, + now: DefaultTimeProvider::default(), + }) + } + +} + +impl AuthCodes { + + /// Writes all `AuthCodes` to a disk. + pub fn to_file(&self, file: &Path) -> io::Result<()> { + let mut file = try!(fs::File::create(file)); + let content = self.codes.join("\n"); + file.write_all(content.as_bytes()) + } + + /// Creates a new `AuthCodes` store with given `TimeProvider`. + pub fn new(codes: Vec, now: T) -> Self { + AuthCodes { + codes: codes, + now: now, + } + } + + /// Checks if given hash is correct identifier of `SignerUI` + pub fn is_valid(&self, hash: &H256, time: u64) -> bool { + let now = self.now.now(); + // check time + if time >= now + TIME_THRESHOLD || time <= now - TIME_THRESHOLD { + warn!(target: "signer", "Received old authentication request."); + return false; + } + + // look for code + self.codes.iter() + .any(|code| &format!("{}:{}", code, time).sha3() == hash) + } + + /// Generates and returns a new code that can be used by `SignerUIs` + pub fn generate_new(&mut self) -> io::Result { + let mut rng = try!(OsRng::new()); + let code = rng.gen_ascii_chars().take(TOKEN_LENGTH).collect::(); + let readable_code = code.as_bytes() + .chunks(4) + .filter_map(|f| String::from_utf8(f.to_vec()).ok()) + .collect::>() + .join("-"); + info!(target: "signer", "New authentication token generated."); + self.codes.push(code); + Ok(readable_code) + } +} + + +#[cfg(test)] +mod tests { + + use util::{H256, Hashable}; + use super::*; + + fn generate_hash(val: &str, time: u64) -> H256 { + format!("{}:{}", val, time).sha3() + } + + #[test] + fn should_return_true_if_hash_is_valid() { + // given + let code = "23521352asdfasdfadf"; + let time = 99; + let codes = AuthCodes::new(vec![code.into()], || 100); + + // when + let res = codes.is_valid(&generate_hash(code, time), time); + + // then + assert_eq!(res, true); + } + + #[test] + fn should_return_false_if_code_is_unknown() { + // given + let code = "23521352asdfasdfadf"; + let time = 99; + let codes = AuthCodes::new(vec!["1".into()], || 100); + + // when + let res = codes.is_valid(&generate_hash(code, time), time); + + // then + assert_eq!(res, false); + } + + #[test] + fn should_return_false_if_hash_is_valid_but_time_is_invalid() { + // given + let code = "23521352asdfasdfadf"; + let time = 105; + let time2 = 95; + let codes = AuthCodes::new(vec![code.into()], || 100); + + // when + let res1 = codes.is_valid(&generate_hash(code, time), time); + let res2 = codes.is_valid(&generate_hash(code, time2), time2); + + // then + assert_eq!(res1, false); + assert_eq!(res2, false); + } + +} + + diff --git a/signer/src/lib.rs b/signer/src/lib.rs index fb3e76cca..3aaed8bcf 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -23,8 +23,8 @@ //! This module manages your private keys and accounts/identities //! that can be used within Dapps. //! -//! It exposes API (over `WebSockets`) accessed by System UIs. -//! Each transaction sent by Dapp is broadcasted to System UIs +//! It exposes API (over `WebSockets`) accessed by Signer UIs. +//! Each transaction sent by Dapp is broadcasted to Signer UIs //! and their responsibility is to confirm (or confirm and sign) //! the transaction for you. //! @@ -38,13 +38,14 @@ //! //! fn main() { //! let queue = Arc::new(ConfirmationsQueue::default()); -//! let _server = ServerBuilder::new(queue).start("127.0.0.1:8084".parse().unwrap()); +//! let _server = ServerBuilder::new(queue, "/tmp/authcodes".into()).start("127.0.0.1:8084".parse().unwrap()); //! } //! ``` #[macro_use] extern crate log; extern crate env_logger; +extern crate rand; extern crate ethcore_util as util; extern crate ethcore_rpc as rpc; @@ -52,7 +53,10 @@ extern crate jsonrpc_core; extern crate ws; extern crate parity_minimal_sysui as sysui; +mod authcode_store; mod ws_server; + +pub use authcode_store::*; pub use ws_server::*; #[cfg(test)] diff --git a/signer/src/ws_server/mod.rs b/signer/src/ws_server/mod.rs index 3b4924230..76c0fbb16 100644 --- a/signer/src/ws_server/mod.rs +++ b/signer/src/ws_server/mod.rs @@ -19,6 +19,7 @@ use ws; use std; use std::thread; +use std::path::PathBuf; use std::default::Default; use std::ops::Drop; use std::sync::Arc; @@ -51,6 +52,7 @@ impl From for ServerError { pub struct ServerBuilder { queue: Arc, handler: Arc, + authcodes_path: PathBuf, } impl Extendable for ServerBuilder { @@ -61,17 +63,18 @@ impl Extendable for ServerBuilder { impl ServerBuilder { /// Creates new `ServerBuilder` - pub fn new(queue: Arc) -> Self { + pub fn new(queue: Arc, authcodes_path: PathBuf) -> Self { ServerBuilder { queue: queue, handler: Arc::new(IoHandler::new()), + authcodes_path: authcodes_path, } } /// Starts a new `WebSocket` server in separate thread. /// Returns a `Server` handle which closes the server when droped. pub fn start(self, addr: SocketAddr) -> Result { - Server::start(addr, self.handler, self.queue) + Server::start(addr, self.handler, self.queue, self.authcodes_path) } } @@ -86,7 +89,7 @@ pub struct Server { impl Server { /// Starts a new `WebSocket` server in separate thread. /// Returns a `Server` handle which closes the server when droped. - fn start(addr: SocketAddr, handler: Arc, queue: Arc) -> Result { + fn start(addr: SocketAddr, handler: Arc, queue: Arc, authcodes_path: PathBuf) -> Result { let config = { let mut config = ws::Settings::default(); config.max_connections = 10; @@ -96,7 +99,7 @@ impl Server { // Create WebSocket let origin = format!("{}", addr); - let ws = try!(ws::Builder::new().with_settings(config).build(session::Factory::new(handler, origin))); + let ws = try!(ws::Builder::new().with_settings(config).build(session::Factory::new(handler, origin, authcodes_path))); let panic_handler = PanicHandler::new_in_arc(); let ph = panic_handler.clone(); diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index 54be63eaf..153bf6622 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -18,8 +18,12 @@ use ws; use sysui; +use authcode_store::AuthCodes; +use std::path::{PathBuf, Path}; use std::sync::Arc; +use std::str::FromStr; use jsonrpc_core::IoHandler; +use util::H256; fn origin_is_allowed(self_origin: &str, header: Option<&Vec>) -> bool { match header { @@ -36,13 +40,32 @@ fn origin_is_allowed(self_origin: &str, header: Option<&Vec>) -> bool { } } -fn auth_is_valid(_header: Option<&Vec>) -> bool { - true +fn auth_is_valid(codes: &Path, protocols: ws::Result>) -> bool { + match protocols { + Ok(ref protocols) if protocols.len() == 1 => { + protocols.iter().any(|protocol| { + let mut split = protocol.split('_'); + let auth = split.next().and_then(|v| H256::from_str(v).ok()); + let time = split.next().and_then(|v| u64::from_str_radix(v, 10).ok()); + + if let (Some(auth), Some(time)) = (auth, time) { + // Check if the code is valid + AuthCodes::from_file(codes) + .map(|codes| codes.is_valid(&auth, time)) + .unwrap_or(false) + } else { + false + } + }) + }, + _ => false + } } pub struct Session { out: ws::Sender, self_origin: String, + authcodes_path: PathBuf, handler: Arc, } @@ -53,17 +76,25 @@ impl ws::Handler for Session { // Check request origin and host header. if !origin_is_allowed(&self.self_origin, origin) && !origin_is_allowed(&self.self_origin, host) { + warn!(target: "signer", "Blocked connection to Signer API from untrusted origin."); return Ok(ws::Response::forbidden("You are not allowed to access system ui.".into())); } - // Check authorization - if !auth_is_valid(req.header("authorization")) { - return Ok(ws::Response::forbidden("You are not authorized.".into())); - } - // Detect if it's a websocket request. if req.header("sec-websocket-key").is_some() { - return ws::Response::from_request(req); + // Check authorization + if !auth_is_valid(&self.authcodes_path, req.protocols()) { + info!(target: "signer", "Unauthorized connection to Signer API blocked."); + return Ok(ws::Response::forbidden("You are not authorized.".into())); + } + + let protocols = req.protocols().expect("Existence checked by authorization."); + let protocol = protocols.get(0).expect("Proved by authorization."); + return ws::Response::from_request(req).map(|mut res| { + // To make WebSockets connection successful we need to send back the protocol header. + res.set_protocol(protocol); + res + }); } // Otherwise try to serve a page. @@ -101,13 +132,15 @@ impl ws::Handler for Session { pub struct Factory { handler: Arc, self_origin: String, + authcodes_path: PathBuf, } impl Factory { - pub fn new(handler: Arc, self_origin: String) -> Self { + pub fn new(handler: Arc, self_origin: String, authcodes_path: PathBuf) -> Self { Factory { handler: handler, self_origin: self_origin, + authcodes_path: authcodes_path, } } } @@ -118,8 +151,9 @@ impl ws::Factory for Factory { fn connection_made(&mut self, sender: ws::Sender) -> Self::Handler { Session { out: sender, - self_origin: self.self_origin.clone(), handler: self.handler.clone(), + self_origin: self.self_origin.clone(), + authcodes_path: self.authcodes_path.clone(), } } } diff --git a/util/src/keys/directory.rs b/util/src/keys/directory.rs index 3f4100163..20be7df7b 100644 --- a/util/src/keys/directory.rs +++ b/util/src/keys/directory.rs @@ -465,7 +465,8 @@ pub struct KeyDirectory { cache_usage: RwLock>, } -fn restrict_permissions_owner(file_path: &Path) -> Result<(), i32> { +/// Restricts the permissions of given path only to the owner. +pub fn restrict_permissions_owner(file_path: &Path) -> Result<(), i32> { let cstr = ::std::ffi::CString::new(file_path.to_str().unwrap()).unwrap(); match unsafe { ::libc::chmod(cstr.as_ptr(), ::libc::S_IWUSR | ::libc::S_IRUSR) } { 0 => Ok(()),