1use 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";
15pub const MAX_FINGERPRINT_SCOPE_BYTES: usize = 1024;
17pub const MAX_FINGERPRINT_DIGEST_BYTES: usize = 64;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum FingerprintScope<'a> {
23 Absent,
25 Value(&'a [u8]),
27}
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub enum DigestAlgorithm {
32 Sha256,
34 Sha384,
36 Sha512,
38 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
52pub trait FingerprintHasher {
58 type Error;
60
61 fn algorithm(&self) -> DigestAlgorithm;
63
64 fn digest(&self, input: &[u8], output: &mut [u8]) -> Result<usize, Self::Error>;
66}
67
68pub enum FingerprintBuildError<E> {
70 MissingOperationId,
72 ScopeTooLong,
74 LengthOverflow,
76 OutputTooSmall,
78 EndpointNotAdmitted,
80 Hasher(E),
82 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
126pub 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 #[must_use]
136 pub fn as_ref(&self) -> FingerprintRef<'_> {
137 FingerprintRef(FingerprintKind::Exact(self.as_bytes()))
138 }
139
140 #[must_use]
142 pub fn subject(&self) -> RetrySubject<'request, '_> {
143 RetrySubject {
144 prepared: &self.prepared,
145 fingerprint: self.as_ref(),
146 }
147 }
148
149 #[must_use]
151 pub const fn len(&self) -> usize {
152 self.len
153 }
154
155 #[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
182pub 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 #[must_use]
193 pub const fn algorithm(&self) -> DigestAlgorithm {
194 self.algorithm
195 }
196
197 #[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 #[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#[derive(Clone, Copy)]
235pub struct FingerprintRef<'a>(FingerprintKind<'a>);
236
237#[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
314pub 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
345pub 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;