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