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