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