diff --git a/.gitignore b/.gitignore index c5348e7f..f592b451 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ node_modules # Foxbox config foxbox.conf + +# Generated certs +certs diff --git a/.travis.yml b/.travis.yml index bbb9622c..60211e0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,4 +33,4 @@ script: - $TRAVIS_BUILD_DIR/tools/execute-unit-tests-with-coverage - jshint static/main/js/*.js static/setup/js/*.js test/selenium/*.js test/integration/lib/*.js test/integration/test/*.js - npm run test-integration # starts foxbox and kills it within the test script - - (cargo run -- -l $BOX_LOCAL_NAME -p $BOX_PORT &> /dev/null &) ; npm run test-selenium + - (cargo run -- -l $BOX_LOCAL_NAME -p $BOX_PORT --disable-tls &> /dev/null &) ; npm run test-selenium diff --git a/Cargo.lock b/Cargo.lock index b2d6eed8..a2f5203c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,8 +21,10 @@ dependencies = [ "mount 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "multicast_dns 0.1.0 (git+https://github.com/fxbox/multicast-dns.git?rev=a6e4bcc)", "nix 0.5.1-pre (git+https://github.com/nix-rust/nix.git?rev=138080)", + "openssl 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.58 (registry+https://github.com/rust-lang/crates.io-index)", "router 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", @@ -34,7 +36,7 @@ dependencies = [ "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", "timer 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "transformable_channels 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", "ws 0.4.5 (git+https://github.com/housleyjk/ws-rs.git?rev=d154fc5)", @@ -152,7 +154,7 @@ name = "docopt" version = "0.6.78" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.58 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -171,7 +173,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.58 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -223,7 +225,7 @@ dependencies = [ "rusqlite 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "urlencoded 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -237,7 +239,7 @@ name = "gdi32-sys" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -280,7 +282,7 @@ dependencies = [ "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -312,7 +314,7 @@ version = "0.1.0" source = "git+https://github.com/fxbox/iron-cors.git?rev=137d42f#137d42f86524f1a16873eba1c308cfa2a45b0bd9" dependencies = [ "iron 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -341,7 +343,7 @@ name = "kernel32-sys" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -421,11 +423,11 @@ dependencies = [ "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.5.0-pre (git+https://github.com/carllerche/nix-rust?rev=c4257f8a76)", "slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -434,8 +436,8 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -464,13 +466,13 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -625,12 +627,12 @@ dependencies = [ [[package]] name = "regex" -version = "0.1.55" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -639,6 +641,11 @@ name = "regex-syntax" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "regex-syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "route-recognizer" version = "0.1.11" @@ -827,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -868,7 +875,7 @@ dependencies = [ [[package]] name = "unicase" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -941,7 +948,7 @@ name = "user32-sys" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -966,7 +973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "winapi" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -993,7 +1000,7 @@ name = "ws2_32-sys" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 64489ee8..3f5aac43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,13 +20,14 @@ foxbox_users = { git = "https://github.com/fxbox/users.git", rev = "3db32f0" } get_if_addrs = "0.3.1" hyper = "0.7.2" multicast_dns = { git = "https://github.com/fxbox/multicast-dns.git", rev = "a6e4bcc" } -iron = "0.2.6" iron-cors = { git = "https://github.com/fxbox/iron-cors.git", rev = "137d42f" } libc = "0.2.7" log = "0.3" mio = { git = "https://github.com/carllerche/mio.git" } mount = "0.0.10" nix = { git = "https://github.com/nix-rust/nix.git", rev = "138080" } # Until 0.5.1 is released +openssl = "0.7.6" +openssl-sys = "0.7.6" router = "0.1.0" rust-crypto = "0.2.34" rustc-serialize = "0.3" @@ -39,10 +40,15 @@ unicase = "1.3.0" time = "0.1" timer = "0.1.6" uuid = "0.1.18" -url = "0.5.5" +url = "0.5.7" ws = { git = "https://github.com/housleyjk/ws-rs.git", rev = "d154fc5" } xml-rs = "0.3.0" +[dependencies.iron] +version = "0.2.6" +default-features = true +features = ["ssl"] + [dev-dependencies] stainless = "0.1.4" iron-test = "0.2.0" diff --git a/iron-fork/src/iron.rs b/iron-fork/src/iron.rs index 96678c3d..81ed82ad 100644 --- a/iron-fork/src/iron.rs +++ b/iron-fork/src/iron.rs @@ -8,7 +8,13 @@ use std::path::PathBuf; pub use hyper::server::Listening; use hyper::server::Server; -use hyper::net::Fresh; +use hyper::net::{Fresh, NetworkListener, HttpListener}; + + +#[cfg(feature = "ssl")] +use hyper::net::{HttpsListener,NetworkStream,Ssl, Openssl}; + +use hyper::error::Error; use request::HttpRequest; use response::HttpResponse; @@ -65,6 +71,59 @@ impl Default for Timeouts { } } +/// Create a Hyper Server for a protocol +pub trait ServerFactory { + /// Get the protocol this Server will use + fn protocol(&self) -> Protocol; + + /// Create the hyper::server::Server + fn create_server(&self, sock_addr: SocketAddr) -> Result, Error>; +} + +/// Default HTTP Server Factory +pub struct HttpServerFactory; + +impl ServerFactory for HttpServerFactory { + fn protocol(&self) -> Protocol { + Protocol::Http + } + + fn create_server(&self, sock_addr: SocketAddr) -> Result { + Server::http(sock_addr) + } +} + + +#[cfg(feature="ssl")] +/// HttpsServerFactory +pub struct HttpsServerFactory { + ssl: S +} + +#[cfg(feature="ssl")] +impl HttpsServerFactory { + /// Create a new HttpsServerFactory from an hyper::net::Openssl context + pub fn new(ssl: Openssl) -> HttpsServerFactory { + HttpsServerFactory { + ssl: ssl + } + } +} + +#[cfg(feature="ssl")] +impl ServerFactory> for HttpsServerFactory { + + fn protocol(&self) -> Protocol { + Protocol::Https + } + + fn create_server(&self, sock_addr: SocketAddr) -> Result>, Error> { + Server::https(sock_addr, self.ssl.clone()) + } +} + + + /// Protocol used to serve content. Future versions of Iron may add new protocols /// to this enum. Thus you should not exhaustively match on its variants. #[derive(Clone)] @@ -73,12 +132,7 @@ pub enum Protocol { Http, /// HTTP/1 over SSL/TLS #[cfg(feature = "ssl")] - Https { - /// Path to SSL certificate file - certificate: PathBuf, - /// Path to SSL private key file - key: PathBuf - } + Https } impl Protocol { @@ -87,7 +141,7 @@ impl Protocol { match *self { Protocol::Http => "http", #[cfg(feature = "ssl")] - Protocol::Https { .. } => "https" + Protocol::Https => "https" } } } @@ -109,7 +163,8 @@ impl Iron { /// Panics if the provided address does not parse. To avoid this /// call `to_socket_addrs` yourself and pass a parsed `SocketAddr`. pub fn http(self, addr: A) -> HttpResult { - self.listen_with(addr, 8 * ::num_cpus::get(), Protocol::Http, None) + let http = &HttpServerFactory; + self.listen_with(addr, 8 * ::num_cpus::get(), http, None) } /// Kick off the server process using the HTTPS protocol. @@ -130,8 +185,10 @@ impl Iron { #[cfg(feature = "ssl")] pub fn https(self, addr: A, certificate: PathBuf, key: PathBuf) -> HttpResult { - self.listen_with(addr, 8 * ::num_cpus::get(), - Protocol::Https { certificate: certificate, key: key }, None) + + let ssl = try!(Openssl::with_cert_and_key(certificate, key)); + + self.listen_with(addr, 8 * ::num_cpus::get(), &HttpsServerFactory::new(ssl), None) } /// Kick off the server process with X threads. @@ -140,38 +197,21 @@ impl Iron { /// /// Panics if the provided address does not parse. To avoid this /// call `to_socket_addrs` yourself and pass a parsed `SocketAddr`. - pub fn listen_with(mut self, addr: A, threads: usize, - protocol: Protocol, + pub fn listen_with(mut self, addr: A, threads: usize, + server_factory: &ServerFactory, timeouts: Option) -> HttpResult { let sock_addr = addr.to_socket_addrs() .ok().and_then(|mut addrs| addrs.next()).expect("Could not parse socket address."); self.addr = Some(sock_addr); - self.protocol = Some(protocol.clone()); - - match protocol { - Protocol::Http => { - let mut server = try!(Server::http(sock_addr)); - let timeouts = timeouts.unwrap_or_default(); - server.keep_alive(timeouts.keep_alive); - server.set_read_timeout(timeouts.read); - server.set_write_timeout(timeouts.write); - server.handle_threads(self, threads) - }, + self.protocol = Some(server_factory.protocol()); - #[cfg(feature = "ssl")] - Protocol::Https { ref certificate, ref key } => { - use hyper::net::Openssl; - - let ssl = try!(Openssl::with_cert_and_key(certificate, key)); - let mut server = try!(Server::https(sock_addr, ssl)); - let timeouts = timeouts.unwrap_or_default(); - server.keep_alive(timeouts.keep_alive); - server.set_read_timeout(timeouts.read); - server.set_write_timeout(timeouts.write); - server.handle_threads(self, threads) - } - } + let mut server = try!(server_factory.create_server(sock_addr)); + let timeouts = timeouts.unwrap_or_default(); + server.keep_alive(timeouts.keep_alive); + server.set_read_timeout(timeouts.read); + server.set_write_timeout(timeouts.write); + server.handle_threads(self, threads) } /// Instantiate a new instance of `Iron`. diff --git a/src/controller.rs b/src/controller.rs index bdc254b8..735c46af 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -24,12 +24,16 @@ use std::net::ToSocketAddrs; use std::sync::{ Arc, Mutex }; use std::sync::atomic::{ AtomicBool, Ordering }; use upnp::UpnpManager; +use std::path::PathBuf; +use tls::{ CertificateManager, TlsOption }; use ws_server::WsServer; use ws; #[derive(Clone)] pub struct FoxBox { pub verbose: bool, + tls_option: TlsOption, + certificate_manager: CertificateManager, hostname: String, http_port: u16, ws_port: u16, @@ -57,6 +61,10 @@ pub trait Controller : Send + Sync + Clone + Reflect + 'static { fn get_http_root_for_service(&self, service_id: String) -> String; fn get_ws_root_for_service(&self, service_id: String) -> String; fn http_as_addrs(&self) -> Result, io::Error>; + + fn get_tls_enabled(&self) -> bool; + fn get_certificate_manager(&self) -> CertificateManager; + fn add_websocket(&mut self, socket: ws::Sender); fn remove_websocket(&mut self, socket: ws::Sender); fn broadcast_to_websockets(&self, data: serde_json::value::Value); @@ -72,10 +80,13 @@ impl FoxBox { pub fn new(verbose: bool, hostname: Option, http_port: u16, - ws_port: u16) -> Self { - + ws_port: u16, + tls_option: TlsOption) -> Self { let profile_service = ProfileService::new(None); + FoxBox { + certificate_manager: CertificateManager::new(), + tls_option: tls_option, services: Arc::new(Mutex::new(HashMap::new())), websockets: Arc::new(Mutex::new(HashMap::new())), verbose: verbose, @@ -101,8 +112,19 @@ impl Controller for FoxBox { { Arc::get_mut(&mut self.upnp).unwrap().start().unwrap(); } + + if self.get_tls_enabled() { + let certificate_directory = PathBuf::from( + self.config.get_or_set_default("foxbox", "certificate_directory", "certs/")); + + // If this fails, it just means that no certificates will be configured, which + // shouldn't cause a crash. + self.certificate_manager.reload_from_directory(certificate_directory).unwrap_or(()); + } + HttpServer::new(self.clone()).start(); WsServer::start(self.clone(), self.hostname.to_owned(), self.ws_port); + let mut adapter_manager = AdapterManager::new(self.clone()); adapter_manager.start(); self.upnp.search(None).unwrap(); @@ -177,7 +199,8 @@ impl Controller for FoxBox { } fn get_http_root_for_service(&self, service_id: String) -> String { - format!("http://{}:{}/services/{}/", self.hostname, self.http_port, service_id) + let scheme = if self.get_tls_enabled() { "https" } else { "http" }; + format!("{}://{}:{}/services/{}/", scheme , self.hostname, self.http_port, service_id) } fn get_ws_root_for_service(&self, service_id: String) -> String { @@ -222,6 +245,14 @@ impl Controller for FoxBox { fn get_users_manager(&self) -> Arc { self.users_manager.clone() } + + fn get_certificate_manager(&self) -> CertificateManager { + self.certificate_manager.clone() + } + + fn get_tls_enabled(&self) -> bool { + self.tls_option == TlsOption::Enabled + } } #[allow(dead_code)] @@ -247,9 +278,10 @@ describe! controller { before_each { use stubs::service::ServiceStub; + use tls::TlsOption; let service = ServiceStub; - let controller = FoxBox::new(false, Some("foxbox".to_owned()), 1234, 5678); + let controller = FoxBox::new(false, Some("foxbox".to_owned()), 1234, 5678, TlsOption::Disabled); } describe! add_service { @@ -269,10 +301,15 @@ describe! controller { } } - it "should create http root" { + it "should create https root if tls enabled and http root id disabled" { controller.add_service(Box::new(service)); assert_eq!(controller.get_http_root_for_service("1".to_string()), "http://foxbox.local:1234/services/1/"); + + let controller = FoxBox::new(false, Some("foxbox".to_owned()), 1234, 5678, TlsOption::Enabled); + controller.add_service(Box::new(service)); + assert_eq!(controller.get_http_root_for_service("1".to_string()), + "https://foxbox.local:1234/services/1/"); } it "should create ws root" { diff --git a/src/http_server.rs b/src/http_server.rs index b3e8656a..b8064eaf 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -3,8 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use controller::Controller; -use iron::{ AfterMiddleware, Chain, Handler, Iron, IronResult, - Request, Response }; +use hyper::net::{ NetworkListener }; +use iron::{ AfterMiddleware, Chain, Handler, + HttpServerFactory, Iron, IronResult, Request, + Response, ServerFactory }; use iron_cors::CORS; use iron::error::{ IronError }; use iron::method::Method; @@ -13,7 +15,11 @@ use mount::Mount; use router::NoRoute; use service_router; use static_router; +use std::net::SocketAddr; use std::thread; +use tls::SniServerFactory; + +const THREAD_COUNT: usize = 8; struct Custom404; @@ -79,13 +85,28 @@ impl HttpServer { let addrs: Vec<_> = self.controller.http_as_addrs().unwrap().collect(); - thread::Builder::new().name("HttpServer".to_owned()) - .spawn(move || { - Iron::new(chain).http(addrs[0]).unwrap(); - }).unwrap(); + if self.controller.get_tls_enabled() { + let mut certificate_manager = self.controller.get_certificate_manager(); + let server_factory = SniServerFactory::new(&mut certificate_manager); + start_server(addrs, chain, server_factory); + } else { + start_server(addrs, chain, HttpServerFactory {}); + } } } +fn start_server(addrs: Vec, chain: Chain, factory: T) + where TListener: NetworkListener + Send + 'static, + T: ServerFactory + Send + 'static { + + thread::Builder::new().name("HttpServer".to_owned()) + .spawn(move || { + Iron::new(chain) + .listen_with(addrs[0], THREAD_COUNT, &factory, None) + .unwrap(); + }).unwrap(); +} + #[cfg(test)] describe! ping { before_each { diff --git a/src/main.rs b/src/main.rs index f603eed9..c18b8a0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ extern crate env_logger; extern crate foxbox_adapters; extern crate foxbox_taxonomy; extern crate foxbox_users; +extern crate hyper; #[macro_use] extern crate iron; extern crate iron_cors; @@ -40,6 +41,8 @@ extern crate log; extern crate mio; extern crate mount; extern crate nix; +extern crate openssl; +extern crate openssl_sys; extern crate router; extern crate rustc_serialize; extern crate serde; @@ -72,6 +75,7 @@ mod upnp; mod service_router; mod stable_uuid; mod static_router; +mod tls; mod tunnel_controller; mod ws_server; @@ -94,9 +98,10 @@ use multicast_dns::host::HostManager; use std::env; use std::mem; use std::sync::atomic::{ AtomicBool, Ordering, ATOMIC_BOOL_INIT }; +use tls::TlsOption; docopt!(Args derive Debug, " -Usage: foxbox [-v] [-h] [-l ] [-p ] [-w ] [-r ] [-i ] [-t ] [-s ] [-u ] [-c ]... +Usage: foxbox [-v] [-h] [-l ] [-p ] [-w ] [-r ] [-i ] [-t ] [-s ] [--disable-tls] [-u ] [-c ]... Options: -v, --verbose Toggle verbose output. @@ -107,7 +112,8 @@ Options: -i, --iface Specify the local IP interface. -t, --tunnel Set the tunnel endpoint's hostname. If omitted, the tunnel is disabled. -s, --tunnel-secret Set the tunnel shared secret. [default: secret] - -u, --remote-name Set remote hostname. This the URL to access the box through the bridge. If omitted, the tunnel is disabled + --disable-tls Run as a plain HTTP server, disabling encryption. + --remote-name Set remote hostname. This the URL to access the box through the bridge. If omitted, the tunnel is disabled -c, --config Set configuration override -h, --help Print this help menu. ", @@ -118,6 +124,7 @@ Options: flag_iface: Option, flag_tunnel: Option, flag_tunnel_secret: String, + flag_disable_tls: bool, flag_remote_name: Option, flag_config: Option>); @@ -209,7 +216,8 @@ fn main() { let mut controller = FoxBox::new( args.flag_verbose, args.flag_local_name.map_or(None, update_hostname), args.flag_port, - args.flag_wsport); + args.flag_wsport, + if args.flag_disable_tls { TlsOption::Disabled } else { TlsOption::Enabled }); // Override config values { diff --git a/src/service_router.rs b/src/service_router.rs index cff8633d..d26cffe2 100644 --- a/src/service_router.rs +++ b/src/service_router.rs @@ -68,8 +68,9 @@ describe! service_router { use iron::Headers; use iron_test::request; use mount::Mount; + use tls::TlsOption; - let controller = FoxBox::new(false, Some("localhost".to_owned()), 1234, 5678); + let controller = FoxBox::new(false, Some("localhost".to_owned()), 1234, 5678, TlsOption::Disabled); let service_router = create(controller.clone()); let mut mount = Mount::new(); diff --git a/src/stubs/controller.rs b/src/stubs/controller.rs index 509a5d06..a6b327fc 100644 --- a/src/stubs/controller.rs +++ b/src/stubs/controller.rs @@ -19,6 +19,7 @@ use std::net::SocketAddr; use std::net::ToSocketAddrs; use std::sync::Arc; use std::sync::atomic::AtomicBool; +use tls::CertificateManager; use upnp::UpnpManager; use ws; @@ -85,4 +86,11 @@ impl Controller for ControllerStub { fn get_profile(&self) -> &ProfileService { &self.profile_service } + fn get_tls_enabled(&self) -> bool { + false + } + + fn get_certificate_manager(&self) -> CertificateManager { + CertificateManager::new() + } } diff --git a/src/tls/certificate_manager.rs b/src/tls/certificate_manager.rs new file mode 100644 index 00000000..779be3ac --- /dev/null +++ b/src/tls/certificate_manager.rs @@ -0,0 +1,232 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; +use std::fs::{ self }; +use std::io::{ Error, ErrorKind }; +use std::path::PathBuf; +use std::sync::{ Arc, Mutex, RwLock }; + +use tls::certificate_record::CertificateRecord; +use tls::ssl_context::SslContextProvider; + +#[derive(Clone)] +pub struct CertificateManager { + ssl_hosts: Arc>>, + + // Observer + context_provider: Option>>> +} + +fn create_records_from_directory(path: &PathBuf) -> Result, Error> { + let mut records = HashMap::new(); + + if try!(fs::metadata(path)).is_dir() { + for entry in try!(fs::read_dir(path)) { + let entry = try!(entry); + + let hostname = entry.file_name().into_string().unwrap(); + info!("Using certificate for host {}", hostname); + + let mut host_path = path.clone(); + host_path.push(hostname.clone()); + + let mut cert_path = host_path.clone(); + cert_path.push("crt.pem"); + + let mut private_key_file = host_path.clone(); + private_key_file.push("private_key.pem"); + + records.insert(hostname.clone(), CertificateRecord { + hostname: hostname, + private_key_file: private_key_file, + cert_file: cert_path + }); + } + + info!("Loaded certificates from directory: {:?}", path); + + Ok(records) + } else { + Err(Error::new( + ErrorKind::InvalidInput, + "The configured SSL certificate directory is not recognised as a directory." + )) + } +} + +impl CertificateManager { + pub fn new() -> CertificateManager { + CertificateManager { + ssl_hosts: Arc::new(RwLock::new(HashMap::new())), + context_provider: None, + } + } + + pub fn set_context_provider(&mut self, context_provider: Arc>>) { + if self.context_provider.is_none() { + self.context_provider = Some(context_provider); + self.notify_provider(); + } else { + error!("SslContextProvider was set more than once in the CertificateManager"); + } + } + + pub fn reload_from_directory(&mut self, directory: PathBuf) -> Result<(), Error> { + let certificates = try!(create_records_from_directory(&directory)); + { + let mut current_hosts = checklock!(self.ssl_hosts.write()); + current_hosts.clear(); + current_hosts.extend(certificates); + } + + self.notify_provider(); + Ok(()) + } + + #[allow(dead_code)] + pub fn add_certificate(&mut self, certificate_record: CertificateRecord) { + { + checklock!(self.ssl_hosts.write()) + .insert(certificate_record.hostname.clone(), certificate_record); + } + + self.notify_provider(); + } + + #[allow(dead_code)] + pub fn get_certificate(&self, hostname: String) -> Option { + let ssl_hosts = checklock!(self.ssl_hosts.read()); + let cert_record = ssl_hosts.get(&hostname); + + if let Some(record) = cert_record { + Some(record.clone()) + } else { + None + } + } + + #[allow(dead_code)] + pub fn remove_certificate(&mut self, hostname: String) { + { + checklock!(self.ssl_hosts.write()).remove(&hostname); + } + + self.notify_provider(); + } + + fn notify_provider(&mut self) { + let ssl_hosts = checklock!(self.ssl_hosts.read()).clone(); + + if let Some(ref mut context_provider) = self.context_provider { + context_provider.lock().unwrap().update(ssl_hosts); + } + } +} + + +#[cfg(test)] +mod certificate_manager { + use openssl::ssl::{ SslContext, SslMethod }; + use std::collections::HashMap; + use std::io::{ Error, ErrorKind }; + use std::path::PathBuf; + use std::sync::{ Arc, Mutex }; + use std::sync::mpsc::{ channel, Sender }; + use tls::{ CertificateRecord, SslContextProvider }; + + use super::*; + + pub struct TestSslContextProvider { + update_called: Sender + } + + impl TestSslContextProvider { + fn new(update_chan: Sender) -> Self { + TestSslContextProvider { + update_called: update_chan + } + } + } + + impl SslContextProvider for TestSslContextProvider { + fn context(&self) -> Result { + SslContext::new(SslMethod::Sslv23).map_err(|_| { + Error::new(ErrorKind::InvalidInput, "An SSL certificate could not be configured") + }) + } + + fn update(&mut self, _: HashMap) -> () { + self.update_called.send(true).unwrap(); + } + } + + fn test_cert_record() -> CertificateRecord { + CertificateRecord { + hostname: "test.example.com".to_owned(), + private_key_file: PathBuf::from("/test/key.pem"), + cert_file: PathBuf::from("/test/crt.pem") + } + } + + #[test] + fn should_allow_certificates_to_be_added() { + let cert_record = test_cert_record(); + let mut cert_manager = CertificateManager::new(); + + cert_manager.add_certificate(cert_record.clone()); + + assert!(cert_manager.get_certificate("test.example.com".to_owned()).unwrap() == cert_record); + + cert_manager.remove_certificate("test.example.com".to_owned()); + + assert!(cert_manager.get_certificate("test.example.com".to_owned()).is_none()); + } + + #[test] + fn should_allow_certificates_to_be_removed() { + let cert_record = test_cert_record(); + let mut cert_manager = CertificateManager::new(); + + cert_manager.add_certificate(cert_record); + + cert_manager.remove_certificate("test.example.com".to_owned()); + + assert!(cert_manager.get_certificate("test.example.com".to_owned()).is_none()); + } + + #[test] + fn should_update_configured_providers_when_cert_added() { + let cert_record = test_cert_record(); + let (tx_update_called, rx_update_called) = channel(); + let mut cert_manager = CertificateManager::new(); + + let provider_one = Box::new(TestSslContextProvider::new(tx_update_called)); + + cert_manager.set_context_provider(Arc::new(Mutex::new(provider_one))); + + cert_manager.add_certificate(cert_record); + + assert!(rx_update_called.recv().unwrap(), "Did not receive notification from handler after add"); + } + + #[test] + fn should_update_configured_providers_when_cert_removed() { + let cert_record = test_cert_record(); + let (tx_update_called, rx_update_called) = channel(); + let mut cert_manager = CertificateManager::new(); + + let provider_one = Box::new(TestSslContextProvider::new(tx_update_called)); + + cert_manager.set_context_provider(Arc::new(Mutex::new(provider_one))); + + cert_manager.add_certificate(cert_record); + + assert!(rx_update_called.recv().unwrap(), "Did not receive notification from handler after add"); + + cert_manager.remove_certificate(test_cert_record().hostname); + + assert!(rx_update_called.recv().unwrap(), "Did not receive notification from handler after remove"); + } +} diff --git a/src/tls/certificate_record.rs b/src/tls/certificate_record.rs new file mode 100644 index 00000000..9bbf4256 --- /dev/null +++ b/src/tls/certificate_record.rs @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::path::PathBuf; + +/// Defines a certificate, including the hostname it is for, +/// the private key file and the certificate file. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct CertificateRecord { + pub hostname: String, + pub private_key_file: PathBuf, + pub cert_file: PathBuf, + // TODO: We should probably keep a hash of the + // file contents as part of the CertRecord. + // See: https://github.com/fxbox/foxbox/issues/224 +} diff --git a/src/tls/https_server_factory.rs b/src/tls/https_server_factory.rs new file mode 100644 index 00000000..58f49b79 --- /dev/null +++ b/src/tls/https_server_factory.rs @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use hyper::error::Error as HyperError; +use hyper::net::{ HttpsListener, Openssl, Ssl }; +use hyper::server::Server; +use iron::{ Protocol, ServerFactory }; +use std::net::SocketAddr; +use std::sync::{ Arc, Mutex }; +use tls::certificate_manager::CertificateManager; +use tls::ssl_context::{ SniSslContextProvider, SslContextProvider }; + +pub struct SniServerFactory { + ssl: S +} + +impl SniServerFactory { + pub fn new(ssl: &mut CertificateManager) -> Self { + let context_provider: Box = Box::new(SniSslContextProvider::new()); + + let shared_context_provider = Arc::new(Mutex::new(context_provider)); + + ssl.set_context_provider(shared_context_provider.clone()); + + let data = shared_context_provider.lock().unwrap(); + + SniServerFactory { + ssl: Openssl { + context: Arc::new(data.context().unwrap()) + } + } + } +} + +impl ServerFactory> for SniServerFactory { + fn protocol(&self) -> Protocol { + Protocol::Https + } + + fn create_server(&self, sock_addr: SocketAddr) -> Result>, HyperError> { + Server::https(sock_addr, self.ssl.clone()) + } +} diff --git a/src/tls/mod.rs b/src/tls/mod.rs new file mode 100644 index 00000000..ded1ed26 --- /dev/null +++ b/src/tls/mod.rs @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +macro_rules! checklock ( + ($e: expr) => { + match $e { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } +); + +mod certificate_manager; +mod certificate_record; +mod https_server_factory; +mod ssl_context; + +pub use tls::certificate_manager::*; +pub use tls::certificate_record::*; +pub use tls::https_server_factory::*; +pub use tls::ssl_context::*; + + +#[derive(Clone, Eq, PartialEq)] +pub enum TlsOption { + Enabled, + Disabled, +} diff --git a/src/tls/ssl_context.rs b/src/tls/ssl_context.rs new file mode 100644 index 00000000..1207e94f --- /dev/null +++ b/src/tls/ssl_context.rs @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use openssl::ssl::{ Ssl, SslContext, SslMethod, SSL_VERIFY_NONE }; +use openssl::ssl::error::SslError; +use openssl::x509::X509FileType; +use openssl_sys; + +use std::collections::HashMap; +use std::io::Error; +use std::path::Path; +use std::sync::{ Arc, RwLock }; + +use tls::certificate_record::CertificateRecord; + +pub trait SslContextProvider : Send { + fn context(&self) -> Result; + fn update(&mut self, HashMap) -> (); +} + +#[derive(Clone)] +pub struct SniSslContextProvider { + main_context: Arc> +} + +impl SslContextProvider for SniSslContextProvider { + fn context(&self) -> Result { + Ok(checklock!(self.main_context.read()).clone()) + } + + fn update(&mut self, configured_hosts: HashMap) -> () { + debug!("Updating SniSslContextProvider"); + + let mut new_ssl_hosts = HashMap::new(); + + for record in configured_hosts.values() { + debug!("Creating SslContext for {}", record.hostname); + let ssl_context = create_ssl_context(&record.cert_file, &record.private_key_file); + + if ssl_context.is_ok() { + let ssl_context = ssl_context.unwrap(); + new_ssl_hosts.insert(record.hostname.clone(), ssl_context); + } else { + error!("Failed to configure SslContext for: {:?}", record); + } + } + + debug!("Update certificates"); + checklock!(self.main_context.write()) + .set_servername_callback_with_data(SniSslContextProvider::servername_callback, new_ssl_hosts); + } +} + +impl SniSslContextProvider { + pub fn new() -> Self { + SniSslContextProvider { + main_context: Arc::new(RwLock::new(SslContext::new(SslMethod::Sslv23).unwrap())) + } + } + + fn servername_callback_impl(ssl: &mut SslForSni, configured_certs: &HashMap) -> i32 { + debug!("servername_callback invoked"); + let requested_hostname = ssl.get_hostname(); + + if requested_hostname.is_none() { + debug!("No SNI - "); + return openssl_sys::SSL_TLSEXT_ERR_NOACK; + } + + let requested_hostname = requested_hostname.unwrap(); + + debug!("Selecting certificate for host {}", requested_hostname); + + let ssl_context_for_hostname = configured_certs.get(&requested_hostname); + + if let Some(ctx)= ssl_context_for_hostname { + ssl.set_context(ctx); + } + + openssl_sys::SSL_TLSEXT_ERR_OK + } + + fn servername_callback(ssl: &mut Ssl, _: &mut i32, configured_certs: &HashMap) -> i32 { + Self::servername_callback_impl(ssl, configured_certs) + } +} + +pub trait SslForSni { + fn get_hostname(&self) -> Option; + fn set_context(&self, ctx: &T) -> Option; +} + +impl SslForSni for Ssl { + fn get_hostname(&self) -> Option { + self.get_servername() + } + + fn set_context(&self, ctx: &SslContext) -> Option { + Some(self.set_ssl_context(ctx)) + } +} + +pub fn create_ssl_context(crt: &C, key: &K) -> Result + where C: AsRef, K: AsRef { + + debug!("Creating SSL Context with Cert: {:?}, Key: {:?}", crt.as_ref().to_str(), key.as_ref().to_str()); + + let mut ctx = try!(SslContext::new(SslMethod::Sslv23)); + try!(ctx.set_cipher_list("DEFAULT")); + try!(ctx.set_certificate_file(crt.as_ref(), X509FileType::PEM)); + try!(ctx.set_private_key_file(key.as_ref(), X509FileType::PEM)); + ctx.set_verify(SSL_VERIFY_NONE, None); + + Ok(ctx) +} + +#[cfg(test)] +mod sni_ssl_context_provider { + use openssl_sys; + use std::collections::HashMap; + use std::sync::mpsc::{ channel, Sender }; + + use super::*; + + pub struct MockSsl { + servername: Option, + context_set: Option> + } + + impl SslForSni for MockSsl { + fn get_hostname(&self) -> Option { + self.servername.clone() + } + + fn set_context(&self, ctx: &String) -> Option { + if let Some(ref context_set) = self.context_set { + context_set.send(ctx.clone()).unwrap(); + } + + None + } + } + + #[test] + fn should_set_context_based_on_servername() { + let (tx_context_called, rx_context_called) = channel(); + + let mut ssl = MockSsl { + servername: Some("test.knilxof.org".to_owned()), + context_set: Some(tx_context_called) + }; + + let mut contexts = HashMap::new(); + + contexts.insert("test.knilxof.org".to_owned(), "fake_context".to_owned()); + + let result = SniSslContextProvider::servername_callback_impl(&mut ssl, &contexts); + + assert!(result == openssl_sys::SSL_TLSEXT_ERR_OK, "Servername callback did not return OK"); + assert!(rx_context_called.recv().unwrap() == "fake_context", "Set context was not called with the expected value"); + } + + #[test] + fn should_return_fail_code_if_servername_is_not_available() { + let mut ssl = MockSsl { + servername: None, + context_set: None, + }; + + let contexts = HashMap::new(); + + let result = SniSslContextProvider::servername_callback_impl(&mut ssl, &contexts); + + assert!(result == openssl_sys::SSL_TLSEXT_ERR_NOACK, "Expected ERR_NOACK result from servername callback"); + } +} diff --git a/test/integration/test/philips_light_test.js b/test/integration/test/philips_light_test.js index e86f56cf..529e9f47 100644 --- a/test/integration/test/philips_light_test.js +++ b/test/integration/test/philips_light_test.js @@ -10,36 +10,36 @@ var config = new Config('./test/integration/lib/config/foxbox.js'); var header = new Config('./test/integration/lib/config/header.js'); describe('Initiate the connection with foxbox as Philips Hue hub',function(){ - var credential = config.get('credential'); + var credential = config.get('credential'); var FOXBOX_STARTUP_WAIT_TIME_IN_MS = 3000; var lightinfo; // to store the light status received from foxbox var foxbox_process; - + before(function(done){ this.timeout(500000); const hue_location = 'localhost:' + config.get('philips_hue.port'); nupnp_server.start(config.get('nupnp_server.id'), hue_location,config.get('nupnp_server.port')); - + philipshue_server.setup(config.get('philips_hue.port')); philipshue_server.turnOffLight(1); philipshue_server.turnOffLight(2); philipshue_server.turnOffLight(3); - foxbox_process = spawn('./target/debug/foxbox', - ['-c', 'philips_hue;nupnp_url;http://localhost:'+ - config.get('nupnp_server.port')+'/']); + foxbox_process = spawn('./target/debug/foxbox', + ['-c', 'philips_hue;nupnp_url;http://localhost:'+ + config.get('nupnp_server.port')+'/', '--disable-tls']); // give time until foxbox is operational - setTimeout(done, FOXBOX_STARTUP_WAIT_TIME_IN_MS); + setTimeout(done, FOXBOX_STARTUP_WAIT_TIME_IN_MS); }); - + after(function(){ foxbox_process.kill(); }); - + it('create a new credential',function(){ return chakram.post(config.get('foxbox.url') + '/users/setup',credential) @@ -53,9 +53,9 @@ describe('Initiate the connection with foxbox as Philips Hue hub',function(){ var encoded_cred = new Buffer(key).toString('base64'); // supply the credential used in previous test - header.Authorization = 'Basic ' + encoded_cred; - - return chakram.post(config.get('foxbox.url') + + header.Authorization = 'Basic ' + encoded_cred; + + return chakram.post(config.get('foxbox.url') + '/users/login',null,{'headers' : header}) .then(function(loginResp){ expect(loginResp).to.have.status(201); @@ -64,7 +64,7 @@ describe('Initiate the connection with foxbox as Philips Hue hub',function(){ describe ('once logged in', function () { before(function() { - return chakram.post(config.get('foxbox.url') + + return chakram.post(config.get('foxbox.url') + '/users/login',null,{'headers' : header}) .then(function(resp){ header.Authorization = 'Bearer ' + resp.body.session_token; @@ -72,7 +72,7 @@ describe('Initiate the connection with foxbox as Philips Hue hub',function(){ }); }); - it('check 3 bulbs are registered',function(){ + it('check 3 bulbs are registered',function(){ return chakram.get(config.get('foxbox.url') + '/services/list') .then(function(listResponse) { @@ -89,32 +89,32 @@ describe('Initiate the connection with foxbox as Philips Hue hub',function(){ }); }); - // Currently, there is no mapping between the foxbox + // Currently, there is no mapping between the foxbox // issues id and the philips hue id until the tag feature is implemented it('Turn on all lights', function(){ - - return chakram.put(config.get('foxbox.url') + '/services/' + + + return chakram.put(config.get('foxbox.url') + '/services/' + lightinfo[0].id + '/state', {'on': true}) .then(function(cmdResponse) { expect(cmdResponse).to.have.status(200); expect(cmdResponse.body.result).equals('success'); expect(philipshue_server.lastCmd()).to.contain('"on":true'); - return chakram.put(config.get('foxbox.url') + '/services/' + + return chakram.put(config.get('foxbox.url') + '/services/' + lightinfo[1].id + '/state', {'on': true});}) .then(function(cmdResponse) { expect(cmdResponse).to.have.status(200); expect(cmdResponse.body.result).equals('success'); expect(philipshue_server.lastCmd()).to.contain('"on":true'); - return chakram.put(config.get('foxbox.url') + '/services/' + + return chakram.put(config.get('foxbox.url') + '/services/' + lightinfo[2].id + '/state', {'on': true});}) .then(function(cmdResponse) { expect(cmdResponse).to.have.status(200); expect(cmdResponse.body.result).equals( 'success'); expect(philipshue_server.lastCmd()).to.contain('"on":true'); - + // check no lights are turned off now - expect(philipshue_server.lightStatus(1) && - philipshue_server.lightStatus(2) && + expect(philipshue_server.lightStatus(1) && + philipshue_server.lightStatus(2) && philipshue_server.lightStatus(3)).equals(true); }); }); diff --git a/tools/scripts/make-root-ca-and-certs.sh b/tools/scripts/make-root-ca-and-certs.sh new file mode 100644 index 00000000..8103468c --- /dev/null +++ b/tools/scripts/make-root-ca-and-certs.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Taken from coolaj86's very useful nodejs-self-signed-certificate-example repo +# (Apache licensed). +# See https://github.com/coolaj86/nodejs-self-signed-certificate-example. + +FQDN=$1 + +# make directories to work from +mkdir -p certs/{server,client,ca,tmp} + +# Create your very own Root Certificate Authority +openssl genrsa \ + -out certs/ca/my-root-ca.key.pem \ + 2048 + +# Self-sign your Root Certificate Authority +# Since this is private, the details can be as bogus as you like +openssl req \ + -x509 \ + -new \ + -nodes \ + -key certs/ca/my-root-ca.key.pem \ + -days 1024 \ + -out certs/ca/my-root-ca.crt.pem \ + -subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.com" + +# Create a Device Certificate for each domain, +# such as example.com, *.example.com, awesome.example.com +# NOTE: You MUST match CN to the domain name or ip address you want to use +openssl genrsa \ + -out certs/server/${FQDN}-server.key.pem \ + 2048 + +# Create a request from your Device, which your Root CA will sign +openssl req -new \ + -key certs/server/${FQDN}-server.key.pem \ + -out certs/tmp/${FQDN}-server.csr.pem \ + -subj "/C=US/ST=Utah/L=Provo/O=ACME Tech Inc/CN=${FQDN}" + +# Sign the request from Device with your Root CA +# -CAserial certs/ca/my-root-ca.srl +openssl x509 \ + -req -in certs/tmp/${FQDN}-server.csr.pem \ + -CA certs/ca/my-root-ca.crt.pem \ + -CAkey certs/ca/my-root-ca.key.pem \ + -CAcreateserial \ + -out certs/server/${FQDN}-server.crt.pem \ + -days 500 + +# Create a public key, for funzies +# see https://gist.github.com/coolaj86/f6f36efce2821dfb046d +openssl rsa \ + -in certs/server/${FQDN}-server.key.pem \ + -pubout -out certs/client/${FQDN}-server.pub + +# Put things in their proper place +rsync -a certs/ca/my-root-ca.crt.pem certs/server/ +rsync -a certs/ca/my-root-ca.crt.pem certs/client/