1use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6use subtle::ConstantTimeEq;
7
8use super::{
9 AttemptBudget, PermitContext, PermitIdempotencyKey, PermitScope, PermitValidity, PlanChange,
10 PlanCost, PlanFingerprintScope, ReplayPolicy,
11};
12use crate::buffer::{encode_snapshot_bounded, measure_snapshot_bounded};
13use crate::operation::PreparedRequest;
14use crate::retry::{DigestAlgorithm, FingerprintHasher};
15use crate::transport::EndpointIdentity;
16
17mod encoding;
18mod error;
19mod evidence;
20mod validation;
21use encoding::encode;
22pub use error::PlanFingerprintBuildError;
23use error::map_infallible;
24pub use evidence::{PlanAuthorizationEvidence, build_plan_digest_with_authorization_evidence};
25use validation::{subject, validate};
26
27const DOMAIN_V1: &[u8] = b"cloud-sdk/plan-confirm/v1\0";
28const DOMAIN_V2: &[u8] = b"cloud-sdk/plan-confirm/v2\0";
29pub const MAX_CANONICAL_PLAN_BYTES: usize = 16_777_216;
31
32#[derive(Clone, Copy)]
34pub struct PlanConfirmation<'a, 'request> {
35 prepared: PreparedRequest<'request>,
36 endpoint: EndpointIdentity<'a>,
37 account: PlanFingerprintScope<'a>,
38 tenant: PlanFingerprintScope<'a>,
39 context: PermitContext<'a>,
40 validity: PermitValidity,
41 replay: ReplayPolicy,
42 attempts: AttemptBudget,
43 change: PlanChange,
44 cost: Option<PlanCost>,
45 idempotency: Option<PermitIdempotencyKey<'a>>,
46}
47
48impl<'a, 'request> PlanConfirmation<'a, 'request> {
49 #[allow(clippy::too_many_arguments)]
51 #[must_use]
52 pub const fn new(
53 prepared: PreparedRequest<'request>,
54 endpoint: EndpointIdentity<'a>,
55 account: PlanFingerprintScope<'a>,
56 tenant: PlanFingerprintScope<'a>,
57 context: PermitContext<'a>,
58 validity: PermitValidity,
59 replay: ReplayPolicy,
60 attempts: AttemptBudget,
61 change: PlanChange,
62 cost: Option<PlanCost>,
63 idempotency: Option<PermitIdempotencyKey<'a>>,
64 ) -> Self {
65 Self {
66 prepared,
67 endpoint,
68 account,
69 tenant,
70 context,
71 validity,
72 replay,
73 attempts,
74 change,
75 cost,
76 idempotency,
77 }
78 }
79}
80
81impl fmt::Debug for PlanConfirmation<'_, '_> {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter
84 .debug_struct("PlanConfirmation")
85 .field("prepared", &self.prepared)
86 .field("endpoint", &self.endpoint)
87 .field("account", &"[redacted]")
88 .field("tenant", &"[redacted]")
89 .field("context", &"[redacted]")
90 .field("validity", &self.validity)
91 .field("replay", &self.replay)
92 .field("attempts", &self.attempts)
93 .field("change", &self.change)
94 .field("cost", &self.cost)
95 .field("idempotency", &"[redacted]")
96 .finish()
97 }
98}
99
100pub struct CanonicalPlanFingerprint<'output, 'plan, 'request> {
102 storage: &'output mut [u8],
103 len: usize,
104 plan: PlanConfirmation<'plan, 'request>,
105 scope: PermitScope,
106}
107
108impl<'output, 'plan, 'request> CanonicalPlanFingerprint<'output, 'plan, 'request> {
109 #[must_use]
111 pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
112 PlanFingerprintRef(PlanFingerprintKind::Exact(self.bytes()))
113 }
114
115 #[must_use]
117 pub fn subject(&self) -> PlanSubject<'request, '_> {
118 subject(&self.plan, self.scope, self.as_ref())
119 }
120
121 #[must_use]
123 pub const fn len(&self) -> usize {
124 self.len
125 }
126
127 #[must_use]
129 pub const fn is_empty(&self) -> bool {
130 false
131 }
132
133 fn bytes(&self) -> &[u8] {
134 self.storage.get(..self.len).unwrap_or_default()
135 }
136}
137
138impl fmt::Debug for CanonicalPlanFingerprint<'_, '_, '_> {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter
141 .debug_struct("CanonicalPlanFingerprint")
142 .field("len", &self.len)
143 .field("scope", &self.scope)
144 .field("bytes", &"[redacted]")
145 .finish()
146 }
147}
148
149impl Drop for CanonicalPlanFingerprint<'_, '_, '_> {
150 fn drop(&mut self) {
151 sanitize_bytes(self.storage);
152 self.len = 0;
153 }
154}
155
156pub struct PlanFingerprintDigest<'output, 'plan, 'request> {
158 algorithm: DigestAlgorithm,
159 storage: &'output mut [u8],
160 len: usize,
161 plan: PlanConfirmation<'plan, 'request>,
162 scope: PermitScope,
163}
164
165impl PlanFingerprintDigest<'_, '_, '_> {
166 #[must_use]
168 pub const fn algorithm(&self) -> DigestAlgorithm {
169 self.algorithm
170 }
171
172 #[must_use]
174 pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
175 PlanFingerprintRef(PlanFingerprintKind::Digest {
176 algorithm: self.algorithm,
177 bytes: self.storage.get(..self.len).unwrap_or_default(),
178 })
179 }
180
181 #[must_use]
183 pub fn subject(&self) -> PlanSubject<'_, '_> {
184 subject(&self.plan, self.scope, self.as_ref())
185 }
186}
187
188impl fmt::Debug for PlanFingerprintDigest<'_, '_, '_> {
189 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
190 formatter
191 .debug_struct("PlanFingerprintDigest")
192 .field("algorithm", &self.algorithm)
193 .field("scope", &self.scope)
194 .field("bytes", &"[redacted]")
195 .finish()
196 }
197}
198
199impl Drop for PlanFingerprintDigest<'_, '_, '_> {
200 fn drop(&mut self) {
201 sanitize_bytes(self.storage);
202 self.len = 0;
203 }
204}
205
206#[derive(Clone, Copy)]
208pub struct PlanFingerprintRef<'a>(PlanFingerprintKind<'a>);
209
210#[derive(Clone, Copy)]
211enum PlanFingerprintKind<'a> {
212 Exact(&'a [u8]),
213 Digest {
214 algorithm: DigestAlgorithm,
215 bytes: &'a [u8],
216 },
217}
218
219impl PlanFingerprintRef<'_> {
220 pub(crate) fn matches(self, other: Self) -> bool {
221 match (self.0, other.0) {
222 (PlanFingerprintKind::Exact(left), PlanFingerprintKind::Exact(right)) => {
223 constant_time_eq(left, right)
224 }
225 (
226 PlanFingerprintKind::Digest {
227 algorithm: left,
228 bytes: left_bytes,
229 },
230 PlanFingerprintKind::Digest {
231 algorithm: right,
232 bytes: right_bytes,
233 },
234 ) => left == right && constant_time_eq(left_bytes, right_bytes),
235 _ => false,
236 }
237 }
238}
239
240impl fmt::Debug for PlanFingerprintRef<'_> {
241 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 formatter.write_str("PlanFingerprintRef([redacted])")
243 }
244}
245
246#[derive(Clone, Copy)]
248pub struct PlanSubject<'request, 'fingerprint> {
249 prepared: &'fingerprint PreparedRequest<'request>,
250 fingerprint: PlanFingerprintRef<'fingerprint>,
251 endpoint: EndpointIdentity<'fingerprint>,
252 scope: PermitScope,
253 validity: PermitValidity,
254 replay: ReplayPolicy,
255 attempts: AttemptBudget,
256 idempotency: Option<PermitIdempotencyKey<'fingerprint>>,
257}
258
259impl<'request, 'fingerprint> PlanSubject<'request, 'fingerprint> {
260 #[must_use]
262 pub const fn scope(self) -> PermitScope {
263 self.scope
264 }
265
266 #[must_use]
268 pub const fn replay_policy(self) -> ReplayPolicy {
269 self.replay
270 }
271
272 #[must_use]
274 pub const fn attempt_budget(self) -> AttemptBudget {
275 self.attempts
276 }
277
278 pub(crate) const fn endpoint(self) -> EndpointIdentity<'fingerprint> {
279 self.endpoint
280 }
281
282 pub(crate) const fn prepared(self) -> PreparedRequest<'request> {
283 *self.prepared
284 }
285
286 pub(crate) const fn fingerprint(self) -> PlanFingerprintRef<'fingerprint> {
287 self.fingerprint
288 }
289
290 pub(crate) const fn validity(self) -> PermitValidity {
291 self.validity
292 }
293
294 pub(crate) const fn idempotency(self) -> Option<PermitIdempotencyKey<'fingerprint>> {
295 self.idempotency
296 }
297}
298
299impl fmt::Debug for PlanSubject<'_, '_> {
300 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301 formatter
302 .debug_struct("PlanSubject")
303 .field("prepared", &self.prepared)
304 .field("fingerprint", &"[redacted]")
305 .field("endpoint", &self.endpoint)
306 .field("scope", &self.scope)
307 .field("validity", &self.validity)
308 .field("replay", &self.replay)
309 .field("attempts", &self.attempts)
310 .field("idempotency", &"[redacted]")
311 .finish()
312 }
313}
314
315pub fn build_canonical_plan<'output, 'plan, 'request>(
317 plan: PlanConfirmation<'plan, 'request>,
318 output: &'output mut [u8],
319) -> Result<
320 CanonicalPlanFingerprint<'output, 'plan, 'request>,
321 PlanFingerprintBuildError<core::convert::Infallible>,
322> {
323 if plan.prepared.body_sensitivity().requires_digest() {
324 sanitize_bytes(output);
325 return Err(PlanFingerprintBuildError::SensitiveBodyRequiresDigest);
326 }
327 build_canonical_plan_inner(plan, output)
328}
329
330#[allow(
331 clippy::large_types_passed_by_value,
332 reason = "the returned fingerprint must own the complete confirmed plan"
333)]
334fn build_canonical_plan_inner<'output, 'plan, 'request>(
335 plan: PlanConfirmation<'plan, 'request>,
336 output: &'output mut [u8],
337) -> Result<
338 CanonicalPlanFingerprint<'output, 'plan, 'request>,
339 PlanFingerprintBuildError<core::convert::Infallible>,
340> {
341 sanitize_bytes(output);
342 let scope = validate(&plan, false)?;
343 let required = measure_snapshot_bounded(
344 &plan,
345 MAX_CANONICAL_PLAN_BYTES,
346 PlanFingerprintBuildError::InputTooLarge,
347 encode,
348 )?;
349 if output.len() < required {
350 return Err(PlanFingerprintBuildError::OutputTooSmall);
351 }
352 let len = encode_snapshot_bounded(
353 &plan,
354 output,
355 MAX_CANONICAL_PLAN_BYTES,
356 PlanFingerprintBuildError::InputTooLarge,
357 encode,
358 )?;
359 Ok(CanonicalPlanFingerprint {
360 storage: output,
361 len,
362 plan,
363 scope,
364 })
365}
366
367pub fn build_plan_digest<'output, 'plan, 'request, H: FingerprintHasher>(
369 plan: PlanConfirmation<'plan, 'request>,
370 scratch: &mut [u8],
371 output: &'output mut [u8],
372 hasher: &H,
373) -> Result<PlanFingerprintDigest<'output, 'plan, 'request>, PlanFingerprintBuildError<H::Error>> {
374 sanitize_bytes(output);
375 let exact = build_canonical_plan_inner(plan, scratch).map_err(map_infallible)?;
376 let algorithm = hasher.algorithm();
377 let expected = algorithm.output_len();
378 if output.len() < expected {
379 return Err(PlanFingerprintBuildError::OutputTooSmall);
380 }
381 let mut rollback = DigestRollback::new(output);
382 let len = hasher
383 .digest(exact.bytes(), rollback.target(expected))
384 .map_err(PlanFingerprintBuildError::Hasher)?;
385 if len != expected {
386 return Err(PlanFingerprintBuildError::InvalidDigestLength);
387 }
388 let output = rollback.disarm();
389 Ok(PlanFingerprintDigest {
390 algorithm,
391 storage: output,
392 len,
393 plan,
394 scope: exact.scope,
395 })
396}
397
398struct DigestRollback<'a> {
399 output: &'a mut [u8],
400 armed: bool,
401}
402
403impl<'a> DigestRollback<'a> {
404 fn new(output: &'a mut [u8]) -> Self {
405 Self {
406 output,
407 armed: true,
408 }
409 }
410
411 fn target(&mut self, len: usize) -> &mut [u8] {
412 self.output.get_mut(..len).unwrap_or_default()
413 }
414
415 fn len(&self) -> usize {
416 self.output.len()
417 }
418 fn disarm(mut self) -> &'a mut [u8] {
419 self.armed = false;
420 core::mem::take(&mut self.output)
421 }
422}
423
424impl Drop for DigestRollback<'_> {
425 fn drop(&mut self) {
426 if self.armed {
427 sanitize_bytes(self.output);
428 }
429 }
430}
431
432fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
433 left.len() == right.len() && bool::from(left.ct_eq(right))
434}