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