Skip to content

Commit

Permalink
Merge pull request #424 from habitat-sh/fnichol/tranport-encryption
Browse files Browse the repository at this point in the history
Add `hab ring key generate` subcommand & prep crypto system for symmetric keys
  • Loading branch information
bookshelfdave committed Apr 22, 2016
2 parents ef84858 + 14a24ca commit d284679
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 0 deletions.
160 changes: 160 additions & 0 deletions components/core/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use rustc_serialize::hex::ToHex;
use sodiumoxide::init as nacl_init;
use sodiumoxide::crypto::sign;
use sodiumoxide::crypto::box_;
use sodiumoxide::crypto::secretbox;
use sodiumoxide::crypto::secretbox::Key as SymSecretKey;
use sodiumoxide::crypto::sign::ed25519::SecretKey as SigSecretKey;
use sodiumoxide::crypto::sign::ed25519::PublicKey as SigPublicKey;
use sodiumoxide::crypto::box_::curve25519xsalsa20poly1305::PublicKey as BoxPublicKey;
Expand All @@ -38,6 +40,7 @@ use util::perm;
/// ### Concepts and terminology:
/// - All public keys/certificates/signatures will be referred to as **public**.
/// - All secret or private keys will be referred to as **secret**.
/// - All symmetric encryption keys will be referred to as **secret**.
/// - The word `key` by itself does not indicate **public** or **secret**. The only
/// exception is if the word key appears as part of a file suffix, where it is then
/// considered the **secret key** file.
Expand All @@ -53,11 +56,17 @@ use util::perm;
/// such as deploying a package signed in a different origin into your own organization.
/// Abbreviated as "org" in CLI params and variable names.
/// - **Org vs Origin** - Habitat packages come from an origin and run in an organization
/// - **Ring** - The full set of Supervisors that communicate with each other.
/// - **Signing keys** - aka **sig** keys. These are used to sign and verify
/// packages. Contains a `sig.key` file suffix. Sig keys are NOT compatible with
/// box keys.
/// - **Box keys** - used for encryption/decryption of arbitrary data. Contains a
/// `.box.key` file suffix. Box keys are NOT compatible with sig keys.
/// - **Sym keys** - used for symmetric encryption, meaning that a shared secret is used to encrypt
/// a message into a cyphertext and that same secret is used later to decryt the cyphertext into
/// the original message.
/// - **Ring key** - A Sym key used when sending messages between the Supervisors to prevent a
/// third party from intercepting the traffic.
/// - **Key revisions** - Habitat can use several keys for any given user, service,
/// or origin via different revision numbers. Revision numbers appear following the
/// key name and are in the format
Expand Down Expand Up @@ -91,6 +100,12 @@ use util::perm;
/// <service_name>.<group>@<organization>-<revision>.box.key
/// ```
///
/// - Ring key
///
/// ```text
/// <ring_name>-<revision>.sym.key
/// ```
///
/// Example origin key file names ("sig" keys):
///
/// ```text
Expand All @@ -113,6 +128,12 @@ use util::perm;
/// [email protected]
/// ```
///
/// Example Ring keys:
///
/// ```text
/// staging-201603312016.sym.key
/// ```
///
///
/// ### Habitat signed artifact format
///
Expand Down Expand Up @@ -154,6 +175,7 @@ use util::perm;
/// tail -n +4 /tmp/somefile.hart > somefile.tar
/// # start at line 4, skipping the first 3 plaintext lines.
/// ```
///
/// ### Habitat encrypted payload format
///
/// The first 4 lines of an encrypted payload are as follows:
Expand All @@ -171,6 +193,21 @@ use util::perm;
/// nonce_base64\n
/// <ciphertext_base64>
/// ```
///
/// ### Habitat Ring key format
///
/// There are 3 lines, that is 3 parts that are separtated by a newline character `\n`. They are as
/// follows:
///
/// 0. Encrypted format version #, the current version is `1`
/// 1. The ring key name, including revision
/// 2. The key itself, which is base64-encoded
///
/// ```text
/// SYM-1\n
/// staging-20160405144945\n
/// <symkey_base64>
/// ```
/// The suffix on the end of a public sig/box file
static PUB_KEY_SUFFIX: &'static str = "pub";
Expand All @@ -181,6 +218,9 @@ static SECRET_SIG_KEY_SUFFIX: &'static str = "sig.key";
/// The suffix on the end of a secret box file
static SECRET_BOX_KEY_SUFFIX: &'static str = "box.key";

/// The suffix on the end of a secret symmetric key file
static SECRET_SYM_KEY_SUFFIX: &'static str = "sym.key";

/// The hashing function we're using during sign/verify
/// See also: https://download.libsodium.org/doc/hashing/generic_hashing.html
static SIG_HASH_TYPE: &'static str = "BLAKE2b";
Expand All @@ -196,9 +236,12 @@ static SECRET_KEY_PERMISSIONS: &'static str = "0400";

static ENCRYPTED_PAYLOAD_VERSION: &'static str = "BOX-0.1.0";

