1use crate::date_time::{format_date, format_date_time};
7use crate::http_request::error::CanonicalRequestError;
8use crate::http_request::settings::SessionTokenMode;
9use crate::http_request::settings::UriPathNormalizationMode;
10use crate::http_request::sign::SignableRequest;
11use crate::http_request::uri_path_normalization::normalize_uri_path;
12use crate::http_request::url_escape::percent_encode_path;
13use crate::http_request::{PayloadChecksumKind, SignableBody, SignatureLocation, SigningParams};
14use crate::http_request::{PercentEncodingMode, SigningSettings};
15use crate::sign::v4::sha256_hex_string;
16use crate::SignatureVersion;
17use aws_smithy_http::query_writer::QueryWriter;
18use http0::header::{AsHeaderName, HeaderName, HOST};
19use http0::{HeaderMap, HeaderValue, Uri};
20use std::borrow::Cow;
21use std::cmp::Ordering;
22use std::convert::TryFrom;
23use std::fmt;
24use std::str::FromStr;
25use std::time::SystemTime;
26
27#[cfg(feature = "sigv4a")]
28pub(crate) mod sigv4a;
29
30pub(crate) mod header {
31 pub(crate) const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256";
32 pub(crate) const X_AMZ_DATE: &str = "x-amz-date";
33 pub(crate) const X_AMZ_SECURITY_TOKEN: &str = "x-amz-security-token";
34 pub(crate) const X_AMZ_USER_AGENT: &str = "x-amz-user-agent";
35}
36
37pub(crate) mod param {
38 pub(crate) const X_AMZ_ALGORITHM: &str = "X-Amz-Algorithm";
39 pub(crate) const X_AMZ_CREDENTIAL: &str = "X-Amz-Credential";
40 pub(crate) const X_AMZ_DATE: &str = "X-Amz-Date";
41 pub(crate) const X_AMZ_EXPIRES: &str = "X-Amz-Expires";
42 pub(crate) const X_AMZ_SECURITY_TOKEN: &str = "X-Amz-Security-Token";
43 pub(crate) const X_AMZ_SIGNED_HEADERS: &str = "X-Amz-SignedHeaders";
44 pub(crate) const X_AMZ_SIGNATURE: &str = "X-Amz-Signature";
45}
46
47pub(crate) const HMAC_256: &str = "AWS4-HMAC-SHA256";
48
49const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
50const STREAMING_UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
51
52#[derive(Debug, PartialEq)]
53pub(crate) struct HeaderValues<'a> {
54 pub(crate) content_sha256: Cow<'a, str>,
55 pub(crate) date_time: String,
56 pub(crate) security_token: Option<&'a str>,
57 pub(crate) signed_headers: SignedHeaders,
58 #[cfg(feature = "sigv4a")]
59 pub(crate) region_set: Option<&'a str>,
60}
61
62#[derive(Debug, PartialEq)]
63pub(crate) struct QueryParamValues<'a> {
64 pub(crate) algorithm: &'static str,
65 pub(crate) content_sha256: Cow<'a, str>,
66 pub(crate) credential: String,
67 pub(crate) date_time: String,
68 pub(crate) expires: String,
69 pub(crate) security_token: Option<&'a str>,
70 pub(crate) signed_headers: SignedHeaders,
71 #[cfg(feature = "sigv4a")]
72 pub(crate) region_set: Option<&'a str>,
73}
74
75#[derive(Debug, PartialEq)]
76pub(crate) enum SignatureValues<'a> {
77 Headers(HeaderValues<'a>),
78 QueryParams(QueryParamValues<'a>),
79}
80
81impl<'a> SignatureValues<'a> {
82 pub(crate) fn signed_headers(&self) -> &SignedHeaders {
83 match self {
84 SignatureValues::Headers(values) => &values.signed_headers,
85 SignatureValues::QueryParams(values) => &values.signed_headers,
86 }
87 }
88
89 fn content_sha256(&self) -> &str {
90 match self {
91 SignatureValues::Headers(values) => &values.content_sha256,
92 SignatureValues::QueryParams(values) => &values.content_sha256,
93 }
94 }
95
96 pub(crate) fn as_headers(&self) -> Option<&HeaderValues<'_>> {
97 match self {
98 SignatureValues::Headers(values) => Some(values),
99 _ => None,
100 }
101 }
102
103 pub(crate) fn into_query_params(self) -> Result<QueryParamValues<'a>, Self> {
104 match self {
105 SignatureValues::QueryParams(values) => Ok(values),
106 _ => Err(self),
107 }
108 }
109}
110
111#[derive(Debug, PartialEq)]
112pub(crate) struct CanonicalRequest<'a> {
113 pub(crate) method: &'a str,
114 pub(crate) path: Cow<'a, str>,
115 pub(crate) params: Option<String>,
116 pub(crate) headers: HeaderMap,
117 pub(crate) values: SignatureValues<'a>,
118}
119
120impl<'a> CanonicalRequest<'a> {
121 pub(crate) fn from<'b>(
138 req: &'b SignableRequest<'b>,
139 params: &'b SigningParams<'b>,
140 ) -> Result<CanonicalRequest<'b>, CanonicalRequestError> {
141 let creds = params
142 .credentials()
143 .map_err(|_| CanonicalRequestError::unsupported_identity_type())?;
144 let path = req.uri().path();
147 let path = match params.settings().uri_path_normalization_mode {
148 UriPathNormalizationMode::Enabled => normalize_uri_path(path),
149 UriPathNormalizationMode::Disabled => Cow::Borrowed(path),
150 };
151 let path = match params.settings().percent_encoding_mode {
152 PercentEncodingMode::Double => Cow::Owned(percent_encode_path(&path)),
154 PercentEncodingMode::Single => path,
155 };
156 let payload_hash = Self::payload_hash(req.body());
157
158 let date_time = format_date_time(*params.time());
159 let (signed_headers, canonical_headers) =
160 Self::headers(req, params, &payload_hash, &date_time)?;
161 let signed_headers = SignedHeaders::new(signed_headers);
162
163 let security_token = match params.settings().session_token_mode {
164 SessionTokenMode::Include => creds.session_token(),
165 SessionTokenMode::Exclude => None,
166 };
167
168 let values = match params.settings().signature_location {
169 SignatureLocation::Headers => SignatureValues::Headers(HeaderValues {
170 content_sha256: payload_hash,
171 date_time,
172 security_token,
173 signed_headers,
174 #[cfg(feature = "sigv4a")]
175 region_set: params.region_set(),
176 }),
177 SignatureLocation::QueryParams => {
178 let credential = match params {
179 SigningParams::V4(params) => {
180 format!(
181 "{}/{}/{}/{}/aws4_request",
182 creds.access_key_id(),
183 format_date(params.time),
184 params.region,
185 params.name,
186 )
187 }
188 #[cfg(feature = "sigv4a")]
189 SigningParams::V4a(params) => {
190 format!(
191 "{}/{}/{}/aws4_request",
192 creds.access_key_id(),
193 format_date(params.time),
194 params.name,
195 )
196 }
197 };
198
199 SignatureValues::QueryParams(QueryParamValues {
200 algorithm: params.algorithm(),
201 content_sha256: payload_hash,
202 credential,
203 date_time,
204 expires: params
205 .settings()
206 .expires_in
207 .expect("presigning requires expires_in")
208 .as_secs()
209 .to_string(),
210 security_token,
211 signed_headers,
212 #[cfg(feature = "sigv4a")]
213 region_set: params.region_set(),
214 })
215 }
216 };
217
218 let creq = CanonicalRequest {
219 method: req.method(),
220 path,
221 params: Self::params(req.uri(), &values, params.settings()),
222 headers: canonical_headers,
223 values,
224 };
225 Ok(creq)
226 }
227
228 fn headers(
229 req: &SignableRequest<'_>,
230 params: &SigningParams<'_>,
231 payload_hash: &str,
232 date_time: &str,
233 ) -> Result<(Vec<CanonicalHeaderName>, HeaderMap), CanonicalRequestError> {
234 let mut canonical_headers = HeaderMap::with_capacity(req.headers().len());
242 for (name, value) in req.headers().iter() {
243 canonical_headers.append(
246 HeaderName::from_str(&name.to_lowercase())?,
247 normalize_header_value(value)?,
248 );
249 }
250
251 Self::insert_host_header(&mut canonical_headers, req.uri());
252
253 let token_header_name = params
254 .settings()
255 .session_token_name_override
256 .unwrap_or(header::X_AMZ_SECURITY_TOKEN);
257
258 if params.settings().signature_location == SignatureLocation::Headers {
259 let creds = params
260 .credentials()
261 .map_err(|_| CanonicalRequestError::unsupported_identity_type())?;
262 Self::insert_date_header(&mut canonical_headers, date_time);
263
264 if let Some(security_token) = creds.session_token() {
265 let mut sec_header = HeaderValue::from_str(security_token)?;
266 sec_header.set_sensitive(true);
267 canonical_headers.insert(token_header_name, sec_header);
268 }
269
270 if params.settings().payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
271 let header = HeaderValue::from_str(payload_hash)?;
272 canonical_headers.insert(header::X_AMZ_CONTENT_SHA_256, header);
273 }
274
275 #[cfg(feature = "sigv4a")]
276 if let Some(region_set) = params.region_set() {
277 let header = HeaderValue::from_str(region_set)?;
278 canonical_headers.insert(sigv4a::header::X_AMZ_REGION_SET, header);
279 }
280 }
281
282 let mut signed_headers = Vec::with_capacity(canonical_headers.len());
283 for name in canonical_headers.keys() {
284 if let Some(excluded_headers) = params.settings().excluded_headers.as_ref() {
285 if excluded_headers.iter().any(|it| name.as_str() == it) {
286 continue;
287 }
288 }
289
290 if params.settings().session_token_mode == SessionTokenMode::Exclude
291 && name == HeaderName::from_static(token_header_name)
292 {
293 continue;
294 }
295
296 if params.settings().signature_location == SignatureLocation::QueryParams {
297 if name == HeaderName::from_static(header::X_AMZ_USER_AGENT) {
299 continue;
300 }
301 }
302 signed_headers.push(CanonicalHeaderName(name.clone()));
303 }
304
305 Ok((signed_headers, canonical_headers))
306 }
307
308 fn payload_hash<'b>(body: &'b SignableBody<'b>) -> Cow<'b, str> {
309 match body {
319 SignableBody::Bytes(data) => Cow::Owned(sha256_hex_string(data)),
320 SignableBody::Precomputed(digest) => Cow::Borrowed(digest.as_str()),
321 SignableBody::UnsignedPayload => Cow::Borrowed(UNSIGNED_PAYLOAD),
322 SignableBody::StreamingUnsignedPayloadTrailer => {
323 Cow::Borrowed(STREAMING_UNSIGNED_PAYLOAD_TRAILER)
324 }
325 }
326 }
327
328 fn params(
329 uri: &Uri,
330 values: &SignatureValues<'_>,
331 settings: &SigningSettings,
332 ) -> Option<String> {
333 let mut params: Vec<(Cow<'_, str>, Cow<'_, str>)> =
334 form_urlencoded::parse(uri.query().unwrap_or_default().as_bytes()).collect();
335 fn add_param<'a>(params: &mut Vec<(Cow<'a, str>, Cow<'a, str>)>, k: &'a str, v: &'a str) {
336 params.push((Cow::Borrowed(k), Cow::Borrowed(v)));
337 }
338
339 if let SignatureValues::QueryParams(values) = values {
340 add_param(&mut params, param::X_AMZ_DATE, &values.date_time);
341 add_param(&mut params, param::X_AMZ_EXPIRES, &values.expires);
342
343 #[cfg(feature = "sigv4a")]
344 if let Some(regions) = values.region_set {
345 add_param(&mut params, sigv4a::param::X_AMZ_REGION_SET, regions);
346 }
347
348 add_param(&mut params, param::X_AMZ_ALGORITHM, values.algorithm);
349 add_param(&mut params, param::X_AMZ_CREDENTIAL, &values.credential);
350 add_param(
351 &mut params,
352 param::X_AMZ_SIGNED_HEADERS,
353 values.signed_headers.as_str(),
354 );
355
356 if let Some(security_token) = values.security_token {
357 add_param(
358 &mut params,
359 settings
360 .session_token_name_override
361 .unwrap_or(param::X_AMZ_SECURITY_TOKEN),
362 security_token,
363 );
364 }
365 }
366 params.sort();
368
369 let mut query = QueryWriter::new(uri);
370 query.clear_params();
371 for (key, value) in params {
372 query.insert(&key, &value);
373 }
374
375 let query = query.build_query();
376 if query.is_empty() {
377 None
378 } else {
379 Some(query)
380 }
381 }
382
383 fn insert_host_header(
384 canonical_headers: &mut HeaderMap<HeaderValue>,
385 uri: &Uri,
386 ) -> HeaderValue {
387 match canonical_headers.get(&HOST) {
388 Some(header) => header.clone(),
389 None => {
390 let authority = uri
391 .authority()
392 .expect("request uri authority must be set for signing");
393 let header = HeaderValue::try_from(authority.as_str())
394 .expect("endpoint must contain valid header characters");
395 canonical_headers.insert(HOST, header.clone());
396 header
397 }
398 }
399 }
400
401 fn insert_date_header(
402 canonical_headers: &mut HeaderMap<HeaderValue>,
403 date_time: &str,
404 ) -> HeaderValue {
405 let x_amz_date = HeaderName::from_static(header::X_AMZ_DATE);
406 let date_header = HeaderValue::try_from(date_time).expect("date is valid header value");
407 canonical_headers.insert(x_amz_date, date_header.clone());
408 date_header
409 }
410
411 fn header_values_for(&self, key: impl AsHeaderName) -> String {
412 let values: Vec<&str> = self
413 .headers
414 .get_all(key)
415 .into_iter()
416 .map(|value| {
417 std::str::from_utf8(value.as_bytes())
418 .expect("SDK request header values are valid UTF-8")
419 })
420 .collect();
421 values.join(",")
422 }
423}
424
425impl<'a> fmt::Display for CanonicalRequest<'a> {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 writeln!(f, "{}", self.method)?;
428 writeln!(f, "{}", self.path)?;
429 writeln!(f, "{}", self.params.as_deref().unwrap_or(""))?;
430 for header in &self.values.signed_headers().headers {
432 write!(f, "{}:", header.0.as_str())?;
433 writeln!(f, "{}", self.header_values_for(&header.0))?;
434 }
435 writeln!(f)?;
436 writeln!(f, "{}", self.values.signed_headers().as_str())?;
438 write!(f, "{}", self.values.content_sha256())?;
439 Ok(())
440 }
441}
442
443fn trim_all(text: &str) -> Cow<'_, str> {
448 let text = text.trim_matches(' ');
449 let requires_filter = text
450 .chars()
451 .zip(text.chars().skip(1))
452 .any(|(a, b)| a == ' ' && b == ' ');
453 if !requires_filter {
454 Cow::Borrowed(text)
455 } else {
456 Cow::Owned(
459 text.chars()
460 .zip(text.chars().skip(1).chain(std::iter::once('!')))
462 .filter(|(a, b)| *a != ' ' || *b != ' ')
463 .map(|(a, _)| a)
464 .collect(),
465 )
466 }
467}
468
469fn normalize_header_value(header_value: &str) -> Result<HeaderValue, CanonicalRequestError> {
472 let trimmed_value = trim_all(header_value);
473 HeaderValue::from_str(&trimmed_value).map_err(CanonicalRequestError::from)
474}
475
476#[derive(Debug, PartialEq, Default)]
477pub(crate) struct SignedHeaders {
478 headers: Vec<CanonicalHeaderName>,
479 formatted: String,
480}
481
482impl SignedHeaders {
483 fn new(mut headers: Vec<CanonicalHeaderName>) -> Self {
484 headers.sort();
485 let formatted = Self::fmt(&headers);
486 SignedHeaders { headers, formatted }
487 }
488
489 fn fmt(headers: &[CanonicalHeaderName]) -> String {
490 let mut value = String::new();
491 let mut iter = headers.iter().peekable();
492 while let Some(next) = iter.next() {
493 value += next.0.as_str();
494 if iter.peek().is_some() {
495 value.push(';');
496 }
497 }
498 value
499 }
500
501 pub(crate) fn as_str(&self) -> &str {
502 &self.formatted
503 }
504}
505
506impl fmt::Display for SignedHeaders {
507 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508 write!(f, "{}", self.formatted)
509 }
510}
511
512#[derive(Debug, PartialEq, Eq, Clone)]
513struct CanonicalHeaderName(HeaderName);
514
515impl PartialOrd for CanonicalHeaderName {
516 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
517 Some(self.cmp(other))
518 }
519}
520
521impl Ord for CanonicalHeaderName {
522 fn cmp(&self, other: &Self) -> Ordering {
523 self.0.as_str().cmp(other.0.as_str())
524 }
525}
526
527#[derive(PartialEq, Debug, Clone)]
528pub(crate) struct SigningScope<'a> {
529 pub(crate) time: SystemTime,
530 pub(crate) region: &'a str,
531 pub(crate) service: &'a str,
532}
533
534impl<'a> SigningScope<'a> {
535 pub(crate) fn v4a_display(&self) -> String {
536 format!("{}/{}/aws4_request", format_date(self.time), self.service)
537 }
538}
539
540impl<'a> fmt::Display for SigningScope<'a> {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 write!(
543 f,
544 "{}/{}/{}/aws4_request",
545 format_date(self.time),
546 self.region,
547 self.service
548 )
549 }
550}
551
552#[derive(PartialEq, Debug, Clone)]
553pub(crate) struct StringToSign<'a> {
554 pub(crate) algorithm: &'static str,
555 pub(crate) scope: SigningScope<'a>,
556 pub(crate) time: SystemTime,
557 pub(crate) region: &'a str,
558 pub(crate) service: &'a str,
559 pub(crate) hashed_creq: &'a str,
560 signature_version: SignatureVersion,
561}
562
563impl<'a> StringToSign<'a> {
564 pub(crate) fn new_v4(
565 time: SystemTime,
566 region: &'a str,
567 service: &'a str,
568 hashed_creq: &'a str,
569 ) -> Self {
570 let scope = SigningScope {
571 time,
572 region,
573 service,
574 };
575 Self {
576 algorithm: HMAC_256,
577 scope,
578 time,
579 region,
580 service,
581 hashed_creq,
582 signature_version: SignatureVersion::V4,
583 }
584 }
585
586 #[cfg(feature = "sigv4a")]
587 pub(crate) fn new_v4a(
588 time: SystemTime,
589 region_set: &'a str,
590 service: &'a str,
591 hashed_creq: &'a str,
592 ) -> Self {
593 use crate::sign::v4a::ECDSA_256;
594
595 let scope = SigningScope {
596 time,
597 region: region_set,
598 service,
599 };
600 Self {
601 algorithm: ECDSA_256,
602 scope,
603 time,
604 region: region_set,
605 service,
606 hashed_creq,
607 signature_version: SignatureVersion::V4a,
608 }
609 }
610}
611
612impl<'a> fmt::Display for StringToSign<'a> {
613 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614 write!(
615 f,
616 "{}\n{}\n{}\n{}",
617 self.algorithm,
618 format_date_time(self.time),
619 match self.signature_version {
620 SignatureVersion::V4 => self.scope.to_string(),
621 SignatureVersion::V4a => self.scope.v4a_display(),
622 },
623 self.hashed_creq
624 )
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use crate::date_time::test_parsers::parse_date_time;
631 use crate::http_request::canonical_request::{
632 normalize_header_value, trim_all, CanonicalRequest, SigningScope, StringToSign,
633 };
634 use crate::http_request::test;
635 use crate::http_request::{
636 PayloadChecksumKind, SessionTokenMode, SignableBody, SignableRequest, SignatureLocation,
637 SigningParams, SigningSettings,
638 };
639 use crate::sign::v4;
640 use crate::sign::v4::sha256_hex_string;
641 use aws_credential_types::Credentials;
642 use aws_smithy_http::query_writer::QueryWriter;
643 use aws_smithy_runtime_api::client::identity::Identity;
644 use http0::{HeaderValue, Uri};
645 use pretty_assertions::assert_eq;
646 use proptest::{prelude::*, proptest};
647 use std::borrow::Cow;
648 use std::time::Duration;
649
650 fn signing_params(identity: &Identity, settings: SigningSettings) -> SigningParams<'_> {
651 v4::signing_params::Builder::default()
652 .identity(identity)
653 .region("test-region")
654 .name("testservicename")
655 .time(parse_date_time("20210511T154045Z").unwrap())
656 .settings(settings)
657 .build()
658 .unwrap()
659 .into()
660 }
661
662 #[test]
663 fn test_repeated_header() {
664 let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
665 req.headers.push((
666 "x-amz-object-attributes".to_string(),
667 "Checksum".to_string(),
668 ));
669 req.headers.push((
670 "x-amz-object-attributes".to_string(),
671 "ObjectSize".to_string(),
672 ));
673 let req = SignableRequest::from(&req);
674 let settings = SigningSettings {
675 payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
676 session_token_mode: SessionTokenMode::Exclude,
677 ..Default::default()
678 };
679 let identity = Credentials::for_tests().into();
680 let signing_params = signing_params(&identity, settings);
681 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
682
683 assert_eq!(
684 creq.values.signed_headers().to_string(),
685 "host;x-amz-content-sha256;x-amz-date;x-amz-object-attributes"
686 );
687 assert_eq!(
688 creq.header_values_for("x-amz-object-attributes"),
689 "Checksum,ObjectSize",
690 );
691 }
692
693 #[test]
694 fn test_set_xamz_sha_256() {
695 let req = test::v4::test_request("get-vanilla-query-order-key-case");
696 let req = SignableRequest::from(&req);
697 let settings = SigningSettings {
698 payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
699 session_token_mode: SessionTokenMode::Exclude,
700 ..Default::default()
701 };
702 let identity = Credentials::for_tests().into();
703 let mut signing_params = signing_params(&identity, settings);
704 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
705 assert_eq!(
706 creq.values.content_sha256(),
707 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
708 );
709 assert_eq!(
711 creq.values.signed_headers().as_str(),
712 "host;x-amz-content-sha256;x-amz-date"
713 );
714
715 signing_params.set_payload_checksum_kind(PayloadChecksumKind::NoHeader);
716 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
717 assert_eq!(creq.values.signed_headers().as_str(), "host;x-amz-date");
718 }
719
720 #[test]
721 fn test_unsigned_payload() {
722 let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
723 req.set_body(SignableBody::UnsignedPayload);
724 let req: SignableRequest<'_> = SignableRequest::from(&req);
725
726 let settings = SigningSettings {
727 payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
728 ..Default::default()
729 };
730 let identity = Credentials::for_tests().into();
731 let signing_params = signing_params(&identity, settings);
732 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
733 assert_eq!(creq.values.content_sha256(), "UNSIGNED-PAYLOAD");
734 assert!(creq.to_string().ends_with("UNSIGNED-PAYLOAD"));
735 }
736
737 #[test]
738 fn test_precomputed_payload() {
739 let payload_hash = "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072";
740 let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
741 req.set_body(SignableBody::Precomputed(String::from(payload_hash)));
742 let req = SignableRequest::from(&req);
743 let settings = SigningSettings {
744 payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
745 ..Default::default()
746 };
747 let identity = Credentials::for_tests().into();
748 let signing_params = signing_params(&identity, settings);
749 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
750 assert_eq!(creq.values.content_sha256(), payload_hash);
751 assert!(creq.to_string().ends_with(payload_hash));
752 }
753
754 #[test]
755 fn test_generate_scope() {
756 let expected = "20150830/us-east-1/iam/aws4_request\n";
757 let scope = SigningScope {
758 time: parse_date_time("20150830T123600Z").unwrap(),
759 region: "us-east-1",
760 service: "iam",
761 };
762 assert_eq!(format!("{}\n", scope), expected);
763 }
764
765 #[test]
766 fn test_string_to_sign() {
767 let time = parse_date_time("20150830T123600Z").unwrap();
768 let creq = test::v4::test_canonical_request("get-vanilla-query-order-key-case");
769 let expected_sts = test::v4::test_sts("get-vanilla-query-order-key-case");
770 let encoded = sha256_hex_string(creq.as_bytes());
771
772 let actual = StringToSign::new_v4(time, "us-east-1", "service", &encoded);
773 assert_eq!(expected_sts, actual.to_string());
774 }
775
776 #[test]
777 fn test_digest_of_canonical_request() {
778 let creq = test::v4::test_canonical_request("get-vanilla-query-order-key-case");
779 let expected = "816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0";
780 let actual = sha256_hex_string(creq.as_bytes());
781 assert_eq!(expected, actual);
782 }
783
784 #[test]
785 fn test_double_url_encode_path() {
786 let req = test::v4::test_request("double-encode-path");
787 let req = SignableRequest::from(&req);
788 let identity = Credentials::for_tests().into();
789 let signing_params = signing_params(&identity, SigningSettings::default());
790 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
791
792 let expected = test::v4::test_canonical_request("double-encode-path");
793 let actual = format!("{}", creq);
794 assert_eq!(actual, expected);
795 }
796
797 #[test]
798 fn test_double_url_encode() {
799 let req = test::v4::test_request("double-url-encode");
800 let req = SignableRequest::from(&req);
801 let identity = Credentials::for_tests().into();
802 let signing_params = signing_params(&identity, SigningSettings::default());
803 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
804
805 let expected = test::v4::test_canonical_request("double-url-encode");
806 let actual = format!("{}", creq);
807 assert_eq!(actual, expected);
808 }
809
810 #[test]
811 fn test_tilde_in_uri() {
812 let req = http0::Request::builder()
813 .uri("https://s3.us-east-1.amazonaws.com/my-bucket?list-type=2&prefix=~objprefix&single&k=&unreserved=-_.~").body("").unwrap().into();
814 let req = SignableRequest::from(&req);
815 let identity = Credentials::for_tests().into();
816 let signing_params = signing_params(&identity, SigningSettings::default());
817 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
818 assert_eq!(
819 Some("k=&list-type=2&prefix=~objprefix&single=&unreserved=-_.~"),
820 creq.params.as_deref(),
821 );
822 }
823
824 #[test]
825 fn test_signing_urls_with_percent_encoded_query_strings() {
826 let all_printable_ascii_chars: String = (32u8..127).map(char::from).collect();
827 let uri = Uri::from_static("https://s3.us-east-1.amazonaws.com/my-bucket");
828
829 let mut query_writer = QueryWriter::new(&uri);
830 query_writer.insert("list-type", "2");
831 query_writer.insert("prefix", &all_printable_ascii_chars);
832
833 let req = http0::Request::builder()
834 .uri(query_writer.build_uri())
835 .body("")
836 .unwrap()
837 .into();
838 let req = SignableRequest::from(&req);
839 let identity = Credentials::for_tests().into();
840 let signing_params = signing_params(&identity, SigningSettings::default());
841 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
842
843 let expected = "list-type=2&prefix=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~";
844 let actual = creq.params.unwrap();
845 assert_eq!(expected, actual);
846 }
847
848 #[test]
849 fn test_omit_session_token() {
850 let req = test::v4::test_request("get-vanilla-query-order-key-case");
851 let req = SignableRequest::from(&req);
852 let settings = SigningSettings {
853 session_token_mode: SessionTokenMode::Include,
854 ..Default::default()
855 };
856 let identity = Credentials::for_tests_with_session_token().into();
857 let mut signing_params = signing_params(&identity, settings);
858
859 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
860 assert_eq!(
861 creq.values.signed_headers().as_str(),
862 "host;x-amz-date;x-amz-security-token"
863 );
864 assert_eq!(
865 creq.headers.get("x-amz-security-token").unwrap(),
866 "notarealsessiontoken"
867 );
868
869 signing_params.set_session_token_mode(SessionTokenMode::Exclude);
870 let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
871 assert_eq!(
872 creq.headers.get("x-amz-security-token").unwrap(),
873 "notarealsessiontoken"
874 );
875 assert_eq!(creq.values.signed_headers().as_str(), "host;x-amz-date");
876 }
877
878 #[test]
880 fn non_presigning_header_exclusion() {
881 let request = http0::Request::builder()
882 .uri("https://some-endpoint.some-region.amazonaws.com")
883 .header("authorization", "test-authorization")
884 .header("content-type", "application/xml")
885 .header("content-length", "0")
886 .header("user-agent", "test-user-agent")
887 .header("x-amzn-trace-id", "test-trace-id")
888 .header("x-amz-user-agent", "test-user-agent")
889 .body("")
890 .unwrap()
891 .into();
892 let request = SignableRequest::from(&request);
893
894 let settings = SigningSettings {
895 signature_location: SignatureLocation::Headers,
896 ..Default::default()
897 };
898
899 let identity = Credentials::for_tests().into();
900 let signing_params = signing_params(&identity, settings);
901 let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
902
903 let values = canonical.values.as_headers().unwrap();
904 assert_eq!(
905 "content-length;content-type;host;x-amz-date;x-amz-user-agent",
906 values.signed_headers.as_str()
907 );
908 }
909
910 #[test]
912 fn presigning_header_exclusion() {
913 let request = http0::Request::builder()
914 .uri("https://some-endpoint.some-region.amazonaws.com")
915 .header("authorization", "test-authorization")
916 .header("content-type", "application/xml")
917 .header("content-length", "0")
918 .header("user-agent", "test-user-agent")
919 .header("x-amzn-trace-id", "test-trace-id")
920 .header("x-amz-user-agent", "test-user-agent")
921 .body("")
922 .unwrap()
923 .into();
924 let request = SignableRequest::from(&request);
925
926 let settings = SigningSettings {
927 signature_location: SignatureLocation::QueryParams,
928 expires_in: Some(Duration::from_secs(30)),
929 ..Default::default()
930 };
931
932 let identity = Credentials::for_tests().into();
933 let signing_params = signing_params(&identity, settings);
934 let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
935
936 let values = canonical.values.into_query_params().unwrap();
937 assert_eq!(
938 "content-length;content-type;host",
939 values.signed_headers.as_str()
940 );
941 }
942
943 #[allow(clippy::ptr_arg)] fn valid_input(input: &Vec<String>) -> bool {
945 [
946 "content-length".to_owned(),
947 "content-type".to_owned(),
948 "host".to_owned(),
949 ]
950 .iter()
951 .all(|element| !input.contains(element))
952 }
953
954 proptest! {
955 #[test]
956 fn presigning_header_exclusion_with_explicit_exclusion_list_specified(
957 excluded_headers in prop::collection::vec("[a-z]{1,20}", 1..10).prop_filter(
958 "`excluded_headers` should pass the `valid_input` check",
959 valid_input,
960 )
961 ) {
962 let mut request_builder = http0::Request::builder()
963 .uri("https://some-endpoint.some-region.amazonaws.com")
964 .header("content-type", "application/xml")
965 .header("content-length", "0");
966 for key in &excluded_headers {
967 request_builder = request_builder.header(key, "value");
968 }
969 let request = request_builder.body("").unwrap().into();
970
971 let request = SignableRequest::from(&request);
972
973 let settings = SigningSettings {
974 signature_location: SignatureLocation::QueryParams,
975 expires_in: Some(Duration::from_secs(30)),
976 excluded_headers: Some(
977 excluded_headers
978 .into_iter()
979 .map(std::borrow::Cow::Owned)
980 .collect(),
981 ),
982 ..Default::default()
983 };
984
985 let identity = Credentials::for_tests().into();
986 let signing_params = signing_params(&identity, settings);
987 let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
988
989 let values = canonical.values.into_query_params().unwrap();
990 assert_eq!(
991 "content-length;content-type;host",
992 values.signed_headers.as_str()
993 );
994 }
995 }
996
997 #[test]
998 fn test_trim_all_handles_spaces_correctly() {
999 assert_eq!(Cow::Borrowed("don't touch me"), trim_all("don't touch me"));
1000 assert_eq!("trim left", trim_all(" trim left"));
1001 assert_eq!("trim right", trim_all("trim right "));
1002 assert_eq!("trim both", trim_all(" trim both "));
1003 assert_eq!("", trim_all(" "));
1004 assert_eq!("", trim_all(" "));
1005 assert_eq!("a b", trim_all(" a b "));
1006 assert_eq!("Some example text", trim_all(" Some example text "));
1007 }
1008
1009 #[test]
1010 fn test_trim_all_ignores_other_forms_of_whitespace() {
1011 assert_eq!(
1013 "\t\u{A0}Some\u{A0} example \u{A0}text\u{A0}\n",
1014 trim_all("\t\u{A0}Some\u{A0} example \u{A0}text\u{A0}\n")
1015 );
1016 }
1017
1018 #[test]
1019 fn trim_spaces_works_on_single_characters() {
1020 assert_eq!(trim_all("2").as_ref(), "2");
1021 }
1022
1023 proptest! {
1024 #[test]
1025 fn test_trim_all_doesnt_elongate_strings(s in ".*") {
1026 assert!(trim_all(&s).len() <= s.len())
1027 }
1028
1029 #[test]
1030 fn test_normalize_header_value_works_on_valid_header_value(v in (".*")) {
1031 prop_assume!(HeaderValue::from_str(&v).is_ok());
1032 assert!(normalize_header_value(&v).is_ok());
1033 }
1034
1035 #[test]
1036 fn test_trim_all_does_nothing_when_there_are_no_spaces(s in "[^ ]*") {
1037 assert_eq!(trim_all(&s).as_ref(), s);
1038 }
1039 }
1040}