diff --git a/Cargo.toml b/Cargo.toml index 605afc4f..c0023a04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ hyper = { version = "1.2", default-features = false, optional = true } md-5 = { version = "0.10.6", default-features = false, optional = true } quick-xml = { version = "0.38.0", features = ["serialize", "overlapped-lists"], optional = true } rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"], optional = true } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"], optional = true } +reqwest = { version = "0.12", default-features = false, features = ["http2", "rustls-tls-no-provider"], optional = true } ring = { version = "0.17", default-features = false, features = ["std"], optional = true } rustls-pki-types = { version = "1.9", default-features = false, features = ["std"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } @@ -71,12 +71,24 @@ wasm-bindgen-futures = "0.4.18" [features] default = ["fs"] -cloud = ["serde", "serde_json", "quick-xml", "hyper", "reqwest", "reqwest/stream", "chrono/serde", "base64", "rand", "ring", "http-body-util", "form_urlencoded", "serde_urlencoded"] -azure = ["cloud", "httparse"] +cloud-no-crypto = ["serde", "serde_json", "quick-xml", "hyper", "reqwest", "reqwest/stream", "chrono/serde", "base64", "rand","http-body-util", "form_urlencoded", "serde_urlencoded"] +cloud = ["ring", "rustls-pki-types", "cloud-no-crypto", "reqwest?/rustls-tls-native-roots"] + + +azure-no-crypto = ["cloud-no-crypto", "httparse"] +azure = ["cloud", "azure-no-crypto"] + fs = ["walkdir"] -gcp = ["cloud", "rustls-pki-types"] -aws = ["cloud", "md-5"] -http = ["cloud"] + +gcp-no-crypto = ["cloud-no-crypto"] +gcp = ["cloud", "gcp-no-crypto"] + +aws-no-crypto = ["cloud-no-crypto", "md-5"] +aws = ["cloud", "aws-no-crypto"] + +http-no-crypto = ["cloud-no-crypto"] +http = ["cloud", "http-no-crypto"] + tls-webpki-roots = ["reqwest?/rustls-tls-webpki-roots"] integration = ["rand"] @@ -105,4 +117,4 @@ features = ["js"] [[test]] name = "get_range_file" path = "tests/get_range_file.rs" -required-features = ["fs"] \ No newline at end of file +required-features = ["fs"] diff --git a/src/aws/builder.rs b/src/aws/builder.rs index e49145a4..002754b5 100644 --- a/src/aws/builder.rs +++ b/src/aws/builder.rs @@ -24,7 +24,7 @@ use crate::aws::{ AmazonS3, AwsCredential, AwsCredentialProvider, Checksum, S3ConditionalPut, S3CopyIfNotExists, STORE, }; -use crate::client::{HttpConnector, TokenCredentialProvider, http_connector}; +use crate::client::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector}; use crate::config::ConfigValue; use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider}; use base64::Engine; @@ -171,6 +171,8 @@ pub struct AmazonS3Builder { client_options: ClientOptions, /// Credentials credentials: Option, + /// The [`CryptoProvider`] to use + crypto: Option>, /// Skip signing requests skip_signature: ConfigValue, /// Copy if not exists @@ -843,6 +845,12 @@ impl AmazonS3Builder { self } + /// The [`CryptoProvider`] to use + pub fn with_crypto_provider(mut self, provider: Arc) -> Self { + self.crypto = Some(provider); + self + } + /// Sets what protocol is allowed. If `allow_http` is : /// * false (default): Only HTTPS are allowed /// * true: HTTP and HTTPS are allowed @@ -1150,6 +1158,7 @@ impl AmazonS3Builder { endpoint: endpoint.clone(), region: region.clone(), credentials: Arc::clone(&credentials), + crypto: self.crypto.clone(), }, http.connect(&self.client_options)?, self.retry_config.clone(), @@ -1190,6 +1199,7 @@ impl AmazonS3Builder { bucket, bucket_endpoint, credentials, + crypto: self.crypto, session_provider, retry_config: self.retry_config, client_options: self.client_options, diff --git a/src/aws/client.rs b/src/aws/client.rs index bd9618ed..cd672e25 100644 --- a/src/aws/client.rs +++ b/src/aws/client.rs @@ -32,7 +32,10 @@ use crate::client::s3::{ CompleteMultipartUpload, CompleteMultipartUploadResult, CopyPartResult, InitiateMultipartUploadResult, ListResponse, PartMetadata, }; -use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpResponse}; +use crate::client::{ + CryptoProvider, DigestAlgorithm, GetOptionsExt, HttpClient, HttpError, HttpResponse, + crypto_provider, +}; use crate::list::{PaginatedListOptions, PaginatedListResult}; use crate::multipart::PartId; use crate::{ @@ -52,8 +55,6 @@ use itertools::Itertools; use md5::{Digest, Md5}; use percent_encoding::{PercentEncode, utf8_percent_encode}; use quick_xml::events::{self as xml_events}; -use ring::digest; -use ring::digest::Context; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -198,6 +199,7 @@ pub(crate) struct S3Config { pub bucket: String, pub bucket_endpoint: String, pub credentials: AwsCredentialProvider, + pub crypto: Option>, pub session_provider: Option, pub retry_config: RetryConfig, pub client_options: ClientOptions, @@ -216,19 +218,18 @@ impl S3Config { format!("{}/{}", self.bucket_endpoint, encode_path(path)) } - async fn get_session_credential(&self) -> Result> { - let credential = match self.skip_signature { + async fn get_session_credential(&self) -> Result>> { + Ok(match self.skip_signature { false => { let provider = self.session_provider.as_ref().unwrap_or(&self.credentials); - Some(provider.get_credential().await?) + let credential = provider.get_credential().await?; + Some(SessionCredential { + credential, + session_token: self.session_provider.is_some(), + config: self, + }) } true => None, - }; - - Ok(SessionCredential { - credential, - session_token: self.session_provider.is_some(), - config: self, }) } @@ -243,27 +244,32 @@ impl S3Config { pub(crate) fn is_s3_express(&self) -> bool { self.session_provider.is_some() } + + pub(crate) fn crypto(&self) -> Result<&dyn CryptoProvider> { + crypto_provider(self.crypto.as_deref()) + } } struct SessionCredential<'a> { - credential: Option>, + credential: Arc, session_token: bool, config: &'a S3Config, } impl SessionCredential<'_> { - fn authorizer(&self) -> Option> { + fn authorizer(&self) -> Result> { let mut authorizer = - AwsAuthorizer::new(self.credential.as_deref()?, "s3", &self.config.region) + AwsAuthorizer::new(self.credential.as_ref(), "s3", &self.config.region) .with_sign_payload(self.config.sign_payload) - .with_request_payer(self.config.request_payer); + .with_request_payer(self.config.request_payer) + .with_crypto(self.config.crypto()?); if self.session_token { let token = HeaderName::from_static("x-amz-s3session-token"); authorizer = authorizer.with_token_header(token) } - Some(authorizer) + Ok(authorizer) } } @@ -296,7 +302,7 @@ pub(crate) struct Request<'a> { path: &'a Path, config: &'a S3Config, builder: HttpRequestBuilder, - payload_sha256: Option, + payload_sha256: Option<[u8; 32]>, payload: Option, use_session_creds: bool, idempotent: bool, @@ -397,13 +403,13 @@ impl Request<'_> { Self { builder, ..self } } - pub(crate) fn with_payload(mut self, payload: PutPayload) -> Self { + pub(crate) fn with_payload(mut self, payload: PutPayload) -> Result { if (!self.config.skip_signature && self.config.sign_payload) || self.config.checksum.is_some() { - let mut sha256 = Context::new(&digest::SHA256); - payload.iter().for_each(|x| sha256.update(x)); - let payload_sha256 = sha256.finish(); + let mut ctx = self.config.crypto()?.digest(DigestAlgorithm::Sha256)?; + payload.iter().for_each(|x| ctx.update(x)); + let payload_sha256 = ctx.finish()?.try_into().unwrap(); if let Some(Checksum::SHA256) = self.config.checksum { self.builder = self @@ -416,24 +422,28 @@ impl Request<'_> { let content_length = payload.content_length(); self.builder = self.builder.header(CONTENT_LENGTH, content_length); self.payload = Some(payload); - self + Ok(self) } pub(crate) async fn send(self) -> Result { let credential = match self.use_session_creds { true => self.config.get_session_credential().await?, - false => SessionCredential { - credential: self.config.get_credential().await?, - session_token: false, - config: self.config, - }, + false => { + let credential = self.config.get_credential().await?; + credential.map(|credential| SessionCredential { + credential, + session_token: false, + config: self.config, + }) + } }; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let sha = self.payload_sha256.as_ref().map(|x| x.as_ref()); let path = self.path.as_ref(); self.builder - .with_aws_sigv4(credential.authorizer(), sha) + .with_aws_sigv4(authorizer, sha)? .retryable(&self.config.retry_config) .retry_on_conflict(self.retry_on_conflict) .idempotent(self.idempotent) @@ -493,6 +503,7 @@ impl S3Client { } let credential = self.config.get_session_credential().await?; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let url = format!("{}?delete", self.config.bucket_endpoint); let mut buffer = Vec::new(); @@ -536,7 +547,11 @@ impl S3Client { let mut builder = self.client.request(Method::POST, url); - let digest = digest::digest(&digest::SHA256, &body); + let crypto = self.config.crypto()?; + let mut ctx = crypto.digest(DigestAlgorithm::Sha256)?; + ctx.update(body.as_ref()); + let digest = ctx.finish()?; + builder = builder.header(SHA256_CHECKSUM, BASE64_STANDARD.encode(digest)); // S3 *requires* DeleteObjects to include a Content-MD5 header: @@ -550,7 +565,7 @@ impl S3Client { let response = builder .header(CONTENT_TYPE, "application/xml") .body(body) - .with_aws_sigv4(credential.authorizer(), Some(digest.as_ref())) + .with_aws_sigv4(authorizer, Some(digest))? .send_retry(&self.config.retry_config) .await .map_err(|source| Error::DeleteObjectsRequest { @@ -690,7 +705,7 @@ impl S3Client { .idempotent(true); request = match data { - PutPartPayload::Part(payload) => request.with_payload(payload), + PutPartPayload::Part(payload) => request.with_payload(payload)?, PutPartPayload::Copy(path) => request.header( "x-amz-copy-source", &format!("{}/{}", self.config.bucket, encode_path(path)), @@ -775,6 +790,7 @@ impl S3Client { let body = quick_xml::se::to_string(&request).unwrap(); let credential = self.config.get_session_credential().await?; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let url = self.config.path_url(location); let request = self @@ -782,7 +798,7 @@ impl S3Client { .post(url) .query(&[("uploadId", upload_id)]) .body(body) - .with_aws_sigv4(credential.authorizer(), None); + .with_aws_sigv4(authorizer, None)?; let request = match mode { CompleteMultipartMode::Overwrite => request, @@ -821,11 +837,12 @@ impl S3Client { #[cfg(test)] pub(crate) async fn get_object_tagging(&self, path: &Path) -> Result { let credential = self.config.get_session_credential().await?; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let url = format!("{}?tagging", self.config.path_url(path)); let response = self .client .request(Method::GET, url) - .with_aws_sigv4(credential.authorizer(), None) + .with_aws_sigv4(authorizer, None)? .send_retry(&self.config.retry_config) .await .map_err(|e| e.error(STORE, path.to_string()))?; @@ -856,6 +873,7 @@ impl GetClient for S3Client { options: GetOptions, ) -> Result { let credential = self.config.get_session_credential().await?; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let url = self.config.path_url(path); let method = match options.head { true => Method::HEAD, @@ -878,7 +896,7 @@ impl GetClient for S3Client { let response = builder .with_get_options(options) - .with_aws_sigv4(credential.authorizer(), None) + .with_aws_sigv4(authorizer, None)? .retryable_request() .send(ctx) .await @@ -897,6 +915,7 @@ impl ListClient for Arc { opts: PaginatedListOptions, ) -> Result { let credential = self.config.get_session_credential().await?; + let authorizer = credential.as_ref().map(|x| x.authorizer()).transpose()?; let url = self.config.bucket_endpoint.clone(); let mut query = Vec::with_capacity(4); @@ -930,7 +949,7 @@ impl ListClient for Arc { .request(Method::GET, &url) .extensions(opts.extensions) .query(&query) - .with_aws_sigv4(credential.authorizer(), None) + .with_aws_sigv4(authorizer, None)? .send_retry(&self.config.retry_config) .await .map_err(|source| Error::ListRequest { source })? @@ -1000,6 +1019,7 @@ mod tests { conditional_put: Default::default(), encryption_headers: Default::default(), request_payer: false, + crypto: None, }; let client = S3Client::new(config, HttpClient::new(reqwest::Client::new())); diff --git a/src/aws/credential.rs b/src/aws/credential.rs index 170c3e44..862e2a9a 100644 --- a/src/aws/credential.rs +++ b/src/aws/credential.rs @@ -19,8 +19,11 @@ use crate::aws::{AwsCredentialProvider, STORE, STRICT_ENCODE_SET, STRICT_PATH_EN use crate::client::builder::HttpRequestBuilder; use crate::client::retry::RetryExt; use crate::client::token::{TemporaryToken, TokenCache}; -use crate::client::{HttpClient, HttpError, HttpRequest, TokenProvider}; -use crate::util::{hex_digest, hex_encode, hmac_sha256}; +use crate::client::{ + CryptoProvider, DigestAlgorithm, HttpClient, HttpError, HttpRequest, TokenProvider, + crypto_provider, +}; +use crate::util::{hex_digest, hex_encode}; use crate::{CredentialProvider, Result, RetryConfig}; use async_trait::async_trait; use bytes::Buf; @@ -91,13 +94,33 @@ impl AwsCredential { /// Signs a string /// /// - fn sign(&self, to_sign: &str, date: DateTime, region: &str, service: &str) -> String { + fn sign( + &self, + crypto: &dyn CryptoProvider, + to_sign: &str, + date: DateTime, + region: &str, + service: &str, + ) -> Result { let date_string = date.format("%Y%m%d").to_string(); - let date_hmac = hmac_sha256(format!("AWS4{}", self.secret_key), date_string); - let region_hmac = hmac_sha256(date_hmac, region); - let service_hmac = hmac_sha256(region_hmac, service); - let signing_hmac = hmac_sha256(service_hmac, b"aws4_request"); - hex_encode(hmac_sha256(signing_hmac, to_sign).as_ref()) + let secret_key = format!("AWS4{}", self.secret_key); + + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, secret_key.as_bytes())?; + ctx.update(date_string.as_bytes()); + + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, ctx.finish()?)?; + ctx.update(region.as_bytes()); + + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, ctx.finish()?)?; + ctx.update(service.as_bytes()); + + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, ctx.finish()?)?; + ctx.update(b"aws4_request"); + + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, ctx.finish()?)?; + ctx.update(to_sign.as_bytes()); + + Ok(hex_encode(ctx.finish()?)) } } @@ -108,6 +131,7 @@ impl AwsCredential { pub struct AwsAuthorizer<'a> { date: Option>, credential: &'a AwsCredential, + crypto: Option<&'a dyn CryptoProvider>, service: &'a str, region: &'a str, token_header: Option, @@ -129,6 +153,7 @@ impl<'a> AwsAuthorizer<'a> { credential, service, region, + crypto: None, date: None, sign_payload: true, token_header: None, @@ -157,6 +182,24 @@ impl<'a> AwsAuthorizer<'a> { self } + /// Specify the crypto provider + pub fn with_crypto(mut self, crypto: &'a dyn CryptoProvider) -> Self { + self.crypto = Some(crypto); + self + } + + /// Authorize `request` with an optional pre-calculated SHA256 digest by attaching + /// the relevant [AWS SigV4] headers + /// + /// # Panics + /// + /// Panics on cryptography error + /// + #[deprecated(note = "use AwsAuthorized::try_authorize")] + pub fn authorize(&self, request: &mut HttpRequest, pre_calculated_digest: Option<&[u8]>) { + self.try_authorize(request, pre_calculated_digest).unwrap() + } + /// Authorize `request` with an optional pre-calculated SHA256 digest by attaching /// the relevant [AWS SigV4] headers /// @@ -170,7 +213,12 @@ impl<'a> AwsAuthorizer<'a> { /// * Otherwise it is set to the hex encoded SHA256 of the request body /// /// [AWS SigV4]: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html - pub fn authorize(&self, request: &mut HttpRequest, pre_calculated_digest: Option<&[u8]>) { + pub fn try_authorize( + &self, + request: &mut HttpRequest, + pre_calculated_digest: Option<&[u8]>, + ) -> Result<()> { + let crypto = crypto_provider(self.crypto)?; let url = Url::parse(&request.uri().to_string()).unwrap(); if let Some(ref token) = self.credential.token { @@ -195,7 +243,7 @@ impl<'a> AwsAuthorizer<'a> { None => match request.body().is_empty() { true => EMPTY_SHA256_HASH.to_string(), false => match request.body().as_bytes() { - Some(bytes) => hex_digest(bytes), + Some(bytes) => hex_digest(crypto, bytes)?, None => STREAMING_PAYLOAD.to_string(), }, }, @@ -219,6 +267,7 @@ impl<'a> AwsAuthorizer<'a> { let scope = self.scope(date); let string_to_sign = self.string_to_sign( + crypto, date, &scope, request.method(), @@ -226,12 +275,12 @@ impl<'a> AwsAuthorizer<'a> { &canonical_headers, &signed_headers, &digest, - ); + )?; // sign the string - let signature = self - .credential - .sign(&string_to_sign, date, self.region, self.service); + let signature = + self.credential + .sign(crypto, &string_to_sign, date, self.region, self.service)?; // build the actual auth header let authorisation = format!( @@ -243,9 +292,12 @@ impl<'a> AwsAuthorizer<'a> { request .headers_mut() .insert(&AUTHORIZATION, authorization_val); + Ok(()) } - pub(crate) fn sign(&self, method: Method, url: &mut Url, expires_in: Duration) { + pub(crate) fn sign(&self, method: Method, url: &mut Url, expires_in: Duration) -> Result<()> { + let crypto = crypto_provider(self.crypto)?; + let date = self.date.unwrap_or_else(Utc::now); let scope = self.scope(date); @@ -285,6 +337,7 @@ impl<'a> AwsAuthorizer<'a> { let (signed_headers, canonical_headers) = canonicalize_headers(&headers); let string_to_sign = self.string_to_sign( + crypto, date, &scope, &method, @@ -292,19 +345,21 @@ impl<'a> AwsAuthorizer<'a> { &canonical_headers, &signed_headers, digest, - ); + )?; - let signature = self - .credential - .sign(&string_to_sign, date, self.region, self.service); + let signature = + self.credential + .sign(crypto, &string_to_sign, date, self.region, self.service)?; url.query_pairs_mut() .append_pair("X-Amz-Signature", &signature); + Ok(()) } #[allow(clippy::too_many_arguments)] fn string_to_sign( &self, + crypto: &dyn CryptoProvider, date: DateTime, scope: &str, request_method: &Method, @@ -312,7 +367,7 @@ impl<'a> AwsAuthorizer<'a> { canonical_headers: &str, signed_headers: &str, digest: &str, - ) -> String { + ) -> Result { // Each path segment must be URI-encoded twice (except for Amazon S3 which only gets // URI-encoded once). // see https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html @@ -334,15 +389,15 @@ impl<'a> AwsAuthorizer<'a> { digest ); - let hashed_canonical_request = hex_digest(canonical_request.as_bytes()); + let hashed_canonical_request = hex_digest(crypto, canonical_request.as_bytes())?; - format!( + Ok(format!( "{}\n{}\n{}\n{}", ALGORITHM, date.format("%Y%m%dT%H%M%SZ"), scope, hashed_canonical_request - ) + )) } fn scope(&self, date: DateTime) -> String { @@ -355,13 +410,13 @@ impl<'a> AwsAuthorizer<'a> { } } -pub(crate) trait CredentialExt { +pub(crate) trait CredentialExt: Sized { /// Sign a request fn with_aws_sigv4( self, authorizer: Option>, payload_sha256: Option<&[u8]>, - ) -> Self; + ) -> Result; } impl CredentialExt for HttpRequestBuilder { @@ -369,16 +424,16 @@ impl CredentialExt for HttpRequestBuilder { self, authorizer: Option>, payload_sha256: Option<&[u8]>, - ) -> Self { + ) -> Result { match authorizer { Some(authorizer) => { let (client, request) = self.into_parts(); let mut request = request.expect("request valid"); - authorizer.authorize(&mut request, payload_sha256); + authorizer.try_authorize(&mut request, payload_sha256)?; - Self::from_parts(client, request) + Ok(Self::from_parts(client, request)) } - None => self, + None => Ok(self), } } } @@ -816,6 +871,7 @@ pub(crate) struct SessionProvider { pub endpoint: String, pub region: String, pub credentials: AwsCredentialProvider, + pub crypto: Option>, } #[async_trait] @@ -827,12 +883,13 @@ impl TokenProvider for SessionProvider { client: &HttpClient, retry: &RetryConfig, ) -> Result>> { + let crypto = crypto_provider(self.crypto.as_deref())?; let creds = self.credentials.get_credential().await?; - let authorizer = AwsAuthorizer::new(&creds, "s3", &self.region); + let authorizer = AwsAuthorizer::new(&creds, "s3", &self.region).with_crypto(crypto); let bytes = client .get(format!("{}?session", self.endpoint)) - .with_aws_sigv4(Some(authorizer), None) + .with_aws_sigv4(Some(authorizer), None)? .send_retry(retry) .await .map_err(|source| Error::CreateSessionRequest { source })? @@ -899,6 +956,7 @@ mod tests { let signer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "ec2", region: "us-east-1", @@ -907,7 +965,7 @@ mod tests { request_payer: false, }; - signer.authorize(&mut request, None); + signer.try_authorize(&mut request, None).unwrap(); assert_eq!( request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4" @@ -943,6 +1001,7 @@ mod tests { let signer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "ec2", region: "us-east-1", @@ -951,7 +1010,7 @@ mod tests { request_payer: true, }; - signer.authorize(&mut request, None); + signer.try_authorize(&mut request, None).unwrap(); assert_eq!( request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-request-payer, Signature=7030625a9e9b57ed2a40e63d749f4a4b7714b6e15004cab026152f870dd8565d" @@ -987,6 +1046,7 @@ mod tests { let authorizer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "ec2", region: "us-east-1", @@ -995,7 +1055,7 @@ mod tests { request_payer: false, }; - authorizer.authorize(&mut request, None); + authorizer.try_authorize(&mut request, None).unwrap(); assert_eq!( request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=653c3d8ea261fd826207df58bc2bb69fbb5003e9eb3c0ef06e4a51f2a81d8699" @@ -1017,6 +1077,7 @@ mod tests { let authorizer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "s3", region: "us-east-1", @@ -1026,7 +1087,9 @@ mod tests { }; let mut url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap(); - authorizer.sign(Method::GET, &mut url, Duration::from_secs(86400)); + authorizer + .sign(Method::GET, &mut url, Duration::from_secs(86400)) + .unwrap(); assert_eq!( url, @@ -1058,6 +1121,7 @@ mod tests { let authorizer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "s3", region: "us-east-1", @@ -1067,7 +1131,9 @@ mod tests { }; let mut url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap(); - authorizer.sign(Method::GET, &mut url, Duration::from_secs(86400)); + authorizer + .sign(Method::GET, &mut url, Duration::from_secs(86400)) + .unwrap(); assert_eq!( url, @@ -1113,6 +1179,7 @@ mod tests { let authorizer = AwsAuthorizer { date: Some(date), + crypto: None, credential: &credential, service: "s3", region: "us-east-1", @@ -1121,7 +1188,7 @@ mod tests { request_payer: false, }; - authorizer.authorize(&mut request, None); + authorizer.try_authorize(&mut request, None).unwrap(); assert_eq!( request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d" diff --git a/src/aws/mod.rs b/src/aws/mod.rs index 030590ad..9dfdeb93 100644 --- a/src/aws/mod.rs +++ b/src/aws/mod.rs @@ -138,9 +138,11 @@ impl Signer for AmazonS3 { /// # } /// ``` async fn signed_url(&self, method: Method, path: &Path, expires_in: Duration) -> Result { + let crypto = self.client.config.crypto()?; let credential = self.credentials().get_credential().await?; let authorizer = AwsAuthorizer::new(&credential, "s3", &self.client.config.region) - .with_request_payer(self.client.config.request_payer); + .with_request_payer(self.client.config.request_payer) + .with_crypto(crypto); let path_url = self.path_url(path); let mut url = path_url.parse().map_err(|e| Error::Generic { @@ -148,7 +150,7 @@ impl Signer for AmazonS3 { source: format!("Unable to parse url {path_url}: {e}").into(), })?; - authorizer.sign(method, &mut url, expires_in); + authorizer.sign(method, &mut url, expires_in)?; Ok(url) } @@ -172,7 +174,7 @@ impl ObjectStore for AmazonS3 { let request = self .client .request(Method::PUT, location) - .with_payload(payload) + .with_payload(payload)? .with_attributes(attributes) .with_tags(tags) .with_extensions(extensions) diff --git a/src/azure/builder.rs b/src/azure/builder.rs index e824217f..0cea5aa7 100644 --- a/src/azure/builder.rs +++ b/src/azure/builder.rs @@ -21,7 +21,7 @@ use crate::azure::credential::{ ImdsManagedIdentityProvider, WorkloadIdentityOAuthProvider, }; use crate::azure::{AzureCredential, AzureCredentialProvider, MicrosoftAzure, STORE}; -use crate::client::{HttpConnector, TokenCredentialProvider, http_connector}; +use crate::client::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector}; use crate::config::ConfigValue; use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider}; use percent_encoding::percent_decode_str; @@ -162,6 +162,8 @@ pub struct MicrosoftAzureBuilder { client_options: ClientOptions, /// Credentials credentials: Option, + /// The [`CryptoProvider`] to use + crypto: Option>, /// Skip signing requests skip_signature: ConfigValue, /// When set to true, fabric url scheme will be used @@ -1044,6 +1046,7 @@ impl MicrosoftAzureBuilder { client_options: self.client_options, service: storage_url, credentials: auth, + crypto: self.crypto, }; let http_client = http.connect(&config.client_options)?; diff --git a/src/azure/client.rs b/src/azure/client.rs index 54ab3077..a547aae1 100644 --- a/src/azure/client.rs +++ b/src/azure/client.rs @@ -23,7 +23,9 @@ use crate::client::get::GetClient; use crate::client::header::{HeaderConfig, get_put_result}; use crate::client::list::ListClient; use crate::client::retry::{RetryContext, RetryExt}; -use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpRequest, HttpResponse}; +use crate::client::{ + CryptoProvider, GetOptionsExt, HttpClient, HttpError, HttpRequest, HttpResponse, +}; use crate::list::{PaginatedListOptions, PaginatedListResult}; use crate::multipart::PartId; use crate::util::{GetRange, deserialize_rfc1123}; @@ -160,6 +162,7 @@ impl From for crate::Error { pub(crate) struct AzureConfig { pub account: String, pub container: String, + pub crypto: Option>, pub credentials: AzureCredentialProvider, pub retry_config: RetryConfig, pub service: Url, @@ -264,10 +267,11 @@ impl PutRequest<'_> { .as_deref() .map(|c| c.sensitive_request()) .unwrap_or_default(); + let crypto = self.config.crypto.as_deref(); let response = self .builder .header(CONTENT_LENGTH, self.payload.content_length()) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(crypto, &credential, &self.config.account)? .retryable(&self.config.retry_config) .sensitive(sensitive) .idempotent(self.idempotent) @@ -521,6 +525,10 @@ impl AzureClient { self.config.get_credential().await } + pub(crate) fn crypto(&self) -> Option<&dyn CryptoProvider> { + self.config.crypto.as_deref() + } + fn put_request<'a>(&'a self, path: &'a Path, payload: PutPayload) -> PutRequest<'a> { let url = self.config.path_url(path); let builder = self.client.request(Method::PUT, url.as_str()); @@ -626,7 +634,8 @@ impl AzureClient { boundary: &str, paths: &[Path], credential: &Option>, - ) -> Vec { + ) -> Result> { + let crypto = self.crypto(); let mut body_bytes = Vec::with_capacity(paths.len() * 2048); for (idx, path) in paths.iter().enumerate() { @@ -640,7 +649,7 @@ impl AzureClient { // Each subrequest must be authorized individually [1] and we use // the CredentialExt for this. // [1]: https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id#request-body - .with_azure_authorization(credential, &self.config.account) + .with_azure_authorization(crypto, credential, &self.config.account)? .into_parts() .1 .unwrap(); @@ -658,7 +667,7 @@ impl AzureClient { extend(&mut body_bytes, boundary.as_bytes()); extend(&mut body_bytes, b"--"); extend(&mut body_bytes, b"\r\n"); - body_bytes + Ok(body_bytes) } pub(crate) async fn bulk_delete_request(&self, paths: Vec) -> Result>> { @@ -672,7 +681,7 @@ impl AzureClient { let random_bytes = rand::random::<[u8; 16]>(); // 128 bits let boundary = format!("batch_{}", BASE64_STANDARD_NO_PAD.encode(random_bytes)); - let body_bytes = self.build_bulk_delete_body(&boundary, &paths, &credential); + let body_bytes = self.build_bulk_delete_body(&boundary, &paths, &credential)?; // Send multipart request let url = self.config.path_url(&Path::from("/")); @@ -687,7 +696,7 @@ impl AzureClient { ) .header(CONTENT_LENGTH, HeaderValue::from(body_bytes.len())) .body(body_bytes) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .send_retry(&self.config.retry_config) .await .map_err(|source| Error::BulkDeleteRequest { source })?; @@ -732,7 +741,7 @@ impl AzureClient { .map(|c| c.sensitive_request()) .unwrap_or_default(); builder - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .retryable(&self.config.retry_config) .sensitive(sensitive) .idempotent(overwrite) @@ -773,7 +782,7 @@ impl AzureClient { .post(url.as_str()) .body(body) .query(&[("restype", "service"), ("comp", "userdelegationkey")]) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .retryable(&self.config.retry_config) .sensitive(sensitive) .idempotent(true) @@ -837,7 +846,7 @@ impl AzureClient { .client .get(url.as_str()) .query(&[("comp", "tags")]) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .retryable(&self.config.retry_config) .sensitive(sensitive) .send() @@ -907,7 +916,7 @@ impl GetClient for AzureClient { let response = builder .with_get_options(options) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .retryable_request() .sensitive(sensitive) .send(ctx) @@ -980,7 +989,7 @@ impl ListClient for Arc { .get(url.as_str()) .extensions(opts.extensions) .query(&query) - .with_azure_authorization(&credential, &self.config.account) + .with_azure_authorization(self.crypto(), &credential, &self.config.account)? .retryable(&self.config.retry_config) .sensitive(sensitive) .send() @@ -1254,8 +1263,7 @@ mod tests { "; - let mut _list_blobs_response_internal: ListResultInternal = - quick_xml::de::from_str(S).unwrap(); + let _list_blobs_response_internal: ListResultInternal = quick_xml::de::from_str(S).unwrap(); } #[test] @@ -1373,6 +1381,7 @@ mod tests { account: "testaccount".to_string(), container: "testcontainer".to_string(), credentials: credential_provider, + crypto: None, service: "http://example.com".try_into().unwrap(), retry_config: Default::default(), is_emulator: false, @@ -1388,7 +1397,9 @@ mod tests { let boundary = "batch_statictestboundary".to_string(); - let body_bytes = client.build_bulk_delete_body(&boundary, paths, &credential); + let body_bytes = client + .build_bulk_delete_body(&boundary, paths, &credential) + .unwrap(); // Replace Date header value with a static date let re = Regex::new("Date:[^\r]+").unwrap(); diff --git a/src/azure/credential.rs b/src/azure/credential.rs index dcc6cdd0..71d74a69 100644 --- a/src/azure/credential.rs +++ b/src/azure/credential.rs @@ -21,8 +21,10 @@ use crate::azure::STORE; use crate::client::builder::{HttpRequestBuilder, add_query_pairs}; use crate::client::retry::RetryExt; use crate::client::token::{TemporaryToken, TokenCache}; -use crate::client::{CredentialProvider, HttpClient, HttpError, HttpRequest, TokenProvider}; -use crate::util::hmac_sha256; +use crate::client::{ + CredentialProvider, CryptoProvider, DigestAlgorithm, HttpClient, HttpError, HttpRequest, + TokenProvider, crypto_provider, +}; use async_trait::async_trait; use base64::Engine; use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD}; @@ -187,7 +189,12 @@ impl AzureSigner { } } - pub(crate) fn sign(&self, method: &Method, url: &mut Url) -> Result<()> { + pub(crate) fn sign( + &self, + crypto: &dyn CryptoProvider, + method: &Method, + url: &mut Url, + ) -> crate::Result<()> { let (str_to_sign, query_pairs) = match &self.delegation_key { Some(delegation_key) => string_to_sign_user_delegation_sas( url, @@ -199,7 +206,10 @@ impl AzureSigner { ), None => string_to_sign_service_sas(url, method, &self.account, &self.start, &self.end), }; - let auth = hmac_sha256(&self.signing_key.0, str_to_sign); + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, &self.signing_key.0)?; + ctx.update(str_to_sign.as_bytes()); + let auth = ctx.finish()?; + url.query_pairs_mut().extend_pairs(query_pairs); url.query_pairs_mut() .append_pair("sig", BASE64_STANDARD.encode(auth).as_str()); @@ -223,6 +233,7 @@ fn add_date_and_version_headers(request: &mut HttpRequest) { #[derive(Debug)] pub struct AzureAuthorizer<'a> { credential: &'a AzureCredential, + crypto: Option<&'a dyn CryptoProvider>, account: &'a str, } @@ -232,23 +243,43 @@ impl<'a> AzureAuthorizer<'a> { AzureAuthorizer { credential, account, + crypto: None, } } + /// Specify the crypto provider + pub fn with_crypto(mut self, crypto: &'a dyn CryptoProvider) -> Self { + self.crypto = Some(crypto); + self + } + /// Authorize `request` + /// + /// # Panics + /// + /// Panics on cryptography error + #[deprecated(note = "use AzureAuthorizer::try_authorize")] pub fn authorize(&self, request: &mut HttpRequest) { + self.try_authorize(request).unwrap() + } + + /// Authorize `request` + pub fn try_authorize(&self, request: &mut HttpRequest) -> crate::Result<()> { + let crypto = crypto_provider(self.crypto)?; + add_date_and_version_headers(request); match self.credential { AzureCredential::AccessKey(key) => { let url = Url::parse(&request.uri().to_string()).unwrap(); let signature = generate_authorization( + crypto, request.headers(), &url, request.method(), self.account, key, - ); + )?; // "signature" is a base 64 encoded string so it should never // contain illegal characters @@ -267,53 +298,65 @@ impl<'a> AzureAuthorizer<'a> { add_query_pairs(request.uri_mut(), query_pairs); } } + Ok(()) } } -pub(crate) trait CredentialExt { +pub(crate) trait CredentialExt: Sized { /// Apply authorization to requests against azure storage accounts /// fn with_azure_authorization( self, + crypto: Option<&dyn CryptoProvider>, credential: &Option>, account: &str, - ) -> Self; + ) -> crate::Result; } impl CredentialExt for HttpRequestBuilder { fn with_azure_authorization( self, + crypto: Option<&dyn CryptoProvider>, credential: &Option>, account: &str, - ) -> Self { + ) -> crate::Result { let (client, request) = self.into_parts(); let mut request = request.expect("request valid"); match credential.as_deref() { Some(credential) => { - AzureAuthorizer::new(credential, account).authorize(&mut request); + AzureAuthorizer::new(credential, account) + .with_crypto(crypto_provider(crypto)?) + .try_authorize(&mut request)?; } None => { add_date_and_version_headers(&mut request); } } - Self::from_parts(client, request) + Ok(Self::from_parts(client, request)) } } /// Generate signed key for authorization via access keys /// fn generate_authorization( + crypto: &dyn CryptoProvider, h: &HeaderMap, u: &Url, method: &Method, account: &str, key: &AzureAccessKey, -) -> String { +) -> crate::Result { let str_to_sign = string_to_sign(h, u, method, account); - let auth = hmac_sha256(&key.0, str_to_sign); - format!("SharedKey {}:{}", account, BASE64_STANDARD.encode(auth)) + let mut ctx = crypto.hmac(DigestAlgorithm::Sha256, &key.0)?; + ctx.update(str_to_sign.as_bytes()); + let auth = ctx.finish()?; + Ok(format!( + "SharedKey {}:{}", + account, + BASE64_STANDARD.encode(auth) + )) } fn add_if_exists<'a>(h: &'a HeaderMap, key: &HeaderName) -> &'a str { diff --git a/src/azure/mod.rs b/src/azure/mod.rs index 04c8f31d..b4cc3bf4 100644 --- a/src/azure/mod.rs +++ b/src/azure/mod.rs @@ -38,9 +38,9 @@ use std::sync::Arc; use std::time::Duration; use url::Url; -use crate::client::CredentialProvider; use crate::client::get::GetClientExt; use crate::client::list::{ListClient, ListClientExt}; +use crate::client::{CredentialProvider, crypto_provider}; pub use credential::{AzureAccessKey, AzureAuthorizer, authority_hosts}; mod builder; @@ -195,9 +195,10 @@ impl Signer for MicrosoftAzure { /// # } /// ``` async fn signed_url(&self, method: Method, path: &Path, expires_in: Duration) -> Result { + let crypto = crypto_provider(self.client.crypto())?; let mut url = self.path_url(path); let signer = self.client.signer(expires_in).await?; - signer.sign(&method, &mut url)?; + signer.sign(crypto, &method, &mut url)?; Ok(url) } @@ -207,11 +208,12 @@ impl Signer for MicrosoftAzure { paths: &[Path], expires_in: Duration, ) -> Result> { + let crypto = crypto_provider(self.client.crypto())?; let mut urls = Vec::with_capacity(paths.len()); let signer = self.client.signer(expires_in).await?; for path in paths { let mut url = self.path_url(path); - signer.sign(&method, &mut url)?; + signer.sign(crypto, &method, &mut url)?; urls.push(url); } Ok(urls) diff --git a/src/client/builder.rs b/src/client/builder.rs index f74c5ec1..a86508eb 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -63,7 +63,7 @@ impl HttpRequestBuilder { } } - #[cfg(any(feature = "aws", feature = "azure"))] + #[cfg(any(feature = "aws-no-crypto", feature = "azure-no-crypto"))] pub(crate) fn from_parts(client: HttpClient, request: HttpRequest) -> Self { Self { client, @@ -116,7 +116,7 @@ impl HttpRequestBuilder { self } - #[cfg(feature = "aws")] + #[cfg(feature = "aws-no-crypto")] pub(crate) fn headers(mut self, headers: http::HeaderMap) -> Self { use http::header::{Entry, OccupiedEntry}; @@ -151,7 +151,7 @@ impl HttpRequestBuilder { self } - #[cfg(feature = "gcp")] + #[cfg(feature = "gcp-no-crypto")] pub(crate) fn bearer_auth(mut self, token: &str) -> Self { let value = HeaderValue::try_from(format!("Bearer {token}")); match (value, &mut self.request) { @@ -165,7 +165,7 @@ impl HttpRequestBuilder { self } - #[cfg(feature = "gcp")] + #[cfg(feature = "gcp-no-crypto")] pub(crate) fn json(mut self, s: S) -> Self { match (serde_json::to_vec(&s), &mut self.request) { (Ok(json), Ok(request)) => { @@ -177,7 +177,12 @@ impl HttpRequestBuilder { self } - #[cfg(any(test, feature = "aws", feature = "gcp", feature = "azure"))] + #[cfg(any( + test, + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" + ))] pub(crate) fn query(mut self, query: &T) -> Self { let mut error = None; if let Ok(ref mut req) = self.request { @@ -205,7 +210,7 @@ impl HttpRequestBuilder { self } - #[cfg(any(feature = "gcp", feature = "azure"))] + #[cfg(any(feature = "gcp-no-crypto", feature = "azure-no-crypto"))] pub(crate) fn form(mut self, form: T) -> Self { let mut error = None; if let Ok(ref mut req) = self.request { @@ -226,7 +231,11 @@ impl HttpRequestBuilder { self } - #[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] + #[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" + ))] pub(crate) fn body(mut self, b: impl Into) -> Self { if let Ok(r) = &mut self.request { *r.body_mut() = b.into(); @@ -239,7 +248,7 @@ impl HttpRequestBuilder { } } -#[cfg(any(test, feature = "azure"))] +#[cfg(any(test, feature = "azure-no-crypto"))] pub(crate) fn add_query_pairs(uri: &mut Uri, query_pairs: I) where I: IntoIterator, diff --git a/src/client/crypto.rs b/src/client/crypto.rs new file mode 100644 index 00000000..26a9a577 --- /dev/null +++ b/src/client/crypto.rs @@ -0,0 +1,252 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::Result; + +/// Algorithm for computing digests +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +#[non_exhaustive] +pub enum DigestAlgorithm { + /// SHA-256 + Sha256, +} + +/// Algorithm for signing payloads +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +#[non_exhaustive] +pub enum SigningAlgorithm { + /// RSASSA-PKCS1-v1_5 using SHA-256 + RS256, +} + +/// Provides cryptographic primitives +pub trait CryptoProvider: std::fmt::Debug + Send + Sync { + /// Compute a digest + fn digest(&self, algorithm: DigestAlgorithm) -> Result>; + + /// Compute an HMAC with the provided `secret` + fn hmac(&self, algorithm: DigestAlgorithm, secret: &[u8]) -> Result>; + + /// Sign a payload with the provided PEM-encoded secret + fn sign(&self, algorithm: SigningAlgorithm, pem: &[u8]) -> Result>; +} + +/// Incrementally compute a digest, see [`CryptoProvider::digest`] +pub trait DigestContext: Send { + ///Updates the digest with all the data in data. + /// + /// It is implementation-defined behaviour to call this after calling [`Self::finish`] + fn update(&mut self, data: &[u8]); + + /// Finalizes the digest calculation and returns the digest value. + /// + /// It is implementation-defined behaviour to call this after calling [`Self::finish`] + fn finish(&mut self) -> Result<&[u8]>; +} + +/// Incrementally compute a HMAC, see [`CryptoProvider::hmac`] +pub trait HmacContext: Send { + /// Updates the HMAC with all the data in data. + /// + /// It is implementation-defined behaviour to call this after calling [`Self::finish`] + fn update(&mut self, data: &[u8]); + + /// Finalizes the HMAC calculation and returns the HMAC value. + /// + /// It is implementation-defined behaviour to call this after calling [`Self::finish`] + fn finish(&mut self) -> Result<&[u8]>; +} + +/// Sign a payload, see [`CryptoProvider::sign`] +pub trait Signer: Send + Sync { + /// Sign the provided payload + fn sign(&self, string_to_sign: &[u8]) -> Result>; +} + +/// Attempts to find a [`CryptoProvider`] +/// +/// If `custom` is `Some(v)` returns `v` otherwise returns the compile-time default +#[cfg(feature = "ring")] +#[inline] +pub(crate) fn crypto_provider(custom: Option<&dyn CryptoProvider>) -> Result<&dyn CryptoProvider> { + match custom { + Some(x) => Ok(x), + None => Ok(&ring::PROVIDER), + } +} + +#[cfg(not(feature = "ring"))] +pub(crate) fn crypto_provider(custom: Option<&dyn CryptoProvider>) -> Result<&dyn CryptoProvider> { + match custom { + Some(x) => Ok(x), + None => Err(crate::Error::NotSupported { + source: "Must enable ring or specify custom CryptoProvider" + .to_string() + .into(), + }), + } +} + +#[cfg(feature = "ring")] +pub(crate) mod ring { + use super::*; + use ::ring::{digest, hmac, rand, signature}; + use thiserror::Error; + + #[derive(Debug, Error)] + pub(crate) enum RingError { + #[error("No RSA key found in pem file")] + MissingKey, + + #[error("Invalid RSA key: {}", source)] + InvalidKey { + #[from] + source: ::ring::error::KeyRejected, + }, + + #[error("Error reading pem file: {}", source)] + ReadPem { + source: rustls_pki_types::pem::Error, + }, + + #[error("Error signing: {}", source)] + Sign { source: ::ring::error::Unspecified }, + } + + impl From for crate::Error { + fn from(value: RingError) -> Self { + Self::Generic { + store: "RingCryptoProvider", + source: Box::new(value), + } + } + } + + pub(crate) const PROVIDER: RingCryptoProvider = RingCryptoProvider { _private: () }; + + #[derive(Debug, Default)] + pub(crate) struct RingCryptoProvider { + _private: (), + } + + impl CryptoProvider for RingCryptoProvider { + fn digest(&self, algorithm: DigestAlgorithm) -> Result> { + let algorithm = match algorithm { + DigestAlgorithm::Sha256 => &digest::SHA256, + }; + let ctx = digest::Context::new(algorithm); + Ok(Box::new(RingDigestContext { + ctx: Some(ctx), + out: None, + })) + } + + fn hmac(&self, algorithm: DigestAlgorithm, secret: &[u8]) -> Result> { + let algorithm = match algorithm { + DigestAlgorithm::Sha256 => hmac::HMAC_SHA256, + }; + let ctx = hmac::Context::with_key(&hmac::Key::new(algorithm, secret)); + Ok(Box::new(RingHmacContext { + ctx: Some(ctx), + out: None, + })) + } + + fn sign(&self, algorithm: SigningAlgorithm, pem: &[u8]) -> Result> { + match algorithm { + SigningAlgorithm::RS256 => Ok(Box::new(RsaKeyPair::from_pem(pem)?)), + } + } + } + + struct RingDigestContext { + ctx: Option, + out: Option, + } + + impl DigestContext for RingDigestContext { + fn update(&mut self, data: &[u8]) { + self.ctx.as_mut().unwrap().update(data); + } + + fn finish(&mut self) -> Result<&[u8]> { + let digest = self.ctx.take().unwrap().finish(); + Ok(digest::Digest::as_ref(self.out.insert(digest))) + } + } + + struct RingHmacContext { + ctx: Option, + out: Option, + } + + impl HmacContext for RingHmacContext { + fn update(&mut self, data: &[u8]) { + self.ctx.as_mut().unwrap().update(data); + } + + fn finish(&mut self) -> Result<&[u8]> { + let tag = self.ctx.take().unwrap().sign(); + Ok(hmac::Tag::as_ref(self.out.insert(tag))) + } + } + + /// A private RSA key for a service account + #[derive(Debug)] + pub(crate) struct RsaKeyPair(signature::RsaKeyPair); + + impl RsaKeyPair { + /// Parses a pem-encoded RSA key + pub(crate) fn from_pem(encoded: &[u8]) -> Result { + use rustls_pki_types::PrivateKeyDer; + use rustls_pki_types::pem::PemObject; + + match PrivateKeyDer::from_pem_slice(encoded) { + Ok(PrivateKeyDer::Pkcs8(key)) => Self::from_pkcs8(key.secret_pkcs8_der()), + Ok(PrivateKeyDer::Pkcs1(key)) => Self::from_der(key.secret_pkcs1_der()), + Ok(_) => Err(RingError::MissingKey), + Err(source) => Err(RingError::ReadPem { source }), + } + } + + /// Parses an unencrypted PKCS#8-encoded RSA private key. + pub(crate) fn from_pkcs8(key: &[u8]) -> Result { + Ok(Self(signature::RsaKeyPair::from_pkcs8(key)?)) + } + + /// Parses an unencrypted PKCS#8-encoded RSA private key. + pub(crate) fn from_der(key: &[u8]) -> Result { + Ok(Self(signature::RsaKeyPair::from_der(key)?)) + } + } + + impl Signer for RsaKeyPair { + fn sign(&self, string_to_sign: &[u8]) -> Result> { + let mut signature = vec![0; self.0.public().modulus_len()]; + self.0 + .sign( + &signature::RSA_PKCS1_SHA256, + &rand::SystemRandom::new(), + string_to_sign, + &mut signature, + ) + .map_err(|source| RingError::Sign { source })?; + + Ok(signature) + } + } +} diff --git a/src/client/get.rs b/src/client/get.rs index cefb07b1..3e769b39 100644 --- a/src/client/get.rs +++ b/src/client/get.rs @@ -481,7 +481,7 @@ mod tests { ); } } -#[cfg(all(test, feature = "http", not(target_arch = "wasm32")))] +#[cfg(all(test, feature = "http-no-crypto", not(target_arch = "wasm32")))] mod http_tests { use crate::client::mock_server::MockServer; use crate::client::{HttpError, HttpErrorKind, HttpResponseBody}; diff --git a/src/client/header.rs b/src/client/header.rs index 4c9470c3..2371b8cc 100644 --- a/src/client/header.rs +++ b/src/client/header.rs @@ -71,7 +71,11 @@ pub(crate) enum Error { } /// Extracts a PutResult from the provided [`HeaderMap`] -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] pub(crate) fn get_put_result( headers: &HeaderMap, version: &str, @@ -82,7 +86,11 @@ pub(crate) fn get_put_result( } /// Extracts a optional version from the provided [`HeaderMap`] -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] pub(crate) fn get_version(headers: &HeaderMap, version: &str) -> Result, Error> { Ok(match headers.get(version) { Some(x) => Some( diff --git a/src/client/http/body.rs b/src/client/http/body.rs index f59da97f..eb1b8f0a 100644 --- a/src/client/http/body.rs +++ b/src/client/http/body.rs @@ -196,7 +196,11 @@ impl HttpResponseBody { String::from_utf8(b.into()).map_err(|e| HttpError::new(HttpErrorKind::Decode, e)) } - #[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] + #[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" + ))] pub(crate) async fn json(self) -> Result { let b = self.bytes().await?; serde_json::from_slice(&b).map_err(|e| HttpError::new(HttpErrorKind::Decode, e)) diff --git a/src/client/mod.rs b/src/client/mod.rs index fcc2a089..637d6a9f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! Generic utilities for [`reqwest`] based [`ObjectStore`] implementations +//! Generic utilities for network based [`ObjectStore`] implementations //! //! [`ObjectStore`]: crate::ObjectStore @@ -30,29 +30,60 @@ pub(crate) mod mock_server; pub(crate) mod retry; -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] pub(crate) mod pagination; pub(crate) mod get; -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] pub(crate) mod list; -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] pub(crate) mod token; pub(crate) mod header; -#[cfg(any(feature = "aws", feature = "gcp"))] +#[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] pub(crate) mod s3; pub(crate) mod builder; mod http; -#[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] -pub(crate) mod parts; pub use http::*; +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] +pub(crate) mod parts; + +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] +mod crypto; + +#[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" +))] +pub use crypto::*; + use async_trait::async_trait; use reqwest::header::{HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; @@ -241,7 +272,7 @@ impl FromStr for ClientConfigKey { "timeout" => Ok(Self::Timeout), "user_agent" => Ok(Self::UserAgent), _ => Err(super::Error::UnknownConfigurationKey { - store: "HTTP", + store: "http-no-crypto", key: s.into(), }), } @@ -723,7 +754,11 @@ impl ClientOptions { /// In particular: /// * Allows HTTP as metadata endpoints do not use TLS /// * Configures a low connection timeout to provide quick feedback if not present - #[cfg(any(feature = "aws", feature = "gcp", feature = "azure"))] + #[cfg(any( + feature = "aws-no-crypto", + feature = "gcp-no-crypto", + feature = "azure-no-crypto" + ))] pub(crate) fn metadata_options(&self) -> Self { self.clone() .with_allow_http(true) @@ -926,7 +961,11 @@ where } } -#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "azure-no-crypto", + feature = "gcp-no-crypto" +))] mod cloud { use super::*; use crate::RetryConfig; @@ -952,7 +991,7 @@ mod cloud { } /// Override the minimum remaining TTL for a cached token to be used - #[cfg(any(feature = "aws", feature = "gcp"))] + #[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] pub(crate) fn with_min_ttl(mut self, min_ttl: Duration) -> Self { self.cache = self.cache.with_min_ttl(min_ttl); self @@ -983,7 +1022,11 @@ mod cloud { } use crate::client::builder::HttpRequestBuilder; -#[cfg(any(feature = "aws", feature = "azure", feature = "gcp"))] +#[cfg(any( + feature = "aws-no-crypto", + feature = "azure-no-crypto", + feature = "gcp-no-crypto" +))] pub(crate) use cloud::*; #[cfg(test)] diff --git a/src/client/retry.rs b/src/client/retry.rs index 186725b8..84c5214e 100644 --- a/src/client/retry.rs +++ b/src/client/retry.rs @@ -280,7 +280,7 @@ impl RetryableRequestBuilder { } /// Set whether this request should be retried on a 409 Conflict response. - #[cfg(feature = "aws")] + #[cfg(feature = "aws-no-crypto")] pub(crate) fn retry_on_conflict(mut self, retry_on_conflict: bool) -> Self { self.request.retry_on_conflict = retry_on_conflict; self diff --git a/src/client/s3.rs b/src/client/s3.rs index a1b113e0..4bb8bbe7 100644 --- a/src/client/s3.rs +++ b/src/client/s3.rs @@ -92,7 +92,7 @@ pub(crate) struct InitiateMultipartUploadResult { pub upload_id: String, } -#[cfg(feature = "aws")] +#[cfg(feature = "aws-no-crypto")] #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub(crate) struct CopyPartResult { diff --git a/src/client/token.rs b/src/client/token.rs index 81ffc110..832e3df2 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -52,7 +52,7 @@ impl Default for TokenCache { impl TokenCache { /// Override the minimum remaining TTL for a cached token to be used - #[cfg(any(feature = "aws", feature = "gcp"))] + #[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] pub(crate) fn with_min_ttl(self, min_ttl: Duration) -> Self { Self { min_ttl, ..self } } diff --git a/src/gcp/builder.rs b/src/gcp/builder.rs index 48a52464..857dd5a7 100644 --- a/src/gcp/builder.rs +++ b/src/gcp/builder.rs @@ -15,7 +15,9 @@ // specific language governing permissions and limitations // under the License. -use crate::client::{HttpConnector, TokenCredentialProvider, http_connector}; +use crate::client::{ + CryptoProvider, HttpConnector, TokenCredentialProvider, crypto_provider, http_connector, +}; use crate::config::ConfigValue; use crate::gcp::client::{GoogleCloudStorageClient, GoogleCloudStorageConfig}; use crate::gcp::credential::{ @@ -112,6 +114,8 @@ pub struct GoogleCloudStorageBuilder { client_options: ClientOptions, /// Credentials credentials: Option, + /// The [`CryptoProvider`] to use + crypto: Option>, /// Skip signing requests skip_signature: ConfigValue, /// Credentials for sign url @@ -240,6 +244,7 @@ impl Default for GoogleCloudStorageBuilder { url: None, base_url: None, credentials: None, + crypto: None, skip_signature: Default::default(), signing_credentials: None, http_connector: None, @@ -459,6 +464,12 @@ impl GoogleCloudStorageBuilder { self } + /// The [`CryptoProvider`] to use + pub fn with_crypto_provider(mut self, provider: Arc) -> Self { + self.crypto = Some(provider); + self + } + /// Set the retry configuration pub fn with_retry(mut self, retry_config: RetryConfig) -> Self { self.retry_config = retry_config; @@ -507,7 +518,6 @@ impl GoogleCloudStorageBuilder { } let bucket_name = self.bucket_name.ok_or(Error::MissingBucketName {})?; - let http = http_connector(self.http_connector)?; // First try to initialize from the service account information. @@ -557,8 +567,9 @@ impl GoogleCloudStorageBuilder { bearer: "".to_string(), })) as _ } else if let Some(credentials) = service_account_credentials.clone() { + let crypto = crypto_provider(self.crypto.as_deref())?; Arc::new(TokenCredentialProvider::new( - credentials.token_provider()?, + credentials.token_provider(crypto)?, http.connect(&self.client_options)?, self.retry_config.clone(), )) as _ @@ -573,8 +584,9 @@ impl GoogleCloudStorageBuilder { .with_min_ttl(TOKEN_MIN_TTL), ) as _, ApplicationDefaultCredentials::ServiceAccount(token) => { + let crypto = crypto_provider(self.crypto.as_deref())?; Arc::new(TokenCredentialProvider::new( - token.token_provider()?, + token.token_provider(crypto)?, http.connect(&self.client_options)?, self.retry_config.clone(), )) as _ @@ -599,7 +611,8 @@ impl GoogleCloudStorageBuilder { private_key: None, })) as _ } else if let Some(credentials) = service_account_credentials.clone() { - credentials.signing_credentials()? + let crypto = crypto_provider(self.crypto.as_deref())?; + credentials.signing_credentials(crypto)? } else if let Some(credentials) = application_default_credentials.clone() { match credentials { ApplicationDefaultCredentials::AuthorizedUser(token) => { @@ -610,7 +623,8 @@ impl GoogleCloudStorageBuilder { )) as _ } ApplicationDefaultCredentials::ServiceAccount(token) => { - token.signing_credentials()? + let crypto = crypto_provider(self.crypto.as_deref())?; + token.signing_credentials(crypto)? } } } else { @@ -626,6 +640,7 @@ impl GoogleCloudStorageBuilder { credentials, signing_credentials, bucket_name, + crypto: self.crypto, retry_config: self.retry_config, client_options: self.client_options, skip_signature: self.skip_signature.get()?, diff --git a/src/gcp/client.rs b/src/gcp/client.rs index 00d3168f..850e48c9 100644 --- a/src/gcp/client.rs +++ b/src/gcp/client.rs @@ -24,7 +24,7 @@ use crate::client::s3::{ CompleteMultipartUpload, CompleteMultipartUploadResult, InitiateMultipartUploadResult, ListResponse, }; -use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpResponse}; +use crate::client::{CryptoProvider, GetOptionsExt, HttpClient, HttpError, HttpResponse}; use crate::gcp::credential::CredentialExt; use crate::gcp::{GcpCredential, GcpCredentialProvider, GcpSigningCredentialProvider, STORE}; use crate::list::{PaginatedListOptions, PaginatedListResult}; @@ -142,6 +142,8 @@ pub(crate) struct GoogleCloudStorageConfig { pub signing_credentials: GcpSigningCredentialProvider, + pub crypto: Option>, + pub bucket_name: String, pub retry_config: RetryConfig, diff --git a/src/gcp/credential.rs b/src/gcp/credential.rs index 75de68c1..8ea974b7 100644 --- a/src/gcp/credential.rs +++ b/src/gcp/credential.rs @@ -19,7 +19,9 @@ use super::client::GoogleCloudStorageClient; use crate::client::builder::HttpRequestBuilder; use crate::client::retry::RetryExt; use crate::client::token::TemporaryToken; -use crate::client::{HttpClient, HttpError, TokenProvider}; +use crate::client::{ + CryptoProvider, HttpClient, HttpError, Signer, SigningAlgorithm, TokenProvider, +}; use crate::gcp::{GcpSigningCredentialProvider, STORE}; use crate::util::{STRICT_ENCODE_SET, hex_digest, hex_encode}; use crate::{RetryConfig, StaticCredentialProvider}; @@ -31,7 +33,6 @@ use futures::TryFutureExt; use http::{HeaderMap, Method}; use itertools::Itertools; use percent_encoding::utf8_percent_encode; -use ring::signature::RsaKeyPair; use serde::Deserialize; use std::collections::BTreeMap; use std::env; @@ -54,7 +55,7 @@ const DEFAULT_METADATA_HOST: &str = "metadata.google.internal"; const DEFAULT_METADATA_IP: &str = "169.254.169.254"; #[derive(Debug, thiserror::Error)] -pub enum Error { +pub(super) enum Error { #[error("Unable to open service account file from {}: {}", path.display(), source)] OpenCredentials { source: std::io::Error, @@ -64,24 +65,9 @@ pub enum Error { #[error("Unable to decode service account file: {}", source)] DecodeCredentials { source: serde_json::Error }, - #[error("No RSA key found in pem file")] - MissingKey, - - #[error("Invalid RSA key: {}", source)] - InvalidKey { - #[from] - source: ring::error::KeyRejected, - }, - - #[error("Error signing: {}", source)] - Sign { source: ring::error::Unspecified }, - #[error("Error encoding jwt payload: {}", source)] Encode { source: serde_json::Error }, - #[error("Unsupported key encoding: {}", encoding)] - UnsupportedKey { encoding: String }, - #[error("Error performing token request: {}", source)] TokenRequest { source: crate::client::retry::RetryError, @@ -89,11 +75,6 @@ pub enum Error { #[error("Error getting token response body: {}", source)] TokenResponseBody { source: HttpError }, - - #[error("Error reading pem file: {}", source)] - ReadPem { - source: rustls_pki_types::pem::Error, - }, } impl From for crate::Error { @@ -123,44 +104,43 @@ pub struct GcpSigningCredential { } /// A private RSA key for a service account -#[derive(Debug)] -pub struct ServiceAccountKey(RsaKeyPair); +pub struct ServiceAccountKey(Box); + +impl std::fmt::Debug for ServiceAccountKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ServiceAccountKey").finish_non_exhaustive() + } +} impl ServiceAccountKey { + /// Creates a [`ServiceAccountKey`] from the provided [`Signer`] + pub fn new(signer: Box) -> Self { + Self(signer) + } + /// Parses a pem-encoded RSA key - pub fn from_pem(encoded: &[u8]) -> Result { - use rustls_pki_types::PrivateKeyDer; - use rustls_pki_types::pem::PemObject; - - match PrivateKeyDer::from_pem_slice(encoded) { - Ok(PrivateKeyDer::Pkcs8(key)) => Self::from_pkcs8(key.secret_pkcs8_der()), - Ok(PrivateKeyDer::Pkcs1(key)) => Self::from_der(key.secret_pkcs1_der()), - Ok(_) => Err(Error::MissingKey), - Err(source) => Err(Error::ReadPem { source }), - } + #[cfg(feature = "ring")] + pub fn from_pem(encoded: &[u8]) -> crate::Result { + let key = crate::client::ring::RsaKeyPair::from_pem(encoded)?; + Ok(Self::new(Box::new(key))) } /// Parses an unencrypted PKCS#8-encoded RSA private key. - pub fn from_pkcs8(key: &[u8]) -> Result { - Ok(Self(RsaKeyPair::from_pkcs8(key)?)) + #[cfg(feature = "ring")] + pub fn from_pkcs8(key: &[u8]) -> crate::Result { + let key = crate::client::ring::RsaKeyPair::from_pkcs8(key)?; + Ok(Self::new(Box::new(key))) } /// Parses an unencrypted PKCS#8-encoded RSA private key. - pub fn from_der(key: &[u8]) -> Result { - Ok(Self(RsaKeyPair::from_der(key)?)) + #[cfg(feature = "ring")] + pub fn from_der(key: &[u8]) -> crate::Result { + let key = crate::client::ring::RsaKeyPair::from_der(key)?; + Ok(Self::new(Box::new(key))) } - fn sign(&self, string_to_sign: &str) -> Result { - let mut signature = vec![0; self.0.public().modulus_len()]; - self.0 - .sign( - &ring::signature::RSA_PKCS1_SHA256, - &ring::rand::SystemRandom::new(), - string_to_sign.as_bytes(), - &mut signature, - ) - .map_err(|source| Error::Sign { source })?; - + fn sign(&self, string_to_sign: &[u8]) -> crate::Result { + let signature = self.0.sign(string_to_sign)?; Ok(hex_encode(&signature)) } } @@ -287,17 +267,7 @@ impl TokenProvider for SelfSignedJwt { let claim_str = b64_encode_obj(&claims)?; let message = [jwt_header.as_ref(), claim_str.as_ref()].join("."); - let mut sig_bytes = vec![0; self.private_key.0.public().modulus_len()]; - self.private_key - .0 - .sign( - &ring::signature::RSA_PKCS1_SHA256, - &ring::rand::SystemRandom::new(), - message.as_bytes(), - &mut sig_bytes, - ) - .map_err(|source| Error::Sign { source })?; - + let sig_bytes = self.private_key.sign(message.as_bytes())?; let signature = BASE64_URL_SAFE_NO_PAD.encode(sig_bytes); let bearer = [message, signature].join("."); @@ -360,20 +330,28 @@ impl ServiceAccountCredentials { /// # References /// - /// - - pub(crate) fn token_provider(self) -> crate::Result { + pub(crate) fn token_provider( + self, + crypto: &dyn CryptoProvider, + ) -> crate::Result { + let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?; Ok(SelfSignedJwt::new( self.private_key_id, self.client_email, - ServiceAccountKey::from_pem(self.private_key.as_bytes())?, + ServiceAccountKey::new(key), DEFAULT_SCOPE.to_string(), )?) } - pub(crate) fn signing_credentials(self) -> crate::Result { + pub(crate) fn signing_credentials( + self, + crypto: &dyn CryptoProvider, + ) -> crate::Result { + let key = crypto.sign(SigningAlgorithm::RS256, self.private_key.as_bytes())?; Ok(Arc::new(StaticCredentialProvider::new( GcpSigningCredential { email: self.client_email, - private_key: Some(ServiceAccountKey::from_pem(self.private_key.as_bytes())?), + private_key: Some(ServiceAccountKey::new(key)), }, ))) } @@ -763,6 +741,7 @@ impl GCSAuthorizer { pub(crate) async fn sign( &self, + crypto: &dyn CryptoProvider, method: Method, url: &mut Url, expires_in: Duration, @@ -785,9 +764,9 @@ impl GCSAuthorizer { .append_pair("X-Goog-Expires", &expires_in.as_secs().to_string()) .append_pair("X-Goog-SignedHeaders", &signed_headers); - let string_to_sign = self.string_to_sign(date, &method, url, &headers); + let string_to_sign = self.string_to_sign(crypto, date, &method, url, &headers)?; let signature = match &self.credential.private_key { - Some(key) => key.sign(&string_to_sign)?, + Some(key) => key.sign(string_to_sign.as_bytes())?, None => client.sign_blob(&string_to_sign, email).await?, }; @@ -885,22 +864,23 @@ impl GCSAuthorizer { /// pub(crate) fn string_to_sign( &self, + crypto: &dyn CryptoProvider, date: DateTime, request_method: &Method, url: &Url, headers: &HeaderMap, - ) -> String { + ) -> crate::Result { let canonical_request = Self::canonicalize_request(url, request_method, headers); - let hashed_canonical_req = hex_digest(canonical_request.as_bytes()); + let hashed_canonical_req = hex_digest(crypto, canonical_request.as_bytes())?; let scope = self.scope(date); - format!( + Ok(format!( "{}\n{}\n{}\n{}", "GOOG4-RSA-SHA256", date.format("%Y%m%dT%H%M%SZ"), scope, hashed_canonical_req - ) + )) } } diff --git a/src/gcp/mod.rs b/src/gcp/mod.rs index 2fb74b4f..9aace3ed 100644 --- a/src/gcp/mod.rs +++ b/src/gcp/mod.rs @@ -37,7 +37,7 @@ use std::sync::Arc; use std::time::Duration; -use crate::client::CredentialProvider; +use crate::client::{CredentialProvider, crypto_provider}; use crate::gcp::credential::GCSAuthorizer; use crate::signer::Signer; use crate::{CopyMode, CopyOptions}; @@ -280,8 +280,9 @@ impl Signer for GoogleCloudStorage { let signing_credentials = self.signing_credentials().get_credential().await?; let authorizer = GCSAuthorizer::new(signing_credentials); + let crypto = crypto_provider(self.client.config().crypto.as_deref())?; authorizer - .sign(method, &mut url, expires_in, &self.client) + .sign(crypto, method, &mut url, expires_in, &self.client) .await?; Ok(url) diff --git a/src/lib.rs b/src/lib.rs index 5ad9d323..2ef00a35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,19 +80,19 @@ doc = "* Local filesystem: [`LocalFileSystem`](local::LocalFileSystem)" )] #![cfg_attr( - feature = "gcp", + feature = "gcp-no-crypto", doc = "* [`gcp`]: [Google Cloud Storage](https://cloud.google.com/storage/) support. See [`GoogleCloudStorageBuilder`](gcp::GoogleCloudStorageBuilder)" )] #![cfg_attr( - feature = "aws", + feature = "aws-no-crypto", doc = "* [`aws`]: [Amazon S3](https://aws.amazon.com/s3/). See [`AmazonS3Builder`](aws::AmazonS3Builder)" )] #![cfg_attr( - feature = "azure", + feature = "azure-no-crypto", doc = "* [`azure`]: [Azure Blob Storage](https://azure.microsoft.com/en-gb/services/storage/blobs/). See [`MicrosoftAzureBuilder`](azure::MicrosoftAzureBuilder)" )] #![cfg_attr( - feature = "http", + feature = "http-no-crypto", doc = "* [`http`]: [HTTP/WebDAV Storage](https://datatracker.ietf.org/doc/html/rfc2518). See [`HttpBuilder`](http::HttpBuilder)" )] //! @@ -134,7 +134,7 @@ //! application complexity. //! //! ```no_run,ignore-wasm32 -//! # #[cfg(feature = "aws")] { +//! # #[cfg(feature = "aws-no-crypto")] { //! # use url::Url; //! # use object_store::{parse_url, parse_url_opts}; //! # use object_store::aws::{AmazonS3, AmazonS3Builder}; @@ -535,17 +535,17 @@ //! //! [`HttpConnector`]: client::HttpConnector -#[cfg(feature = "aws")] +#[cfg(feature = "aws-no-crypto")] pub mod aws; -#[cfg(feature = "azure")] +#[cfg(feature = "azure-no-crypto")] pub mod azure; pub mod buffered; #[cfg(not(target_arch = "wasm32"))] pub mod chunked; pub mod delimited; -#[cfg(feature = "gcp")] +#[cfg(feature = "gcp-no-crypto")] pub mod gcp; -#[cfg(feature = "http")] +#[cfg(feature = "http-no-crypto")] pub mod http; pub mod limit; #[cfg(all(feature = "fs", not(target_arch = "wasm32")))] @@ -554,23 +554,23 @@ pub mod memory; pub mod path; pub mod prefix; pub mod registry; -#[cfg(feature = "cloud")] +#[cfg(feature = "cloud-no-crypto")] pub mod signer; pub mod throttle; -#[cfg(feature = "cloud")] +#[cfg(feature = "cloud-no-crypto")] pub mod client; -#[cfg(feature = "cloud")] +#[cfg(feature = "cloud-no-crypto")] pub use client::{ ClientConfigKey, ClientOptions, CredentialProvider, StaticCredentialProvider, backoff::BackoffConfig, retry::RetryConfig, }; -#[cfg(all(feature = "cloud", not(target_arch = "wasm32")))] +#[cfg(all(feature = "cloud-no-crypto", not(target_arch = "wasm32")))] pub use client::Certificate; -#[cfg(feature = "cloud")] +#[cfg(feature = "cloud-no-crypto")] mod config; mod tags; @@ -2174,7 +2174,7 @@ mod tests { store.list(Some(&path)) } - #[cfg(any(feature = "azure", feature = "aws"))] + #[cfg(any(feature = "azure-no-crypto", feature = "aws-no-crypto"))] pub(crate) async fn signing(integration: &T) where T: ObjectStore + signer::Signer, @@ -2197,7 +2197,7 @@ mod tests { assert_eq!(data, loaded); } - #[cfg(any(feature = "aws", feature = "azure"))] + #[cfg(any(feature = "aws-no-crypto", feature = "azure-no-crypto"))] pub(crate) async fn tagging(storage: Arc, validate: bool, get_tags: F) where F: Fn(Path) -> Fut + Send + Sync, @@ -2370,7 +2370,7 @@ mod tests { } #[test] - #[cfg(feature = "http")] + #[cfg(feature = "http-no-crypto")] fn test_reexported_types() { // Test HeaderMap let mut headers = HeaderMap::new(); diff --git a/src/parse.rs b/src/parse.rs index b21a8e24..d7b8a348 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -138,7 +138,7 @@ impl ObjectStoreScheme { } } -#[cfg(feature = "cloud")] +#[cfg(feature = "cloud-no-crypto")] macro_rules! builder_opts { ($builder:ty, $url:expr, $options:expr) => {{ let builder = $options.into_iter().fold( @@ -201,29 +201,29 @@ where #[cfg(all(feature = "fs", not(target_arch = "wasm32")))] ObjectStoreScheme::Local => Box::new(LocalFileSystem::new()) as _, ObjectStoreScheme::Memory => Box::new(InMemory::new()) as _, - #[cfg(feature = "aws")] + #[cfg(feature = "aws-no-crypto")] ObjectStoreScheme::AmazonS3 => { builder_opts!(crate::aws::AmazonS3Builder, url, _options) } - #[cfg(feature = "gcp")] + #[cfg(feature = "gcp-no-crypto")] ObjectStoreScheme::GoogleCloudStorage => { builder_opts!(crate::gcp::GoogleCloudStorageBuilder, url, _options) } - #[cfg(feature = "azure")] + #[cfg(feature = "azure-no-crypto")] ObjectStoreScheme::MicrosoftAzure => { builder_opts!(crate::azure::MicrosoftAzureBuilder, url, _options) } - #[cfg(feature = "http")] + #[cfg(feature = "http-no-crypto")] ObjectStoreScheme::Http => { let url = &url[..url::Position::BeforePath]; builder_opts!(crate::http::HttpBuilder, url, _options) } #[cfg(not(all( feature = "fs", - feature = "aws", - feature = "azure", - feature = "gcp", - feature = "http", + feature = "aws-no-crypto", + feature = "azure-no-crypto", + feature = "gcp-no-crypto", + feature = "http-no-crypto", not(target_arch = "wasm32") )))] s => { @@ -399,7 +399,7 @@ mod tests { } #[tokio::test] - #[cfg(all(feature = "http", not(target_arch = "wasm32")))] + #[cfg(all(feature = "http-no-crypto", not(target_arch = "wasm32")))] async fn test_url_http() { use crate::{ObjectStoreExt, client::mock_server::MockServer}; use http::{Response, header::USER_AGENT}; diff --git a/src/util.rs b/src/util.rs index b7f9182c..dd6b104b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -25,11 +25,11 @@ use super::Result; use bytes::Bytes; use futures::{Stream, TryStreamExt, stream::StreamExt}; -#[cfg(any(feature = "azure", feature = "http"))] +#[cfg(any(feature = "azure-no-crypto", feature = "http-no-crypto"))] pub(crate) static RFC1123_FMT: &str = "%a, %d %h %Y %T GMT"; // deserialize dates according to rfc1123 -#[cfg(any(feature = "azure", feature = "http"))] +#[cfg(any(feature = "azure-no-crypto", feature = "http-no-crypto"))] pub(crate) fn deserialize_rfc1123<'de, D>( deserializer: D, ) -> Result, D::Error> @@ -42,12 +42,6 @@ where Ok(chrono::TimeZone::from_utc_datetime(&chrono::Utc, &naive)) } -#[cfg(any(feature = "aws", feature = "azure"))] -pub(crate) fn hmac_sha256(secret: impl AsRef<[u8]>, bytes: impl AsRef<[u8]>) -> ring::hmac::Tag { - let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret.as_ref()); - ring::hmac::sign(&key, bytes.as_ref()) -} - /// Collect a stream into [`Bytes`] avoiding copying in the event of a single chunk pub async fn collect_bytes(mut stream: S, size_hint: Option) -> Result where @@ -300,7 +294,7 @@ impl> From for GetRange { // // Do not URI-encode any of the unreserved characters that RFC 3986 defines: // A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ), and tilde ( ~ ). -#[cfg(any(feature = "aws", feature = "gcp"))] +#[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] pub(crate) const STRICT_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC .remove(b'-') .remove(b'.') @@ -308,14 +302,18 @@ pub(crate) const STRICT_ENCODE_SET: percent_encoding::AsciiSet = percent_encodin .remove(b'~'); /// Computes the SHA256 digest of `body` returned as a hex encoded string -#[cfg(any(feature = "aws", feature = "gcp"))] -pub(crate) fn hex_digest(bytes: &[u8]) -> String { - let digest = ring::digest::digest(&ring::digest::SHA256, bytes); - hex_encode(digest.as_ref()) +#[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] +pub(crate) fn hex_digest( + crypto: &dyn crate::client::CryptoProvider, + bytes: &[u8], +) -> Result { + let mut ctx = crypto.digest(crate::client::DigestAlgorithm::Sha256)?; + ctx.update(bytes); + Ok(hex_encode(ctx.finish()?)) } /// Returns `bytes` as a lower-case hex encoded string -#[cfg(any(feature = "aws", feature = "gcp"))] +#[cfg(any(feature = "aws-no-crypto", feature = "gcp-no-crypto"))] pub(crate) fn hex_encode(bytes: &[u8]) -> String { use std::fmt::Write; let mut out = String::with_capacity(bytes.len() * 2);