Skip to main content

ic_backup/model/restore_safety/requirement/
mod.rs

1//! Immutable original restore/source declarations; no current safety or fence authority.
2
3use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::{
5    artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
6    operation_plan::OperationPlanRecord,
7};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Maximum raw input and canonical output bytes for the retained safety requirement.
12pub const MAX_RESTORE_SAFETY_REQUIREMENT_BYTES: u64 = 1024;
13/// Explicit original safety lane; neither declaration proves the application safe.
14#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum RestoreSafetyLaneRecord {
17    /// An application-qualified absence of irreversible external effects is required.
18    NoIrreversibleEffects,
19    /// An exact continuously retained fence outside rewindable state is required.
20    ApplicationFenced,
21}
22/// Original integration-retained fence and authority revisions; never a release token.
23#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct RestoreFenceBindingRecord {
26    /// Exact original fence identity.
27    pub identity: ArtifactChecksumRecord,
28    /// Original membership authority revision bound to that fence.
29    pub membership_revision: ArtifactChecksumRecord,
30    /// Original external-obligation authority revision, retained outside the snapshot.
31    pub external_obligations_revision: ArtifactChecksumRecord,
32}
33/// Passive original source and application safety declaration; no neutral default.
34#[derive(Clone, Debug)]
35pub struct RestoreSafetyRequirementRequest {
36    /// Exact integration-qualified complete source artifact/manifest digest.
37    pub source_artifacts: ArtifactChecksumRecord,
38    /// Explicit required application safety lane.
39    pub safety: RestoreSafetyLaneRecord,
40    /// Required only for the fenced lane; recovered from the integration's durable owner.
41    pub expected_fence: Option<RestoreFenceBindingRecord>,
42}
43/// Strict v1 immutable restore intent, original source and safety declaration.
44///
45/// Source plan and artifacts must be qualified and kept in stable custody by the
46/// integration. This record authenticates no backup, creates no fence, and grants
47/// no load/start, paid call, artifact completeness or reference-release authority.
48#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(try_from = "RequirementFields")]
50pub struct RestoreSafetyRequirementRecord {
51    version: u16,
52    plan_intent: String,
53    source_plan_intent: String,
54    source_artifacts: ArtifactChecksumRecord,
55    safety: RestoreSafetyLaneRecord,
56    expected_fence: Option<RestoreFenceBindingRecord>,
57}
58#[derive(Deserialize)]
59#[serde(deny_unknown_fields)]
60struct RequirementFields {
61    version: u16,
62    plan_intent: String,
63    source_plan_intent: String,
64    source_artifacts: ArtifactChecksumRecord,
65    safety: RestoreSafetyLaneRecord,
66    #[serde(deserialize_with = "crate::model::required_option::deserialize")]
67    expected_fence: Option<RestoreFenceBindingRecord>,
68}
69
70impl TryFrom<RequirementFields> for RestoreSafetyRequirementRecord {
71    type Error = RestoreSafetyRequirementError;
72    fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
73        if fields.version != 1 {
74            return Err(RestoreSafetyRequirementError::UnsupportedVersion(
75                fields.version,
76            ));
77        }
78        validate_lane(fields.safety, fields.expected_fence.as_ref())?;
79        Ok(Self {
80            version: 1,
81            plan_intent: canonical_hash(&fields.plan_intent)?,
82            source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
83            source_artifacts: fields.source_artifacts,
84            safety: fields.safety,
85            expected_fence: fields.expected_fence,
86        })
87    }
88}
89fn validate_lane(
90    safety: RestoreSafetyLaneRecord,
91    fence: Option<&RestoreFenceBindingRecord>,
92) -> Result<(), RestoreSafetyRequirementError> {
93    match (safety, fence) {
94        (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
95            Err(RestoreSafetyRequirementError::FenceRequired)
96        }
97        (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
98            Err(RestoreSafetyRequirementError::UnexpectedFence)
99        }
100        _ => Ok(()),
101    }
102}
103fn validate_source(
104    plan: &OperationPlanRecord,
105    source: &OperationPlanRecord,
106) -> Result<(), RestoreSafetyRequirementError> {
107    if plan.context().network() != source.context().network() {
108        return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
109    }
110    if plan.context().release() != source.context().release() {
111        return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
112    }
113    if plan
114        .selected_targets()
115        .iter()
116        .any(|target| source.selected_targets().binary_search(target).is_err())
117    {
118        return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
119    }
120    Ok(())
121}
122impl RestoreSafetyRequirementRecord {
123    /// Bind original restore and source plans, same network/release/IDs and explicit safety lane.
124    ///
125    /// A restore selection may be a subset of the source selection. Application
126    /// safety for that exact subset remains qualified by the provider. Source caller
127    /// equality is not required; current caller permissions are a separate boundary.
128    /// # Errors
129    /// Rejects source network/release/selection mismatch or an inappropriate fence.
130    pub fn new(
131        plan: &OperationPlanRecord,
132        source: &OperationPlanRecord,
133        input: RestoreSafetyRequirementRequest,
134    ) -> Result<Self, RestoreSafetyRequirementError> {
135        validate_source(plan, source)?;
136        validate_lane(input.safety, input.expected_fence.as_ref())?;
137        Ok(Self {
138            version: 1,
139            plan_intent: plan.digest().hash().into(),
140            source_plan_intent: source.digest().hash().into(),
141            source_artifacts: input.source_artifacts,
142            safety: input.safety,
143            expected_fence: input.expected_fence,
144        })
145    }
146    /// Read full original restore intent, including requests and attempt allowances.
147    #[must_use]
148    pub fn plan_intent(&self) -> &str {
149        &self.plan_intent
150    }
151    /// Read full original source plan identity; not source completeness or authenticity.
152    #[must_use]
153    pub fn source_plan_intent(&self) -> &str {
154        &self.source_plan_intent
155    }
156    /// Read exact original source artifact binding supplied by its qualified owner.
157    #[must_use]
158    pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
159        &self.source_artifacts
160    }
161    /// Read the original explicit safety lane.
162    #[must_use]
163    pub const fn safety(&self) -> RestoreSafetyLaneRecord {
164        self.safety
165    }
166    /// Read original retained fence/revisions; creates no current custody or release capability.
167    #[must_use]
168    pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
169        self.expected_fence.as_ref()
170    }
171    /// Validate both exact original plans and same-network/release/ID source admission.
172    /// # Errors
173    /// Rejects changed original intent/source or source context/selection mismatch.
174    pub fn validate_plans(
175        &self,
176        plan: &OperationPlanRecord,
177        source: &OperationPlanRecord,
178    ) -> Result<(), RestoreSafetyRequirementError> {
179        if self.plan_intent != plan.digest().hash() {
180            return Err(RestoreSafetyRequirementError::PlanMismatch);
181        }
182        if self.source_plan_intent != source.digest().hash() {
183            return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
184        }
185        validate_source(plan, source)
186    }
187    /// Hash NUL-terminated v1 ASCII domain, three 64-byte ASCII digests and safety tag.
188    ///
189    /// Tags: no irreversible effects=0, application fenced=1. The fenced lane
190    /// appends 64 ASCII bytes each of fence identity, membership revision and
191    /// external-obligations revision. Version is bound through the domain.
192    #[must_use]
193    pub fn digest(&self) -> ArtifactChecksumRecord {
194        let mut bytes = EvidenceDigest::new(b"ic-backup/restore-safety-requirement/v1\0");
195        bytes.append(self.plan_intent.as_bytes());
196        bytes.append(self.source_plan_intent.as_bytes());
197        bytes.append(self.source_artifacts.hash().as_bytes());
198        bytes.byte(match self.safety {
199            RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
200            RestoreSafetyLaneRecord::ApplicationFenced => 1,
201        });
202        if let Some(fence) = &self.expected_fence {
203            bytes.append(fence.identity.hash().as_bytes());
204            bytes.append(fence.membership_revision.hash().as_bytes());
205            bytes.append(fence.external_obligations_revision.hash().as_bytes());
206        }
207        bytes.finish()
208    }
209}
210/// Typed original source/safety declaration denial; retains all existing obligations.
211#[derive(Debug, Error)]
212pub enum RestoreSafetyRequirementError {
213    /// Only v1 is maintained.
214    #[error("unsupported restore safety requirement version {0}")]
215    UnsupportedVersion(u16),
216    /// Full original restore intent differs.
217    #[error("restore safety original plan mismatch")]
218    PlanMismatch,
219    /// Full original source plan differs.
220    #[error("restore safety original source plan mismatch")]
221    SourcePlanMismatch,
222    /// Cross-network recovery is outside this product scope.
223    #[error("restore source network mismatch")]
224    SourceNetworkMismatch,
225    /// Cross-release recovery is outside this product scope.
226    #[error("restore source release mismatch")]
227    SourceReleaseMismatch,
228    /// Every selected exact target must also have been selected in the source.
229    #[error("restore source selection mismatch")]
230    SourceSelectionMismatch,
231    /// The application-fenced lane must retain original fence/revisions.
232    #[error("restore safety requires original fence binding")]
233    FenceRequired,
234    /// The no-irreversible-effects lane cannot silently add a fence obligation.
235    #[error("unexpected restore safety fence binding")]
236    UnexpectedFence,
237    /// Intent hash admission failed.
238    #[error(transparent)]
239    Checksum(#[from] ChecksumError),
240}