diff --git a/tuftool/Cargo.toml b/tuftool/Cargo.toml index e792829a..e5a82d90 100644 --- a/tuftool/Cargo.toml +++ b/tuftool/Cargo.toml @@ -34,6 +34,7 @@ tempfile = "3.1.0" url = "2.1.0" walkdir = "2.2.9" tough = { version = "0.5.0", path = "../tough", features = ["http"] } +tough-ssm = { version = "0.1.0", path = "../tough-ssm" } tokio = "0.2.13" [dev-dependencies] diff --git a/tuftool/src/create.rs b/tuftool/src/create.rs index 946a6e3a..ec34463f 100644 --- a/tuftool/src/create.rs +++ b/tuftool/src/create.rs @@ -6,7 +6,7 @@ use crate::error::{self, Result}; use crate::key::RootKeys; use crate::metadata; use crate::root_digest::RootDigest; -use crate::source::KeySource; +use crate::source::parse_key_source; use chrono::{DateTime, Utc}; use maplit::hashmap; use rayon::prelude::*; @@ -20,6 +20,7 @@ use std::io::Read; use std::num::{NonZeroU64, NonZeroUsize}; use std::path::{Path, PathBuf}; use structopt::StructOpt; +use tough::key_source::KeySource; use tough::schema::{ decoded::Decoded, Hashes, Role, Snapshot, SnapshotMeta, Target, Targets, Timestamp, TimestampMeta, @@ -37,8 +38,8 @@ pub(crate) struct CreateArgs { jobs: Option, /// Key files to sign with - #[structopt(short = "k", long = "key", required = true)] - keys: Vec, + #[structopt(short = "k", long = "key", required = true, parse(try_from_str = parse_key_source))] + keys: Vec>, /// Version of snapshot.json file #[structopt(long = "snapshot-version")] diff --git a/tuftool/src/deref.rs b/tuftool/src/deref.rs deleted file mode 100644 index aa64786c..00000000 --- a/tuftool/src/deref.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Stable-supported shim for [`Option::deref`] ([tracking issue][inner_deref]). -//! -//! [inner_deref]: https://github.com/rust-lang/rust/issues/50264 - -use std::ops::Deref; - -pub(crate) trait OptionDeref { - fn deref_shim(&self) -> Option<&T::Target>; -} - -impl OptionDeref for Option { - fn deref_shim(&self) -> Option<&T::Target> { - self.as_ref().map(Deref::deref) - } -} diff --git a/tuftool/src/error.rs b/tuftool/src/error.rs index 5e2effb8..369e907e 100644 --- a/tuftool/src/error.rs +++ b/tuftool/src/error.rs @@ -6,9 +6,6 @@ use snafu::{Backtrace, Snafu}; use std::path::PathBuf; -#[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] -use crate::deref::OptionDeref; - pub(crate) type Result = std::result::Result; #[derive(Debug, Snafu)] @@ -124,14 +121,6 @@ pub(crate) enum Error { backtrace: Backtrace, }, - #[snafu(display("{}: {}", path.display(), source))] - Key { - path: PathBuf, - #[snafu(source(from(Error, Box::new)))] - #[snafu(backtrace)] - source: Box, - }, - #[snafu(display("Duplicate key ID: {}", key_id))] KeyDuplicate { key_id: String, @@ -150,6 +139,12 @@ pub(crate) enum Error { backtrace: Backtrace, }, + #[snafu(display("Unable to parse keypair: {}", source))] + KeyPairFromKeySource { + source: Box, + backtrace: Backtrace, + }, + #[snafu(display("Unable to match any of the provided keys with root.json"))] KeysNotFoundInRoot { backtrace: Backtrace }, @@ -200,28 +195,6 @@ pub(crate) enum Error { backtrace: Backtrace, }, - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display("Error creating AWS credentials provider: {}", source))] - RusotoCreds { - source: rusoto_credential::CredentialsError, - backtrace: Backtrace, - }, - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display("Unknown AWS region \"{}\": {}", region, source))] - RusotoRegion { - region: String, - source: rusoto_core::region::ParseRegionError, - backtrace: Backtrace, - }, - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display("Error creating AWS request dispatcher: {}", source))] - RusotoTls { - source: rusoto_core::request::TlsError, - backtrace: Backtrace, - }, - #[snafu(display("Failed to sign message"))] Sign { source: tough::error::Error, @@ -234,41 +207,6 @@ pub(crate) enum Error { backtrace: Backtrace, }, - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display( - "Failed to get aws-ssm://{}{}: {}", - profile.deref_shim().unwrap_or(""), - parameter_name, - source, - ))] - SsmGetParameter { - profile: Option, - parameter_name: String, - source: rusoto_core::RusotoError, - backtrace: Backtrace, - }, - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display( - "Failed to put aws-ssm://{}{}: {}", - profile.deref_shim().unwrap_or(""), - parameter_name, - source, - ))] - SsmPutParameter { - profile: Option, - parameter_name: String, - source: rusoto_core::RusotoError, - backtrace: Backtrace, - }, - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - #[snafu(display("Missing field in SSM response: {}", field))] - SsmMissingField { - field: &'static str, - backtrace: Backtrace, - }, - #[snafu(display("Target not found: {}", target))] TargetNotFound { target: String, @@ -309,6 +247,12 @@ pub(crate) enum Error { backtrace: Backtrace, }, + #[snafu(display("Failed write: {}", source))] + WriteKeySource { + source: Box, + backtrace: Backtrace, + }, + #[snafu(display("Failed writing target data to disk: {}", source))] WriteTarget { source: std::io::Error, diff --git a/tuftool/src/main.rs b/tuftool/src/main.rs index 2b0661a4..732d610c 100644 --- a/tuftool/src/main.rs +++ b/tuftool/src/main.rs @@ -13,7 +13,6 @@ mod create; mod datetime; -mod deref; mod download; mod error; mod key; @@ -23,7 +22,6 @@ mod root; mod root_digest; mod sign; mod source; -mod ssm; use crate::error::Result; use snafu::{ErrorCompat, OptionExt, ResultExt}; diff --git a/tuftool/src/refresh.rs b/tuftool/src/refresh.rs index 668a796a..313abbf7 100644 --- a/tuftool/src/refresh.rs +++ b/tuftool/src/refresh.rs @@ -5,7 +5,7 @@ use crate::datetime::parse_datetime; use crate::error::{self, Result}; use crate::metadata; use crate::root_digest::RootDigest; -use crate::source::KeySource; +use crate::source::parse_key_source; use chrono::{DateTime, Utc}; use maplit::hashmap; use ring::rand::SystemRandom; @@ -15,6 +15,7 @@ use std::fs::File; use std::num::{NonZeroU64, NonZeroUsize}; use std::path::PathBuf; use structopt::StructOpt; +use tough::key_source::KeySource; use tough::schema::{Hashes, Snapshot, SnapshotMeta, Targets, Timestamp, TimestampMeta}; use tough::{FilesystemTransport, HttpTransport, Limits, Repository, Transport}; use url::Url; @@ -42,8 +43,8 @@ pub(crate) struct RefreshArgs { jobs: Option, /// Key files to sign with - #[structopt(short = "k", long = "key", required = true)] - keys: Vec, + #[structopt(short = "k", long = "key", required = true, parse(try_from_str = parse_key_source))] + keys: Vec>, /// Version of snapshot.json file #[structopt(long = "snapshot-version")] diff --git a/tuftool/src/root.rs b/tuftool/src/root.rs index 2bcd97b7..11211238 100644 --- a/tuftool/src/root.rs +++ b/tuftool/src/root.rs @@ -3,7 +3,7 @@ use crate::datetime::parse_datetime; use crate::error::{self, Result}; -use crate::source::KeySource; +use crate::source::parse_key_source; use crate::{load_file, write_file}; use chrono::{DateTime, Timelike, Utc}; use maplit::hashmap; @@ -12,6 +12,7 @@ use std::collections::HashMap; use std::num::NonZeroU64; use std::path::PathBuf; use structopt::StructOpt; +use tough::key_source::KeySource; use tough::schema::decoded::{Decoded, Hex}; use tough::schema::{key::Key, RoleKeys, RoleType, Root, Signed}; use tough::sign::{parse_keypair, Sign}; @@ -51,7 +52,8 @@ pub(crate) enum Command { /// Path to root.json path: PathBuf, /// The new key - key_path: KeySource, + #[structopt(parse(try_from_str = parse_key_source))] + key_source: Box, /// The role to add the key to #[structopt(short = "r", long = "role")] roles: Vec, @@ -71,7 +73,8 @@ pub(crate) enum Command { /// Path to root.json path: PathBuf, /// Where to write the new key - key_path: KeySource, + #[structopt(parse(try_from_str = parse_key_source))] + key_source: Box, /// Bit length of new key #[structopt(short = "b", long = "bits", default_value = "2048")] bits: u16, @@ -113,16 +116,16 @@ impl Command { Command::AddKey { path, roles, - key_path, - } => Command::add_key(path, roles, key_path), + key_source, + } => Command::add_key(path, roles, key_source), Command::RemoveKey { path, key_id, role } => Command::remove_key(path, key_id, *role), Command::GenRsaKey { path, roles, - key_path, + key_source, bits, exponent, - } => Command::gen_rsa_key(path, roles, key_path, *bits, *exponent), + } => Command::gen_rsa_key(path, roles, key_source, *bits, *exponent), } } @@ -181,9 +184,13 @@ impl Command { write_file(path, &root) } - fn add_key(path: &PathBuf, roles: &[RoleType], key_path: &KeySource) -> Result<()> { + #[allow(clippy::borrowed_box)] + fn add_key(path: &PathBuf, roles: &[RoleType], key_source: &Box) -> Result<()> { let mut root: Signed = load_file(path)?; - let key_pair = key_path.as_public_key()?; + let key_pair = key_source + .as_sign() + .context(error::KeyPairFromKeySource)? + .tuf_key(); let key_id = hex::encode(add_key(&mut root.signed, roles, key_pair)?); clear_sigs(&mut root); println!("{}", key_id); @@ -214,10 +221,11 @@ impl Command { write_file(path, &root) } + #[allow(clippy::borrowed_box)] fn gen_rsa_key( path: &PathBuf, roles: &[RoleType], - key_path: &KeySource, + key_source: &Box, bits: u16, exponent: u32, ) -> Result<()> { @@ -247,7 +255,9 @@ impl Command { let key_pair = parse_keypair(stdout.as_bytes()).context(error::KeyPairParse)?; let key_id = hex::encode(add_key(&mut root.signed, roles, key_pair.tuf_key())?); - key_path.write(&stdout, &key_id)?; + key_source + .write(&stdout, &key_id) + .context(error::WriteKeySource)?; clear_sigs(&mut root); println!("{}", key_id); write_file(path, &root) diff --git a/tuftool/src/root_digest.rs b/tuftool/src/root_digest.rs index 9fdbe77e..57a1b5ad 100644 --- a/tuftool/src/root_digest.rs +++ b/tuftool/src/root_digest.rs @@ -1,12 +1,12 @@ use crate::error; use crate::error::Result; use crate::key::RootKeys; -use crate::source::KeySource; use ring::digest::{SHA256, SHA256_OUTPUT_LEN}; use snafu::ensure; use snafu::ResultExt; use std::collections::HashMap; use std::path::PathBuf; +use tough::key_source::KeySource; use tough::schema::{Root, Signed}; /// Represents a loaded root.json file along with its sha256 digest and size in bytes @@ -56,10 +56,10 @@ impl RootDigest { /// /// * An error can occur for io reasons /// - pub(crate) fn load_keys(&self, keys: &[KeySource]) -> Result { + pub(crate) fn load_keys(&self, keys: &[Box]) -> Result { let mut map = HashMap::new(); for source in keys { - let key_pair = source.as_sign()?; + let key_pair = source.as_sign().context(error::KeyPairFromKeySource)?; if let Some((keyid, _)) = self .root .keys diff --git a/tuftool/src/sign.rs b/tuftool/src/sign.rs index 2d03bec8..e046ae96 100644 --- a/tuftool/src/sign.rs +++ b/tuftool/src/sign.rs @@ -4,13 +4,14 @@ use crate::error::Result; use crate::key::sign_metadata; use crate::root_digest::RootDigest; -use crate::source::KeySource; +use crate::source::parse_key_source; use crate::{load_file, write_file}; use ring::rand::SystemRandom; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use structopt::StructOpt; +use tough::key_source::KeySource; use tough::schema::{RoleType, Signed}; #[derive(Debug, StructOpt)] @@ -20,8 +21,8 @@ pub(crate) struct SignArgs { root: PathBuf, /// Key files to sign with - #[structopt(short = "k", long = "key")] - keys: Vec, + #[structopt(short = "k", long = "key", parse(try_from_str = parse_key_source))] + keys: Vec>, /// Metadata file to sign metadata_file: PathBuf, diff --git a/tuftool/src/source.rs b/tuftool/src/source.rs index 77fac167..9a33e7b6 100644 --- a/tuftool/src/source.rs +++ b/tuftool/src/source.rs @@ -1,176 +1,88 @@ // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT OR Apache-2.0 +#![allow(clippy::doc_markdown)] //! Private keys are generally provided as paths, but may sometimes be provided as a URL. For -//! example, when one of the Rusoto features is enabled, you can use an aws-ssm:// URL to refer to -//! a key accessible in SSM. +//! example, when one of the Rusoto features is enabled, you can use an aws-ssm:// special URL +//! to refer to a key accessible in SSM. (See below for more format examples) //! //! This module parses a key source command line parameter as a URL, relative to `file://$PWD`, //! then matches the URL scheme against ones we understand. +//! +//! Currently supported key sources are local files and AWS SSM. +//! +//! Examples of currently supported formats: +//! +//! Local files may be specified using a path or "file:///" prefixed path: +//! "./a/key/file/here" +//! "file:///./a/key/file/here" (notice the 3 slashes after the colon) +//! +//! Keys stored in AWS SSM use a special format: +//! "aws-ssm:///key/path/in/SSM?kms-key-id=12345" +//! +//! "kms-key-id" is an optional parameter you can provide. It is only used for writing +//! a key back to SSM. If it is not provided, the default key associated with your AWS +//! account is used. +//! +//! For example, using a profile "foo" and a key located at "a/key" +//! "aws-ssm://foo/a/key" +//! +//! Adding a specific KMS key: +//! "aws-ssm://foo/a/key?kms-key-id=1234567890" +//! +//! You may also skip the profile bit and just use your local environment's default profile: +//! "aws-ssm:///a/key" (notice the 3 slashes after the colon) -use crate::error::{self, Error, Result}; -use snafu::{OptionExt, ResultExt}; +use crate::error::{self, Result}; +use snafu::ResultExt; use std::path::PathBuf; -use std::str::FromStr; -use tough::schema::key::Key; -use tough::sign::{parse_keypair, Sign}; +use tough::key_source::{KeySource, LocalKeySource}; +use tough_ssm::SsmKeySource; use url::Url; -#[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] -use tokio; +/// Parses a user-specified source of signing keys. +/// Sources are passed to `tuftool` as arguments in string format: +/// "file:///..." or "./a/path/here" or "aws-ssm://...". See above +/// doc comment for more info on the appropriate format. +/// +/// Users are welcome to add their own sources of keys by implementing +/// the `KeySource` trait in the `tough` library. A user can then add +/// to this parser to support them in `tuftool`. +pub(crate) fn parse_key_source(input: &str) -> Result> { + let pwd_url = Url::from_directory_path(std::env::current_dir().context(error::CurrentDir)?) + .expect("expected current directory to be absolute"); + let url = Url::options() + .base_url(Some(&pwd_url)) + .parse(input) + .context(error::UrlParse { url: input })?; -#[derive(Debug)] -pub(crate) enum KeySource { - Local(PathBuf), - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - Ssm { - profile: Option, - parameter_name: String, - key_id: Option, - }, -} - -impl KeySource { - pub(crate) fn as_sign(&self) -> Result> { - let keypair = parse_keypair(&self.read()?).context(error::KeyPairParse)?; - Ok(Box::new(keypair)) - } - - pub(crate) fn as_public_key(&self) -> Result { - let data = self.read()?; - if let Ok(key_pair) = parse_keypair(&data) { - Ok(key_pair.tuf_key()) - } else { - let data = String::from_utf8(data) - .ok() - .context(error::UnrecognizedKey)?; - Key::from_str(&data).ok().context(error::UnrecognizedKey) - } - } - - fn read(&self) -> Result> { - match self { - KeySource::Local(path) => std::fs::read(path).context(error::FileRead { path }), - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - KeySource::Ssm { - profile, - parameter_name, - .. - } => KeySource::read_with_ssm_key(profile, ¶meter_name), - } - } - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - fn read_with_ssm_key(profile: &Option, parameter_name: &str) -> Result> { - use crate::deref::OptionDeref; - use rusoto_ssm::Ssm; - - let ssm_client = crate::ssm::build_client(profile.deref_shim())?; - let fut = ssm_client.get_parameter(rusoto_ssm::GetParameterRequest { - name: parameter_name.to_owned(), - with_decryption: Some(true), - }); - let response = tokio::runtime::Runtime::new() - .unwrap() - .block_on(fut) - .context(error::SsmGetParameter { - profile: profile.clone(), - parameter_name, - })?; - Ok(response - .parameter - .context(error::SsmMissingField { field: "parameter" })? - .value - .context(error::SsmMissingField { - field: "parameter.value", - })? - .as_bytes() - .to_vec()) - } - - #[cfg_attr( - not(any(feature = "rusoto-native-tls", feature = "rusoto-rustls")), - allow(unused) - )] - pub(crate) fn write(&self, value: &str, key_id_hex: &str) -> Result<()> { - match self { - KeySource::Local(path) => { - std::fs::write(path, value.as_bytes()).context(error::FileWrite { path }) - } - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - KeySource::Ssm { - profile, - parameter_name, - key_id, - } => KeySource::write_with_ssm_key(value, key_id_hex, profile, ¶meter_name, key_id), - } - } - - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - fn write_with_ssm_key( - value: &str, - key_id_hex: &str, - profile: &Option, - parameter_name: &str, - key_id: &Option, - ) -> Result<()> { - use crate::deref::OptionDeref; - use rusoto_ssm::Ssm; - - let ssm_client = crate::ssm::build_client(profile.deref_shim())?; - let fut = ssm_client.put_parameter(rusoto_ssm::PutParameterRequest { - name: parameter_name.to_owned(), - description: Some(key_id_hex.to_owned()), - key_id: key_id.as_ref().cloned(), - overwrite: Some(true), - type_: "SecureString".to_owned(), - value: value.to_owned(), - ..rusoto_ssm::PutParameterRequest::default() - }); - tokio::runtime::Runtime::new() - .unwrap() - .block_on(fut) - .context(error::SsmPutParameter { - profile: profile.clone(), - parameter_name, - })?; - Ok(()) - } -} - -impl FromStr for KeySource { - type Err = Error; - - #[allow(clippy::find_map)] - fn from_str(s: &str) -> Result { - let pwd_url = Url::from_directory_path(std::env::current_dir().context(error::CurrentDir)?) - .expect("expected current directory to be absolute"); - let url = Url::options() - .base_url(Some(&pwd_url)) - .parse(s) - .context(error::UrlParse { url: s })?; - - match url.scheme() { - "file" => Ok(KeySource::Local(PathBuf::from(url.path()))), - #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - "aws-ssm" => Ok(KeySource::Ssm { - profile: url.host_str().and_then(|s| { - if s.is_empty() { - None - } else { - Some(s.to_owned()) - } - }), - parameter_name: url.path().to_owned(), - key_id: url - .query_pairs() - .find(|(k, _)| k == "kms-key-id") - .map(|(_, v)| v.into_owned()), + match url.scheme() { + "file" => Ok(Box::new(LocalKeySource { + path: PathBuf::from(url.path()), + })), + #[cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] + "aws-ssm" => Ok(Box::new(SsmKeySource { + profile: url.host_str().and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }), + parameter_name: url.path().to_owned(), + // If a key ID isn't provided, the system uses the default key + // associated with your AWS account. + key_id: url.query_pairs().find_map(|(k, v)| { + if k == "kms-key-id" { + Some(v.into_owned()) + } else { + None + } }), - _ => error::UnrecognizedScheme { - scheme: url.scheme(), - } - .fail(), + })), + _ => error::UnrecognizedScheme { + scheme: url.scheme(), } + .fail(), } } diff --git a/tuftool/src/ssm.rs b/tuftool/src/ssm.rs deleted file mode 100644 index fed2d22a..00000000 --- a/tuftool/src/ssm.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT OR Apache-2.0 - -#![cfg(any(feature = "rusoto-native-tls", feature = "rusoto-rustls"))] - -use crate::error::{self, Result}; -use rusoto_core::{HttpClient, Region}; -use rusoto_credential::ProfileProvider; -use rusoto_ssm::SsmClient; -use snafu::ResultExt; -use std::env; -use std::str::FromStr; - -/// Builds an SSM client for a given profile name. -/// -/// This **cannot** be called concurrently as it modifies environment variables (due to Rusoto's -/// inflexibility for determining the region given a profile name). -// -// A better explanation: we want to know what region to make SSM calls in based on ~/.aws/config, -// but `ProfileProvider::region` is an associated function, not a method; this means we can't tell -// it what profile to select the region for. -// -// However, `region` calls `ProfileProvider::default_profile_name`, which uses the `AWS_PROFILE` -// environment variable. So we set that :( -// -// This behavior should be better supported in `rusoto_credential` -// TODO(iliana): submit issue + PR upstream -pub(crate) fn build_client(profile: Option<&str>) -> Result { - Ok(if let Some(profile) = profile { - let mut provider = ProfileProvider::new().context(error::RusotoCreds)?; - provider.set_profile(profile); - - let profile_prev = env::var_os("AWS_PROFILE"); - env::set_var("AWS_PROFILE", profile); - let region = ProfileProvider::region().context(error::RusotoCreds)?; - match profile_prev { - Some(v) => env::set_var("AWS_PROFILE", v), - None => env::remove_var("AWS_PROFILE"), - } - - SsmClient::new_with( - HttpClient::new().context(error::RusotoTls)?, - provider, - match region { - Some(region) => { - Region::from_str(®ion).context(error::RusotoRegion { region })? - } - None => Region::default(), - }, - ) - } else { - SsmClient::new(Region::default()) - }) -}