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, errno_to_io, hex_bytes,
7 open_directory, 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::checksum_reader,
15 persistence::{DownloadIntegrityError, read_operation_plan},
16 },
17 policy::download_integrity::validate,
18};
19use std::{io::Read, os::unix::fs::MetadataExt};
20
21impl DownloadJournalGuard<'_> {
22 pub fn verify_ic_snapshot_artifact(
42 &self,
43 plan: &OperationPlanRecord,
44 snapshot: &str,
45 metadata: &IcSnapshotMetadataReply<'_>,
46 ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
47 self.admit_ic_artifact_original(plan)?;
48 let entry = self
49 .record
50 .artifact(metadata.request().target(), snapshot)
51 .map_err(super::DownloadJournalError::from)?;
52 if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
53 return Err(IcSnapshotArtifactError::OriginalMismatch);
54 }
55 let expected = entry
56 .checksum()
57 .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
58 self.check_artifact_parent()?;
59 let parent_path = self.layout.root().join("artifacts");
60 let parent = open_directory(&parent_path)?;
61 let path = self.layout.root().join(entry.artifact_path());
62 let directory = File::from(
63 unix_fs::openat(
64 &parent,
65 path.file_name()
66 .ok_or(IcSnapshotArtifactError::CustodyChanged)?,
67 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
68 Mode::empty(),
69 )
70 .map_err(errno_to_io)?,
71 );
72 checksum_ic_tree(&directory, metadata)?.verify(expected.hash())?;
73 check_directory_identity(&parent_path, &parent)?;
74 check_directory_identity(&path, &directory)?;
75 self.admit_ic_artifact_original(plan)?;
76 Ok(expected.clone())
77 }
78
79 fn admit_ic_artifact_original(
80 &self,
81 plan: &OperationPlanRecord,
82 ) -> Result<(), DownloadIntegrityError> {
83 self.check_usable()?;
84 read_operation_plan(self.layout, &plan.digest())?;
85 self.require_unchanged_integrity_journal()?;
86 validate(plan, &self.record)?;
87 Ok(())
88 }
89}
90
91fn checksum_ic_tree(
92 directory: &File,
93 metadata: &IcSnapshotMetadataReply<'_>,
94) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
95 let mut checksums = vec![
96 ("format".into(), ArtifactChecksumRecord::from_bytes(FORMAT)),
97 (
98 "metadata.candid".into(),
99 metadata.payload_checksum().clone(),
100 ),
101 (
102 "metadata-arguments.candid".into(),
103 ArtifactChecksumRecord::from_bytes(metadata.request().arguments()),
104 ),
105 ];
106 let values = metadata.metadata();
107 let sizes = [
108 values.wasm_module_size,
109 values.wasm_memory_size,
110 values.stable_memory_size,
111 ];
112 let empty = ArtifactChecksumRecord::from_bytes(&[]);
115 checksums.extend(REGIONS.map(|name| (name.into(), empty.clone())));
116 for chunk in &values.wasm_chunk_store {
117 checksums.push((
118 format!("chunk-{}.bin", hex_bytes(&chunk.hash)).into(),
119 ArtifactChecksumRecord::from_digest(
120 chunk
121 .hash
122 .as_slice()
123 .try_into()
124 .map_err(|_| IcSnapshotArtifactError::OriginalMismatch)?,
125 ),
126 ));
127 }
128 check_closed_tree(directory, &checksums)?;
129 for (index, (name, checksum)) in checksums.iter_mut().enumerate() {
130 let (length, maximum) = match index {
131 0 => (Some(FORMAT.len() as u64), FORMAT.len() as u64),
132 1 => (None, MAX_IC_SNAPSHOT_METADATA_BYTES as u64),
133 2 => {
134 let length = metadata.request().arguments().len() as u64;
135 (Some(length), length)
136 }
137 3..=5 => (Some(sizes[index - 3]), sizes[index - 3]),
138 _ => (None, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64),
139 };
140 let actual = checksum_child(
141 directory,
142 name.to_str()
143 .ok_or(IcSnapshotArtifactError::UnexpectedEntry)?,
144 length,
145 maximum,
146 )?;
147 if !(3..=5).contains(&index) {
148 actual.verify(checksum.hash())?;
149 }
150 *checksum = actual;
151 }
152 check_closed_tree(directory, &checksums)?;
153 Ok(checksum_relative_files(checksums))
154}
155
156fn checksum_child(
157 directory: &File,
158 name: &str,
159 length: Option<u64>,
160 maximum: u64,
161) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
162 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
163 let mut file =
164 File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?);
165 let original = file.metadata()?;
166 if !original.is_file()
167 || original.len() > maximum
168 || length.is_some_and(|length| length != original.len())
169 {
170 return Err(IcSnapshotArtifactError::FileShape);
171 }
172 let limit = original
175 .len()
176 .checked_add(1)
177 .ok_or(IcSnapshotArtifactError::FileShape)?;
178 let mut bounded = (&mut file).take(limit);
179 let checksum = checksum_reader(&mut bounded)?;
180 if bounded.limit() != 1 {
181 return Err(IcSnapshotArtifactError::FileShape);
182 }
183 let held = file.metadata()?;
184 let current =
185 File::from(unix_fs::openat(directory, name, flags, Mode::empty()).map_err(errno_to_io)?)
186 .metadata()?;
187 if !current.is_file()
188 || original.dev() != current.dev()
189 || original.ino() != current.ino()
190 || original.len() != current.len()
191 || original.len() != held.len()
192 {
193 return Err(IcSnapshotArtifactError::CustodyChanged);
194 }
195 Ok(checksum)
196}