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