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::{
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        with_artifact_directory(
61            self.layout.root(),
62            entry.artifact_path(),
63            |directory| {
64                checksum_ic_tree(directory, metadata)?.verify(expected.hash())?;
65                Ok::<_, IcSnapshotArtifactError>(())
66            },
67            || Ok(self.admit_ic_artifact_original(plan)?),
68        )?;
69        Ok(expected.clone())
70    }
71
72    pub(super) fn admit_ic_artifact_original(
73        &self,
74        plan: &OperationPlanRecord,
75    ) -> Result<(), DownloadIntegrityError> {
76        self.check_usable()?;
77        read_operation_plan(self.layout, &plan.digest())?;
78        self.require_unchanged_integrity_journal()?;
79        validate(plan, &self.record)?;
80        Ok(())
81    }
82}
83
84fn checksum_ic_tree(
85    directory: &File,
86    metadata: &IcSnapshotMetadataReply<'_>,
87) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
88    checksum_ic_tree_with(directory, metadata, checksum_child)
89}
90
91pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
92    directory: &File,
93    metadata: &IcSnapshotMetadataReply<'_>,
94    mut read_child: impl FnMut(&File, &member::ExpectedMember) -> Result<ArtifactChecksumRecord, E>,
95) -> Result<ArtifactChecksumRecord, E> {
96    let members = member::expected(metadata)?;
97    check_closed_tree(
98        directory,
99        members
100            .iter()
101            .map(|member| std::ffi::OsStr::new(&member.name)),
102    )?;
103    let mut checksums = Vec::with_capacity(members.len());
104    for member in members {
105        let actual = read_child(directory, &member)?;
106        if let Some(expected) = member.checksum() {
107            actual
108                .verify(expected.hash())
109                .map_err(IcSnapshotArtifactError::from)?;
110        }
111        checksums.push((member.name.into(), actual));
112    }
113    check_closed_tree(
114        directory,
115        checksums
116            .iter()
117            .map(|(path, _): &(std::path::PathBuf, _)| path.as_os_str()),
118    )?;
119    checksum_relative_files(checksums)
120        .map_err(ArtifactError::from)
121        .map_err(IcSnapshotArtifactError::from)
122        .map_err(E::from)
123}
124
125fn checksum_child(
126    directory: &File,
127    member: &member::ExpectedMember,
128) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
129    checksum_child_with(directory, member, |file, length| {
130        checksum_exact_reader(file, length)
131    })
132}
133
134pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
135    directory: &File,
136    member: &member::ExpectedMember,
137    read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
138) -> Result<ArtifactChecksumRecord, E> {
139    let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
140    let (mut file, original) = member.open(directory)?;
141    let checksum = read(&mut file, original.len())?;
142    let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
143    let current = File::from(
144        unix_fs::openat(directory, member.name.as_str(), flags, Mode::empty())
145            .map_err(io::Error::from)
146            .map_err(IcSnapshotArtifactError::from)?,
147    )
148    .metadata()
149    .map_err(IcSnapshotArtifactError::from)?;
150    if !current.is_file()
151        || original.dev() != current.dev()
152        || original.ino() != current.ino()
153        || original.len() != current.len()
154        || original.len() != held.len()
155    {
156        return Err(IcSnapshotArtifactError::CustodyChanged.into());
157    }
158    Ok(checksum)
159}
160
161pub(super) fn checksum_exact_reader<R: Read>(
162    reader: R,
163    length: u64,
164) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
165    #[cfg(test)]
166    let reader = super::read_count::Reader(reader);
167    let identity = hash_exact_reader(reader, length).map_err(|error| match error {
168        InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
169            IcSnapshotArtifactError::FileShape
170        }
171        error => ArtifactError::Io(error.into()).into(),
172    })?;
173    Ok(ArtifactChecksumRecord::from_digest(
174        *identity.sha256.as_bytes(),
175    ))
176}
177
178pub(super) fn open_regular_child(
179    directory: &File,
180    name: &str,
181    length: Option<u64>,
182    maximum: u64,
183) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
184    let file = File::from(
185        unix_fs::openat(
186            directory,
187            name,
188            OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
189            Mode::empty(),
190        )
191        .map_err(io::Error::from)?,
192    );
193    let metadata = file.metadata()?;
194    if !metadata.is_file()
195        || metadata.len() > maximum
196        || length.is_some_and(|length| length != metadata.len())
197    {
198        return Err(IcSnapshotArtifactError::FileShape);
199    }
200    Ok((file, metadata))
201}
202
203#[cfg(test)]
204mod tests;