Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attempt to download signing keys upon artifact installation, new origin keys Depot API #488

Merged
merged 1 commit into from
May 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions components/common/src/command/package/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use hcore::crypto;
use hcore::fs::CACHE_ARTIFACT_PATH;
use hcore::package::{PackageArchive, PackageIdent, PackageInstall};
use depot_core::data_object;
Expand Down Expand Up @@ -75,7 +76,7 @@ pub fn from_archive<P: AsRef<Path>>(url: &str, path: &P) -> Result<()> {
for dep in try!(archive.tdeps()) {
try!(install_from_depot(url, &dep, dep.as_ref()));
}
try!(install_from_archive(archive, &ident));
try!(install_from_archive(url, archive, &ident));
Ok(())
}

Expand All @@ -98,23 +99,48 @@ fn install_from_depot<P: AsRef<PackageIdent>>(url: &str,
ident.as_ref(),
CACHE_ARTIFACT_PATH));
let ident = try!(archive.ident());
try!(archive.verify());
try!(verify(url, &archive, &ident));
try!(archive.unpack());
println!("Installed {}", ident);
}
}
Ok(())
}

fn install_from_archive(archive: PackageArchive, ident: &PackageIdent) -> Result<()> {
fn install_from_archive(url: &str, archive: PackageArchive, ident: &PackageIdent) -> Result<()> {
match PackageInstall::load(ident.as_ref(), None) {
Ok(_) => {
println!("Package {} already installed", ident);
}
Err(_) => {
try!(verify(url, &archive, &ident));
try!(archive.unpack());
println!("Installed {}", ident);
}
}
Ok(())
}

/// get the signer for the artifact and see if we have the key locally.
/// If we don't, attempt to download it from the depot.
fn verify(url: &str, archive: &PackageArchive, ident: &PackageIdent) -> Result<()> {
let crypto_ctx = crypto::Context::default();
let (signer_origin, signer_revision) = try!(crypto_ctx.get_artifact_signer(&archive.path));
let signer_key_with_rev = format!("{}-{}", signer_origin, signer_revision);

if let Err(_) = crypto_ctx.get_sig_public_key(&signer_key_with_rev) {
// we don't have the key locally, so try and download it before verification
println!("Can't find {} origin key in local key cache, fetching it from the depot", &signer_key_with_rev);
try!(depot_client::get_origin_key(url,
&signer_origin,
&signer_revision,
&crypto::nacl_key_dir()));
}

try!(archive.verify());
println!("Successful verification of {}/{} signed by {}",
&ident.origin,
&ident.name,
&signer_key_with_rev);
Ok(())
}
104 changes: 99 additions & 5 deletions components/core/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
//! - All public keys, certificates, and signatures are to be referred to as **public**.
//! - All secret or private keys are to be referred to as **secret**.
//! - All symmetric encryption keys are to be referred to as **secret**.
//! - The word `key` by itself does not indicate whether it is **public** or **secret**. The only
//! exception is if the word `key` appears as part of a file suffix, where it is then considered as
//! a **secret key** file.
//! - In general, the word `key` by itself does not indicate something as
//! **public** or **secret**. The exceptions to this rule are as follows:
//! - if the word key appears in a URL, then we are referring to a public key to
//! conform to other API's that offer similar public key downloading functionality.
//! - the word `key` appears as part of a file suffix, where it is then considered as
//! a **secret key** file.
//! - Referring to keys (by example):
//! - A key name: `habitat`
//! - A key rev: `201603312016`
Expand Down Expand Up @@ -313,7 +316,8 @@ pub type SigKeyPair = KeyPair<SigPublicKey, SigSecretKey>;
pub type BoxKeyPair = KeyPair<BoxPublicKey, BoxSecretKey>;
pub type SymKey = KeyPair<(), SymSecretKey>;

enum KeyType {
#[derive(PartialEq, Eq)]
pub enum KeyType {
Sig,
Box,
Sym,
Expand All @@ -332,10 +336,83 @@ fn env_var_or_default(env_var: &str, default: &str) -> String {
/// Return the canonical location for nacl keys
/// This value can be overridden via CACHE_KEY_PATH_ENV_VAR,
/// which is useful for testing
fn nacl_key_dir() -> String {
pub fn nacl_key_dir() -> String {
env_var_or_default(CACHE_KEY_PATH_ENV_VAR, CACHE_KEY_PATH)
}

/// takes a Path to a key, and returns the origin and revision in a tuple
/// ex: /src/foo/core-xyz-20160423193745.pub yields ("core", "20160423193745")
/// TODO DP: this should be in a crypto::utils package
pub fn parse_origin_key_filename<P: AsRef<Path>>(keyfile: P) -> Result<(String, String)> {
let stem = match keyfile.as_ref().file_stem().and_then(|s| s.to_str()) {
Some(s) => s,
None => return Err(Error::CryptoError("Can't parse key filename".to_string()))
};

parse_origin_key_name(stem)
}

/// takes a string in the form origin-revision and returns a
/// tuple in the form (origin, revision)
pub fn parse_origin_key_name(origin_rev: &str) -> Result<(String, String)> {
let mut chunks: Vec<&str> = origin_rev.split("-").collect();
if chunks.len() < 2 {
return Err(Error::CryptoError("Invalid origin key name".to_string()))
}
let rev = match chunks.pop() {
Some(r) => r,
None => return Err(Error::CryptoError("Invalid origin key revision".to_string()))
};
let origin = chunks.join("-").trim().to_owned();
Ok((origin, rev.trim().to_owned()))
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's conventional to put all tests within a module called tests inside the file you're writing the tests for

#[cfg(test)]
mod tests {
  // this line will get you access to the outer module's functions/structs
  use super::*;
  #[test]
  fn my_test() {  }
}

#[test]
fn test_parse_origin_key_name() {
assert!(parse_origin_key_name("foo").is_ok() == false);
match parse_origin_key_name("foo-20160423193745\n") {
Ok((origin, rev)) => {
assert!(origin == "foo");
assert!(rev == "20160423193745");
}
Err(_) => panic!("Fail!")
};


match parse_origin_key_name("foo-bar-baz-20160423193745") {
Ok((origin, rev)) => {
assert!(origin == "foo-bar-baz");
assert!(rev == "20160423193745");
}
Err(_) => panic!("Fail!")
};
}

#[test]
fn test_parse_origin_key_filename() {
if parse_origin_key_filename(Path::new("/tmp/foo.pub")).is_ok() {
panic!("Shouldn't match")
};


match parse_origin_key_filename(Path::new("/tmp/core-20160423193745.pub")) {
Ok((origin, rev)) => {
assert!(origin == "core");
assert!(rev == "20160423193745");
}
Err(_) => panic!("Bad filename")
};

match parse_origin_key_filename(Path::new("/tmp/multi-dash-origin-20160423193745.pub")) {
Ok((origin, rev)) => {
assert!(origin == "multi-dash-origin");
assert!(rev == "20160423193745");
}
Err(_) => panic!("Bad filename")
};
}


/// A Context makes crypto operations available centered on a given
/// key cache directory.
#[derive(Debug)]
Expand Down Expand Up @@ -446,6 +523,23 @@ impl Context {
Ok(reader)
}

/// opens up a .hart file and returns the name of the (key, revision)
/// of the signer
pub fn get_artifact_signer<P: AsRef<Path>>(&self, infilename: P) -> Result<(String, String)> {
let f = try!(File::open(infilename));
let mut your_format_version = String::new();
let mut your_key_name = String::new();
let mut reader = BufReader::new(f);
if try!(reader.read_line(&mut your_format_version)) <= 0 {
return Err(Error::CryptoError("Can't read format version".to_string()));
}
if try!(reader.read_line(&mut your_key_name)) <= 0 {
return Err(Error::CryptoError("Can't read keyname".to_string()));
}
let origin_rev_pair = try!(parse_origin_key_name(&your_key_name));
Ok(origin_rev_pair)
}

/// verify the crypto signature of a .hart file
pub fn artifact_verify(&self, infilename: &str) -> Result<()> {

Expand Down
3 changes: 3 additions & 0 deletions components/depot-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum Error {
IO(io::Error),
NoFilePart,
NoXFilename,
RemoteOriginKeyNotFound(String),
RemotePackageNotFound(package::PackageIdent),
WriteSyncFailed,
}
Expand All @@ -40,6 +41,7 @@ impl fmt::Display for Error {
not have one")
}
Error::NoXFilename => format!("Invalid download from a Depot - missing X-Filename header"),
Error::RemoteOriginKeyNotFound(ref e) => format!("{}", e),
Error::RemotePackageNotFound(ref pkg) => {
if pkg.fully_qualified() {
format!("Cannot find package in any sources: {}", pkg)
Expand All @@ -62,6 +64,7 @@ impl error::Error for Error {
Error::IO(ref err) => err.description(),
Error::NoFilePart => "An invalid path was passed - we needed a filename, and this path does not have one",
Error::NoXFilename => "Invalid download from a Depot - missing X-Filename header",
Error::RemoteOriginKeyNotFound(_) => "Remote origin key not found",
Error::RemotePackageNotFound(_) => "Cannot find a package in any sources",
Error::WriteSyncFailed => "Could not write to destination; bytes written was 0 on a non-0 buffer",
}
Expand Down
42 changes: 35 additions & 7 deletions components/depot-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,38 @@ use rustc_serialize::json;
/// * Key cannot be found
/// * Remote Depot is not available
/// * File cannot be created and written to
pub fn fetch_key(depot: &str, key: &str, path: &str) -> Result<String> {
let url = Url::parse(&format!("{}/keys/{}", depot, key)).unwrap();
download(key, url, path)
pub fn get_origin_key(depot: &str, origin: &str, revision: &str, path: &str) -> Result<String> {
let url = Url::parse(&format!("{}/origins/{}/keys/{}", depot, origin, revision)).unwrap();
debug!("get_origin_key URL = {}", &url);
let fname = format!("{}/{}-{}.pub", &path, &origin, &revision);
debug!("Output filename = {}", &fname);
download(&fname, url, path)
}

/// Download all public keys for a given origin.
///
/// # Failures
///
/// * Origin cannot be found
/// * Remote Depot is not available
/// * File write errors
pub fn get_origin_keys(depot: &str, origin: &str, path: &str) -> Result<()> {
let url = format!("{}/origins/{}/keys", depot, origin);
debug!("URL = {}", &url);
let client = Client::new();
let request = client.get(&url);
let mut res = try!(request.send());
if res.status != hyper::status::StatusCode::Ok {
return Err(Error::RemoteOriginKeyNotFound(origin.to_string()))
};

let mut encoded = String::new();
try!(res.read_to_string(&mut encoded));
let revisions: Vec<data_object::OriginKeyIdent> = json::decode(&encoded).unwrap();
for rev in &revisions {
try!(get_origin_key(depot, origin, &rev.revision, path));
}
Ok(())
}

/// Download the latest release of a package.
Expand Down Expand Up @@ -92,16 +121,15 @@ pub fn show_package(depot: &str, ident: &PackageIdent) -> Result<data_object::Pa
Ok(package)
}

/// Upload a public key to a remote Depot.
/// Upload a public origin key to a remote Depot.
///
/// # Failures
///
/// * Remote Depot is not available
/// * File cannot be read
pub fn put_key(depot: &str, path: &Path) -> Result<()> {
pub fn post_origin_key(depot: &str, origin: &str, revision: &str, path: &Path) -> Result<()> {
let mut file = try!(File::open(path));
let file_name = try!(path.file_name().ok_or(Error::NoFilePart));
let url = Url::parse(&format!("{}/keys/{}", depot, file_name.to_string_lossy())).unwrap();
let url = Url::parse(&format!("{}/origins/{}/keys/{}", depot, origin, revision)).unwrap();
upload(url, &mut file)
}

Expand Down
31 changes: 31 additions & 0 deletions components/depot-core/src/data_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,34 @@ impl AsRef<package::PackageIdent> for Package {
self.ident.as_ref()
}
}


#[derive(RustcEncodable, RustcDecodable, Eq, PartialEq, Debug, Clone)]
pub struct OriginKeyIdent {
pub origin: String,
pub revision: String,
pub location: String,
}

impl OriginKeyIdent {
pub fn new(origin: String, revision: String, location: String) -> OriginKeyIdent {
OriginKeyIdent {
origin: origin,
revision: revision,
location: location,
}
}
}

impl fmt::Display for OriginKeyIdent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}-{}", self.origin, self.revision)
}
}

impl AsRef<OriginKeyIdent> for OriginKeyIdent {
fn as_ref(&self) -> &OriginKeyIdent {
self
}
}

10 changes: 10 additions & 0 deletions components/depot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,15 @@ extern crate rustc_serialize;

pub mod data_object;

use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset};

header! { (XFileName, "X-Filename") => [String] }
header! { (ETag, "ETag") => [String] }

/// convenience function for setting Content-Disposition
pub fn set_disposition(headers: &mut Headers, filename: String, charset: Charset) -> () {
headers.set(ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename( charset, None, filename.into_bytes())],
});
}
Loading