ic_backup/ops/persistence/download_journal/ic_snapshot_artifact/verification/
mod.rs1use 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, member,
7 open_directory_no_follow, 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::ArtifactError,
15 persistence::{DownloadIntegrityError, read_operation_plan},
16 },
17 policy::download_integrity::validate,
18};
19use ic_host_artifacts::artifact::{ArtifactError as InputError, hash_exact_reader};
20use std::{
21 io::{self, Read},
22 os::unix::fs::MetadataExt,
23};
24
25impl DownloadJournalGuard<'_> {
26 pub fn verify_ic_snapshot_artifact(
46 &self,
47 plan: &OperationPlanRecord,
48 snapshot: &str,
49 metadata: &IcSnapshotMetadataReply<'_>,
50 ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
51 self.admit_ic_artifact_original(plan)?;
52 let entry = self
53 .record
54 .artifact(metadata.request().target(), snapshot)
55 .map_err(super::DownloadJournalError::from)?;
56 if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
57 return Err(IcSnapshotArtifactError::OriginalMismatch);
58 }
59 let expected = entry
60 .checksum()
61 .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
62 self.check_artifact_parent()?;
63 let parent_path = self.layout.root().join("artifacts");
64 let parent = open_directory_no_follow(&parent_path)?;
65 let path = self.layout.root().join(entry.artifact_path());
66 let directory = File::from(
67 unix_fs::openat(
68 &parent,
69 path.file_name()
70 .ok_or(IcSnapshotArtifactError::CustodyChanged)?,
71 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
72 Mode::empty(),
73 )
74 .map_err(io::Error::from)?,
75 );
76 checksum_ic_tree(&directory, metadata)?.verify(expected.hash())?;
77 check_directory_identity(&parent_path, &parent)?;
78 check_directory_identity(&path, &directory)?;
79 self.admit_ic_artifact_original(plan)?;
80 Ok(expected.clone())
81 }
82
83 fn admit_ic_artifact_original(
84 &self,
85 plan: &OperationPlanRecord,
86 ) -> Result<(), DownloadIntegrityError> {
87 self.check_usable()?;
88 read_operation_plan(self.layout, &plan.digest())?;
89 self.require_unchanged_integrity_journal()?;
90 validate(plan, &self.record)?;
91 Ok(())
92 }
93}
94
95fn checksum_ic_tree(
96 directory: &File,
97 metadata: &IcSnapshotMetadataReply<'_>,
98) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
99 checksum_ic_tree_with(directory, metadata, checksum_child)
100}
101
102pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
103 directory: &File,
104 metadata: &IcSnapshotMetadataReply<'_>,
105 mut read_child: impl FnMut(&File, &str, Option<u64>, u64) -> Result<ArtifactChecksumRecord, E>,
106) -> Result<ArtifactChecksumRecord, E> {
107 let mut checksums = vec![
108 ("format".into(), ArtifactChecksumRecord::from_bytes(FORMAT)),
109 (
110 "metadata.candid".into(),
111 metadata.payload_checksum().clone(),
112 ),
113 (
114 "metadata-arguments.candid".into(),
115 ArtifactChecksumRecord::from_bytes(metadata.request().arguments()),
116 ),
117 ];
118 let values = metadata.metadata();
119 let sizes = [
120 values.wasm_module_size,
121 values.wasm_memory_size,
122 values.stable_memory_size,
123 ];
124 let empty = ArtifactChecksumRecord::from_bytes(&[]);
127 checksums.extend(REGIONS.map(|name| (name.into(), empty.clone())));
128 for chunk in &values.wasm_chunk_store {
129 let digest = ic_host_artifacts::artifact::Sha256Digest::from_bytes(
130 chunk
131 .hash
132 .as_slice()
133 .try_into()
134 .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
135 );
136 checksums.push((
137 member::chunk_name(digest).into(),
138 ArtifactChecksumRecord::from_digest(*digest.as_bytes()),
139 ));
140 }
141 check_closed_tree(directory, &checksums)?;
142 for (index, (name, checksum)) in checksums.iter_mut().enumerate() {
143 let (length, maximum) = match index {
144 0 => (Some(FORMAT.len() as u64), FORMAT.len() as u64),
145 1 => (None, MAX_IC_SNAPSHOT_METADATA_BYTES as u64),
146 2 => {
147 let length = metadata.request().arguments().len() as u64;
148 (Some(length), length)
149 }
150 3..=5 => (Some(sizes[index - 3]), sizes[index - 3]),
151 _ => (None, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64),
152 };
153 let actual = read_child(
154 directory,
155 name.to_str()
156 .ok_or(IcSnapshotArtifactError::UnexpectedEntry)?,
157 length,
158 maximum,
159 )?;
160 if !(3..=5).contains(&index) {
161 actual
162 .verify(checksum.hash())
163 .map_err(IcSnapshotArtifactError::from)?;
164 }
165 *checksum = actual;
166 }
167 check_closed_tree(directory, &checksums)?;
168 checksum_relative_files(checksums)
169 .map_err(ArtifactError::from)
170 .map_err(IcSnapshotArtifactError::from)
171 .map_err(E::from)
172}
173
174fn checksum_child(
175 directory: &File,
176 name: &str,
177 length: Option<u64>,
178 maximum: u64,
179) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
180 checksum_child_with(directory, name, length, maximum, |file, length| {
181 checksum_exact_reader(file, length)
182 })
183}
184
185pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
186 directory: &File,
187 name: &str,
188 length: Option<u64>,
189 maximum: u64,
190 read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
191) -> Result<ArtifactChecksumRecord, E> {
192 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
193 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
194 let checksum = read(&mut file, original.len())?;
195 let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
196 let current = File::from(
197 unix_fs::openat(directory, name, flags, Mode::empty())
198 .map_err(io::Error::from)
199 .map_err(IcSnapshotArtifactError::from)?,
200 )
201 .metadata()
202 .map_err(IcSnapshotArtifactError::from)?;
203 if !current.is_file()
204 || original.dev() != current.dev()
205 || original.ino() != current.ino()
206 || original.len() != current.len()
207 || original.len() != held.len()
208 {
209 return Err(IcSnapshotArtifactError::CustodyChanged.into());
210 }
211 Ok(checksum)
212}
213
214pub(super) fn checksum_exact_reader<R: Read>(
215 reader: R,
216 length: u64,
217) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
218 let identity = hash_exact_reader(reader, length).map_err(|error| match error {
219 InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
220 IcSnapshotArtifactError::FileShape
221 }
222 error => ArtifactError::Io(error.into()).into(),
223 })?;
224 Ok(ArtifactChecksumRecord::from_digest(
225 *identity.sha256.as_bytes(),
226 ))
227}
228
229pub(super) fn open_regular_child(
230 directory: &File,
231 name: &str,
232 length: Option<u64>,
233 maximum: u64,
234) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
235 let file = File::from(
236 unix_fs::openat(
237 directory,
238 name,
239 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
240 Mode::empty(),
241 )
242 .map_err(io::Error::from)?,
243 );
244 let metadata = file.metadata()?;
245 if !metadata.is_file()
246 || metadata.len() > maximum
247 || length.is_some_and(|length| length != metadata.len())
248 {
249 return Err(IcSnapshotArtifactError::FileShape);
250 }
251 Ok((file, metadata))
252}
253
254#[cfg(test)]
255mod tests;