Remove the public HTTP dependency from aws-sigv4 (#2921)

## Motivation and Context
Removes the public http dependency from the aws-sigv4 crate to avoid
compatibility issues with http = 1.0 and to support the http refactor

## Description
- Changes `SignableRequest::new` to remove the direct exposure of HTTP
types
- Assorted test refactorings as a result
- Update calling code

## Testing
IT/UT

## Checklist
TODO: changelog
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
This commit is contained in:
Russell Cohen 2023-08-16 16:28:32 -04:00 committed by GitHub
parent 89802592b0
commit 2d61502221
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 357 additions and 294 deletions

View File

@ -34,3 +34,14 @@ message = "Fix requests to S3 with `no_credentials` set."
references = ["smithy-rs#2907", "aws-sdk-rust#864"]
meta = { "breaking" = false, "tada" = false, "bug" = true }
author = "jdisanti"
[[aws-sdk-rust]]
message = """Several breaking changes were made to the aws-sigv4 API to remove the direct HTTP dependency:
- The `take_parameters` and `take_headers` APIs were removed from `SigningInstructions`. Use `into_parts()` instead
- The arguments of `SignableRequest::new` were changed to accept string types instead of types from the HTTP crate
- `SigningInstructions::apply_to_request` was gated beyond an `http0-compat` feature flag for backwards compatibility. This API MAY be removed in a future release.
- Several public accessors were removed from `SigningInstructions`.
"""
references = ["smithy-rs#2921"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "rcoh"

View File

@ -14,7 +14,8 @@ test-util = []
[dependencies]
aws-credential-types = { path = "../aws-credential-types" }
aws-http = { path = "../aws-http" }
aws-sigv4 = { path = "../aws-sigv4" }
# TODO(httpRefactor): Remove the http0-compat feature
aws-sigv4 = { path = "../aws-sigv4", features = ["http0-compat"] }
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }
aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true }
aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" }

View File

@ -356,11 +356,17 @@ impl Signer for SigV4Signer {
});
let signable_request = SignableRequest::new(
request.method(),
request.uri(),
request.headers(),
request.method().as_str(),
request.uri().to_string(),
request.headers().iter().map(|(k, v)| {
(
k.as_str(),
// use from_utf8 instead of to_str because we _do_ allow non-ascii header values
std::str::from_utf8(v.as_bytes()).expect("only utf-8 headers are signable"),
)
}),
signable_body,
);
)?;
sign(signable_request, &signing_params)?
}
.into_parts();
@ -384,7 +390,6 @@ impl Signer for SigV4Signer {
.expect("failed to send deferred signer");
}
}
signing_instructions.apply_to_request(request);
Ok(())
}

View File

@ -12,7 +12,8 @@ sign-eventstream = ["aws-smithy-eventstream", "aws-sigv4/sign-eventstream"]
[dependencies]
aws-credential-types = { path = "../aws-credential-types" }
aws-sigv4 = { path = "../aws-sigv4" }
# TODO(httpRefactor): Remove feature was http refactor is complete
aws-sigv4 = { path = "../aws-sigv4", features = ["http0-compat"] }
aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true }
aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" }
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }

View File

@ -16,6 +16,7 @@ use std::time::{Duration, SystemTime};
use crate::middleware::Signature;
pub use aws_sigv4::http_request::SignableBody;
pub type SigningError = aws_sigv4::http_request::SigningError;
const EXPIRATION_WARNING: &str = "Presigned request will expire before the given \
@ -212,11 +213,17 @@ impl SigV4Signer {
});
let signable_request = SignableRequest::new(
request.method(),
request.uri(),
request.headers(),
request.method().as_str(),
request.uri().to_string(),
request.headers().iter().map(|(k, v)| {
(
k.as_str(),
std::str::from_utf8(v.as_bytes())
.expect("only string headers are signable"),
)
}),
signable_body,
);
)?;
sign(signable_request, &signing_params)?
}
.into_parts();

View File

@ -11,6 +11,7 @@ repository = "https://github.com/awslabs/smithy-rs"
[features]
sign-http = ["http", "percent-encoding", "form_urlencoded"]
sign-eventstream = ["aws-smithy-eventstream", "bytes"]
http0-compat = ["http"]
default = ["sign-http"]
[dependencies]

View File

@ -1,11 +1,6 @@
allowed_external_types = [
"http::header::map::HeaderMap",
"http::header::name::HeaderName",
"http::header::value::HeaderValue",
"http::method::Method",
# TODO(refactorHttp): Remove this and remove the signing helpers
"http::request::Request",
"http::uri::Uri",
# TODO(https://github.com/awslabs/smithy-rs/issues/1193): Once tooling permits it, only allow the following types in the `event-stream` feature
"aws_smithy_eventstream::frame::Message",
]

View File

