diff --git a/Cargo.lock b/Cargo.lock index c0218f36..27e93026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,6 +1881,20 @@ dependencies = [ "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tough-ssm" +version = "0.1.0" +dependencies = [ + "rusoto_core 0.43.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_credential 0.43.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_ssm 0.43.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "tough 0.5.0", +] + [[package]] name = "tower-service" version = "0.3.0" @@ -1919,6 +1933,7 @@ dependencies = [ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "tough 0.5.0", + "tough-ssm 0.1.0", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 96100f24..430118ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "olpc-cjson", "tough", + "tough-ssm", "tuftool", ] diff --git a/tough-ssm/Cargo.toml b/tough-ssm/Cargo.toml new file mode 100644 index 00000000..2683198c --- /dev/null +++ b/tough-ssm/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tough-ssm" +version = "0.1.0" +authors = ["Zac Mrowicki "] +edition = "2018" + +[features] +default = ["rusoto"] +rusoto = ["rusoto-rustls"] +rusoto-native-tls = ["rusoto_core/native-tls", "rusoto_credential", "rusoto_ssm/native-tls"] +rusoto-rustls = ["rusoto_core/rustls", "rusoto_credential", "rusoto_ssm/rustls"] + +[dependencies] +tough = { version = "0.5.0", path = "../tough", features = ["http"] } +rusoto_core = { version = "0.43", optional = true, default-features = false } +rusoto_credential = { version = "0.43", optional = true } +rusoto_ssm = { version = "0.43", optional = true, default-features = false } +serde = "1.0.105" +serde_json = "1.0.50" +snafu = { version = "0.6.6", features = ["backtraces-impl-backtrace-crate"] } +tokio = "0.2.13" diff --git a/tough-ssm/src/client.rs b/tough-ssm/src/client.rs new file mode 100644 index 00000000..f6c6e8f5 --- /dev/null +++ b/tough-ssm/src/client.rs @@ -0,0 +1,53 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +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`: this PR +// implements it: https://github.com/rusoto/rusoto/pull/1741 +// TODO: Update rusoto once the above PR is merged and all our problems magically dissapear! +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()) + }) +} diff --git a/tough-ssm/src/error.rs b/tough-ssm/src/error.rs new file mode 100644 index 00000000..6cd1abe0 --- /dev/null +++ b/tough-ssm/src/error.rs @@ -0,0 +1,79 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use snafu::{Backtrace, Snafu}; + +pub type Result = std::result::Result; + +/// The error type for this library. +#[derive(Debug, Snafu)] +#[snafu(visibility = "pub(crate)")] +pub enum Error { + #[snafu(display("Unable to parse keypair: {}", source))] + KeyPairParse { + source: tough::error::Error, + backtrace: Backtrace, + }, + + #[snafu(display("Error creating AWS credentials provider: {}", source))] + RusotoCreds { + source: rusoto_credential::CredentialsError, + backtrace: Backtrace, + }, + + #[snafu(display("Unknown AWS region \"{}\": {}", region, source))] + RusotoRegion { + region: String, + source: rusoto_core::region::ParseRegionError, + backtrace: Backtrace, + }, + + #[snafu(display("Error creating AWS request dispatcher: {}", source))] + RusotoTls { + source: rusoto_core::request::TlsError, + backtrace: Backtrace, + }, + + #[snafu(display("Unable to create tokio runtime: {}", source))] + RuntimeCreation { + source: std::io::Error, + backtrace: Backtrace, + }, + + #[snafu(display( + "Failed to get aws-ssm://{}{}: {}", + profile.as_deref().unwrap_or(""), + parameter_name, + source, + ))] + SsmGetParameter { + profile: Option, + parameter_name: String, + source: rusoto_core::RusotoError, + backtrace: Backtrace, + }, + + #[snafu(display( + "Missing field in SSM response for parameter '{}': {}", + parameter_name, + field + ))] + SsmMissingField { + parameter_name: String, + field: &'static str, + backtrace: Backtrace, + }, + + #[snafu(display( + "Failed to put aws-ssm://{}{}: {}", + profile.as_deref().unwrap_or(""), + parameter_name, + source, + ))] + SsmPutParameter { + profile: Option, + parameter_name: String, + source: rusoto_core::RusotoError, + backtrace: Backtrace, + }, +} diff --git a/tough-ssm/src/lib.rs b/tough-ssm/src/lib.rs new file mode 100644 index 00000000..b51b0cd3 --- /dev/null +++ b/tough-ssm/src/lib.rs @@ -0,0 +1,79 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod client; +pub mod error; + +use rusoto_ssm::Ssm; +use snafu::{OptionExt, ResultExt}; +use tough::key_source::KeySource; +use tough::sign::{parse_keypair, Sign}; + +/// Implements the KeySource trait for keys that live in AWS SSM. +#[derive(Debug)] +pub struct SsmKeySource { + pub profile: Option, + pub parameter_name: String, + pub key_id: Option, +} + +/// Implements the KeySource trait. +impl KeySource for SsmKeySource { + fn as_sign( + &self, + ) -> std::result::Result, Box> + { + let ssm_client = client::build_client(self.profile.as_deref())?; + let fut = ssm_client.get_parameter(rusoto_ssm::GetParameterRequest { + name: self.parameter_name.to_owned(), + with_decryption: Some(true), + }); + let response = tokio::runtime::Runtime::new() + .context(error::RuntimeCreation)? + .block_on(fut) + .context(error::SsmGetParameter { + profile: self.profile.clone(), + parameter_name: &self.parameter_name, + })?; + let data = response + .parameter + .context(error::SsmMissingField { + parameter_name: &self.parameter_name, + field: "parameter", + })? + .value + .context(error::SsmMissingField { + parameter_name: &self.parameter_name, + field: "parameter.value", + })? + .as_bytes() + .to_vec(); + let sign = Box::new(parse_keypair(&data).context(error::KeyPairParse)?); + Ok(sign) + } + + fn write( + &self, + value: &str, + key_id_hex: &str, + ) -> std::result::Result<(), Box> { + let ssm_client = client::build_client(self.profile.as_deref())?; + let fut = ssm_client.put_parameter(rusoto_ssm::PutParameterRequest { + name: self.parameter_name.to_owned(), + description: Some(key_id_hex.to_owned()), + key_id: self.key_id.as_ref().cloned(), + overwrite: Some(true), + type_: "SecureString".to_owned(), + value: value.to_owned(), + ..rusoto_ssm::PutParameterRequest::default() + }); + tokio::runtime::Runtime::new() + .context(error::RuntimeCreation)? + .block_on(fut) + .context(error::SsmPutParameter { + profile: self.profile.clone(), + parameter_name: &self.parameter_name, + })?; + Ok(()) + } +}