Skip to content

Commit

Permalink
attempt to download signing keys upon artifact installation
Browse files Browse the repository at this point in the history
- updated depot key api
- added raml WIP doc for depot API
- set Content-Disposition for package + key download
- removed GPG key installation from test setup
- attempt to download keys upon verify failure
- add /origins/:origin/keys/latest route

Signed-off-by: Dave Parfitt <[email protected]>

Pull request: #488
Approved by: reset
  • Loading branch information
Dave Parfitt authored and jtimberman committed Jun 12, 2016
1 parent 7454518 commit 302d7ea
Show file tree
Hide file tree
Showing 14 changed files with 662 additions and 68 deletions.
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 @@ -314,7 +317,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 @@ -333,10 +337,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()))
}

#[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 @@ -447,6 +524,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

0 comments on commit 302d7ea

Please sign in to comment.