From 6a1c3e3e99bdd010f47aedb80313961464a74625 Mon Sep 17 00:00:00 2001 From: Fletcher Nichol Date: Wed, 4 May 2016 11:07:54 -0600 Subject: [PATCH 1/3] [sup] Honor `HAB_RING` for `start` subcommand. This change introduces an optional way to set the Ring name when a Supervisor starts via setting a `HAB_RING` environment variable. The environment variable will always be overrident if the user uses an explicit `--ring` option on start, meaning that a CLI arugment is always first priority and environment variable is secondary. Current explicit behavior: hab-sup start --ring possums core/redis New, alternative use with an environment variable: env HAB_RING=possums hab-sup start core/redis Signed-off-by: Fletcher Nichol --- components/sup/src/main.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/components/sup/src/main.rs b/components/sup/src/main.rs index b31100be18..12190fb778 100644 --- a/components/sup/src/main.rs +++ b/components/sup/src/main.rs @@ -45,6 +45,8 @@ const VERSION: &'static str = include_str!(concat!(env!("OUT_DIR"), "/VERSION")) static DEFAULT_GROUP: &'static str = "default"; static DEFAULT_GOSSIP_LISTEN: &'static str = "0.0.0.0:9634"; +static RING_ENVVAR: &'static str = "HAB_RING"; + /// Creates a [Config](config/struct.Config.html) from global args /// and subcommand args. fn config_from_args(args: &ArgMatches, subcommand: &str, sub_args: &ArgMatches) -> Result { @@ -133,8 +135,17 @@ fn config_from_args(args: &ArgMatches, subcommand: &str, sub_args: &ArgMatches) config.set_file_path(fp.to_string()); } config.set_version_number(value_t!(sub_args, "version-number", u64).unwrap_or(0)); - if let Some(ring) = sub_args.value_of("ring") { - config.set_ring(ring.to_string()); + let ring = match sub_args.value_of("ring") { + Some(val) => Some(val.to_string()), + None => { + match henv::var(RING_ENVVAR) { + Ok(val) => Some(val), + Err(_) => None, + } + } + }; + if let Some(ring) = ring { + config.set_ring(ring); } if args.value_of("verbose").is_some() { sup::output::set_verbose(true); From aa3c58d7bef6dfe9640b09665360106699812a32 Mon Sep 17 00:00:00 2001 From: Fletcher Nichol Date: Wed, 4 May 2016 12:10:49 -0600 Subject: [PATCH 2/3] [sup] Honor `HAB_RING_KEY` containing key's content for `start` subcommand. This change introduces a way to inject a Ring key to a Supervisor when it starts via setting a new `HAB_RING_KEY` environment variable. Whereas the previous `HAB_RING` environment variable contains the name of the key (which is supposed to already exist on disk locally in the key cache), the `HAB_RING_KEY` contains the contents of a key file itself. This allows an operator to start a brand new Supervisor with the following: env HAB_RING_KEY='SYM-SEC-1 beyonce-20160504220722 RCFaO84j41GmrzWddxMdsXpGdn3iuIy7Mw3xYrjPLsE=' hab-sup start core/redis or alternatively: cat < /tmp/key SYM-SEC-1 beyonce-20160504220722 RCFaO84j41GmrzWddxMdsXpGdn3iuIy7Mw3xYrjPLsE= EOF env HAB_RING_KEY="$(cat /tmp/key)" hab-sup start core/redis or, even: env HAB_RING_KEY="$(curl https://extreme.trust/key)" hab-sup start core/redis As before, there is a priority order in which CLI options and environment variables are checked when setting the ring key on start: 1. The `--ring` option on the command line wins over any other setting 2. A set `$HAB_RING_KEY` environment variable is used next 3. A set `$HAB_RING` environment variable is used last 4. The Supervisor will start in an unencrypted mode Signed-off-by: Fletcher Nichol --- components/core/src/crypto.rs | 242 ++++++++++++++++++ components/core/src/lib.rs | 2 + components/core/src/service.rs | 2 +- ...key-invalid-version-20160504221247.sym.key | 4 + .../ring-key-valid-20160504220722.sym.key | 4 + components/sup/src/main.rs | 18 +- 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 components/core/tests/fixtures/keys/ring-key-invalid-version-20160504221247.sym.key create mode 100644 components/core/tests/fixtures/keys/ring-key-valid-20160504220722.sym.key diff --git a/components/core/src/crypto.rs b/components/core/src/crypto.rs index 1a09bc8d30..00e29387ad 100644 --- a/components/core/src/crypto.rs +++ b/components/core/src/crypto.rs @@ -237,6 +237,7 @@ use sodiumoxide::crypto::sign::ed25519::PublicKey as SigPublicKey; use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::PublicKey as BoxPublicKey; use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::SecretKey as BoxSecretKey; use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::{Nonce, gen_nonce}; +use sodiumoxide::randombytes::randombytes; use time; use env as henv; @@ -1255,4 +1256,245 @@ impl Context { candidate_vec.reverse(); Ok(candidate_vec) } + + /// Writes a sym key to the key cache from the contents of a string slice. + /// + /// The return is a `Result` of a `String` containing the key's name with revision. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// extern crate habitat_core; + /// extern crate tempdir; + /// + /// use habitat_core::crypto::Context; + /// use tempdir::TempDir; + /// + /// fn main() { + /// let key_cache = TempDir::new("key_cache").unwrap(); + /// let ctx = Context { key_cache: key_cache.path().to_string_lossy().into_owned() }; + /// let content = "SYM-SEC-1 + /// beyonce-20160504220722 + /// + /// RCFaO84j41GmrzWddxMdsXpGdn3iuIy7Mw3xYrjPLsE="; + /// + /// let keyname = ctx.write_sym_key_from_str(content).unwrap(); + /// assert_eq!(keyname, "beyonce-20160504220722"); + /// assert!(key_cache.path().join("beyonce-20160504220722.sym.key").is_file()); + /// } + /// ``` + /// + /// # Errors + /// + /// * If there is a key version mismatch + /// * If the key version is missing + /// * If the key name with revision is missing + /// * If the key value (the Bas64 payload) is missing + /// * If the key file cannot be written to disk + /// * If an existing key is already installed, but the new content is different from the + /// existing + pub fn write_sym_key_from_str(&self, content: &str) -> Result { + let mut lines = content.lines(); + let _ = match lines.next() { + Some(val) => { + if val != SECRET_SYM_KEY_VERSION { + return Err(Error::CryptoError(format!("Unsupported key version: {}", val))); + } + () + } + None => { + let msg = format!("write_sym_key_from_str:1 Malformed sym key string:\n({})", + content); + return Err(Error::CryptoError(msg)); + } + }; + let keyname = match lines.next() { + Some(val) => val, + None => { + let msg = format!("write_sym_key_from_str:2 Malformed sym key string:\n({})", + content); + return Err(Error::CryptoError(msg)); + } + }; + let sk = match lines.nth(1) { + Some(val) => val, + None => { + let msg = format!("write_sym_key_from_str:3 Malformed sym key string:\n({})", + content); + return Err(Error::CryptoError(msg)); + } + }; + let secret_keyfile = self.mk_key_filename(&self.key_cache, &keyname, SECRET_SYM_KEY_SUFFIX); + let tmpfile = { + let mut t = secret_keyfile.clone(); + t.push('.'); + t.push_str(&randombytes(6).as_slice().to_hex()); + t + }; + + debug!("Writing temp key file {}", &tmpfile); + try!(self.write_keypair_files(KeyType::Sym, + &keyname, + None, + None, + &tmpfile, + &sk.as_bytes().to_vec())); + + if Path::new(&secret_keyfile).is_file() { + let existing_hash = try!(self.hash_file(&secret_keyfile)); + let new_hash = try!(self.hash_file(&tmpfile)); + if existing_hash != new_hash { + let msg = format!("Existing key file {} found but new version hash is different, \ + failing to write new file over existing. ({} = {}, {} = {})", + secret_keyfile, + secret_keyfile, + existing_hash, + tmpfile, + new_hash); + return Err(Error::CryptoError(msg)); + } else { + // Otherwise, hashes match and we can skip writing over the exisiting file + debug!("New content hash matches existing file {} hash, removing temp key file {}.", + secret_keyfile, + tmpfile); + try!(fs::remove_file(tmpfile)); + } + } else { + debug!("Moving {} to {}", tmpfile, secret_keyfile); + try!(fs::rename(tmpfile, secret_keyfile)); + } + + Ok(keyname.to_string()) + } +} + +#[cfg(test)] +mod test { + mod write_sym_key_from_str { + use std::env; + use std::fs::{self, File}; + use std::io::Read; + use std::path::{Path, PathBuf}; + + use tempdir::TempDir; + + use crypto::Context; + + static VALID_KEY: &'static str = "ring-key-valid-20160504220722.sym.key"; + static VALID_NAME: &'static str = "ring-key-valid-20160504220722"; + + fn random_ctx() -> (TempDir, Context) { + let tempdir = TempDir::new("key_cache").unwrap(); + let ctx = Context { key_cache: tempdir.path().to_string_lossy().into_owned() }; + (tempdir, ctx) + } + + fn fixture(name: &str) -> PathBuf { + let file = env::current_exe() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("tests") + .join("fixtures") + .join(name); + if !file.is_file() { + panic!("No fixture {} exists!", file.display()); + } + file + } + + fn fixture_as_string(name: &str) -> String { + let mut file = File::open(fixture(name)).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + content + } + + #[test] + fn writes_new_key_file() { + let (cache, ctx) = random_ctx(); + let content = fixture_as_string(&format!("keys/{}", VALID_KEY)); + let new_key_file = Path::new(cache.path()).join(VALID_KEY); + + assert_eq!(new_key_file.is_file(), false); + let keyname = ctx.write_sym_key_from_str(&content).unwrap(); + assert_eq!(keyname, VALID_NAME); + assert!(new_key_file.is_file()); + + let new_content = { + let mut new_content_file = File::open(new_key_file).unwrap(); + let mut new_content = String::new(); + new_content_file.read_to_string(&mut new_content).unwrap(); + new_content + }; + + assert_eq!(new_content, content); + } + + #[test] + fn doesnt_error_when_key_exists_and_is_identical() { + let (cache, ctx) = random_ctx(); + let content = fixture_as_string(&format!("keys/{}", VALID_KEY)); + let new_key_file = Path::new(cache.path()).join(VALID_KEY); + + // install the key into the cache + fs::copy(fixture(&format!("keys/{}", VALID_KEY)), &new_key_file).unwrap(); + + let keyname = ctx.write_sym_key_from_str(&content).unwrap(); + assert_eq!(keyname, VALID_NAME); + assert!(new_key_file.is_file()); + } + + #[test] + #[should_panic(expected = "Unsupported key version")] + fn error_when_version_is_supported() { + let (_, ctx) = random_ctx(); + let content = fixture_as_string("keys/ring-key-invalid-version-20160504221247.sym.key"); + + ctx.write_sym_key_from_str(&content).unwrap(); + } + + #[test] + #[should_panic(expected = "write_sym_key_from_str:1 Malformed sym key string")] + fn error_when_missing_version() { + let (_, ctx) = random_ctx(); + + ctx.write_sym_key_from_str("").unwrap(); + } + + #[test] + #[should_panic(expected = "write_sym_key_from_str:2 Malformed sym key string")] + fn error_when_missing_name() { + let (_, ctx) = random_ctx(); + + ctx.write_sym_key_from_str("SYM-SEC-1\n").unwrap(); + } + + #[test] + #[should_panic(expected = "write_sym_key_from_str:3 Malformed sym key string")] + fn error_when_missing_key() { + let (_, ctx) = random_ctx(); + + ctx.write_sym_key_from_str("SYM-SEC-1\nim-in-trouble-123\n").unwrap(); + } + + #[test] + #[should_panic(expected = "Existing key file")] + fn error_when_key_exists_and_hashes_differ() { + let (cache, ctx) = random_ctx(); + let key = fixture("keys/ring-key-valid-20160504220722.sym.key"); + fs::copy(key, + cache.path().join("ring-key-valid-20160504220722.sym.key")) + .unwrap(); + + ctx.write_sym_key_from_str("SYM-SEC-1\nring-key-valid-20160504220722\n\nsomething") + .unwrap(); + } + } } diff --git a/components/core/src/lib.rs b/components/core/src/lib.rs index 3e0e632783..c1e2c30100 100644 --- a/components/core/src/lib.rs +++ b/components/core/src/lib.rs @@ -14,6 +14,8 @@ extern crate regex; extern crate rustc_serialize; extern crate sodiumoxide; extern crate libsodium_sys; +#[cfg(test)] +extern crate tempdir; extern crate time; extern crate toml; diff --git a/components/core/src/service.rs b/components/core/src/service.rs index f4eb5103cd..7c491fa177 100644 --- a/components/core/src/service.rs +++ b/components/core/src/service.rs @@ -61,7 +61,7 @@ impl FromStr for ServiceGroup { // you can't specify a key with an "@", but without an org // ex: "foo.bar@" if value.ends_with('@') { - return Err(Error::InvalidServiceGroup(value.to_string())) + return Err(Error::InvalidServiceGroup(value.to_string())); }; Ok(ServiceGroup::new(name, group, caps.at(4).map(|s| s.to_string()))) diff --git a/components/core/tests/fixtures/keys/ring-key-invalid-version-20160504221247.sym.key b/components/core/tests/fixtures/keys/ring-key-invalid-version-20160504221247.sym.key new file mode 100644 index 0000000000..f194c9204b --- /dev/null +++ b/components/core/tests/fixtures/keys/ring-key-invalid-version-20160504221247.sym.key @@ -0,0 +1,4 @@ +NONSENSE-12 +ring-key-invalid-version-20160504221247 + +x3TABP5co+HIKY+crMP6JaawVxgG3tXRIJMtUPUXgxw= \ No newline at end of file diff --git a/components/core/tests/fixtures/keys/ring-key-valid-20160504220722.sym.key b/components/core/tests/fixtures/keys/ring-key-valid-20160504220722.sym.key new file mode 100644 index 0000000000..c52816d69b --- /dev/null +++ b/components/core/tests/fixtures/keys/ring-key-valid-20160504220722.sym.key @@ -0,0 +1,4 @@ +SYM-SEC-1 +ring-key-valid-20160504220722 + +RCFaO84j41GmrzWddxMdsXpGdn3iuIy7Mw3xYrjPLsE= \ No newline at end of file diff --git a/components/sup/src/main.rs b/components/sup/src/main.rs index 12190fb778..379bdb7121 100644 --- a/components/sup/src/main.rs +++ b/components/sup/src/main.rs @@ -25,9 +25,10 @@ use std::str::FromStr; use ansi_term::Colour::Yellow; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; -use hcore::package::PackageIdent; use hcore::env as henv; use hcore::fs; +use hcore::crypto::Context; +use hcore::package::PackageIdent; use hcore::url::{DEFAULT_DEPOT_URL, DEPOT_URL_ENVVAR}; use sup::config::{Command, Config, UpdateStrategy}; @@ -46,6 +47,7 @@ static DEFAULT_GROUP: &'static str = "default"; static DEFAULT_GOSSIP_LISTEN: &'static str = "0.0.0.0:9634"; static RING_ENVVAR: &'static str = "HAB_RING"; +static RING_KEY_ENVVAR: &'static str = "HAB_RING_KEY"; /// Creates a [Config](config/struct.Config.html) from global args /// and subcommand args. @@ -138,9 +140,17 @@ fn config_from_args(args: &ArgMatches, subcommand: &str, sub_args: &ArgMatches) let ring = match sub_args.value_of("ring") { Some(val) => Some(val.to_string()), None => { - match henv::var(RING_ENVVAR) { - Ok(val) => Some(val), - Err(_) => None, + match henv::var(RING_KEY_ENVVAR) { + Ok(val) => { + let ctx = Context::default(); + Some(try!(ctx.write_sym_key_from_str(&val))) + } + Err(_) => { + match henv::var(RING_ENVVAR) { + Ok(val) => Some(val), + Err(_) => None, + } + } } } }; From bdeda500144118f8fd55fa77f754eb659d7ce02c Mon Sep 17 00:00:00 2001 From: Fletcher Nichol Date: Wed, 4 May 2016 18:32:11 -0600 Subject: [PATCH 3/3] [hab] Add `ring key import` & `ring key export` subcommands. This change introduces two new subcommands which are intended to work together to support a server-based workflow, where "server" may mean a bare metal server, virtual machine, cloud instance, etc. The first is a `hab ring key export` subcommand which outputs the latest ring key's file contents to standard out. For example: hab ring key generate unicorns #=> Successfully generated ring key unicorns-20160505003452 hab ring key export unicorns #=> SYM-SEC-1 # unicorns-20160505003452 # # 9vXIC0OQyfP2+MYakkhma9eU9oAM+xHPIkylFn4fXj4= The flip side of this pairing is a `hab ring key import` subcommand which consumes a standard input stream containing a ring key's file contents, which it then writes to disk in the key cache with the correct file name. Extending the example from above: cat < Imported key unicorns-20160505003452 While this may not be terribly useful on a single system, pushing and pulling ring keys across systems is now much easier with SSH pipes. For example: # Push a ring key to another host hab ring key export unicorns | ssh node2 hab ring key import # Pull a ring key from another host ssh node2 hab ring key export unicorns | hab ring key import # Pull a ring key from another host and start a Supervisor env HAB_RING_KEY='$(ssh node1 hab ring key export unicorns)' hab-sup start core/redis Signed-off-by: Fletcher Nichol --- components/core/src/crypto.rs | 141 +++++++++++++++++++------ components/hab/Cargo.lock | 1 + components/hab/Cargo.toml | 1 + components/hab/src/cli.rs | 7 ++ components/hab/src/command/ring/key.rs | 41 +++++++ components/hab/src/main.rs | 20 +++- 6 files changed, 174 insertions(+), 37 deletions(-) diff --git a/components/core/src/crypto.rs b/components/core/src/crypto.rs index 00e29387ad..dfeed9dccc 100644 --- a/components/core/src/crypto.rs +++ b/components/core/src/crypto.rs @@ -222,7 +222,7 @@ use std::io::prelude::*; use std::io; use std::io::{BufReader, BufWriter}; use std::mem; -use std::path::Path; +use std::path::{Path, PathBuf}; use libsodium_sys; use rustc_serialize::base64::{STANDARD, ToBase64, FromBase64}; @@ -1127,6 +1127,46 @@ impl Context { self.read_key_bytes(&secret_keyfile) } + /// Returns the full path to the secret sym key given a key name with revision. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// extern crate habitat_core; + /// extern crate tempdir; + /// + /// use habitat_core::crypto::Context; + /// use std::fs::File; + /// use tempdir::TempDir; + /// + /// fn main() { + /// let key_cache = TempDir::new("key_cache").unwrap(); + /// let ctx = Context { key_cache: key_cache.path().to_string_lossy().into_owned() }; + /// let keyfile = key_cache.path().join("beyonce-20160504220722.sym.key"); + /// let _ = File::create(&keyfile).unwrap(); + /// + /// let path = ctx.get_sym_secret_key_path("beyonce-20160504220722").unwrap(); + /// assert_eq!(path, keyfile); + /// } + /// ``` + /// + /// # Errors + /// + /// * If no file exists at the the computed file path + pub fn get_sym_secret_key_path(&self, key_with_rev: &str) -> Result { + let secret_keyfile = self.mk_key_filename(&self.key_cache, + key_with_rev, + SECRET_SYM_KEY_SUFFIX); + let path = PathBuf::from(secret_keyfile); + if !path.is_file() { + return Err(Error::CryptoError(format!("No sym secret key found at {}", + path.display()))); + } + Ok(path) + } + /// Read a file into a Vec fn read_key_bytes(&self, keyfile: &str) -> Result> { let mut f = try!(File::open(keyfile)); @@ -1372,49 +1412,80 @@ impl Context { #[cfg(test)] mod test { - mod write_sym_key_from_str { - use std::env; - use std::fs::{self, File}; - use std::io::Read; - use std::path::{Path, PathBuf}; + use std::env; + use std::fs::File; + use std::io::Read; + use std::path::PathBuf; - use tempdir::TempDir; + use tempdir::TempDir; - use crypto::Context; + use crypto::Context; - static VALID_KEY: &'static str = "ring-key-valid-20160504220722.sym.key"; - static VALID_NAME: &'static str = "ring-key-valid-20160504220722"; + pub static VALID_KEY: &'static str = "ring-key-valid-20160504220722.sym.key"; + pub static VALID_NAME: &'static str = "ring-key-valid-20160504220722"; - fn random_ctx() -> (TempDir, Context) { - let tempdir = TempDir::new("key_cache").unwrap(); - let ctx = Context { key_cache: tempdir.path().to_string_lossy().into_owned() }; - (tempdir, ctx) + pub fn random_ctx() -> (TempDir, Context) { + let tempdir = TempDir::new("key_cache").unwrap(); + let ctx = Context { key_cache: tempdir.path().to_string_lossy().into_owned() }; + (tempdir, ctx) + } + + pub fn fixture(name: &str) -> PathBuf { + let file = env::current_exe() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("tests") + .join("fixtures") + .join(name); + if !file.is_file() { + panic!("No fixture {} exists!", file.display()); } + file + } - fn fixture(name: &str) -> PathBuf { - let file = env::current_exe() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("tests") - .join("fixtures") - .join(name); - if !file.is_file() { - panic!("No fixture {} exists!", file.display()); - } - file + pub fn fixture_as_string(name: &str) -> String { + let mut file = File::open(fixture(name)).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + content + } + + mod get_sym_secret_key_path { + use super::*; + + use std::fs; + + #[test] + fn returns_a_path() { + let (cache, ctx) = random_ctx(); + fs::copy(fixture(&format!("keys/{}", VALID_KEY)), + cache.path().join(VALID_KEY)) + .unwrap(); + + let result = ctx.get_sym_secret_key_path(VALID_NAME).unwrap(); + assert_eq!(result, cache.path().join(VALID_KEY)); } - fn fixture_as_string(name: &str) -> String { - let mut file = File::open(fixture(name)).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - content + #[test] + #[should_panic(expected = "No sym secret key found at")] + fn errors_when_key_doesnt_exist() { + let (_, ctx) = random_ctx(); + + ctx.get_sym_secret_key_path("nope-nope").unwrap(); } + } + + mod write_sym_key_from_str { + use super::*; + + use std::fs::{self, File}; + use std::io::Read; + use std::path::Path; #[test] fn writes_new_key_file() { diff --git a/components/hab/Cargo.lock b/components/hab/Cargo.lock index 210caca166..573584cfd6 100644 --- a/components/hab/Cargo.lock +++ b/components/hab/Cargo.lock @@ -3,6 +3,7 @@ name = "hab" version = "0.4.0" dependencies = [ "clap 2.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "habitat_common 0.4.0", "habitat_core 0.4.0", "habitat_depot_client 0.4.0", diff --git a/components/hab/Cargo.toml b/components/hab/Cargo.toml index e610854c2f..ca96665b8b 100644 --- a/components/hab/Cargo.toml +++ b/components/hab/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Adam Jacob ", "Jamie Winsor ", "Fletche build = "build.rs" [dependencies] +env_logger = "*" # Used to handle responses code back from depot-core hyper = "*" libc = "*" diff --git a/components/hab/src/cli.rs b/components/hab/src/cli.rs index 273d35ff00..010a712981 100644 --- a/components/hab/src/cli.rs +++ b/components/hab/src/cli.rs @@ -129,6 +129,13 @@ pub fn get() -> App<'static, 'static> { (@subcommand key => (about: "Commands relating to Habitat ring keys") (@setting ArgRequiredElseHelp) + (@subcommand export => + (about: "Outputs the latest ring key contents to stdout") + (@arg RING: +required +takes_value) + ) + (@subcommand import => + (about: "Reads a stdin stream containing ring key contents and writes the key to disk") + ) (@subcommand generate => (about: "Generates a Habitat ring key") (@arg RING: +required +takes_value) diff --git a/components/hab/src/command/ring/key.rs b/components/hab/src/command/ring/key.rs index 411e30def6..3deb6446ff 100644 --- a/components/hab/src/command/ring/key.rs +++ b/components/hab/src/command/ring/key.rs @@ -5,6 +5,47 @@ // the Software until such time that the Software is made available under an // open source license such as the Apache 2.0 License. +pub mod export { + use std::io; + use std::fs::File; + + use hcore::{self, crypto}; + + use error::{Error, Result}; + + pub fn start(ring: &str) -> Result<()> { + let ctx = crypto::Context::default(); + let mut candidates = try!(ctx.read_sym_keys(&ring)); + let latest = match candidates.len() { + 1 => candidates.remove(0), + _ => { + let msg = format!("Cannot find a suitable key for ring: {}", ring); + return Err(Error::HabitatCore(hcore::Error::CryptoError(msg))); + } + }; + + let path = try!(ctx.get_sym_secret_key_path(&latest.name_with_rev)); + let mut file = try!(File::open(&path)); + debug!("Streaming file contents of {} to standard out", + &path.display()); + try!(io::copy(&mut file, &mut io::stdout())); + Ok(()) + } +} + +pub mod import { + use hcore::crypto; + + use error::Result; + + pub fn start(content: &str) -> Result<()> { + let ctx = crypto::Context::default(); + let keyname = try!(ctx.write_sym_key_from_str(content)); + println!("Imported key {}", keyname); + Ok(()) + } +} + pub mod generate { use hcore::crypto; diff --git a/components/hab/src/main.rs b/components/hab/src/main.rs index 86fcf5a753..0f8c5e4445 100644 --- a/components/hab/src/main.rs +++ b/components/hab/src/main.rs @@ -10,6 +10,7 @@ extern crate habitat_common as common; extern crate habitat_depot_client as depot_client; #[macro_use] extern crate clap; +extern crate env_logger; extern crate hyper; #[macro_use] extern crate log; @@ -29,6 +30,7 @@ mod exec; mod gossip; use std::env; +use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -58,13 +60,14 @@ const HABITAT_USER_ENVVAR: &'static str = "HAB_USER"; const MAX_FILE_UPLOAD_SIZE_BYTES: u64 = 4096; fn main() { - if let Err(e) = run_hab() { + if let Err(e) = start() { println!("{}", e); std::process::exit(1) } } -fn run_hab() -> Result<()> { +fn start() -> Result<()> { + env_logger::init().unwrap(); try!(exec_subcommand_if_called()); let app_matches = cli::get().get_matches(); @@ -113,6 +116,8 @@ fn run_hab() -> Result<()> { match matches.subcommand() { ("key", Some(m)) => { match m.subcommand() { + ("export", Some(sc)) => try!(sub_ring_key_export(sc)), + ("import", Some(_)) => try!(sub_ring_key_import()), ("generate", Some(sc)) => try!(sub_ring_key_generate(sc)), _ => unreachable!(), } @@ -280,6 +285,17 @@ fn sub_package_install(m: &ArgMatches) -> Result<()> { Ok(()) } +fn sub_ring_key_export(m: &ArgMatches) -> Result<()> { + let ring = m.value_of("RING").unwrap(); + command::ring::key::export::start(ring) +} + +fn sub_ring_key_import() -> Result<()> { + let mut content = String::new(); + try!(io::stdin().read_to_string(&mut content)); + command::ring::key::import::start(&content) +} + fn sub_ring_key_generate(m: &ArgMatches) -> Result<()> { let ring = m.value_of("RING").unwrap(); command::ring::key::generate::start(ring)