object_store/gcp/
credential.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// A Google Cloud Storage Credential for signing
100#[derive(Debug)]
101pub struct GcpSigningCredential {
102    /// The email of the service account
103    pub email: String,
104
105    /// An optional RSA private key
106    ///
107    /// If provided this will be used to sign the URL, otherwise a call will be made to
108    /// [`iam.serviceAccounts.signBlob`]. This allows supporting credential sources
109    /// that don't expose the service account private key, e.g. [IMDS].
110    ///
111    /// [IMDS]: https://cloud.google.com/docs/authentication/get-id-token#metadata-server
112    /// [`iam.serviceAccounts.signBlob`]: https://cloud.google.com/storage/docs/authentication/creating-signatures
113    pub private_key: Option<ServiceAccountKey>,
114}
115
116/// A private RSA key for a service account
117#[derive(Debug)]
118pub struct ServiceAccountKey(RsaKeyPair);
119
120impl ServiceAccountKey {
121    /// Parses a pem-encoded RSA key
122    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        // Reading from string is infallible
130        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    /// Parses an unencrypted PKCS#8-encoded RSA private key.
138    pub fn from_pkcs8(key: &[u8]) -> Result<Self> {
139        Ok(Self(RsaKeyPair::from_pkcs8(key)?))
140    }
141
142    /// Parses an unencrypted PKCS#8-encoded RSA private key.
143    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/// A Google Cloud Storage Credential
163#[derive(Debug, Eq, PartialEq)]
164pub struct GcpCredential {
165    /// An HTTP bearer token
166    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    /// The type of JWS: it can only be "JWT" here
174    ///
175    /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub typ: Option<&'a str>,
178    /// The algorithm used
179    ///
180    /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
181    pub alg: &'a str,
182    /// Content type
183    ///
184    /// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub cty: Option<&'a str>,
187    /// JSON Key URL
188    ///
189    /// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub jku: Option<&'a str>,
192    /// Key ID
193    ///
194    /// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub kid: Option<&'a str>,
197    /// X.509 URL
198    ///
199    /// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub x5u: Option<&'a str>,
202    /// X.509 certificate thumbprint
203    ///
204    /// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
205    #[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/// Self-signed JWT (JSON Web Token).
225///
226/// # References
227/// - <https://google.aip.dev/auth/4111>
228#[derive(Debug)]
229pub struct SelfSignedJwt {
230    issuer: String,
231    scope: String,
232    private_key: ServiceAccountKey,
233    key_id: String,
234}
235
236impl SelfSignedJwt {
237    /// Create a new [`SelfSignedJwt`]
238    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    /// Fetch a fresh token
258    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/// A deserialized `service-account-********.json`-file.
316#[derive(serde::Deserialize, Debug, Clone)]
317pub struct ServiceAccountCredentials {
318    /// The private key in RSA format.
319    pub private_key: String,
320
321    /// The private key ID
322    pub private_key_id: String,
323
324    /// The email address associated with the service account.
325    pub client_email: String,
326
327    /// Base URL for GCS
328    #[serde(default)]
329    pub gcs_base_url: Option<String>,
330
331    /// Disable oauth and use empty tokens.
332    #[serde(default)]
333    pub disable_oauth: bool,
334}
335
336impl ServiceAccountCredentials {
337    /// Create a new [`ServiceAccountCredentials`] from a file.
338    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
339        read_credentials_file(path)
340    }
341
342    /// Create a new [`ServiceAccountCredentials`] from a string.
343    pub fn from_key(key: &str) -> Result<Self> {
344        serde_json::from_str(key).context(DecodeCredentialsSnafu)
345    }
346
347    /// Create a [`SelfSignedJwt`] from this credentials struct.
348    ///
349    /// We use a scope of [`DEFAULT_SCOPE`] as opposed to an audience
350    /// as GCS appears to not support audience
351    ///
352    /// # References
353    /// - <https://stackoverflow.com/questions/63222450/service-account-authorization-without-oauth-can-we-get-file-from-google-cloud/71834557#71834557>
354    /// - <https://www.codejam.info/2022/05/google-cloud-service-account-authorization-without-oauth.html>
355    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
374/// Returns the number of seconds since unix epoch
375fn 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/// A provider that uses the Google Cloud Platform metadata server to fetch a token.
388///
389/// <https://cloud.google.com/docs/authentication/get-id-token#metadata-server>
390#[derive(Debug, Default)]
391pub struct InstanceCredentialProvider {}
392
393/// Make a request to the metadata server to fetch a token, using a a given hostname.
394async 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    /// Fetch a token from the metadata server.
419    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
420    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
421    /// environment variables.
422    ///  
423    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
424    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
457/// Make a request to the metadata server to fetch the client email, using a given hostname.
458async 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/// A provider that uses the Google Cloud Platform metadata server to fetch a email for signing.
478///
479/// <https://cloud.google.com/appengine/docs/legacy/standard/java/accessing-instance-metadata>
480#[derive(Debug, Default)]
481pub struct InstanceSigningCredentialProvider {}
482
483#[async_trait]
484impl TokenProvider for InstanceSigningCredentialProvider {
485    type Credential = GcpSigningCredential;
486
487    /// Fetch a token from the metadata server.
488    /// Since the connection is local we need to enable http access and don't actually use the client object passed in.
489    /// Respects the `GCE_METADATA_HOST`, `GCE_METADATA_ROOT`, and `GCE_METADATA_IP`
490    /// environment variables.
491    ///  
492    /// References: <https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html>
493    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/// A deserialized `application_default_credentials.json`-file.
530///
531/// # References
532/// - <https://cloud.google.com/docs/authentication/application-default-credentials#personal>
533/// - <https://google.aip.dev/auth/4110>
534#[derive(serde::Deserialize, Clone)]
535#[serde(tag = "type")]
536pub enum ApplicationDefaultCredentials {
537    /// Service Account.
538    ///
539    /// # References
540    /// - <https://google.aip.dev/auth/4112>
541    #[serde(rename = "service_account")]
542    ServiceAccount(ServiceAccountCredentials),
543    /// Authorized user via "gcloud CLI Integration".
544    ///
545    /// # References
546    /// - <https://google.aip.dev/auth/4113>
547    #[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    // Create a new application default credential in the following situations:
559    //  1. a file is passed in and the type matches.
560    //  2. without argument if the well-known configuration file is present.
561    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            // It's expected for this file to not exist unless it has been explicitly configured by the user.
571            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/// <https://google.aip.dev/auth/4113>
582#[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///<https://oauth2.googleapis.com/tokeninfo?access_token=ACCESS_TOKEN>
595#[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
676/// Trim whitespace from header values
677fn 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/// A Google Cloud Storage Authorizer for generating signed URL using [Google SigV4]
684///
685/// [Google SigV4]: https://cloud.google.com/storage/docs/access-control/signed-urls
686#[derive(Debug)]
687pub struct GCSAuthorizer {
688    date: Option<DateTime<Utc>>,
689    credential: Arc<GcpSigningCredential>,
690}
691
692impl GCSAuthorizer {
693    /// Create a new [`GCSAuthorizer`]
694    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    /// Get scope for the request
737    ///
738    /// <https://cloud.google.com/storage/docs/authentication/signatures#credential-scope>
739    fn scope(&self, date: DateTime<Utc>) -> String {
740        format!("{}/auto/storage/goog4_request", date.format("%Y%m%d"),)
741    }
742
743    /// Canonicalizes query parameters into the GCP canonical form
744    /// form like:
745    ///```plaintext
746    ///HTTP_VERB
747    ///PATH_TO_RESOURCE
748    ///CANONICAL_QUERY_STRING
749    ///CANONICAL_HEADERS
750    ///
751    ///SIGNED_HEADERS
752    ///PAYLOAD
753    ///```
754    ///
755    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests>
756    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    /// Canonicalizes query parameters into the GCP canonical form
769    /// form like `max-keys=2&prefix=object`
770    ///
771    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-query-strings>
772    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    /// Canonicalizes header into the GCP canonical form
786    ///
787    /// <https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers>
788    fn canonicalize_headers(header_map: &HeaderMap) -> (String, String) {
789        //FIXME add error handling for invalid header values
790        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    ///construct the string to sign
815    ///form like:
816    ///```plaintext
817    ///SIGNING_ALGORITHM
818    ///ACTIVE_DATETIME
819    ///CREDENTIAL_SCOPE
820    ///HASHED_CANONICAL_REQUEST
821    ///```
822    ///`ACTIVE_DATETIME` format:`YYYYMMDD'T'HHMMSS'Z'`
823    /// <https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign>
824    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}