Skip to main content

cloud_sdk/retry/
fingerprint.rs

1//! Versioned canonical request identity and caller-supplied strong digests.
2
3use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6use subtle::ConstantTimeEq;
7
8use crate::operation::PreparedRequest;
9use crate::transport::EndpointIdentity;
10
11mod encoding;
12mod writer;
13use encoding::{encode, encoded_len};
14use writer::Writer;
15
16const DOMAIN: &[u8] = b"cloud-sdk/retry-fingerprint/v2\0";
17/// Maximum account or tenant scope bytes admitted to a fingerprint.
18pub const MAX_FINGERPRINT_SCOPE_BYTES: usize = 1024;
19/// Maximum supported collision-resistant digest output.
20pub const MAX_FINGERPRINT_DIGEST_BYTES: usize = 64;
21
22/// Explicit account or tenant scope bound into a request fingerprint.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FingerprintScope<'a> {
25    /// The provider operation has no additional account or tenant scope.
26    Absent,
27    /// Exact caller-provided account or tenant scope bytes.
28    Value(&'a [u8]),
29}
30
31/// Admitted collision-resistant digest algorithms.
32#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub enum DigestAlgorithm {
34    /// SHA-256 with a 32-byte output.
35    Sha256,
36    /// SHA-384 with a 48-byte output.
37    Sha384,
38    /// SHA-512 with a 64-byte output.
39    Sha512,
40    /// BLAKE3 with its standard 32-byte output.
41    Blake3,
42}
43
44impl DigestAlgorithm {
45    pub(crate) const fn output_len(self) -> usize {
46        match self {
47            Self::Sha256 | Self::Blake3 => 32,
48            Self::Sha384 => 48,
49            Self::Sha512 => 64,
50        }
51    }
52}
53
54/// Caller-provided collision-resistant fingerprint digest implementation.
55///
56/// Implementations must compute the declared algorithm over exactly `input`.
57/// Ordinary `Hash`, CRC, and other non-cryptographic digests do not satisfy
58/// this security contract.
59pub trait FingerprintHasher {
60    /// Hashing failure.
61    type Error;
62
63    /// Returns the collision-resistant algorithm implemented by this value.
64    fn algorithm(&self) -> DigestAlgorithm;
65
66    /// Writes the digest and returns its initialized length.
67    fn digest(&self, input: &[u8], output: &mut [u8]) -> Result<usize, Self::Error>;
68}
69
70/// Canonical fingerprint or digest construction failure.
71pub enum FingerprintBuildError<E> {
72    /// Prepared requests used for retry must have an operation identifier.
73    MissingOperationId,
74    /// Account or tenant scope exceeds the bounded policy.
75    ScopeTooLong,
76    /// Canonical length arithmetic overflowed.
77    LengthOverflow,
78    /// Caller storage cannot hold the complete canonical fingerprint.
79    OutputTooSmall,
80    /// The prepared service policy does not admit the fingerprint endpoint.
81    EndpointNotAdmitted,
82    /// Sensitive request bodies may only use collision-resistant digests.
83    SensitiveBodyRequiresDigest,
84    /// The caller-provided digest implementation failed.
85    Hasher(E),
86    /// The digest implementation returned the wrong initialized length.
87    InvalidDigestLength,
88}
89
90impl<E> fmt::Debug for FingerprintBuildError<E> {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        formatter.write_str(match self {
93            Self::MissingOperationId => "FingerprintBuildError::MissingOperationId",
94            Self::ScopeTooLong => "FingerprintBuildError::ScopeTooLong",
95            Self::LengthOverflow => "FingerprintBuildError::LengthOverflow",
96            Self::OutputTooSmall => "FingerprintBuildError::OutputTooSmall",
97            Self::EndpointNotAdmitted => "FingerprintBuildError::EndpointNotAdmitted",
98            Self::SensitiveBodyRequiresDigest => {
99                "FingerprintBuildError::SensitiveBodyRequiresDigest"
100            }
101            Self::Hasher(_) => "FingerprintBuildError::Hasher([redacted])",
102            Self::InvalidDigestLength => "FingerprintBuildError::InvalidDigestLength",
103        })
104    }
105}
106
107impl<E> fmt::Display for FingerprintBuildError<E> {
108    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109        formatter.write_str(match self {
110            Self::MissingOperationId => "retry fingerprint requires an operation identifier",
111            Self::ScopeTooLong => "retry fingerprint scope exceeds the length limit",
112            Self::LengthOverflow => "retry fingerprint length overflowed",
113            Self::OutputTooSmall => "retry fingerprint output is too small",
114            Self::EndpointNotAdmitted => "retry fingerprint endpoint is not admitted",
115            Self::SensitiveBodyRequiresDigest => {
116                "sensitive request body requires a collision-resistant retry digest"
117            }
118            Self::Hasher(_) => "retry fingerprint hashing failed",
119            Self::InvalidDigestLength => "retry fingerprint digest length is invalid",
120        })
121    }
122}
123
124impl<E> core::error::Error for FingerprintBuildError<E>
125where
126    E: core::error::Error + 'static,
127{
128    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
129        match self {
130            Self::Hasher(error) => Some(error),
131            _ => None,
132        }
133    }
134}
135
136/// Caller-buffer canonical request fingerprint cleared on drop.
137pub struct CanonicalFingerprint<'output, 'request> {
138    storage: &'output mut [u8],
139    len: usize,
140    prepared: PreparedRequest<'request>,
141}
142
143impl<'output, 'request> CanonicalFingerprint<'output, 'request> {
144    /// Returns a redacted comparison reference without exposing diagnostics.
145    #[must_use]
146    pub fn as_ref(&self) -> FingerprintRef<'_> {
147        FingerprintRef(FingerprintKind::Exact(self.as_bytes()))
148    }
149
150    /// Binds this fingerprint to the exact prepared request used to build it.
151    #[must_use]
152    pub fn subject(&self) -> RetrySubject<'request, '_> {
153        RetrySubject {
154            prepared: &self.prepared,
155            fingerprint: self.as_ref(),
156        }
157    }
158
159    /// Returns the initialized canonical length.
160    #[must_use]
161    pub const fn len(&self) -> usize {
162        self.len
163    }
164
165    /// Reports whether the canonical input is empty. It is never empty.
166    #[must_use]
167    pub const fn is_empty(&self) -> bool {
168        false
169    }
170
171    fn as_bytes(&self) -> &[u8] {
172        self.storage.get(..self.len).unwrap_or_default()
173    }
174}
175
176impl fmt::Debug for CanonicalFingerprint<'_, '_> {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        formatter
179            .debug_struct("CanonicalFingerprint")
180            .field("len", &self.len)
181            .field("bytes", &"[redacted]")
182            .finish()
183    }
184}
185
186impl Drop for CanonicalFingerprint<'_, '_> {
187    fn drop(&mut self) {
188        sanitize_bytes(self.storage);
189    }
190}
191
192/// Caller-buffer collision-resistant digest cleared on drop.
193pub struct FingerprintDigest<'output, 'request> {
194    algorithm: DigestAlgorithm,
195    storage: &'output mut [u8],
196    len: usize,
197    prepared: PreparedRequest<'request>,
198}
199
200impl<'output, 'request> FingerprintDigest<'output, 'request> {
201    /// Returns the admitted digest algorithm.
202    #[must_use]
203    pub const fn algorithm(&self) -> DigestAlgorithm {
204        self.algorithm
205    }
206
207    /// Returns a redacted comparison reference.
208    #[must_use]
209    pub fn as_ref(&self) -> FingerprintRef<'_> {
210        FingerprintRef(FingerprintKind::Digest {
211            algorithm: self.algorithm,
212            bytes: self.storage.get(..self.len).unwrap_or_default(),
213        })
214    }
215
216    /// Binds this digest to the exact prepared request used to build it.
217    #[must_use]
218    pub fn subject(&self) -> RetrySubject<'request, '_> {
219        RetrySubject {
220            prepared: &self.prepared,
221            fingerprint: self.as_ref(),
222        }
223    }
224}
225
226impl fmt::Debug for FingerprintDigest<'_, '_> {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        formatter
229            .debug_struct("FingerprintDigest")
230            .field("algorithm", &self.algorithm)
231            .field("bytes", &"[redacted]")
232            .finish()
233    }
234}
235
236impl Drop for FingerprintDigest<'_, '_> {
237    fn drop(&mut self) {
238        sanitize_bytes(self.storage);
239        self.len = 0;
240    }
241}
242
243/// Borrowed exact or collision-resistant request fingerprint.
244#[derive(Clone, Copy)]
245pub struct FingerprintRef<'a>(FingerprintKind<'a>);
246
247/// One prepared request inseparably bound to its canonical fingerprint.
248///
249/// Fields are private and only fingerprint guards can construct this value.
250///
251/// ```compile_fail
252/// use cloud_sdk::operation::PreparedRequest;
253/// use cloud_sdk::retry::{FingerprintRef, RetrySubject};
254///
255/// fn forge<'request, 'fingerprint>(
256///     prepared: &'fingerprint PreparedRequest<'request>,
257///     fingerprint: FingerprintRef<'fingerprint>,
258/// ) -> RetrySubject<'request, 'fingerprint> {
259///     RetrySubject { prepared, fingerprint }
260/// }
261/// ```
262#[derive(Clone, Copy)]
263pub struct RetrySubject<'request, 'fingerprint> {
264    prepared: &'fingerprint PreparedRequest<'request>,
265    fingerprint: FingerprintRef<'fingerprint>,
266}
267
268impl<'request, 'fingerprint> RetrySubject<'request, 'fingerprint> {
269    pub(crate) const fn prepared(self) -> &'fingerprint PreparedRequest<'request> {
270        self.prepared
271    }
272
273    pub(crate) const fn fingerprint(self) -> FingerprintRef<'fingerprint> {
274        self.fingerprint
275    }
276}
277
278impl fmt::Debug for RetrySubject<'_, '_> {
279    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
280        formatter
281            .debug_struct("RetrySubject")
282            .field("prepared", &self.prepared)
283            .field("fingerprint", &"[redacted]")
284            .finish()
285    }
286}
287
288#[derive(Clone, Copy)]
289enum FingerprintKind<'a> {
290    Exact(&'a [u8]),
291    Digest {
292        algorithm: DigestAlgorithm,
293        bytes: &'a [u8],
294    },
295}
296
297impl<'a> FingerprintRef<'a> {
298    pub(crate) fn matches(self, other: Self) -> bool {
299        match (self.0, other.0) {
300            (FingerprintKind::Exact(left), FingerprintKind::Exact(right)) => {
301                constant_time_eq(left, right)
302            }
303            (
304                FingerprintKind::Digest {
305                    algorithm: left_algorithm,
306                    bytes: left,
307                },
308                FingerprintKind::Digest {
309                    algorithm: right_algorithm,
310                    bytes: right,
311                },
312            ) => left_algorithm == right_algorithm && constant_time_eq(left, right),
313            _ => false,
314        }
315    }
316}
317
318impl fmt::Debug for FingerprintRef<'_> {
319    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
320        formatter.write_str("FingerprintRef([redacted])")
321    }
322}
323
324/// Builds exact versioned canonical bytes into caller-owned storage.
325pub fn build_canonical_fingerprint<'output, 'request>(
326    request: PreparedRequest<'request>,
327    endpoint: EndpointIdentity<'_>,
328    scope: FingerprintScope<'_>,
329    output: &'output mut [u8],
330) -> Result<CanonicalFingerprint<'output, 'request>, FingerprintBuildError<core::convert::Infallible>>
331{
332    sanitize_bytes(output);
333    if request.body_sensitivity().requires_digest() {
334        return Err(FingerprintBuildError::SensitiveBodyRequiresDigest);
335    }
336    build_canonical_fingerprint_inner(request, endpoint, scope, output)
337}
338
339#[allow(
340    clippy::large_types_passed_by_value,
341    reason = "the returned fingerprint must own the complete prepared request"
342)]
343fn build_canonical_fingerprint_inner<'output, 'request>(
344    request: PreparedRequest<'request>,
345    endpoint: EndpointIdentity<'_>,
346    scope: FingerprintScope<'_>,
347    output: &'output mut [u8],
348) -> Result<CanonicalFingerprint<'output, 'request>, FingerprintBuildError<core::convert::Infallible>>
349{
350    sanitize_bytes(output);
351    if !request.service().endpoint_policy().admits(endpoint) {
352        return Err(FingerprintBuildError::EndpointNotAdmitted);
353    }
354    let required = encoded_len(&request, endpoint, scope)?;
355    if output.len() < required {
356        return Err(FingerprintBuildError::OutputTooSmall);
357    }
358    let encoded = {
359        let mut writer = Writer::new(output);
360        encode(&request, endpoint, scope, &mut writer)
361    };
362    if let Err(error) = encoded {
363        sanitize_bytes(output);
364        return Err(error);
365    }
366    Ok(CanonicalFingerprint {
367        storage: output,
368        len: required,
369        prepared: request,
370    })
371}
372
373/// Builds and hashes canonical bytes, clearing caller scratch on every path.
374pub fn build_fingerprint_digest<'output, 'request, H: FingerprintHasher>(
375    request: PreparedRequest<'request>,
376    endpoint: EndpointIdentity<'_>,
377    scope: FingerprintScope<'_>,
378    scratch: &mut [u8],
379    output: &'output mut [u8],
380    hasher: &H,
381) -> Result<FingerprintDigest<'output, 'request>, FingerprintBuildError<H::Error>> {
382    sanitize_bytes(output);
383    let canonical = build_canonical_fingerprint_inner(request, endpoint, scope, scratch)
384        .map_err(map_infallible_error)?;
385    let algorithm = hasher.algorithm();
386    let expected = algorithm.output_len();
387    let mut digest = FingerprintDigest {
388        algorithm,
389        storage: output,
390        len: 0,
391        prepared: request,
392    };
393    let output = digest
394        .storage
395        .get_mut(..expected)
396        .ok_or(FingerprintBuildError::OutputTooSmall)?;
397    let len = hasher
398        .digest(canonical.as_bytes(), output)
399        .map_err(FingerprintBuildError::Hasher)?;
400    if len != expected {
401        return Err(FingerprintBuildError::InvalidDigestLength);
402    }
403    digest.len = len;
404    Ok(digest)
405}
406
407fn map_infallible_error<E>(
408    error: FingerprintBuildError<core::convert::Infallible>,
409) -> FingerprintBuildError<E> {
410    match error {
411        FingerprintBuildError::MissingOperationId => FingerprintBuildError::MissingOperationId,
412        FingerprintBuildError::ScopeTooLong => FingerprintBuildError::ScopeTooLong,
413        FingerprintBuildError::LengthOverflow => FingerprintBuildError::LengthOverflow,
414        FingerprintBuildError::OutputTooSmall => FingerprintBuildError::OutputTooSmall,
415        FingerprintBuildError::EndpointNotAdmitted => FingerprintBuildError::EndpointNotAdmitted,
416        FingerprintBuildError::SensitiveBodyRequiresDigest => {
417            FingerprintBuildError::SensitiveBodyRequiresDigest
418        }
419        FingerprintBuildError::InvalidDigestLength => FingerprintBuildError::InvalidDigestLength,
420        FingerprintBuildError::Hasher(never) => match never {},
421    }
422}
423
424fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
425    left.len() == right.len() && bool::from(left.ct_eq(right))
426}
427
428#[cfg(test)]
429mod tests;