diff --git a/src/auth/src/lib.rs b/src/auth/src/lib.rs index 29e8caaaa7..1a7d7bc0af 100644 --- a/src/auth/src/lib.rs +++ b/src/auth/src/lib.rs @@ -57,3 +57,7 @@ pub(crate) mod retry; /// /// [Credentials]: https://cloud.google.com/docs/authentication#credentials pub(crate) mod headers_util; + +#[cfg(google_cloud_unstable_signed_url)] +#[allow(dead_code)] +pub mod signer; diff --git a/src/auth/src/signer.rs b/src/auth/src/signer.rs new file mode 100644 index 0000000000..0b68276ff4 --- /dev/null +++ b/src/auth/src/signer.rs @@ -0,0 +1,223 @@ +// 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. +// 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. +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 "xxx@developer.gserviceaccount.com". Required. + fn client_email(&self) -> impl Future> + Send; + + /// Signs the content. + /// + /// Returns the signature. + fn sign(&self, content: &[u8]) -> impl Future> + Send; +} + +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), +} + +#[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(()) + } +}