1use super::client::GoogleCloudStorageClient;
19use crate::client::retry::RetryExt;
20use crate::client::token::TemporaryToken;
21use crate::client::TokenProvider;
22use crate::gcp::{GcpSigningCredentialProvider, STORE};
23use crate::util::{hex_digest, hex_encode, STRICT_ENCODE_SET};
24use crate::{RetryConfig, StaticCredentialProvider};
25use async_trait::async_trait;
26use base64::prelude::BASE64_URL_SAFE_NO_PAD;
27use base64::Engine;
28use chrono::{DateTime, Utc};
29use futures::TryFutureExt;
30use hyper::HeaderMap;
31use itertools::Itertools;
32use percent_encoding::utf8_percent_encode;
33use reqwest::{Client, Method};
34use ring::signature::RsaKeyPair;
35use serde::Deserialize;
36use snafu::{ResultExt, Snafu};
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 const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
48
49pub 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, Snafu)]
58pub enum Error {
59 #[snafu(display("Unable to open service account file from {}: {}", path.display(), source))]
60 OpenCredentials {
61 source: std::io::Error,
62 path: PathBuf,
63 },
64
65 #[snafu(display("Unable to decode service account file: {}", source))]
66 DecodeCredentials { source: serde_json::Error },
67
68 #[snafu(display("No RSA key found in pem file"))]
69 MissingKey,
70
71 #[snafu(display("Invalid RSA key: {}", source), context(false))]
72 InvalidKey { source: ring::error::KeyRejected },
73
74 #[snafu(display("Error signing: {}", source))]
75 Sign { source: ring::error::Unspecified },
76
77 #[snafu(display("Error encoding jwt payload: {}", source))]
78 Encode { source: serde_json::Error },
79
80 #[snafu(display("Unsupported key encoding: {}", encoding))]
81 UnsupportedKey { encoding: String },
82
83 #[snafu(display("Error performing token request: {}", source))]
84 TokenRequest { source: crate::client::retry::Error },
85
86 #[snafu(display("Error getting token response body: {}", source))]
87 TokenResponseBody { source: reqwest::Error },
88}
89
90impl From<Error> for crate::Error {
91 fn from(value: Error) -> Self {
92 Self::Generic {
93 store: STORE,
94 source: Box::new(value),
95 }
96 }
97}
98
99#[derive(Debug)]
101pub struct GcpSigningCredential {
102 pub email: String,
104
105 pub private_key: Option<ServiceAccountKey>,
114}
115
116#[derive(Debug)]
118pub struct ServiceAccountKey(RsaKeyPair);
119
120impl ServiceAccountKey {
121 pub fn from_pem(encoded: &[u8]) -> Result<Self> {
123 use rustls_pemfile::Item;
124 use std::io::Cursor;
125
126 let mut cursor = Cursor::new(encoded);
127 let mut reader = BufReader::new(&mut cursor);
128
129 match rustls_pemfile::read_one(&mut reader).unwrap() {
131 Some(Item::Pkcs8Key(key)) => Self::from_pkcs8(key.secret_pkcs8_der()),
132 Some(Item::Pkcs1Key(key)) => Self::from_der(key.secret_pkcs1_der()),
133 _ => Err(Error::MissingKey),
134 }
135 }
136
137 pub fn from_pkcs8(key: &[u8]) -> Result<Self> {
139 Ok(Self(RsaKeyPair::from_pkcs8(key)?))
140 }
141
142 pub fn from_der(key: &[u8]) -> Result<Self> {
144 Ok(Self(RsaKeyPair::from_der(key)?))
145 }
146
147 fn sign(&self, string_to_sign: &str) -> Result<String> {
148 let mut signature = vec![0; self.0.public().modulus_len()];
149 self.0
150 .sign(
151 &ring::signature::RSA_PKCS1_SHA256,
152 &ring::rand::SystemRandom::new(),
153 string_to_sign.as_bytes(),
154 &mut signature,
155 )
156 .context(SignSnafu)?;
157
158 Ok(hex_encode(&signature))
159 }
160}
161
162#[derive(Debug, Eq, PartialEq)]
164pub struct GcpCredential {
165 pub bearer: String,
167}
168
169pub type Result<T, E = Error> = std::result::Result<T, E>;
170
171#[derive(Debug, Default, serde::Serialize)]
172pub struct JwtHeader<'a> {
173 #[serde(skip_serializing_if = "Option::is_none")]
177 pub typ: Option<&'a str>,
178 pub alg: &'a str,
182 #[serde(skip_serializing_if = "Option::is_none")]
186 pub cty: Option<&'a str>,
187 #[serde(skip_serializing_if = "Option::is_none")]
191 pub jku: Option<&'a str>,
192 #[serde(skip_serializing_if = "Option::is_none")]
196 pub kid: Option<&'a str>,
197 #[serde(skip_serializing_if = "Option::is_none")]
201 pub x5u: Option<&'a str>,
202 #[serde(skip_serializing_if = "Option::is_none")]
206 pub x5t: Option<&'a str>,
207}
208
209#[derive(serde::Serialize)]
210struct TokenClaims<'a> {
211 iss: &'a str,
212 sub: &'a str,
213 scope: &'a str,
214 exp: u64,
215 iat: u64,
216}
217
218#[derive(serde::Deserialize, Debug)]
219struct TokenResponse {
220 access_token: String,
221 expires_in: u64,
222}
223
224#[derive(Debug)]
229pub struct SelfSignedJwt {
230 issuer: String,
231 scope: String,
232 private_key: ServiceAccountKey,
233 key_id: String,
234}
235
236impl SelfSignedJwt {
237 pub fn new(
239 key_id: String,
240 issuer: String,
241 private_key: ServiceAccountKey,
242 scope: String,
243 ) -> Result<Self> {
244 Ok(Self {
245 issuer,
246 scope,
247 private_key,
248 key_id,
249 })
250 }
251}
252
253#[async_trait]
254impl TokenProvider for SelfSignedJwt {
255 type Credential = GcpCredential;
256
257 async fn fetch_token(
259 &self,
260 _client: &Client,
261 _retry: &RetryConfig,
262 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
263 let now = seconds_since_epoch();
264 let exp = now + 3600;
265
266 let claims = TokenClaims {
267 iss: &self.issuer,
268 sub: &self.issuer,
269 scope: &self.scope,
270 iat: now,
271 exp,
272 };
273
274 let jwt_header = b64_encode_obj(&JwtHeader {
275 alg: "RS256",
276 typ: Some("JWT"),
277 kid: Some(&self.key_id),
278 ..Default::default()
279 })?;
280
281 let claim_str = b64_encode_obj(&claims)?;
282 let message = [jwt_header.as_ref(), claim_str.as_ref()].join(".");
283 let mut sig_bytes = vec![0; self.private_key.0.public().modulus_len()];
284 self.private_key
285 .0
286 .sign(
287 &ring::signature::RSA_PKCS1_SHA256,
288 &ring::rand::SystemRandom::new(),
289 message.as_bytes(),
290 &mut sig_bytes,
291 )
292 .context(SignSnafu)?;
293
294 let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes);
295 let bearer = [message, signature].join(".");
296
297 Ok(TemporaryToken {
298 token: Arc::new(GcpCredential { bearer }),
299 expiry: Some(Instant::now() + Duration::from_secs(3600)),
300 })
301 }
302}
303
304fn read_credentials_file<T>(service_account_path: impl AsRef<std::path::Path>) -> Result<T>
305where
306 T: serde::de::DeserializeOwned,
307{
308 let file = File::open(&service_account_path).context(OpenCredentialsSnafu {
309 path: service_account_path.as_ref().to_owned(),
310 })?;
311 let reader = BufReader::new(file);
312 serde_json::from_reader(reader).context(DecodeCredentialsSnafu)
313}
314
315#[derive(serde::Deserialize, Debug, Clone)]
317pub struct ServiceAccountCredentials {
318 pub private_key: String,
320
321 pub private_key_id: String,
323
324 pub client_email: String,
326
327 #[serde(default)]
329 pub gcs_base_url: Option<String>,
330
331 #[serde(default)]
333 pub disable_oauth: bool,
334}
335
336impl ServiceAccountCredentials {
337 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
339 read_credentials_file(path)
340 }
341
342 pub fn from_key(key: &str) -> Result<Self> {
344 serde_json::from_str(key).context(DecodeCredentialsSnafu)
345 }
346
347 pub fn token_provider(self) -> crate::Result<SelfSignedJwt> {
356 Ok(SelfSignedJwt::new(
357 self.private_key_id,
358 self.client_email,
359 ServiceAccountKey::from_pem(self.private_key.as_bytes())?,
360 DEFAULT_SCOPE.to_string(),
361 )?)
362 }
363
364 pub fn signing_credentials(self) -> crate::Result<GcpSigningCredentialProvider> {
365 Ok(Arc::new(StaticCredentialProvider::new(
366 GcpSigningCredential {
367 email: self.client_email,
368 private_key: Some(ServiceAccountKey::from_pem(self.private_key.as_bytes())?),
369 },
370 )))
371 }
372}
373
374fn seconds_since_epoch() -> u64 {
376 std::time::SystemTime::now()
377 .duration_since(std::time::SystemTime::UNIX_EPOCH)
378 .unwrap()
379 .as_secs()
380}
381
382fn b64_encode_obj<T: serde::Serialize>(obj: &T) -> Result<String> {
383 let string = serde_json::to_string(obj).context(EncodeSnafu)?;
384 Ok(BASE64_URL_SAFE_NO_PAD.encode(string))
385}
386
387#[derive(Debug, Default)]
391pub struct InstanceCredentialProvider {}
392
393async fn make_metadata_request(
395 client: &Client,
396 hostname: &str,
397 retry: &RetryConfig,
398) -> crate::Result<TokenResponse> {
399 let url =
400 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/token");
401 let response: TokenResponse = client
402 .request(Method::GET, url)
403 .header("Metadata-Flavor", "Google")
404 .query(&[("audience", "https://www.googleapis.com/oauth2/v4/token")])
405 .send_retry(retry)
406 .await
407 .context(TokenRequestSnafu)?
408 .json()
409 .await
410 .context(TokenResponseBodySnafu)?;
411 Ok(response)
412}
413
414#[async_trait]
415impl TokenProvider for InstanceCredentialProvider {
416 type Credential = GcpCredential;
417
418 async fn fetch_token(
425 &self,
426 client: &Client,
427 retry: &RetryConfig,
428 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
429 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
430 host
431 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
432 host
433 } else {
434 DEFAULT_METADATA_HOST.to_string()
435 };
436 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
437 ip
438 } else {
439 DEFAULT_METADATA_IP.to_string()
440 };
441
442 info!("fetching token from metadata server");
443 let response = make_metadata_request(client, &metadata_host, retry)
444 .or_else(|_| make_metadata_request(client, &metadata_ip, retry))
445 .await?;
446
447 let token = TemporaryToken {
448 token: Arc::new(GcpCredential {
449 bearer: response.access_token,
450 }),
451 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
452 };
453 Ok(token)
454 }
455}
456
457async fn make_metadata_request_for_email(
459 client: &Client,
460 hostname: &str,
461 retry: &RetryConfig,
462) -> crate::Result<String> {
463 let url =
464 format!("http://{hostname}/computeMetadata/v1/instance/service-accounts/default/email",);
465 let response = client
466 .request(Method::GET, url)
467 .header("Metadata-Flavor", "Google")
468 .send_retry(retry)
469 .await
470 .context(TokenRequestSnafu)?
471 .text()
472 .await
473 .context(TokenResponseBodySnafu)?;
474 Ok(response)
475}
476
477#[derive(Debug, Default)]
481pub struct InstanceSigningCredentialProvider {}
482
483#[async_trait]
484impl TokenProvider for InstanceSigningCredentialProvider {
485 type Credential = GcpSigningCredential;
486
487 async fn fetch_token(
494 &self,
495 client: &Client,
496 retry: &RetryConfig,
497 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
498 let metadata_host = if let Ok(host) = env::var("GCE_METADATA_HOST") {
499 host
500 } else if let Ok(host) = env::var("GCE_METADATA_ROOT") {
501 host
502 } else {
503 DEFAULT_METADATA_HOST.to_string()
504 };
505
506 let metadata_ip = if let Ok(ip) = env::var("GCE_METADATA_IP") {
507 ip
508 } else {
509 DEFAULT_METADATA_IP.to_string()
510 };
511
512 info!("fetching token from metadata server");
513
514 let email = make_metadata_request_for_email(client, &metadata_host, retry)
515 .or_else(|_| make_metadata_request_for_email(client, &metadata_ip, retry))
516 .await?;
517
518 let token = TemporaryToken {
519 token: Arc::new(GcpSigningCredential {
520 email,
521 private_key: None,
522 }),
523 expiry: None,
524 };
525 Ok(token)
526 }
527}
528
529#[derive(serde::Deserialize, Clone)]
535#[serde(tag = "type")]
536pub enum ApplicationDefaultCredentials {
537 #[serde(rename = "service_account")]
542 ServiceAccount(ServiceAccountCredentials),
543 #[serde(rename = "authorized_user")]
548 AuthorizedUser(AuthorizedUserCredentials),
549}
550
551impl ApplicationDefaultCredentials {
552 const CREDENTIALS_PATH: &'static str = if cfg!(windows) {
553 "gcloud/application_default_credentials.json"
554 } else {
555 ".config/gcloud/application_default_credentials.json"
556 };
557
558 pub fn read(path: Option<&str>) -> Result<Option<Self>, Error> {
562 if let Some(path) = path {
563 return read_credentials_file::<Self>(path).map(Some);
564 }
565
566 let home_var = if cfg!(windows) { "APPDATA" } else { "HOME" };
567 if let Some(home) = env::var_os(home_var) {
568 let path = Path::new(&home).join(Self::CREDENTIALS_PATH);
569
570 if path.exists() {
572 return read_credentials_file::<Self>(path).map(Some);
573 }
574 }
575 Ok(None)
576 }
577}
578
579const DEFAULT_TOKEN_GCP_URI: &str = "https://accounts.google.com/o/oauth2/token";
580
581#[derive(Debug, Deserialize, Clone)]
583pub struct AuthorizedUserCredentials {
584 client_id: String,
585 client_secret: String,
586 refresh_token: String,
587}
588
589#[derive(Debug, Deserialize)]
590pub struct AuthorizedUserSigningCredentials {
591 credential: AuthorizedUserCredentials,
592}
593
594#[derive(Debug, Deserialize)]
596struct EmailResponse {
597 email: String,
598}
599
600impl AuthorizedUserSigningCredentials {
601 pub fn from(credential: AuthorizedUserCredentials) -> crate::Result<Self> {
602 Ok(Self { credential })
603 }
604
605 async fn client_email(&self, client: &Client, retry: &RetryConfig) -> crate::Result<String> {
606 let response = client
607 .request(Method::GET, "https://oauth2.googleapis.com/tokeninfo")
608 .query(&[("access_token", &self.credential.refresh_token)])
609 .send_retry(retry)
610 .await
611 .context(TokenRequestSnafu)?
612 .json::<EmailResponse>()
613 .await
614 .context(TokenResponseBodySnafu)?;
615
616 Ok(response.email)
617 }
618}
619
620#[async_trait]
621impl TokenProvider for AuthorizedUserSigningCredentials {
622 type Credential = GcpSigningCredential;
623
624 async fn fetch_token(
625 &self,
626 client: &Client,
627 retry: &RetryConfig,
628 ) -> crate::Result<TemporaryToken<Arc<GcpSigningCredential>>> {
629 let email = self.client_email(client, retry).await?;
630
631 Ok(TemporaryToken {
632 token: Arc::new(GcpSigningCredential {
633 email,
634 private_key: None,
635 }),
636 expiry: None,
637 })
638 }
639}
640
641#[async_trait]
642impl TokenProvider for AuthorizedUserCredentials {
643 type Credential = GcpCredential;
644
645 async fn fetch_token(
646 &self,
647 client: &Client,
648 retry: &RetryConfig,
649 ) -> crate::Result<TemporaryToken<Arc<GcpCredential>>> {
650 let response = client
651 .request(Method::POST, DEFAULT_TOKEN_GCP_URI)
652 .form(&[
653 ("grant_type", "refresh_token"),
654 ("client_id", &self.client_id),
655 ("client_secret", &self.client_secret),
656 ("refresh_token", &self.refresh_token),
657 ])
658 .retryable(retry)
659 .idempotent(true)
660 .send()
661 .await
662 .context(TokenRequestSnafu)?
663 .json::<TokenResponse>()
664 .await
665 .context(TokenResponseBodySnafu)?;
666
667 Ok(TemporaryToken {
668 token: Arc::new(GcpCredential {
669 bearer: response.access_token,
670 }),
671 expiry: Some(Instant::now() + Duration::from_secs(response.expires_in)),
672 })
673 }
674}
675
676fn trim_header_value(value: &str) -> String {
678 let mut ret = value.to_string();
679 ret.retain(|c| !c.is_whitespace());
680 ret
681}
682
683#[derive(Debug)]
687pub struct GCSAuthorizer {
688 date: Option<DateTime<Utc>>,
689 credential: Arc<GcpSigningCredential>,
690}
691
692impl GCSAuthorizer {
693 pub fn new(credential: Arc<GcpSigningCredential>) -> Self {
695 Self {
696 date: None,
697 credential,
698 }
699 }
700
701 pub(crate) async fn sign(
702 &self,
703 method: Method,
704 url: &mut Url,
705 expires_in: Duration,
706 client: &GoogleCloudStorageClient,
707 ) -> crate::Result<()> {
708 let email = &self.credential.email;
709 let date = self.date.unwrap_or_else(Utc::now);
710 let scope = self.scope(date);
711 let credential_with_scope = format!("{}/{}", email, scope);
712
713 let mut headers = HeaderMap::new();
714 headers.insert("host", DEFAULT_GCS_SIGN_BLOB_HOST.parse().unwrap());
715
716 let (_, signed_headers) = Self::canonicalize_headers(&headers);
717
718 url.query_pairs_mut()
719 .append_pair("X-Goog-Algorithm", "GOOG4-RSA-SHA256")
720 .append_pair("X-Goog-Credential", &credential_with_scope)
721 .append_pair("X-Goog-Date", &date.format("%Y%m%dT%H%M%SZ").to_string())
722 .append_pair("X-Goog-Expires", &expires_in.as_secs().to_string())
723 .append_pair("X-Goog-SignedHeaders", &signed_headers);
724
725 let string_to_sign = self.string_to_sign(date, &method, url, &headers);
726 let signature = match &self.credential.private_key {
727 Some(key) => key.sign(&string_to_sign)?,
728 None => client.sign_blob(&string_to_sign, email).await?,
729 };
730
731 url.query_pairs_mut()
732 .append_pair("X-Goog-Signature", &signature);
733 Ok(())
734 }
735
736 fn scope(&self, date: DateTime<Utc>) -> String {
740 format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
741 }
742
743 fn canonicalize_request(url: &Url, method: &Method, headers: &HeaderMap) -> String {
757 let verb = method.as_str();
758 let path = url.path();
759 let query = Self::canonicalize_query(url);
760 let (canonical_headers, signed_headers) = Self::canonicalize_headers(headers);
761
762 format!(
763 "{}\n{}\n{}\n{}\n\n{}\n{}",
764 verb, path, query, canonical_headers, signed_headers, DEFAULT_GCS_PLAYLOAD_STRING
765 )
766 }
767
768 fn canonicalize_query(url: &Url) -> String {
773 url.query_pairs()
774 .sorted_unstable_by(|a, b| a.0.cmp(&b.0))
775 .map(|(k, v)| {
776 format!(
777 "{}={}",
778 utf8_percent_encode(k.as_ref(), &STRICT_ENCODE_SET),
779 utf8_percent_encode(v.as_ref(), &STRICT_ENCODE_SET)
780 )
781 })
782 .join("&")
783 }
784
785 fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
789 let mut headers = BTreeMap::<String, Vec<&str>>::new();
791 for (k, v) in header_map {
792 headers
793 .entry(k.as_str().to_lowercase())
794 .or_default()
795 .push(std::str::from_utf8(v.as_bytes()).unwrap());
796 }
797
798 let canonicalize_headers = headers
799 .iter()
800 .map(|(k, v)| {
801 format!(
802 "{}:{}",
803 k.trim(),
804 v.iter().map(|v| trim_header_value(v)).join(",")
805 )
806 })
807 .join("\n");
808
809 let signed_headers = headers.keys().join(";");
810
811 (canonicalize_headers, signed_headers)
812 }
813
814 pub fn string_to_sign(
825 &self,
826 date: DateTime<Utc>,
827 request_method: &Method,
828 url: &Url,
829 headers: &HeaderMap,
830 ) -> String {
831 let canonical_request = Self::canonicalize_request(url, request_method, headers);
832 let hashed_canonical_req = hex_digest(canonical_request.as_bytes());
833 let scope = self.scope(date);
834
835 format!(
836 "{}\n{}\n{}\n{}",
837 "GOOG4-RSA-SHA256",
838 date.format("%Y%m%dT%H%M%SZ"),
839 scope,
840 hashed_canonical_req
841 )
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848
849 #[test]
850 fn test_canonicalize_headers() {
851 let mut input_header = HeaderMap::new();
852 input_header.insert("content-type", "text/plain".parse().unwrap());
853 input_header.insert("host", "storage.googleapis.com".parse().unwrap());
854 input_header.insert("x-goog-meta-reviewer", "jane".parse().unwrap());
855 input_header.append("x-goog-meta-reviewer", "john".parse().unwrap());
856 assert_eq!(
857 GCSAuthorizer::canonicalize_headers(&input_header),
858 (
859 "content-type:text/plain
860host:storage.googleapis.com
861x-goog-meta-reviewer:jane,john"
862 .into(),
863 "content-type;host;x-goog-meta-reviewer".to_string()
864 )
865 );
866 }
867
868 #[test]
869 fn test_canonicalize_query() {
870 let mut url = Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
871 url.query_pairs_mut()
872 .append_pair("max-keys", "2")
873 .append_pair("prefix", "object");
874 assert_eq!(
875 GCSAuthorizer::canonicalize_query(&url),
876 "max-keys=2&prefix=object".to_string()
877 );
878 }
879}