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