From 68956a3804c7f5dd3cb77b61844f3e841cc114e0 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 11 Aug 2021 10:51:17 -0700 Subject: [PATCH] Merge changes from rcoh/sigv4 into aws-sigv4 (#646) * Merge aws-sigv4 changes from rcoh/sigv4 * Remove readme * Use aws-sigv4 package in smithy-rs instead of rcoh/sigv4 --- aws/rust-runtime/aws-sig-auth/Cargo.toml | 3 +- .../aws-sig-auth/src/middleware.rs | 2 +- aws/rust-runtime/aws-sig-auth/src/signer.rs | 8 +- aws/rust-runtime/aws-sigv4/Cargo.toml | 3 +- aws/rust-runtime/aws-sigv4/README.md | 3 - .../get-vanilla-query-order-key-case.req | 1 - aws/rust-runtime/aws-sigv4/src/lib.rs | 306 ++++++++++++++---- aws/rust-runtime/aws-sigv4/src/sign.rs | 8 - aws/rust-runtime/aws-sigv4/src/types.rs | 156 ++++++++- aws/sdk/build.gradle.kts | 1 + 10 files changed, 383 insertions(+), 108 deletions(-) delete mode 100644 aws/rust-runtime/aws-sigv4/README.md diff --git a/aws/rust-runtime/aws-sig-auth/Cargo.toml b/aws/rust-runtime/aws-sig-auth/Cargo.toml index a94f6a505..9b3bf21e3 100644 --- a/aws/rust-runtime/aws-sig-auth/Cargo.toml +++ b/aws/rust-runtime/aws-sig-auth/Cargo.toml @@ -9,8 +9,7 @@ license = "Apache-2.0" [dependencies] http = "0.2.2" -# Renaming to clearly indicate that this is not a permanent signing solution -aws-sigv4-poc = { package = "aws-sigv4", git = "https://github.com/rcoh/sigv4", rev = "66b1646a7ab119c73be966ca70ee5f556bd8379b"} +aws-sigv4 = { path = "../aws-sigv4" } aws-auth = { path = "../aws-auth" } aws-types = { path = "../aws-types" } smithy-http = { path = "../../../rust-runtime/smithy-http" } diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs index 5c83fd6b9..dd793924f 100644 --- a/aws/rust-runtime/aws-sig-auth/src/middleware.rs +++ b/aws/rust-runtime/aws-sig-auth/src/middleware.rs @@ -5,7 +5,7 @@ use crate::signer::{OperationSigningConfig, RequestConfig, SigV4Signer, SigningError}; use aws_auth::Credentials; -use aws_sigv4_poc::SignableBody; +use aws_sigv4::SignableBody; use aws_types::region::SigningRegion; use aws_types::SigningService; use smithy_http::middleware::MapRequest; diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs index 2fef30079..d15cad2eb 100644 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ b/aws/rust-runtime/aws-sig-auth/src/signer.rs @@ -4,7 +4,7 @@ */ use aws_auth::Credentials; -use aws_sigv4_poc::{PayloadChecksumKind, SigningSettings, UriEncoding}; +use aws_sigv4::{PayloadChecksumKind, SigningSettings, UriEncoding}; use aws_types::region::SigningRegion; use aws_types::SigningService; use http::header::HeaderName; @@ -13,7 +13,7 @@ use std::error::Error; use std::fmt; use std::time::SystemTime; -pub use aws_sigv4_poc::SignableBody; +pub use aws_sigv4::SignableBody; #[derive(Eq, PartialEq, Clone, Copy)] pub enum SigningAlgorithm { @@ -126,7 +126,7 @@ impl SigV4Signer { } else { PayloadChecksumKind::NoHeader }; - let sigv4_config = aws_sigv4_poc::Config { + let sigv4_config = aws_sigv4::Config { access_key: credentials.access_key_id(), secret_key: credentials.secret_access_key(), security_token: credentials.session_token(), @@ -150,7 +150,7 @@ impl SigV4Signer { .map(SignableBody::Bytes) .unwrap_or(SignableBody::UnsignedPayload) }); - for (key, value) in aws_sigv4_poc::sign_core(request, signable_body, &sigv4_config)? { + for (key, value) in aws_sigv4::sign_core(request, signable_body, &sigv4_config)? { request .headers_mut() .append(HeaderName::from_static(key), value); diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index e636d2496..73c9cdcc8 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -21,7 +21,8 @@ serde_urlencoded = "0.7" bytes = "1" hex = "0.4" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +percent-encoding = "2.1" [dev-dependencies] pretty_assertions = "0.6" -httparse = "1" \ No newline at end of file +httparse = "1" diff --git a/aws/rust-runtime/aws-sigv4/README.md b/aws/rust-runtime/aws-sigv4/README.md deleted file mode 100644 index 595f20375..000000000 --- a/aws/rust-runtime/aws-sigv4/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## SigV4 - -This crates implements an incomplete, poorly documented implementation of SigV4 signing. Use at your own risk! \ No newline at end of file diff --git a/aws/rust-runtime/aws-sigv4/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req b/aws/rust-runtime/aws-sigv4/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req index ff2ffdca6..1158ac4eb 100644 --- a/aws/rust-runtime/aws-sigv4/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req +++ b/aws/rust-runtime/aws-sigv4/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req @@ -1,3 +1,2 @@ GET /?Param2=value2&Param1=value1 HTTP/1.1 Host:example.amazonaws.com -X-Amz-Date:20150830T123600Z diff --git a/aws/rust-runtime/aws-sigv4/src/lib.rs b/aws/rust-runtime/aws-sigv4/src/lib.rs index 1bbf5d023..f9b902910 100644 --- a/aws/rust-runtime/aws-sigv4/src/lib.rs +++ b/aws/rust-runtime/aws-sigv4/src/lib.rs @@ -1,5 +1,8 @@ use chrono::{DateTime, Utc}; -use http::header; +use http::{ + header::{self, HeaderName}, + HeaderValue, +}; use serde::{Deserialize, Serialize}; use std::{iter, str}; @@ -8,6 +11,7 @@ pub const DATE_FORMAT: &str = "%Y%m%dT%H%M%SZ"; pub const X_AMZ_SECURITY_TOKEN: &str = "x-amz-security-token"; pub const X_AMZ_DATE: &str = "x-amz-date"; pub const X_AMZ_TARGET: &str = "x-amz-target"; +pub const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256"; pub mod sign; pub mod types; @@ -15,10 +19,9 @@ pub mod types; type Error = Box; use crate::UriEncoding::Double; -use http::header::HeaderName; -use sign::{calculate_signature, encode_with_hex, generate_signing_key}; +use sign::{calculate_signature, encode_bytes_with_hex, generate_signing_key}; use std::time::SystemTime; -use types::{AsSigV4, CanonicalRequest, DateTimeExt, StringToSign}; +use types::{AsSigV4, CanonicalRequest, StringToSign}; pub fn sign( req: &mut http::Request, @@ -29,9 +32,11 @@ pub fn sign( where B: AsRef<[u8]>, { + let signable_body = SignableBody::Bytes(req.body().as_ref()); for (header_name, header_value) in sign_core( &req, - Config { + signable_body, + &Config { access_key: &credential.access_key, secret_key: &credential.secret_key, security_token: credential.security_token.as_deref(), @@ -40,34 +45,14 @@ where date: SystemTime::now(), settings: Default::default(), }, - ) { + )? { req.headers_mut() - .append(header_name.header_name(), header_value.parse()?); + .append(HeaderName::from_static(header_name), header_value); } Ok(()) } -/// SignatureKey is the key portion of the key-value pair of a generated SigV4 signature. -/// -/// When signing with SigV4, the algorithm produces multiple components of a signature that MUST -/// be applied to a request. -pub enum SignatureKey { - Authorization, - AmzDate, - AmzSecurityToken, -} - -impl SignatureKey { - pub fn header_name(&self) -> HeaderName { - match self { - SignatureKey::Authorization => header::AUTHORIZATION, - SignatureKey::AmzDate => HeaderName::from_static(X_AMZ_DATE), - SignatureKey::AmzSecurityToken => HeaderName::from_static(X_AMZ_SECURITY_TOKEN), - } - } -} - pub struct Config<'a> { pub access_key: &'a str, pub secret_key: &'a str, @@ -82,11 +67,29 @@ pub struct Config<'a> { } #[derive(Debug, PartialEq)] +#[non_exhaustive] pub struct SigningSettings { /// We assume the URI will be encoded _once_ prior to transmission. Some services /// do not decode the path prior to checking the signature, requiring clients to actually /// _double-encode_ the URI in creating the canonical request in order to pass a signature check. pub uri_encoding: UriEncoding, + + /// Add an additional checksum header + pub payload_checksum_kind: PayloadChecksumKind, +} + +#[non_exhaustive] +#[derive(Debug, Eq, PartialEq)] +pub enum PayloadChecksumKind { + /// Add x-amz-checksum-sha256 to the canonical request + /// + /// This setting is required for S3 + XAmzSha256, + + /// Do not add an additional header when creating the canonical request + /// + /// This is "normal mode" and will work for services other than S3 + NoHeader, } #[non_exhaustive] @@ -103,17 +106,38 @@ impl Default for SigningSettings { fn default() -> Self { Self { uri_encoding: Double, + payload_checksum_kind: PayloadChecksumKind::NoHeader, } } } +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum SignableBody<'a> { + /// A body composed of a slice of bytes + Bytes(&'a [u8]), + /// An unsigned payload + /// + /// UnsignedPayload is used for streaming requests where the contents of the body cannot be + /// known prior to signing + UnsignedPayload, + + /// A precomputed body checksum. The checksum should be a SHA256 checksum of the body, + /// lowercase hex encoded. Eg: + /// `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` + Precomputed(String), +} + +/// req MUST NOT contain any of the following headers: +/// - x-amz-date +/// - x-amz-content-sha-256 +/// - x-amz-security-token pub fn sign_core<'a, B>( req: &'a http::Request, - config: Config<'a>, -) -> impl Iterator -where - B: AsRef<[u8]>, -{ + body: SignableBody, + config: &'a Config<'a>, +) -> Result, Error> { + // Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html. let Config { access_key, secret_key, @@ -123,12 +147,11 @@ where date, settings, } = config; - // Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html. - let creq = CanonicalRequest::from(req, &settings).unwrap(); + let date = DateTime::::from(*date); + let (creq, extra_headers) = CanonicalRequest::from(req, body, settings, date, *security_token)?; // Step 2: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-string-to-sign.html. - let encoded_creq = &encode_with_hex(creq.fmt()); - let date = DateTime::::from(date); + let encoded_creq = &encode_bytes_with_hex(creq.fmt().as_bytes()); let sts = StringToSign::new(date, region, svc, encoded_creq); // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html @@ -136,15 +159,24 @@ where let signature = calculate_signature(signing_key, &sts.fmt().as_bytes()); // Step 4: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-add-signature-to-request.html - let authorization = build_authorization_header(access_key, creq, sts, &signature); - let x_azn_date = date.fmt_aws(); - - let mut tok = security_token.map(|it| it.to_string()); - iter::once((SignatureKey::Authorization, authorization)) - .chain(iter::once((SignatureKey::AmzDate, x_azn_date))) - .chain(iter::from_fn(move || { - tok.take().map(|tok| (SignatureKey::AmzSecurityToken, tok)) - })) + let mut authorization: HeaderValue = + build_authorization_header(access_key, &creq, sts, &signature).parse()?; + authorization.set_sensitive(true); + + // Construct an iterator of headers that the caller can attach to their request + // either as headers or as query parameters to create a presigned URL + let date = (X_AMZ_DATE, extra_headers.x_amz_date); + let mut security_token = extra_headers + .x_amz_security_token + .map(|tok| (X_AMZ_SECURITY_TOKEN, tok)); + let mut content = extra_headers + .x_amz_content_256 + .map(|content| (X_AMZ_CONTENT_SHA_256, content)); + let auth = iter::once(("authorization", authorization)); + let date = iter::once(date); + Ok(auth.chain(date).chain(iter::from_fn(move || { + security_token.take().or_else(|| content.take()) + }))) } #[derive(Debug, PartialEq, Serialize, Deserialize, Default, Clone)] @@ -171,14 +203,15 @@ impl<'a> Credentials<'a> { mod tests { use crate::{ assert_req_eq, build_authorization_header, read, - sign::{calculate_signature, encode_with_hex, generate_signing_key}, + sign::{calculate_signature, encode_bytes_with_hex, generate_signing_key}, types::{AsSigV4, CanonicalRequest, DateExt, DateTimeExt, Scope, StringToSign}, - Error, SigningSettings, DATE_FORMAT, + Error, PayloadChecksumKind, SignableBody, SigningSettings, DATE_FORMAT, }; use chrono::{Date, DateTime, NaiveDateTime, Utc}; - use http::{Method, Request, Uri, Version}; + use http::{HeaderValue, Method, Request, Uri, Version}; use pretty_assertions::assert_eq; - use std::{convert::TryFrom, fs, str::FromStr}; + use std::fs; + use std::{convert::TryFrom, str::FromStr}; #[test] fn read_request() -> Result<(), Error> { @@ -189,19 +222,24 @@ mod tests { //file-name.sreq— the signed request. // Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html. - // let s = read!(req: "get-vanilla-query-order-key-case")?; - let s = std::fs::read_to_string("./aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req").unwrap(); - let mut req = parse_request(s.as_bytes())?; - let creq = CanonicalRequest::from(&mut req, &SigningSettings::default())?; + let s = read!(req: "get-vanilla-query-order-key-case"); + let req = parse_request(s.as_bytes())?; + let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); + let date = DateTime::::from_utc(date, Utc); + let (creq, _) = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &SigningSettings::default(), + date, + None, + )?; let actual = format!("{}", creq); let expected = read!(creq: "get-vanilla-query-order-key-case"); assert_eq!(actual, expected); // Step 2: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-string-to-sign.html. - let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); - let date = DateTime::::from_utc(date, Utc); - let encoded_creq = &encode_with_hex(creq.fmt()); + let encoded_creq = &encode_bytes_with_hex(creq.fmt().as_bytes()); let sts = StringToSign::new(date, "us-east-1", "service", encoded_creq); // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html @@ -212,15 +250,15 @@ mod tests { let access = "AKIDEXAMPLE"; // step 4: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-add-signature-to-request.html - let authorization = build_authorization_header(access, creq, sts, &signature); + let authorization = build_authorization_header(access, &creq, sts, &signature); let x_azn_date = date.fmt_aws(); let s = read!(req: "get-vanilla-query-order-key-case"); let mut req = parse_request(s.as_bytes())?; let headers = req.headers_mut(); - headers.insert("authorization", authorization.parse()?); headers.insert("X-Amz-Date", x_azn_date.parse()?); + headers.insert("authorization", authorization.parse()?); let expected = read!(sreq: "get-vanilla-query-order-key-case"); let expected = parse_request(expected.as_bytes())?; assert_req_eq!(expected, req); @@ -229,21 +267,121 @@ mod tests { } #[test] - fn test_build_authorization_header() -> Result<(), Error> { + fn test_set_xamz_sha_256() -> Result<(), Error> { let s = read!(req: "get-vanilla-query-order-key-case"); - let mut req = parse_request(s.as_bytes())?; - let creq = CanonicalRequest::from(&mut req, &SigningSettings::default())?; + let req = parse_request(s.as_bytes())?; + let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); + let date = DateTime::::from_utc(date, Utc); + let mut signing_settings = SigningSettings::default(); + signing_settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; + let (creq, new_headers) = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &signing_settings, + date, + None, + )?; + assert_eq!( + new_headers.x_amz_content_256, + Some(HeaderValue::from_static( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + )) + ); + // assert that the sha256 header was added + assert_eq!( + creq.signed_headers.fmt(), + "host;x-amz-content-sha256;x-amz-date" + ); + + signing_settings.payload_checksum_kind = PayloadChecksumKind::NoHeader; + let (creq, new_headers) = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &signing_settings, + date, + None, + )?; + assert_eq!(new_headers.x_amz_content_256, None); + assert_eq!(creq.signed_headers.fmt(), "host;x-amz-date"); + Ok(()) + } + + #[test] + fn test_unsigned_payload() -> Result<(), Error> { + let s = read!(req: "get-vanilla-query-order-key-case"); + let req = parse_request(s.as_bytes())?; + let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); + let date = DateTime::::from_utc(date, Utc); + let mut signing_settings = SigningSettings::default(); + signing_settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; + let (creq, new_headers) = CanonicalRequest::from( + &req, + SignableBody::UnsignedPayload, + &signing_settings, + date, + None, + )?; + assert_eq!( + new_headers.x_amz_content_256, + Some(HeaderValue::from_static("UNSIGNED-PAYLOAD")) + ); + assert_eq!(creq.payload_hash, "UNSIGNED-PAYLOAD"); + Ok(()) + } + + #[test] + fn test_precomputed_payload() -> Result<(), Error> { + let s = read!(req: "get-vanilla-query-order-key-case"); + let req = parse_request(s.as_bytes())?; + let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); + let date = DateTime::::from_utc(date, Utc); + let mut signing_settings = SigningSettings::default(); + signing_settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; + let (creq, new_headers) = CanonicalRequest::from( + &req, + SignableBody::Precomputed(String::from( + "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072", + )), + &signing_settings, + date, + None, + )?; + assert_eq!( + new_headers.x_amz_content_256, + Some(HeaderValue::from_static( + "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072" + )) + ); + assert_eq!( + creq.payload_hash, + "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072" + ); + Ok(()) + } + #[test] + fn test_build_authorization_header() -> Result<(), Error> { + let s = read!(req: "get-vanilla-query-order-key-case"); + let req = parse_request(s.as_bytes())?; let date = NaiveDateTime::parse_from_str("20150830T123600Z", DATE_FORMAT).unwrap(); let date = DateTime::::from_utc(date, Utc); - let encoded_creq = &encode_with_hex(creq.fmt()); + let creq = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &SigningSettings::default(), + date, + None, + )? + .0; + + let encoded_creq = &encode_bytes_with_hex(creq.fmt().as_bytes()); let sts = StringToSign::new(date, "us-east-1", "service", encoded_creq); let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; let signing_key = generate_signing_key(secret, date.date(), "us-east-1", "service"); let signature = calculate_signature(signing_key, &sts.fmt().as_bytes()); let expected_header = read!(authz: "get-vanilla-query-order-key-case"); - let header = build_authorization_header("AKIDEXAMPLE", creq, sts, &signature); + let header = build_authorization_header("AKIDEXAMPLE", &creq, sts, &signature); assert_eq!(expected_header, header); Ok(()) @@ -302,7 +440,7 @@ mod tests { #[test] fn sign_payload_empty_string() { let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - let actual = encode_with_hex(String::new()); + let actual = encode_bytes_with_hex(&[]); assert_eq!(expected, actual); } @@ -329,7 +467,7 @@ mod tests { let date = DateTime::parse_aws("20150830T123600Z")?; let creq = read!(creq: "get-vanilla-query-order-key-case"); let expected_sts = read!(sts: "get-vanilla-query-order-key-case"); - let encoded = encode_with_hex(creq); + let encoded = encode_bytes_with_hex(creq.as_bytes()); let actual = StringToSign::new(date, "us-east-1", "service", &encoded); assert_eq!(expected_sts, actual.fmt()); @@ -340,7 +478,8 @@ mod tests { #[test] fn test_signature_calculation() -> Result<(), Error> { let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; - let creq = fs::read_to_string("./aws-sig-v4-test-suite/iam.creq")?.replace("\r\n", "\n"); + let creq = + std::fs::read_to_string("./aws-sig-v4-test-suite/iam.creq")?.replace("\r\n", "\n"); let date = DateTime::parse_aws("20150830T123600Z")?; let derived_key = generate_signing_key(secret, date.date(), "us-east-1", "iam"); @@ -369,7 +508,7 @@ mod tests { #[test] fn test_digest_of_canonical_request() -> Result<(), Error> { let creq = read!(creq: "get-vanilla-query-order-key-case"); - let actual = encode_with_hex(creq); + let actual = encode_bytes_with_hex(creq.as_bytes()); let expected = "816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0"; assert_eq!(expected, actual); @@ -380,16 +519,41 @@ mod tests { fn test_double_url_encode() -> Result<(), Error> { let s = read!(req: "double-url-encode"); let req = parse_request(s.as_bytes())?; - println!("{:?}", req); - let creq = CanonicalRequest::from(&req, &SigningSettings::default())?; + let date = DateTime::parse_aws("20210511T154045Z")?; + let creq = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &SigningSettings::default(), + date, + None, + )? + .0; let actual = format!("{}", creq); let expected = read!(creq: "double-url-encode"); - println!("{}", actual); assert_eq!(actual, expected); Ok(()) } + #[test] + fn test_tilde_in_uri() -> Result<(), Error> { + 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(); + let date = DateTime::parse_aws("20210511T154045Z")?; + let creq = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body().as_ref()), + &SigningSettings::default(), + date, + None, + )? + .0; + assert_eq!( + creq.params, + "k=&list-type=2&prefix=~objprefix&single=&unreserved=-_.~" + ); + Ok(()) + } + fn parse_request(s: &[u8]) -> Result, Error> { let mut headers = [httparse::EMPTY_HEADER; 64]; let mut req = httparse::Request::new(&mut headers); @@ -428,7 +592,7 @@ mod tests { // Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature fn build_authorization_header( access_key: &str, - creq: CanonicalRequest, + creq: &CanonicalRequest, sts: StringToSign, signature: &str, ) -> String { diff --git a/aws/rust-runtime/aws-sigv4/src/sign.rs b/aws/rust-runtime/aws-sigv4/src/sign.rs index 978e7f776..5f4157824 100644 --- a/aws/rust-runtime/aws-sigv4/src/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/sign.rs @@ -11,14 +11,6 @@ pub fn encode(s: String) -> Vec { calculated.as_ref().to_vec() } -/// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) -pub fn encode_with_hex(s: String) -> String { - let digest: Digest = digest::digest(&digest::SHA256, s.as_bytes()); - // no need to lower-case as in step six, as hex::encode - // already returns a lower-cased string. - hex::encode(digest) -} - /// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) pub fn encode_bytes_with_hex(bytes: B) -> String where diff --git a/aws/rust-runtime/aws-sigv4/src/types.rs b/aws/rust-runtime/aws-sigv4/src/types.rs index eaae88764..2bbc46e6e 100644 --- a/aws/rust-runtime/aws-sigv4/src/types.rs +++ b/aws/rust-runtime/aws-sigv4/src/types.rs @@ -1,16 +1,23 @@ use crate::{ - sign::encode_bytes_with_hex, Error, SigningSettings, UriEncoding, DATE_FORMAT, HMAC_256, + header::HeaderValue, sign::encode_bytes_with_hex, Error, PayloadChecksumKind, SignableBody, + SigningSettings, UriEncoding, DATE_FORMAT, HMAC_256, X_AMZ_CONTENT_SHA_256, X_AMZ_DATE, + X_AMZ_SECURITY_TOKEN, }; use chrono::{format::ParseError, Date, DateTime, NaiveDate, NaiveDateTime, Utc}; -use http::{header::HeaderName, HeaderMap, Method, Request}; +use http::{ + header::{HeaderName, USER_AGENT}, + HeaderMap, Method, Request, +}; use serde_urlencoded as qs; use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, - convert::{AsRef, TryFrom}, + convert::TryFrom, fmt, }; +const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; + pub(crate) trait AsSigV4 { fn fmt(&self) -> String; } @@ -25,14 +32,37 @@ pub(crate) struct CanonicalRequest { pub(crate) payload_hash: String, } +pub(crate) struct AddedHeaders { + pub x_amz_date: HeaderValue, + pub x_amz_content_256: Option, + pub x_amz_security_token: Option, +} + impl CanonicalRequest { + /// Construct a CanonicalRequest from an HttpRequest and a signable body + /// + /// This function returns 2 things: + /// 1. The canonical request to use for signing + /// 2. `AddedHeaders`, a struct recording the additional headers that were added. These will + /// behavior returned to the top level caller. If the caller wants to create a + /// presigned URL, they can apply these parameters to the query string. + /// + /// ## Behavior + /// There are several settings which alter signing behavior: + /// - If a `security_token` is provided as part of the credentials it will be included in the signed headers + /// - If `settings.uri_encoding` specifies double encoding, `%` in the URL will be rencoded as + /// `%25` + /// - If settings.payload_checksum_kind is XAmzSha256, add a x-amz-content-sha256 with the body + /// checksum. This is the same checksum used as the "payload_hash" in the canonical request pub(crate) fn from( req: &Request, + body: SignableBody, settings: &SigningSettings, - ) -> Result - where - B: AsRef<[u8]>, - { + date: DateTime, + security_token: Option<&str>, + ) -> Result<(CanonicalRequest, AddedHeaders), Error> { + // Path encoding: if specified, rencode % as %25 + // Set method and path into CanonicalRequest let path = req.uri().path(); let path = match settings.uri_encoding { // The string is already URI encoded, we don't need to encode everything again, just `%` @@ -47,23 +77,115 @@ impl CanonicalRequest { if let Some(path) = req.uri().query() { let params: BTreeMap = qs::from_str(path)?; - creq.params = qs::to_string(params)?; + let n = params.len(); + let mut out = String::new(); + for (i, (k, v)) in params.into_iter().enumerate() { + let last = i == n - 1; + out.push_str( + &percent_encoding::percent_encode(&k.as_bytes(), BASE_SET).to_string(), + ); + out.push('='); + out.push_str( + &percent_encoding::percent_encode(&v.as_bytes(), BASE_SET).to_string(), + ); + if !last { + out.push('&'); + } + } + creq.params = out; + } + + // Payload hash computation + // + // Based on the input body, set the payload_hash of the canonical request: + // Either: + // - compute a hash + // - use the precomputed hash + // - use `UnsignedPayload` + let payload_hash = match body { + SignableBody::Bytes(data) => encode_bytes_with_hex(data), + SignableBody::Precomputed(digest) => digest, + SignableBody::UnsignedPayload => UNSIGNED_PAYLOAD.to_string(), + }; + creq.payload_hash = payload_hash; + + // Header computation: + // The canonical request will include headers not present in the input. We need to clone + // the headers from the original request and add: + // - x-amz-date + // - x-amz-security-token (if provided) + // - x-amz-content-sha256 (if requested by signing settings) + let mut canonical_headers = req.headers().clone(); + let x_amz_date = HeaderName::from_static(X_AMZ_DATE); + let date_header = + HeaderValue::try_from(date.fmt_aws()).expect("date is valid header value"); + canonical_headers.insert(x_amz_date, date_header.clone()); + // to return headers to the user, record which headers we added + let mut out = AddedHeaders { + x_amz_date: date_header, + x_amz_content_256: None, + x_amz_security_token: None, + }; + + if let Some(security_token) = security_token { + let mut sec_header = HeaderValue::from_str(security_token)?; + sec_header.set_sensitive(true); + canonical_headers.insert(X_AMZ_SECURITY_TOKEN, sec_header.clone()); + out.x_amz_security_token = Some(sec_header); + } + + if settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 { + let header = HeaderValue::from_str(&creq.payload_hash)?; + canonical_headers.insert(X_AMZ_CONTENT_SHA_256, header.clone()); + out.x_amz_content_256 = Some(header); } #[allow(clippy::mutable_key_type)] - let mut headers = BTreeSet::new(); - for (name, _) in req.headers() { - headers.insert(CanonicalHeaderName(name.clone())); + let mut signed_headers = BTreeSet::new(); + for (name, _) in canonical_headers.iter() { + // The user agent header should not be signed because it may + // be alterted by proxies + if name != USER_AGENT { + signed_headers.insert(CanonicalHeaderName(name.clone())); + } } - creq.signed_headers = SignedHeaders { inner: headers }; - creq.headers = req.headers().clone(); - let body: &[u8] = req.body().as_ref(); - let payload = encode_bytes_with_hex(body); - creq.payload_hash = payload; - Ok(creq) + creq.signed_headers = SignedHeaders { + inner: signed_headers, + }; + creq.headers = canonical_headers; + Ok((creq, out)) } } +use percent_encoding::{AsciiSet, CONTROLS}; + +/// base set of characters that must be URL encoded +pub const BASE_SET: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'/') + // RFC-3986 §3.3 allows sub-delims (defined in section2.2) to be in the path component. + // This includes both colon ':' and comma ',' characters. + // Smithy protocol tests & AWS services percent encode these expected values. Signing + // will fail if these values are not percent encoded + .add(b':') + .add(b',') + .add(b'?') + .add(b'#') + .add(b'[') + .add(b']') + .add(b'@') + .add(b'!') + .add(b'$') + .add(b'&') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b'+') + .add(b';') + .add(b'=') + .add(b'%'); + impl AsSigV4 for CanonicalRequest { fn fmt(&self) -> String { self.to_string() diff --git a/aws/sdk/build.gradle.kts b/aws/sdk/build.gradle.kts index 0ba5c6ed5..35b57def7 100644 --- a/aws/sdk/build.gradle.kts +++ b/aws/sdk/build.gradle.kts @@ -39,6 +39,7 @@ val awsModules = listOf( "aws-http", "aws-hyper", "aws-sig-auth", + "aws-sigv4", "aws-types" ) -- GitLab