This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git
The following commit(s) were added to refs/heads/main by this push:
new 996e084 Pluggable Crypto / Update reqwest 0.13 (#707)
996e084 is described below
commit 996e084600a4adc7e14cf34d332d3cb0972547b4
Author: Geoffry Song <[email protected]>
AuthorDate: Wed Jun 17 12:42:15 2026 -0700
Pluggable Crypto / Update reqwest 0.13 (#707)
* Pluggable Crypto
* Add CI
* Upgrade reqwest to 0.13
* Certificates
* Switch to aws-lc-rs
* Fix CI
* Fix doc
* Remove reqwest/rustls-no-provider feature dependency
* Compile with reqwest/rustls feature in CI when not selecting a crypto
backend
* Hack around WASIp1 CI by downgrading reqwest
* Update docs
* Improve
* Document how to use flags
* fixes
* Updates
* typo
* docs
* Clarify base feature crypto and HTTP requirements
* revert non fips
* Improve comments, and fix clippy
* Use pluggable crypto API for Azure CPK key digest instead of ring
The merged CPK support (#742) computed the encryption key SHA-256 via
ring::digest directly, which fails to compile on this branch since
azure-base no longer depends on ring. Route it through the CryptoProvider
abstraction (DigestAlgorithm::Sha256) instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Raphael Taylor-Davies <[email protected]>
Co-authored-by: Adam Gutglick <[email protected]>
Co-authored-by: Andrew Lamb <[email protected]>
Co-authored-by: Kevin Liu <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.github/workflows/ci.yml | 103 ++++++++++--
Cargo.toml | 39 +++--
src/aws/builder.rs | 12 +-
src/aws/client.rs | 112 ++++++++-----
src/aws/credential.rs | 141 +++++++++++-----
src/aws/mod.rs | 8 +-
src/azure/builder.rs | 19 ++-
src/azure/client.rs | 71 +++++---
src/azure/credential.rs | 120 ++++++++++++--
src/azure/mod.rs | 8 +-
src/client/crypto.rs | 410 +++++++++++++++++++++++++++++++++++++++++++++++
src/client/mod.rs | 61 ++++++-
src/gcp/builder.rs | 27 +++-
src/gcp/client.rs | 4 +-
src/gcp/credential.rs | 273 ++++++++++++++++++++++---------
src/gcp/mod.rs | 5 +-
src/lib.rs | 124 +++++++++++---
src/util.rs | 16 +-
18 files changed, 1272 insertions(+), 281 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c14df42..60be27e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,55 @@ jobs:
- uses: actions/checkout@v6
- name: Setup Clippy
run: rustup component add clippy
+ - name: Verify base features do not enable TLS/signing crypto crates
+ # `--prefix none -f '{p}'` prints one `name vX.Y.Z` per line, so the
+ # crate name can be anchored with `^`. Avoid `\b`, which is not
+ # portable (e.g. BSD grep) and would also substring-match crates like
+ # `iri-string`.
+ run: |
+ found=$(cargo tree --no-default-features --features
aws-base,azure-base,gcp-base,http-base --edges normal --prefix none -f '{p}' \
+ | grep -E '^(aws-lc-rs|aws-lc-sys|native-tls|openssl|ring|rustls)
v' | sort -u || true)
+ if [ -n "$found" ]; then
+ echo "❌ disallowed crate(s) found:"
+ echo "$found"
+ exit 1
+ fi
+ echo "✅ no TLS/signing crypto crates"
+ - name: Verify base features do not enable reqwest
+ run: |
+ found=$(cargo tree --no-default-features --features
aws-base,azure-base,gcp-base,http-base --edges normal --prefix none -f '{p}' \
+ | grep -E '^reqwest v' | sort -u || true)
+ if [ -n "$found" ]; then
+ echo "❌ reqwest must not be enabled by *-base features:"
+ echo "$found"
+ exit 1
+ fi
+ echo "✅ *-base features do not enable reqwest"
+ - name: Check ring build does not pull aws-lc-rs
+ # A `ring` build (with a non-aws-lc-rs reqwest TLS backend) must be
free
+ # of aws-lc-rs entirely, so users can opt out of it on every target.
+ run: |
+ found=$(cargo tree --no-default-features --features
aws-base,azure-base,gcp-base,http-base,reqwest,reqwest/native-tls,ring --edges
normal --prefix none -f '{p}' \
+ | grep -E '^(aws-lc-rs|aws-lc-sys) v' | sort -u || true)
+ if [ -n "$found" ]; then
+ echo "❌ aws-lc-rs pulled into a ring-only build:"
+ echo "$found"
+ exit 1
+ fi
+ echo "✅ ring build is free of aws-lc-rs"
+ - name: Check batteries-included features do not pull ring
+ # The batteries-included features use reqwest/rustls, which uses
aws-lc-rs
+ # for TLS by default. Keep object_store's default bundled crypto
provider
+ # aligned with that choice by ensuring these features don't pull ring.
+ run: |
+ found=$(cargo tree --no-default-features --features
aws,azure,gcp,http --edges normal --prefix none -f '{p}' \
+ | grep -E '^ring v' | sort -u || true)
+ if [ -n "$found" ]; then
+ echo "❌ batteries-included features pulled ring:"
+ echo "$found"
+ exit 1
+ fi
+ echo "✅ batteries-included features are free of ring"
# Run different tests for the library on its own as well as
# all targets to ensure that it still works in the absence of
# features that might be enabled by dev-dependencies of other
@@ -65,18 +114,23 @@ jobs:
run: cargo clippy --no-default-features --features gcp-base -- -D
warnings
- name: Run clippy with http-base feature
run: cargo clippy --no-default-features --features http-base -- -D
warnings
- - name: Verify base features do not enable reqwest
- run: |
- if cargo tree \
- --no-default-features \
- --features aws-base,azure-base,gcp-base,http-base \
- --edges normal \
- --prefix none \
- -f '{p}' \
- | grep -E '^reqwest v'; then
- echo "reqwest must not be enabled by *-base features"
- exit 1
- fi
+ # Testing matrix of HTTP and crypto providers
+ #
+ # Improvements tracked in
https://githubcom/apache/arrow-rs-object-store/issues/759
+ #
+ # Notes:
+ # Direct `reqwest` needs a TLS backend. Use `rustls-no-provider` so TLS
+ # does not add AWS-LC to ring/custom CryptoProvider checks.
+ - name: Run clippy with base features, reqwest and ring CryptoProvider
+ run: cargo clippy --no-default-features --features
aws-base,azure-base,gcp-base,http-base,reqwest,reqwest/rustls-no-provider,ring
-- -D warnings
+ - name: Run clippy with base features, reqwest and no bundled
CryptoProvider
+ run: cargo clippy --no-default-features --features
aws-base,azure-base,gcp-base,http-base,reqwest,reqwest/rustls-no-provider -- -D
warnings
+ - name: Run clippy with base features, custom HTTP and aws-lc-rs
CryptoProvider
+ run: cargo clippy --no-default-features --features
aws-base,azure-base,gcp-base,http-base,aws-lc-rs,aws-lc-rs/aws-lc-sys -- -D
warnings
+ - name: Run clippy with base features, custom HTTP and ring
CryptoProvider
+ run: cargo clippy --no-default-features --features
aws-base,azure-base,gcp-base,http-base,ring -- -D warnings
+ - name: Run clippy with base features, custom HTTP and no bundled
CryptoProvider
+ run: cargo clippy --no-default-features --features
aws-base,azure-base,gcp-base,http-base -- -D warnings
- name: Run clippy with integration feature
run: cargo clippy --no-default-features --features integration -- -D
warnings
- name: Run clippy with all features
@@ -159,7 +213,7 @@ jobs:
- name: Configure Azurite (Azure emulation)
# the magical connection string is from
#
https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio#http-connection-strings
- # We skip the API version check to prevent breaks related to
differences between Azurite, Azure and the azure-cli,
+ # We skip the API version check to prevent breaks related to
differences between Azurite, Azure and the azure-cli,
# see https://github.com/Azure/Azurite/issues/2623
run: |
echo "AZURITE_CONTAINER=$(docker run -d -p 10000:10000 -p
10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite azurite -l
/data --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
--skipApiVersionCheck)" >> $GITHUB_ENV
@@ -180,6 +234,17 @@ jobs:
AWS_CONDITIONAL_PUT: etag
AWS_COPY_IF_NOT_EXISTS: multipart
+ # Exercise the `ring` crypto provider at runtime (the other jobs only
+ # compile it)
+ #
+ # Notes:
+ # * Use `reqwest/native-tls` to avoid aws-lc-rs anywhere in the tree
+ #
+ # * use `env -u TEST_INTEGRATION` to run only the (crypto) unit tests
+ # as the integration tests already ran above.
+ - name: Run object_store tests with ring crypto provider
+ run: env -u TEST_INTEGRATION cargo test --lib --tests
--no-default-features --features
fs,aws-base,azure-base,gcp-base,http-base,reqwest,reqwest/native-tls,ring
+
- name: GCS Output
if: ${{ !cancelled() }}
run: docker logs $GCS_CONTAINER
@@ -212,10 +277,18 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- name: Build wasm32-unknown-unknown
run: cargo build --target wasm32-unknown-unknown
+ # Note: we don't `cargo build` the cloud features for
wasm32-unknown-unknown.
+ # They pull in `rand` -> `getrandom`, which on this target only compiles
with
+ # its `wasm_js` feature enabled. We can't enable that via `--features`
because
+ # `getrandom` is a transitive dependency (pulled in by `rand`), not
direct
- name: Install wasm32-wasip1
run: rustup target add wasm32-wasip1
+ - name: Use reqwest 0.13.2 for wasm32-wasip1
+ # TODO: reqwest 0.13.3+ doesn't compile against wasm32-wasip1
+ run: cargo update -p reqwest --precise 0.13.2
- name: Build wasm32-wasip1
- run: cargo build --all-features --target wasm32-wasip1
+ # aws-lc-rs does not build for wasm32-wasip1, so use the ring crypto
provider here
+ run: cargo build --no-default-features --features
aws-base,gcp-base,azure-base,http-base,reqwest,ring --target wasm32-wasip1
- name: Build wasm32-wasip1 without reqwest
run: cargo build --no-default-features --features aws-base --target
wasm32-wasip1
- name: Install wasm-pack
@@ -224,7 +297,7 @@ jobs:
with:
node-version: 20
- name: Run wasm32-unknown-unknown tests (via Node)
- run: wasm-pack test --node --features http --no-default-features
+ run: wasm-pack test --node --features http-base,reqwest,ring
--no-default-features
windows:
name: cargo test LocalFileSystem (win64)
diff --git a/Cargo.toml b/Cargo.toml
index e115cae..0b8d7f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,8 +55,9 @@ hyper = { version = "1.2", default-features = false, optional
= true }
md-5 = { version = "0.11.0", default-features = false, optional = true }
quick-xml = { version = "0.40.1", features = ["serialize",
"overlapped-lists"], optional = true }
rand = { version = "0.10", 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.13", default-features = false, features = ["http2"],
optional = true }
ring = { version = "0.17", default-features = false, features = ["std"],
optional = true }
+aws-lc-rs = { version = "1.15", default-features = false, 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 }
serde_json = { version = "1.0", default-features = false, features = ["std"],
optional = true }
@@ -84,29 +85,36 @@ futures-channel = {version = "0.3", features = ["sink"]}
default = ["fs"]
# Shared cloud/provider implementation dependencies.
-# This intentionally does NOT include reqwest.
-cloud-base = ["serde", "serde_json", "quick-xml", "hyper", "chrono/serde",
"base64", "rand", "ring", "http-body-util", "form_urlencoded",
"serde_urlencoded", "tokio"]
+# This intentionally does NOT include reqwest or a bundled crypto provider.
+cloud-base = ["serde", "serde_json", "quick-xml", "hyper", "chrono/serde",
"base64", "rand", "http-body-util", "form_urlencoded", "serde_urlencoded",
"tokio"]
# Built-in reqwest-based HTTP transport.
reqwest = ["dep:reqwest", "reqwest/stream"]
-# Provider base features.
-# These compile provider logic without forcing reqwest.
+# Bundled crypto providers.
+# Selecting one provides the default `CryptoProvider`; otherwise a custom
+# provider must be supplied at runtime.
+aws-lc-rs = ["dep:aws-lc-rs", "rustls-pki-types"]
+ring = ["dep:ring", "rustls-pki-types"]
+
+# Implementation base features.
+# These compile provider logic without forcing reqwest or a crypto provider.
azure-base = ["cloud-base", "httparse"]
-gcp-base = ["cloud-base", "rustls-pki-types"]
+gcp-base = ["cloud-base"]
aws-base = ["cloud-base", "crc-fast", "md-5"]
http-base = ["cloud-base"]
-# Batteries-included provider features.
-# These preserve existing behavior: enabling aws/azure/gcp/http gives
-# the default reqwest transport.
-azure = ["azure-base", "reqwest"]
-gcp = ["gcp-base", "reqwest"]
-aws = ["aws-base", "reqwest"]
-http = ["http-base", "reqwest"]
+# "Batteries-included" implementation features.
+# Each enables the default `reqwest` transport (with rustls) and `aws-lc-rs`.
+# This keeps object_store's bundled crypto provider aligned with
reqwest/rustls,
+# which uses aws-lc-rs for TLS by default.
+azure = ["azure-base", "reqwest", "reqwest/rustls", "aws-lc-rs"]
+gcp = ["gcp-base", "reqwest", "reqwest/rustls", "aws-lc-rs"]
+aws = ["aws-base", "reqwest", "reqwest/rustls", "aws-lc-rs"]
+# HTTP/WebDAV doesn't sign object_store requests, but reqwest/rustls uses
aws-lc-rs by default.
+http = ["http-base", "reqwest", "reqwest/rustls", "aws-lc-rs"]
fs = ["walkdir", "tokio", "nix", "windows-sys"]
-tls-webpki-roots = ["reqwest?/rustls-tls-webpki-roots"]
integration = ["rand", "tokio"]
tokio = ["dep:tokio", "dep:tracing"]
@@ -117,8 +125,9 @@ hyper-util = "0.1"
rand = "0.10"
tempfile = "3.1.0"
regex = "1.11.1"
+webpki-root-certs = "1"
# The "gzip" feature for reqwest is enabled for an integration test.
-reqwest = { version = "0.12", default-features = false, features = ["gzip"] }
+reqwest = { version = "0.13", default-features = false, features = ["gzip"] }
[target.'cfg(all(target_arch = "wasm32", target_os =
"unknown"))'.dev-dependencies]
wasm-bindgen-test = "0.3.50"
diff --git a/src/aws/builder.rs b/src/aws/builder.rs
index c9fd6e3..b660a6c 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;
@@ -173,6 +173,8 @@ pub struct AmazonS3Builder {
client_options: ClientOptions,
/// Credentials
credentials: Option<AwsCredentialProvider>,
+ /// The [`CryptoProvider`] to use
+ crypto: Option<Arc<dyn CryptoProvider>>,
/// Skip signing requests
skip_signature: ConfigValue<bool>,
/// Copy if not exists
@@ -894,6 +896,12 @@ impl AmazonS3Builder {
self
}
+ /// The [`CryptoProvider`] to use
+ pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>)
-> Self {
+ self.crypto = Some(provider);
+ self
+ }
+
/// Sets what protocol is allowed.
///
/// If `allow_http` is :
@@ -1226,6 +1234,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(),
@@ -1269,6 +1278,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 f72155f..4ea3b88 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;
@@ -199,6 +200,7 @@ pub(crate) struct S3Config {
pub bucket: String,
pub bucket_endpoint: String,
pub credentials: AwsCredentialProvider,
+ pub crypto: Option<Arc<dyn CryptoProvider>>,
pub session_provider: Option<AwsCredentialProvider>,
pub retry_config: RetryConfig,
pub client_options: ClientOptions,
@@ -218,19 +220,18 @@ impl S3Config {
format!("{}/{}", self.bucket_endpoint, encode_path(path))
}
- async fn get_session_credential(&self) -> Result<SessionCredential<'_>> {
- let credential = match self.skip_signature {
+ async fn get_session_credential(&self) ->
Result<Option<SessionCredential<'_>>> {
+ 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,
})
}
@@ -245,27 +246,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<Arc<AwsCredential>>,
+ credential: Arc<AwsCredential>,
session_token: bool,
config: &'a S3Config,
}
impl SessionCredential<'_> {
- fn authorizer(&self) -> Option<AwsAuthorizer<'_>> {
+ fn authorizer(&self) -> Result<AwsAuthorizer<'_>> {
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)
}
}
@@ -293,12 +299,12 @@ impl From<RequestError> for crate::Error {
}
}
-/// A builder for a request allowing customisation of the headers and query
string
+/// A builder for a request allowing customization of the headers and query
string
pub(crate) struct Request<'a> {
path: &'a Path,
config: &'a S3Config,
builder: HttpRequestBuilder,
- payload_sha256: Option<digest::Digest>,
+ payload_sha256: Option<[u8; 32]>,
payload: Option<PutPayload>,
use_session_creds: bool,
idempotent: bool,
@@ -399,26 +405,30 @@ impl Request<'_> {
Self { builder, ..self }
}
- pub(crate) fn with_payload(mut self, payload: PutPayload) -> Self {
- use std::cell::LazyCell;
-
- let sha256_digest = LazyCell::new(|| {
- let mut sha256 = Context::new(&digest::SHA256);
+ pub(crate) fn with_payload(mut self, payload: PutPayload) -> Result<Self> {
+ let mut cached_digest: Option<[u8; 32]> = None;
+ let mut sha256_digest = || -> Result<[u8; 32]> {
+ if let Some(digest) = cached_digest {
+ return Ok(digest);
+ }
+ let mut ctx =
self.config.crypto()?.digest(DigestAlgorithm::Sha256)?;
for part in &payload {
- sha256.update(part);
+ ctx.update(part);
}
- sha256.finish()
- });
+ let digest = ctx.finish()?.try_into().unwrap();
+ cached_digest = Some(digest);
+ Ok(digest)
+ };
if !self.config.skip_signature && self.config.sign_payload {
- self.payload_sha256 = Some(*sha256_digest);
+ self.payload_sha256 = Some(sha256_digest()?);
}
match self.config.checksum {
Some(Checksum::SHA256) => {
self.builder = self
.builder
- .header(SHA256_CHECKSUM,
BASE64_STANDARD.encode(*sha256_digest));
+ .header(SHA256_CHECKSUM,
BASE64_STANDARD.encode(sha256_digest()?));
}
Some(Checksum::CRC64NVME) => {
let crc_algo = crc_fast::CrcAlgorithm::Crc64Nvme;
@@ -437,24 +447,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<HttpResponse, RequestError> {
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)
@@ -520,6 +534,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();
@@ -566,7 +581,11 @@ impl S3Client {
builder = builder.headers(headers.clone());
}
- 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:
@@ -580,7 +599,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))?
.retryable(&self.config.retry_config)
.retry_error_body(true)
.send()
@@ -735,7 +754,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)),
@@ -831,6 +850,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 mut builder = self.client.post(url);
@@ -841,7 +861,7 @@ impl S3Client {
let request = builder
.query(&[("uploadId", upload_id)])
.body(body)
- .with_aws_sigv4(credential.authorizer(), None);
+ .with_aws_sigv4(authorizer, None)?;
let request = match mode {
CompleteMultipartMode::Overwrite => request,
@@ -882,11 +902,12 @@ impl S3Client {
#[cfg(test)]
pub(crate) async fn get_object_tagging(&self, path: &Path) ->
Result<HttpResponse> {
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()))?;
@@ -917,6 +938,7 @@ impl GetClient for S3Client {
options: GetOptions,
) -> Result<HttpResponse> {
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,
@@ -942,7 +964,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
@@ -961,6 +983,7 @@ impl ListClient for Arc<S3Client> {
opts: PaginatedListOptions,
) -> Result<PaginatedListResult> {
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);
@@ -994,7 +1017,7 @@ impl ListClient for Arc<S3Client> {
.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 })?;
@@ -1080,6 +1103,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()));
@@ -1125,6 +1149,7 @@ mod tests {
client_options: ClientOptions::new()
.with_allow_http(true)
.with_default_headers(default_headers),
+ crypto: None,
skip_signature: false,
session_provider: None,
retry_config: Default::default(),
@@ -1157,6 +1182,7 @@ mod tests {
let result = client
.request(Method::PUT, &Path::from("test"))
.with_payload(PutPayload::default())
+ .unwrap()
.do_put()
.await;
diff --git a/src/aws/credential.rs b/src/aws/credential.rs
index df2ea67..c16330a 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
///
///
<https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html>
- fn sign(&self, to_sign: &str, date: DateTime<Utc>, region: &str, service:
&str) -> String {
+ fn sign(
+ &self,
+ crypto: &dyn CryptoProvider,
+ to_sign: &str,
+ date: DateTime<Utc>,
+ region: &str,
+ service: &str,
+ ) -> Result<String> {
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<DateTime<Utc>>,
credential: &'a AwsCredential,
+ crypto: Option<&'a dyn CryptoProvider>,
service: &'a str,
region: &'a str,
token_header: Option<HeaderName>,
@@ -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<Utc>,
scope: &str,
request_method: &Method,
@@ -312,7 +367,7 @@ impl<'a> AwsAuthorizer<'a> {
canonical_headers: &str,
signed_headers: &str,
digest: &str,
- ) -> String {
+ ) -> Result<String> {
// 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<Utc>) -> String {
@@ -355,13 +410,13 @@ impl<'a> AwsAuthorizer<'a> {
}
}
-pub(crate) trait CredentialExt {
+pub(crate) trait CredentialExt: Sized {
/// Sign a request
<https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html>
fn with_aws_sigv4(
self,
authorizer: Option<AwsAuthorizer<'_>>,
payload_sha256: Option<&[u8]>,
- ) -> Self;
+ ) -> Result<Self>;
}
impl CredentialExt for HttpRequestBuilder {
@@ -369,16 +424,16 @@ impl CredentialExt for HttpRequestBuilder {
self,
authorizer: Option<AwsAuthorizer<'_>>,
payload_sha256: Option<&[u8]>,
- ) -> Self {
+ ) -> Result<Self> {
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<Arc<dyn CryptoProvider>>,
}
#[async_trait]
@@ -827,12 +883,13 @@ impl TokenProvider for SessionProvider {
client: &HttpClient,
retry: &RetryConfig,
) -> Result<TemporaryToken<Arc<Self::Credential>>> {
+ 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 })?
@@ -901,6 +958,7 @@ mod tests {
let signer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "ec2",
region: "us-east-1",
@@ -909,7 +967,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"
@@ -946,6 +1004,7 @@ mod tests {
let signer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "ec2",
region: "us-east-1",
@@ -954,7 +1013,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"
@@ -991,6 +1050,7 @@ mod tests {
let authorizer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "ec2",
region: "us-east-1",
@@ -999,7 +1059,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"
@@ -1021,6 +1081,7 @@ mod tests {
let authorizer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "s3",
region: "us-east-1",
@@ -1030,7 +1091,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,
@@ -1062,6 +1125,7 @@ mod tests {
let authorizer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "s3",
region: "us-east-1",
@@ -1071,7 +1135,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,
@@ -1118,6 +1184,7 @@ mod tests {
let authorizer = AwsAuthorizer {
date: Some(date),
+ crypto: None,
credential: &credential,
service: "s3",
region: "us-east-1",
@@ -1126,7 +1193,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 c4d8063..149d9e2 100644
--- a/src/aws/mod.rs
+++ b/src/aws/mod.rs
@@ -141,9 +141,11 @@ impl Signer for AmazonS3 {
/// # }
/// ```
async fn signed_url(&self, method: Method, path: &Path, expires_in:
Duration) -> Result<Url> {
+ 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 {
@@ -151,7 +153,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)
}
@@ -175,7 +177,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 64a44b8..c1f0d72 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;
@@ -167,6 +167,8 @@ pub struct MicrosoftAzureBuilder {
client_options: ClientOptions,
/// Credentials
credentials: Option<AzureCredentialProvider>,
+ /// The [`CryptoProvider`] to use
+ crypto: Option<Arc<dyn CryptoProvider>>,
/// Skip signing requests
skip_signature: ConfigValue<bool>,
/// When set to true, fabric url scheme will be used
@@ -840,6 +842,12 @@ impl MicrosoftAzureBuilder {
self
}
+ /// The [`CryptoProvider`] to use
+ pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>)
-> Self {
+ self.crypto = Some(provider);
+ self
+ }
+
/// Set if the Azure emulator should be used (defaults to false)
pub fn with_use_emulator(mut self, use_emulator: bool) -> Self {
self.use_emulator = use_emulator.into();
@@ -1117,14 +1125,14 @@ impl MicrosoftAzureBuilder {
};
let encryption_headers =
-
AzureEncryptionHeaders::try_new(self.encryption_key).map_err(|source| {
- Error::InvalidEncryptionKey {
+ AzureEncryptionHeaders::try_new(self.crypto.as_deref(),
self.encryption_key).map_err(
+ |source| Error::InvalidEncryptionKey {
source: match source {
crate::Error::Generic { source, .. } => source,
other => Box::new(other),
},
- }
- })?;
+ },
+ )?;
let config = AzureConfig {
account,
@@ -1136,6 +1144,7 @@ impl MicrosoftAzureBuilder {
client_options: self.client_options,
service: storage_url,
credentials: auth,
+ crypto: self.crypto,
encryption_headers,
};
diff --git a/src/azure/client.rs b/src/azure/client.rs
index 3c76b1c..1c21fa9 100644
--- a/src/azure/client.rs
+++ b/src/azure/client.rs
@@ -23,7 +23,10 @@ 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, DigestAlgorithm, GetOptionsExt, HttpClient, HttpError,
HttpRequest,
+ HttpResponse, crypto_provider,
+};
use crate::list::{PaginatedListOptions, PaginatedListResult};
use crate::multipart::PartId;
use crate::util::{GetRange, deserialize_rfc1123};
@@ -41,7 +44,6 @@ use http::{
header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderMap, HeaderValue, IF_MATCH,
IF_NONE_MATCH},
};
use rand::RngExt;
-use ring::digest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
@@ -180,6 +182,7 @@ impl From<Error> for crate::Error {
pub(crate) struct AzureConfig {
pub account: String,
pub container: String,
+ pub crypto: Option<Arc<dyn CryptoProvider>>,
pub credentials: AzureCredentialProvider,
pub retry_config: RetryConfig,
pub service: Url,
@@ -250,7 +253,10 @@ impl std::fmt::Debug for AzureEncryptionHeaders {
}
impl AzureEncryptionHeaders {
- pub(crate) fn try_new(encryption_key: Option<String>) -> Result<Self> {
+ pub(crate) fn try_new(
+ crypto: Option<&dyn CryptoProvider>,
+ encryption_key: Option<String>,
+ ) -> Result<Self> {
let Some(encryption_key) = encryption_key else {
return Ok(Self::default());
};
@@ -275,8 +281,10 @@ impl AzureEncryptionHeaders {
});
}
- let encryption_key_sha256 =
- BASE64_STANDARD.encode(digest::digest(&digest::SHA256,
&decoded_key));
+ let crypto = crypto_provider(crypto)?;
+ let mut ctx = crypto.digest(DigestAlgorithm::Sha256)?;
+ ctx.update(&decoded_key);
+ let encryption_key_sha256 = BASE64_STANDARD.encode(ctx.finish()?);
Ok(Self {
encryption_key: Some(encryption_key),
@@ -416,11 +424,12 @@ impl PutRequest<'_> {
async fn send(self) -> Result<HttpResponse> {
let credential = self.config.get_credential().await?;
let sensitive = self.config.is_sensitive(&credential);
+ let crypto = self.config.crypto.as_deref();
let response = self
.builder
.with_azure_encryption_headers(&self.config.encryption_headers)
.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)
@@ -674,6 +683,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());
@@ -783,7 +796,8 @@ impl AzureClient {
boundary: &str,
paths: &[Path],
credential: &Option<Arc<AzureCredential>>,
- ) -> Vec<u8> {
+ ) -> Result<Vec<u8>> {
+ let crypto = self.crypto();
let mut body_bytes = Vec::with_capacity(paths.len() * 2048);
for (idx, path) in paths.iter().enumerate() {
@@ -799,7 +813,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();
@@ -817,7 +831,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<Path>) ->
Result<Vec<Result<Path>>> {
@@ -831,7 +845,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("/"));
@@ -847,7 +861,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)?
.retryable(&self.config.retry_config)
.sensitive(sensitive)
.send()
@@ -901,7 +915,11 @@ impl AzureClient {
signed_expiry,
None,
)
- .sign(&Method::GET, &mut source)?;
+ .sign(
+ crypto_provider(self.crypto())?,
+ &Method::GET,
+ &mut source,
+ )?;
}
Some(AzureCredential::BearerToken(token)) => {
source_authorization = Some(format!("Bearer {token}"));
@@ -935,7 +953,7 @@ impl AzureClient {
let sensitive = self.config.is_sensitive(&credential);
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)
@@ -973,7 +991,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)
@@ -1036,7 +1054,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()
@@ -1105,7 +1123,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)
@@ -1173,7 +1191,7 @@ impl ListClient for Arc<AzureClient> {
.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()
@@ -1468,8 +1486,7 @@ mod tests {
<NextMarker />
</EnumerationResults>";
- 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]
@@ -1588,15 +1605,17 @@ 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,
skip_signature: false,
disable_tagging: false,
client_options: Default::default(),
- encryption_headers: AzureEncryptionHeaders::try_new(Some(
- BASE64_STANDARD.encode([7_u8; 32]),
- ))
+ encryption_headers: AzureEncryptionHeaders::try_new(
+ None,
+ Some(BASE64_STANDARD.encode([7_u8; 32])),
+ )
.unwrap(),
};
@@ -1607,7 +1626,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();
@@ -1660,7 +1681,7 @@ Authorization: Bearer static-token\r
#[test]
fn test_azure_encryption_headers_debug_redacts_key() {
let encryption_key = BASE64_STANDARD.encode([7_u8; 32]);
- let headers =
AzureEncryptionHeaders::try_new(Some(encryption_key.clone())).unwrap();
+ let headers = AzureEncryptionHeaders::try_new(None,
Some(encryption_key.clone())).unwrap();
let encryption_key_sha256 =
headers.encryption_key_sha256.clone().unwrap();
let debug = format!("{headers:?}");
@@ -1674,7 +1695,7 @@ Authorization: Bearer static-token\r
#[test]
fn test_azure_sensitive_headers_redact_client_request_debug() {
let encryption_key = BASE64_STANDARD.encode([7_u8; 32]);
- let headers =
AzureEncryptionHeaders::try_new(Some(encryption_key.clone())).unwrap();
+ let headers = AzureEncryptionHeaders::try_new(None,
Some(encryption_key.clone())).unwrap();
let encryption_key_sha256 =
headers.encryption_key_sha256.clone().unwrap();
let copy_source =
"http://example.com/source.txt?sig=secret-source-sas";
let source_authorization = "Bearer static-token";
diff --git a/src/azure/credential.rs b/src/azure/credential.rs
index 111c908..f4c2989 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());
@@ -227,6 +237,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,
}
@@ -236,23 +247,46 @@ 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
+ }
+
+ fn crypto(&self) -> crate::Result<&dyn CryptoProvider> {
+ crypto_provider(self.crypto)
+ }
+
/// 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<()> {
add_date_and_version_headers(request);
match self.credential {
AzureCredential::AccessKey(key) => {
+ let crypto = self.crypto()?;
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
@@ -271,53 +305,67 @@ 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
///
<https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage>
fn with_azure_authorization(
self,
+ crypto: Option<&dyn CryptoProvider>,
credential: &Option<impl Deref<Target = AzureCredential>>,
account: &str,
- ) -> Self;
+ ) -> crate::Result<Self>;
}
impl CredentialExt for HttpRequestBuilder {
fn with_azure_authorization(
self,
+ crypto: Option<&dyn CryptoProvider>,
credential: &Option<impl Deref<Target = AzureCredential>>,
account: &str,
- ) -> Self {
+ ) -> crate::Result<Self> {
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);
+ let mut authorizer = AzureAuthorizer::new(credential, account);
+ if let Some(crypto) = crypto {
+ authorizer = authorizer.with_crypto(crypto);
+ }
+ authorizer.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
///
<https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key>
fn generate_authorization(
+ crypto: &dyn CryptoProvider,
h: &HeaderMap,
u: &Url,
method: &Method,
account: &str,
key: &AzureAccessKey,
-) -> String {
+) -> crate::Result<String> {
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 {
@@ -1077,6 +1125,7 @@ mod tests {
use super::*;
use crate::azure::MicrosoftAzureBuilder;
use crate::azure::client::RequestVersionExt;
+ use crate::client::HttpRequestBody;
use crate::client::mock_server::MockServer;
use crate::{ObjectStoreExt, Path};
@@ -1226,6 +1275,48 @@ mod tests {
}
}
+ #[test]
+ fn bearer_authorization_does_not_require_crypto_provider() {
+ let credential =
Some(Arc::new(AzureCredential::BearerToken("TOKEN".into())));
+ let builder = HttpRequestBuilder::new(HttpClient::new(Client::new()))
+ .method(Method::GET)
+ .uri("https://account.blob.core.windows.net/container/file.txt")
+ .body(HttpRequestBody::empty())
+ .with_azure_authorization(None, &credential, "account")
+ .unwrap();
+
+ let (_, request) = builder.into_parts();
+ let request = request.unwrap();
+ assert_eq!(
+ request.headers().get(AUTHORIZATION).unwrap(),
+ "Bearer TOKEN"
+ );
+ assert!(request.headers().contains_key(&DATE));
+ assert!(request.headers().contains_key(&VERSION));
+ }
+
+ #[test]
+ fn sas_authorization_does_not_require_crypto_provider() {
+ let credential = Some(Arc::new(AzureCredential::SASToken(vec![
+ ("sig".into(), "signature".into()),
+ ("se".into(), "expiry".into()),
+ ])));
+ let builder = HttpRequestBuilder::new(HttpClient::new(Client::new()))
+ .method(Method::GET)
+ .uri("https://account.blob.core.windows.net/container/file.txt")
+ .body(HttpRequestBody::empty())
+ .with_azure_authorization(None, &credential, "account")
+ .unwrap();
+
+ let (_, request) = builder.into_parts();
+ let request = request.unwrap();
+ let query = request.uri().query().unwrap();
+ assert!(query.contains("sig=signature"));
+ assert!(query.contains("se=expiry"));
+ assert!(request.headers().contains_key(&DATE));
+ assert!(request.headers().contains_key(&VERSION));
+ }
+
#[cfg(feature = "reqwest")]
#[tokio::test]
async fn test_fabric_refresh_expired_token() {
@@ -1295,7 +1386,8 @@ mod tests {
let request = client
.request(Method::GET, "http://example.com/container/blob")
.with_azure_version("2026-02-06")
- .with_azure_authorization(&credential, "account")
+ .with_azure_authorization(None, &credential, "account")
+ .unwrap()
.into_parts()
.1
.unwrap();
diff --git a/src/azure/mod.rs b/src/azure/mod.rs
index 2e802e3..b75798f 100644
--- a/src/azure/mod.rs
+++ b/src/azure/mod.rs
@@ -42,9 +42,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;
@@ -224,9 +224,10 @@ impl Signer for MicrosoftAzure {
});
}
+ 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)
}
@@ -242,11 +243,12 @@ impl Signer for MicrosoftAzure {
});
}
+ 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/crypto.rs b/src/client/crypto.rs
new file mode 100644
index 0000000..41e3ef5
--- /dev/null
+++ b/src/client/crypto.rs
@@ -0,0 +1,410 @@
+// 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<Box<dyn
DigestContext>>;
+
+ /// Compute an HMAC with the provided `secret`
+ fn hmac(&self, algorithm: DigestAlgorithm, secret: &[u8]) ->
Result<Box<dyn HmacContext>>;
+
+ /// Sign a payload with the provided PEM-encoded secret
+ fn sign(&self, algorithm: SigningAlgorithm, pem: &[u8]) -> Result<Box<dyn
Signer>>;
+}
+
+/// 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<Vec<u8>>;
+}
+
+/// Attempts to find a [`CryptoProvider`]
+///
+/// If `custom` is `Some(v)` returns `v` otherwise returns the compile-time
default
+///
+/// If both `ring` and `aws-lc-rs` are enabled, the `aws-lc-rs` provider is
used.
+pub(crate) fn crypto_provider(custom: Option<&dyn CryptoProvider>) ->
Result<&dyn CryptoProvider> {
+ if let Some(x) = custom {
+ return Ok(x);
+ }
+
+ #[cfg(feature = "aws-lc-rs")]
+ {
+ Ok(&aws_lc_rs::PROVIDER)
+ }
+
+ #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
+ {
+ Ok(&ring::PROVIDER)
+ }
+
+ #[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))]
+ {
+ Err(crate::Error::NotSupported {
+ source: "Must enable aws-lc-rs, ring, or specify custom
CryptoProvider"
+ .to_string()
+ .into(),
+ })
+ }
+}
+
+#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
+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<RingError> 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<Box<dyn
DigestContext>> {
+ 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<Box<dyn HmacContext>> {
+ 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<Box<dyn Signer>> {
+ match algorithm {
+ SigningAlgorithm::RS256 =>
Ok(Box::new(RsaKeyPair::from_pem(pem)?)),
+ }
+ }
+ }
+
+ struct RingDigestContext {
+ ctx: Option<digest::Context>,
+ out: Option<digest::Digest>,
+ }
+
+ 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<hmac::Context>,
+ out: Option<hmac::Tag>,
+ }
+
+ 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<Self, RingError> {
+ 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<Self, RingError> {
+ Ok(Self(signature::RsaKeyPair::from_pkcs8(key)?))
+ }
+
+ /// Parses an unencrypted PKCS#8-encoded RSA private key.
+ pub(crate) fn from_der(key: &[u8]) -> Result<Self, RingError> {
+ Ok(Self(signature::RsaKeyPair::from_der(key)?))
+ }
+ }
+
+ impl Signer for RsaKeyPair {
+ fn sign(&self, string_to_sign: &[u8]) -> Result<Vec<u8>> {
+ 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)
+ }
+ }
+}
+
+#[cfg(feature = "aws-lc-rs")]
+pub(crate) mod aws_lc_rs {
+ use super::*;
+ use ::aws_lc_rs::{digest, hmac, rand, signature};
+ use thiserror::Error;
+
+ #[derive(Debug, Error)]
+ pub(crate) enum AwsLcError {
+ #[error("No RSA key found in pem file")]
+ MissingKey,
+
+ #[error("Invalid RSA key: {}", source)]
+ InvalidKey {
+ #[from]
+ source: ::aws_lc_rs::error::KeyRejected,
+ },
+
+ #[error("Error reading pem file: {}", source)]
+ ReadPem {
+ source: rustls_pki_types::pem::Error,
+ },
+
+ #[error("Error signing: {}", source)]
+ Sign {
+ source: ::aws_lc_rs::error::Unspecified,
+ },
+ }
+
+ impl From<AwsLcError> for crate::Error {
+ fn from(value: AwsLcError) -> Self {
+ Self::Generic {
+ store: "AwsLcCryptoProvider",
+ source: Box::new(value),
+ }
+ }
+ }
+
+ pub(crate) const PROVIDER: AwsLcCryptoProvider = AwsLcCryptoProvider {
_private: () };
+
+ #[derive(Debug, Default)]
+ pub(crate) struct AwsLcCryptoProvider {
+ _private: (),
+ }
+
+ impl CryptoProvider for AwsLcCryptoProvider {
+ fn digest(&self, algorithm: DigestAlgorithm) -> Result<Box<dyn
DigestContext>> {
+ let algorithm = match algorithm {
+ DigestAlgorithm::Sha256 => &digest::SHA256,
+ };
+ let ctx = digest::Context::new(algorithm);
+ Ok(Box::new(AwsLcDigestContext {
+ ctx: Some(ctx),
+ out: None,
+ }))
+ }
+
+ fn hmac(&self, algorithm: DigestAlgorithm, secret: &[u8]) ->
Result<Box<dyn HmacContext>> {
+ let algorithm = match algorithm {
+ DigestAlgorithm::Sha256 => hmac::HMAC_SHA256,
+ };
+ let ctx = hmac::Context::with_key(&hmac::Key::new(algorithm,
secret));
+ Ok(Box::new(AwsLcHmacContext {
+ ctx: Some(ctx),
+ out: None,
+ }))
+ }
+
+ fn sign(&self, algorithm: SigningAlgorithm, pem: &[u8]) ->
Result<Box<dyn Signer>> {
+ match algorithm {
+ SigningAlgorithm::RS256 =>
Ok(Box::new(RsaKeyPair::from_pem(pem)?)),
+ }
+ }
+ }
+
+ struct AwsLcDigestContext {
+ ctx: Option<digest::Context>,
+ out: Option<digest::Digest>,
+ }
+
+ impl DigestContext for AwsLcDigestContext {
+ 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 AwsLcHmacContext {
+ ctx: Option<hmac::Context>,
+ out: Option<hmac::Tag>,
+ }
+
+ impl HmacContext for AwsLcHmacContext {
+ 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<Self, AwsLcError> {
+ 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(AwsLcError::MissingKey),
+ Err(source) => Err(AwsLcError::ReadPem { source }),
+ }
+ }
+
+ /// Parses an unencrypted PKCS#8-encoded RSA private key.
+ pub(crate) fn from_pkcs8(key: &[u8]) -> Result<Self, AwsLcError> {
+ Ok(Self(signature::RsaKeyPair::from_pkcs8(key)?))
+ }
+
+ /// Parses an unencrypted PKCS#8-encoded RSA private key.
+ pub(crate) fn from_der(key: &[u8]) -> Result<Self, AwsLcError> {
+ Ok(Self(signature::RsaKeyPair::from_der(key)?))
+ }
+ }
+
+ impl Signer for RsaKeyPair {
+ fn sign(&self, string_to_sign: &[u8]) -> Result<Vec<u8>> {
+ 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| AwsLcError::Sign { source })?;
+
+ Ok(signature)
+ }
+ }
+}
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 896fcb2..a8d2ae5 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
@@ -53,6 +53,12 @@ mod http;
pub(crate) mod parts;
pub use http::*;
+#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
+mod crypto;
+
+#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
+pub use crypto::*;
+
use ::http::header::{HeaderMap, HeaderValue};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
@@ -105,6 +111,14 @@ pub enum ClientConfigKey {
/// Supported keys:
/// - `allow_invalid_certificates`
AllowInvalidCertificates,
+ /// Disable certificate validation using the operating system's
certificate facilities.
+ ///
+ /// See [`ClientOptions::with_no_system_certificates`]
+ ///
+ /// Supported keys:
+ ///
+ /// - `disable_system_certificates`
+ NoSystemCertificates,
/// Timeout for only the connect phase of a Client
///
/// Supported keys:
@@ -216,6 +230,7 @@ impl AsRef<str> for ClientConfigKey {
match self {
Self::AllowHttp => "allow_http",
Self::AllowInvalidCertificates => "allow_invalid_certificates",
+ Self::NoSystemCertificates => "disable_system_certificates",
Self::ConnectTimeout => "connect_timeout",
Self::DefaultContentType => "default_content_type",
Self::Http1Only => "http1_only",
@@ -244,6 +259,7 @@ impl FromStr for ClientConfigKey {
match s {
"allow_http" => Ok(Self::AllowHttp),
"allow_invalid_certificates" => Ok(Self::AllowInvalidCertificates),
+ "disable_system_certificates" => Ok(Self::NoSystemCertificates),
"connect_timeout" => Ok(Self::ConnectTimeout),
"default_content_type" => Ok(Self::DefaultContentType),
"http1_only" => Ok(Self::Http1Only),
@@ -326,6 +342,7 @@ pub struct ClientOptions {
user_agent: Option<ConfigValue<HeaderValue>>,
#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
root_certificates: Vec<Certificate>,
+ no_system_certificates: ConfigValue<bool>,
content_type_map: HashMap<String, String>,
default_content_type: Option<String>,
default_headers: Option<HeaderMap>,
@@ -361,6 +378,7 @@ impl Default for ClientOptions {
user_agent: None,
#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
root_certificates: Default::default(),
+ no_system_certificates: false.into(),
content_type_map: Default::default(),
default_content_type: None,
default_headers: None,
@@ -401,6 +419,7 @@ impl ClientOptions {
ClientConfigKey::AllowInvalidCertificates => {
self.allow_invalid_certificates.parse(value)
}
+ ClientConfigKey::NoSystemCertificates =>
self.no_system_certificates.parse(value),
ClientConfigKey::ConnectTimeout => {
self.connect_timeout =
Some(ConfigValue::Deferred(value.into()))
}
@@ -449,6 +468,7 @@ impl ClientOptions {
ClientConfigKey::AllowInvalidCertificates => {
Some(self.allow_invalid_certificates.to_string())
}
+ ClientConfigKey::NoSystemCertificates =>
Some(self.no_system_certificates.to_string()),
ClientConfigKey::ConnectTimeout =>
self.connect_timeout.as_ref().map(fmt_duration),
ClientConfigKey::ReadTimeout =>
self.read_timeout.as_ref().map(fmt_duration),
ClientConfigKey::DefaultContentType =>
self.default_content_type.clone(),
@@ -532,6 +552,7 @@ impl ClientOptions {
self.allow_http = allow_http.into();
self
}
+
/// Allows connections to invalid SSL certificates
///
/// If `allow_invalid_certificates` is :
@@ -554,6 +575,20 @@ impl ClientOptions {
self
}
+ /// Disable certificates provided by the system
+ ///
+ /// By default TLS certificates are validated using
[`rustls-platform-verifier`],
+ /// which makes use of the system's trust store, in addition to any
certificates
+ /// registered using [`Self::with_root_certificate`]. If disabled, instead
[`rustls-webpki`]
+ /// is used with only the certificates registered using
[`Self::with_root_certificate`].
+ ///
+ /// [`rustls-platform-verifier`]:
https://crates.io/crates/rustls-platform-verifier
+ /// [`rustls-webpki`]: https://crates.io/crates/rustls-webpki
+ pub fn with_no_system_certificates(mut self, no_certs: bool) -> Self {
+ self.no_system_certificates = no_certs.into();
+ self
+ }
+
/// Only use HTTP/1 connections (default)
///
/// # See Also
@@ -816,7 +851,7 @@ impl ClientOptions {
let certificate =
reqwest::tls::Certificate::from_pem(certificate.as_bytes())
.map_err(map_client_error)?;
- builder = builder.add_root_certificate(certificate);
+ builder =
builder.tls_certs_merge(std::iter::once(certificate));
}
if let Some(proxy_excludes) = &self.proxy_excludes {
@@ -828,8 +863,15 @@ impl ClientOptions {
builder = builder.proxy(proxy);
}
- for certificate in &self.root_certificates {
- builder = builder.add_root_certificate(certificate.0.clone());
+ let certs = self
+ .root_certificates
+ .iter()
+ .map(|certificate| certificate.0.clone());
+
+ if self.no_system_certificates.get()? {
+ builder = builder.tls_certs_only(certs);
+ } else {
+ builder = builder.tls_certs_merge(certs);
}
if let Some(timeout) = &self.timeout {
@@ -1073,6 +1115,7 @@ mod tests {
let http2_keep_alive_timeout = "91 seconds".to_string();
let http2_keep_alive_while_idle = "92 seconds".to_string();
let http2_max_frame_size = "1337".to_string();
+ let no_system_certificates = "true".to_string();
let pool_idle_timeout = "93 seconds".to_string();
let pool_max_idle_per_host = "94".to_string();
let proxy_url = "https://fake_proxy_url".to_string();
@@ -1100,6 +1143,10 @@ mod tests {
http2_keep_alive_while_idle.clone(),
),
("http2_max_frame_size", http2_max_frame_size.clone()),
+ (
+ "disable_system_certificates",
+ no_system_certificates.clone(),
+ ),
("pool_idle_timeout", pool_idle_timeout.clone()),
("pool_max_idle_per_host", pool_max_idle_per_host.clone()),
("proxy_url", proxy_url.clone()),
@@ -1174,6 +1221,12 @@ mod tests {
.unwrap(),
http2_max_frame_size
);
+ assert_eq!(
+ builder
+ .get_config_value(&ClientConfigKey::NoSystemCertificates)
+ .unwrap(),
+ no_system_certificates
+ );
assert_eq!(
builder
diff --git a/src/gcp/builder.rs b/src/gcp/builder.rs
index 55cfe63..7487ad6 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::{
@@ -114,6 +116,8 @@ pub struct GoogleCloudStorageBuilder {
credentials: Option<GcpCredentialProvider>,
/// Explicit bearer token, if configured
bearer_token: Option<String>,
+ /// The [`CryptoProvider`] to use
+ crypto: Option<Arc<dyn CryptoProvider>>,
/// Skip signing requests
skip_signature: ConfigValue<bool>,
/// Credentials for sign url
@@ -254,6 +258,7 @@ impl Default for GoogleCloudStorageBuilder {
base_url: None,
credentials: None,
bearer_token: None,
+ crypto: None,
skip_signature: Default::default(),
signing_credentials: None,
http_connector: None,
@@ -494,6 +499,12 @@ impl GoogleCloudStorageBuilder {
self
}
+ /// The [`CryptoProvider`] to use
+ pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>)
-> 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;
@@ -542,7 +553,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.
@@ -592,8 +602,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 _
@@ -608,8 +619,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 _
@@ -634,7 +646,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) => {
@@ -645,7 +658,8 @@ impl GoogleCloudStorageBuilder {
)) as _
}
ApplicationDefaultCredentials::ServiceAccount(token) => {
- token.signing_credentials()?
+ let crypto = crypto_provider(self.crypto.as_deref())?;
+ token.signing_credentials(crypto)?
}
}
} else {
@@ -661,6 +675,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 5356cc4..f41171d 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<Arc<dyn CryptoProvider>>,
+
pub bucket_name: String,
pub retry_config: RetryConfig,
diff --git a/src/gcp/credential.rs b/src/gcp/credential.rs
index dfba358..4a43929 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_util::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<Error> for crate::Error {
@@ -123,45 +104,64 @@ pub struct GcpSigningCredential {
}
/// A private RSA key for a service account
-#[derive(Debug)]
-pub struct ServiceAccountKey(RsaKeyPair);
+pub struct ServiceAccountKey(Box<dyn Signer>);
+
+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<dyn Signer>) -> Self {
+ Self(signer)
+ }
+
/// Parses a pem-encoded RSA key
- pub fn from_pem(encoded: &[u8]) -> Result<Self> {
- 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 = "aws-lc-rs")]
+ pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::aws_lc_rs::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<Self> {
- Ok(Self(RsaKeyPair::from_pkcs8(key)?))
+ #[cfg(feature = "aws-lc-rs")]
+ pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::aws_lc_rs::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<Self> {
- Ok(Self(RsaKeyPair::from_der(key)?))
- }
-
- fn sign(&self, string_to_sign: &str) -> Result<String> {
- 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 })?;
+ #[cfg(feature = "aws-lc-rs")]
+ pub fn from_der(key: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::aws_lc_rs::RsaKeyPair::from_der(key)?;
+ Ok(Self::new(Box::new(key)))
+ }
- Ok(hex_encode(&signature))
+ /// Parses a pem-encoded RSA key
+ #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
+ pub fn from_pem(encoded: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::ring::RsaKeyPair::from_pem(encoded)?;
+ Ok(Self::new(Box::new(key)))
+ }
+
+ /// Parses an unencrypted PKCS#8-encoded RSA private key.
+ #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
+ pub fn from_pkcs8(key: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::ring::RsaKeyPair::from_pkcs8(key)?;
+ Ok(Self::new(Box::new(key)))
+ }
+
+ /// Parses an unencrypted PKCS#8-encoded RSA private key.
+ #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
+ pub fn from_der(key: &[u8]) -> crate::Result<Self> {
+ let key = crate::client::ring::RsaKeyPair::from_der(key)?;
+ Ok(Self::new(Box::new(key)))
+ }
+
+ fn sign(&self, string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
+ self.0.sign(string_to_sign)
}
}
@@ -287,17 +287,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 +350,28 @@ impl ServiceAccountCredentials {
/// # References
/// -
<https://stackoverflow.com/questions/63222450/service-account-authorization-without-oauth-can-we-get-file-from-google-cloud/71834557#71834557>
/// -
<https://www.codejam.info/2022/05/google-cloud-service-account-authorization-without-oauth.html>
- pub(crate) fn token_provider(self) -> crate::Result<SelfSignedJwt> {
+ pub(crate) fn token_provider(
+ self,
+ crypto: &dyn CryptoProvider,
+ ) -> crate::Result<SelfSignedJwt> {
+ 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<GcpSigningCredentialProvider> {
+ pub(crate) fn signing_credentials(
+ self,
+ crypto: &dyn CryptoProvider,
+ ) -> crate::Result<GcpSigningCredentialProvider> {
+ 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 +761,7 @@ impl GCSAuthorizer {
pub(crate) async fn sign(
&self,
+ crypto: &dyn CryptoProvider,
method: Method,
url: &mut Url,
expires_in: Duration,
@@ -785,9 +784,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) => hex_encode(&key.sign(string_to_sign.as_bytes())?),
None => client.sign_blob(&string_to_sign, email).await?,
};
@@ -885,22 +884,23 @@ impl GCSAuthorizer {
///
<https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign>
pub(crate) fn string_to_sign(
&self,
+ crypto: &dyn CryptoProvider,
date: DateTime<Utc>,
request_method: &Method,
url: &Url,
headers: &HeaderMap,
- ) -> String {
+ ) -> crate::Result<String> {
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
- )
+ ))
}
}
@@ -927,6 +927,133 @@ impl CredentialExt for HttpRequestBuilder {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::client::{
+ ClientOptions, DigestAlgorithm, DigestContext, HmacContext,
StaticCredentialProvider,
+ };
+ use crate::gcp::client::{GoogleCloudStorageClient,
GoogleCloudStorageConfig};
+
+ const SIGNATURE_BYTES: &[u8] = &[0x00, 0x01, 0x02, 0xab, 0xcd];
+
+ struct FixedSigner;
+
+ impl Signer for FixedSigner {
+ fn sign(&self, _string_to_sign: &[u8]) -> crate::Result<Vec<u8>> {
+ Ok(SIGNATURE_BYTES.to_vec())
+ }
+ }
+
+ #[derive(Debug)]
+ struct FixedCryptoProvider;
+
+ impl CryptoProvider for FixedCryptoProvider {
+ fn digest(&self, _algorithm: DigestAlgorithm) -> crate::Result<Box<dyn
DigestContext>> {
+ Ok(Box::new(FixedDigestContext))
+ }
+
+ fn hmac(
+ &self,
+ _algorithm: DigestAlgorithm,
+ _secret: &[u8],
+ ) -> crate::Result<Box<dyn HmacContext>> {
+ panic!("GCS signed URL should not use HMAC")
+ }
+
+ fn sign(
+ &self,
+ _algorithm: SigningAlgorithm,
+ _pem: &[u8],
+ ) -> crate::Result<Box<dyn Signer>> {
+ Ok(Box::new(FixedSigner))
+ }
+ }
+
+ struct FixedDigestContext;
+
+ impl DigestContext for FixedDigestContext {
+ fn update(&mut self, _data: &[u8]) {}
+
+ fn finish(&mut self) -> crate::Result<&[u8]> {
+ Ok(&[0x12, 0x34])
+ }
+ }
+
+ #[derive(Debug)]
+ struct UnusedHttpService;
+
+ #[async_trait::async_trait]
+ impl crate::client::HttpService for UnusedHttpService {
+ async fn call(
+ &self,
+ _req: crate::client::HttpRequest,
+ ) -> std::result::Result<crate::client::HttpResponse, HttpError> {
+ panic!("SelfSignedJwt should not make HTTP requests")
+ }
+ }
+
+ #[test]
+ fn self_signed_jwt_base64url_encodes_raw_signature_bytes() {
+ let jwt = SelfSignedJwt::new(
+ "key-id".into(),
+ "[email protected]".into(),
+ ServiceAccountKey::new(Box::new(FixedSigner)),
+ DEFAULT_SCOPE.to_string(),
+ )
+ .unwrap();
+ let client = HttpClient::new(UnusedHttpService);
+ let token = futures_executor::block_on(jwt.fetch_token(&client,
&RetryConfig::default()))
+ .unwrap()
+ .token;
+ let signature = token.bearer.rsplit('.').next().unwrap();
+
+ assert_eq!(signature, BASE64_URL_SAFE_NO_PAD.encode(SIGNATURE_BYTES));
+ assert_ne!(
+ signature,
+ BASE64_URL_SAFE_NO_PAD.encode(hex_encode(SIGNATURE_BYTES))
+ );
+ }
+
+ #[test]
+ fn signed_url_hex_encodes_local_signature_bytes() {
+ let signing_credential = Arc::new(GcpSigningCredential {
+ email: "[email protected]".into(),
+ private_key: Some(ServiceAccountKey::new(Box::new(FixedSigner))),
+ });
+ let authorizer = GCSAuthorizer::new(Arc::clone(&signing_credential));
+ let config = GoogleCloudStorageConfig {
+ base_url: DEFAULT_GCS_BASE_URL.into(),
+ credentials: Arc::new(StaticCredentialProvider::new(GcpCredential {
+ bearer: "bearer".into(),
+ })),
+ signing_credentials:
Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
+ email: "[email protected]".into(),
+ private_key: None,
+ })),
+ crypto: None,
+ bucket_name: "bucket".into(),
+ retry_config: RetryConfig::default(),
+ client_options: ClientOptions::default(),
+ skip_signature: false,
+ };
+ let client =
+ GoogleCloudStorageClient::new(config,
HttpClient::new(UnusedHttpService)).unwrap();
+ let mut url =
Url::parse("https://storage.googleapis.com/bucket/object").unwrap();
+
+ futures_executor::block_on(authorizer.sign(
+ &FixedCryptoProvider,
+ Method::GET,
+ &mut url,
+ Duration::from_secs(60),
+ &client,
+ ))
+ .unwrap();
+
+ let signature = url
+ .query_pairs()
+ .find(|(key, _)| key == "X-Goog-Signature")
+ .unwrap()
+ .1;
+ assert_eq!(signature, hex_encode(SIGNATURE_BYTES));
+ }
#[test]
fn test_canonicalize_headers() {
diff --git a/src/gcp/mod.rs b/src/gcp/mod.rs
index 7c663b8..fe9b290 100644
--- a/src/gcp/mod.rs
+++ b/src/gcp/mod.rs
@@ -41,7 +41,7 @@ use std::sync::Arc;
use std::time::Duration;
use crate::CopyOptions;
-use crate::client::CredentialProvider;
+use crate::client::{CredentialProvider, crypto_provider};
use crate::gcp::credential::GCSAuthorizer;
use crate::signer::Signer;
use crate::{
@@ -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 48bf994..115770d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -521,39 +521,43 @@
//!
//! # Feature Flags
//!
-//! The feature set is layered so that you can pick a provider independently
-//! of its HTTP transport:
+//! The feature set is layered so that you can pick an object store
+//! implementation, its HTTP transport, and its cryptography provider
+//! independently:
//!
-//! * `cloud-base` holds the shared provider implementation (XML/JSON parsing,
+//! * `cloud-base` shared cloud implementation (XML/JSON parsing,
//! credentials, retry, etc.) and intentionally does *not* depend on
-//! `reqwest`.
+//! `reqwest` or a cryptography provider.
//! * `reqwest` enables the built-in [`reqwest`]-based [`HttpConnector`].
+//! * `aws-lc-rs` and `ring` each provide a bundled [`client::CryptoProvider`].
//! * `<provider>-base` (`aws-base`, `azure-base`, `gcp-base`, `http-base`)
-//! adds the per-provider logic on top of `cloud-base` without pulling in
-//! `reqwest`.
+//! adds the implementation specific logic on top of `cloud-base` without
pulling in
+//! `reqwest` or a cryptography provider.
//! * `<provider>` (`aws`, `azure`, `gcp`, `http`) is the batteries-included
-//! alias for `<provider>-base` + `reqwest` and is the typical choice.
+//! feature for `<provider>-base` + `reqwest` (with `rustls`) + the default
+//! `aws-lc-rs` cryptography provider, and is the typical choice.
//!
-//! ## Provider features
+//! ## Implementation specific features
//!
//! | Feature | Enables | Notes |
//! | --- | --- | --- |
-//! | `aws` | `aws-base` + `reqwest` | Amazon S3 with the built-in HTTP
transport. |
-//! | `azure` | `azure-base` + `reqwest` | Azure Blob Storage with the
built-in HTTP transport. |
-//! | `gcp` | `gcp-base` + `reqwest` | Google Cloud Storage with the built-in
HTTP transport. |
-//! | `http` | `http-base` + `reqwest` | HTTP/WebDAV with the built-in HTTP
transport. |
-//! | `aws-base` | provider only | S3 provider without `reqwest`; supply your
own [`HttpConnector`]. |
-//! | `azure-base` | provider only | Azure provider without `reqwest`; supply
your own [`HttpConnector`]. |
-//! | `gcp-base` | provider only | GCS provider without `reqwest`; supply your
own [`HttpConnector`]. |
-//! | `http-base` | provider only | HTTP/WebDAV provider without `reqwest`;
supply your own [`HttpConnector`]. |
+//! | `aws` | `aws-base` + `reqwest` + `aws-lc-rs` | Amazon S3 with the
built-in HTTP transport. |
+//! | `azure` | `azure-base` + `reqwest` + `aws-lc-rs` | Azure Blob Storage
with the built-in HTTP transport. |
+//! | `gcp` | `gcp-base` + `reqwest` + `aws-lc-rs` | Google Cloud Storage with
the built-in HTTP transport. |
+//! | `http` | `http-base` + `reqwest` + `aws-lc-rs` | HTTP/WebDAV with the
built-in HTTP transport. |
+//! | `aws-base` | | S3 without `reqwest` or crypto; supply your own
[`HttpConnector`] and [`client::CryptoProvider`]. |
+//! | `azure-base` | | Azure without `reqwest` or crypto; supply your own
[`HttpConnector`] and [`client::CryptoProvider`]. |
+//! | `gcp-base` | | GCS without `reqwest` or crypto; supply your own
[`HttpConnector`] and [`client::CryptoProvider`]. |
+//! | `http-base` | | HTTP/WebDAV without `reqwest`; supply your own
[`HttpConnector`]. |
//!
-//! ## Transport and shared features
+//! ## Transport and crypto features
//!
//! | Feature | Description |
//! | --- | --- |
-//! | `reqwest` | Enables the default [`reqwest`]-based [`HttpConnector`].
Pulled in automatically by `aws`, `azure`, `gcp`, and `http`. |
-//! | `tls-webpki-roots` | When `reqwest` is enabled, also bundle Mozilla's
[`webpki-roots`] CA certificates. See [TLS Certificates](#tls-certificates). |
-//! | `cloud-base` | Shared cloud-provider implementation. Pulled in
automatically by every `*-base` feature; usually not enabled directly. |
+//! | `reqwest` | Enables the [`reqwest`]-based [`HttpConnector`]. Enabled
automatically by `aws`, `azure`, `gcp`, and `http`. |
+//! | `aws-lc-rs` | Bundled [`aws-lc-rs`]-based [`client::CryptoProvider`].
The default for the batteries-included provider features. |
+//! | `ring` | Bundled [`ring`]-based [`client::CryptoProvider`], e.g. for
WASM targets. |
+//! | `cloud-base` | Shared cloud-provider implementation. Enabled
automatically by `*-base` features; usually not enabled directly. |
//!
//! ## Other features
//!
@@ -563,15 +567,85 @@
//! | `tokio` | Enables Tokio-based utilities such as
[`BufReader`](buffered::BufReader) and [`BufWriter`](buffered::BufWriter).
Pulled in automatically by `fs` and the `*-base` features. |
//! | `integration` | Exposes the [`integration`] module, a reusable test
suite for verifying custom [`ObjectStore`] implementations. Not API-stable. |
//!
+//! ## Selecting a `reqwest` TLS backend
+//!
+//! `reqwest` needs a TLS backend to compile, so whenever you enable the
`reqwest` feature directly
+//! you must also enable one of `reqwest`'s TLS features:
+//!
+//! | reqwest feature | TLS stack | Notes |
+//! | --- | --- | --- |
+//! | `reqwest/rustls` | [rustls] with [`aws-lc-rs`] | enables `aws-lc-rs`.
This is what `aws`/`azure`/`gcp`/`http` enable. |
+//! | `reqwest/native-tls` | the platform's native TLS (OpenSSL / SChannel /
Secure Transport) | enables neither `rustls` nor `aws-lc-rs`. |
+//! | `reqwest/rustls-no-provider` | [rustls] with no bundled provider |
enables neither provider; you must install one at runtime, e.g.
`rustls::crypto::ring::default_provider().install_default()`. |
+//!
+//! ## Feature examples
+//!
+//! S3 implementation only; user provides the HTTP connector and crypto
provider:
+//! ```toml
+//! object_store = { default-features = false, features = ["aws-base"] }
+//! ```
+//!
+//! S3 implementation + `reqwest` + `aws-lc-rs` signing (equivalent to the
`aws` feature):
+//! ```toml
+//! object_store = { default-features = false, features = ["aws-base",
"reqwest", "reqwest/rustls", "aws-lc-rs"] }
+//! ```
+//!
+//! S3 implementation + `reqwest` with native TLS + `ring` signing (no
`aws-lc-rs` in the dependency tree):
+//! ```toml
+//! object_store = { default-features = false, features = ["aws-base",
"reqwest", "reqwest/native-tls", "ring"] }
+//! ```
+//!
+//! [rustls]: https://crates.io/crates/rustls/
+//!
+//! # Cryptography
+//!
+//! Request signing (e.g. AWS SigV4 or GCP service-account signing) requires a
+//! [`client::CryptoProvider`]. The `aws`, `gcp`, and `azure` features
+//! use [`aws-lc-rs`], matching `reqwest`'s default so that applications do
not end up with
+//! two crypto stacks.
+//!
+//! If you wish to use [`ring`] (e.g. to support WASM targets), use the
+//! `*-base` feature flags, e.g. `aws-base`, and then enable the `ring`
feature.
+//!
+//! If both `ring` and `aws-lc-rs` are enabled, `aws-lc-rs` is used by default.
+//!
+//! You can also implement a custom [`client::CryptoProvider`] to use your own
cryptographic library.
+//!
+//! This signing provider is independent of the TLS crypto provider used by the
+//! built-in `reqwest` transport — see
+//! [Selecting a `reqwest` TLS backend](#selecting-a-reqwest-tls-backend). The
+//! only combination that needs the provider registered manually (e.g.
+//! `rustls::crypto::ring::default_provider().install_default()` in your
`main`)
+//! is `reqwest/rustls-no-provider`; `reqwest/rustls` and `reqwest/native-tls`
+//! configure their TLS stack automatically.
+//!
+//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs/
+//! [`ring`]: https://crates.io/crates/ring/
+//!
//! # TLS Certificates
//!
-//! Stores that use HTTPS/TLS (this is true for most cloud stores) can choose
the source of their [CA]
-//! certificates. By default the system-bundled certificates are used (see
-//! [`rustls-native-certs`]). The `tls-webpki-roots` feature switch can be
used to also bundle Mozilla's
-//! root certificates with the library/application (see [`webpki-roots`]).
+//! Stores that use HTTPS/TLS (this is true for most cloud stores) can choose
how certificates are validated.
+//!
+//! By default [`rustls-platform-verifier`] is used to verify certificates
using the system's certificate
+//! facilities. Alternatively, this functionality can be disabled using
+//! [`ClientOptions::with_no_system_certificates`] and certificates manually
registered using
+//! [`ClientOptions::with_root_certificate`].
+//!
+//! These could be a custom CA chain, or alternatively an alternative trust
store, e.g. [`webpki-roots`].
+//!
+//! ```ignore-wasm32
+//! # #[cfg(feature = "aws")] {
+//! use object_store::{ClientOptions, Certificate};
+//!
+//! let mut options =
ClientOptions::default().with_no_system_certificates(true);
+//! for root_cert in webpki_root_certs::TLS_SERVER_ROOT_CERTS {
+//! options =
options.with_root_certificate(Certificate::from_der(root_cert.as_ref()).unwrap());
+//! }
+//! # }
+//! ```
//!
//! [CA]: https://en.wikipedia.org/wiki/Certificate_authority
-//! [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs/
+//! [`rustls-platform-verifier`]:
https://crates.io/crates/rustls-platform-verifier/
//! [`webpki-roots`]: https://crates.io/crates/webpki-roots
//!
//! # Customizing HTTP Clients
diff --git a/src/util.rs b/src/util.rs
index 21c9f7a..0dd6745 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -42,12 +42,6 @@ where
Ok(chrono::TimeZone::from_utc_datetime(&chrono::Utc, &naive))
}
-#[cfg(any(feature = "aws-base", feature = "azure-base"))]
-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<S, E>(mut stream: S, size_hint: Option<u64>) ->
Result<Bytes, E>
where
@@ -309,9 +303,13 @@ pub(crate) const STRICT_ENCODE_SET:
percent_encoding::AsciiSet = percent_encodin
/// Computes the SHA256 digest of `body` returned as a hex encoded string
#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
-pub(crate) fn hex_digest(bytes: &[u8]) -> String {
- let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
- hex_encode(digest.as_ref())
+pub(crate) fn hex_digest(
+ crypto: &dyn crate::client::CryptoProvider,
+ bytes: &[u8],
+) -> Result<String> {
+ 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