-
Notifications
You must be signed in to change notification settings - Fork 202
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
Add credentials exposure test & fix STS + SSO #2603
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -374,7 +374,7 @@ mod loader { | |
/// | ||
/// # Example: Using a custom profile file path | ||
/// | ||
/// ``` | ||
/// ```no_run | ||
/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider}; | ||
/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind}; | ||
/// | ||
|
@@ -417,7 +417,7 @@ mod loader { | |
/// | ||
/// # Example: Using a custom profile name | ||
/// | ||
/// ``` | ||
/// ```no_run | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we want to run these anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we shouldn't be running doc tests ever, these were misses. They should only be compiled—running them will try to load up an SSL chain etc. and makes them very slow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably do a pass (as a separate PR) and check that we aren't running them elsewhere. |
||
/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider}; | ||
/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind}; | ||
/// | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,11 +16,17 @@ use serde::Deserialize; | |
use crate::connector::default_connector; | ||
use aws_smithy_types::error::display::DisplayErrorContext; | ||
use std::collections::HashMap; | ||
use std::env; | ||
use std::error::Error; | ||
use std::fmt::Debug; | ||
use std::future::Future; | ||
use std::io::Write; | ||
use std::path::{Path, PathBuf}; | ||
use std::sync::{Arc, Mutex}; | ||
use std::time::{Duration, UNIX_EPOCH}; | ||
use tracing::dispatcher::DefaultGuard; | ||
use tracing::Level; | ||
use tracing_subscriber::fmt::TestWriter; | ||
|
||
/// Test case credentials | ||
/// | ||
|
@@ -84,7 +90,7 @@ impl AsyncSleep for InstantSleep { | |
} | ||
} | ||
|
||
#[derive(Deserialize)] | ||
#[derive(Deserialize, Debug)] | ||
pub(crate) enum GenericTestResult<T> { | ||
Ok(T), | ||
ErrorContains(String), | ||
|
@@ -129,6 +135,79 @@ pub(crate) struct Metadata { | |
name: String, | ||
} | ||
|
||
struct Tee<W> { | ||
buf: Arc<Mutex<Vec<u8>>>, | ||
quiet: bool, | ||
inner: W, | ||
} | ||
|
||
/// Capture logs from this test. | ||
/// | ||
/// The logs will be captured until the `DefaultGuard` is dropped. | ||
/// | ||
/// *Why use this instead of traced_test?* | ||
/// This captures _all_ logs, not just logs produced by the current crate. | ||
Comment on lines
+148
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ty for adding this comment! It's exactly what I would have asked. |
||
fn capture_test_logs() -> (DefaultGuard, Rx) { | ||
// it may be helpful to upstream this at some point | ||
let (mut writer, rx) = Tee::stdout(); | ||
if env::var("VERBOSE_TEST_LOGS").is_ok() { | ||
writer.loud(); | ||
} else { | ||
eprintln!("To see full logs from this test set VERBOSE_TEST_LOGS=true"); | ||
} | ||
let subscriber = tracing_subscriber::fmt() | ||
.with_max_level(Level::TRACE) | ||
.with_writer(Mutex::new(writer)) | ||
.finish(); | ||
let guard = tracing::subscriber::set_default(subscriber); | ||
(guard, rx) | ||
} | ||
|
||
struct Rx(Arc<Mutex<Vec<u8>>>); | ||
impl Rx { | ||
pub(crate) fn contents(&self) -> String { | ||
String::from_utf8(self.0.lock().unwrap().clone()).unwrap() | ||
} | ||
} | ||
|
||
impl Tee<TestWriter> { | ||
fn stdout() -> (Self, Rx) { | ||
let buf: Arc<Mutex<Vec<u8>>> = Default::default(); | ||
( | ||
Tee { | ||
buf: buf.clone(), | ||
quiet: true, | ||
inner: TestWriter::new(), | ||
}, | ||
Rx(buf), | ||
) | ||
} | ||
} | ||
|
||
impl<W> Tee<W> { | ||
fn loud(&mut self) { | ||
self.quiet = false; | ||
} | ||
} | ||
|
||
impl<W> Write for Tee<W> | ||
where | ||
W: Write, | ||
{ | ||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
self.buf.lock().unwrap().extend_from_slice(buf); | ||
if !self.quiet { | ||
self.inner.write(buf) | ||
} else { | ||
Ok(buf.len()) | ||
} | ||
} | ||
|
||
fn flush(&mut self) -> std::io::Result<()> { | ||
self.inner.flush() | ||
} | ||
} | ||
|
||
impl TestEnvironment { | ||
pub(crate) async fn from_dir(dir: impl AsRef<Path>) -> Result<TestEnvironment, Box<dyn Error>> { | ||
let dir = dir.as_ref(); | ||
|
@@ -232,12 +311,26 @@ impl TestEnvironment { | |
eprintln!("test case: {}. {}", self.metadata.name, self.metadata.docs); | ||
} | ||
|
||
fn lines_with_secrets<'a>(&'a self, logs: &'a str) -> Vec<&'a str> { | ||
logs.lines().filter(|l| self.contains_secret(l)).collect() | ||
} | ||
|
||
fn contains_secret(&self, log_line: &str) -> bool { | ||
assert!(log_line.lines().count() <= 1); | ||
match &self.metadata.result { | ||
// NOTE: we aren't currently erroring if the session token is leaked, that is in the canonical request among other things | ||
TestResult::Ok(creds) => log_line.contains(&creds.secret_access_key), | ||
TestResult::ErrorContains(_) => false, | ||
} | ||
} | ||
|
||
/// Execute a test case. Failures lead to panics. | ||
pub(crate) async fn execute<F, P>(&self, make_provider: impl Fn(ProviderConfig) -> F) | ||
where | ||
F: Future<Output = P>, | ||
P: ProvideCredentials, | ||
{ | ||
let (_guard, rx) = capture_test_logs(); | ||
let provider = make_provider(self.provider_config.clone()).await; | ||
let result = provider.provide_credentials().await; | ||
tokio::time::pause(); | ||
|
@@ -256,6 +349,14 @@ impl TestEnvironment { | |
Ok(()) => {} | ||
Err(e) => panic!("{}", e), | ||
} | ||
let contents = rx.contents(); | ||
let leaking_lines = self.lines_with_secrets(&contents); | ||
assert!( | ||
leaking_lines.is_empty(), | ||
"secret was exposed\n{:?}\nSee the following log lines:\n {}", | ||
self.metadata.result, | ||
leaking_lines.join("\n ") | ||
) | ||
} | ||
|
||
#[track_caller] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
[default] | ||
source_profile = base | ||
credential_process = echo '{ "Version": 1, "AccessKeyId": "ASIARTESTID", "SecretAccessKey": "TESTSECRETKEY", "SessionToken": "TESTSESSIONTOKEN", "Expiration": "2022-05-02T18:36:00+00:00" }' | ||
# these fake credentials are base64 encoded to prevent triggering the unit test that scans logs for secrets | ||
credential_process = echo eyAiVmVyc2lvbiI6IDEsICJBY2Nlc3NLZXlJZCI6ICJBU0lBUlRFU1RJRCIsICJTZWNyZXRBY2Nlc3NLZXkiOiAiVEVTVFNFQ1JFVEtFWSIsICJTZXNzaW9uVG9rZW4iOiAiVEVTVFNFU1NJT05UT0tFTiIsICJFeHBpcmF0aW9uIjogIjIwMjItMDUtMDJUMTg6MzY6MDArMDA6MDAiIH0K | base64 --decode | ||
|
||
[profile base] | ||
region = us-east-1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -256,7 +256,7 @@ fn calculate_signing_headers<'a>( | |
// Step 4: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-add-signature-to-request.html | ||
let values = creq.values.as_headers().expect("signing with headers"); | ||
let mut headers = HeaderMap::new(); | ||
add_header(&mut headers, header::X_AMZ_DATE, &values.date_time); | ||
add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be worth creating an enum for |
||
headers.insert( | ||
"authorization", | ||
build_authorization_header(params.access_key, &creq, sts, &signature), | ||
|
@@ -266,18 +266,26 @@ fn calculate_signing_headers<'a>( | |
&mut headers, | ||
header::X_AMZ_CONTENT_SHA_256, | ||
&values.content_sha256, | ||
false, | ||
); | ||
} | ||
|
||
if let Some(security_token) = params.security_token { | ||
add_header(&mut headers, header::X_AMZ_SECURITY_TOKEN, security_token); | ||
add_header( | ||
&mut headers, | ||
header::X_AMZ_SECURITY_TOKEN, | ||
security_token, | ||
true, | ||
); | ||
} | ||
|
||
Ok(SigningOutput::new(headers, signature)) | ||
} | ||
|
||
fn add_header(map: &mut HeaderMap<HeaderValue>, key: &'static str, value: &str) { | ||
map.insert(key, HeaderValue::try_from(value).expect(key)); | ||
fn add_header(map: &mut HeaderMap<HeaderValue>, key: &'static str, value: &str, sensitive: bool) { | ||
let mut value = HeaderValue::try_from(value).expect(key); | ||
value.set_sensitive(sensitive); | ||
map.insert(key, value); | ||
} | ||
|
||
// add signature to authorization header | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.rustsdk.customize.sso | ||
|
||
import software.amazon.smithy.model.Model | ||
import software.amazon.smithy.model.shapes.ServiceShape | ||
import software.amazon.smithy.model.shapes.Shape | ||
import software.amazon.smithy.model.shapes.ShapeId | ||
import software.amazon.smithy.model.shapes.StructureShape | ||
import software.amazon.smithy.model.traits.SensitiveTrait | ||
import software.amazon.smithy.model.transform.ModelTransformer | ||
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator | ||
import software.amazon.smithy.rust.codegen.core.util.letIf | ||
import java.util.logging.Logger | ||
|
||
class SSODecorator : ClientCodegenDecorator { | ||
override val name: String = "SSO" | ||
override val order: Byte = 0 | ||
private val logger: Logger = Logger.getLogger(javaClass.name) | ||
|
||
private fun isAwsCredentials(shape: Shape): Boolean = shape.id == ShapeId.from("com.amazonaws.sso#RoleCredentials") | ||
|
||
override fun transformModel(service: ServiceShape, model: Model): Model = | ||
jdisanti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ModelTransformer.create().mapShapes(model) { shape -> | ||
shape.letIf(isAwsCredentials(shape)) { | ||
(shape as StructureShape).toBuilder().addTrait(SensitiveTrait()).build() | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this going to render correctly? Seems like the sub bullet points might become top-level bullet points in the rendered changelog.