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, errno_to_io, 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::checksum_reader,
16 persistence::{DownloadIntegrityError, read_operation_plan},
17 },
18 policy::download_integrity::validate,
19};
20use std::time::Instant;
21use std::{io::Read, os::unix::fs::MetadataExt};
22
23impl DownloadJournalGuard<'_> {
24 pub fn verify_ic_snapshot_artifact(
44 &self,
45 plan: &OperationPlanRecord,
46 snapshot: &str,
47 metadata: &IcSnapshotMetadataReply<'_>,
48 ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
49 let started = Instant::now();
50 let result = (|| {
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(&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(errno_to_io)?,
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 self.record_ic_snapshot_metrics(
83 LocalOperation::Verification,
84 started,
85 result.is_ok(),
86 None,
87 );
88 result
89 }
90
91 fn admit_ic_artifact_original(
92 &self,
93 plan: &OperationPlanRecord,
94 ) -> Result<(), DownloadIntegrityError> {
95 self.check_usable()?;
96 read_operation_plan(self.layout, &plan.digest())?;
97 self.require_unchanged_integrity_journal()?;
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 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 checksums.push((
130 format!("chunk-{}.bin", hex_bytes(&chunk.hash)).into(),
131 ArtifactChecksumRecord::from_digest(
132 chunk
133 .hash
134 .as_slice()
135 .try_into()
136 .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
137 ),
138 ));
139 }
140 check_closed_tree(directory, &checksums)?;
141 for (index, (name, checksum)) in checksums.iter_mut().enumerate() {
142 let (length, maximum) = match index {
143 0 => (Some(FORMAT.len() as u64), FORMAT.len() as u64),
144 1 => (None, MAX_IC_SNAPSHOT_METADATA_BYTES as u64),
145 2 => {
146 let length = metadata.request().arguments().len() as u64;
147 (Some(length), length)
148 }
149 3..=5 => (Some(sizes[index - 3]), sizes[index - 3]),
150 _ => (None, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64),
151 };
152 let actual = checksum_child(
153 directory,
154 name.to_str()
155 .ok_or(IcSnapshotArtifactError::UnexpectedEntry)?,
156 length,
157 maximum,
158 )?;
159 if !(3..=5).contains(&index) {
160 actual.verify(checksum.hash())?;
161 }
162 *checksum = actual;
163 }
164 check_closed_tree(directory, &checksums)?;
165 Ok(checksum_relative_files(checksums))
166}
167
168fn checksum_child(
169 directory: &File,
170 name: &str,
171 length: Option<u64>,
172 maximum: u64,
173) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
174 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
175 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
176 let limit = original
179 .len()
180 .checked_add(1)
181 .ok_or(IcSnapshotArtifactError::FileShape)?;
182 let mut bounded = (&mut file).take(limit);
183 let checksum = checksum_reader(&mut bounded)?;
184 if bounded.limit() != 1 {
185 return Err(IcSnapshotArtifactError::FileShape);
186 }
187 let held = file.metadata()?;
188 let current =
189 File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?)
190 .metadata()?;
191 if !current.is_file()
192 || original.dev() != current.dev()
193 || original.ino() != current.ino()
194 || original.len() != current.len()
195 || original.len() != held.len()
196 {
197 return Err(IcSnapshotArtifactError::CustodyChanged);
198 }
199 Ok(checksum)
200}
201
202pub(super) fn open_regular_child(
203 directory: &File,
204 name: &str,
205 length: Option<u64>,
206 maximum: u64,
207) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
208 let file = File::from(
209 unix_fs::openat(
210 directory,
211 name,
212 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
213 Mode::empty(),
214 )
215 .map_err(errno_to_io)?,
216 );
217 let metadata = file.metadata()?;
218 if !metadata.is_file()
219 || metadata.len() > maximum
220 || length.is_some_and(|length| length != metadata.len())
221 {
222 return Err(IcSnapshotArtifactError::FileShape);
223 }
224 Ok((file, metadata))
225}