Skip to main content

ic_backup/model/control_authority/
mod.rs

1//! Ephemeral direct-controller observations for exact IC mutation payloads; no dispatch permits.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::OperationBindingRecord,
6    ic_request::{IcManagementRequestRecord, IcRequestError},
7    operation_plan::{OperationPlanError, OperationPlanRecord, PlanContextRecord},
8};
9use thiserror::Error;
10
11/// Maximum controllers in the maintained IC controller-set boundary.
12pub const MAX_CONTROLLERS: usize = 10;
13/// Maximum descriptive remote observations per request; never spending authority.
14pub const MAX_CONTROL_REMOTE_OBSERVATIONS: u32 = 1024;
15
16/// Canonical bounded known controller set, including an explicitly known empty set.
17///
18/// This validates identities, not observation authenticity. Missing/unknown controller
19/// evidence must fail at the provider boundary, never default into a success result.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct ControllerSet {
22    principals: Vec<String>,
23}
24impl ControllerSet {
25    /// Normalize and sort exact principals; equivalent duplicate entries reject.
26    /// # Errors
27    /// Rejects excessive counts, malformed principals and canonical duplicates.
28    pub fn new(mut principals: Vec<String>) -> Result<Self, ControlObservationError> {
29        if principals.len() > MAX_CONTROLLERS {
30            return Err(ControlObservationError::TooManyControllers);
31        }
32        for principal in &mut principals {
33            *principal = super::principal::canonical_text(principal)
34                .ok_or(ControlObservationError::InvalidPrincipal)?;
35        }
36        principals.sort();
37        if principals.windows(2).any(|pair| pair[0] == pair[1]) {
38            return Err(ControlObservationError::DuplicateController);
39        }
40        Ok(Self { principals })
41    }
42    /// Read canonical controllers; ordering grants no routing or lifecycle semantics.
43    #[must_use]
44    pub fn principals(&self) -> &[String] {
45        &self.principals
46    }
47    /// Check the exact canonical original caller, without delegating through other controllers.
48    #[must_use]
49    pub fn contains_caller(&self, binding: &OperationBindingRecord) -> bool {
50        self.principals
51            .binary_search_by(|principal| principal.as_str().cmp(binding.caller()))
52            .is_ok()
53    }
54}
55
56/// Ephemeral immutable request bound to original intent and exact host-ingress mutation bytes.
57///
58/// The integration owns challenge freshness, authenticated observations and prior
59/// per-call accounting. This request is only for direct caller-controller checks;
60/// read visibility, Root proxies and subnet-admin exceptions grant no admission here.
61#[derive(Clone, Debug)]
62pub struct ControlObservationRequest<'a> {
63    binding: OperationBindingRecord,
64    wire: &'a IcManagementRequestRecord,
65    challenge: ArtifactChecksumRecord,
66    max_remote_observations: u32,
67}
68impl<'a> ControlObservationRequest<'a> {
69    /// Derive original binding and validate exact mutation target/method/payload bytes.
70    /// # Errors
71    /// Rejects unknown operations, observation methods, changed payload/target or excessive ceiling.
72    pub fn new(
73        plan: &OperationPlanRecord,
74        sequence: u64,
75        wire: &'a IcManagementRequestRecord,
76        challenge: ArtifactChecksumRecord,
77        max_remote_observations: u32,
78    ) -> Result<Self, ControlRequestError> {
79        if max_remote_observations > MAX_CONTROL_REMOTE_OBSERVATIONS {
80            return Err(ControlRequestError::ObservationLimitTooLarge);
81        }
82        let binding = plan.attempt_authority(sequence)?.binding().clone();
83        wire.validate_mutation_binding(&binding)?;
84        Ok(Self {
85            binding,
86            wire,
87            challenge,
88            max_remote_observations,
89        })
90    }
91    /// Read exact original intent/context/target/payload identity.
92    #[must_use]
93    pub const fn binding(&self) -> &OperationBindingRecord {
94        &self.binding
95    }
96    /// Read validated exact method/routing/Candid mutation bytes.
97    #[must_use]
98    pub const fn wire(&self) -> &IcManagementRequestRecord {
99        self.wire
100    }
101    /// Read integration-owned challenge; its value alone proves no freshness.
102    #[must_use]
103    pub const fn challenge(&self) -> &ArtifactChecksumRecord {
104        &self.challenge
105    }
106    /// Read descriptive call ceiling, separate from original spending allowances.
107    #[must_use]
108    pub const fn max_remote_observations(&self) -> u32 {
109        self.max_remote_observations
110    }
111    /// Hash original intent/sequence, exact wire digest, challenge and descriptive call ceiling.
112    ///
113    /// Encoding: NUL-terminated ASCII v1 domain, 64 ASCII intent bytes, u64
114    /// big-endian sequence, 64 ASCII wire-digest bytes, 64 ASCII challenge bytes,
115    /// then u32 big-endian ceiling. Observed evidence/controllers are excluded.
116    #[must_use]
117    pub fn digest(&self) -> ArtifactChecksumRecord {
118        let mut bytes = b"ic-backup/control-observation/v1\0".to_vec();
119        bytes.extend_from_slice(self.binding.intent().as_bytes());
120        bytes.extend_from_slice(&self.binding.operation_sequence().to_be_bytes());
121        bytes.extend_from_slice(self.wire.digest().hash().as_bytes());
122        bytes.extend_from_slice(self.challenge.hash().as_bytes());
123        bytes.extend_from_slice(&self.max_remote_observations.to_be_bytes());
124        ArtifactChecksumRecord::from_bytes(&bytes)
125    }
126}
127
128/// Passive provider data; no permissive default or automatic JSON admission.
129#[derive(Clone, Debug)]
130pub struct ControlObservationInput {
131    /// Exact current challenge-bound request digest.
132    pub request: ArtifactChecksumRecord,
133    /// Actually observed canonical network/caller/release, not echoed expected labels.
134    pub context: PlanContextRecord,
135    /// Actually observed physical target; normalized at model admission.
136    pub target: String,
137    /// Complete known controller set; public/read access is never a controller entry.
138    pub controllers: ControllerSet,
139    /// Opaque evidence digest qualified by the integration, not a signature or permission.
140    pub evidence: ArtifactChecksumRecord,
141    /// Actual calls, requiring separately approved prior per-call accounting.
142    pub remote_observations: u32,
143}
144/// Immutable canonical ephemeral provider result; has no Serde or persisted authority lane.
145#[derive(Clone, Debug)]
146pub struct ControlObservation {
147    input: ControlObservationInput,
148}
149impl ControlObservation {
150    /// Admit canonical observed target; other fields are validated by their owning types.
151    /// # Errors
152    /// Rejects malformed observed target principals.
153    pub fn new(mut input: ControlObservationInput) -> Result<Self, ControlObservationError> {
154        input.target = super::principal::canonical_text(&input.target)
155            .ok_or(ControlObservationError::InvalidPrincipal)?;
156        Ok(Self { input })
157    }
158    /// Read exact current request identity.
159    #[must_use]
160    pub const fn request(&self) -> &ArtifactChecksumRecord {
161        &self.input.request
162    }
163    /// Read actual observed context.
164    #[must_use]
165    pub const fn context(&self) -> &PlanContextRecord {
166        &self.input.context
167    }
168    /// Read canonical actual target.
169    #[must_use]
170    pub fn target(&self) -> &str {
171        &self.input.target
172    }
173    /// Read known controllers; an empty set never satisfies caller-control admission.
174    #[must_use]
175    pub const fn controllers(&self) -> &ControllerSet {
176        &self.input.controllers
177    }
178    /// Read opaque integration evidence identifier.
179    #[must_use]
180    pub const fn evidence(&self) -> &ArtifactChecksumRecord {
181        &self.input.evidence
182    }
183    /// Read reported calls; this changes no original allowance.
184    #[must_use]
185    pub const fn remote_observations(&self) -> u32 {
186        self.input.remote_observations
187    }
188}
189
190/// Typed owning-boundary observation rejection, without raw provider output.
191#[derive(Debug, Eq, Error, PartialEq)]
192pub enum ControlObservationError {
193    /// Target/controller principal failed bounded canonical admission.
194    #[error("invalid control observation principal")]
195    InvalidPrincipal,
196    /// Full set exceeds the maintained bound.
197    #[error("controller set exceeds {MAX_CONTROLLERS}")]
198    TooManyControllers,
199    /// Equivalent principal appears twice.
200    #[error("duplicate controller principal")]
201    DuplicateController,
202}
203/// Typed request rejection before provider use or any remote effect.
204#[derive(Debug, Error)]
205pub enum ControlRequestError {
206    /// Descriptive call ceiling exceeds the maintained bound.
207    #[error("control observation ceiling exceeds {MAX_CONTROL_REMOTE_OBSERVATIONS}")]
208    ObservationLimitTooLarge,
209    /// Original plan cannot derive the named operation.
210    #[error(transparent)]
211    Plan(#[from] OperationPlanError),
212    /// Actual payload is not the exact original mutation.
213    #[error(transparent)]
214    Payload(#[from] IcRequestError),
215}
216
217#[cfg(test)]
218mod tests;