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