Skip to main content

ic_backup/ops/persistence/download_journal/ic_snapshot_artifact/verification/
mod.rs

1//! Explicit fresh local checks of retained opt-in IC trees; no upload permission.
2
3use super::{
4    ArtifactChecksumRecord, DownloadJournalGuard, File, IcSnapshotArtifactError,
5    IcSnapshotMetadataReply, Mode, OFlags, check_closed_tree, checksum_relative_files, member,
6    unix_fs, with_artifact_directory,
7};
8use crate::{
9    model::operation_plan::OperationPlanRecord,
10    ops::{artifacts::ArtifactError, persistence::DownloadIntegrityError},
11    policy::download_integrity::validate,
12};
13use ic_host_artifacts::artifact::{ArtifactError as InputError, hash_exact_reader};
14use ic_host_fs::identity::FileIdentity;
15use std::io::{self, Read};
16
17impl DownloadJournalGuard<'_> {
18    /// Explicitly verify one published IC tree against exact original metadata and intent.
19    ///
20    /// Requires the retained full plan, unchanged held journal and complete Durable
21    /// selected set. Only this target's artifact bytes are read. The fixed format,
22    /// original metadata/request hashes, region lengths, known bounded chunk hashes
23    /// and closed direct-child tree must match the retained checksum. No metadata
24    /// defaults, generic-token/raw-ID inference or progress reconstruction occurs.
25    ///
26    /// Reads use no-follow descriptors and a bounded checksum buffer; journal and
27    /// directory identities are rechecked at closing. These sequential observations
28    /// cannot fence noncooperating writers or hold fresh byte custody after return.
29    /// The returned checksum is passive local evidence. Integrations still qualify
30    /// authentic complete transfer, token association, fresh permission/accounting,
31    /// stable byte/command custody and upload/load/start safety. Nothing is written,
32    /// spent, settled or released; ordinary resume never invokes this check.
33    ///
34    /// # Errors
35    /// Rejects wrong original evidence, incomplete selection, changed records/custody,
36    /// missing/unsafe/extra children, wrong bounded lengths/hashes and IO failure.
37    pub fn verify_ic_snapshot_artifact(
38        &self,
39        plan: &OperationPlanRecord,
40        snapshot: &str,
41        metadata: &IcSnapshotMetadataReply<'_>,
42    ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
43        self.admit_ic_artifact_original(plan)?;
44        let entry = self
45            .record
46            .artifact(metadata.request().target(), snapshot)
47            .map_err(super::DownloadJournalError::from)?;
48        if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
49            return Err(IcSnapshotArtifactError::OriginalMismatch);
50        }
51        let expected = entry
52            .checksum()
53            .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
54        self.check_artifact_parent()?;
55        with_artifact_directory(
56            self.layout.root(),
57            entry.artifact_path(),
58            |directory| {
59                checksum_ic_tree(directory, metadata)?.verify(expected.hash())?;
60                Ok::<_, IcSnapshotArtifactError>(())
61            },
62            || Ok(self.admit_ic_artifact_original(plan)?),
63        )?;
64        Ok(expected.clone())
65    }
66
67    pub(super) fn admit_ic_artifact_original(
68        &self,
69        plan: &OperationPlanRecord,
70    ) -> Result<(), DownloadIntegrityError> {
71        self.admit_integrity_original(plan)?;
72        validate(plan, &self.record)?;
73        Ok(())
74    }
75}
76
77fn checksum_ic_tree(
78    directory: &File,
79    metadata: &IcSnapshotMetadataReply<'_>,
80) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
81    checksum_ic_tree_with(directory, metadata, checksum_child)
82}
83
84pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
85    directory: &File,
86    metadata: &IcSnapshotMetadataReply<'_>,
87    mut read_child: impl FnMut(&File, &member::ExpectedMember) -> Result<ArtifactChecksumRecord, E>,
88) -> Result<ArtifactChecksumRecord, E> {
89    let members = member::expected(metadata)?;
90    check_closed_tree(
91        directory,
92        members
93            .iter()
94            .map(|member| std::ffi::OsStr::new(&member.name)),
95    )?;
96    let mut checksums = Vec::with_capacity(members.len());
97    for member in members {
98        let actual = read_child(directory, &member)?;
99        if let Some(expected) = member.checksum() {
100            actual
101                .verify(expected.hash())
102                .map_err(IcSnapshotArtifactError::from)?;
103        }
104        checksums.push((member.name.into(), actual));
105    }
106    check_closed_tree(
107        directory,
108        checksums
109            .iter()
110            .map(|(path, _): &(std::path::PathBuf, _)| path.as_os_str()),
111    )?;
112    checksum_relative_files(checksums)
113        .map_err(ArtifactError::from)
114        .map_err(IcSnapshotArtifactError::from)
115        .map_err(E::from)
116}
117
118fn checksum_child(
119    directory: &File,
120    member: &member::ExpectedMember,
121) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
122    checksum_child_with(directory, member, |file, length| {
123        checksum_exact_reader(file, length)
124    })
125}
126
127pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
128    directory: &File,
129    member: &member::ExpectedMember,
130    read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
131) -> Result<ArtifactChecksumRecord, E> {
132    let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
133    let (mut file, original) = member.open(directory)?;
134    let checksum = read(&mut file, original.len())?;
135    let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
136    let current = File::from(
137        unix_fs::openat(directory, member.name.as_str(), flags, Mode::empty())
138            .map_err(io::Error::from)
139            .map_err(IcSnapshotArtifactError::from)?,
140    )
141    .metadata()
142    .map_err(IcSnapshotArtifactError::from)?;
143    if !current.is_file()
144        || FileIdentity::from_metadata(&original) != FileIdentity::from_metadata(&current)
145        || original.len() != current.len()
146        || original.len() != held.len()
147    {
148        return Err(IcSnapshotArtifactError::CustodyChanged.into());
149    }
150    Ok(checksum)
151}
152
153pub(super) fn checksum_exact_reader<R: Read>(
154    reader: R,
155    length: u64,
156) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
157    #[cfg(test)]
158    let reader = super::tests::read_count::Reader(reader);
159    let identity = hash_exact_reader(reader, length).map_err(|error| match error {
160        InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
161            IcSnapshotArtifactError::FileShape
162        }
163        error => ArtifactError::Io(error.into()).into(),
164    })?;
165    Ok(ArtifactChecksumRecord::from_digest(
166        *identity.sha256.as_bytes(),
167    ))
168}
169
170pub(super) fn open_regular_child(
171    directory: &File,
172    name: &str,
173    length: Option<u64>,
174    maximum: u64,
175) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
176    let file = File::from(
177        unix_fs::openat(
178            directory,
179            name,
180            OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
181            Mode::empty(),
182        )
183        .map_err(io::Error::from)?,
184    );
185    let metadata = file.metadata()?;
186    if !metadata.is_file()
187        || metadata.len() > maximum
188        || length.is_some_and(|length| length != metadata.len())
189    {
190        return Err(IcSnapshotArtifactError::FileShape);
191    }
192    Ok((file, metadata))
193}
194
195#[cfg(test)]
196mod tests;