diff --git a/CHANGELOG.md b/CHANGELOG.md index bcfdbd622a57f4cd7525bc7df5e4c59a326cee4a..08cb9b426e361ee767774fb59ac447df089c6716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ vNext (Month Day, Year) - Add experimental `dvr` module to smithy-client. This will enable easier testing of HTTP traffic. (#640) - Add profile file credential provider implementation. This implementation currently does not support credential sources for assume role providers other than environment variables. (#640) +- Add Event Stream support to aws-sigv4 (#648) - :bug: Fix name collision that occurred when a model had both a union and a structure named `Result` (#643) - Add initial implementation of a default provider chain. (#650) - Update smithy-client to simplify creating HTTP/HTTPS connectors (#650) diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs index dd793924f54f508be5b8a210b0ab2216d20efc19..5b1a868adf9dc2594f7d6986cb4c2c75edf1ca76 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::SignableBody; +use aws_sigv4::http_request::SignableBody; use aws_types::region::SigningRegion; use aws_types::SigningService; use smithy_http::middleware::MapRequest; @@ -14,6 +14,20 @@ use smithy_http::property_bag::PropertyBag; use std::time::SystemTime; use thiserror::Error; +/// Container for the request signature for use in the property bag. +#[non_exhaustive] +pub struct Signature(String); + +impl Signature { + pub fn new(signature: String) -> Self { + Self(signature) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + /// Middleware stage to sign requests with SigV4 /// /// SigV4RequestSignerStage will load configuration from the request property bag and add @@ -94,9 +108,11 @@ impl MapRequest for SigV4SigningStage { req.augment(|mut req, config| { let (operation_config, request_config, creds) = signing_config(config)?; - self.signer + let signature = self + .signer .sign(&operation_config, &request_config, &creds, &mut req) .map_err(|err| SigningStageError::SigningFailure(err))?; + config.insert(signature); Ok(req) }) } @@ -104,12 +120,12 @@ impl MapRequest for SigV4SigningStage { #[cfg(test)] mod test { - use crate::middleware::{SigV4SigningStage, SigningStageError}; + use crate::middleware::{SigV4SigningStage, Signature, SigningStageError}; use crate::signer::{OperationSigningConfig, SigV4Signer}; use aws_auth::Credentials; use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion}; use aws_endpoint::{set_endpoint_resolver, AwsEndpointStage}; - use aws_types::region::Region; + use aws_types::region::{Region, SigningRegion}; use aws_types::SigningService; use http::header::AUTHORIZATION; use smithy_http::body::SdkBody; @@ -119,6 +135,30 @@ mod test { use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; + #[test] + fn places_signature_in_property_bag() { + let req = http::Request::new(SdkBody::from("")); + let region = Region::new("us-east-1"); + let req = operation::Request::new(req) + .augment(|req, properties| { + properties.insert(region.clone()); + properties.insert(UNIX_EPOCH + Duration::new(1611160427, 0)); + properties.insert(SigningService::from_static("kinesis")); + properties.insert(OperationSigningConfig::default_config()); + properties.insert(Credentials::from_keys("AKIAfoo", "bar", None)); + properties.insert(SigningRegion::from(region)); + Result::<_, Infallible>::Ok(req) + }) + .expect("succeeds"); + + let signer = SigV4SigningStage::new(SigV4Signer::new()); + let req = signer.apply(req).unwrap(); + + let property_bag = req.properties(); + let signature = property_bag.get::(); + assert!(signature.is_some()); + } + // check that the endpoint middleware followed by signing middleware produce the expected result #[test] fn endpoint_plus_signer() { @@ -143,12 +183,9 @@ mod test { let endpoint = AwsEndpointStage; let signer = SigV4SigningStage::new(SigV4Signer::new()); let mut req = endpoint.apply(req).expect("add endpoint should succeed"); - let mut errs = vec![]; - errs.push( - signer - .apply(req.try_clone().expect("can clone")) - .expect_err("no signing config"), - ); + let mut errs = vec![signer + .apply(req.try_clone().expect("can clone")) + .expect_err("no signing config")]; let mut config = OperationSigningConfig::default_config(); config.signing_options.content_sha256_header = true; req.properties_mut().insert(config); diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs index d15cad2eb53c3224d4a583f1f5944e4b0f0696aa..6a7244b39e4b065aa5d94351c49b6e2eaeee5964 100644 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ b/aws/rust-runtime/aws-sig-auth/src/signer.rs @@ -4,7 +4,9 @@ */ use aws_auth::Credentials; -use aws_sigv4::{PayloadChecksumKind, SigningSettings, UriEncoding}; +use aws_sigv4::http_request::{ + calculate_signing_headers, PayloadChecksumKind, SigningSettings, UriEncoding, +}; use aws_types::region::SigningRegion; use aws_types::SigningService; use http::header::HeaderName; @@ -13,7 +15,8 @@ use std::error::Error; use std::fmt; use std::time::SystemTime; -pub use aws_sigv4::SignableBody; +use crate::middleware::Signature; +pub use aws_sigv4::http_request::SignableBody; #[derive(Eq, PartialEq, Clone, Copy)] pub enum SigningAlgorithm { @@ -114,7 +117,7 @@ impl SigV4Signer { request_config: &RequestConfig<'_>, credentials: &Credentials, request: &mut http::Request, - ) -> Result<(), SigningError> { + ) -> Result { let mut settings = SigningSettings::default(); settings.uri_encoding = if operation_config.signing_options.double_uri_encode { UriEncoding::Double @@ -126,13 +129,13 @@ impl SigV4Signer { } else { PayloadChecksumKind::NoHeader }; - let sigv4_config = aws_sigv4::Config { + let sigv4_config = aws_sigv4::http_request::SigningParams { access_key: credentials.access_key_id(), secret_key: credentials.secret_access_key(), security_token: credentials.session_token(), region: request_config.region.as_ref(), - svc: request_config.service.as_ref(), - date: request_config.request_ts, + service_name: request_config.service.as_ref(), + date_time: request_config.request_ts.into(), settings, }; @@ -150,12 +153,15 @@ impl SigV4Signer { .map(SignableBody::Bytes) .unwrap_or(SignableBody::UnsignedPayload) }); - for (key, value) in aws_sigv4::sign_core(request, signable_body, &sigv4_config)? { + + let (signing_headers, signature) = + calculate_signing_headers(request, signable_body, &sigv4_config)?.into_parts(); + for (key, value) in signing_headers { request .headers_mut() .append(HeaderName::from_static(key), value); } - Ok(()) + Ok(Signature::new(signature)) } } diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index 73c9cdcc811ba56f49d1a316c51fbec4412bb6e3..f14630c8213ca491edce6bcb40f648cce5b59177 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -1,28 +1,28 @@ [package] name = "aws-sigv4" version = "0.1.0" -authors = ["David Barsky "] +authors = ["David Barsky ", "AWS Rust SDK Team "] edition = "2018" -exclude = [ - "aws-sig-v4-test-suite/*" -] +exclude = ["aws-sig-v4-test-suite/*"] license = "MIT OR Apache-2.0" -description = "An AWS SigV4 request signer." -repository = "https://github.com/davidbarsky/sigv4" -homepage = "https://github.com/davidbarsky/sigv4" -documentation = "https://docs.rs/aws-sigv4" +description = "AWS SigV4 signer" + +[features] +sign-http = ["http", "http-body", "percent-encoding", "form_urlencoded"] +sign-eventstream = ["smithy-eventstream"] +default = ["sign-http", "sign-eventstream"] [dependencies] -http = "0.2" -http-body = "0.4" -ring = "0.16" -serde = { version = "1", features = ["derive"] } -serde_urlencoded = "0.7" -bytes = "1" -hex = "0.4" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } -percent-encoding = "2.1" +form_urlencoded = { version = "1.0", optional = true } +hex = "0.4" +http = { version = "0.2", optional = true } +http-body = { version = "0.4", optional = true } +percent-encoding = { version = "2.1", optional = true } +ring = "0.16" +smithy-eventstream = { path = "../../../rust-runtime/smithy-eventstream", optional = true } [dev-dependencies] +bytes = "1" pretty_assertions = "0.6" httparse = "1" diff --git a/aws/rust-runtime/aws-sigv4/src/date_fmt.rs b/aws/rust-runtime/aws-sigv4/src/date_fmt.rs new file mode 100644 index 0000000000000000000000000000000000000000..c84574a434c46994ce4359e64396d0044f80b149 --- /dev/null +++ b/aws/rust-runtime/aws-sigv4/src/date_fmt.rs @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +// Some of the functions in this file are unused when disabling certain features +#![allow(dead_code)] +use chrono::{Date, DateTime, NaiveDate, NaiveDateTime, ParseError, Utc}; + +const DATE_TIME_FORMAT: &str = "%Y%m%dT%H%M%SZ"; +const DATE_FORMAT: &str = "%Y%m%d"; + +/// Formats a chrono `Date` in `YYYYMMDD` format. +pub fn format_date(date: &Date) -> String { + date.format(DATE_FORMAT).to_string() +} + +/// Parses `YYYYMMDD` formatted dates into a chrono `Date`. +pub fn parse_date(date_str: &str) -> Result, ParseError> { + Ok(Date::::from_utc( + NaiveDate::parse_from_str(date_str, "%Y%m%d")?, + Utc, + )) +} + +/// Formats a chrono `DateTime` in `YYYYMMDD'T'HHMMSS'Z'` format. +pub fn format_date_time(date_time: &DateTime) -> String { + date_time.format(DATE_TIME_FORMAT).to_string() +} + +/// Parses `YYYYMMDD'T'HHMMSS'Z'` formatted dates into a chrono `DateTime`. +pub fn parse_date_time(date_time_str: &str) -> Result, ParseError> { + Ok(DateTime::::from_utc( + NaiveDateTime::parse_from_str(date_time_str, DATE_TIME_FORMAT)?, + Utc, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn date_time_roundtrip() { + let date = parse_date_time("20150830T123600Z").unwrap(); + assert_eq!("20150830T123600Z", format_date_time(&date)); + } + + #[test] + fn date_roundtrip() { + let date = parse_date("20150830").unwrap(); + assert_eq!("20150830", format_date(&date)); + } +} diff --git a/aws/rust-runtime/aws-sigv4/src/event_stream.rs b/aws/rust-runtime/aws-sigv4/src/event_stream.rs new file mode 100644 index 0000000000000000000000000000000000000000..2563753cb78f639ea62d7b5d5faf738bf69df174 --- /dev/null +++ b/aws/rust-runtime/aws-sigv4/src/event_stream.rs @@ -0,0 +1,172 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Utilities to sign Event Stream messages. + +use crate::date_fmt::{format_date, format_date_time}; +use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string}; +use crate::SigningOutput; +use chrono::{DateTime, SubsecRound, Utc}; +use smithy_eventstream::frame::{write_headers_to, Header, HeaderValue, Message}; +use std::io::Write; + +pub type SigningParams<'a> = super::SigningParams<'a, ()>; + +/// Creates a string to sign for an Event Stream message. +fn calculate_string_to_sign( + message_payload: &[u8], + last_signature: &str, + date_time: &DateTime, + params: &SigningParams<'_>, +) -> Vec { + // Event Stream string to sign format is documented here: + // https://docs.aws.amazon.com/transcribe/latest/dg/how-streaming.html + let date_time_str = format_date_time(&date_time); + let date_str = format_date(&date_time.date()); + + let mut sts: Vec = Vec::new(); + writeln!(sts, "AWS4-HMAC-SHA256-PAYLOAD").unwrap(); + writeln!(sts, "{}", date_time_str).unwrap(); + writeln!( + sts, + "{}/{}/{}/aws4_request", + date_str, params.region, params.service_name + ) + .unwrap(); + writeln!(sts, "{}", last_signature).unwrap(); + + let date_header = Header::new(":date", HeaderValue::Timestamp((*date_time).into())); + let mut date_buffer = Vec::new(); + write_headers_to(&[date_header], &mut date_buffer).unwrap(); + writeln!(sts, "{}", sha256_hex_string(&date_buffer)).unwrap(); + write!(sts, "{}", sha256_hex_string(&message_payload)).unwrap(); + sts +} + +/// Signs an Event Stream message with the given `credentials`. +/// +/// Each message's signature incorporates the signature of the previous message (`last_signature`). +/// The very first message incorporates the signature of the top-level request +/// for both HTTP 2 and WebSocket. +pub fn sign_message<'a>( + message: &'a Message, + last_signature: &'a str, + params: &'a SigningParams<'a>, +) -> SigningOutput { + // Truncate the sub-seconds up front since the timestamp written to the signed message header + // needs to exactly match the string formatted timestamp, which doesn't include sub-seconds. + let date_time = params.date_time.trunc_subsecs(0); + + let signing_key = generate_signing_key( + params.secret_key, + date_time.date(), + params.region, + params.service_name, + ); + let message_payload = { + let mut payload = Vec::new(); + message.write_to(&mut payload).unwrap(); + payload + }; + let string_to_sign = + calculate_string_to_sign(&message_payload, last_signature, &date_time, params); + let signature = calculate_signature(signing_key, &string_to_sign); + + // Generate the signed wrapper event frame + SigningOutput::new( + Message::new(message_payload) + .add_header(Header::new( + ":chunk-signature", + HeaderValue::ByteArray(hex::decode(&signature).unwrap().into()), + )) + .add_header(Header::new( + ":date", + HeaderValue::Timestamp(date_time.into()), + )), + signature, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, UNIX_EPOCH}; + + #[test] + fn string_to_sign() { + let message_to_sign = Message::new(&b"test payload"[..]).add_header(Header::new( + "some-header", + HeaderValue::String("value".into()), + )); + let mut message_payload = Vec::new(); + message_to_sign.write_to(&mut message_payload).unwrap(); + + let params = SigningParams { + access_key: "fake access key", + secret_key: "fake secret key", + security_token: None, + region: "us-east-1", + service_name: "testservice", + date_time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), + settings: (), + }; + + let expected = "\ + AWS4-HMAC-SHA256-PAYLOAD\n\ + 19731129T213309Z\n\ + 19731129/us-east-1/testservice/aws4_request\n\ + be1f8c7d79ef8e1abc5254a2c70e4da3bfaf4f07328f527444e1fc6ea67273e2\n\ + 0c0e3b3bf66b59b976181bd7d401927bbd624107303c713fd1e5f3d3c8dd1b1e\n\ + f2eba0f2e95967ee9fbc6db5e678d2fd599229c0d04b11e4fc8e0f2a02a806c6\ + "; + + let last_signature = sha256_hex_string(b"last message sts"); + assert_eq!( + expected, + std::str::from_utf8(&calculate_string_to_sign( + &message_payload, + &last_signature, + ¶ms.date_time, + ¶ms + )) + .unwrap() + ); + } + + #[test] + fn sign() { + let message_to_sign = Message::new(&b"test payload"[..]).add_header(Header::new( + "some-header", + HeaderValue::String("value".into()), + )); + let params = SigningParams { + access_key: "fake access key", + secret_key: "fake secret key", + security_token: None, + region: "us-east-1", + service_name: "testservice", + date_time: (UNIX_EPOCH + Duration::new(123_456_789_u64, 1234u32)).into(), + settings: (), + }; + + let last_signature = sha256_hex_string(b"last message sts"); + let (signed, signature) = + sign_message(&message_to_sign, &last_signature, ¶ms).into_parts(); + assert_eq!(":chunk-signature", signed.headers()[0].name().as_str()); + if let HeaderValue::ByteArray(bytes) = signed.headers()[0].value() { + assert_eq!(signature, hex::encode(bytes)); + } else { + panic!("expected byte array for :chunk-signature header"); + } + assert_eq!(":date", signed.headers()[1].name().as_str()); + if let HeaderValue::Timestamp(value) = signed.headers()[1].value() { + assert_eq!(123_456_789_i64, value.epoch_seconds()); + // The subseconds should have been truncated off + assert_eq!(0, value.epoch_subsecond_nanos()); + } else { + panic!("expected timestamp for :date header"); + } + } +} diff --git a/aws/rust-runtime/aws-sigv4/src/types.rs b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs similarity index 66% rename from aws/rust-runtime/aws-sigv4/src/types.rs rename to aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs index 2bbc46e6e011284dd39e792d955cd8f0abc6fa86..ebebc686c17b09ee1f8d0deb97e7007a3492503c 100644 --- a/aws/rust-runtime/aws-sigv4/src/types.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs @@ -1,43 +1,73 @@ -use crate::{ - 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, USER_AGENT}, - HeaderMap, Method, Request, -}; -use serde_urlencoded as qs; -use std::{ - cmp::Ordering, - collections::{BTreeMap, BTreeSet}, - convert::TryFrom, - fmt, +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use super::{ + Error, PayloadChecksumKind, SignableBody, SigningSettings, UriEncoding, HMAC_256, + X_AMZ_CONTENT_SHA_256, X_AMZ_DATE, X_AMZ_SECURITY_TOKEN, }; +use crate::date_fmt::{format_date, format_date_time, parse_date, parse_date_time}; +use crate::sign::sha256_hex_string; +use chrono::{Date, DateTime, Utc}; +use http::header::{HeaderName, USER_AGENT}; +use http::{HeaderMap, HeaderValue, Method, Request}; +use percent_encoding::{AsciiSet, CONTROLS}; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::convert::TryFrom; +use std::fmt; +use std::fmt::Formatter; const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; -pub(crate) trait AsSigV4 { - fn fmt(&self) -> String; -} +/// base set of characters that must be URL encoded +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'%'); -#[derive(Default, Debug, PartialEq)] -pub(crate) struct CanonicalRequest { - pub(crate) method: Method, - pub(crate) path: String, - pub(crate) params: String, - pub(crate) headers: HeaderMap, - pub(crate) signed_headers: SignedHeaders, - pub(crate) payload_hash: String, +fn percent_encode(value: &str) -> String { + percent_encoding::percent_encode(&value.as_bytes(), BASE_SET).to_string() } -pub(crate) struct AddedHeaders { +pub struct AddedHeaders { pub x_amz_date: HeaderValue, pub x_amz_content_256: Option, pub x_amz_security_token: Option, } +#[derive(Default, Debug, PartialEq)] +pub struct CanonicalRequest { + pub method: Method, + pub path: String, + pub params: String, + pub headers: HeaderMap, + pub signed_headers: SignedHeaders, + pub payload_hash: String, +} + impl CanonicalRequest { /// Construct a CanonicalRequest from an HttpRequest and a signable body /// @@ -54,7 +84,7 @@ impl CanonicalRequest { /// `%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( + pub fn from( req: &Request, body: SignableBody, settings: &SigningSettings, @@ -75,22 +105,22 @@ impl CanonicalRequest { ..Default::default() }; - if let Some(path) = req.uri().query() { - let params: BTreeMap = qs::from_str(path)?; - let n = params.len(); + if let Some(query) = req.uri().query() { + let mut first = true; 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 { + let mut params: Vec<(Cow, Cow)> = + form_urlencoded::parse(query.as_bytes()).collect(); + // Sort by param name, and then by param value + params.sort(); + for (key, value) in params { + if !first { out.push('&'); } + first = false; + + out.push_str(&percent_encode(&key)); + out.push('='); + out.push_str(&percent_encode(&value)); } creq.params = out; } @@ -103,7 +133,7 @@ impl CanonicalRequest { // - use the precomputed hash // - use `UnsignedPayload` let payload_hash = match body { - SignableBody::Bytes(data) => encode_bytes_with_hex(data), + SignableBody::Bytes(data) => sha256_hex_string(data), SignableBody::Precomputed(digest) => digest, SignableBody::UnsignedPayload => UNSIGNED_PAYLOAD.to_string(), }; @@ -118,7 +148,7 @@ impl CanonicalRequest { 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"); + HeaderValue::try_from(format_date_time(&date)).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 { @@ -140,58 +170,19 @@ impl CanonicalRequest { out.x_amz_content_256 = Some(header); } - #[allow(clippy::mutable_key_type)] - 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 + let mut signed_headers = Vec::with_capacity(canonical_headers.len()); + for (name, _) in &canonical_headers { + // The user agent header should not be signed because it may be altered by proxies if name != USER_AGENT { - signed_headers.insert(CanonicalHeaderName(name.clone())); + signed_headers.push(CanonicalHeaderName(name.clone())); } } - creq.signed_headers = SignedHeaders { - inner: signed_headers, - }; + creq.signed_headers = SignedHeaders::new(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() - } -} - impl fmt::Display for CanonicalRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", self.method)?; @@ -214,13 +205,14 @@ impl fmt::Display for CanonicalRequest { } #[derive(Debug, PartialEq, Default)] -pub(crate) struct SignedHeaders { - inner: BTreeSet, +pub struct SignedHeaders { + inner: Vec, } -impl AsSigV4 for SignedHeaders { - fn fmt(&self) -> String { - self.to_string() +impl SignedHeaders { + fn new(mut inner: Vec) -> Self { + inner.sort(); + SignedHeaders { inner } } } @@ -238,7 +230,7 @@ impl fmt::Display for SignedHeaders { } #[derive(Debug, PartialEq, Eq, Clone)] -pub(crate) struct CanonicalHeaderName(HeaderName); +pub struct CanonicalHeaderName(HeaderName); impl PartialOrd for CanonicalHeaderName { fn partial_cmp(&self, other: &Self) -> Option { @@ -253,17 +245,18 @@ impl Ord for CanonicalHeaderName { } #[derive(PartialEq, Debug, Clone)] -pub(crate) struct Scope<'a> { - pub(crate) date: Date, - pub(crate) region: &'a str, - pub(crate) service: &'a str, +pub struct Scope<'a> { + pub date: Date, + pub region: &'a str, + pub service: &'a str, } -impl<'a> AsSigV4 for Scope<'a> { - fn fmt(&self) -> String { - format!( +impl<'a> fmt::Display for Scope<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, "{}/{}/{}/aws4_request", - self.date.fmt_aws(), + format_date(&self.date), self.region, self.service ) @@ -274,7 +267,7 @@ impl<'a> TryFrom<&'a str> for Scope<'a> { type Error = Error; fn try_from(s: &'a str) -> Result, Self::Error> { let mut scopes = s.split('/'); - let date = Date::::parse_aws(scopes.next().expect("missing date"))?; + let date = parse_date(scopes.next().expect("missing date"))?; let region = scopes.next().expect("missing region"); let service = scopes.next().expect("missing service"); @@ -289,19 +282,19 @@ impl<'a> TryFrom<&'a str> for Scope<'a> { } #[derive(PartialEq, Debug)] -pub(crate) struct StringToSign<'a> { - pub(crate) scope: Scope<'a>, - pub(crate) date: DateTime, - pub(crate) region: &'a str, - pub(crate) service: &'a str, - pub(crate) hashed_creq: &'a str, +pub struct StringToSign<'a> { + pub scope: Scope<'a>, + pub date: DateTime, + pub region: &'a str, + pub service: &'a str, + pub hashed_creq: &'a str, } impl<'a> TryFrom<&'a str> for StringToSign<'a> { type Error = Error; fn try_from(s: &'a str) -> Result { let lines = s.lines().collect::>(); - let date = DateTime::::parse_aws(&lines[1])?; + let date = parse_date_time(&lines[1])?; let scope: Scope = TryFrom::try_from(lines[2])?; let hashed_creq = &lines[3]; @@ -339,48 +332,15 @@ impl<'a> StringToSign<'a> { } } -impl<'a> AsSigV4 for StringToSign<'a> { - fn fmt(&self) -> String { - format!( +impl<'a> fmt::Display for StringToSign<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, "{}\n{}\n{}\n{}", HMAC_256, - self.date.fmt_aws(), - self.scope.fmt(), + format_date_time(&self.date), + self.scope.to_string(), self.hashed_creq ) } } - -pub(crate) trait DateTimeExt { - // formats using SigV4's format. YYYYMMDD'T'HHMMSS'Z'. - fn fmt_aws(&self) -> String; - // YYYYMMDD - fn parse_aws(s: &str) -> Result, ParseError>; -} - -pub(crate) trait DateExt { - fn fmt_aws(&self) -> String; - - fn parse_aws(s: &str) -> Result, ParseError>; -} - -impl DateExt for Date { - fn fmt_aws(&self) -> String { - self.format("%Y%m%d").to_string() - } - fn parse_aws(s: &str) -> Result, ParseError> { - let date = NaiveDate::parse_from_str(s, "%Y%m%d")?; - Ok(Date::::from_utc(date, Utc)) - } -} - -impl DateTimeExt for DateTime { - fn fmt_aws(&self) -> String { - self.format(DATE_FORMAT).to_string() - } - - fn parse_aws(s: &str) -> Result, ParseError> { - let date = NaiveDateTime::parse_from_str(s, DATE_FORMAT)?; - Ok(DateTime::::from_utc(date, Utc)) - } -} diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe2475e1a34d6d1b8f5c6ffa40dd85e809bc3a12 --- /dev/null +++ b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs @@ -0,0 +1,588 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Utilities to sign HTTP requests. + +use crate::http_request::canonical_request::{CanonicalRequest, StringToSign}; +use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string}; +use crate::SigningOutput; +use http::header::{HeaderName, HeaderValue}; +use std::error::Error as StdError; +use std::{iter, str}; + +mod canonical_request; + +const HMAC_256: &str = "AWS4-HMAC-SHA256"; +const X_AMZ_SECURITY_TOKEN: &str = "x-amz-security-token"; +const X_AMZ_DATE: &str = "x-amz-date"; +const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256"; + +pub type Error = Box; + +/// Signs the given `request` with the signing params. +/// This will directly add the signature headers to the request. +pub fn sign<'a, B>( + request: &'a mut http::Request, + params: &'a SigningParams<'a>, +) -> Result, Error> +where + B: AsRef<[u8]>, +{ + let signable_body = SignableBody::Bytes(request.body().as_ref()); + let (signing_headers, signature) = + calculate_signing_headers(&request, signable_body, params)?.into_parts(); + for (header_name, header_value) in signing_headers { + request + .headers_mut() + .append(HeaderName::from_static(header_name), header_value); + } + + Ok(SigningOutput::new((), signature)) +} + +pub type SigningParams<'a> = super::SigningParams<'a, SigningSettings>; + +#[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] +#[derive(Debug, Eq, PartialEq)] +pub enum UriEncoding { + /// Re-encode the resulting URL (eg. %30 becomes `%2530) + Double, + + /// Take the resulting URL as-is + Single, +} + +impl Default for SigningSettings { + fn default() -> Self { + Self { + uri_encoding: UriEncoding::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), +} + +/// Calculates the signature headers that need to get added to the given `request`. +/// +/// `request` MUST NOT contain any of the following headers: +/// - x-amz-date +/// - x-amz-content-sha-256 +/// - x-amz-security-token +pub fn calculate_signing_headers<'a, B>( + request: &'a http::Request, + body: SignableBody, + params: &'a SigningParams<'a>, +) -> Result>, Error> { + // Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html. + let SigningParams { + access_key, + secret_key, + security_token, + region, + service_name, + date_time, + settings, + } = params; + let (creq, extra_headers) = + CanonicalRequest::from(request, body, settings, *date_time, *security_token)?; + + // Step 2: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-string-to-sign.html. + let encoded_creq = &sha256_hex_string(creq.to_string().as_bytes()); + let sts = StringToSign::new(*date_time, region, service_name, encoded_creq); + + // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html + let signing_key = generate_signing_key(secret_key, date_time.date(), region, service_name); + let signature = calculate_signature(signing_key, &sts.to_string().as_bytes()); + + // Step 4: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-add-signature-to-request.html + 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(SigningOutput::new( + auth.chain(date).chain(iter::from_fn(move || { + security_token.take().or_else(|| content.take()) + })), + signature, + )) +} + +// add signature to authorization header +// Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature +fn build_authorization_header( + access_key: &str, + creq: &CanonicalRequest, + sts: StringToSign, + signature: &str, +) -> String { + format!( + "{} Credential={}/{}, SignedHeaders={}, Signature={}", + HMAC_256, + access_key, + sts.scope.to_string(), + creq.signed_headers, + signature + ) +} + +#[cfg(test)] +mod tests { + use super::{ + build_authorization_header, Error, PayloadChecksumKind, SignableBody, SigningSettings, + }; + use crate::date_fmt::{format_date_time, parse_date_time}; + use crate::http_request::canonical_request::{CanonicalRequest, Scope, StringToSign}; + use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string}; + use http::{HeaderValue, Method, Request, Uri, Version}; + use pretty_assertions::assert_eq; + use std::fs; + use std::{convert::TryFrom, str::FromStr}; + + macro_rules! assert_req_eq { + ($a:tt, $b:tt) => { + assert_eq!(format!("{:?}", $a), format!("{:?}", $b)) + }; + } + + macro_rules! read { + (req: $case:tt) => { + fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.req", $case, $case))? + // this replacement is necessary for tests to pass on Windows, as reading the + // sigv4 snapshots from the file system results in CRLF line endings being inserted. + .replace("\r\n", "\n") + }; + + (creq: $case:tt) => { + fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.creq", $case, $case))? + .replace("\r\n", "\n") + }; + + (sreq: $case:tt) => { + fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.sreq", $case, $case))? + .replace("\r\n", "\n") + }; + + (sts: $case:tt) => { + fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.sts", $case, $case))? + .replace("\r\n", "\n") + }; + + (authz: $case:tt) => { + fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.authz", $case, $case))? + .replace("\r\n", "\n") + }; + } + + #[test] + fn read_request() -> Result<(), Error> { + //file-name.req—the web request to be signed. + //file-name.creq—the resulting canonical request. + //file-name.sts—the resulting string to sign. + //file-name.authz—the Authorization header. + //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 req = parse_request(s.as_bytes())?; + let date = parse_date_time("20150830T123600Z").unwrap(); + 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 encoded_creq = &sha256_hex_string(creq.to_string().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 + 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.to_string().as_bytes()); + 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 x_azn_date = format_date_time(&date); + + 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("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); + + Ok(()) + } + + #[test] + fn test_set_xamz_sha_256() -> Result<(), Error> { + let s = read!(req: "get-vanilla-query-order-key-case"); + let req = parse_request(s.as_bytes())?; + let date = parse_date_time("20150830T123600Z").unwrap(); + let mut signing_settings = SigningSettings { + payload_checksum_kind: PayloadChecksumKind::XAmzSha256, + ..Default::default() + }; + 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.to_string(), + "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.to_string(), "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 = parse_date_time("20150830T123600Z").unwrap(); + let signing_settings = SigningSettings { + payload_checksum_kind: PayloadChecksumKind::XAmzSha256, + ..Default::default() + }; + 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 = parse_date_time("20150830T123600Z").unwrap(); + let signing_settings = SigningSettings { + payload_checksum_kind: PayloadChecksumKind::XAmzSha256, + ..Default::default() + }; + 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 = parse_date_time("20150830T123600Z").unwrap(); + let creq = CanonicalRequest::from( + &req, + SignableBody::Bytes(req.body()), + &SigningSettings::default(), + date, + None, + )? + .0; + + let encoded_creq = &sha256_hex_string(creq.to_string().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.to_string().as_bytes()); + let expected_header = read!(authz: "get-vanilla-query-order-key-case"); + let header = build_authorization_header("AKIDEXAMPLE", &creq, sts, &signature); + assert_eq!(expected_header, header); + + Ok(()) + } + + #[test] + fn test_generate_scope() -> Result<(), Error> { + let expected = "20150830/us-east-1/iam/aws4_request\n"; + let date = parse_date_time("20150830T123600Z")?; + let scope = Scope { + date: date.date(), + region: "us-east-1", + service: "iam", + }; + assert_eq!(format!("{}\n", scope.to_string()), expected); + + Ok(()) + } + + #[test] + fn test_parse() -> Result<(), Error> { + let buf = read!(req: "post-header-key-case"); + parse_request(buf.as_bytes())?; + Ok(()) + } + + #[test] + fn test_read_query_params() -> Result<(), Error> { + let buf = read!(req: "get-vanilla-query-order-key-case"); + parse_request(buf.as_bytes()).unwrap(); + Ok(()) + } + + #[test] + fn test_parse_headers() { + let buf = b"Host:example.amazonaws.com\nX-Amz-Date:20150830T123600Z\n\nblah blah"; + let mut headers = [httparse::EMPTY_HEADER; 4]; + assert_eq!( + httparse::parse_headers(buf, &mut headers), + Ok(httparse::Status::Complete(( + 56, + &[ + httparse::Header { + name: "Host", + value: b"example.amazonaws.com", + }, + httparse::Header { + name: "X-Amz-Date", + value: b"20150830T123600Z", + } + ][..] + ))) + ); + } + + #[test] + fn sign_payload_empty_string() { + let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + let actual = sha256_hex_string(&[]); + assert_eq!(expected, actual); + } + + #[test] + fn test_string_to_sign() -> Result<(), Error> { + let date = parse_date_time("20150830T123600Z")?; + let creq = read!(creq: "get-vanilla-query-order-key-case"); + let expected_sts = read!(sts: "get-vanilla-query-order-key-case"); + let encoded = sha256_hex_string(creq.as_bytes()); + + let actual = StringToSign::new(date, "us-east-1", "service", &encoded); + assert_eq!(expected_sts, actual.to_string()); + + Ok(()) + } + + #[test] + fn test_signature_calculation() -> Result<(), Error> { + let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; + let creq = + std::fs::read_to_string("./aws-sig-v4-test-suite/iam.creq")?.replace("\r\n", "\n"); + let date = parse_date_time("20150830T123600Z")?; + + let derived_key = generate_signing_key(secret, date.date(), "us-east-1", "iam"); + let signature = calculate_signature(derived_key, creq.as_bytes()); + + let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"; + assert_eq!(expected, &signature); + + Ok(()) + } + + #[test] + fn parse_signed_request() -> Result<(), Error> { + let req = read!(sreq: "post-header-key-case"); + let _: Request<_> = parse_request(req.as_bytes())?; + Ok(()) + } + + #[test] + fn read_sts() -> Result<(), Error> { + let sts = read!(sts: "get-vanilla-query-order-key-case"); + let _ = StringToSign::try_from(sts.as_ref())?; + Ok(()) + } + + #[test] + fn test_digest_of_canonical_request() -> Result<(), Error> { + let creq = read!(creq: "get-vanilla-query-order-key-case"); + let actual = sha256_hex_string(creq.as_bytes()); + let expected = "816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0"; + + assert_eq!(expected, actual); + Ok(()) + } + + #[test] + fn test_double_url_encode() -> Result<(), Error> { + let s = read!(req: "double-url-encode"); + let req = parse_request(s.as_bytes())?; + let date = parse_date_time("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"); + 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 = parse_date_time("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); + let _ = req.parse(s).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 builder = Request::builder(); + let builder = builder.version(version); + let mut builder = builder.method(method); + if let Some(path) = req.path { + builder = builder.uri(Uri::from_str(path)?); + } + for header in req.headers { + let name = header.name.to_lowercase(); + if !name.is_empty() { + builder = builder.header(&name, header.value); + } + } + + let req = builder.body(bytes::Bytes::new())?; + Ok(req) + } +} diff --git a/aws/rust-runtime/aws-sigv4/src/lib.rs b/aws/rust-runtime/aws-sigv4/src/lib.rs index f9b9029100b0d7cdc7320a463fbc627beb9814b0..29ac80071e43f05435a22aefd1784154ee47c273 100644 --- a/aws/rust-runtime/aws-sigv4/src/lib.rs +++ b/aws/rust-runtime/aws-sigv4/src/lib.rs @@ -1,644 +1,63 @@ -use chrono::{DateTime, Utc}; -use http::{ - header::{self, HeaderName}, - HeaderValue, -}; -use serde::{Deserialize, Serialize}; -use std::{iter, str}; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Provides functions for calculating Sigv4 signing keys, signatures, and +//! optional utilities for signing HTTP requests and Event Stream messages. -pub const HMAC_256: &str = "AWS4-HMAC-SHA256"; -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"; +use chrono::{DateTime, Utc}; pub mod sign; -pub mod types; -type Error = Box; +mod date_fmt; -use crate::UriEncoding::Double; -use sign::{calculate_signature, encode_bytes_with_hex, generate_signing_key}; -use std::time::SystemTime; -use types::{AsSigV4, CanonicalRequest, StringToSign}; +#[cfg(feature = "sign-eventstream")] +pub mod event_stream; -pub fn sign( - req: &mut http::Request, - credential: &Credentials, - region: &str, - svc: &str, -) -> Result<(), Error> -where - B: AsRef<[u8]>, -{ - let signable_body = SignableBody::Bytes(req.body().as_ref()); - for (header_name, header_value) in sign_core( - &req, - signable_body, - &Config { - access_key: &credential.access_key, - secret_key: &credential.secret_key, - security_token: credential.security_token.as_deref(), - region, - svc, - date: SystemTime::now(), - settings: Default::default(), - }, - )? { - req.headers_mut() - .append(HeaderName::from_static(header_name), header_value); - } +#[cfg(feature = "sign-http")] +pub mod http_request; - Ok(()) -} - -pub struct Config<'a> { +/// Parameters to use when signing. +pub struct SigningParams<'a, S> { + /// Access Key ID to use. pub access_key: &'a str, + /// Secret access key to use. pub secret_key: &'a str, + /// (Optional) Security token to use. pub security_token: Option<&'a str>, + /// Region to sign for. pub region: &'a str, - pub svc: &'a str, - - pub date: SystemTime, - - pub settings: SigningSettings, -} - -#[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] -#[derive(Debug, Eq, PartialEq)] -pub enum UriEncoding { - /// Re-encode the resulting URL (eg. %30 becomes `%2530) - Double, - - /// Take the resulting URL as-is - Single, -} - -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, - 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, - security_token, - region, - svc, - date, - settings, - } = config; - 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_bytes_with_hex(creq.fmt().as_bytes()); - let sts = StringToSign::new(date, region, svc, encoded_creq); + /// AWS Service Name to sign for. + pub service_name: &'a str, + /// Timestamp to use in the signature (should be `Utc::now()` unless testing). + pub date_time: DateTime, - // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html - let signing_key = generate_signing_key(secret_key, date.date(), region, svc); - 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 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()) - }))) + /// Additional signing settings. These differ between HTTP and Event Stream. + pub settings: S, } -#[derive(Debug, PartialEq, Serialize, Deserialize, Default, Clone)] -pub struct Credentials<'a> { - #[serde(rename = "aws_access_key_id")] - pub access_key: &'a str, - #[serde(rename = "aws_secret_access_key")] - pub secret_key: &'a str, - #[serde(rename = "aws_session_token")] - pub security_token: Option<&'a str>, +/// Container for the signed output and the signature. +pub struct SigningOutput { + output: T, + signature: String, } -impl<'a> Credentials<'a> { - pub fn new(access_key: &'a str, secret_key: &'a str, security_token: Option<&'a str>) -> Self { - Self { - access_key, - secret_key, - security_token, - } - } -} - -#[cfg(test)] -mod tests { - use crate::{ - assert_req_eq, build_authorization_header, read, - sign::{calculate_signature, encode_bytes_with_hex, generate_signing_key}, - types::{AsSigV4, CanonicalRequest, DateExt, DateTimeExt, Scope, StringToSign}, - Error, PayloadChecksumKind, SignableBody, SigningSettings, DATE_FORMAT, - }; - use chrono::{Date, DateTime, NaiveDateTime, Utc}; - use http::{HeaderValue, Method, Request, Uri, Version}; - use pretty_assertions::assert_eq; - use std::fs; - use std::{convert::TryFrom, str::FromStr}; - - #[test] - fn read_request() -> Result<(), Error> { - //file-name.req—the web request to be signed. - //file-name.creq—the resulting canonical request. - //file-name.sts—the resulting string to sign. - //file-name.authz—the Authorization header. - //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 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 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 - 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 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 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("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); - - Ok(()) - } - - #[test] - fn test_set_xamz_sha_256() -> 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::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 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); - assert_eq!(expected_header, header); - - Ok(()) - } - - #[test] - fn test_generate_scope() -> Result<(), Error> { - let expected = "20150830/us-east-1/iam/aws4_request\n"; - let date = DateTime::parse_aws("20150830T123600Z")?; - let scope = Scope { - date: date.date(), - region: "us-east-1", - service: "iam", - }; - assert_eq!(format!("{}\n", scope.fmt()), expected); - - Ok(()) - } - - #[test] - fn test_parse() -> Result<(), Error> { - let buf = read!(req: "post-header-key-case"); - parse_request(buf.as_bytes())?; - Ok(()) - } - - #[test] - fn test_read_query_params() -> Result<(), Error> { - let buf = read!(req: "get-vanilla-query-order-key-case"); - parse_request(buf.as_bytes()).unwrap(); - Ok(()) - } - - #[test] - fn test_parse_headers() { - let buf = b"Host:example.amazonaws.com\nX-Amz-Date:20150830T123600Z\n\nblah blah"; - let mut headers = [httparse::EMPTY_HEADER; 4]; - assert_eq!( - httparse::parse_headers(buf, &mut headers), - Ok(httparse::Status::Complete(( - 56, - &[ - httparse::Header { - name: "Host", - value: b"example.amazonaws.com", - }, - httparse::Header { - name: "X-Amz-Date", - value: b"20150830T123600Z", - } - ][..] - ))) - ); - } - - #[test] - fn sign_payload_empty_string() { - let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - let actual = encode_bytes_with_hex(&[]); - assert_eq!(expected, actual); - } - - #[test] - fn datetime_format() -> Result<(), Error> { - let date = DateTime::parse_aws("20150830T123600Z")?; - let expected = "20150830T123600Z"; - assert_eq!(expected, date.fmt_aws()); - - Ok(()) - } - - #[test] - fn date_format() -> Result<(), Error> { - let date = Date::parse_aws("20150830")?; - let expected = "20150830"; - assert_eq!(expected, date.fmt_aws()); - - Ok(()) - } - - #[test] - fn test_string_to_sign() -> Result<(), Error> { - 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_bytes_with_hex(creq.as_bytes()); - - let actual = StringToSign::new(date, "us-east-1", "service", &encoded); - assert_eq!(expected_sts, actual.fmt()); - - Ok(()) - } - - #[test] - fn test_signature_calculation() -> Result<(), Error> { - let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; - 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"); - let signature = calculate_signature(derived_key, creq.as_bytes()); - - let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"; - assert_eq!(expected, &signature); - - Ok(()) +impl SigningOutput { + pub fn new(output: T, signature: String) -> Self { + Self { output, signature } } - #[test] - fn parse_signed_request() -> Result<(), Error> { - let req = read!(sreq: "post-header-key-case"); - let _: Request<_> = parse_request(req.as_bytes())?; - Ok(()) + pub fn output(&self) -> &T { + &self.output } - #[test] - fn read_sts() -> Result<(), Error> { - let sts = read!(sts: "get-vanilla-query-order-key-case"); - let _ = StringToSign::try_from(sts.as_ref())?; - Ok(()) - } - - #[test] - fn test_digest_of_canonical_request() -> Result<(), Error> { - let creq = read!(creq: "get-vanilla-query-order-key-case"); - let actual = encode_bytes_with_hex(creq.as_bytes()); - let expected = "816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0"; - - assert_eq!(expected, actual); - Ok(()) + pub fn signature(&self) -> &str { + &self.signature } - #[test] - fn test_double_url_encode() -> Result<(), Error> { - let s = read!(req: "double-url-encode"); - let req = parse_request(s.as_bytes())?; - 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"); - 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); - let _ = req.parse(s).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 builder = Request::builder(); - let builder = builder.version(version); - let mut builder = builder.method(method); - if let Some(path) = req.path { - builder = builder.uri(Uri::from_str(path)?); - } - for header in req.headers { - let name = header.name.to_lowercase(); - if !name.is_empty() { - builder = builder.header(&name, header.value); - } - } - - let req = builder.body(bytes::Bytes::new())?; - Ok(req) + pub fn into_parts(self) -> (T, String) { + (self.output, self.signature) } } - -// add signature to authorization header -// Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature -fn build_authorization_header( - access_key: &str, - creq: &CanonicalRequest, - sts: StringToSign, - signature: &str, -) -> String { - format!( - "{} Credential={}/{}, SignedHeaders={}, Signature={}", - HMAC_256, - access_key, - sts.scope.fmt(), - creq.signed_headers, - signature - ) -} - -#[macro_export] -macro_rules! assert_req_eq { - ($a:tt, $b:tt) => { - assert_eq!(format!("{:?}", $a), format!("{:?}", $b)) - }; -} - -#[macro_export] -macro_rules! read { - (req: $case:tt) => { - fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.req", $case, $case))? - // this replacement is necessary for tests to pass on Windows, as reading the - // sigv4 snapshots from the file system results in CRLF line endings being inserted. - .replace("\r\n", "\n") - }; - - (creq: $case:tt) => { - fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.creq", $case, $case))? - .replace("\r\n", "\n") - }; - - (sreq: $case:tt) => { - fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.sreq", $case, $case))? - .replace("\r\n", "\n") - }; - - (sts: $case:tt) => { - fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.sts", $case, $case))? - .replace("\r\n", "\n") - }; - - (authz: $case:tt) => { - fs::read_to_string(format!("./aws-sig-v4-test-suite/{}/{}.authz", $case, $case))? - .replace("\r\n", "\n") - }; -} diff --git a/aws/rust-runtime/aws-sigv4/src/sign.rs b/aws/rust-runtime/aws-sigv4/src/sign.rs index 5f4157824db36213debaf0850b5fc2298c644bc4..5984683147daa98d43cfee66f4caa42fb3d01b9a 100644 --- a/aws/rust-runtime/aws-sigv4/src/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/sign.rs @@ -1,48 +1,47 @@ -use crate::types::DateExt; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +//! Functions to create signing keys and calculate signatures. + +use crate::date_fmt::format_date; use chrono::{Date, Utc}; use ring::{ - digest::{self, Digest}, + digest::{self}, hmac::{self, Key, Tag}, }; -// HMAC -pub fn encode(s: String) -> Vec { - let calculated = digest::digest(&digest::SHA256, s.as_bytes()); - calculated.as_ref().to_vec() -} - /// HashedPayload = Lowercase(HexEncode(Hash(requestPayload))) -pub fn encode_bytes_with_hex(bytes: B) -> String -where - B: AsRef<[u8]>, -{ - let digest: Digest = digest::digest(&digest::SHA256, bytes.as_ref()); - // no need to lower-case as in step six, as hex::encode - // already returns a lower-cased string. - hex::encode(digest) +#[allow(dead_code)] // Unused when compiling without certain features +pub(crate) fn sha256_hex_string(bytes: impl AsRef<[u8]>) -> String { + // hex::encode returns a lowercase string + hex::encode(digest::digest(&digest::SHA256, bytes.as_ref())) } +/// Calculates a Sigv4 signature pub fn calculate_signature(signing_key: Tag, string_to_sign: &[u8]) -> String { let s_key = Key::new(hmac::HMAC_SHA256, signing_key.as_ref()); let tag = hmac::sign(&s_key, string_to_sign); - hex::encode(tag) } -// kSecret = your secret access key -// kDate = HMAC("AWS4" + kSecret, Date) -// kRegion = HMAC(kDate, Region) -// kService = HMAC(kRegion, Service) -// kSigning = HMAC(kService, "aws4_request") +/// Generates a signing key for Sigv4 pub fn generate_signing_key( secret: &str, date: Date, region: &str, service: &str, ) -> hmac::Tag { + // kSecret = your secret access key + // kDate = HMAC("AWS4" + kSecret, Date) + // kRegion = HMAC(kDate, Region) + // kService = HMAC(kRegion, Service) + // kSigning = HMAC(kService, "aws4_request") + let secret = format!("AWS4{}", secret); let secret = hmac::Key::new(hmac::HMAC_SHA256, &secret.as_bytes()); - let tag = hmac::sign(&secret, date.fmt_aws().as_bytes()); + let tag = hmac::sign(&secret, format_date(&date).as_bytes()); // sign region let key = hmac::Key::new(hmac::HMAC_SHA256, tag.as_ref());