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(
130        directory,
131        &member.name,
132        member.length,
133        member.maximum,
134        |file, length| checksum_exact_reader(file, length),
135    )
136}
137
138pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
139    directory: &File,
140    name: &str,
141    length: Option<u64>,
142    maximum: u64,
143    read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
144) -> Result<ArtifactChecksumRecord, E> {
145    let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
146    let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
147    let checksum = read(&mut file, original.len())?;
148    let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
149    let current = File::from(
150        unix_fs::openat(directory, name, flags, Mode::empty())
151            .map_err(io::Error::from)
152            .map_err(IcSnapshotArtifactError::from)?,
153    )
154    .metadata()
155    .map_err(IcSnapshotArtifactError::from)?;
156    if !current.is_file()
157        || original.dev() != current.dev()
158        || original.ino() != current.ino()
159        || original.len() != current.len()
160        || original.len() != held.len()
161    {
162        return Err(IcSnapshotArtifactError::CustodyChanged.into());
163    }
164    Ok(checksum)
165}
166
167pub(super) fn checksum_exact_reader<R: Read>(
168    reader: R,
169    length: u64,
170) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
171    #[cfg(test)]
172    let reader = super::read_count::Reader(reader);
173    let identity = hash_exact_reader(reader, length).map_err(|error| match error {
174        InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
175            IcSnapshotArtifactError::FileShape
176        }
177        error => ArtifactError::Io(error.into()).into(),
178    })?;
179    Ok(ArtifactChecksumRecord::from_digest(
180        *identity.sha256.as_bytes(),
181    ))
182}
183
184pub(super) fn open_regular_child(
185    directory: &File,
186    name: &str,
187    length: Option<u64>,
188    maximum: u64,
189) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
190    let file = File::from(
191        unix_fs::openat(
192            directory,
193            name,
194            OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
195            Mode::empty(),
196        )
197        .map_err(io::Error::from)?,
198    );
199    let metadata = file.metadata()?;
200    if !metadata.is_file()
201        || metadata.len() > maximum
202        || length.is_some_and(|length| length != metadata.len())
203    {
204        return Err(IcSnapshotArtifactError::FileShape);
205    }
206    Ok((file, metadata))
207}
208
209#[cfg(test)]
210mod tests;