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 let mut checksums = vec![
100 ("format".into(), ArtifactChecksumRecord::from_bytes(FORMAT)),
101 (
102 "metadata.candid".into(),
103 metadata.payload_checksum().clone(),
104 ),
105 (
106 "metadata-arguments.candid".into(),
107 ArtifactChecksumRecord::from_bytes(metadata.request().arguments()),
108 ),
109 ];
110 let values = metadata.metadata();
111 let sizes = [
112 values.wasm_module_size,
113 values.wasm_memory_size,
114 values.stable_memory_size,
115 ];
116 let empty = ArtifactChecksumRecord::from_bytes(&[]);
119 checksums.extend(REGIONS.map(|name| (name.into(), empty.clone())));
120 for chunk in &values.wasm_chunk_store {
121 let digest = ic_host_artifacts::artifact::Sha256Digest::from_bytes(
122 chunk
123 .hash
124 .as_slice()
125 .try_into()
126 .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
127 );
128 checksums.push((
129 member::chunk_name(digest).into(),
130 ArtifactChecksumRecord::from_digest(*digest.as_bytes()),
131 ));
132 }
133 check_closed_tree(directory, &checksums)?;
134 for (index, (name, checksum)) in checksums.iter_mut().enumerate() {
135 let (length, maximum) = match index {
136 0 => (Some(FORMAT.len() as u64), FORMAT.len() as u64),
137 1 => (None, MAX_IC_SNAPSHOT_METADATA_BYTES as u64),
138 2 => {
139 let length = metadata.request().arguments().len() as u64;
140 (Some(length), length)
141 }
142 3..=5 => (Some(sizes[index - 3]), sizes[index - 3]),
143 _ => (None, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64),
144 };
145 let actual = checksum_child(
146 directory,
147 name.to_str()
148 .ok_or(IcSnapshotArtifactError::UnexpectedEntry)?,
149 length,
150 maximum,
151 )?;
152 if !(3..=5).contains(&index) {
153 actual.verify(checksum.hash())?;
154 }
155 *checksum = actual;
156 }
157 check_closed_tree(directory, &checksums)?;
158 checksum_relative_files(checksums)
159 .map_err(ArtifactError::from)
160 .map_err(IcSnapshotArtifactError::from)
161}
162
163fn checksum_child(
164 directory: &File,
165 name: &str,
166 length: Option<u64>,
167 maximum: u64,
168) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
169 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
170 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
171 let checksum = checksum_exact_reader(&mut file, original.len())?;
172 let held = file.metadata()?;
173 let current = File::from(
174 unix_fs::openat(directory, name, flags, Mode::empty()).map_err(io::Error::from)?,
175 )
176 .metadata()?;
177 if !current.is_file()
178 || original.dev() != current.dev()
179 || original.ino() != current.ino()
180 || original.len() != current.len()
181 || original.len() != held.len()
182 {
183 return Err(IcSnapshotArtifactError::CustodyChanged);
184 }
185 Ok(checksum)
186}
187
188fn checksum_exact_reader(
189 reader: impl Read,
190 length: u64,
191) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
192 let identity = hash_exact_reader(reader, length).map_err(|error| match error {
193 InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
194 IcSnapshotArtifactError::FileShape
195 }
196 error => ArtifactError::Io(error.into()).into(),
197 })?;
198 Ok(ArtifactChecksumRecord::from_digest(
199 *identity.sha256.as_bytes(),
200 ))
201}
202
203pub(super) fn open_regular_child(
204 directory: &File,
205 name: &str,
206 length: Option<u64>,
207 maximum: u64,
208) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
209 let file = File::from(
210 unix_fs::openat(
211 directory,
212 name,
213 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
214 Mode::empty(),
215 )
216 .map_err(io::Error::from)?,
217 );
218 let metadata = file.metadata()?;
219 if !metadata.is_file()
220 || metadata.len() > maximum
221 || length.is_some_and(|length| length != metadata.len())
222 {
223 return Err(IcSnapshotArtifactError::FileShape);
224 }
225 Ok((file, metadata))
226}
227
228#[cfg(test)]
229mod tests;