Skip to main content

cloud_sdk/authentication/signing/
context.rs

1use core::fmt;
2
3use crate::transport::EndpointIdentity;
4use crate::{ProviderId, ServiceId};
5
6use super::super::ScopeValue;
7
8/// Maximum bytes in a signing key identifier.
9pub const MAX_SIGNING_KEY_ID_BYTES: usize = 256;
10/// Maximum bytes in a body-digest algorithm identifier.
11pub const MAX_SIGNING_DIGEST_ALGORITHM_BYTES: usize = 128;
12/// Maximum bytes in a signing algorithm identifier.
13pub const MAX_SIGNING_ALGORITHM_BYTES: usize = 128;
14
15/// Signing-context text validation failure.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum SigningContextValueError {
18    /// The value must not be empty.
19    Empty,
20    /// The value exceeds its type-specific byte limit.
21    TooLong,
22    /// The value must be visible ASCII without backslashes.
23    InvalidByte,
24}
25
26impl_static_error!(SigningContextValueError,
27    Self::Empty => "signing context value is empty",
28    Self::TooLong => "signing context value exceeds the length limit",
29    Self::InvalidByte => "signing context value contains an invalid byte",
30);
31
32macro_rules! define_context_value {
33    ($name:ident, $maximum:ident, $label:literal) => {
34        #[doc = $label]
35        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
36        pub struct $name<'a>(&'a str);
37
38        impl<'a> $name<'a> {
39            /// Validates a provider-selected canonical identifier.
40            pub fn new(value: &'a str) -> Result<Self, SigningContextValueError> {
41                validate_context_value(value, $maximum)?;
42                Ok(Self(value))
43            }
44
45            /// Returns the exact canonical provider-selected identifier.
46            #[must_use]
47            pub const fn as_str(self) -> &'a str {
48                self.0
49            }
50        }
51
52        impl fmt::Debug for $name<'_> {
53            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54                formatter.write_str(concat!(stringify!($name), "([redacted])"))
55            }
56        }
57    };
58}
59
60define_context_value!(
61    SigningKeyId,
62    MAX_SIGNING_KEY_ID_BYTES,
63    "Borrowed provider-owned signing key identifier."
64);
65define_context_value!(
66    SigningDigestAlgorithm,
67    MAX_SIGNING_DIGEST_ALGORITHM_BYTES,
68    "Borrowed provider-owned request-body digest algorithm identifier."
69);
70define_context_value!(
71    SigningAlgorithm,
72    MAX_SIGNING_ALGORITHM_BYTES,
73    "Borrowed provider-owned signature algorithm identifier."
74);
75
76/// Complete security domain bound into one canonical signature.
77#[derive(Clone, Copy)]
78pub struct SigningContext<'a> {
79    provider: ProviderId,
80    service: ServiceId,
81    endpoint: EndpointIdentity<'a>,
82    audience: Option<ScopeValue<'a>>,
83    account: Option<ScopeValue<'a>>,
84    tenant: Option<ScopeValue<'a>>,
85    key_id: SigningKeyId<'a>,
86    digest_algorithm: SigningDigestAlgorithm<'a>,
87    signature_algorithm: SigningAlgorithm<'a>,
88}
89
90impl<'a> SigningContext<'a> {
91    /// Binds required provider, service, endpoint, key, and algorithm identity.
92    #[must_use]
93    pub const fn new(
94        provider: ProviderId,
95        service: ServiceId,
96        endpoint: EndpointIdentity<'a>,
97        key_id: SigningKeyId<'a>,
98        digest_algorithm: SigningDigestAlgorithm<'a>,
99        signature_algorithm: SigningAlgorithm<'a>,
100    ) -> Self {
101        Self {
102            provider,
103            service,
104            endpoint,
105            audience: None,
106            account: None,
107            tenant: None,
108            key_id,
109            digest_algorithm,
110            signature_algorithm,
111        }
112    }
113
114    /// Binds a provider-owned audience.
115    #[must_use]
116    pub const fn with_audience(mut self, value: ScopeValue<'a>) -> Self {
117        self.audience = Some(value);
118        self
119    }
120
121    /// Binds a provider-owned account.
122    #[must_use]
123    pub const fn with_account(mut self, value: ScopeValue<'a>) -> Self {
124        self.account = Some(value);
125        self
126    }
127
128    /// Binds a provider-owned tenant.
129    #[must_use]
130    pub const fn with_tenant(mut self, value: ScopeValue<'a>) -> Self {
131        self.tenant = Some(value);
132        self
133    }
134
135    /// Returns the bound provider.
136    #[must_use]
137    pub const fn provider(self) -> ProviderId {
138        self.provider
139    }
140
141    /// Returns the bound service.
142    #[must_use]
143    pub const fn service(self) -> ServiceId {
144        self.service
145    }
146
147    /// Returns the normalized bound endpoint.
148    #[must_use]
149    pub const fn endpoint(self) -> EndpointIdentity<'a> {
150        self.endpoint
151    }
152
153    /// Returns the optional bound audience.
154    #[must_use]
155    pub const fn audience(self) -> Option<ScopeValue<'a>> {
156        self.audience
157    }
158
159    /// Returns the optional bound account.
160    #[must_use]
161    pub const fn account(self) -> Option<ScopeValue<'a>> {
162        self.account
163    }
164
165    /// Returns the optional bound tenant.
166    #[must_use]
167    pub const fn tenant(self) -> Option<ScopeValue<'a>> {
168        self.tenant
169    }
170
171    /// Returns the bound key identifier.
172    #[must_use]
173    pub const fn key_id(self) -> SigningKeyId<'a> {
174        self.key_id
175    }
176
177    /// Returns the bound request-body digest algorithm identifier.
178    #[must_use]
179    pub const fn digest_algorithm(self) -> SigningDigestAlgorithm<'a> {
180        self.digest_algorithm
181    }
182
183    /// Returns the bound signature algorithm identifier.
184    #[must_use]
185    pub const fn signature_algorithm(self) -> SigningAlgorithm<'a> {
186        self.signature_algorithm
187    }
188}
189
190impl fmt::Debug for SigningContext<'_> {
191    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192        formatter
193            .debug_struct("SigningContext")
194            .field("provider", &self.provider)
195            .field("service", &self.service)
196            .field("endpoint", &"[redacted]")
197            .field("audience", &self.audience.map(|_| "[redacted]"))
198            .field("account", &self.account.map(|_| "[redacted]"))
199            .field("tenant", &self.tenant.map(|_| "[redacted]"))
200            .field("key_id", &"[redacted]")
201            .field("digest_algorithm", &"[redacted]")
202            .field("signature_algorithm", &"[redacted]")
203            .finish()
204    }
205}
206
207fn validate_context_value(value: &str, maximum: usize) -> Result<(), SigningContextValueError> {
208    if value.is_empty() {
209        return Err(SigningContextValueError::Empty);
210    }
211    if value.len() > maximum {
212        return Err(SigningContextValueError::TooLong);
213    }
214    if !value
215        .bytes()
216        .all(|byte| byte.is_ascii_graphic() && byte != b'\\')
217    {
218        return Err(SigningContextValueError::InvalidByte);
219    }
220    Ok(())
221}