Skip to main content

cloud_sdk/operation/permit/fingerprint/
evidence.rs

1use cloud_sdk_sanitization::sanitize_bytes;
2
3use super::encoding::encode_with_authorization_evidence;
4use super::error::map_infallible;
5use super::{
6    DigestRollback, MAX_CANONICAL_PLAN_BYTES, PlanConfirmation, PlanFingerprintBuildError,
7    PlanFingerprintDigest, validate,
8};
9use crate::buffer::{SnapshotEncoder, encode_snapshot_bounded};
10use crate::operation::PermitTimestamp;
11use crate::retry::FingerprintHasher;
12
13const DOMAIN: &[u8] = b"cloud-sdk/authorization-evidence/v1\0";
14
15/// Immutable provider-owned authorization evidence appended only to a digest preimage.
16///
17/// Implementations must emit the same bounded bytes on every call and must not
18/// read clocks, random sources, atomics, or other mutable state. Sensitive
19/// values should be exposed only for the duration of [`Self::encode`].
20pub trait PlanAuthorizationEvidence {
21    /// Returns the exclusive upper bound for authority derived from this evidence.
22    ///
23    /// `None` means the evidence has no independent time limit. A returned
24    /// timestamp must cover the complete permit validity interval.
25    fn valid_until(&self) -> Option<PermitTimestamp> {
26        None
27    }
28
29    /// Encodes a provider-versioned, unambiguous evidence snapshot.
30    fn encode<E: Copy>(
31        &self,
32        writer: &mut SnapshotEncoder<'_, PlanFingerprintBuildError<E>>,
33    ) -> Result<(), PlanFingerprintBuildError<E>>;
34}
35
36/// Builds a digest over the canonical plan and sensitive authorization evidence.
37///
38/// Evidence is written only to caller-owned scratch storage, included under a
39/// separate domain, hashed with the complete plan, and cleared before return.
40pub fn build_plan_digest_with_authorization_evidence<
41    'output,
42    'plan,
43    'request,
44    H: FingerprintHasher,
45    A: PlanAuthorizationEvidence + ?Sized,
46>(
47    plan: PlanConfirmation<'plan, 'request>,
48    evidence: &A,
49    scratch: &mut [u8],
50    output: &'output mut [u8],
51    hasher: &H,
52) -> Result<PlanFingerprintDigest<'output, 'plan, 'request>, PlanFingerprintBuildError<H::Error>> {
53    sanitize_bytes(output);
54    sanitize_bytes(scratch);
55    let mut scratch = SensitiveScratch::new(scratch);
56    let mut rollback = DigestRollback::new(output);
57    let scope = validate(&plan, true)?;
58    if evidence
59        .valid_until()
60        .is_some_and(|expires_at| plan.validity.expires_at() > expires_at)
61    {
62        return Err(PlanFingerprintBuildError::AuthorizationEvidenceValidityMismatch);
63    }
64    let len = encode_snapshot_bounded(
65        (plan, evidence),
66        scratch.as_mut(),
67        MAX_CANONICAL_PLAN_BYTES,
68        PlanFingerprintBuildError::InputTooLarge,
69        encode_with_evidence::<core::convert::Infallible, A>,
70    )
71    .map_err(map_infallible)?;
72    let algorithm = hasher.algorithm();
73    let expected = algorithm.output_len();
74    if rollback.len() < expected {
75        return Err(PlanFingerprintBuildError::OutputTooSmall);
76    }
77    let digest_len = hasher
78        .digest(scratch.bytes(len), rollback.target(expected))
79        .map_err(PlanFingerprintBuildError::Hasher)?;
80    if digest_len != expected {
81        return Err(PlanFingerprintBuildError::InvalidDigestLength);
82    }
83    let output = rollback.disarm();
84    Ok(PlanFingerprintDigest {
85        algorithm,
86        storage: output,
87        len: digest_len,
88        plan,
89        scope,
90    })
91}
92
93fn encode_with_evidence<E: Copy, A: PlanAuthorizationEvidence + ?Sized>(
94    (plan, evidence): (PlanConfirmation<'_, '_>, &A),
95    writer: &mut SnapshotEncoder<'_, PlanFingerprintBuildError<E>>,
96) -> Result<(), PlanFingerprintBuildError<E>> {
97    encode_with_authorization_evidence(&plan, writer)?;
98    writer.bytes(DOMAIN)?;
99    evidence.encode(writer)
100}
101
102struct SensitiveScratch<'a>(&'a mut [u8]);
103
104impl<'a> SensitiveScratch<'a> {
105    fn new(scratch: &'a mut [u8]) -> Self {
106        Self(scratch)
107    }
108
109    fn bytes(&self, len: usize) -> &[u8] {
110        self.0.get(..len).unwrap_or_default()
111    }
112
113    fn as_mut(&mut self) -> &mut [u8] {
114        self.0
115    }
116}
117
118impl Drop for SensitiveScratch<'_> {
119    fn drop(&mut self) {
120        sanitize_bytes(self.0);
121    }
122}