1use super::error::SigningError;
7use super::{PayloadChecksumKind, SignatureLocation};
8use crate::http_request::canonical_request::header;
9use crate::http_request::canonical_request::param;
10use crate::http_request::canonical_request::{CanonicalRequest, StringToSign};
11use crate::http_request::error::CanonicalRequestError;
12use crate::http_request::SigningParams;
13use crate::sign::v4;
14#[cfg(feature = "sigv4a")]
15use crate::sign::v4a;
16use crate::{SignatureVersion, SigningOutput};
17use http::Uri;
18use std::borrow::Cow;
19use std::fmt::{Debug, Formatter};
20use std::str;
21
22const LOG_SIGNABLE_BODY: &str = "LOG_SIGNABLE_BODY";
23
24#[derive(Debug)]
26#[non_exhaustive]
27pub struct SignableRequest<'a> {
28 method: &'a str,
29 uri: Uri,
30 headers: Vec<(&'a str, &'a str)>,
31 body: SignableBody<'a>,
32}
33
34impl<'a> SignableRequest<'a> {
35 pub fn new(
39 method: &'a str,
40 uri: impl Into<Cow<'a, str>>,
41 headers: impl Iterator<Item = (&'a str, &'a str)>,
42 body: SignableBody<'a>,
43 ) -> Result<Self, SigningError> {
44 let uri = uri
45 .into()
46 .parse()
47 .map_err(|e| SigningError::from(CanonicalRequestError::from(e)))?;
48 let headers = headers.collect();
49 Ok(Self {
50 method,
51 uri,
52 headers,
53 body,
54 })
55 }
56
57 pub(crate) fn uri(&self) -> &Uri {
59 &self.uri
60 }
61
62 pub(crate) fn method(&self) -> &str {
64 self.method
65 }
66
67 pub(crate) fn headers(&self) -> &[(&str, &str)] {
69 self.headers.as_slice()
70 }
71
72 pub fn body(&self) -> &SignableBody<'_> {
74 &self.body
75 }
76}
77
78#[derive(Clone, Eq, PartialEq)]
80#[non_exhaustive]
81pub enum SignableBody<'a> {
82 Bytes(&'a [u8]),
84
85 UnsignedPayload,
90
91 Precomputed(String),
95
96 StreamingUnsignedPayloadTrailer,
98
99 StreamingSignedPayloadTrailer,
101}
102
103impl Debug for SignableBody<'_> {
105 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
106 let should_log_signable_body = std::env::var(LOG_SIGNABLE_BODY)
107 .map(|v| v.eq_ignore_ascii_case("true"))
108 .unwrap_or_default();
109 match self {
110 Self::Bytes(arg0) => {
111 if should_log_signable_body {
112 f.debug_tuple("Bytes").field(arg0).finish()
113 } else {
114 let redacted = format!("** REDACTED **. To print {body_size} bytes of raw data, set environment variable `LOG_SIGNABLE_BODY=true`", body_size = arg0.len());
115 f.debug_tuple("Bytes").field(&redacted).finish()
116 }
117 }
118 Self::UnsignedPayload => write!(f, "UnsignedPayload"),
119 Self::Precomputed(arg0) => f.debug_tuple("Precomputed").field(arg0).finish(),
120 Self::StreamingUnsignedPayloadTrailer => {
121 write!(f, "StreamingUnsignedPayloadTrailer")
122 }
123 Self::StreamingSignedPayloadTrailer => {
124 write!(f, "StreamingSignedPayloadTrailer")
125 }
126 }
127 }
128}
129
130impl SignableBody<'_> {
131 pub fn empty() -> SignableBody<'static> {
133 SignableBody::Bytes(&[])
134 }
135}
136
137#[derive(Debug)]
139pub struct SigningInstructions {
140 headers: Vec<Header>,
141 params: Vec<(&'static str, Cow<'static, str>)>,
142}
143
144pub struct Header {
146 key: &'static str,
147 value: String,
148 sensitive: bool,
149}
150
151impl Debug for Header {
152 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153 let mut fmt = f.debug_struct("Header");
154 fmt.field("key", &self.key);
155 let value = if self.sensitive {
156 "** REDACTED **"
157 } else {
158 &self.value
159 };
160 fmt.field("value", &value);
161 fmt.finish()
162 }
163}
164
165impl Header {
166 pub fn name(&self) -> &'static str {
168 self.key
169 }
170
171 pub fn value(&self) -> &str {
173 &self.value
174 }
175
176 pub fn sensitive(&self) -> bool {
178 self.sensitive
179 }
180}
181
182impl SigningInstructions {
183 fn new(headers: Vec<Header>, params: Vec<(&'static str, Cow<'static, str>)>) -> Self {
184 Self { headers, params }
185 }
186
187 pub fn into_parts(self) -> (Vec<Header>, Vec<(&'static str, Cow<'static, str>)>) {
189 (self.headers, self.params)
190 }
191
192 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
194 self.headers
195 .iter()
196 .map(|header| (header.key, header.value.as_str()))
197 }
198
199 pub fn params(&self) -> &[(&str, Cow<'static, str>)] {
201 self.params.as_slice()
202 }
203
204 #[cfg(any(feature = "http0-compat", test))]
205 pub fn apply_to_request_http0x<B>(self, request: &mut http0::Request<B>) {
207 let (new_headers, new_query) = self.into_parts();
208 for header in new_headers.into_iter() {
209 let mut value = http0::HeaderValue::from_str(&header.value).unwrap();
210 value.set_sensitive(header.sensitive);
211 request.headers_mut().insert(header.key, value);
212 }
213
214 if !new_query.is_empty() {
215 let mut query = aws_smithy_http::query_writer::QueryWriter::new_from_string(
216 &request.uri().to_string(),
217 )
218 .expect("unreachable: URI is valid");
219 for (name, value) in new_query {
220 query.insert(name, &value);
221 }
222 let query_uri = query.build_uri().to_string();
223 let query_http0 = query_uri.parse::<http0::Uri>().expect("URI is valid");
224 *request.uri_mut() = query_http0;
225 }
226 }
227
228 #[cfg(any(feature = "http1", test))]
229 pub fn apply_to_request_http1x<B>(self, request: &mut http::Request<B>) {
231 let (new_headers, new_query) = self.into_parts();
234 for header in new_headers.into_iter() {
235 let mut value = http::HeaderValue::from_str(&header.value).unwrap();
236 value.set_sensitive(header.sensitive);
237 request.headers_mut().insert(header.key, value);
238 }
239
240 if !new_query.is_empty() {
241 let mut query = aws_smithy_http::query_writer::QueryWriter::new_from_string(
242 &request.uri().to_string(),
243 )
244 .expect("unreachable: URI is valid");
245 for (name, value) in new_query {
246 query.insert(name, &value);
247 }
248 *request.uri_mut() = query
249 .build_uri()
250 .to_string()
251 .parse()
252 .expect("unreachable: URI is valid");
253 }
254 }
255}
256
257pub fn sign<'a>(
260 request: SignableRequest<'a>,
261 params: &'a SigningParams<'a>,
262) -> Result<SigningOutput<SigningInstructions>, SigningError> {
263 tracing::trace!(request = ?request, params = ?params, "signing request");
264 match params.settings().signature_location {
265 SignatureLocation::Headers => {
266 let (signing_headers, signature) =
267 calculate_signing_headers(&request, params)?.into_parts();
268 Ok(SigningOutput::new(
269 SigningInstructions::new(signing_headers, vec![]),
270 signature,
271 ))
272 }
273 SignatureLocation::QueryParams => {
274 let (params, signature) = calculate_signing_params(&request, params)?;
275 Ok(SigningOutput::new(
276 SigningInstructions::new(vec![], params),
277 signature,
278 ))
279 }
280 }
281}
282
283type CalculatedParams = Vec<(&'static str, Cow<'static, str>)>;
284
285fn calculate_signing_params<'a>(
286 request: &'a SignableRequest<'a>,
287 params: &'a SigningParams<'a>,
288) -> Result<(CalculatedParams, String), SigningError> {
289 let creds = params.credentials()?;
290 let creq = CanonicalRequest::from(request, params)?;
291 let encoded_creq = &v4::sha256_hex_string(creq.to_string().as_bytes());
292
293 let (signature, string_to_sign) = match params {
294 SigningParams::V4(params) => {
295 let string_to_sign =
296 StringToSign::new_v4(params.time, params.region, params.name, encoded_creq)
297 .to_string();
298 let signing_key = v4::generate_signing_key(
299 creds.secret_access_key(),
300 params.time,
301 params.region,
302 params.name,
303 );
304 let signature = v4::calculate_signature(signing_key, string_to_sign.as_bytes());
305 (signature, string_to_sign)
306 }
307 #[cfg(feature = "sigv4a")]
308 SigningParams::V4a(params) => {
309 let string_to_sign =
310 StringToSign::new_v4a(params.time, params.region_set, params.name, encoded_creq)
311 .to_string();
312
313 let secret_key =
314 v4a::generate_signing_key(creds.access_key_id(), creds.secret_access_key());
315 let signature = v4a::calculate_signature(&secret_key, string_to_sign.as_bytes());
316 (signature, string_to_sign)
317 }
318 };
319 tracing::trace!(canonical_request = %creq, string_to_sign = %string_to_sign, "calculated signing parameters");
320
321 let values = creq.values.into_query_params().expect("signing with query");
322 let mut signing_params = vec![
323 (param::X_AMZ_ALGORITHM, Cow::Borrowed(values.algorithm)),
324 (param::X_AMZ_CREDENTIAL, Cow::Owned(values.credential)),
325 (param::X_AMZ_DATE, Cow::Owned(values.date_time)),
326 (param::X_AMZ_EXPIRES, Cow::Owned(values.expires)),
327 (
328 param::X_AMZ_SIGNED_HEADERS,
329 Cow::Owned(values.signed_headers.as_str().into()),
330 ),
331 (param::X_AMZ_SIGNATURE, Cow::Owned(signature.clone())),
332 ];
333
334 #[cfg(feature = "sigv4a")]
335 if let Some(region_set) = params.region_set() {
336 if params.signature_version() == SignatureVersion::V4a {
337 signing_params.push((
338 crate::http_request::canonical_request::sigv4a::param::X_AMZ_REGION_SET,
339 Cow::Owned(region_set.to_owned()),
340 ));
341 }
342 }
343
344 if let Some(security_token) = creds.session_token() {
345 signing_params.push((
346 params
347 .settings()
348 .session_token_name_override
349 .unwrap_or(param::X_AMZ_SECURITY_TOKEN),
350 Cow::Owned(security_token.to_string()),
351 ));
352 }
353
354 Ok((signing_params, signature))
355}
356
357fn calculate_signing_headers<'a>(
364 request: &'a SignableRequest<'a>,
365 params: &'a SigningParams<'a>,
366) -> Result<SigningOutput<Vec<Header>>, SigningError> {
367 let creds = params.credentials()?;
368
369 let creq = CanonicalRequest::from(request, params)?;
371 let encoded_creq = v4::sha256_hex_string(creq.to_string().as_bytes());
373 tracing::trace!(canonical_request = %creq);
374 let mut headers = vec![];
375
376 let signature = match params {
377 SigningParams::V4(params) => {
378 let sts = StringToSign::new_v4(
379 params.time,
380 params.region,
381 params.name,
382 encoded_creq.as_str(),
383 );
384
385 let signing_key = v4::generate_signing_key(
387 creds.secret_access_key(),
388 params.time,
389 params.region,
390 params.name,
391 );
392 let signature = v4::calculate_signature(signing_key, sts.to_string().as_bytes());
393
394 let values = creq.values.as_headers().expect("signing with headers");
396 add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false);
397 headers.push(Header {
398 key: "authorization",
399 value: build_authorization_header(
400 creds.access_key_id(),
401 &creq,
402 sts,
403 &signature,
404 SignatureVersion::V4,
405 ),
406 sensitive: false,
407 });
408 if params.settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
409 add_header(
410 &mut headers,
411 header::X_AMZ_CONTENT_SHA_256,
412 &values.content_sha256,
413 false,
414 );
415 }
416
417 if let Some(security_token) = creds.session_token() {
418 add_header(
419 &mut headers,
420 params
421 .settings
422 .session_token_name_override
423 .unwrap_or(header::X_AMZ_SECURITY_TOKEN),
424 security_token,
425 true,
426 );
427 }
428 signature
429 }
430 #[cfg(feature = "sigv4a")]
431 SigningParams::V4a(params) => {
432 let sts = StringToSign::new_v4a(
433 params.time,
434 params.region_set,
435 params.name,
436 encoded_creq.as_str(),
437 );
438
439 let signing_key =
440 v4a::generate_signing_key(creds.access_key_id(), creds.secret_access_key());
441 let signature = v4a::calculate_signature(&signing_key, sts.to_string().as_bytes());
442
443 let values = creq.values.as_headers().expect("signing with headers");
444 add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false);
445 add_header(
446 &mut headers,
447 crate::http_request::canonical_request::sigv4a::header::X_AMZ_REGION_SET,
448 params.region_set,
449 false,
450 );
451
452 headers.push(Header {
453 key: "authorization",
454 value: build_authorization_header(
455 creds.access_key_id(),
456 &creq,
457 sts,
458 &signature,
459 SignatureVersion::V4a,
460 ),
461 sensitive: false,
462 });
463 if params.settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
464 add_header(
465 &mut headers,
466 header::X_AMZ_CONTENT_SHA_256,
467 &values.content_sha256,
468 false,
469 );
470 }
471
472 if let Some(security_token) = creds.session_token() {
473 add_header(
474 &mut headers,
475 header::X_AMZ_SECURITY_TOKEN,
476 security_token,
477 true,
478 );
479 }
480 signature
481 }
482 };
483
484 Ok(SigningOutput::new(headers, signature))
485}
486
487fn add_header(map: &mut Vec<Header>, key: &'static str, value: &str, sensitive: bool) {
488 map.push(Header {
489 key,
490 value: value.to_string(),
491 sensitive,
492 });
493}
494
495fn build_authorization_header(
498 access_key: &str,
499 creq: &CanonicalRequest<'_>,
500 sts: StringToSign<'_>,
501 signature: &str,
502 signature_version: SignatureVersion,
503) -> String {
504 let scope = match signature_version {
505 SignatureVersion::V4 => sts.scope.to_string(),
506 SignatureVersion::V4a => sts.scope.v4a_display(),
507 };
508 format!(
509 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
510 sts.algorithm,
511 access_key,
512 scope,
513 creq.values.signed_headers().as_str(),
514 signature
515 )
516}
517#[cfg(test)]
518mod tests {
519 use crate::date_time::test_parsers::parse_date_time;
520 use crate::http_request::sign::{add_header, SignableRequest};
521 use crate::http_request::test::SigningSuiteTest;
522 use crate::http_request::{
523 sign, SessionTokenMode, SignableBody, SignatureLocation, SigningInstructions,
524 SigningSettings,
525 };
526 use crate::sign::v4;
527 use aws_credential_types::Credentials;
528 use http::{HeaderValue, Request};
529 use pretty_assertions::assert_eq;
530 use proptest::proptest;
531 use std::borrow::Cow;
532 use std::iter;
533
534 macro_rules! assert_req_eq {
535 (http: $expected:expr, $actual:expr) => {
536 let mut expected = ($expected).map(|_b|"body");
537 let mut actual = ($actual).map(|_b|"body");
538 make_headers_comparable(&mut expected);
539 make_headers_comparable(&mut actual);
540 assert_eq!(format!("{:?}", expected), format!("{:?}", actual));
541 };
542 ($expected:tt, $actual:tt) => {
543 assert_req_eq!(http: ($expected).as_http_request(), $actual);
544 };
545 }
546
547 pub(crate) fn make_headers_comparable<B>(request: &mut Request<B>) {
548 for (_name, value) in request.headers_mut() {
549 value.set_sensitive(false);
550 }
551 }
552
553 #[cfg(feature = "sigv4a")]
555 mod v4a_suite {
556 use crate::http_request::test::v4a::run_test_suite_v4a;
557
558 #[test]
559 fn test_get_header_key_duplicate() {
560 run_test_suite_v4a("get-header-key-duplicate")
561 }
562
563 #[test]
564 #[ignore = "httpparse doesn't support parsing multiline headers since they are deprecated in RFC7230"]
565 fn test_get_header_value_multiline() {
566 run_test_suite_v4a("get-header-value-multiline")
567 }
568
569 #[test]
570 fn test_get_header_value_order() {
571 run_test_suite_v4a("get-header-value-order")
572 }
573
574 #[test]
575 fn test_get_header_value_trim() {
576 run_test_suite_v4a("get-header-value-trim");
577 }
578
579 #[test]
580 fn test_get_relative_normalized() {
581 run_test_suite_v4a("get-relative-normalized");
582 }
583
584 #[test]
585 fn test_get_relative_relative_normalized() {
586 run_test_suite_v4a("get-relative-relative-normalized");
587 }
588
589 #[test]
590 fn test_get_relative_relative_unnormalized() {
591 run_test_suite_v4a("get-relative-relative-unnormalized");
592 }
593
594 #[test]
595 fn test_get_relative_unnormalized() {
596 run_test_suite_v4a("get-relative-unnormalized");
597 }
598
599 #[test]
600 fn test_get_slash_dot_slash_normalized() {
601 run_test_suite_v4a("get-slash-dot-slash-normalized");
602 }
603
604 #[test]
605 fn test_get_slash_dot_slash_unnormalized() {
606 run_test_suite_v4a("get-slash-dot-slash-unnormalized");
607 }
608
609 #[test]
610 fn test_get_slash_normalized() {
611 run_test_suite_v4a("get-slash-normalized");
612 }
613
614 #[test]
615 fn test_get_slash_pointless_dot_normalized() {
616 run_test_suite_v4a("get-slash-pointless-dot-normalized");
617 }
618
619 #[test]
620 fn test_get_slash_pointless_dot_unnormalized() {
621 run_test_suite_v4a("get-slash-pointless-dot-unnormalized");
622 }
623
624 #[test]
625 fn test_get_slash_unnormalized() {
626 run_test_suite_v4a("get-slash-unnormalized");
627 }
628
629 #[test]
630 fn test_get_slashes_normalized() {
631 run_test_suite_v4a("get-slashes-normalized");
632 }
633
634 #[test]
635 fn test_get_slashes_unnormalized() {
636 run_test_suite_v4a("get-slashes-unnormalized");
637 }
638
639 #[test]
640 #[ignore = "relies on single encode of path segments"]
641 fn test_get_space_normalized() {
644 run_test_suite_v4a("get-space-normalized");
645 }
646
647 #[test]
648 #[ignore = "httpparse fails on unencoded spaces in path"]
649 fn test_get_space_unnormalized() {
651 run_test_suite_v4a("get-space-unnormalized");
652 }
653
654 #[test]
655 fn test_get_unreserved() {
656 run_test_suite_v4a("get-unreserved");
657 }
658
659 #[test]
660 #[ignore = "httparse fails on invalid uri character"]
661 fn test_get_utf8() {
663 run_test_suite_v4a("get-utf8");
664 }
665
666 #[test]
667 fn test_get_vanilla() {
668 run_test_suite_v4a("get-vanilla");
669 }
670
671 #[test]
672 fn test_get_vanilla_empty_query_key() {
673 run_test_suite_v4a("get-vanilla-empty-query-key");
674 }
675
676 #[test]
677 fn test_get_vanilla_query() {
678 run_test_suite_v4a("get-vanilla-query");
679 }
680
681 #[test]
682 fn test_get_vanilla_query_order_encoded() {
683 run_test_suite_v4a("get-vanilla-query-order-encoded");
684 }
685
686 #[test]
687 fn test_get_vanilla_query_order_key_case() {
688 run_test_suite_v4a("get-vanilla-query-order-key-case");
689 }
690
691 #[test]
692 fn test_get_vanilla_query_unreserved() {
693 run_test_suite_v4a("get-vanilla-query-unreserved");
694 }
695
696 #[test]
697 #[ignore = "httparse fails on invalid uri character"]
698 fn test_get_vanilla_utf8_query() {
700 run_test_suite_v4a("get-vanilla-utf8-query");
701 }
702
703 #[test]
704 fn test_get_vanilla_with_session_token() {
705 run_test_suite_v4a("get-vanilla-with-session-token")
706 }
707
708 #[test]
709 fn test_post_header_key_case() {
710 run_test_suite_v4a("post-header-key-case");
711 }
712
713 #[test]
714 fn test_post_header_key_sort() {
715 run_test_suite_v4a("post-header-key-sort");
716 }
717
718 #[test]
719 fn test_post_header_value_case() {
720 run_test_suite_v4a("post-header-value-case");
721 }
722
723 #[test]
724 fn test_post_sts_header_after() {
725 run_test_suite_v4a("post-sts-header-after");
726 }
727
728 #[test]
729 fn test_post_sts_header_before() {
730 run_test_suite_v4a("post-sts-header-before");
731 }
732
733 #[test]
734 fn test_post_vanilla() {
735 run_test_suite_v4a("post-vanilla");
736 }
737
738 #[test]
739 fn test_post_vanilla_empty_query_value() {
740 run_test_suite_v4a("post-vanilla-empty-query-value");
741 }
742
743 #[test]
744 fn test_post_vanilla_query() {
745 run_test_suite_v4a("post-vanilla-query");
746 }
747
748 #[test]
749 fn test_post_x_www_form_urlencoded() {
750 run_test_suite_v4a("post-x-www-form-urlencoded");
751 }
752
753 #[test]
754 fn test_post_x_www_form_urlencoded_parameters() {
755 run_test_suite_v4a("post-x-www-form-urlencoded-parameters");
756 }
757 }
758
759 #[test]
760 fn test_sign_url_escape() {
761 let test = SigningSuiteTest::v4("double-encode-path");
762 let settings = SigningSettings::default();
763 let identity = &Credentials::for_tests().into();
764 let params = v4::SigningParams {
765 identity,
766 region: "us-east-1",
767 name: "service",
768 time: parse_date_time("20150830T123600Z").unwrap(),
769 settings,
770 }
771 .into();
772
773 let original = test.request();
774 let signable = SignableRequest::from(&original);
775 let out = sign(signable, ¶ms).unwrap();
776 assert_eq!(
777 "57d157672191bac40bae387e48bbe14b15303c001fdbb01f4abf295dccb09705",
778 out.signature
779 );
780
781 let mut signed = original.as_http_request();
782 out.output.apply_to_request_http1x(&mut signed);
783
784 let expected = test.signed_request(SignatureLocation::Headers);
785 assert_req_eq!(expected, signed);
786 }
787
788 #[test]
789 fn test_sign_headers_utf8() {
790 let settings = SigningSettings::default();
791 let identity = &Credentials::for_tests().into();
792 let params = v4::SigningParams {
793 identity,
794 region: "us-east-1",
795 name: "service",
796 time: parse_date_time("20150830T123600Z").unwrap(),
797 settings,
798 }
799 .into();
800
801 let original = http::Request::builder()
802 .uri("https://some-endpoint.some-region.amazonaws.com")
803 .header("some-header", HeaderValue::from_str("テスト").unwrap())
804 .body("")
805 .unwrap()
806 .into();
807 let signable = SignableRequest::from(&original);
808 let out = sign(signable, ¶ms).unwrap();
809 assert_eq!(
810 "55e16b31f9bde5fd04f9d3b780dd2b5e5f11a5219001f91a8ca9ec83eaf1618f",
811 out.signature
812 );
813
814 let mut signed = original.as_http_request();
815 out.output.apply_to_request_http1x(&mut signed);
816
817 let expected = http::Request::builder()
818 .uri("https://some-endpoint.some-region.amazonaws.com")
819 .header("some-header", HeaderValue::from_str("テスト").unwrap())
820 .header(
821 "x-amz-date",
822 HeaderValue::from_str("20150830T123600Z").unwrap(),
823 )
824 .header(
825 "authorization",
826 HeaderValue::from_str(
827 "AWS4-HMAC-SHA256 \
828 Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
829 SignedHeaders=host;some-header;x-amz-date, \
830 Signature=55e16b31f9bde5fd04f9d3b780dd2b5e5f11a5219001f91a8ca9ec83eaf1618f",
831 )
832 .unwrap(),
833 )
834 .body("")
835 .unwrap();
836 assert_req_eq!(http: expected, signed);
837 }
838
839 #[test]
840 fn test_sign_headers_excluding_session_token() {
841 let settings = SigningSettings {
842 session_token_mode: SessionTokenMode::Exclude,
843 ..Default::default()
844 };
845 let identity = &Credentials::for_tests_with_session_token().into();
846 let params = v4::SigningParams {
847 identity,
848 region: "us-east-1",
849 name: "service",
850 time: parse_date_time("20150830T123600Z").unwrap(),
851 settings,
852 }
853 .into();
854
855 let original = http::Request::builder()
856 .uri("https://some-endpoint.some-region.amazonaws.com")
857 .body("")
858 .unwrap()
859 .into();
860 let out_without_session_token = sign(SignableRequest::from(&original), ¶ms).unwrap();
861
862 let out_with_session_token_but_excluded =
863 sign(SignableRequest::from(&original), ¶ms).unwrap();
864 assert_eq!(
865 "ab32de057edf094958d178b3c91f3c8d5c296d526b11da991cd5773d09cea560",
866 out_with_session_token_but_excluded.signature
867 );
868 assert_eq!(
869 out_with_session_token_but_excluded.signature,
870 out_without_session_token.signature
871 );
872
873 let mut signed = original.as_http_request();
874 out_with_session_token_but_excluded
875 .output
876 .apply_to_request_http1x(&mut signed);
877
878 let expected = http::Request::builder()
879 .uri("https://some-endpoint.some-region.amazonaws.com")
880 .header(
881 "x-amz-date",
882 HeaderValue::from_str("20150830T123600Z").unwrap(),
883 )
884 .header(
885 "authorization",
886 HeaderValue::from_str(
887 "AWS4-HMAC-SHA256 \
888 Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
889 SignedHeaders=host;x-amz-date, \
890 Signature=ab32de057edf094958d178b3c91f3c8d5c296d526b11da991cd5773d09cea560",
891 )
892 .unwrap(),
893 )
894 .header(
895 "x-amz-security-token",
896 HeaderValue::from_str("notarealsessiontoken").unwrap(),
897 )
898 .body(b"")
899 .unwrap();
900 assert_req_eq!(http: expected, signed);
901 }
902
903 #[test]
904 fn test_sign_headers_space_trimming() {
905 let settings = SigningSettings::default();
906 let identity = &Credentials::for_tests().into();
907 let params = v4::SigningParams {
908 identity,
909 region: "us-east-1",
910 name: "service",
911 time: parse_date_time("20150830T123600Z").unwrap(),
912 settings,
913 }
914 .into();
915
916 let original = http::Request::builder()
917 .uri("https://some-endpoint.some-region.amazonaws.com")
918 .header(
919 "some-header",
920 HeaderValue::from_str(" test test ").unwrap(),
921 )
922 .body("")
923 .unwrap()
924 .into();
925 let signable = SignableRequest::from(&original);
926 let out = sign(signable, ¶ms).unwrap();
927 assert_eq!(
928 "244f2a0db34c97a528f22715fe01b2417b7750c8a95c7fc104a3c48d81d84c08",
929 out.signature
930 );
931
932 let mut signed = original.as_http_request();
933 out.output.apply_to_request_http1x(&mut signed);
934
935 let expected = http::Request::builder()
936 .uri("https://some-endpoint.some-region.amazonaws.com")
937 .header(
938 "some-header",
939 HeaderValue::from_str(" test test ").unwrap(),
940 )
941 .header(
942 "x-amz-date",
943 HeaderValue::from_str("20150830T123600Z").unwrap(),
944 )
945 .header(
946 "authorization",
947 HeaderValue::from_str(
948 "AWS4-HMAC-SHA256 \
949 Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
950 SignedHeaders=host;some-header;x-amz-date, \
951 Signature=244f2a0db34c97a528f22715fe01b2417b7750c8a95c7fc104a3c48d81d84c08",
952 )
953 .unwrap(),
954 )
955 .body("")
956 .unwrap();
957 assert_req_eq!(http: expected, signed);
958 }
959
960 proptest! {
961 #[test]
962 fn test_sign_headers_no_panic(
965 header in ".*"
966 ) {
967 let settings = SigningSettings::default();
968 let identity = &Credentials::for_tests().into();
969 let params = v4::SigningParams {
970 identity,
971 region: "us-east-1",
972 name: "foo",
973 time: std::time::SystemTime::UNIX_EPOCH,
974 settings,
975 }.into();
976
977 let req = SignableRequest::new(
978 "GET",
979 "https://foo.com",
980 iter::once(("x-sign-me", header.as_str())),
981 SignableBody::Bytes(&[])
982 );
983
984 if let Ok(req) = req {
985 let _creq = crate::http_request::sign(req, ¶ms);
987 }
988 }
989 }
990
991 #[test]
992 fn apply_signing_instructions_headers() {
993 let mut headers = vec![];
994 add_header(&mut headers, "some-header", "foo", false);
995 add_header(&mut headers, "some-other-header", "bar", false);
996 let instructions = SigningInstructions::new(headers, vec![]);
997
998 let mut request = http::Request::builder()
999 .uri("https://some-endpoint.some-region.amazonaws.com")
1000 .body("")
1001 .unwrap();
1002
1003 instructions.apply_to_request_http1x(&mut request);
1004
1005 let get_header = |n: &str| request.headers().get(n).unwrap().to_str().unwrap();
1006 assert_eq!("foo", get_header("some-header"));
1007 assert_eq!("bar", get_header("some-other-header"));
1008 }
1009
1010 #[test]
1011 fn apply_signing_instructions_query_params() {
1012 let params = vec![
1013 ("some-param", Cow::Borrowed("f&o?o")),
1014 ("some-other-param?", Cow::Borrowed("bar")),
1015 ];
1016 let instructions = SigningInstructions::new(vec![], params);
1017
1018 let mut request = http::Request::builder()
1019 .uri("https://some-endpoint.some-region.amazonaws.com/some/path")
1020 .body("")
1021 .unwrap();
1022
1023 instructions.apply_to_request_http1x(&mut request);
1024
1025 assert_eq!(
1026 "/some/path?some-param=f%26o%3Fo&some-other-param%3F=bar",
1027 request.uri().path_and_query().unwrap().to_string()
1028 );
1029 }
1030
1031 #[test]
1032 fn apply_signing_instructions_query_params_http_1x() {
1033 let params = vec![
1034 ("some-param", Cow::Borrowed("f&o?o")),
1035 ("some-other-param?", Cow::Borrowed("bar")),
1036 ];
1037 let instructions = SigningInstructions::new(vec![], params);
1038
1039 let mut request = http::Request::builder()
1040 .uri("https://some-endpoint.some-region.amazonaws.com/some/path")
1041 .body("")
1042 .unwrap();
1043
1044 instructions.apply_to_request_http1x(&mut request);
1045
1046 assert_eq!(
1047 "/some/path?some-param=f%26o%3Fo&some-other-param%3F=bar",
1048 request.uri().path_and_query().unwrap().to_string()
1049 );
1050 }
1051
1052 #[test]
1053 fn test_debug_signable_body() {
1054 let sut = SignableBody::Bytes(b"hello signable body");
1055 assert_eq!(
1056 "Bytes(\"** REDACTED **. To print 19 bytes of raw data, set environment variable `LOG_SIGNABLE_BODY=true`\")",
1057 format!("{sut:?}")
1058 );
1059
1060 let sut = SignableBody::UnsignedPayload;
1061 assert_eq!("UnsignedPayload", format!("{sut:?}"));
1062
1063 let sut = SignableBody::Precomputed("precomputed".to_owned());
1064 assert_eq!("Precomputed(\"precomputed\")", format!("{sut:?}"));
1065
1066 let sut = SignableBody::StreamingUnsignedPayloadTrailer;
1067 assert_eq!("StreamingUnsignedPayloadTrailer", format!("{sut:?}"));
1068 }
1069
1070 mod v4_suite {
1072 use crate::http_request::test::run_test_suite_v4;
1073
1074 #[test]
1075 fn test_get_header_key_duplicate() {
1076 run_test_suite_v4("get-header-key-duplicate");
1077 }
1078
1079 #[test]
1080 #[ignore = "httpparse doesn't support parsing multiline headers since they are deprecated in RFC7230"]
1081 fn test_get_header_value_multiline() {
1082 run_test_suite_v4("get-header-value-multiline");
1083 }
1084
1085 #[test]
1086 fn test_get_header_value_order() {
1087 run_test_suite_v4("get-header-value-order");
1088 }
1089
1090 #[test]
1091 fn test_get_header_value_trim() {
1092 run_test_suite_v4("get-header-value-trim");
1093 }
1094
1095 #[test]
1096 fn test_get_relative_normalized() {
1097 run_test_suite_v4("get-relative-normalized");
1098 }
1099
1100 #[test]
1101 fn test_get_relative_relative_normalized() {
1102 run_test_suite_v4("get-relative-relative-normalized");
1103 }
1104
1105 #[test]
1106 fn test_get_relative_relative_unnormalized() {
1107 run_test_suite_v4("get-relative-relative-unnormalized");
1108 }
1109
1110 #[test]
1111 fn test_get_relative_unnormalized() {
1112 run_test_suite_v4("get-relative-unnormalized");
1113 }
1114
1115 #[test]
1116 fn test_get_slash_dot_slash_normalized() {
1117 run_test_suite_v4("get-slash-dot-slash-normalized");
1118 }
1119
1120 #[test]
1121 fn test_get_slash_dot_slash_unnormalized() {
1122 run_test_suite_v4("get-slash-dot-slash-unnormalized");
1123 }
1124
1125 #[test]
1126 fn test_get_slash_normalized() {
1127 run_test_suite_v4("get-slash-normalized");
1128 }
1129
1130 #[test]
1131 fn test_get_slash_pointless_dot_normalized() {
1132 run_test_suite_v4("get-slash-pointless-dot-normalized");
1133 }
1134
1135 #[test]
1136 fn test_get_slash_pointless_dot_unnormalized() {
1137 run_test_suite_v4("get-slash-pointless-dot-unnormalized");
1138 }
1139
1140 #[test]
1141 fn test_get_slash_unnormalized() {
1142 run_test_suite_v4("get-slash-unnormalized");
1143 }
1144
1145 #[test]
1146 fn test_get_slashes_normalized() {
1147 run_test_suite_v4("get-slashes-normalized");
1148 }
1149
1150 #[test]
1151 fn test_get_slashes_unnormalized() {
1152 run_test_suite_v4("get-slashes-unnormalized");
1153 }
1154
1155 #[test]
1156 #[ignore = "relies on single encode of path segments"]
1157 fn test_get_space_normalized() {
1160 run_test_suite_v4("get-space-normalized");
1161 }
1162
1163 #[test]
1164 #[ignore = "httpparse fails on unencoded spaces in path"]
1165 fn test_get_space_unnormalized() {
1167 run_test_suite_v4("get-space-unnormalized");
1168 }
1169
1170 #[test]
1171 fn test_get_unreserved() {
1172 run_test_suite_v4("get-unreserved");
1173 }
1174
1175 #[test]
1176 #[ignore = "httparse fails on invalid uri character"]
1177 fn test_get_utf8() {
1179 run_test_suite_v4("get-utf8");
1180 }
1181
1182 #[test]
1183 fn test_get_vanilla() {
1184 run_test_suite_v4("get-vanilla");
1185 }
1186
1187 #[test]
1188 fn test_get_vanilla_empty_query_key() {
1189 run_test_suite_v4("get-vanilla-empty-query-key");
1190 }
1191
1192 #[test]
1193 fn test_get_vanilla_query() {
1194 run_test_suite_v4("get-vanilla-query");
1195 }
1196
1197 #[test]
1198 fn test_get_vanilla_query_order_encoded() {
1199 run_test_suite_v4("get-vanilla-query-order-encoded");
1200 }
1201
1202 #[test]
1203 fn test_get_vanilla_query_order_key_case() {
1204 run_test_suite_v4("get-vanilla-query-order-key-case");
1205 }
1206
1207 #[test]
1208 fn test_get_vanilla_query_unreserved() {
1209 run_test_suite_v4("get-vanilla-query-unreserved");
1210 }
1211
1212 #[test]
1213 #[ignore = "httparse fails on invalid uri character"]
1214 fn test_get_vanilla_utf8_query() {
1216 run_test_suite_v4("get-vanilla-utf8-query");
1217 }
1218
1219 #[test]
1220 fn test_get_vanilla_with_session_token() {
1221 run_test_suite_v4("get-vanilla-with-session-token");
1222 }
1223
1224 #[test]
1225 fn test_post_header_key_case() {
1226 run_test_suite_v4("post-header-key-case");
1227 }
1228
1229 #[test]
1230 fn test_post_header_key_sort() {
1231 run_test_suite_v4("post-header-key-sort");
1232 }
1233
1234 #[test]
1235 fn test_post_header_value_case() {
1236 run_test_suite_v4("post-header-value-case");
1237 }
1238
1239 #[test]
1240 fn test_post_sts_header_after() {
1241 run_test_suite_v4("post-sts-header-after");
1242 }
1243
1244 #[test]
1245 fn test_post_sts_header_before() {
1246 run_test_suite_v4("post-sts-header-before");
1247 }
1248
1249 #[test]
1250 fn test_post_vanilla() {
1251 run_test_suite_v4("post-vanilla");
1252 }
1253
1254 #[test]
1255 fn test_post_vanilla_empty_query_value() {
1256 run_test_suite_v4("post-vanilla-empty-query-value");
1257 }
1258
1259 #[test]
1260 fn test_post_vanilla_query() {
1261 run_test_suite_v4("post-vanilla-query");
1262 }
1263
1264 #[test]
1265 fn test_post_x_www_form_urlencoded() {
1266 run_test_suite_v4("post-x-www-form-urlencoded");
1267 }
1268
1269 #[test]
1270 fn test_post_x_www_form_urlencoded_parameters() {
1271 run_test_suite_v4("post-x-www-form-urlencoded-parameters");
1272 }
1273 }
1274}