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, FORMAT, File, IcSnapshotArtifactError,
5    IcSnapshotMetadataReply, MAX_IC_SNAPSHOT_METADATA_BYTES, Mode, OFlags, REGIONS,
6    check_closed_tree, check_directory_identity, checksum_relative_files, errno_to_io, hex_bytes,
7    open_directory, unix_fs,
8};
9use crate::{
10    model::{
11        ic_snapshot_data::MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES, operation_plan::OperationPlanRecord,
12    },
13    ops::{
14        artifacts::checksum_reader,
15        persistence::{DownloadIntegrityError, read_operation_plan},
16    },
17    policy::download_integrity::validate,
18};
19use std::{io::Read, os::unix::fs::MetadataExt};
20
21impl DownloadJournalGuard<'_> {
22    /// Explicitly verify one published IC tree against exact original metadata and intent.
23    ///
24    /// Requires the retained full plan, unchanged held journal and complete Durable
25    /// selected set. Only this target's artifact bytes are read. The fixed format,
26    /// original metadata/request hashes, region lengths, known bounded chunk hashes
27    /// and closed direct-child tree must match the retained checksum. No metadata
28    /// defaults, generic-token/raw-ID inference or progress reconstruction occurs.
29    ///
30    /// Reads use no-follow descriptors and a bounded checksum buffer; journal and
31    /// directory identities are rechecked at closing. These sequential observations
32    /// cannot fence noncooperating writers or hold fresh byte custody after return.
33    /// The returned checksum is passive local evidence. Integrations still qualify
34    /// authentic complete transfer, token association, fresh permission/accounting,
35    /// stable byte/command custody and upload/load/start safety. Nothing is written,
36    /// spent, settled or released; ordinary resume never invokes this check.
37    ///
38    /// # Errors
39    /// Rejects wrong original evidence, incomplete selection, changed records/custody,
40    /// missing/unsafe/extra children, wrong bounded lengths/hashes and IO failure.
41    pub fn verify_ic_snapshot_artifact(
42        &self,
43        plan: &OperationPlanRecord,
44        snapshot: &str,
45        metadata: &IcSnapshotMetadataReply<'_>,
46    ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
47        self.admit_ic_artifact_original(plan)?;
48        let entry = self
49            .record
50            .artifact(metadata.request().target(), snapshot)
51            .map_err(super::DownloadJournalError::from)?;
52        if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
53            return Err(IcSnapshotArtifactError::OriginalMismatch);
54        }
55        let expected = entry
56            .checksum()
57            .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
58        self.check_artifact_parent()?;
59        let parent_path = self.layout.root().join("artifacts");
60        let parent = open_directory(&parent_path)?;
61        let path = self.layout.root().join(entry.artifact_path());
62        let directory = File::from(
63            unix_fs::openat(
64                &parent,
65                path.file_name()
66                    .ok_or(IcSnapshotArtifactError::CustodyChanged)?,
67                OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
68                Mode::empty(),
69            )
70            .map_err(errno_to_io)?,
71        );
72        checksum_ic_tree(&directory, metadata)?.verify(expected.hash())?;
73        check_directory_identity(&parent_path, &parent)?;
74        check_directory_identity(&path, &directory)?;
75        self.admit_ic_artifact_original(plan)?;
76        Ok(expected.clone())
77    }
78
79    fn admit_ic_artifact_original(
80        &self,
81        plan: &OperationPlanRecord,
82    ) -> Result<(), DownloadIntegrityError> {
83        self.check_usable()?;
84        read_operation_plan(self.layout, &plan.digest())?;
85        self.require_unchanged_integrity_journal()?;
86        validate(plan, &self.record)?;
87        Ok(())
88    }
89}
90
91fn checksum_ic_tree(
92    directory: &File,
93    metadata: &IcSnapshotMetadataReply<'_>,
94) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
95    let mut checksums = vec![
96        ("format".into(), ArtifactChecksumRecord::from_bytes(FORMAT)),
97        (
98            "metadata.candid".into(),
99            metadata.payload_checksum().clone(),
100        ),
101        (
102            "metadata-arguments.candid".into(),
103            ArtifactChecksumRecord::from_bytes(metadata.request().arguments()),
104        ),
105    ];
106    let values = metadata.metadata();
107    let sizes = [
108        values.wasm_module_size,
109        values.wasm_memory_size,
110        values.stable_memory_size,
111    ];
112    // Empty expected digests here name the closed tree; the actual region hashes
113    // below are admitted by the existing retained whole-tree checksum owner.
114    let empty = ArtifactChecksumRecord::from_bytes(&[]);
115    checksums.extend(REGIONS.map(|name| (name.into(), empty.clone())));
116    for chunk in &values.wasm_chunk_store {
117        checksums.push((
118            format!("chunk-{}.bin", hex_bytes(&chunk.hash)).into(),
119            ArtifactChecksumRecord::from_digest(
120                chunk
121                    .hash
122                    .as_slice()
123                    .try_into()
124                    .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
125            ),
126        ));
127    }
128    check_closed_tree(directory, &checksums)?;
129    for (index, (name, checksum)) in checksums.iter_mut().enumerate() {
130        let (length, maximum) = match index {
131            0 => (Some(FORMAT.len() as u64), FORMAT.len() as u64),
132            1 => (None, MAX_IC_SNAPSHOT_METADATA_BYTES as u64),
133            2 => {
134                let length = metadata.request().arguments().len() as u64;
135                (Some(length), length)
136            }
137            3..=5 => (Some(sizes[index - 3]), sizes[index - 3]),
138            _ => (None, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64),
139        };
140        let actual = checksum_child(
141            directory,
142            name.to_str()
143                .ok_or(IcSnapshotArtifactError::UnexpectedEntry)?,
144            length,
145            maximum,
146        )?;
147        if !(3..=5).contains(&index) {
148            actual.verify(checksum.hash())?;
149        }
150        *checksum = actual;
151    }
152    check_closed_tree(directory, &checksums)?;
153    Ok(checksum_relative_files(checksums))
154}
155
156fn checksum_child(
157    directory: &File,
158    name: &str,
159    length: Option<u64>,
160    maximum: u64,
161) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
162    let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
163    let mut file =
164        File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?);
165    let original = file.metadata()?;
166    if !original.is_file()
167        || original.len() > maximum
168        || length.is_some_and(|length| length != original.len())
169    {
170        return Err(IcSnapshotArtifactError::FileShape);
171    }
172    // Read at most the observed length plus one, detecting shrinking/growing files
173    // without an unbounded read even if a noncooperating writer changes the file.
174    let limit = original
175        .len()
176        .checked_add(1)
177        .ok_or(IcSnapshotArtifactError::FileShape)?;
178    let mut bounded = (&mut file).take(limit);
179    let checksum = checksum_reader(&mut bounded)?;
180    if bounded.limit() != 1 {
181        return Err(IcSnapshotArtifactError::FileShape);
182    }
183    let held = file.metadata()?;
184    let current =
185        File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?)
186            .metadata()?;
187    if !current.is_file()
188        || original.dev() != current.dev()
189        || original.ino() != current.ino()
190        || original.len() != current.len()
191        || original.len() != held.len()
192    {
193        return Err(IcSnapshotArtifactError::CustodyChanged);
194    }
195    Ok(checksum)
196}