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::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::{io, io::Read, os::unix::fs::MetadataExt};
23
24impl DownloadJournalGuard<'_> {
25 pub fn verify_ic_snapshot_artifact(
45 &self,
46 plan: &OperationPlanRecord,
47 snapshot: &str,
48 metadata: &IcSnapshotMetadataReply<'_>,
49 ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
50 let started = Instant::now();
51 let result = (|| {
52 self.admit_ic_artifact_original(plan)?;
53 let entry = self
54 .record
55 .artifact(metadata.request().target(), snapshot)
56 .map_err(super::DownloadJournalError::from)?;
57 if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
58 return Err(IcSnapshotArtifactError::OriginalMismatch);
59 }
60 let expected = entry
61 .checksum()
62 .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
63 self.check_artifact_parent()?;
64 let parent_path = self.layout.root().join("artifacts");
65 let parent = open_directory(&parent_path)?;
66 let path = self.layout.root().join(entry.artifact_path());
67 let directory = File::from(
68 unix_fs::openat(
69 &parent,
70 path.file_name()
71 .ok_or(IcSnapshotArtifactError::CustodyChanged)?,
72 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
73 Mode::empty(),
74 )
75 .map_err(errno_to_io)?,
76 );
77 checksum_ic_tree(&directory, metadata)?.verify(expected.hash())?;
78 check_directory_identity(&parent_path, &parent)?;
79 check_directory_identity(&path, &directory)?;
80 self.admit_ic_artifact_original(plan)?;
81 Ok(expected.clone())
82 })();
83 self.record_ic_snapshot_metrics(
84 LocalOperation::Verification,
85 started,
86 result.is_ok(),
87 None,
88 );
89 result
90 }
91
92 fn admit_ic_artifact_original(
93 &self,
94 plan: &OperationPlanRecord,
95 ) -> Result<(), DownloadIntegrityError> {
96 self.check_usable()?;
97 read_operation_plan(self.layout, &plan.digest())?;
98 self.require_unchanged_integrity_journal()?;
99 validate(plan, &self.record)?;
100 Ok(())
101 }
102}
103
104fn checksum_ic_tree(
105 directory: &File,
106 metadata: &IcSnapshotMetadataReply<'_>,
107) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
108 let mut checksums = vec![
109 ("format".into(), ArtifactChecksumRecord::from_bytes(FORMAT)),
110 (
111 "metadata.candid".into(),
112 metadata.payload_checksum().clone(),
113 ),
114 (
115 "metadata-arguments.candid".into(),
116 ArtifactChecksumRecord::from_bytes(metadata.request().arguments()),
117 ),
118 ];
119 let values = metadata.metadata();
120 let sizes = [
121 values.wasm_module_size,
122 values.wasm_memory_size,
123 values.stable_memory_size,
124 ];
125 let empty = ArtifactChecksumRecord::from_bytes(&[]);
128 checksums.extend(REGIONS.map(|name| (name.into(), empty.clone())));
129 for chunk in &values.wasm_chunk_store {
130 checksums.push((
131 format!("chunk-{}.bin", hex_bytes(&chunk.hash)).into(),
132 ArtifactChecksumRecord::from_digest(
133 chunk
134 .hash
135 .as_slice()
136 .try_into()
137 .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
138 ),
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 = checksum_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.verify(checksum.hash())?;
162 }
163 *checksum = actual;
164 }
165 check_closed_tree(directory, &checksums)?;
166 Ok(checksum_relative_files(checksums))
167}
168
169fn checksum_child(
170 directory: &File,
171 name: &str,
172 length: Option<u64>,
173 maximum: u64,
174) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
175 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
176 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
177 let checksum = checksum_exact_reader(&mut file, original.len())?;
178 let held = file.metadata()?;
179 let current =
180 File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?)
181 .metadata()?;
182 if !current.is_file()
183 || original.dev() != current.dev()
184 || original.ino() != current.ino()
185 || original.len() != current.len()
186 || original.len() != held.len()
187 {
188 return Err(IcSnapshotArtifactError::CustodyChanged);
189 }
190 Ok(checksum)
191}
192
193fn checksum_exact_reader(
194 reader: impl Read,
195 length: u64,
196) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
197 let identity = hash_reader(reader, length).map_err(|error| match error {
200 InputError::LimitExceeded { .. } => IcSnapshotArtifactError::FileShape,
201 InputError::Io(error) => ArtifactError::Io(error).into(),
202 error => ArtifactError::Io(io::Error::other(error)).into(),
203 })?;
204 if identity.bytes != length {
205 return Err(IcSnapshotArtifactError::FileShape);
206 }
207 Ok(ArtifactChecksumRecord::from_digest(
208 *identity.sha256.as_bytes(),
209 ))
210}
211
212pub(super) fn open_regular_child(
213 directory: &File,
214 name: &str,
215 length: Option<u64>,
216 maximum: u64,
217) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
218 let file = File::from(
219 unix_fs::openat(
220 directory,
221 name,
222 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
223 Mode::empty(),
224 )
225 .map_err(errno_to_io)?,
226 );
227 let metadata = file.metadata()?;
228 if !metadata.is_file()
229 || metadata.len() > maximum
230 || length.is_some_and(|length| length != metadata.len())
231 {
232 return Err(IcSnapshotArtifactError::FileShape);
233 }
234 Ok((file, metadata))
235}
236
237#[cfg(test)]
238mod tests;