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