Skip to content

Commit 5a0baf1

Browse files
l1nxytustvold
andauthored
Add GCS signed URL support (#5300)
* add util function for gcp sign url * add string to sign and other sign functions * add GoogleCloudStorageConfig::new and config and move functions to client * add more code and rearrange struct * add client_email for credential and return the signed url * clean some code * add client email for AuthorizedUserCredentials * tidy some code * format doc * Add GcpSigningCredentialProvider for getting email * add test * Move some functions which shared by aws and gcp to utils. * fix some bug and make it can get proper result * remoe useless code * tidy some code * do not export host * add sign_by_key * Cleanup * Add ServiceAccountKey * Further tweaks * add more scope for signing. * tidy * Tweak and add test * Retry and handle errors for signBlob --------- Co-authored-by: Raphael Taylor-Davies <[email protected]>
1 parent eddef43 commit 5a0baf1

File tree

7 files changed

+677
-81
lines changed

7 files changed

+677
-81
lines changed

object_store/src/aws/credential.rs

+1-18
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::aws::{AwsCredentialProvider, STORE, STRICT_ENCODE_SET, STRICT_PATH_EN
1919
use crate::client::retry::RetryExt;
2020
use crate::client::token::{TemporaryToken, TokenCache};
2121
use crate::client::TokenProvider;
22-
use crate::util::hmac_sha256;
22+
use crate::util::{hex_digest, hex_encode, hmac_sha256};
2323
use crate::{CredentialProvider, Result, RetryConfig};
2424
use async_trait::async_trait;
2525
use bytes::Buf;
@@ -342,23 +342,6 @@ impl CredentialExt for RequestBuilder {
342342
}
343343
}
344344

345-
/// Computes the SHA256 digest of `body` returned as a hex encoded string
346-
fn hex_digest(bytes: &[u8]) -> String {
347-
let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
348-
hex_encode(digest.as_ref())
349-
}
350-
351-
/// Returns `bytes` as a lower-case hex encoded string
352-
fn hex_encode(bytes: &[u8]) -> String {
353-
use std::fmt::Write;
354-
let mut out = String::with_capacity(bytes.len() * 2);
355-
for byte in bytes {
356-
// String writing is infallible
357-
let _ = write!(out, "{byte:02x}");
358-
}
359-
out
360-
}
361-
362345
/// Canonicalizes query parameters into the AWS canonical form
363346
///
364347
/// <https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html>

object_store/src/aws/mod.rs

+1-10
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use crate::client::list::ListClientExt;
4343
use crate::client::CredentialProvider;
4444
use crate::multipart::{MultipartStore, PartId};
4545
use crate::signer::Signer;
46+
use crate::util::STRICT_ENCODE_SET;
4647
use crate::{
4748
Error, GetOptions, GetResult, ListResult, MultipartId, MultipartUpload, ObjectMeta,
4849
ObjectStore, Path, PutMode, PutOptions, PutResult, Result, UploadPart,
@@ -64,16 +65,6 @@ pub use dynamo::DynamoCommit;
6465
pub use precondition::{S3ConditionalPut, S3CopyIfNotExists};
6566
pub use resolve::resolve_bucket_region;
6667

67-
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
68-
//
69-
// Do not URI-encode any of the unreserved characters that RFC 3986 defines:
70-
// A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ), and tilde ( ~ ).
71-
pub(crate) const STRICT_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
72-
.remove(b'-')
73-
.remove(b'.')
74-
.remove(b'_')
75-
.remove(b'~');
76-
7768
/// This struct is used to maintain the URI path encoding
7869
const STRICT_PATH_ENCODE_SET: percent_encoding::AsciiSet = STRICT_ENCODE_SET.remove(b'/');
7970

object_store/src/gcp/builder.rs

+47-8
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ use crate::gcp::credential::{
2121
ApplicationDefaultCredentials, InstanceCredentialProvider, ServiceAccountCredentials,
2222
DEFAULT_GCS_BASE_URL,
2323
};
24-
use crate::gcp::{credential, GcpCredential, GcpCredentialProvider, GoogleCloudStorage, STORE};
24+
use crate::gcp::{
25+
credential, GcpCredential, GcpCredentialProvider, GcpSigningCredential,
26+
GcpSigningCredentialProvider, GoogleCloudStorage, STORE,
27+
};
2528
use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
2629
use serde::{Deserialize, Serialize};
2730
use snafu::{OptionExt, ResultExt, Snafu};
2831
use std::str::FromStr;
2932
use std::sync::Arc;
3033
use url::Url;
3134

35+
use super::credential::{AuthorizedUserSigningCredentials, InstanceSigningCredentialProvider};
36+
3237
#[derive(Debug, Snafu)]
3338
enum Error {
3439
#[snafu(display("Missing bucket name"))]
@@ -107,6 +112,8 @@ pub struct GoogleCloudStorageBuilder {
107112
client_options: ClientOptions,
108113
/// Credentials
109114
credentials: Option<GcpCredentialProvider>,
115+
/// Credentials for sign url
116+
signing_cedentials: Option<GcpSigningCredentialProvider>,
110117
}
111118

112119
/// Configuration keys for [`GoogleCloudStorageBuilder`]
@@ -202,6 +209,7 @@ impl Default for GoogleCloudStorageBuilder {
202209
client_options: ClientOptions::new().with_allow_http(true),
203210
url: None,
204211
credentials: None,
212+
signing_cedentials: None,
205213
}
206214
}
207215
}
@@ -452,13 +460,13 @@ impl GoogleCloudStorageBuilder {
452460
Arc::new(StaticCredentialProvider::new(GcpCredential {
453461
bearer: "".to_string(),
454462
})) as _
455-
} else if let Some(credentials) = service_account_credentials {
463+
} else if let Some(credentials) = service_account_credentials.clone() {
456464
Arc::new(TokenCredentialProvider::new(
457465
credentials.token_provider()?,
458466
self.client_options.client()?,
459467
self.retry_config.clone(),
460468
)) as _
461-
} else if let Some(credentials) = application_default_credentials {
469+
} else if let Some(credentials) = application_default_credentials.clone() {
462470
match credentials {
463471
ApplicationDefaultCredentials::AuthorizedUser(token) => {
464472
Arc::new(TokenCredentialProvider::new(
@@ -483,13 +491,44 @@ impl GoogleCloudStorageBuilder {
483491
)) as _
484492
};
485493

486-
let config = GoogleCloudStorageConfig {
487-
base_url: gcs_base_url,
494+
let signing_credentials = if let Some(signing_credentials) = self.signing_cedentials {
495+
signing_credentials
496+
} else if disable_oauth {
497+
Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
498+
email: "".to_string(),
499+
private_key: None,
500+
})) as _
501+
} else if let Some(credentials) = service_account_credentials.clone() {
502+
credentials.signing_credentials()?
503+
} else if let Some(credentials) = application_default_credentials.clone() {
504+
match credentials {
505+
ApplicationDefaultCredentials::AuthorizedUser(token) => {
506+
Arc::new(TokenCredentialProvider::new(
507+
AuthorizedUserSigningCredentials::from(token)?,
508+
self.client_options.client()?,
509+
self.retry_config.clone(),
510+
)) as _
511+
}
512+
ApplicationDefaultCredentials::ServiceAccount(token) => {
513+
token.signing_credentials()?
514+
}
515+
}
516+
} else {
517+
Arc::new(TokenCredentialProvider::new(
518+
InstanceSigningCredentialProvider::default(),
519+
self.client_options.metadata_client()?,
520+
self.retry_config.clone(),
521+
)) as _
522+
};
523+
524+
let config = GoogleCloudStorageConfig::new(
525+
gcs_base_url,
488526
credentials,
527+
signing_credentials,
489528
bucket_name,
490-
retry_config: self.retry_config,
491-
client_options: self.client_options,
492-
};
529+
self.retry_config,
530+
self.client_options,
531+
);
493532

494533
Ok(GoogleCloudStorage {
495534
client: Arc::new(GoogleCloudStorageClient::new(config)?),

object_store/src/gcp/client.rs

+103-2
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,22 @@ use crate::client::s3::{
2424
ListResponse,
2525
};
2626
use crate::client::GetOptionsExt;
27-
use crate::gcp::{GcpCredential, GcpCredentialProvider, STORE};
27+
use crate::gcp::{GcpCredential, GcpCredentialProvider, GcpSigningCredentialProvider, STORE};
2828
use crate::multipart::PartId;
2929
use crate::path::{Path, DELIMITER};
30+
use crate::util::hex_encode;
3031
use crate::{
3132
ClientOptions, GetOptions, ListResult, MultipartId, PutMode, PutOptions, PutResult, Result,
3233
RetryConfig,
3334
};
3435
use async_trait::async_trait;
36+
use base64::prelude::BASE64_STANDARD;
37+
use base64::Engine;
3538
use bytes::{Buf, Bytes};
3639
use percent_encoding::{percent_encode, utf8_percent_encode, NON_ALPHANUMERIC};
3740
use reqwest::header::HeaderName;
3841
use reqwest::{header, Client, Method, RequestBuilder, Response, StatusCode};
39-
use serde::Serialize;
42+
use serde::{Deserialize, Serialize};
4043
use snafu::{OptionExt, ResultExt, Snafu};
4144
use std::sync::Arc;
4245

@@ -101,6 +104,15 @@ enum Error {
101104

102105
#[snafu(display("Got invalid multipart response: {}", source))]
103106
InvalidMultipartResponse { source: quick_xml::de::DeError },
107+
108+
#[snafu(display("Error signing blob: {}", source))]
109+
SignBlobRequest { source: crate::client::retry::Error },
110+
111+
#[snafu(display("Got invalid signing blob repsonse: {}", source))]
112+
InvalidSignBlobResponse { source: reqwest::Error },
113+
114+
#[snafu(display("Got invalid signing blob signature: {}", source))]
115+
InvalidSignBlobSignature { source: base64::DecodeError },
104116
}
105117

106118
impl From<Error> for crate::Error {
@@ -123,13 +135,39 @@ pub struct GoogleCloudStorageConfig {
123135

124136
pub credentials: GcpCredentialProvider,
125137

138+
pub signing_credentials: GcpSigningCredentialProvider,
139+
126140
pub bucket_name: String,
127141

128142
pub retry_config: RetryConfig,
129143

130144
pub client_options: ClientOptions,
131145
}
132146

147+
impl GoogleCloudStorageConfig {
148+
pub fn new(
149+
base_url: String,
150+
credentials: GcpCredentialProvider,
151+
signing_credentials: GcpSigningCredentialProvider,
152+
bucket_name: String,
153+
retry_config: RetryConfig,
154+
client_options: ClientOptions,
155+
) -> Self {
156+
Self {
157+
base_url,
158+
credentials,
159+
signing_credentials,
160+
bucket_name,
161+
retry_config,
162+
client_options,
163+
}
164+
}
165+
166+
pub fn path_url(&self, path: &Path) -> String {
167+
format!("{}/{}/{}", self.base_url, self.bucket_name, path)
168+
}
169+
}
170+
133171
/// A builder for a put request allowing customisation of the headers and query string
134172
pub struct PutRequest<'a> {
135173
path: &'a Path,
@@ -163,6 +201,21 @@ impl<'a> PutRequest<'a> {
163201
}
164202
}
165203

204+
/// Sign Blob Request Body
205+
#[derive(Debug, Serialize)]
206+
struct SignBlobBody {
207+
/// The payload to sign
208+
payload: String,
209+
}
210+
211+
/// Sign Blob Response
212+
#[derive(Deserialize)]
213+
#[serde(rename_all = "camelCase")]
214+
struct SignBlobResponse {
215+
/// The signature for the payload
216+
signed_blob: String,
217+
}
218+
166219
#[derive(Debug)]
167220
pub struct GoogleCloudStorageClient {
168221
config: GoogleCloudStorageConfig,
@@ -197,6 +250,54 @@ impl GoogleCloudStorageClient {
197250
self.config.credentials.get_credential().await
198251
}
199252

253+
/// Create a signature from a string-to-sign using Google Cloud signBlob method.
254+
/// form like:
255+
/// ```plaintext
256+
/// curl -X POST --data-binary @JSON_FILE_NAME \
257+
/// -H "Authorization: Bearer OAUTH2_TOKEN" \
258+
/// -H "Content-Type: application/json" \
259+
/// "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT_EMAIL:signBlob"
260+
/// ```
261+
///
262+
/// 'JSON_FILE_NAME' is a file containing the following JSON object:
263+
/// ```plaintext
264+
/// {
265+
/// "payload": "REQUEST_INFORMATION"
266+
/// }
267+
/// ```
268+
pub async fn sign_blob(&self, string_to_sign: &str, client_email: &str) -> Result<String> {
269+
let credential = self.get_credential().await?;
270+
let body = SignBlobBody {
271+
payload: BASE64_STANDARD.encode(string_to_sign),
272+
};
273+
274+
let url = format!(
275+
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:signBlob",
276+
client_email
277+
);
278+
279+
let response = self
280+
.client
281+
.post(&url)
282+
.bearer_auth(&credential.bearer)
283+
.json(&body)
284+
.send_retry(&self.config.retry_config)
285+
.await
286+
.context(SignBlobRequestSnafu)?;
287+
288+
//If successful, the signature is returned in the signedBlob field in the response.
289+
let response = response
290+
.json::<SignBlobResponse>()
291+
.await
292+
.context(InvalidSignBlobResponseSnafu)?;
293+
294+
let signed_blob = BASE64_STANDARD
295+
.decode(response.signed_blob)
296+
.context(InvalidSignBlobSignatureSnafu)?;
297+
298+
Ok(hex_encode(&signed_blob))
299+
}
300+
200301
pub fn object_url(&self, path: &Path) -> String {
201302
let encoded = utf8_percent_encode(path.as_ref(), NON_ALPHANUMERIC);
202303
format!(

0 commit comments

Comments
 (0)