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