@ -15,7 +15,7 @@ use crate::http_request::{PayloadChecksumKind, SignableBody, SignatureLocation,
use crate::sign::sha256_hex_string;
use aws_smithy_http::query_writer::QueryWriter;
use http::header::{AsHeaderName, HeaderName, HOST};
use http::{HeaderMap, HeaderValue, Method, Uri};
use http::{HeaderMap, HeaderValue, Uri};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::convert::TryFrom;
@ -103,7 +103,7 @@ impl<'a> SignatureValues<'a> {
#[derive(Debug, PartialEq)]
pub(super) struct CanonicalRequest<'a> {
pub(super) method: &'a Method,
pub(super) method: &'a str,
pub(super) path: Cow<'a, str>,
pub(super) params: Option<String>,
pub(super) headers: HeaderMap,
@ -212,7 +212,7 @@ impl<'a> CanonicalRequest<'a> {
// Header names and values need to be normalized according to Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
// Using append instead of insert means this will not clobber headers that have the same lowercased name
canonical_headers.append(
HeaderName::from_str(&name.as_str().to_lowercase())?,
HeaderName::from_str(&name.to_lowercase())?,
normalize_header_value(value)?,
);
}
@ -237,7 +237,7 @@ impl<'a> CanonicalRequest<'a> {
let mut signed_headers = Vec::with_capacity(canonical_headers.len());
for name in canonical_headers.keys() {
if let Some(excluded_headers) = params.settings.excluded_headers.as_ref() {
if excluded_headers.contains(name) {
if excluded_headers.iter().any(|it| name.as_str() == it) {
continue;
}
}
@ -406,9 +406,7 @@ fn trim_spaces_from_byte_string(bytes: &[u8]) -> &[u8] {
/// Works just like [trim_all] but acts on HeaderValues instead of bytes.
/// Will ensure that the underlying bytes are valid UTF-8.
fn normalize_header_value(
header_value: &HeaderValue,
) -> Result<HeaderValue, CanonicalRequestError> {
fn normalize_header_value(header_value: &str) -> Result<HeaderValue, CanonicalRequestError> {
let trimmed_value = trim_all(header_value.as_bytes());
HeaderValue::from_str(
std::str::from_utf8(&trimmed_value)
@ -544,8 +542,8 @@ mod tests {
use crate::http_request::{SignatureLocation, SigningParams};
use crate::sign::sha256_hex_string;
use aws_smithy_http::query_writer::QueryWriter;
use http::Uri;
use http::{header::HeaderName, HeaderValue};
use http::{HeaderValue, Uri};
use pretty_assertions::assert_eq;
use proptest::{prelude::*, proptest};
use std::time::Duration;
@ -565,14 +563,14 @@ mod tests {
#[test]
fn test_repeated_header() {
let mut req = test_request("get-vanilla-query-order-key-case");
req.headers_mut().append(
"x-amz-object-attributes",
HeaderValue::from_static("Checksum"),
);
req.headers_mut().append(
"x-amz-object-attributes",
HeaderValue::from_static("ObjectSize"),
);
req.headers.push((
"x-amz-object-attributes".to_string(),
"Checksum".to_string(),
));
req.headers.push((
"x-amz-object-attributes".to_string(),
"ObjectSize".to_string(),
));
let req = SignableRequest::from(&req);
let settings = SigningSettings {
payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
@ -618,13 +616,10 @@ mod tests {
#[test]
fn test_unsigned_payload() {
let req = test_request("get-vanilla-query-order-key-case");
let req = SignableRequest::new(
req.method(),
req.uri(),
req.headers(),
SignableBody::UnsignedPayload,
);
let mut req = test_request("get-vanilla-query-order-key-case");
req.set_body(SignableBody::UnsignedPayload);
let req: SignableRequest<'_> = SignableRequest::from(&req);
let settings = SigningSettings {
payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
..Default::default()
@ -638,13 +633,9 @@ mod tests {
#[test]
fn test_precomputed_payload() {
let payload_hash = "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072";
let req = test_request("get-vanilla-query-order-key-case");
let req = SignableRequest::new(
req.method(),
req.uri(),
req.headers(),
SignableBody::Precomputed(String::from(payload_hash)),
);
let mut req = test_request("get-vanilla-query-order-key-case");
req.set_body(SignableBody::Precomputed(String::from(payload_hash)));
let req = SignableRequest::from(&req);
let settings = SigningSettings {
payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
..Default::default()
@ -712,7 +703,7 @@ mod tests {
#[test]
fn test_tilde_in_uri() {
let req = http::Request::builder()
.uri("https://s3.us-east-1.amazonaws.com/my-bucket?list-type=2&prefix=~objprefix&single&k=&unreserved=-_.~").body("").unwrap();
.uri("https://s3.us-east-1.amazonaws.com/my-bucket?list-type=2&prefix=~objprefix&single&k=&unreserved=-_.~").body("").unwrap().into();
let req = SignableRequest::from(&req);
let signing_params = signing_params(SigningSettings::default());
let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
@ -734,7 +725,8 @@ mod tests {
let req = http::Request::builder()
.uri(query_writer.build_uri())
.body("")
.unwrap();
.unwrap()
.into();
let req = SignableRequest::from(&req);
let signing_params = signing_params(SigningSettings::default());
let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
@ -786,7 +778,8 @@ mod tests {
.header("x-amzn-trace-id", "test-trace-id")
.header("x-amz-user-agent", "test-user-agent")
.body("")
.unwrap();
.unwrap()
.into();
let request = SignableRequest::from(&request);
let settings = SigningSettings {
@ -816,7 +809,8 @@ mod tests {
.header("x-amzn-trace-id", "test-trace-id")
.header("x-amz-user-agent", "test-user-agent")
.body("")
.unwrap();
.unwrap()
.into();
let request = SignableRequest::from(&request);
let settings = SigningSettings {
@ -847,7 +841,7 @@ mod tests {
for key in &excluded_headers {
request_builder = request_builder.header(key, "value");
}
let request = request_builder.body("").unwrap();
let request = request_builder.body("").unwrap().into();
let request = SignableRequest::from(&request);
@ -857,9 +851,7 @@ mod tests {
excluded_headers: Some(
excluded_headers
.into_iter()
.map(|header_string| {
HeaderName::from_static(Box::leak(header_string.into_boxed_str()))
})
.map(std::borrow::Cow::Owned)
.collect(),
),
..Default::default()
@ -908,9 +900,8 @@ mod tests {
#[test]
fn test_normalize_header_value_works_on_valid_header_value(v in (".*")) {
if let Ok(header_value) = HeaderValue::from_maybe_shared(v) {
assert!(normalize_header_value(&header_value).is_ok());
}
prop_assume!(HeaderValue::from_str(&v).is_ok());
assert!(normalize_header_value(&v).is_ok());
}
#[test]
@ -918,10 +909,4 @@ mod tests {
assert_eq!(trim_all(s.as_bytes()).as_ref(), s.as_bytes());
}
}
#[test]
fn test_normalize_header_value_returns_expected_error_on_invalid_utf8() {
let header_value = HeaderValue::from_bytes(&[0xC0, 0xC1]).unwrap();
assert!(normalize_header_value(&header_value).is_err());
}
}

View File

@ -4,6 +4,7 @@
*/
use http::header::{InvalidHeaderName, InvalidHeaderValue};
use http::uri::InvalidUri;
use std::error::Error;
use std::fmt;
use std::str::Utf8Error;
@ -50,6 +51,7 @@ enum CanonicalRequestErrorKind {
InvalidHeaderName { source: InvalidHeaderName },
InvalidHeaderValue { source: InvalidHeaderValue },
InvalidUtf8InHeaderValue { source: Utf8Error },
InvalidUri { source: InvalidUri },
}
#[derive(Debug)]
@ -64,6 +66,7 @@ impl fmt::Display for CanonicalRequestError {
InvalidHeaderName { .. } => write!(f, "invalid header name"),
InvalidHeaderValue { .. } => write!(f, "invalid header value"),
InvalidUtf8InHeaderValue { .. } => write!(f, "invalid UTF-8 in header value"),
InvalidUri { .. } => write!(f, "the uri was invalid"),
}
}
}
@ -75,6 +78,7 @@ impl Error for CanonicalRequestError {
InvalidHeaderName { source } => Some(source),
InvalidHeaderValue { source } => Some(source),
InvalidUtf8InHeaderValue { source } => Some(source),
InvalidUri { source } => Some(source),
}
}
}
@ -102,3 +106,11 @@ impl From<InvalidHeaderValue> for CanonicalRequestError {
}
}
}
impl From<InvalidUri> for CanonicalRequestError {
fn from(source: InvalidUri) -> Self {
Self {
kind: CanonicalRequestErrorKind::InvalidUri { source },
}
}
}

View File

@ -7,18 +7,16 @@
//!
//! # Example: Signing an HTTP request
//!
//! **Note**: This requires `http0-compat` to be enabled.
//!
//! ```rust
//! # fn test() -> Result<(), aws_sigv4::http_request::SigningError> {
//! # use aws_sigv4::http_request::SignableBody;
//! #[cfg(feature = "http0-compat")]
//! fn test() -> Result<(), aws_sigv4::http_request::SigningError> {
//! use aws_sigv4::http_request::{sign, SigningSettings, SigningParams, SignableRequest};
//! use http;
//! use std::time::SystemTime;
//!
//! // Create the request to sign
//! let mut request = http::Request::builder()
//! .uri("https://some-endpoint.some-region.amazonaws.com")
//! .body("")
//! .unwrap();
//!
//! // Set up information and settings for the signing
//! let signing_settings = SigningSettings::default();
//! let signing_params = SigningParams::builder()
@ -31,11 +29,17 @@
//! .build()
//! .unwrap();
//! // Convert the HTTP request into a signable request
//! let signable_request = SignableRequest::from(&request);
//! let signable_request = SignableRequest::new(
//! "GET",
//! "https://some-endpoint.some-region.amazonaws.com",
//! std::iter::empty(),
//! SignableBody::Bytes(&[])
//! ).expect("signable request");
//!
//! let mut my_req = http::Request::new("...");
//! // Sign and then apply the signature to the request
//! let (signing_instructions, _signature) = sign(signable_request, &signing_params)?.into_parts();
//! signing_instructions.apply_to_request(&mut request);
//! signing_instructions.apply_to_request(&mut my_req);
//! # Ok(())
//! # }
//! ```

View File

@ -3,7 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
use http::header::{HeaderName, AUTHORIZATION, USER_AGENT};
use http::header::{AUTHORIZATION, USER_AGENT};
use std::borrow::Cow;
use std::time::Duration;
/// HTTP signing parameters
@ -30,7 +31,7 @@ pub struct SigningSettings {
pub expires_in: Option<Duration>,
/// Headers that should be excluded from the signing process
pub excluded_headers: Option<Vec<HeaderName>>,
pub excluded_headers: Option<Vec<Cow<'static, str>>>,
/// Specifies whether the absolute path component of the URI should be normalized during signing.
pub uri_path_normalization_mode: UriPathNormalizationMode,
@ -109,11 +110,11 @@ impl Default for SigningSettings {
let excluded_headers = Some(
[
// This header is calculated as part of the signing process, so if it's present, discard it
AUTHORIZATION,
Cow::Borrowed(AUTHORIZATION.as_str()),
// Changes when sent by proxy
USER_AGENT,
Cow::Borrowed(USER_AGENT.as_str()),
// Changes based on the request from the client
HeaderName::from_static(HEADER_NAME_X_RAY_TRACE_ID),
Cow::Borrowed(HEADER_NAME_X_RAY_TRACE_ID),
]
.to_vec(),
);

View File

@ -8,56 +8,61 @@ use super::{PayloadChecksumKind, SignatureLocation};
use crate::http_request::canonical_request::header;
use crate::http_request::canonical_request::param;
use crate::http_request::canonical_request::{CanonicalRequest, StringToSign, HMAC_256};
use crate::http_request::error::CanonicalRequestError;
use crate::http_request::SigningParams;
use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string};
use crate::SigningOutput;
use aws_smithy_http::query_writer::QueryWriter;
use http::header::HeaderValue;
use http::{HeaderMap, Method, Uri};
use http::Uri;
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt::{Debug, Formatter};
use std::str;
/// Represents all of the information necessary to sign an HTTP request.
#[derive(Debug)]
#[non_exhaustive]
pub struct SignableRequest<'a> {
method: &'a Method,
uri: &'a Uri,
headers: &'a HeaderMap<HeaderValue>,
method: &'a str,
uri: Uri,
headers: Vec<(&'a str, &'a str)>,
body: SignableBody<'a>,
}
impl<'a> SignableRequest<'a> {
/// Creates a new `SignableRequest`. If you have an [`http::Request`], then
/// consider using [`SignableRequest::from`] instead of `new`.
/// Creates a new `SignableRequest`.
pub fn new(
method: &'a Method,
uri: &'a Uri,
headers: &'a HeaderMap<HeaderValue>,
method: &'a str,
uri: impl Into<Cow<'a, str>>,
headers: impl Iterator<Item = (&'a str, &'a str)>,
body: SignableBody<'a>,
) -> Self {
Self {
) -> Result<Self, SigningError> {
let uri = uri
.into()
.parse()
.map_err(|e| SigningError::from(CanonicalRequestError::from(e)))?;
let headers = headers.collect();
Ok(Self {
method,
uri,
headers,
body,
}
})
}
/// Returns the signable URI
pub fn uri(&self) -> &Uri {
self.uri
pub(crate) fn uri(&self) -> &Uri {
&self.uri
}
/// Returns the signable HTTP method
pub fn method(&self) -> &Method {
pub(crate) fn method(&self) -> &str {
self.method
}
/// Returns the request headers
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
self.headers
pub(crate) fn headers(&self) -> &[(&str, &str)] {
self.headers.as_slice()
}
/// Returns the signable body
@ -66,21 +71,6 @@ impl<'a> SignableRequest<'a> {
}
}
impl<'a, B> From<&'a http::Request<B>> for SignableRequest<'a>
where
B: 'a,
B: AsRef<[u8]>,
{
fn from(request: &'a http::Request<B>) -> SignableRequest<'a> {
SignableRequest::new(
request.method(),
request.uri(),
request.headers(),
SignableBody::Bytes(request.body().as_ref()),
)
}
}
/// A signable HTTP request body
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
@ -106,48 +96,83 @@ pub enum SignableBody<'a> {
/// Instructions for applying a signature to an HTTP request.
#[derive(Debug)]
pub struct SigningInstructions {
headers: Option<HeaderMap<HeaderValue>>,
params: Option<Vec<(&'static str, Cow<'static, str>)>>,
headers: Vec<Header>,
params: Vec<(&'static str, Cow<'static, str>)>,
}
/// Header representation for use in [`SigningInstructions`]
pub struct Header {
key: &'static str,
value: String,
sensitive: bool,
}
impl Debug for Header {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut fmt = f.debug_struct("Header");
fmt.field("key", &self.key);
let value = if self.sensitive {
"** REDACTED **"
} else {
&self.value
};
fmt.field("value", &value);
fmt.finish()
}
}
impl Header {
/// The name of this header
pub fn name(&self) -> &'static str {
self.key
}
/// The value of this header
pub fn value(&self) -> &str {
&self.value
}
/// Whether this header has a sensitive value
pub fn sensitive(&self) -> bool {
self.sensitive
}
}
impl SigningInstructions {
fn new(
headers: Option<HeaderMap<HeaderValue>>,
params: Option<Vec<(&'static str, Cow<'static, str>)>>,
) -> Self {
fn new(headers: Vec<Header>, params: Vec<(&'static str, Cow<'static, str>)>) -> Self {
Self { headers, params }
}
/// Returns a reference to the headers that should be added to the request.
pub fn headers(&self) -> Option<&HeaderMap<HeaderValue>> {
self.headers.as_ref()
/// Returns the headers and query params that should be applied to this request
pub fn into_parts(self) -> (Vec<Header>, Vec<(&'static str, Cow<'static, str>)>) {
(self.headers, self.params)
}
/// Returns the headers and sets the internal value to `None`.
pub fn take_headers(&mut self) -> Option<HeaderMap<HeaderValue>> {
self.headers.take()
/// Returns a reference to the headers that should be added to the request.
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers
.iter()
.map(|header| (header.key, header.value.as_str()))
}
/// Returns a reference to the query parameters that should be added to the request.
pub fn params(&self) -> Option<&Vec<(&'static str, Cow<'static, str>)>> {
self.params.as_ref()
}
/// Returns the query parameters and sets the internal value to `None`.
pub fn take_params(&mut self) -> Option<Vec<(&'static str, Cow<'static, str>)>> {
self.params.take()
pub fn params(&self) -> &[(&str, Cow<'static, str>)] {
self.params.as_slice()
}
#[cfg(any(feature = "http0-compat", test))]
/// Applies the instructions to the given `request`.
pub fn apply_to_request<B>(mut self, request: &mut http::Request<B>) {
if let Some(new_headers) = self.take_headers() {
for (name, value) in new_headers.into_iter() {
request.headers_mut().insert(name.unwrap(), value);
}
pub fn apply_to_request<B>(self, request: &mut http::Request<B>) {
let (new_headers, new_query) = self.into_parts();
for header in new_headers.into_iter() {
let mut value = http::HeaderValue::from_str(&header.value).unwrap();
value.set_sensitive(header.sensitive);
request.headers_mut().insert(header.key, value);
}
if let Some(params) = self.take_params() {
let mut query = QueryWriter::new(request.uri());
for (name, value) in params {
if !new_query.is_empty() {
let mut query = aws_smithy_http::query_writer::QueryWriter::new(request.uri());
for (name, value) in new_query {
query.insert(name, &value);
}
*request.uri_mut() = query.build_uri();
@ -167,14 +192,14 @@ pub fn sign<'a>(
let (signing_headers, signature) =
calculate_signing_headers(&request, params)?.into_parts();
Ok(SigningOutput::new(
SigningInstructions::new(Some(signing_headers), None),
SigningInstructions::new(signing_headers, vec![]),
signature,
))
}
SignatureLocation::QueryParams => {
let (params, signature) = calculate_signing_params(&request, params)?;
Ok(SigningOutput::new(
SigningInstructions::new(None, Some(params)),
SigningInstructions::new(vec![], params),
signature,
))
}
@ -238,7 +263,7 @@ fn calculate_signing_params<'a>(
fn calculate_signing_headers<'a>(
request: &'a SignableRequest<'a>,
params: &'a SigningParams<'a>,
) -> Result<SigningOutput<HeaderMap<HeaderValue>>, SigningError> {
) -> Result<SigningOutput<Vec<Header>>, SigningError> {
// Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html.
let creq = CanonicalRequest::from(request, params)?;
tracing::trace!(canonical_request = %creq);
@ -263,12 +288,14 @@ 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();
let mut headers = vec![];
add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false);
headers.insert(
"authorization",
build_authorization_header(params.access_key, &creq, sts, &signature),
);
headers.push(Header {
key: "authorization",
value: build_authorization_header(params.access_key, &creq, sts, &signature),
sensitive: false,
});
if params.settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
add_header(
&mut headers,
@ -290,10 +317,12 @@ fn calculate_signing_headers<'a>(
Ok(SigningOutput::new(headers, signature))
}
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);
fn add_header(map: &mut Vec<Header>, key: &'static str, value: &str, sensitive: bool) {
map.push(Header {
key,
value: value.to_string(),
sensitive,
});
}
// add signature to authorization header
@ -303,44 +332,52 @@ fn build_authorization_header(
creq: &CanonicalRequest<'_>,
sts: StringToSign<'_>,
signature: &str,
) -> HeaderValue {
let mut value = HeaderValue::try_from(format!(
) -> String {
format!(
"{} Credential={}/{}, SignedHeaders={}, Signature={}",
HMAC_256,
access_key,
sts.scope,
creq.values.signed_headers().as_str(),
signature
))
.unwrap();
value.set_sensitive(true);
value
)
}
#[cfg(test)]
mod tests {
use super::{sign, SigningInstructions};
use super::sign;
use crate::date_time::test_parsers::parse_date_time;
use crate::http_request::sign::SignableRequest;
use crate::http_request::test::{
make_headers_comparable, test_request, test_signed_request,
test_signed_request_query_params,
test_request, test_signed_request, test_signed_request_query_params,
};
use crate::http_request::{
SessionTokenMode, SignatureLocation, SigningParams, SigningSettings,
SessionTokenMode, SignableBody, SignatureLocation, SigningParams, SigningSettings,
};
use http::{HeaderMap, HeaderValue};
use http::{HeaderValue, Request};
use pretty_assertions::assert_eq;
use proptest::proptest;
use std::borrow::Cow;
use std::iter;
use std::time::Duration;
macro_rules! assert_req_eq {
($a:tt, $b:tt) => {
make_headers_comparable(&mut $a);
make_headers_comparable(&mut $b);
assert_eq!(format!("{:?}", $a), format!("{:?}", $b))
(http: $expected:expr, $actual:expr) => {
let mut expected = ($expected).map(|_b|"body");
let mut actual = ($actual).map(|_b|"body");
make_headers_comparable(&mut expected);
make_headers_comparable(&mut actual);
assert_eq!(format!("{:?}", expected), format!("{:?}", actual));
};
($expected:tt, $actual:tt) => {
assert_req_eq!(http: ($expected).as_http_request(), $actual);
};
}
pub(crate) fn make_headers_comparable<B>(request: &mut Request<B>) {
for (_name, value) in request.headers_mut() {
value.set_sensitive(false);
}
}
#[test]
@ -364,10 +401,10 @@ mod tests {
out.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out.output.apply_to_request(&mut signed);
let mut expected = test_signed_request("get-vanilla-query-order-key-case");
let expected = test_signed_request("get-vanilla-query-order-key-case");
assert_req_eq!(expected, signed);
}
@ -393,10 +430,10 @@ mod tests {
out.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out.output.apply_to_request(&mut signed);
let mut expected = test_signed_request(test);
let expected = test_signed_request(test);
assert_req_eq!(expected, signed);
}
@ -425,10 +462,10 @@ mod tests {
out.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out.output.apply_to_request(&mut signed);
let mut expected = test_signed_request_query_params("get-vanilla-query-order-key-case");
let expected = test_signed_request_query_params("get-vanilla-query-order-key-case");
assert_req_eq!(expected, signed);
}
@ -449,7 +486,8 @@ mod tests {
.uri("https://some-endpoint.some-region.amazonaws.com")
.header("some-header", HeaderValue::from_str("テスト").unwrap())
.body("")
.unwrap();
.unwrap()
.into();
let signable = SignableRequest::from(&original);
let out = sign(signable, &params).unwrap();
assert_eq!(
@ -457,10 +495,10 @@ mod tests {
out.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out.output.apply_to_request(&mut signed);
let mut expected = http::Request::builder()
let expected = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com")
.header("some-header", HeaderValue::from_str("テスト").unwrap())
.header(
@ -479,7 +517,7 @@ mod tests {
)
.body("")
.unwrap();
assert_req_eq!(expected, signed);
assert_req_eq!(http: expected, signed);
}
#[test]
@ -501,7 +539,8 @@ mod tests {
let original = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com")
.body("")
.unwrap();
.unwrap()
.into();
let out_without_session_token = sign(SignableRequest::from(&original), &params).unwrap();
params.security_token = Some("notarealsessiontoken");
@ -516,12 +555,12 @@ mod tests {
out_without_session_token.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out_with_session_token_but_excluded
.output
.apply_to_request(&mut signed);
let mut expected = http::Request::builder()
let expected = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com")
.header(
"x-amz-date",
@ -541,9 +580,9 @@ mod tests {
"x-amz-security-token",
HeaderValue::from_str("notarealsessiontoken").unwrap(),
)
.body("")
.body(b"")
.unwrap();
assert_req_eq!(expected, signed);
assert_req_eq!(http: expected, signed);
}
#[test]
@ -566,7 +605,8 @@ mod tests {
HeaderValue::from_str("  test test ").unwrap(),
)
.body("")
.unwrap();
.unwrap()
.into();
let signable = SignableRequest::from(&original);
let out = sign(signable, &params).unwrap();
assert_eq!(
@ -574,10 +614,10 @@ mod tests {
out.signature
);
let mut signed = original;
let mut signed = original.as_http_request();
out.output.apply_to_request(&mut signed);
let mut expected = http::Request::builder()
let expected = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com")
.header(
"some-header",
@ -599,30 +639,7 @@ mod tests {
)
.body("")
.unwrap();
assert_req_eq!(expected, signed);
}
#[test]
fn test_sign_headers_returning_expected_error_on_invalid_utf8() {
let settings = SigningSettings::default();
let params = SigningParams {
access_key: "123",
secret_key: "asdf",
security_token: None,
region: "us-east-1",
service_name: "foo",
time: std::time::SystemTime::UNIX_EPOCH,
settings,
};
let req = http::Request::builder()
.uri("https://foo.com/")
.header("x-sign-me", HeaderValue::from_bytes(&[0xC0, 0xC1]).unwrap())
.body(&[])
.unwrap();
let creq = crate::http_request::sign(SignableRequest::from(&req), &params);
assert!(creq.is_err());
assert_req_eq!(http: expected, signed);
}
proptest! {
@ -630,8 +647,7 @@ mod tests {
// Only byte values between 32 and 255 (inclusive) are permitted, excluding byte 127, for
// [HeaderValue](https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.from_bytes).
fn test_sign_headers_no_panic(
left in proptest::collection::vec(32_u8..=126, 0..100),
right in proptest::collection::vec(128_u8..=255, 0..100),
header in ".*"
) {
let settings = SigningSettings::default();
let params = SigningParams {
@ -644,57 +660,17 @@ mod tests {
settings,
};
let bytes = left.iter().chain(right.iter()).cloned().collect::<Vec<_>>();
let req = http::Request::builder()
.uri("https://foo.com/")
.header("x-sign-me", HeaderValue::from_bytes(&bytes).unwrap())
.body(&[])
.unwrap();
let req = SignableRequest::new(
"GET",
"https://foo.com",
iter::once(("x-sign-me", header.as_str())),
SignableBody::Bytes(&[])
);
// The test considered a pass if the creation of `creq` does not panic.
let _creq = crate::http_request::sign(
SignableRequest::from(&req),
&params);
if let Ok(req) = req {
// The test considered a pass if the creation of `creq` does not panic.
let _creq = crate::http_request::sign(req, &params);
}
}
}
#[test]
fn apply_signing_instructions_headers() {
let mut headers = HeaderMap::new();
headers.insert("some-header", HeaderValue::from_static("foo"));
headers.insert("some-other-header", HeaderValue::from_static("bar"));
let instructions = SigningInstructions::new(Some(headers), None);
let mut request = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com")
.body("")
.unwrap();
instructions.apply_to_request(&mut request);
let get_header = |n: &str| request.headers().get(n).unwrap().to_str().unwrap();
assert_eq!("foo", get_header("some-header"));
assert_eq!("bar", get_header("some-other-header"));
}
#[test]
fn apply_signing_instructions_query_params() {
let params = vec![
("some-param", Cow::Borrowed("f&o?o")),
("some-other-param?", Cow::Borrowed("bar")),
];
let instructions = SigningInstructions::new(None, Some(params));
let mut request = http::Request::builder()
.uri("https://some-endpoint.some-region.amazonaws.com/some/path")
.body("")
.unwrap();
instructions.apply_to_request(&mut request);
assert_eq!(
"/some/path?some-param=f%26o%3Fo&some-other-param%3F=bar",
request.uri().path_and_query().unwrap().to_string()
);
}
}

View File

@ -5,8 +5,8 @@
//! Functions shared between the tests of several modules.
use bytes::Bytes;
use http::{Method, Request, Uri, Version};
use crate::http_request::{SignableBody, SignableRequest};
use http::{Method, Request, Uri};
use std::error::Error as StdError;
fn path(name: &str, ext: &str) -> String {
@ -34,19 +34,19 @@ pub(crate) fn test_sts(name: &str) -> String {
read(&path(name, "sts"))
}
pub(crate) fn test_request(name: &str) -> Request<Bytes> {
pub(crate) fn test_request(name: &str) -> TestRequest {
test_parsed_request(name, "req")
}
pub(crate) fn test_signed_request(name: &str) -> Request<Bytes> {
pub(crate) fn test_signed_request(name: &str) -> TestRequest {
test_parsed_request(name, "sreq")
}
pub(crate) fn test_signed_request_query_params(name: &str) -> Request<Bytes> {
pub(crate) fn test_signed_request_query_params(name: &str) -> TestRequest {
test_parsed_request(name, "qpsreq")
}
fn test_parsed_request(name: &str, ext: &str) -> Request<Bytes> {
fn test_parsed_request(name: &str, ext: &str) -> TestRequest {
let path = path(name, ext);
match parse_request(read(&path).as_bytes()) {
Ok(parsed) => parsed,
@ -54,15 +54,86 @@ fn test_parsed_request(name: &str, ext: &str) -> Request<Bytes> {
}
}
pub(crate) fn make_headers_comparable<B>(request: &mut Request<B>) {
for (_name, value) in request.headers_mut() {
value.set_sensitive(false);
pub(crate) struct TestRequest {
pub(crate) uri: String,
pub(crate) method: String,
pub(crate) headers: Vec<(String, String)>,
pub(crate) body: TestSignedBody,
}
pub(crate) enum TestSignedBody {
Signable(SignableBody<'static>),
Bytes(Vec<u8>),
}
impl TestSignedBody {
fn as_signable_body(&self) -> SignableBody<'_> {
match self {
TestSignedBody::Signable(data) => data.clone(),
TestSignedBody::Bytes(data) => SignableBody::Bytes(data.as_slice()),
}
}
}
fn parse_request(
s: &[u8],
) -> Result<Request<bytes::Bytes>, Box<dyn StdError + Send + Sync + 'static>> {
impl TestRequest {
pub(crate) fn set_body(&mut self, body: SignableBody<'static>) {
self.body = TestSignedBody::Signable(body);
}
pub(crate) fn as_http_request(&self) -> http::Request<&'static str> {
let mut builder = http::Request::builder()
.uri(&self.uri)
.method(Method::from_bytes(self.method.as_bytes()).unwrap());
for (k, v) in &self.headers {
builder = builder.header(k, v);
}
builder.body("body").unwrap()
}
}
impl<B: AsRef<[u8]>> From<http::Request<B>> for TestRequest {
fn from(value: Request<B>) -> Self {
let invalid = value
.headers()
.values()
.find(|h| std::str::from_utf8(h.as_bytes()).is_err());
if let Some(invalid) = invalid {
panic!("invalid header: {:?}", invalid);
}
Self {
uri: value.uri().to_string(),
method: value.method().to_string(),
headers: value
.headers()
.iter()
.map(|(k, v)| {
(
k.to_string(),
String::from_utf8(v.as_bytes().to_vec()).unwrap(),
)
})
.collect::<Vec<_>>(),
body: TestSignedBody::Bytes(value.body().as_ref().to_vec()),
}
}
}
impl<'a> From<&'a TestRequest> for SignableRequest<'a> {
fn from(request: &'a TestRequest) -> SignableRequest<'a> {
SignableRequest::new(
&request.method,
&request.uri,
request
.headers
.iter()
.map(|(k, v)| (k.as_str(), v.as_str())),
request.body.as_signable_body(),
)
.expect("URI MUST be valid")
}
}
fn parse_request(s: &[u8]) -> Result<TestRequest, Box<dyn StdError + Send + Sync + 'static>> {
let mut headers = [httparse::EMPTY_HEADER; 64];
// httparse 1.5 requires two trailing newlines to head the header section.
let mut with_newline = Vec::from(s);
@ -70,37 +141,30 @@ fn parse_request(
let mut req = httparse::Request::new(&mut headers);
let _ = req.parse(&with_newline).unwrap();
let version = match req.version.unwrap() {
1 => Version::HTTP_11,
_ => unimplemented!(),
};
let method = match req.method.unwrap() {
"GET" => Method::GET,
"POST" => Method::POST,
_ => unimplemented!(),
};
let mut builder = Request::builder();
builder = builder.version(version);
builder = builder.method(method);
let mut uri_builder = Uri::builder().scheme("https");
if let Some(path) = req.path {
uri_builder = uri_builder.path_and_query(path);
}
let mut headers = vec![];
for header in req.headers {
let name = header.name.to_lowercase();
if name == "host" {
uri_builder = uri_builder.authority(header.value);
} else if !name.is_empty() {
builder = builder.header(&name, header.value);
headers.push((
header.name.to_string(),
std::str::from_utf8(header.value)?.to_string(),
));
}
}
builder = builder.uri(uri_builder.build()?);
let req = builder.body(bytes::Bytes::new())?;
Ok(req)
Ok(TestRequest {
uri: uri_builder.build()?.to_string(),
method: req.method.unwrap().to_string(),
headers,
body: TestSignedBody::Bytes(vec![]),
})
}
#[test]