From 4a6cae0fe6296fe6bfd18835f41a2de794dca3fb Mon Sep 17 00:00:00 2001 From: hashmap Date: Tue, 2 Oct 2018 09:49:36 +0200 Subject: [PATCH] Allow TLS for Wallet APIs (#1626) * Allow TLS for Wallet APIs This PR adds an optional support of TLS for wallet APIs. Only PKCS12 format is supported, will address .pem support in next PR and provide some documentation. Address #1425 --- api/src/handlers.rs | 3 ++- api/src/rest.rs | 38 +++++++++++++++++++++++++++--- api/tests/rest.rs | 4 ++-- config/src/comments.rs | 8 +++++++ servers/src/grin/server.rs | 1 + servers/tests/framework/mod.rs | 1 + servers/tests/simulnet.rs | 4 ++-- src/bin/cmd/wallet.rs | 33 +++++++++++++++++--------- wallet/src/libwallet/controller.rs | 29 +++++++++++++---------- wallet/src/types.rs | 6 +++++ 10 files changed, 96 insertions(+), 31 deletions(-) diff --git a/api/src/handlers.rs b/api/src/handlers.rs index 854c8f1d8..33fdd7d61 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -797,6 +797,7 @@ pub fn start_rest_apis( tx_pool: Weak>, peers: Weak, api_secret: Option, + tls_config: Option, ) -> bool { let mut apis = ApiServer::new(); let mut router = build_router(chain, tx_pool, peers).expect("unable to build API router"); @@ -810,7 +811,7 @@ pub fn start_rest_apis( info!(LOGGER, "Starting HTTP API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); - apis.start(socket_addr, router).is_ok() + apis.start(socket_addr, router, tls_config).is_ok() } pub fn build_router( diff --git a/api/src/rest.rs b/api/src/rest.rs index 38cdd9d93..9d440a4ef 100644 --- a/api/src/rest.rs +++ b/api/src/rest.rs @@ -18,7 +18,7 @@ //! To use it, just have your service(s) implement the ApiEndpoint trait and //! register them on a ApiServer. -use failure::{Backtrace, Context, Fail}; +use failure::{Backtrace, Context, Fail, ResultExt}; use futures::sync::oneshot; use futures::Stream; use hyper::rt::Future; @@ -27,6 +27,8 @@ use hyper::{rt, Body, Request, Server}; use native_tls::{Identity, TlsAcceptor}; use router::{Handler, HandlerObj, ResponseFuture, Router}; use std::fmt::{self, Display}; +use std::fs::File; +use std::io::Read; use std::net::SocketAddr; use std::{io, thread}; use tokio::net::TcpListener; @@ -95,6 +97,22 @@ pub struct TLSConfig { pub pass: String, } +impl TLSConfig { + pub fn new(pass: String, file: String) -> Result { + let mut f = File::open(&file).context(ErrorKind::Internal(format!( + "can't open TLS certifiacte file {}", + file + )))?; + let mut pkcs_bytes = Vec::new(); + f.read_to_end(&mut pkcs_bytes) + .context(ErrorKind::Internal(format!( + "can't read TLS certifiacte file {}", + file + )))?; + Ok(TLSConfig { pkcs_bytes, pass }) + } +} + /// HTTP server allowing the registration of ApiEndpoint implementations. pub struct ApiServer { shutdown_sender: Option>, @@ -109,11 +127,25 @@ impl ApiServer { } } - /// Starts the ApiServer at the provided address. + /// Starts ApiServer at the provided address. + /// TODO support stop operation pub fn start( &mut self, addr: SocketAddr, router: Router, + conf: Option, + ) -> Result, Error> { + match conf { + Some(conf) => self.start_tls(addr, router, conf), + None => self.start_no_tls(addr, router), + } + } + + /// Starts the ApiServer at the provided address. + fn start_no_tls( + &mut self, + addr: SocketAddr, + router: Router, ) -> Result, Error> { if self.shutdown_sender.is_some() { return Err(ErrorKind::Internal( @@ -137,7 +169,7 @@ impl ApiServer { /// Starts the TLS ApiServer at the provided address. /// TODO support stop operation - pub fn start_tls( + fn start_tls( &mut self, addr: SocketAddr, router: Router, diff --git a/api/tests/rest.rs b/api/tests/rest.rs index ccc8579bb..c17495198 100644 --- a/api/tests/rest.rs +++ b/api/tests/rest.rs @@ -69,7 +69,7 @@ fn test_start_api() { router.add_middleware(counter.clone()); let server_addr = "127.0.0.1:14434"; let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); - assert!(server.start(addr, router).is_ok()); + assert!(server.start(addr, router, None).is_ok()); let url = format!("http://{}/v1/", server_addr); let index = api::client::get::>(url.as_str(), None).unwrap(); assert_eq!(index.len(), 2); @@ -94,7 +94,7 @@ fn test_start_api_tls() { let router = build_router(); let server_addr = "127.0.0.1:14444"; let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); - assert!(server.start_tls(addr, router, tls_conf).is_ok()); + assert!(server.start(addr, router, Some(tls_conf)).is_ok()); let url = format!("https://{}/v1/", server_addr); let index = api::client::get::>(url.as_str(), None).unwrap(); assert_eq!(index.len(), 2); diff --git a/config/src/comments.rs b/config/src/comments.rs index 2158d98da..a5f31cbaf 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -310,6 +310,14 @@ fn comments() -> HashMap { "api_listen_port".to_string(), " #port for wallet listener + +#path of TLS certificate file (PKCS#12 format is supported) +#self-signed certificates are not supported, use https://github.com/FiloSottile/mkcert +#to test on localhost +#tls_certificate_file = \"\" +#password of TLS certificate file (PKCS#12 format is supported) +#tls_certificate_pass = \"\" + ".to_string(), ); diff --git a/servers/src/grin/server.rs b/servers/src/grin/server.rs index cc62de1ca..c08c20ce9 100644 --- a/servers/src/grin/server.rs +++ b/servers/src/grin/server.rs @@ -263,6 +263,7 @@ impl Server { Arc::downgrade(&tx_pool), Arc::downgrade(&p2p_server.peers), api_secret, + None, ); info!( diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index f49040f30..7c3253cc9 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -280,6 +280,7 @@ impl LocalServerContainer { wallet::controller::foreign_listener( Box::new(wallet), &self.wallet_config.api_listen_addr(), + None, ).unwrap_or_else(|e| { panic!( "Error creating wallet listener: {:?} Config: {:?}", diff --git a/servers/tests/simulnet.rs b/servers/tests/simulnet.rs index 03f2130ae..a800c81df 100644 --- a/servers/tests/simulnet.rs +++ b/servers/tests/simulnet.rs @@ -426,7 +426,7 @@ fn replicate_tx_fluff_failure() { let client1 = HTTPWalletClient::new("http://127.0.0.1:23003", None); let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); let wallet1_handle = thread::spawn(move || { - controller::foreign_listener(wallet1, "127.0.0.1:33000") + controller::foreign_listener(wallet1, "127.0.0.1:33000", None) .unwrap_or_else(|e| panic!("Error creating wallet1 listener: {:?}", e,)); }); @@ -434,7 +434,7 @@ fn replicate_tx_fluff_failure() { let client2 = HTTPWalletClient::new("http://127.0.0.1:23001", None); let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); let wallet2_handle = thread::spawn(move || { - controller::foreign_listener(wallet2, "127.0.0.1:33001") + controller::foreign_listener(wallet2, "127.0.0.1:33001", None) .unwrap_or_else(|e| panic!("Error creating wallet2 listener: {:?}", e,)); }); diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 437706299..ac34ad327 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -24,6 +24,7 @@ use std::time::Duration; use clap::ArgMatches; +use api::TLSConfig; use config::GlobalWalletConfig; use core::{core, global}; use grin_wallet::{self, controller, display, libwallet}; @@ -134,12 +135,24 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { let wallet = instantiate_wallet(wallet_config.clone(), passphrase, node_api_secret.clone()); let api_secret = get_first_line(wallet_config.api_secret_path.clone()); + let tls_conf = match wallet_config.tls_certificate_file.clone() { + None => None, + Some(file) => Some( + TLSConfig::new( + wallet_config + .tls_certificate_pass + .clone() + .expect("tls_certificate_pass must be set"), + file, + ).expect("failed to configure TLS"), + ), + }; match wallet_args.subcommand() { ("listen", Some(listen_args)) => { if let Some(port) = listen_args.value_of("port") { wallet_config.api_listen_port = port.parse().unwrap(); } - controller::foreign_listener(wallet, &wallet_config.api_listen_addr()) + controller::foreign_listener(wallet, &wallet_config.api_listen_addr(), tls_conf) .unwrap_or_else(|e| { panic!( "Error creating wallet listener: {:?} Config: {:?}", @@ -148,26 +161,24 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { }); } ("owner_api", Some(_api_args)) => { - controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( - |e| { + controller::owner_listener(wallet, "127.0.0.1:13420", api_secret, tls_conf) + .unwrap_or_else(|e| { panic!( "Error creating wallet api listener: {:?} Config: {:?}", e, wallet_config ) - }, - ); + }); } ("web", Some(_api_args)) => { // start owner listener and run static file server start_webwallet_server(); - controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( - |e| { + controller::owner_listener(wallet, "127.0.0.1:13420", api_secret, tls_conf) + .unwrap_or_else(|e| { panic!( "Error creating wallet api listener: {:?} Config: {:?}", e, wallet_config ) - }, - ); + }); } _ => {} }; @@ -209,7 +220,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { let fluff = send_args.is_present("fluff"); let max_outputs = 500; if method == "http" { - if dest.starts_with("http://") { + if dest.starts_with("http://") || dest.starts_with("https://") { let result = api.issue_send_tx( amount, minimum_confirmations, @@ -258,7 +269,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { } else { error!( LOGGER, - "HTTP Destination should start with http://: {}", dest + "HTTP Destination should start with http://: or https://: {}", dest ); panic!(); } diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index 080aad4cc..e3013f398 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -15,7 +15,7 @@ //! Controller for wallet.. instantiates and handles listeners (or single-run //! invocations) as needed. //! Still experimental -use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router}; +use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig}; use core::core::Transaction; use failure::ResultExt; use futures::future::{err, ok}; @@ -70,6 +70,7 @@ pub fn owner_listener( wallet: Box, addr: &str, api_secret: Option, + tls_config: Option, ) -> Result<(), Error> where T: WalletBackend + Send + Sync + 'static, @@ -95,11 +96,11 @@ where let mut apis = ApiServer::new(); info!(LOGGER, "Starting HTTP Owner API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); - let api_thread = apis - .start(socket_addr, router) - .context(ErrorKind::GenericError( - "API thread failed to start".to_string(), - ))?; + let api_thread = + apis.start(socket_addr, router, tls_config) + .context(ErrorKind::GenericError( + "API thread failed to start".to_string(), + ))?; api_thread .join() .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) @@ -107,7 +108,11 @@ where /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls -pub fn foreign_listener(wallet: Box, addr: &str) -> Result<(), Error> +pub fn foreign_listener( + wallet: Box, + addr: &str, + tls_config: Option, +) -> Result<(), Error> where T: WalletBackend + Send + Sync + 'static, C: WalletClient + 'static, @@ -123,11 +128,11 @@ where let mut apis = ApiServer::new(); info!(LOGGER, "Starting HTTP Foreign API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); - let api_thread = apis - .start(socket_addr, router) - .context(ErrorKind::GenericError( - "API thread failed to start".to_string(), - ))?; + let api_thread = + apis.start(socket_addr, router, tls_config) + .context(ErrorKind::GenericError( + "API thread failed to start".to_string(), + ))?; api_thread .join() diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 6d55be07e..8bbc97119 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -48,6 +48,10 @@ pub struct WalletConfig { pub check_node_api_http_addr: String, // The directory in which wallet files are stored pub data_file_dir: String, + /// TLS ceritificate file + pub tls_certificate_file: Option, + /// TLS ceritificate password + pub tls_certificate_pass: Option, } impl Default for WalletConfig { @@ -60,6 +64,8 @@ impl Default for WalletConfig { node_api_secret_path: Some(".api_secret".to_string()), check_node_api_http_addr: "http://127.0.0.1:13413".to_string(), data_file_dir: ".".to_string(), + tls_certificate_file: None, + tls_certificate_pass: None, } } }