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