1use super::client::GoogleCloudStorageClient;
19use crate::client::builder::HttpRequestBuilder;
20use crate::client::retry::RetryExt;
21use crate::client::token::TemporaryToken;
22use crate::client::{
23 CryptoProvider, HttpClient, HttpError, Signer, SigningAlgorithm, TokenProvider,
24};
25use crate::gcp::{GcpSigningCredentialProvider, STORE};
26use crate::util::{STRICT_ENCODE_SET, append_strict_query_pairs, hex_digest, hex_encode};
27use crate::{RetryConfig, StaticCredentialProvider};
28use async_trait::async_trait;
29use base64::Engine;
30use base64::prelude::BASE64_URL_SAFE_NO_PAD;
31use chrono::{DateTime, Utc};
32use futures_util::TryFutureExt;
33use http::{HeaderMap, Method};
34use itertools::Itertools;
35use percent_encoding::utf8_percent_encode;
36use serde::Deserialize;
37use std::collections::BTreeMap;
38use std::env;
39use std::fs::File;
40use std::io::BufReader;
41use std::path::{Path, PathBuf};
42use std::sync::Arc;
43use std::time::{Duration, Instant};
44use tracing::info;
45use url::Url;
46
47pub(crate) const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
48
49pub(crate) const DEFAULT_GCS_BASE_URL: &str = "https://storage.googleapis.com";
50
51const DEFAULT_GCS_PLAYLOAD_STRING: &str = "UNSIGNED-PAYLOAD";
52const DEFAULT_GCS_SIGN_BLOB_HOST: &str = "storage.googleapis.com";
53
54const DEFAULT_METADATA_HOST: &str = "metadata.google.internal";
55const DEFAULT_METADATA_IP: &str = "169.254.169.254";
56
57#[derive(Debug, thiserror::Error)]
58pub(super) enum Error {
59 #[error("Unable to open service account file from {}: {}", path.display(), source)]
60 OpenCredentials {
61 source: std::io::Error,
62 path: PathBuf,
63 },
64
65 #[error("Unable to decode service account file: {}", source)]
66 DecodeCredentials { source: serde_json::Error },
67
68 #[error("Error encoding jwt payload: {}", source)]
69 Encode { source: serde_json::Error },
70
71 #[error("Error performing token request: {}", source)]
72 TokenRequest {
73 source: crate::client::retry::RetryError,
74 },
75
76 #[error("Error getting token response body: {}", source)]
77 TokenResponseBody { source: HttpError },
78}
79
80impl From<Error> for crate::Error {
81 fn from(value: Error) -> Self {
82 Self::Generic {
83 store: STORE,
84 source: Box::new(value),
85 }
86 }
87}
88
89#[derive(Debug)]
91pub struct GcpSigningCredential {
92 pub email: String,
94
95 pub private_key: Option<ServiceAccountKey>,
104}
105
106pub struct ServiceAccountKey(Box<dyn Signer>);
108
109impl std::fmt::Debug for ServiceAccountKey {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_tuple("ServiceAccountKey").finish_non_exhaustive()
112 }
113}
114
115impl ServiceAccountKey {
116 pub fn new(signer: Box<dyn Signer>) -> Self {
118 Self(signer)
119 }
120
121 #[cfg(feature = "aws-lc-rs")]
123 pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
124 let key = crate::client::aws_lc_rs::RsaKeyPair::from_pem(encoded)?;
125 Ok(Self::new(Box::new(key)))
126 }
127
128 #[cfg(feature = "aws-lc-rs")]
130 pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
131 let key = crate::client::aws_lc_rs::RsaKeyPair::from_pkcs8(key)?;
132 Ok(Self::new(Box::new(key)))
133 }
134
135 #[cfg(feature = "aws-lc-rs")]
137 pub fn from_der(key: &[u8]) -> crate::Result<Self> {
138 let key = crate::client::aws_lc_rs::RsaKeyPair::from_der(key)?;
139 Ok(Self::new(Box::new(key)))
140 }
141
142 #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
144 pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
145 let key = crate::client::ring::RsaKeyPair::from_pem(encoded)?;
146 Ok(Self::new(Box::new(key)))
147 }
148
149 #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
151 pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
152 let key = crate::client::ring::RsaKeyPair::from_pkcs8(key)?;
153 Ok(Self::new(Box::new(key)))
154 }
155
156 #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
158 pub fn from_der(key: &[u8]) -> crate::Result<Self> {
159 let key = crate::client::ring::RsaKeyPair::from_der(key)?;
160 Ok(Self::new(Box::new(key)))
161 }
162
163 fn sign(&self, string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
164 self.0.sign(string_to_sign)
165 }
166}
167
168#[derive(Debug, Eq, PartialEq)]
170pub struct GcpCredential {
171 pub bearer: String,
173}
174
175pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
176
177#[derive(Debug, Default, serde::Serialize)]
178pub(crate) struct JwtHeader<'a> {
179 #[serde(skip_serializing_if = "Option::is_none")]
183 pub typ: Option<&'a str>,
184 pub alg: &'a str,
188 #[serde(skip_serializing_if = "Option::is_none")]
192 pub cty: Option<&'a str>,
193 #[serde(skip_serializing_if = "Option::is_none")]
197 pub jku: Option<&'a str>,
198 #[serde(skip_serializing_if = "Option::is_none")]
202 pub kid: Option<&'a str>,
203 #[serde(skip_serializing_if = "Option::is_none")]
207 pub x5u: Option<&'a str>,
208 #[serde(skip_serializing_if = "Option::is_none")]
212 pub x5t: Option<&'a str>,
213}
214
215#[derive(serde::Serialize)]
216struct TokenClaims<'a> {
217 iss: &'a str,
218 sub: &'a str,
219 scope: &'a str,
220 exp: u64,
221 iat: u64,
222}
223
224#[derive(serde::Deserialize, Debug)]
225struct TokenResponse {
226 access_token: String,
227 expires_in: u64,
228 id_token: Option<String>,
229}
230
231#[derive(Debug)]
236pub(crate) struct SelfSignedJwt {
237 issuer: String,
238 scope: String,
239 private_key: ServiceAccountKey,
240 key_id: String,
241}
242
243impl SelfSignedJwt {
244 pub(crate) fn new(
246 key_id: String,
247 issuer: String,
248 private_key: ServiceAccountKey,
249 scope: String,
250 ) -> Result<Self> {
251 Ok(Self {
252 issuer,
253 scope,
254 private_key,
255 key_id,
256 })
257 }
258}
259
260#[async_trait]
261impl TokenProvider for SelfSignedJwt {
262 type Credential = GcpCredential;
263
264 async fn fetch_token(
266 &self,
267 _client: &HttpClient,
268 _retry: &RetryConfig,
269 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
270 let now = seconds_since_epoch();
271 let exp = now + 3600;
272
273 let claims = TokenClaims {
274 iss: &self.issuer,
275 sub: &self.issuer,
276 scope: &self.scope,
277 iat: now,
278 exp,
279 };
280
281 let jwt_header = b64_encode_obj(&JwtHeader {
282 alg: "RS256",
283 typ: Some("JWT"),
284 kid: Some(&self.key_id),
285 ..Default::default()
286 })?;
287
288 let claim_str = b64_encode_obj(&claims)?;
289 let message = [jwt_header.as_ref(), claim_str.as_ref()].join(".");
290 let sig_bytes = self.private_key.sign(message.as_bytes())?;
291 let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes);
292 let bearer = [message, signature].join(".");
293
294 Ok(TemporaryToken {
295 token: Arc::new(GcpCredential { bearer }),
296 expiry: Some(Instant::now() + Duration::from_secs(3600)),
297 })
298 }
299}
300
301fn read_credentials_file<T>(service_account_path: impl AsRef<std::path::Path>) -> Result<T>
302where
303 T: serde::de::DeserializeOwned,
304{
305 let file = File::open(&service_account_path).map_err(|source| {
306 let path = service_account_path.as_ref().to_owned();
307 Error::OpenCredentials { source, path }
308 })?;
309 let reader = BufReader::new(file);
310 serde_json::from_reader(reader).map_err(|source| Error::DecodeCredentials { source })
311}
312
313#[derive(serde::Deserialize, Debug, Clone)]
315pub(crate) struct ServiceAccountCredentials {
316 pub private_key: String,
318
319 pub private_key_id: String,
321
322 pub client_email: String,
324
325 #[serde(default)]
327 pub gcs_base_url: Option<String>,
328
329 #[serde(default)]
331 pub disable_oauth: bool,
332}
333
334impl ServiceAccountCredentials {
335 pub(crate) fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
337 read_credentials_file(path)
338 }
339
340 pub(crate) fn from_key(key: &str) -> Result<Self> {
342 serde_json::from_str(key).map_err(|source| Error::DecodeCredentials { source })
343 }
344
345 pub(crate) fn token_provider(
354 self,
355 crypto: &dyn CryptoProvider,
356 ) -> crate::Result<SelfSignedJwt> {
357 let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
358 Ok(SelfSignedJwt::new(
359 self.private_key_id,
360 self.client_email,
361 ServiceAccountKey::new(key),
362 DEFAULT_SCOPE.to_string(),
363 )?)
364 }
365
366 pub(crate) fn signing_credentials(
367 self,
368 crypto: &dyn CryptoProvider,
369 ) -> crate::Result<GcpSigningCredentialProvider> {
370 let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?;
371 Ok(Arc::new(StaticCredentialProvider::new(
372 GcpSigningCredential {
373 email: self.client_email,
374 private_key: Some(ServiceAccountKey::new(key)),
375 },
376 )))
377 }
378}
379
380fn seconds_since_epoch() -> u64 {
382 std::time::SystemTime::now()
383 .duration_since(std::time::SystemTime::UNIX_EPOCH)
384 .unwrap()
385 .as_secs()
386}
387
388fn b64_encode_obj<T: serde::Serialize>(obj: &T) -> Result<String> {
389 let string = serde_json::to_string(obj).map_err(|source| Error::Encode { source })?;
390 Ok(BASE64_URL_SAFE_NO_PAD.encode(string))
391}
392
393#[derive(Debug, Default)]
397pub(crate) struct InstanceCredentialProvider {}
398
399async fn make_metadata_request(
401 client: &HttpClient,
402 hostname: &str,
403 retry: &RetryConfig,
404) -> crate::Result<TokenResponse> {
405 let url =
406 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/token");
407 let response: TokenResponse = client
408 .get(url)
409 .header("Metadata-Flavor", "Google")
410 .query(&[("audience", "https://www.googleapis.com/oauth2/v4/token")])
411 .send_retry(retry)
412 .await
413 .map_err(|source| Error::TokenRequest { source })?
414 .into_body()
415 .json()
416 .await
417 .map_err(|source| Error::TokenResponseBody { source })?;
418 Ok(response)
419}
420
421#[async_trait]
422impl TokenProvider for InstanceCredentialProvider {
423 type Credential = GcpCredential;
424
425 async fn fetch_token(
432 &self,
433 client: &HttpClient,
434 retry: &RetryConfig,
435 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
436 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
437 host
438 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
439 host
440 } else {
441 DEFAULT_METADATA_HOST.to_string()
442 };
443 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
444 ip
445 } else {
446 DEFAULT_METADATA_IP.to_string()
447 };
448
449 info!("fetching token from metadata server");
450 let response = make_metadata_request(client, &metadata_host, retry)
451 .or_else(|_| make_metadata_request(client, &metadata_ip, retry))
452 .await?;
453
454 let token = TemporaryToken {
455 token: Arc::new(GcpCredential {
456 bearer: response.access_token,
457 }),
458 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
459 };
460 Ok(token)
461 }
462}
463
464async fn make_metadata_request_for_email(
466 client: &HttpClient,
467 hostname: &str,
468 retry: &RetryConfig,
469) -> crate::Result<String> {
470 let url =
471 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/email",);
472 let response = client
473 .get(url)
474 .header("Metadata-Flavor", "Google")
475 .send_retry(retry)
476 .await
477 .map_err(|source| Error::TokenRequest { source })?
478 .into_body()
479 .text()
480 .await
481 .map_err(|source| Error::TokenResponseBody { source })?;
482 Ok(response)
483}
484
485#[derive(Debug, Default)]
489pub(crate) struct InstanceSigningCredentialProvider {}
490
491#[async_trait]
492impl TokenProvider for InstanceSigningCredentialProvider {
493 type Credential = GcpSigningCredential;
494
495 async fn fetch_token(
502 &self,
503 client: &HttpClient,
504 retry: &RetryConfig,
505 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
506 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
507 host
508 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
509 host
510 } else {
511 DEFAULT_METADATA_HOST.to_string()
512 };
513
514 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
515 ip
516 } else {
517 DEFAULT_METADATA_IP.to_string()
518 };
519
520 info!("fetching token from metadata server");
521
522 let email = make_metadata_request_for_email(client, &metadata_host, retry)
523 .or_else(|_| make_metadata_request_for_email(client, &metadata_ip, retry))
524 .await?;
525
526 let token = TemporaryToken {
527 token: Arc::new(GcpSigningCredential {
528 email,
529 private_key: None,
530 }),
531 expiry: None,
532 };
533 Ok(token)
534 }
535}
536
537#[derive(serde::Deserialize, Clone)]
543#[serde(tag = "type")]
544pub(crate) enum ApplicationDefaultCredentials {
545 #[serde(rename = "service_account")]
550 ServiceAccount(ServiceAccountCredentials),
551 #[serde(rename = "authorized_user")]
556 AuthorizedUser(AuthorizedUserCredentials),
557}
558
559impl ApplicationDefaultCredentials {
560 const CREDENTIALS_PATH: &'static str = if cfg!(windows) {
561 "gcloud/application_default_credentials.json"
562 } else {
563 ".config/gcloud/application_default_credentials.json"
564 };
565
566 pub(crate) fn read(path: Option<&str>) -> Result<Option<Self>, Error> {
570 if let Some(path) = path {
571 return read_credentials_file::<Self>(path).map(Some);
572 }
573
574 let home_var = if cfg!(windows) { "APPDATA" } else { "HOME" };
575 if let Some(home) = env::var_os(home_var) {
576 let path = Path::new(&home).join(Self::CREDENTIALS_PATH);
577
578 if path.exists() {
580 return read_credentials_file::<Self>(path).map(Some);
581 }
582 }
583 Ok(None)
584 }
585}
586
587const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
588
589#[derive(Debug, Deserialize, Clone)]
591pub(crate) struct AuthorizedUserCredentials {
592 client_id: String,
593 client_secret: String,
594 refresh_token: String,
595}
596
597#[derive(Debug, Deserialize)]
598pub(crate) struct AuthorizedUserSigningCredentials {
599 credential: AuthorizedUserCredentials,
600}
601
602#[derive(Debug, Deserialize)]
604struct EmailResponse {
605 email: String,
606}
607
608#[derive(Debug, Deserialize)]
609struct IdTokenClaims {
610 email: String,
611}
612
613async fn get_token_response(
614 client_id: &str,
615 client_secret: &str,
616 refresh_token: &str,
617 client: &HttpClient,
618 retry: &RetryConfig,
619) -> Result<TokenResponse> {
620 client
621 .post(DEFAULT_TOKEN_GCP_URI)
622 .form([
623 ("grant_type", "refresh_token"),
624 ("client_id", client_id),
625 ("client_secret", client_secret),
626 ("refresh_token", refresh_token),
627 ])
628 .retryable(retry)
629 .idempotent(true)
630 .send()
631 .await
632 .map_err(|source| Error::TokenRequest { source })?
633 .into_body()
634 .json::<TokenResponse>()
635 .await
636 .map_err(|source| Error::TokenResponseBody { source })
637}
638
639impl AuthorizedUserSigningCredentials {
640 pub(crate) fn from(credential: AuthorizedUserCredentials) -> crate::Result<Self> {
641 Ok(Self { credential })
642 }
643
644 async fn client_email(
645 &self,
646 client: &HttpClient,
647 retry: &RetryConfig,
648 ) -> crate::Result<String> {
649 let response = get_token_response(
650 &self.credential.client_id,
651 &self.credential.client_secret,
652 &self.credential.refresh_token,
653 client,
654 retry,
655 )
656 .await?;
657
658 if let Some(id_token) = response.id_token {
660 let parts: Vec<&str> = id_token.split('.').collect();
662 if parts.len() == 3 {
663 if let Ok(payload) = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) {
665 if let Ok(claims) = serde_json::from_slice::<IdTokenClaims>(&payload) {
667 return Ok(claims.email);
668 }
669 }
670 }
672 }
673
674 let response = client
676 .get("https://oauth2.googleapis.com/tokeninfo")
677 .query(&[("access_token", response.access_token)])
678 .send_retry(retry)
679 .await
680 .map_err(|source| Error::TokenRequest { source })?
681 .into_body()
682 .json::<EmailResponse>()
683 .await
684 .map_err(|source: HttpError| Error::TokenResponseBody { source })?;
685
686 Ok(response.email)
687 }
688}
689
690#[async_trait]
691impl TokenProvider for AuthorizedUserSigningCredentials {
692 type Credential = GcpSigningCredential;
693
694 async fn fetch_token(
695 &self,
696 client: &HttpClient,
697 retry: &RetryConfig,
698 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
699 let email = self.client_email(client, retry).await?;
700 Ok(TemporaryToken {
701 token: Arc::new(GcpSigningCredential {
702 email,
703 private_key: None,
704 }),
705 expiry: None,
706 })
707 }
708}
709
710#[async_trait]
711impl TokenProvider for AuthorizedUserCredentials {
712 type Credential = GcpCredential;
713
714 async fn fetch_token(
715 &self,
716 client: &HttpClient,
717 retry: &RetryConfig,
718 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
719 let response = get_token_response(
720 &self.client_id,
721 &self.client_secret,
722 &self.refresh_token,
723 client,
724 retry,
725 )
726 .await?;
727
728 Ok(TemporaryToken {
729 token: Arc::new(GcpCredential {
730 bearer: response.access_token,
731 }),
732 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
733 })
734 }
735}
736
737fn trim_header_value(value: &str) -> String {
743 value.split_whitespace().collect::<Vec<_>>().join(" ")
744}
745
746#[derive(Debug)]
750pub(crate) struct GCSAuthorizer {
751 date: Option<DateTime<Utc>>,
752 credential: Arc<GcpSigningCredential>,
753}
754
755impl GCSAuthorizer {
756 pub(crate) fn new(credential: Arc<GcpSigningCredential>) -> Self {
758 Self {
759 date: None,
760 credential,
761 }
762 }
763
764 #[allow(clippy::too_many_arguments)]
774 pub(crate) async fn sign_with(
775 &self,
776 crypto: &dyn CryptoProvider,
777 method: Method,
778 url: &mut Url,
779 extra_query: &[(String, String)],
780 signed_headers: &HeaderMap,
781 expires_in: Duration,
782 client: &GoogleCloudStorageClient,
783 ) -> crate::Result<()> {
784 let email = &self.credential.email;
785 let date = self.date.unwrap_or_else(Utc::now);
786 let scope = self.scope(date);
787 let credential_with_scope = format!("{email}/{scope}");
788
789 append_strict_query_pairs(url, extra_query);
792
793 let mut headers = HeaderMap::with_capacity(1 + signed_headers.len());
796 headers.insert("host", DEFAULT_GCS_SIGN_BLOB_HOST.parse().unwrap());
797 for (name, value) in signed_headers {
798 headers.append(name.clone(), value.clone());
799 }
800
801 let (_, signed_headers) = Self::canonicalize_headers(&headers);
802
803 url.query_pairs_mut()
804 .append_pair("X-Goog-Algorithm", "GOOG4-RSA-SHA256")
805 .append_pair("X-Goog-Credential", &credential_with_scope)
806 .append_pair("X-Goog-Date", &date.format("%Y%m%dT%H%M%SZ").to_string())
807 .append_pair("X-Goog-Expires", &expires_in.as_secs().to_string())
808 .append_pair("X-Goog-SignedHeaders", &signed_headers);
809
810 let string_to_sign = self.string_to_sign(crypto, date, &method, url, &headers)?;
811 let signature = match &self.credential.private_key {
812 Some(key) => hex_encode(&key.sign(string_to_sign.as_bytes())?),
813 None => client.sign_blob(&string_to_sign, email).await?,
814 };
815
816 url.query_pairs_mut()
817 .append_pair("X-Goog-Signature", &signature);
818 Ok(())
819 }
820
821 fn scope(&self, date: DateTime<Utc>) -> String {
825 format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
826 }
827
828 fn canonicalize_request(url: &Url, method: &Method, headers: &HeaderMap) -> String {
842 let verb = method.as_str();
843 let path = url.path();
844 let query = Self::canonicalize_query(url);
845 let (canonical_headers, signed_headers) = Self::canonicalize_headers(headers);
846
847 format!(
848 "{verb}\n{path}\n{query}\n{canonical_headers}\n\n{signed_headers}\n{DEFAULT_GCS_PLAYLOAD_STRING}"
849 )
850 }
851
852 fn canonicalize_query(url: &Url) -> String {
861 url.query_pairs()
862 .map(|(k, v)| {
863 (
864 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET).to_string(),
865 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET).to_string(),
866 )
867 })
868 .sorted_unstable()
869 .map(|(k, v)| format!("{k}={v}"))
870 .join("&")
871 }
872
873 fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
877 let mut headers = BTreeMap::<String, Vec<&str>>::new();
879 for (k, v) in header_map {
880 headers
881 .entry(k.as_str().to_lowercase())
882 .or_default()
883 .push(std::str::from_utf8(v.as_bytes()).unwrap());
884 }
885
886 let canonicalize_headers = headers
887 .iter()
888 .map(|(k, v)| {
889 format!(
890 "{}:{}",
891 k.trim(),
892 v.iter().map(|v| trim_header_value(v)).join(",")
893 )
894 })
895 .join("\n");
896
897 let signed_headers = headers.keys().join(";");
898
899 (canonicalize_headers, signed_headers)
900 }
901
902 pub(crate) fn string_to_sign(
913 &self,
914 crypto: &dyn CryptoProvider,
915 date: DateTime<Utc>,
916 request_method: &Method,
917 url: &Url,
918 headers: &HeaderMap,
919 ) -> crate::Result<String> {
920 let canonical_request = Self::canonicalize_request(url, request_method, headers);
921 let hashed_canonical_req = hex_digest(crypto, canonical_request.as_bytes())?;
922 let scope = self.scope(date);
923
924 Ok(format!(
925 "{}\n{}\n{}\n{}",
926 "GOOG4-RSA-SHA256",
927 date.format("%Y%m%dT%H%M%SZ"),
928 scope,
929 hashed_canonical_req
930 ))
931 }
932}
933
934pub(crate) trait CredentialExt {
935 fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self;
937}
938
939impl CredentialExt for HttpRequestBuilder {
940 fn with_bearer_auth(self, credential: Option<&GcpCredential>) -> Self {
941 match credential {
942 Some(credential) => {
943 if credential.bearer.is_empty() {
944 self
945 } else {
946 self.bearer_auth(&credential.bearer)
947 }
948 }
949 None => self,
950 }
951 }
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957 use crate::client::{
958 ClientOptions, DigestAlgorithm, DigestContext, HmacContext, StaticCredentialProvider,
959 };
960 use crate::gcp::client::{GoogleCloudStorageClient, GoogleCloudStorageConfig};
961 use http::{HeaderName, HeaderValue};
962
963 const SIGNATURE_BYTES: &[u8] = &[0x00, 0x01, 0x02, 0xab, 0xcd];
964
965 struct FixedSigner;
966
967 impl Signer for FixedSigner {
968 fn sign(&self, _string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
969 Ok(SIGNATURE_BYTES.to_vec())
970 }
971 }
972
973 #[derive(Debug)]
974 struct FixedCryptoProvider;
975
976 impl CryptoProvider for FixedCryptoProvider {
977 fn digest(&self, _algorithm: DigestAlgorithm) -> crate::Result<Box<dyn DigestContext>> {
978 Ok(Box::new(FixedDigestContext))
979 }
980
981 fn hmac(
982 &self,
983 _algorithm: DigestAlgorithm,
984 _secret: &[u8],
985 ) -> crate::Result<Box<dyn HmacContext>> {
986 panic!("GCS signed URL should not use HMAC")
987 }
988
989 fn sign(
990 &self,
991 _algorithm: SigningAlgorithm,
992 _pem: &[u8],
993 ) -> crate::Result<Box<dyn Signer>> {
994 Ok(Box::new(FixedSigner))
995 }
996 }
997
998 struct FixedDigestContext;
999
1000 impl DigestContext for FixedDigestContext {
1001 fn update(&mut self, _data: &[u8]) {}
1002
1003 fn finish(&mut self) -> crate::Result<&[u8]> {
1004 Ok(&[0x12, 0x34])
1005 }
1006 }
1007
1008 #[derive(Debug)]
1009 struct UnusedHttpService;
1010
1011 #[async_trait::async_trait]
1012 impl crate::client::HttpService for UnusedHttpService {
1013 async fn call(
1014 &self,
1015 _req: crate::client::HttpRequest,
1016 ) -> std::result::Result<crate::client::HttpResponse, HttpError> {
1017 panic!("SelfSignedJwt should not make HTTP requests")
1018 }
1019 }
1020
1021 #[test]
1022 fn self_signed_jwt_base64url_encodes_raw_signature_bytes() {
1023 let jwt = SelfSignedJwt::new(
1024 "key-id".into(),
1025 "service-account@example.com".into(),
1026 ServiceAccountKey::new(Box::new(FixedSigner)),
1027 DEFAULT_SCOPE.to_string(),
1028 )
1029 .unwrap();
1030 let client = HttpClient::new(UnusedHttpService);
1031 let token = futures_executor::block_on(jwt.fetch_token(&client, &RetryConfig::default()))
1032 .unwrap()
1033 .token;
1034 let signature = token.bearer.rsplit('.').next().unwrap();
1035
1036 assert_eq!(signature, BASE64_URL_SAFE_NO_PAD.encode(SIGNATURE_BYTES));
1037 assert_ne!(
1038 signature,
1039 BASE64_URL_SAFE_NO_PAD.encode(hex_encode(SIGNATURE_BYTES))
1040 );
1041 }
1042
1043 #[test]
1044 fn signed_url_hex_encodes_local_signature_bytes() {
1045 let signing_credential = Arc::new(GcpSigningCredential {
1046 email: "service-account@example.com".into(),
1047 private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
1048 });
1049 let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
1050 let config = GoogleCloudStorageConfig {
1051 base_url: DEFAULT_GCS_BASE_URL.into(),
1052 credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
1053 bearer: "bearer".into(),
1054 })),
1055 signing_credentials: Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
1056 email: "service-account@example.com".into(),
1057 private_key: None,
1058 })),
1059 crypto: None,
1060 bucket_name: "bucket".into(),
1061 retry_config: RetryConfig::default(),
1062 client_options: ClientOptions::default(),
1063 skip_signature: false,
1064 };
1065 let client =
1066 GoogleCloudStorageClient::new(config, HttpClient::new(UnusedHttpService)).unwrap();
1067 let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1068
1069 futures_executor::block_on(authorizer.sign_with(
1070 &FixedCryptoProvider,
1071 Method::GET,
1072 &mut url,
1073 &[],
1074 &HeaderMap::new(),
1075 Duration::from_secs(60),
1076 &client,
1077 ))
1078 .unwrap();
1079
1080 let signature = url
1081 .query_pairs()
1082 .find(|(key, _)| key == "X-Goog-Signature")
1083 .unwrap()
1084 .1;
1085 assert_eq!(signature, hex_encode(SIGNATURE_BYTES));
1086 }
1087
1088 #[test]
1089 fn signed_url_with_folds_query_and_headers() {
1090 let signing_credential = Arc::new(GcpSigningCredential {
1091 email: "service-account@example.com".into(),
1092 private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
1093 });
1094 let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
1095 let config = GoogleCloudStorageConfig {
1096 base_url: DEFAULT_GCS_BASE_URL.into(),
1097 credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
1098 bearer: "bearer".into(),
1099 })),
1100 signing_credentials: Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
1101 email: "service-account@example.com".into(),
1102 private_key: None,
1103 })),
1104 crypto: None,
1105 bucket_name: "bucket".into(),
1106 retry_config: RetryConfig::default(),
1107 client_options: ClientOptions::default(),
1108 skip_signature: false,
1109 };
1110 let client =
1111 GoogleCloudStorageClient::new(config, HttpClient::new(UnusedHttpService)).unwrap();
1112 let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1113
1114 let mut signed_headers = HeaderMap::new();
1115 signed_headers.insert(
1116 HeaderName::from_static("content-type"),
1117 HeaderValue::from_static("text/plain"),
1118 );
1119
1120 futures_executor::block_on(authorizer.sign_with(
1121 &FixedCryptoProvider,
1122 Method::PUT,
1123 &mut url,
1124 &[
1125 ("partNumber".to_string(), "1".to_string()),
1126 ("uploadId".to_string(), "abc123".to_string()),
1127 ],
1128 &signed_headers,
1129 Duration::from_secs(60),
1130 &client,
1131 ))
1132 .unwrap();
1133
1134 let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
1135 assert_eq!(pairs.get("partNumber").map(String::as_str), Some("1"));
1137 assert_eq!(pairs.get("uploadId").map(String::as_str), Some("abc123"));
1138 assert_eq!(
1140 pairs.get("X-Goog-SignedHeaders").map(String::as_str),
1141 Some("content-type;host")
1142 );
1143 assert!(pairs.contains_key("X-Goog-Signature"));
1144 }
1145
1146 #[test]
1147 fn test_canonicalize_headers() {
1148 let mut input_header = HeaderMap::new();
1149 input_header.insert("content-type", "text/plain".parse().unwrap());
1150 input_header.insert("host", "storage.googleapis.com".parse().unwrap());
1151 input_header.insert("x-goog-meta-reviewer", "jane".parse().unwrap());
1152 input_header.append("x-goog-meta-reviewer", "john".parse().unwrap());
1153 assert_eq!(
1154 GCSAuthorizer::canonicalize_headers(&input_header),
1155 (
1156 "content-type:text/plain
1157host:storage.googleapis.com
1158x-goog-meta-reviewer:jane,john"
1159 .into(),
1160 "content-type;host;x-goog-meta-reviewer".to_string()
1161 )
1162 );
1163 }
1164
1165 #[test]
1166 fn test_canonicalize_query() {
1167 let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
1168 url.query_pairs_mut()
1169 .append_pair("max-keys", "2")
1170 .append_pair("prefix", "object");
1171 assert_eq!(
1172 GCSAuthorizer::canonicalize_query(&url),
1173 "max-keys=2&prefix=object".to_string()
1174 );
1175 }
1176
1177 #[test]
1178 fn trim_header_value_collapses_internal_whitespace() {
1179 assert_eq!(trim_header_value(" foo bar "), "foo bar");
1183 assert_eq!(
1184 trim_header_value("text/plain; charset=utf-8"),
1185 "text/plain; charset=utf-8"
1186 );
1187 assert_eq!(trim_header_value("single"), "single");
1188 }
1189}