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, canonical_hash},
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: canonical_hash(&fields.plan_intent)?,
85            source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
86            source_artifacts: fields.source_artifacts,
87            safety: fields.safety,
88            expected_fence: fields.expected_fence,
89        })
90    }
91}
92fn validate_lane(
93    safety: RestoreSafetyLaneRecord,
94    fence: Option<&RestoreFenceBindingRecord>,
95) -> Result<(), RestoreSafetyRequirementError> {
96    match (safety, fence) {
97        (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
98            Err(RestoreSafetyRequirementError::FenceRequired)
99        }
100        (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
101            Err(RestoreSafetyRequirementError::UnexpectedFence)
102        }
103        _ => Ok(()),
104    }
105}
106fn validate_source(
107    plan: &OperationPlanRecord,
108    source: &OperationPlanRecord,
109) -> Result<(), RestoreSafetyRequirementError> {
110    if plan.context().network() != source.context().network() {
111        return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
112    }
113    if plan.context().release() != source.context().release() {
114        return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
115    }
116    if plan
117        .selected_targets()
118        .iter()
119        .any(|target| source.selected_targets().binary_search(target).is_err())
120    {
121        return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
122    }
123    Ok(())
124}
125impl RestoreSafetyRequirementRecord {
126    /// Bind original restore and source plans, same network/release/IDs and explicit safety lane.
127    ///
128    /// A restore selection may be a subset of the source selection. Application
129    /// safety for that exact subset remains qualified by the provider. Source caller
130    /// equality is not required; current caller permissions are a separate boundary.
131    /// # Errors
132    /// Rejects source network/release/selection mismatch or an inappropriate fence.
133    pub fn new(
134        plan: &OperationPlanRecord,
135        source: &OperationPlanRecord,
136        input: RestoreSafetyRequirementRequest,
137    ) -> Result<Self, RestoreSafetyRequirementError> {
138        validate_source(plan, source)?;
139        validate_lane(input.safety, input.expected_fence.as_ref())?;
140        Ok(Self {
141            version: 1,
142            plan_intent: plan.digest().hash().into(),
143            source_plan_intent: source.digest().hash().into(),
144            source_artifacts: input.source_artifacts,
145            safety: input.safety,
146            expected_fence: input.expected_fence,
147        })
148    }
149    /// Read full original restore intent, including requests and attempt allowances.
150    #[must_use]
151    pub fn plan_intent(&self) -> &str {
152        &self.plan_intent
153    }
154    /// Read full original source plan identity; not source completeness or authenticity.
155    #[must_use]
156    pub fn source_plan_intent(&self) -> &str {
157        &self.source_plan_intent
158    }
159    /// Read exact original source artifact binding supplied by its qualified owner.
160    #[must_use]
161    pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
162        &self.source_artifacts
163    }
164    /// Read the original explicit safety lane.
165    #[must_use]
166    pub const fn safety(&self) -> RestoreSafetyLaneRecord {
167        self.safety
168    }
169    /// Read original retained fence/revisions; creates no current custody or release capability.
170    #[must_use]
171    pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
172        self.expected_fence.as_ref()
173    }
174    /// Validate both exact original plans and same-network/release/ID source admission.
175    /// # Errors
176    /// Rejects changed original intent/source or source context/selection mismatch.
177    pub fn validate_plans(
178        &self,
179        plan: &OperationPlanRecord,
180        source: &OperationPlanRecord,
181    ) -> Result<(), RestoreSafetyRequirementError> {
182        if self.plan_intent != plan.digest().hash() {
183            return Err(RestoreSafetyRequirementError::PlanMismatch);
184        }
185        if self.source_plan_intent != source.digest().hash() {
186            return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
187        }
188        validate_source(plan, source)
189    }
190    /// Hash NUL-terminated v1 ASCII domain, three 64-byte ASCII digests and safety tag.
191    ///
192    /// Tags: no irreversible effects=0, application fenced=1. The fenced lane
193    /// appends 64 ASCII bytes each of fence identity, membership revision and
194    /// external-obligations revision. Version is bound through the domain.
195    #[must_use]
196    pub fn digest(&self) -> ArtifactChecksumRecord {
197        let mut bytes = b"ic-backup/restore-safety-requirement/v1\0".to_vec();
198        bytes.extend_from_slice(self.plan_intent.as_bytes());
199        bytes.extend_from_slice(self.source_plan_intent.as_bytes());
200        bytes.extend_from_slice(self.source_artifacts.hash().as_bytes());
201        bytes.push(match self.safety {
202            RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
203            RestoreSafetyLaneRecord::ApplicationFenced => 1,
204        });
205        if let Some(fence) = &self.expected_fence {
206            bytes.extend_from_slice(fence.identity.hash().as_bytes());
207            bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
208            bytes.extend_from_slice(fence.external_obligations_revision.hash().as_bytes());
209        }
210        ArtifactChecksumRecord::from_bytes(&bytes)
211    }
212}
213/// Typed original source/safety declaration denial; retains all existing obligations.
214#[derive(Debug, Error)]
215pub enum RestoreSafetyRequirementError {
216    /// Only v1 is maintained.
217    #[error("unsupported restore safety requirement version {0}")]
218    UnsupportedVersion(u16),
219    /// Full original restore intent differs.
220    #[error("restore safety original plan mismatch")]
221    PlanMismatch,
222    /// Full original source plan differs.
223    #[error("restore safety original source plan mismatch")]
224    SourcePlanMismatch,
225    /// Cross-network recovery is outside this product scope.
226    #[error("restore source network mismatch")]
227    SourceNetworkMismatch,
228    /// Cross-release recovery is outside this product scope.
229    #[error("restore source release mismatch")]
230    SourceReleaseMismatch,
231    /// Every selected exact target must also have been selected in the source.
232    #[error("restore source selection mismatch")]
233    SourceSelectionMismatch,
234    /// The application-fenced lane must retain original fence/revisions.
235    #[error("restore safety requires original fence binding")]
236    FenceRequired,
237    /// The no-irreversible-effects lane cannot silently add a fence obligation.
238    #[error("unexpected restore safety fence binding")]
239    UnexpectedFence,
240    /// Intent hash admission failed.
241    #[error(transparent)]
242    Checksum(#[from] ChecksumError),
243}