From e86c2829206b11f8dd98966a0475d20c2f9e2878 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Tue, 2 Dec 2025 21:20:44 +0000 Subject: [PATCH 1/3] impl(auth): add SigningProvider trait --- src/auth/src/lib.rs | 3 + src/auth/src/signer.rs | 172 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/auth/src/signer.rs diff --git a/src/auth/src/lib.rs b/src/auth/src/lib.rs index 29e8caaaa7..7224fb97e6 100644 --- a/src/auth/src/lib.rs +++ b/src/auth/src/lib.rs @@ -57,3 +57,6 @@ pub(crate) mod retry; /// /// [Credentials]: https://cloud.google.com/docs/authentication#credentials pub(crate) mod headers_util; + +#[cfg(google_cloud_unstable_signed_url)] +pub mod signer; diff --git a/src/auth/src/signer.rs b/src/auth/src/signer.rs new file mode 100644 index 0000000000..6d3a608181 --- /dev/null +++ b/src/auth/src/signer.rs @@ -0,0 +1,172 @@ +// Copyright 2024 Google LLC +// +// Licensed 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 +// +// https://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 std::sync::Arc; + +pub type Result = std::result::Result; + +/// An implementation of [crate::signer::SigningProvider] that wraps a dynamic provider. +/// +/// This struct is the primary entry point for signing operations. It can be created +/// from any type that implements [SigningProvider]. +#[derive(Clone, Debug)] +pub struct Signer { + pub(crate) inner: Arc, +} + +impl std::convert::From for Signer +where + T: SigningProvider + Send + Sync + 'static, +{ + fn from(value: T) -> Self { + Self { + inner: Arc::new(value), + } + } +} + +impl Signer { + /// Returns the email address of the client performing the signing. + /// + /// This is typically the service account email. + pub async fn client_email(&self) -> Result { + self.inner.client_email().await + } + + /// Signs the provided content using the underlying provider. + /// + /// The content is typically a string-to-sign generated by the caller. + /// Returns the signature as a base64 encoded string (or other format depending on implementation, + /// but typically hex or base64). + pub async fn sign(&self, content: T) -> Result + where + T: AsRef<[u8]> + Send + Sync, + { + self.inner.sign(content.as_ref()).await + } +} + +/// A trait for types that can sign content. +#[async_trait::async_trait] +pub trait SigningProvider: Send + Sync + std::fmt::Debug { + /// Returns the email address of the authorizer. + /// + /// It is typically the Google service account client email address from the Google Developers Console + /// in the form of "xyz@developer.gserviceaccount.com". Required. + async fn client_email(&self) -> Result; + + /// Signs the content. + /// + /// Returns the signature in hex format. + async fn sign(&self, content: &[u8]) -> Result; +} + +pub(crate) mod dynamic { + use super::Result; + + /// A dyn-compatible, crate-private version of `SigningProvider`. + #[async_trait::async_trait] + pub trait SigningProvider: Send + Sync + std::fmt::Debug { + async fn client_email(&self) -> Result; + async fn sign(&self, content: &[u8]) -> Result; + } + + /// The public CredentialsProvider implements the dyn-compatible CredentialsProvider. + #[async_trait::async_trait] + impl SigningProvider for T + where + T: super::SigningProvider + Send + Sync, + { + async fn client_email(&self) -> Result { + T::client_email(self).await + } + + async fn sign(&self, content: &[u8]) -> Result { + T::sign(self, content).await + } + } +} + +type BoxError = Box; + +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +pub struct SigningError(SigningErrorKind); + +impl SigningError { + /// A problem using API to sign blob. + pub fn is_transport(&self) -> bool { + matches!(self.0, SigningErrorKind::Transport(_)) + } + + /// A problem parsing a private key for local signing. + pub fn is_parsing(&self) -> bool { + matches!(self.0, SigningErrorKind::Parsing(_)) + } + + /// A problem signing content. + pub fn is_sign(&self) -> bool { + matches!(self.0, SigningErrorKind::Sign(_)) + } + + /// A problem parsing a private key for local signing. + pub(crate) fn parsing(source: T) -> SigningError + where + T: Into, + { + SigningError(SigningErrorKind::Parsing(source.into())) + } + + /// A problem using API to sign blob. + pub(crate) fn transport(source: T) -> SigningError + where + T: Into, + { + SigningError(SigningErrorKind::Transport(source.into())) + } + + /// A problem signing content. + pub(crate) fn sign(source: T) -> SigningError + where + T: Into, + { + SigningError(SigningErrorKind::Sign(source.into())) + } + + /// Creates a new `SigningError`. + /// + /// This function is only intended for use in the client libraries + /// implementation. Application may use this in mocks, though we do not + /// recommend that you write tests for specific error cases. + /// + /// # Parameters + /// * `message` - The underlying error that caused the signing failure. + #[doc(hidden)] + pub fn from_msg(message: T) -> SigningError + where + T: Into, + { + SigningError(SigningErrorKind::Sign(message.into())) + } +} + +#[derive(thiserror::Error, Debug)] +enum SigningErrorKind { + #[error("failed to generate signature via IAM API: {0}")] + Transport(#[source] BoxError), + #[error("failed to parse private key: {0}")] + Parsing(#[source] BoxError), + #[error("failed to sign content: {0}")] + Sign(#[source] BoxError), +} From 1257c10141c8eb9d3a614f3b43c323d025908463 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 3 Dec 2025 15:07:13 +0000 Subject: [PATCH 2/3] fix: mark as dead code for now --- src/auth/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auth/src/lib.rs b/src/auth/src/lib.rs index 7224fb97e6..1a7d7bc0af 100644 --- a/src/auth/src/lib.rs +++ b/src/auth/src/lib.rs @@ -59,4 +59,5 @@ pub(crate) mod retry; pub(crate) mod headers_util; #[cfg(google_cloud_unstable_signed_url)] +#[allow(dead_code)] pub mod signer; From 5d701aa3de80e07133bdb876363d4e42c979a7d7 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 3 Dec 2025 16:48:47 +0000 Subject: [PATCH 3/3] fix: address review comments --- src/auth/src/signer.rs | 65 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/auth/src/signer.rs b/src/auth/src/signer.rs index 6d3a608181..0b68276ff4 100644 --- a/src/auth/src/signer.rs +++ b/src/auth/src/signer.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -58,18 +58,17 @@ impl Signer { } /// A trait for types that can sign content. -#[async_trait::async_trait] -pub trait SigningProvider: Send + Sync + std::fmt::Debug { +pub trait SigningProvider: std::fmt::Debug { /// Returns the email address of the authorizer. /// /// It is typically the Google service account client email address from the Google Developers Console - /// in the form of "xyz@developer.gserviceaccount.com". Required. - async fn client_email(&self) -> Result; + /// in the form of "xxx@developer.gserviceaccount.com". Required. + fn client_email(&self) -> impl Future> + Send; /// Signs the content. /// - /// Returns the signature in hex format. - async fn sign(&self, content: &[u8]) -> Result; + /// Returns the signature. + fn sign(&self, content: &[u8]) -> impl Future> + Send; } pub(crate) mod dynamic { @@ -170,3 +169,55 @@ enum SigningErrorKind { #[error("failed to sign content: {0}")] Sign(#[source] BoxError), } + +#[cfg(test)] +mod tests { + use super::*; + + type TestResult = anyhow::Result<()>; + + mockall::mock! { + #[derive(Debug)] + Signer{} + + impl SigningProvider for Signer { + async fn client_email(&self) -> Result; + async fn sign(&self, content: &[u8]) -> Result; + } + } + + #[tokio::test] + async fn test_signer_success() -> TestResult { + let mut mock = MockSigner::new(); + mock.expect_client_email() + .returning(|| Ok("test".to_string())); + mock.expect_sign().returning(|_| Ok("test".to_string())); + let signer = Signer::from(mock); + + let result = signer.client_email().await?; + assert_eq!(result, "test"); + let result = signer.sign("test").await?; + assert_eq!(result, "test"); + + Ok(()) + } + + #[tokio::test] + async fn test_signer_error() -> TestResult { + let mut mock = MockSigner::new(); + mock.expect_client_email() + .returning(|| Err(SigningError::transport("test"))); + mock.expect_sign() + .returning(|_| Err(SigningError::sign("test"))); + let signer = Signer::from(mock); + + let result = signer.client_email().await; + assert!(result.is_err()); + assert!(result.unwrap_err().is_transport()); + let result = signer.sign("test").await; + assert!(result.is_err()); + assert!(result.unwrap_err().is_sign()); + + Ok(()) + } +}