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::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";
29/// Maximum complete exact plan-confirm input.
30pub const MAX_CANONICAL_PLAN_BYTES: usize = 16_777_216;
31
32/// Complete immutable plan-confirm inputs.
33#[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    /// Binds caller policy and current plan observations to one prepared request.
50    #[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
100/// Caller-buffer exact plan-confirm input, cleared in full on drop.
101pub 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    /// Returns a redacted exact comparison reference.
110    #[must_use]
111    pub fn as_ref(&self) -> PlanFingerprintRef<'_> {
112        PlanFingerprintRef(PlanFingerprintKind::Exact(self.bytes()))
113    }
114
115    /// Returns the exact prepared request and confirmed authority metadata.
116    #[must_use]
117    pub fn subject(&self) -> PlanSubject<'request, '_> {
118        subject(&self.plan, self.scope, self.as_ref())
119    }
120
121    /// Returns the complete canonical byte length without exposing bytes.
122    #[must_use]
123    pub const fn len(&self) -> usize {
124        self.len
125    }
126
127    /// Reports whether the canonical input is empty. It is never empty.
128    #[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
156/// Caller-buffer strong digest of a plan confirmation, cleared on drop.
157pub 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    /// Returns the admitted collision-resistant digest algorithm.
167    #[must_use]
168    pub const fn algorithm(&self) -> DigestAlgorithm {
169        self.algorithm
170    }
171
172    /// Returns a redacted strong-digest comparison reference.
173    #[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    /// Returns the exact request and confirmed authority metadata.
182    #[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/// Borrowed exact or strong-digest plan identity.
207#[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/// One prepared request inseparably bound to a validated plan confirmation.
247#[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    /// Returns the required permit scope.
261    #[must_use]
262    pub const fn scope(self) -> PermitScope {
263        self.scope
264    }
265
266    /// Returns the caller-selected replay policy.
267    #[must_use]
268    pub const fn replay_policy(self) -> ReplayPolicy {
269        self.replay
270    }
271
272    /// Returns the hard attempt budget.
273    #[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
315/// Builds exact canonical plan-confirm bytes into caller-owned storage.
316pub 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
367/// Builds a caller-selected collision-resistant digest and clears scratch.
368pub 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}