1use 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";
17pub const MAX_FINGERPRINT_SCOPE_BYTES: usize = 1024;
19pub const MAX_FINGERPRINT_DIGEST_BYTES: usize = 64;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FingerprintScope<'a> {
25 Absent,
27 Value(&'a [u8]),
29}
30
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub enum DigestAlgorithm {
34 Sha256,
36 Sha384,
38 Sha512,
40 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
54pub trait FingerprintHasher {
60 type Error;
62
63 fn algorithm(&self) -> DigestAlgorithm;
65
66 fn digest(&self, input: &[u8], output: &mut [u8]) -> Result<usize, Self::Error>;
68}
69
70pub enum FingerprintBuildError<E> {
72 MissingOperationId,
74 ScopeTooLong,
76 LengthOverflow,
78 OutputTooSmall,
80 EndpointNotAdmitted,
82 SensitiveBodyRequiresDigest,
84 Hasher(E),
86 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
136pub 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 #[must_use]
146 pub fn as_ref(&self) -> FingerprintRef<'_> {
147 FingerprintRef(FingerprintKind::Exact(self.as_bytes()))
148 }
149
150 #[must_use]
152 pub fn subject(&self) -> RetrySubject<'request, '_> {
153 RetrySubject {
154 prepared: &self.prepared,
155 fingerprint: self.as_ref(),
156 }
157 }
158
159 #[must_use]
161 pub const fn len(&self) -> usize {
162 self.len
163 }
164
165 #[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
192pub 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 #[must_use]
203 pub const fn algorithm(&self) -> DigestAlgorithm {
204 self.algorithm
205 }
206
207 #[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 #[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#[derive(Clone, Copy)]
245pub struct FingerprintRef<'a>(FingerprintKind<'a>);
246
247#[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
324pub 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
373pub 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;