Skip to main content

cloud_sdk/operation/permit/
fingerprint.rs

1//! Versioned plan-confirm identity with exact or strong-digest comparison.
2
3use 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::{CostIntent, OperationImpact, PreparedRequest};
14use crate::retry::{DigestAlgorithm, FingerprintHasher};
15use crate::transport::EndpointIdentity;
16
17mod encoding;
18mod error;
19use encoding::encode;
20pub use error::PlanFingerprintBuildError;
21use error::map_infallible;
22
23const DOMAIN: &[u8] = b"cloud-sdk/plan-confirm/v1\0";
24/// Maximum complete exact plan-confirm input.
25pub const MAX_CANONICAL_PLAN_BYTES: usize = 16_777_216;
26
27/// Complete immutable plan-confirm inputs.
28#[derive(Clone, Copy)]
29pub struct PlanConfirmation<'a, 'request> {
30    prepared: PreparedRequest<'request>,
31    endpoint: EndpointIdentity<'a>,
32    account: PlanFingerprintScope<'a>,
33    tenant: PlanFingerprintScope<'a>,
34    context: PermitContext<'a>,
35    validity: PermitValidity,
36    replay: ReplayPolicy,
37    attempts: AttemptBudget,
38    change: PlanChange,
39    cost: Option<PlanCost>,
40    idempotency: Option<PermitIdempotencyKey<'a>>,
41}
42
43impl<'a, 'request> PlanConfirmation<'a, 'request> {
44    /// Binds caller policy and current plan observations to one prepared request.
45    #[allow(clippy::too_many_arguments)]
46    #[must_use]
47    pub const fn new(
48        prepared: PreparedRequest<'request>,
49        endpoint: EndpointIdentity<'a>,
50        account: PlanFingerprintScope<'a>,
51        tenant: PlanFingerprintScope<'a>,
52        context: PermitContext<'a>,
53        validity: PermitValidity,
54        replay: ReplayPolicy,
55        attempts: AttemptBudget,
56        change: PlanChange,
57        cost: Option<PlanCost>,
58        idempotency: Option<PermitIdempotencyKey<'a>>,
59    ) -> Self {
60        Self {
61            prepared,
62            endpoint,
63            account,
64            tenant,
65            context,
66            validity,
67            replay,
68            attempts,
69            change,
70            cost,
71            idempotency,
72        }
73    }
74}
75
76impl fmt::Debug for PlanConfirmation<'_, '_> {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        formatter
79            .debug_struct("PlanConfirmation")
80            .field("prepared", &self.prepared)
81            .field("endpoint", &self.endpoint)
82            .field("account", &"[redacted]")
83            .field("tenant", &"[redacted]")
84            .field("context", &"[redacted]")
85            .field("validity", &self.validity)
86            .field("replay", &self.replay)
87            .field("attempts", &self.attempts)
88            .field("change", &self.change)
89            .field("cost", &self.cost)
90            .field("idempotency", &"[redacted]")
91            .finish()
92    }
93}
94
95/// Caller-buffer exact plan-confirm input, cleared in full on drop.
96pub struct CanonicalPlanFingerprint<'output, 'plan, 'request> {
97    storage: &'output mut [u8],
98    len: usize,
99    plan: PlanConfirmation<'plan, 'request>,
100    scope: PermitScope,
101}
102
103impl<'output, 'plan, 'request> CanonicalPlanFingerprint<'output, 'plan, 'request> {
104    /// Returns a redacted exact comparison reference.
105    #[must_use]
106    pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
107        PlanFingerprintRef(PlanFingerprintKind::Exact(self.bytes()))
108    }
109
110    /// Returns the exact prepared request and confirmed authority metadata.
111    #[must_use]
112    pub fn subject(&self) -> PlanSubject<'request, '_> {
113        subject(&self.plan, self.scope, self.as_ref())
114    }
115
116    /// Returns the complete canonical byte length without exposing bytes.
117    #[must_use]
118    pub const fn len(&self) -> usize {
119        self.len
120    }
121
122    /// Reports whether the canonical input is empty. It is never empty.
123    #[must_use]
124    pub const fn is_empty(&self) -> bool {
125        false
126    }
127
128    fn bytes(&self) -> &[u8] {
129        self.storage.get(..self.len).unwrap_or_default()
130    }
131}
132
133impl fmt::Debug for CanonicalPlanFingerprint<'_, '_, '_> {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter
136            .debug_struct("CanonicalPlanFingerprint")
137            .field("len", &self.len)
138            .field("scope", &self.scope)
139            .field("bytes", &"[redacted]")
140            .finish()
141    }
142}
143
144impl Drop for CanonicalPlanFingerprint<'_, '_, '_> {
145    fn drop(&mut self) {
146        sanitize_bytes(self.storage);
147        self.len = 0;
148    }
149}
150
151/// Caller-buffer strong digest of a plan confirmation, cleared on drop.
152pub struct PlanFingerprintDigest<'output, 'plan, 'request> {
153    algorithm: DigestAlgorithm,
154    storage: &'output mut [u8],
155    len: usize,
156    plan: PlanConfirmation<'plan, 'request>,
157    scope: PermitScope,
158}
159
160impl PlanFingerprintDigest<'_, '_, '_> {
161    /// Returns the admitted collision-resistant digest algorithm.
162    #[must_use]
163    pub const fn algorithm(&self) -> DigestAlgorithm {
164        self.algorithm
165    }
166
167    /// Returns a redacted strong-digest comparison reference.
168    #[must_use]
169    pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
170        PlanFingerprintRef(PlanFingerprintKind::Digest {
171            algorithm: self.algorithm,
172            bytes: self.storage.get(..self.len).unwrap_or_default(),
173        })
174    }
175
176    /// Returns the exact request and confirmed authority metadata.
177    #[must_use]
178    pub fn subject(&self) -> PlanSubject<'_, '_> {
179        subject(&self.plan, self.scope, self.as_ref())
180    }
181}
182
183impl fmt::Debug for PlanFingerprintDigest<'_, '_, '_> {
184    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185        formatter
186            .debug_struct("PlanFingerprintDigest")
187            .field("algorithm", &self.algorithm)
188            .field("scope", &self.scope)
189            .field("bytes", &"[redacted]")
190            .finish()
191    }
192}
193
194impl Drop for PlanFingerprintDigest<'_, '_, '_> {
195    fn drop(&mut self) {
196        sanitize_bytes(self.storage);
197        self.len = 0;
198    }
199}
200
201/// Borrowed exact or strong-digest plan identity.
202#[derive(Clone, Copy)]
203pub struct PlanFingerprintRef<'a>(PlanFingerprintKind<'a>);
204
205#[derive(Clone, Copy)]
206enum PlanFingerprintKind<'a> {
207    Exact(&'a [u8]),
208    Digest {
209        algorithm: DigestAlgorithm,
210        bytes: &'a [u8],
211    },
212}
213
214impl PlanFingerprintRef<'_> {
215    pub(crate) fn matches(self, other: Self) -> bool {
216        match (self.0, other.0) {
217            (PlanFingerprintKind::Exact(left), PlanFingerprintKind::Exact(right)) => {
218                constant_time_eq(left, right)
219            }
220            (
221                PlanFingerprintKind::Digest {
222                    algorithm: left,
223                    bytes: left_bytes,
224                },
225                PlanFingerprintKind::Digest {
226                    algorithm: right,
227                    bytes: right_bytes,
228                },
229            ) => left == right && constant_time_eq(left_bytes, right_bytes),
230            _ => false,
231        }
232    }
233}
234
235impl fmt::Debug for PlanFingerprintRef<'_> {
236    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237        formatter.write_str("PlanFingerprintRef([redacted])")
238    }
239}
240
241/// One prepared request inseparably bound to a validated plan confirmation.
242#[derive(Clone, Copy)]
243pub struct PlanSubject<'request, 'fingerprint> {
244    prepared: &'fingerprint PreparedRequest<'request>,
245    fingerprint: PlanFingerprintRef<'fingerprint>,
246    endpoint: EndpointIdentity<'fingerprint>,
247    scope: PermitScope,
248    validity: PermitValidity,
249    replay: ReplayPolicy,
250    attempts: AttemptBudget,
251    idempotency: Option<PermitIdempotencyKey<'fingerprint>>,
252}
253
254impl<'request, 'fingerprint> PlanSubject<'request, 'fingerprint> {
255    /// Returns the required permit scope.
256    #[must_use]
257    pub const fn scope(self) -> PermitScope {
258        self.scope
259    }
260
261    /// Returns the caller-selected replay policy.
262    #[must_use]
263    pub const fn replay_policy(self) -> ReplayPolicy {
264        self.replay
265    }
266
267    /// Returns the hard attempt budget.
268    #[must_use]
269    pub const fn attempt_budget(self) -> AttemptBudget {
270        self.attempts
271    }
272
273    pub(crate) const fn endpoint(self) -> EndpointIdentity<'fingerprint> {
274        self.endpoint
275    }
276
277    pub(crate) const fn prepared(self) -> PreparedRequest<'request> {
278        *self.prepared
279    }
280
281    pub(crate) const fn fingerprint(self) -> PlanFingerprintRef<'fingerprint> {
282        self.fingerprint
283    }
284
285    pub(crate) const fn validity(self) -> PermitValidity {
286        self.validity
287    }
288
289    pub(crate) const fn idempotency(self) -> Option<PermitIdempotencyKey<'fingerprint>> {
290        self.idempotency
291    }
292}
293
294impl fmt::Debug for PlanSubject<'_, '_> {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        formatter
297            .debug_struct("PlanSubject")
298            .field("prepared", &self.prepared)
299            .field("fingerprint", &"[redacted]")
300            .field("endpoint", &self.endpoint)
301            .field("scope", &self.scope)
302            .field("validity", &self.validity)
303            .field("replay", &self.replay)
304            .field("attempts", &self.attempts)
305            .field("idempotency", &"[redacted]")
306            .finish()
307    }
308}
309
310/// Builds exact canonical plan-confirm bytes into caller-owned storage.
311pub fn build_canonical_plan<'output, 'plan, 'request>(
312    plan: PlanConfirmation<'plan, 'request>,
313    output: &'output mut [u8],
314) -> Result<
315    CanonicalPlanFingerprint<'output, 'plan, 'request>,
316    PlanFingerprintBuildError<core::convert::Infallible>,
317> {
318    sanitize_bytes(output);
319    let scope = validate(&plan)?;
320    let required = measure_snapshot_bounded(
321        &plan,
322        MAX_CANONICAL_PLAN_BYTES,
323        PlanFingerprintBuildError::InputTooLarge,
324        encode,
325    )?;
326    if output.len() < required {
327        return Err(PlanFingerprintBuildError::OutputTooSmall);
328    }
329    let len = encode_snapshot_bounded(
330        &plan,
331        output,
332        MAX_CANONICAL_PLAN_BYTES,
333        PlanFingerprintBuildError::InputTooLarge,
334        encode,
335    )?;
336    Ok(CanonicalPlanFingerprint {
337        storage: output,
338        len,
339        plan,
340        scope,
341    })
342}
343
344/// Builds a caller-selected collision-resistant digest and clears scratch.
345pub fn build_plan_digest<'output, 'plan, 'request, H: FingerprintHasher>(
346    plan: PlanConfirmation<'plan, 'request>,
347    scratch: &mut [u8],
348    output: &'output mut [u8],
349    hasher: &H,
350) -> Result<PlanFingerprintDigest<'output, 'plan, 'request>, PlanFingerprintBuildError<H::Error>> {
351    sanitize_bytes(output);
352    let exact = build_canonical_plan(plan, scratch).map_err(map_infallible)?;
353    let algorithm = hasher.algorithm();
354    let expected = algorithm.output_len();
355    if output.len() < expected {
356        return Err(PlanFingerprintBuildError::OutputTooSmall);
357    }
358    let mut rollback = DigestRollback::new(output);
359    let len = hasher
360        .digest(exact.bytes(), rollback.target(expected))
361        .map_err(PlanFingerprintBuildError::Hasher)?;
362    if len != expected {
363        return Err(PlanFingerprintBuildError::InvalidDigestLength);
364    }
365    let output = rollback.disarm();
366    Ok(PlanFingerprintDigest {
367        algorithm,
368        storage: output,
369        len,
370        plan,
371        scope: exact.scope,
372    })
373}
374
375struct DigestRollback<'a> {
376    output: &'a mut [u8],
377    armed: bool,
378}
379
380impl<'a> DigestRollback<'a> {
381    fn new(output: &'a mut [u8]) -> Self {
382        Self {
383            output,
384            armed: true,
385        }
386    }
387
388    fn target(&mut self, len: usize) -> &mut [u8] {
389        self.output.get_mut(..len).unwrap_or_default()
390    }
391
392    fn disarm(mut self) -> &'a mut [u8] {
393        self.armed = false;
394        core::mem::take(&mut self.output)
395    }
396}
397
398impl Drop for DigestRollback<'_> {
399    fn drop(&mut self) {
400        if self.armed {
401            sanitize_bytes(self.output);
402        }
403    }
404}
405
406pub(super) fn validate<E>(
407    plan: &PlanConfirmation<'_, '_>,
408) -> Result<PermitScope, PlanFingerprintBuildError<E>> {
409    if plan.prepared.operation_id().is_none() {
410        return Err(PlanFingerprintBuildError::MissingOperationId);
411    }
412    if !plan
413        .prepared
414        .service()
415        .endpoint_policy()
416        .admits(plan.endpoint)
417    {
418        return Err(PlanFingerprintBuildError::EndpointNotAdmitted);
419    }
420    if plan.change == PlanChange::NoOp {
421        return Err(PlanFingerprintBuildError::NoOp);
422    }
423    plan.account
424        .bytes()
425        .map_err(PlanFingerprintBuildError::Context)?;
426    plan.tenant
427        .bytes()
428        .map_err(PlanFingerprintBuildError::Context)?;
429    let metadata = plan.prepared.metadata();
430    let scope = match (metadata.cost_intent(), metadata.impact()) {
431        (CostIntent::MayIncurCost, _) => PermitScope::Cost,
432        (_, OperationImpact::Destructive) => PermitScope::Destructive,
433        (_, OperationImpact::Mutation) => PermitScope::Mutation,
434        (_, OperationImpact::ReadOnly) => return Err(PlanFingerprintBuildError::ReadOnlyOperation),
435    };
436    match (scope, plan.cost) {
437        (PermitScope::Cost, None) => return Err(PlanFingerprintBuildError::MissingCost),
438        (PermitScope::Mutation | PermitScope::Destructive, Some(_)) => {
439            return Err(PlanFingerprintBuildError::UnexpectedCost);
440        }
441        _ => {}
442    }
443    match (plan.replay, plan.attempts.get(), plan.idempotency) {
444        (ReplayPolicy::SingleAttempt, 1, None)
445        | (ReplayPolicy::RecoverNotSent, _, None)
446        | (ReplayPolicy::ReconcileThenRetry, _, Some(_)) => {}
447        (ReplayPolicy::SingleAttempt, _, _) => {
448            return Err(PlanFingerprintBuildError::InvalidSingleAttemptBudget);
449        }
450        (ReplayPolicy::ReconcileThenRetry, _, None) => {
451            return Err(PlanFingerprintBuildError::MissingIdempotency);
452        }
453        (_, _, Some(_)) => return Err(PlanFingerprintBuildError::UnexpectedIdempotency),
454    }
455    Ok(scope)
456}
457
458fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
459    left.len() == right.len() && bool::from(left.ct_eq(right))
460}
461
462fn subject<'request, 'plan: 'fingerprint, 'fingerprint>(
463    plan: &'fingerprint PlanConfirmation<'plan, 'request>,
464    scope: PermitScope,
465    fingerprint: PlanFingerprintRef<'fingerprint>,
466) -> PlanSubject<'request, 'fingerprint> {
467    PlanSubject {
468        prepared: &plan.prepared,
469        fingerprint,
470        endpoint: plan.endpoint,
471        scope,
472        validity: plan.validity,
473        replay: plan.replay,
474        attempts: plan.attempts,
475        idempotency: plan.idempotency,
476    }
477}