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.verify_ic_snapshot_artifact_with(plan, snapshot, metadata, |directory, _| {
44            Ok::<_, IcSnapshotArtifactError>((checksum_ic_tree(directory, metadata)?, ()))
45        })
46        .map(|(checksum, ())| checksum)
47    }
48
49    // Admit originals once, then let the owning reader hash the exact held files.
50    // Upload planning derives payload bindings from those same bytes; both callers
51    // retain the original record and namespace checks around their reads.
52    pub(super) fn verify_ic_snapshot_artifact_with<T, E: From<IcSnapshotArtifactError>>(
53        &self,
54        plan: &OperationPlanRecord,
55        snapshot: &str,
56        metadata: &IcSnapshotMetadataReply<'_>,
57        read: impl FnOnce(&File, &ArtifactChecksumRecord) -> Result<(ArtifactChecksumRecord, T), E>,
58    ) -> Result<(ArtifactChecksumRecord, T), E> {
59        self.admit_ic_artifact_original(plan)
60            .map_err(IcSnapshotArtifactError::from)?;
61        let entry = self
62            .record
63            .artifact(metadata.request().target(), snapshot)
64            .map_err(super::DownloadJournalError::from)
65            .map_err(IcSnapshotArtifactError::from)?;
66        if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
67            return Err(IcSnapshotArtifactError::OriginalMismatch.into());
68        }
69        let expected = entry
70            .checksum()
71            .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
72        self.check_artifact_parent()
73            .map_err(IcSnapshotArtifactError::from)?;
74        let result = with_artifact_directory::<_, E>(
75            self.layout.root(),
76            entry.artifact_path(),
77            |directory| {
78                let (checksum, result) = read(directory, expected)?;
79                checksum
80                    .verify(expected.hash())
81                    .map_err(IcSnapshotArtifactError::from)?;
82                Ok((checksum, result))
83            },
84            || {
85                self.admit_ic_artifact_original(plan)
86                    .map_err(IcSnapshotArtifactError::from)
87                    .map_err(E::from)
88            },
89        )?;
90        Ok(result)
91    }
92
93    pub(super) fn admit_ic_artifact_original(
94        &self,
95        plan: &OperationPlanRecord,
96    ) -> Result<(), DownloadIntegrityError> {
97        self.admit_integrity_original(plan)?;
98        validate(plan, &self.record)?;
99        Ok(())
100    }
101}
102
103fn checksum_ic_tree(
104    directory: &File,
105    metadata: &IcSnapshotMetadataReply<'_>,
106) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
107    checksum_ic_tree_with(directory, metadata, checksum_child)
108}
109
110pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
111    directory: &File,
112    metadata: &IcSnapshotMetadataReply<'_>,
113    mut read_child: impl FnMut(&File, &member::ExpectedMember) -> Result<ArtifactChecksumRecord, E>,
114) -> Result<ArtifactChecksumRecord, E> {
115    let members = member::expected(metadata)?;
116    check_closed_tree(
117        directory,
118        members
119            .iter()
120            .map(|member| std::ffi::OsStr::new(&member.name)),
121    )?;
122    let mut checksums = Vec::with_capacity(members.len());
123    for member in members {
124        let actual = read_child(directory, &member)?;
125        if let Some(expected) = member.checksum() {
126            actual
127                .verify(expected.hash())
128                .map_err(IcSnapshotArtifactError::from)?;
129        }
130        checksums.push((member.name.into(), actual));
131    }
132    check_closed_tree(
133        directory,
134        checksums
135            .iter()
136            .map(|(path, _): &(std::path::PathBuf, _)| path.as_os_str()),
137    )?;
138    checksum_relative_files(checksums)
139        .map_err(ArtifactError::from)
140        .map_err(IcSnapshotArtifactError::from)
141        .map_err(E::from)
142}
143
144fn checksum_child(
145    directory: &File,
146    member: &member::ExpectedMember,
147) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
148    checksum_child_with(directory, member, |file, length| {
149        checksum_exact_reader(file, length)
150    })
151}
152
153pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
154    directory: &File,
155    member: &member::ExpectedMember,
156    read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
157) -> Result<ArtifactChecksumRecord, E> {
158    let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
159    let (mut file, original) = member.open(directory)?;
160    let checksum = read(&mut file, original.len())?;
161    let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
162    let current = File::from(
163        unix_fs::openat(directory, member.name.as_str(), flags, Mode::empty())
164            .map_err(io::Error::from)
165            .map_err(IcSnapshotArtifactError::from)?,
166    )
167    .metadata()
168    .map_err(IcSnapshotArtifactError::from)?;
169    if !current.is_file()
170        || FileIdentity::from_metadata(&original) != FileIdentity::from_metadata(&current)
171        || original.len() != current.len()
172        || original.len() != held.len()
173    {
174        return Err(IcSnapshotArtifactError::CustodyChanged.into());
175    }
176    Ok(checksum)
177}
178
179pub(super) fn checksum_exact_reader<R: Read>(
180    reader: R,
181    length: u64,
182) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
183    #[cfg(test)]
184    let reader = super::tests::read_count::Reader(reader);
185    let identity = hash_exact_reader(reader, length).map_err(|error| match error {
186        InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
187            IcSnapshotArtifactError::FileShape
188        }
189        error => ArtifactError::Io(error.into()).into(),
190    })?;
191    Ok(ArtifactChecksumRecord::from_digest(
192        *identity.sha256.as_bytes(),
193    ))
194}
195
196pub(super) fn open_regular_child(
197    directory: &File,
198    name: &str,
199    length: Option<u64>,
200    maximum: u64,
201) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
202    let file = File::from(
203        unix_fs::openat(
204            directory,
205            name,
206            OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
207            Mode::empty(),
208        )
209        .map_err(io::Error::from)?,
210    );
211    let metadata = file.metadata()?;
212    if !metadata.is_file()
213        || metadata.len() > maximum
214        || length.is_some_and(|length| length != metadata.len())
215    {
216        return Err(IcSnapshotArtifactError::FileShape);
217    }
218    Ok((file, metadata))
219}
220
221#[cfg(test)]
222mod tests;