static SYM_KEY_VERSION: &'static str = "SYM-1";

const BUF_SIZE: usize = 1024;

/// You can ask for both keys at once
#[derive(Clone)]
pub struct KeyPair<P, S> {
/// The name of the key, ex: "habitat"
pub name: String,
Expand All @@ -224,6 +267,7 @@ impl<P, S> KeyPair<P, S> {

pub type SigKeyPair = KeyPair<SigPublicKey, SigSecretKey>;
pub type BoxKeyPair = KeyPair<BoxPublicKey, BoxSecretKey>;
pub type SymKey = KeyPair<(), SymSecretKey>;

/// If an env var is set, then return it's value.
/// If it's not, return the default
Expand Down Expand Up @@ -562,6 +606,14 @@ impl Context {
Ok(keyname)
}

/// generate a ring key, return the name of the key we generated
pub fn generate_ring_sym_key(&self, ring: &str) -> Result<String> {
let revision = self.mk_revision_string();
let keyname = self.mk_ring_sym_key_name(&revision, &ring);
debug!("new ring key name = {}", &keyname);
let _ = try!(self.generate_sym_key_file(&keyname));
Ok(keyname)
}

/// generates a revision string in the form:
/// `{year}{month}{day}{hour24}{minute}{second}`
Expand Down Expand Up @@ -592,6 +644,10 @@ impl Context {
format!("{}-{}", user, revision)
}

fn mk_ring_sym_key_name(&self, revision: &str, ring: &str) -> String {
format!("{}-{}", ring, revision)
}

fn generate_box_keypair_files(&self, keyname: &str) -> Result<(BoxPublicKey, BoxSecretKey)> {
let (pk, sk) = box_::gen_keypair();

Expand Down Expand Up @@ -621,6 +677,18 @@ impl Context {
Ok((pk, sk))
}

fn generate_sym_key_file(&self, keyname: &str) -> Result<SymSecretKey> {
let sk = secretbox::gen_key();

let secret_keyfile = self.mk_key_filename(&self.key_cache, keyname, SECRET_SYM_KEY_SUFFIX);
debug!("secret ring keyfile = {}", &secret_keyfile);

try!(self.write_sym_key_file(&keyname,
&secret_keyfile,
&sk[..].to_base64(STANDARD).into_bytes()));
Ok(sk)
}

fn write_keypair_files<K1: AsRef<Path>, K2: AsRef<Path>>(&self,
public_keyfile: K1,
public_content: &Vec<u8>,
Expand Down Expand Up @@ -661,6 +729,30 @@ impl Context {
Ok(())
}

fn write_sym_key_file<K: AsRef<Path>>(&self,
keyname: &str,
secret_keyfile: K,
secret_content: &Vec<u8>)
-> Result<()> {
if let Some(sk_dir) = secret_keyfile.as_ref().parent() {
try!(fs::create_dir_all(sk_dir));
} else {
return Err(Error::BadKeyPath(secret_keyfile.as_ref().to_string_lossy().into_owned()));
}

if secret_keyfile.as_ref().exists() && secret_keyfile.as_ref().is_file() {
return Err(Error::CryptoError(format!("Secret keyfile already exists {}",
secret_keyfile.as_ref().display())));
}

let secret_file = try!(File::create(secret_keyfile.as_ref()));
let mut secret_writer = BufWriter::new(&secret_file);
try!(write!(secret_writer, "{}\n{}\n\n", SYM_KEY_VERSION, keyname));
try!(secret_writer.write_all(secret_content));
try!(perm::set_permissions(secret_keyfile, SECRET_KEY_PERMISSIONS));
Ok(())
}

/// *********************************************
/// Key reading functions
/// *******************************************
Expand Down Expand Up @@ -723,6 +815,25 @@ impl Context {
Ok(key_pairs)
}

pub fn read_sym_keys(&self, keyname: &str) -> Result<Vec<SymKey>> {
let revisions = try!(self.get_key_revisions(keyname));
let mut keys = Vec::new();
for rev in &revisions {
debug!("Attempting to read key rev {} for {}", rev, keyname);
let sk = match self.get_sym_secret_key(rev) {
Ok(k) => Some(k),
Err(e) => {
// Not an error, just continue
debug!("Can't find secret key for rev {}: {}", rev, e);
None
}
};
let k = SymKey::new(keyname.to_string(), rev.clone(), None, sk);
keys.push(k);
}
Ok(keys)
}


pub fn get_sig_secret_key(&self, key_with_rev: &str) -> Result<SigSecretKey> {
let bytes = try!(self.get_sig_secret_key_bytes(key_with_rev));
Expand Down Expand Up @@ -768,6 +879,17 @@ impl Context {
}
}

pub fn get_sym_secret_key(&self, key_with_rev: &str) -> Result<SymSecretKey> {
let bytes = try!(self.get_sym_secret_key_bytes(key_with_rev));
match SymSecretKey::from_slice(&bytes) {
Some(sk) => Ok(sk),
None => {
return Err(Error::CryptoError(format!("Can't read sym secret key for {}",
key_with_rev)))
}
}
}

fn get_box_public_key_bytes(&self, key_with_rev: &str) -> Result<Vec<u8>> {
let public_keyfile = self.mk_key_filename(&self.key_cache, key_with_rev, PUB_KEY_SUFFIX);
self.read_key_bytes(&public_keyfile)
Expand All @@ -792,6 +914,13 @@ impl Context {
self.read_key_bytes(&secret_keyfile)
}

fn get_sym_secret_key_bytes(&self, key_with_rev: &str) -> Result<Vec<u8>> {
let secret_keyfile = self.mk_key_filename(&self.key_cache,
key_with_rev,
SECRET_SYM_KEY_SUFFIX);
self.read_key_bytes_after_header(&secret_keyfile)
}

/// Read a file into a Vec<u8>
fn read_key_bytes(&self, keyfile: &str) -> Result<Vec<u8>> {
let mut f = try!(File::open(keyfile));
Expand All @@ -809,6 +938,29 @@ impl Context {
}
}

fn read_key_bytes_after_header(&self, keyfile: &str) -> Result<Vec<u8>> {
let mut f = try!(File::open(keyfile));
let mut s = String::new();
if try!(f.read_to_string(&mut s)) <= 0 {
return Err(Error::CryptoError("Can't read key bytes".to_string()));
}
let start_index = match s.find("\n\n") {
Some(i) => i + 1,
None => {
return Err(Error::CryptoError(format!("Malformed key contents for: {}", keyfile)))
}
};

match s[start_index..].as_bytes().from_base64() {
Ok(keybytes) => Ok(keybytes),
Err(e) => {
return Err(Error::CryptoError(format!("Can't read raw key from {}: {}",
keyfile,
e)))
}
}
}


/// If a key "belongs" to a filename revision, then add the full stem of the
/// file (without path, without .suffix)
Expand Down Expand Up @@ -844,6 +996,14 @@ impl Context {
1);
candidates.insert(stem.to_string());
}
} else if filename.ends_with(SECRET_SYM_KEY_SUFFIX) {
// -1 for the '.' before the suffix
if filename.starts_with(keyname) {
let (stem, _) = filename.split_at(filename.chars().count() -
SECRET_SYM_KEY_SUFFIX.chars().count() -
1);
candidates.insert(stem.to_string());
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions components/hab/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ pub fn get() -> App<'static, 'static> {
(@setting ArgRequiredElseHelp)
(subcommand: sub_package_install())
)
(@subcommand ring =>
(about: "Commands relating to Habitat rings")
(@setting ArgRequiredElseHelp)
(@subcommand key =>
(about: "Commands relating to Habitat ring keys")
(@setting ArgRequiredElseHelp)
(@subcommand generate =>
(about: "Generates a Habitat ring key")
(@arg RING: +required +takes_value)
)
)
)
(@subcommand sup =>
(about: "Commands relating to the Habitat Supervisor")
)
Expand Down
1 change: 1 addition & 0 deletions components/hab/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

pub mod artifact;
pub mod config;
pub mod ring;
19 changes: 19 additions & 0 deletions components/hab/src/command/ring/key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers
//
// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc.
// and the party accessing this file ("Licensee") apply to Licensee's use of
// 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 generate {
use hcore::crypto;

use error::Result;

pub fn start(ring: &str) -> Result<()> {
let crypto_ctx = crypto::Context::default();
let keyname = try!(crypto_ctx.generate_ring_sym_key(ring));
println!("Successfully generated ring key {}", keyname);
Ok(())
}
}
8 changes: 8 additions & 0 deletions components/hab/src/command/ring/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright:: Copyright (c) 2015-2016 The Habitat Maintainers
//
// The terms of the Evaluation Agreement (Habitat) between Chef Software Inc.
// and the party accessing this file ("Licensee") apply to Licensee's use of
// 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 key;
16 changes: 16 additions & 0 deletions components/hab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ fn run_hab() -> Result<()> {
_ => unreachable!(),
}
}
("ring", Some(matches)) => {
match matches.subcommand() {
("key", Some(m)) => {
match m.subcommand() {
("generate", Some(sc)) => try!(sub_ring_key_generate(sc)),
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}
("service", Some(matches)) => {
match matches.subcommand() {
("key", Some(m)) => {
Expand Down Expand Up @@ -188,6 +199,11 @@ fn sub_package_install(m: &ArgMatches) -> Result<()> {
Ok(())
}

fn sub_ring_key_generate(m: &ArgMatches) -> Result<()> {
let ring = m.value_of("RING").unwrap();
command::ring::key::generate::start(ring)
}

fn sub_service_key_generate(m: &ArgMatches) -> Result<()> {
let org = try!(org_param_or_env(&m));
let service_group = m.value_of("SERVICE_GROUP").unwrap(); // clap required
Expand Down

0 comments on commit d284679

Please sign in to comment.