1use std::collections::BTreeMap;
19use std::sync::Arc;
20use std::time::Instant;
21
22use async_trait::async_trait;
23use bytes::Buf;
24use chrono::{DateTime, Utc};
25use percent_encoding::utf8_percent_encode;
26use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
27use reqwest::{Client, Method, Request, RequestBuilder, StatusCode};
28use serde::Deserialize;
29use tracing::warn;
30use url::Url;
31
32use crate::retry::RetryExt;
33use crate::service::HttpService;
34use crate::token::{TemporaryToken, TokenCache};
35use crate::util::{hex_digest, hex_encode, hmac_sha256};
36use crate::{CredentialProvider, Result, RetryConfig, TokenProvider};
37
38use crate::util::STRICT_ENCODE_SET;
39
40const STRICT_PATH_ENCODE_SET: percent_encoding::AsciiSet = STRICT_ENCODE_SET.remove(b'/');
42
43type StdError = Box<dyn std::error::Error + Send + Sync>;
44
45static EMPTY_SHA256_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
47
48#[derive(Eq, PartialEq)]
50pub struct AwsCredential {
51 pub key_id: String,
53 pub secret_key: String,
55 pub token: Option<String>,
57}
58
59impl std::fmt::Debug for AwsCredential {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("AwsCredential")
65 .field("key_id", &"<redacted>")
66 .field("secret_key", &"<redacted>")
67 .field("token", &self.token.as_ref().map(|_| "<redacted>"))
68 .finish()
69 }
70}
71
72impl AwsCredential {
73 fn sign(&self, to_sign: &str, date: DateTime<Utc>, region: &str, service: &str) -> String {
77 let date_string = date.format("%Y%m%d").to_string();
78 let date_hmac = hmac_sha256(format!("AWS4{}", self.secret_key), date_string);
79 let region_hmac = hmac_sha256(date_hmac, region);
80 let service_hmac = hmac_sha256(region_hmac, service);
81 let signing_hmac = hmac_sha256(service_hmac, b"aws4_request");
82 hex_encode(hmac_sha256(signing_hmac, to_sign).as_ref())
83 }
84}
85
86#[derive(Debug)]
90pub struct AwsAuthorizer<'a> {
91 date: Option<DateTime<Utc>>,
92 credential: &'a AwsCredential,
93 service: &'a str,
94 region: &'a str,
95}
96
97static DATE_HEADER: hyper::header::HeaderName =
98 hyper::header::HeaderName::from_static("x-amz-date");
99static HASH_HEADER: hyper::header::HeaderName =
100 hyper::header::HeaderName::from_static("x-amz-content-sha256");
101static TOKEN_HEADER: hyper::header::HeaderName =
102 hyper::header::HeaderName::from_static("x-amz-security-token");
103const ALGORITHM: &str = "AWS4-HMAC-SHA256";
104
105impl<'a> AwsAuthorizer<'a> {
106 pub fn new(credential: &'a AwsCredential, service: &'a str, region: &'a str) -> Self {
108 Self {
109 credential,
110 service,
111 region,
112 date: None,
113 }
114 }
115
116 pub fn authorize(
121 &self,
122 request: &mut Request,
123 pre_calculated_digest: Option<&[u8]>,
124 ) -> crate::Result<()> {
125 if let Some(ref token) = self.credential.token {
126 let token_val = HeaderValue::from_str(token)?;
127 request.headers_mut().insert(&TOKEN_HEADER, token_val);
128 }
129
130 let host = &request.url()[url::Position::BeforeHost..url::Position::AfterPort];
131 let host_val = HeaderValue::from_str(host)?;
132 request.headers_mut().insert("host", host_val);
133
134 let date = self.date.unwrap_or_else(Utc::now);
135 let date_str = date.format("%Y%m%dT%H%M%SZ").to_string();
136 let date_val = HeaderValue::from_str(&date_str)?;
137 request.headers_mut().insert(&DATE_HEADER, date_val);
138
139 let digest = match pre_calculated_digest {
140 Some(digest) => hex_encode(digest),
141 None => match request.body() {
142 None => EMPTY_SHA256_HASH.to_string(),
143 Some(body) => match body.as_bytes() {
144 Some(bytes) => hex_digest(bytes),
145 None => EMPTY_SHA256_HASH.to_string(),
146 },
147 },
148 };
149
150 let header_digest = HeaderValue::from_str(&digest)?;
151 request.headers_mut().insert(&HASH_HEADER, header_digest);
152
153 let (signed_headers, canonical_headers) = canonicalize_headers(request.headers());
154
155 let scope = self.scope(date);
156
157 let string_to_sign = self.string_to_sign(
158 date,
159 &scope,
160 request.method(),
161 request.url(),
162 &canonical_headers,
163 &signed_headers,
164 &digest,
165 );
166
167 let signature = self
168 .credential
169 .sign(&string_to_sign, date, self.region, self.service);
170
171 let authorisation = format!(
172 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
173 ALGORITHM, self.credential.key_id, scope, signed_headers, signature
174 );
175
176 let authorization_val = HeaderValue::from_str(&authorisation)?;
177 request
178 .headers_mut()
179 .insert(&AUTHORIZATION, authorization_val);
180 Ok(())
181 }
182
183 #[allow(clippy::too_many_arguments)]
184 fn string_to_sign(
185 &self,
186 date: DateTime<Utc>,
187 scope: &str,
188 request_method: &Method,
189 url: &Url,
190 canonical_headers: &str,
191 signed_headers: &str,
192 digest: &str,
193 ) -> String {
194 let canonical_uri = match self.service {
198 "s3" => url.path().to_string(),
199 _ => utf8_percent_encode(url.path(), &STRICT_PATH_ENCODE_SET).to_string(),
200 };
201
202 let canonical_query = canonicalize_query(url);
203
204 let canonical_request = format!(
205 "{}\n{}\n{}\n{}\n{}\n{}",
206 request_method.as_str(),
207 canonical_uri,
208 canonical_query,
209 canonical_headers,
210 signed_headers,
211 digest
212 );
213
214 let hashed_canonical_request = hex_digest(canonical_request.as_bytes());
215
216 format!(
217 "{}\n{}\n{}\n{}",
218 ALGORITHM,
219 date.format("%Y%m%dT%H%M%SZ"),
220 scope,
221 hashed_canonical_request
222 )
223 }
224
225 fn scope(&self, date: DateTime<Utc>) -> String {
226 format!(
227 "{}/{}/{}/aws4_request",
228 date.format("%Y%m%d"),
229 self.region,
230 self.service
231 )
232 }
233}
234
235pub(crate) trait CredentialExt {
236 fn with_aws_sigv4(
249 self,
250 authorizer: Option<AwsAuthorizer<'_>>,
251 payload_sha256: Option<&[u8]>,
252 ) -> Self;
253}
254
255impl CredentialExt for RequestBuilder {
256 fn with_aws_sigv4(
257 self,
258 authorizer: Option<AwsAuthorizer<'_>>,
259 payload_sha256: Option<&[u8]>,
260 ) -> Self {
261 match authorizer {
262 Some(authorizer) => {
263 let (client, request) = self.build_split();
264 let mut request = request.expect("request valid");
265 authorizer
266 .authorize(&mut request, payload_sha256)
267 .expect("credential values must be valid header characters");
268
269 Self::from_parts(client, request)
270 }
271 None => self,
272 }
273 }
274}
275
276fn canonicalize_query(url: &Url) -> String {
280 use std::fmt::Write;
281
282 let capacity = match url.query() {
283 Some(q) if !q.is_empty() => q.len(),
284 _ => return String::new(),
285 };
286 let mut encoded = String::with_capacity(capacity + 1);
287
288 let mut headers = url.query_pairs().collect::<Vec<_>>();
289 headers.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
290
291 let mut first = true;
292 for (k, v) in headers {
293 if !first {
294 encoded.push('&');
295 }
296 first = false;
297 let _ = write!(
298 encoded,
299 "{}={}",
300 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
301 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
302 );
303 }
304 encoded
305}
306
307fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
311 let mut headers = BTreeMap::<&str, Vec<String>>::new();
314 let mut value_count = 0;
315 let mut value_bytes = 0;
316 let mut key_bytes = 0;
317
318 for (key, value) in header_map {
319 let key = key.as_str();
320 if ["authorization", "content-length", "user-agent"].contains(&key) {
321 continue;
322 }
323
324 let value = String::from_utf8_lossy(value.as_bytes()).into_owned();
325 key_bytes += key.len();
326 value_bytes += value.len();
327 value_count += 1;
328 headers.entry(key).or_default().push(value);
329 }
330
331 let mut signed_headers = String::with_capacity(key_bytes + headers.len());
332 let mut canonical_headers =
333 String::with_capacity(key_bytes + value_bytes + headers.len() + value_count);
334
335 for (header_idx, (name, values)) in headers.into_iter().enumerate() {
336 if header_idx != 0 {
337 signed_headers.push(';');
338 }
339
340 signed_headers.push_str(name);
341 canonical_headers.push_str(name);
342 canonical_headers.push(':');
343 for (value_idx, value) in values.into_iter().enumerate() {
344 if value_idx != 0 {
345 canonical_headers.push(',');
346 }
347 canonical_headers.push_str(value.trim());
348 }
349 canonical_headers.push('\n');
350 }
351
352 (signed_headers, canonical_headers)
353}
354
355#[derive(Debug)]
365pub(crate) struct InstanceCredentialProvider {
366 pub imdsv1_fallback: bool,
367 pub metadata_endpoint: String,
368}
369
370#[async_trait]
371impl TokenProvider for InstanceCredentialProvider {
372 type Credential = AwsCredential;
373
374 async fn fetch_token(
385 &self,
386 client: &Client,
387 service: &Arc<dyn HttpService>,
388 retry: &RetryConfig,
389 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
390 instance_creds(
391 client,
392 service,
393 retry,
394 &self.metadata_endpoint,
395 self.imdsv1_fallback,
396 )
397 .await
398 .map_err(|source| crate::Error::Generic { source })
399 }
400}
401
402#[derive(Debug)]
414pub(crate) struct WebIdentityProvider {
415 pub token_path: String,
416 pub role_arn: String,
417 pub session_name: String,
418 pub endpoint: String,
419}
420
421#[async_trait]
422impl TokenProvider for WebIdentityProvider {
423 type Credential = AwsCredential;
424
425 async fn fetch_token(
436 &self,
437 client: &Client,
438 service: &Arc<dyn HttpService>,
439 retry: &RetryConfig,
440 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
441 web_identity(
442 client,
443 service,
444 retry,
445 &self.token_path,
446 &self.role_arn,
447 &self.session_name,
448 &self.endpoint,
449 )
450 .await
451 .map_err(|source| crate::Error::Generic { source })
452 }
453}
454
455#[derive(Debug, Deserialize)]
456#[serde(rename_all = "PascalCase")]
457struct InstanceCredentials {
458 access_key_id: String,
459 secret_access_key: String,
460 token: String,
461 expiration: DateTime<Utc>,
462}
463
464impl From<InstanceCredentials> for AwsCredential {
465 fn from(s: InstanceCredentials) -> Self {
466 Self {
467 key_id: s.access_key_id,
468 secret_key: s.secret_access_key,
469 token: Some(s.token),
470 }
471 }
472}
473
474async fn instance_creds(
476 client: &Client,
477 service: &Arc<dyn HttpService>,
478 retry_config: &RetryConfig,
479 endpoint: &str,
480 imdsv1_fallback: bool,
481) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
482 const CREDENTIALS_PATH: &str = "latest/meta-data/iam/security-credentials";
483 const AWS_EC2_METADATA_TOKEN_HEADER: &str = "X-aws-ec2-metadata-token";
484
485 let token_url = format!("{endpoint}/latest/api/token");
486
487 let token_result = client
488 .request(Method::PUT, token_url)
489 .header("X-aws-ec2-metadata-token-ttl-seconds", "600") .retryable(retry_config, service.clone())
491 .idempotent(true)
492 .send()
493 .await;
494
495 let token = match token_result {
496 Ok(t) => Some(t.text().await?),
497 Err(e) if imdsv1_fallback && matches!(e.status(), Some(StatusCode::FORBIDDEN)) => {
498 warn!("received 403 from metadata endpoint, falling back to IMDSv1");
499 None
500 }
501 Err(e) => return Err(e.into()),
502 };
503
504 let role_url = format!("{endpoint}/{CREDENTIALS_PATH}/");
505 let mut role_request = client.request(Method::GET, role_url);
506
507 if let Some(token) = &token {
508 role_request = role_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
509 }
510
511 let role = role_request
512 .send_retry(retry_config, service.clone())
513 .await?
514 .text()
515 .await?;
516
517 let creds_url = format!("{endpoint}/{CREDENTIALS_PATH}/{role}");
518 let mut creds_request = client.request(Method::GET, creds_url);
519 if let Some(token) = &token {
520 creds_request = creds_request.header(AWS_EC2_METADATA_TOKEN_HEADER, token);
521 }
522
523 let creds: InstanceCredentials = creds_request
524 .send_retry(retry_config, service.clone())
525 .await?
526 .json()
527 .await?;
528
529 let now = Utc::now();
530 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
531 Ok(TemporaryToken {
532 token: Arc::new(creds.into()),
533 expiry: Some(Instant::now() + ttl),
534 })
535}
536
537#[derive(Debug)]
549pub(crate) struct AssumeRoleProvider {
550 pub role_arn: String,
551 pub session_name: String,
552 pub endpoint: String,
553 pub base_credentials: Arc<dyn CredentialProvider<Credential = AwsCredential>>,
554 pub region: String,
555 pub policy: Option<String>,
558}
559
560#[async_trait]
561impl TokenProvider for AssumeRoleProvider {
562 type Credential = AwsCredential;
563
564 async fn fetch_token(
575 &self,
576 client: &Client,
577 service: &Arc<dyn HttpService>,
578 retry: &RetryConfig,
579 ) -> Result<TemporaryToken<Arc<AwsCredential>>> {
580 let base_cred = self
581 .base_credentials
582 .get_credential()
583 .await
584 .map_err(|source| crate::Error::Generic {
585 source: Box::new(source),
586 })?;
587
588 assume_role(
589 client,
590 service,
591 retry,
592 &base_cred,
593 &self.region,
594 &self.role_arn,
595 &self.session_name,
596 &self.endpoint,
597 self.policy.as_deref(),
598 )
599 .await
600 .map_err(|source| crate::Error::Generic { source })
601 }
602}
603
604#[allow(clippy::too_many_arguments)]
611async fn assume_role(
612 client: &Client,
613 service: &Arc<dyn HttpService>,
614 retry_config: &RetryConfig,
615 base_cred: &AwsCredential,
616 region: &str,
617 role_arn: &str,
618 session_name: &str,
619 endpoint: &str,
620 policy: Option<&str>,
621) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
622 let params = AssumeRoleParams {
623 role_arn,
624 session_name,
625 policy,
626 ..AssumeRoleParams::default()
627 };
628 let (credential, expiration) = assume_role_request(
629 client,
630 service,
631 retry_config,
632 base_cred,
633 region,
634 endpoint,
635 ¶ms,
636 )
637 .await?;
638 let ttl = (expiration - Utc::now()).to_std().unwrap_or_default();
639 Ok(TemporaryToken {
640 token: Arc::new(credential),
641 expiry: Some(Instant::now() + ttl),
642 })
643}
644
645#[derive(Debug, Default)]
647pub(crate) struct AssumeRoleParams<'a> {
648 pub role_arn: &'a str,
649 pub session_name: &'a str,
650 pub duration_secs: Option<u64>,
652 pub policy: Option<&'a str>,
653 pub external_id: Option<&'a str>,
654 pub source_identity: Option<&'a str>,
655 pub tags: &'a [(String, String)],
656}
657
658pub(crate) async fn assume_role_request(
660 client: &Client,
661 service: &Arc<dyn HttpService>,
662 retry_config: &RetryConfig,
663 base_cred: &AwsCredential,
664 region: &str,
665 endpoint: &str,
666 params: &AssumeRoleParams<'_>,
667) -> Result<(AwsCredential, DateTime<Utc>), StdError> {
668 let duration = params.duration_secs.unwrap_or(3600).to_string();
669 let mut form: Vec<(String, &str)> = vec![
670 ("Action".into(), "AssumeRole"),
671 ("DurationSeconds".into(), &duration),
672 ("RoleArn".into(), params.role_arn),
673 ("RoleSessionName".into(), params.session_name),
674 ("Version".into(), "2011-06-15"),
675 ];
676 if let Some(policy) = params.policy {
677 form.push(("Policy".into(), policy));
678 }
679 if let Some(external_id) = params.external_id {
680 form.push(("ExternalId".into(), external_id));
681 }
682 if let Some(source_identity) = params.source_identity {
683 form.push(("SourceIdentity".into(), source_identity));
684 }
685 for (i, (key, value)) in params.tags.iter().enumerate() {
686 let n = i + 1;
687 form.push((format!("Tags.member.{n}.Key"), key));
688 form.push((format!("Tags.member.{n}.Value"), value));
689 }
690 let bytes = client
694 .request(Method::POST, endpoint)
695 .form(&form)
696 .with_aws_sigv4(Some(AwsAuthorizer::new(base_cred, "sts", region)), None)
697 .retryable(retry_config, service.clone())
698 .idempotent(true)
699 .send()
700 .await?
701 .bytes()
702 .await?;
703
704 let resp: AssumeRoleXmlResponse = quick_xml::de::from_reader(bytes.reader())
705 .map_err(|e| format!("Invalid AssumeRole response: {e}"))?;
706 let creds = resp.assume_role_result.credentials;
707 let expiration = creds.expiration;
708 Ok((creds.into(), expiration))
709}
710
711#[derive(Debug, Deserialize)]
713#[serde(rename_all = "PascalCase")]
714struct AssumeRoleXmlResponse {
715 assume_role_result: AssumeRoleResult,
716}
717
718#[derive(Debug, Deserialize)]
719#[serde(rename_all = "PascalCase")]
720struct AssumeRoleResponse {
721 assume_role_with_web_identity_result: AssumeRoleResult,
722}
723
724#[derive(Debug, Deserialize)]
725#[serde(rename_all = "PascalCase")]
726struct AssumeRoleResult {
727 credentials: SessionCredentials,
728}
729
730#[derive(Debug, Deserialize)]
731#[serde(rename_all = "PascalCase")]
732struct SessionCredentials {
733 session_token: String,
734 secret_access_key: String,
735 access_key_id: String,
736 expiration: DateTime<Utc>,
737}
738
739impl From<SessionCredentials> for AwsCredential {
740 fn from(s: SessionCredentials) -> Self {
741 Self {
742 key_id: s.access_key_id,
743 secret_key: s.secret_access_key,
744 token: Some(s.session_token),
745 }
746 }
747}
748
749async fn web_identity(
751 client: &Client,
752 service: &Arc<dyn HttpService>,
753 retry_config: &RetryConfig,
754 token_path: &str,
755 role_arn: &str,
756 session_name: &str,
757 endpoint: &str,
758) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
759 let token = std::fs::read_to_string(token_path)
760 .map_err(|e| format!("Failed to read token file '{token_path}': {e}"))?;
761
762 let bytes = client
763 .request(Method::POST, endpoint)
764 .query(&[
765 ("Action", "AssumeRoleWithWebIdentity"),
766 ("DurationSeconds", "3600"),
767 ("RoleArn", role_arn),
768 ("RoleSessionName", session_name),
769 ("Version", "2011-06-15"),
770 ("WebIdentityToken", &token),
771 ])
772 .retryable(retry_config, service.clone())
773 .idempotent(true)
774 .sensitive(true)
775 .send()
776 .await?
777 .bytes()
778 .await?;
779
780 let resp: AssumeRoleResponse = quick_xml::de::from_reader(bytes.reader())
781 .map_err(|e| format!("Invalid AssumeRoleWithWebIdentity response: {e}"))?;
782
783 let creds = resp.assume_role_with_web_identity_result.credentials;
784 let now = Utc::now();
785 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
786
787 Ok(TemporaryToken {
788 token: Arc::new(creds.into()),
789 expiry: Some(Instant::now() + ttl),
790 })
791}
792
793#[derive(Debug)]
812pub(crate) struct TaskCredentialProvider {
813 pub url: String,
814 pub auth_token_file: Option<String>,
817 pub retry: RetryConfig,
818 pub client: Client,
819 pub service: Arc<dyn HttpService>,
820 pub cache: TokenCache<Arc<AwsCredential>>,
821}
822
823#[async_trait]
824impl CredentialProvider for TaskCredentialProvider {
825 type Credential = AwsCredential;
826
827 async fn get_credential(&self) -> Result<Arc<AwsCredential>> {
828 self.cache
829 .get_or_insert_with(|| {
830 task_credential(
831 &self.client,
832 &self.service,
833 &self.retry,
834 &self.url,
835 self.auth_token_file.as_deref(),
836 )
837 })
838 .await
839 .map_err(|source| crate::Error::Generic { source })
840 }
841}
842
843async fn task_credential(
845 client: &Client,
846 service: &Arc<dyn HttpService>,
847 retry: &RetryConfig,
848 url: &str,
849 auth_token_file: Option<&str>,
850) -> Result<TemporaryToken<Arc<AwsCredential>>, StdError> {
851 let mut req = client.get(url);
852
853 if let Some(token_file) = auth_token_file {
855 let token = std::fs::read_to_string(token_file)
856 .map_err(|e| format!("Failed to read auth token file '{token_file}': {e}"))?;
857 req = req.header(reqwest::header::AUTHORIZATION, token.trim());
858 }
859
860 let creds: InstanceCredentials = req.send_retry(retry, service.clone()).await?.json().await?;
861
862 let now = Utc::now();
863 let ttl = (creds.expiration - now).to_std().unwrap_or_default();
864 Ok(TemporaryToken {
865 token: Arc::new(creds.into()),
866 expiry: Some(Instant::now() + ttl),
867 })
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use crate::service::ReqwestService;
874 use reqwest::{Client, Method};
875 use std::env;
876
877 #[test]
878 fn test_debug_does_not_leak_secrets() {
879 let credential = AwsCredential {
883 key_id: "fake-key-id-do-not-print".to_string(),
884 secret_key: "fake-secret-do-not-print".to_string(),
885 token: Some("fake-session-token-do-not-print".to_string()),
886 };
887 let rendered = format!("{credential:?}");
888 assert!(
889 !rendered.contains("fake-key-id-do-not-print"),
890 "key_id leaked: {rendered}"
891 );
892 assert!(
893 !rendered.contains("fake-secret-do-not-print"),
894 "secret_key leaked: {rendered}"
895 );
896 assert!(
897 !rendered.contains("fake-session-token-do-not-print"),
898 "session token leaked: {rendered}"
899 );
900 assert!(rendered.contains("<redacted>"));
901 }
902
903 #[test]
904 fn test_sign_with_signed_payload() {
905 let client = Client::new();
906
907 let credential = AwsCredential {
908 key_id: "AKIAIOSFODNN7EXAMPLE".to_string(), secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), token: None,
914 };
915
916 let date = DateTime::parse_from_rfc3339("2022-08-06T18:01:34Z")
917 .unwrap()
918 .with_timezone(&Utc);
919
920 let mut request = client
921 .request(Method::GET, "https://ec2.amazon.com/")
922 .build()
923 .unwrap();
924
925 let signer = AwsAuthorizer {
926 date: Some(date),
927 credential: &credential,
928 service: "ec2",
929 region: "us-east-1",
930 };
931
932 signer.authorize(&mut request, None).unwrap();
933 assert_eq!(
934 request.headers().get(&AUTHORIZATION).unwrap(),
935 "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4" )
937 }
939
940 #[test]
941 fn test_sign_port() {
942 let client = Client::new();
943
944 let credential = AwsCredential {
945 key_id: "H20ABqCkLZID4rLe".to_string(),
946 secret_key: "jMqRDgxSsBqqznfmddGdu1TmmZOJQxdM".to_string(),
947 token: None,
948 };
949
950 let date = DateTime::parse_from_rfc3339("2022-08-09T13:05:25Z")
951 .unwrap()
952 .with_timezone(&Utc);
953
954 let mut request = client
955 .request(Method::GET, "http://localhost:9000/tsm-schemas")
956 .query(&[
957 ("delimiter", "/"),
958 ("encoding-type", "url"),
959 ("list-type", "2"),
960 ("prefix", ""),
961 ])
962 .build()
963 .unwrap();
964
965 let authorizer = AwsAuthorizer {
966 date: Some(date),
967 credential: &credential,
968 service: "s3",
969 region: "us-east-1",
970 };
971
972 authorizer.authorize(&mut request, None).unwrap();
973 assert_eq!(
974 request.headers().get(&AUTHORIZATION).unwrap(),
975 "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d"
976 )
977 }
978
979 #[tokio::test]
980 async fn test_instance_metadata() {
981 if env::var("TEST_INTEGRATION").is_err() {
982 eprintln!("skipping AWS integration test");
983 return;
984 }
985
986 let endpoint = env::var("EC2_METADATA_ENDPOINT").unwrap();
987 let client = Client::new();
988 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
989 let retry_config = RetryConfig::default();
990
991 let resp = client
992 .request(Method::GET, format!("{endpoint}/latest/meta-data/ami-id"))
993 .send()
994 .await
995 .unwrap();
996
997 assert_eq!(
998 resp.status(),
999 StatusCode::UNAUTHORIZED,
1000 "Ensure metadata endpoint is set to only allow IMDSv2"
1001 );
1002
1003 let creds = instance_creds(&client, &service, &retry_config, &endpoint, false)
1004 .await
1005 .unwrap();
1006
1007 let id = &creds.token.key_id;
1008 let secret = &creds.token.secret_key;
1009 let token = creds.token.token.as_ref().unwrap();
1010
1011 assert!(!id.is_empty());
1012 assert!(!secret.is_empty());
1013 assert!(!token.is_empty())
1014 }
1015 #[tokio::test]
1016 async fn test_mock() {
1017 let mut server = mockito::Server::new_async().await;
1018
1019 const IMDSV2_HEADER: &str = "X-aws-ec2-metadata-token";
1020
1021 let secret_access_key = "SECRET";
1022 let access_key_id = "KEYID";
1023 let token = "TOKEN";
1024
1025 let endpoint = server.url();
1026 let client = Client::new();
1027 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1028 let retry_config = RetryConfig::default();
1029
1030 let _mock1 = server
1032 .mock("PUT", "/latest/api/token")
1033 .with_status(200)
1034 .with_body("cupcakes")
1035 .create_async()
1036 .await;
1037
1038 let _mock2 = server
1039 .mock("GET", "/latest/meta-data/iam/security-credentials/")
1040 .match_header(IMDSV2_HEADER, "cupcakes")
1041 .with_status(200)
1042 .with_body("myrole")
1043 .create_async()
1044 .await;
1045
1046 let _mock3 = server
1047 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
1048 .match_header(IMDSV2_HEADER, "cupcakes")
1049 .with_status(200)
1050 .with_body(r#"{"AccessKeyId":"KEYID","Code":"Success","Expiration":"2022-08-30T10:51:04Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"SECRET","Token":"TOKEN","Type":"AWS-HMAC"}"#)
1051 .create_async()
1052 .await;
1053
1054 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
1055 .await
1056 .unwrap();
1057
1058 assert_eq!(creds.token.token.as_deref().unwrap(), token);
1059 assert_eq!(&creds.token.key_id, access_key_id);
1060 assert_eq!(&creds.token.secret_key, secret_access_key);
1061
1062 let _mock4 = server
1064 .mock("PUT", "/latest/api/token")
1065 .with_status(403)
1066 .with_body("")
1067 .create_async()
1068 .await;
1069
1070 let _mock5 = server
1071 .mock("GET", "/latest/meta-data/iam/security-credentials/")
1072 .with_status(200)
1073 .with_body("myrole")
1074 .create_async()
1075 .await;
1076
1077 let _mock6 = server
1078 .mock("GET", "/latest/meta-data/iam/security-credentials/myrole")
1079 .with_status(200)
1080 .with_body(r#"{"AccessKeyId":"KEYID","Code":"Success","Expiration":"2022-08-30T10:51:04Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"SECRET","Token":"TOKEN","Type":"AWS-HMAC"}"#)
1081 .create_async()
1082 .await;
1083
1084 let creds = instance_creds(&client, &service, &retry_config, &endpoint, true)
1085 .await
1086 .unwrap();
1087
1088 assert_eq!(creds.token.token.as_deref().unwrap(), token);
1089 assert_eq!(&creds.token.key_id, access_key_id);
1090 assert_eq!(&creds.token.secret_key, secret_access_key);
1091
1092 let _mock7 = server
1094 .mock("PUT", "/latest/api/token")
1095 .with_status(403)
1096 .with_body("")
1097 .create_async()
1098 .await;
1099
1100 instance_creds(&client, &service, &retry_config, &endpoint, false)
1102 .await
1103 .unwrap_err();
1104 }
1105
1106 const STS_WEB_IDENTITY_XML: &str = r#"<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"><AssumeRoleWithWebIdentityResult><Credentials><AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId><SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</SecretAccessKey><SessionToken>FwoGZXIvYXdzEJr//////////wEaDHer8</SessionToken><Expiration>2099-01-01T00:00:00Z</Expiration></Credentials></AssumeRoleWithWebIdentityResult></AssumeRoleWithWebIdentityResponse>"#; const STS_ASSUME_ROLE_XML: &str = r#"<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"><AssumeRoleResult><Credentials><AccessKeyId>AKIAIOSFODNN7EXAMPLE</AccessKeyId><SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</SecretAccessKey><SessionToken>FwoGZXIvYXdzEJr//////////wEaDHer8</SessionToken><Expiration>2099-01-01T00:00:00Z</Expiration></Credentials></AssumeRoleResult></AssumeRoleResponse>"#; #[tokio::test]
1111 async fn test_web_identity_provider() {
1112 use std::io::Write as _;
1113 let mut server = mockito::Server::new_async().await;
1114
1115 let mut token_file = tempfile::NamedTempFile::new().unwrap();
1117 write!(token_file, "fake-jwt-token").unwrap();
1118
1119 let sts_url = server.url();
1120
1121 let _mock = server
1122 .mock("POST", "/")
1123 .match_query(mockito::Matcher::AllOf(vec![
1124 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRoleWithWebIdentity".into()),
1125 mockito::Matcher::UrlEncoded(
1126 "RoleArn".into(),
1127 "arn:aws:iam::123456789012:role/TestRole".into(),
1128 ),
1129 mockito::Matcher::UrlEncoded("WebIdentityToken".into(), "fake-jwt-token".into()),
1130 ]))
1131 .with_status(200)
1132 .with_body(STS_WEB_IDENTITY_XML)
1133 .create_async()
1134 .await;
1135
1136 let client = Client::new();
1137 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1138 let retry_config = RetryConfig::default();
1139
1140 let provider = WebIdentityProvider {
1141 token_path: token_file.path().to_str().unwrap().to_owned(),
1142 role_arn: "arn:aws:iam::123456789012:role/TestRole".into(),
1143 session_name: "test-session".into(),
1144 endpoint: sts_url,
1145 };
1146
1147 let creds = provider
1148 .fetch_token(&client, &service, &retry_config)
1149 .await
1150 .unwrap();
1151
1152 assert_eq!(creds.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(creds.token.token.is_some());
1154 assert!(creds.expiry.is_some());
1155
1156 _mock.assert_async().await;
1157 }
1158
1159 #[tokio::test]
1160 async fn test_task_credential_provider() {
1161 let mut server = mockito::Server::new_async().await;
1162
1163 let endpoint = server.url();
1164
1165 let _mock = server
1166 .mock("GET", "/v2/credentials/test")
1167 .with_status(200)
1168 .with_body(r#"{"AccessKeyId":"TASKID","Code":"Success","Expiration":"2099-01-01T00:00:00Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"TASKSECRET","Token":"TASKTOKEN","Type":"AWS-HMAC"}"#)
1169 .create_async()
1170 .await;
1171
1172 let client = Client::new();
1173 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1174 let retry = RetryConfig::default();
1175
1176 let provider = TaskCredentialProvider {
1177 url: format!("{endpoint}/v2/credentials/test"),
1178 auth_token_file: None,
1179 retry: retry.clone(),
1180 client: client.clone(),
1181 service: service.clone(),
1182 cache: Default::default(),
1183 };
1184
1185 let creds = provider.get_credential().await.unwrap();
1186
1187 assert_eq!(creds.key_id, "TASKID");
1188 assert_eq!(creds.secret_key, "TASKSECRET");
1189 assert_eq!(creds.token.as_deref(), Some("TASKTOKEN"));
1190
1191 _mock.assert_async().await;
1192 }
1193
1194 #[tokio::test]
1195 async fn test_task_credential_with_auth_token() {
1196 use std::io::Write as _;
1197 let mut server = mockito::Server::new_async().await;
1198
1199 let mut auth_file = tempfile::NamedTempFile::new().unwrap();
1201 write!(auth_file, "my-pod-identity-token").unwrap();
1202
1203 let endpoint = server.url();
1204
1205 let _mock = server
1206 .mock("GET", "/v2/credentials/eks")
1207 .match_header("Authorization", "my-pod-identity-token")
1208 .with_status(200)
1209 .with_body(r#"{"AccessKeyId":"EKSID","Code":"Success","Expiration":"2099-01-01T00:00:00Z","LastUpdated":"2022-08-30T10:21:04Z","SecretAccessKey":"EKSSECRET","Token":"EKSTOKEN","Type":"AWS-HMAC"}"#)
1210 .create_async()
1211 .await;
1212
1213 let client = Client::new();
1214 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1215 let retry = RetryConfig::default();
1216
1217 let provider = TaskCredentialProvider {
1218 url: format!("{endpoint}/v2/credentials/eks"),
1219 auth_token_file: Some(auth_file.path().to_str().unwrap().to_owned()),
1220 retry: retry.clone(),
1221 client: client.clone(),
1222 service: service.clone(),
1223 cache: Default::default(),
1224 };
1225
1226 let creds = provider.get_credential().await.unwrap();
1227
1228 assert_eq!(creds.key_id, "EKSID");
1229 assert_eq!(creds.token.as_deref(), Some("EKSTOKEN"));
1230
1231 _mock.assert_async().await;
1232 }
1233
1234 #[tokio::test]
1235 async fn test_assume_role_provider() {
1236 use crate::StaticCredentialProvider;
1237 let mut server = mockito::Server::new_async().await;
1238
1239 let sts_endpoint = server.url();
1240
1241 let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
1242 let _mock = server
1243 .mock("POST", "/")
1244 .match_query(mockito::Matcher::Missing)
1245 .match_header("content-type", "application/x-www-form-urlencoded")
1246 .match_header(
1247 "authorization",
1248 mockito::Matcher::Regex("^AWS4-HMAC-SHA256 Credential=BASEKEYID/".into()),
1249 )
1250 .match_body(mockito::Matcher::AllOf(vec![
1251 mockito::Matcher::UrlEncoded("Action".into(), "AssumeRole".into()),
1252 mockito::Matcher::UrlEncoded(
1253 "RoleArn".into(),
1254 "arn:aws:iam::123456789012:role/AssumedRole".into(),
1255 ),
1256 mockito::Matcher::UrlEncoded("Policy".into(), policy.into()),
1257 ]))
1258 .with_status(200)
1259 .with_body(STS_ASSUME_ROLE_XML)
1260 .create_async()
1261 .await;
1262
1263 let base_cred = AwsCredential {
1264 key_id: "BASEKEYID".to_string(),
1265 secret_key: "BASESECRET".to_string(),
1266 token: None,
1267 };
1268 let base_provider: Arc<dyn CredentialProvider<Credential = AwsCredential>> =
1269 Arc::new(StaticCredentialProvider::new(base_cred));
1270
1271 let client = Client::new();
1272 let service: Arc<dyn HttpService> = Arc::new(ReqwestService::new(client.clone()));
1273 let retry_config = RetryConfig::default();
1274
1275 let provider = AssumeRoleProvider {
1276 role_arn: "arn:aws:iam::123456789012:role/AssumedRole".into(),
1277 session_name: "test-assume-session".into(),
1278 endpoint: format!("{sts_endpoint}/"),
1279 base_credentials: base_provider,
1280 region: "us-east-1".into(),
1281 policy: Some(policy.into()),
1282 };
1283
1284 let token = provider
1285 .fetch_token(&client, &service, &retry_config)
1286 .await
1287 .unwrap();
1288
1289 assert_eq!(token.token.key_id, "AKIAIOSFODNN7EXAMPLE"); assert!(token.token.token.is_some());
1291 assert!(token.expiry.is_some());
1292
1293 _mock.assert_async().await;
1294 }
1295}