ic_backup/ops/persistence/download_journal/ic_snapshot_artifact/verification/
mod.rs1use super::{
4 ArtifactChecksumRecord, DownloadJournalGuard, File, IcSnapshotArtifactError,
5 IcSnapshotMetadataReply, Mode, OFlags, check_closed_tree, check_directory_identity,
6 checksum_relative_files, member, open_directory_no_follow, unix_fs,
7};
8use crate::{
9 model::operation_plan::OperationPlanRecord,
10 ops::{
11 artifacts::ArtifactError,
12 persistence::{DownloadIntegrityError, read_operation_plan},
13 },
14 policy::download_integrity::validate,
15};
16use ic_host_artifacts::artifact::{ArtifactError as InputError, hash_exact_reader};
17use std::{
18 io::{self, Read},
19 os::unix::fs::MetadataExt,
20};
21
22impl DownloadJournalGuard<'_> {
23 pub fn verify_ic_snapshot_artifact(
43 &self,
44 plan: &OperationPlanRecord,
45 snapshot: &str,
46 metadata: &IcSnapshotMetadataReply<'_>,
47 ) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
48 self.admit_ic_artifact_original(plan)?;
49 let entry = self
50 .record
51 .artifact(metadata.request().target(), snapshot)
52 .map_err(super::DownloadJournalError::from)?;
53 if entry.snapshot_taken_at_timestamp() != metadata.metadata().taken_at_timestamp {
54 return Err(IcSnapshotArtifactError::OriginalMismatch);
55 }
56 let expected = entry
57 .checksum()
58 .ok_or(IcSnapshotArtifactError::OriginalMismatch)?;
59 self.check_artifact_parent()?;
60 let parent_path = self.layout.root().join("artifacts");
61 let parent = open_directory_no_follow(&parent_path)?;
62 let path = self.layout.root().join(entry.artifact_path());
63 let directory = File::from(
64 unix_fs::openat(
65 &parent,
66 path.file_name()
67 .ok_or(IcSnapshotArtifactError::CustodyChanged)?,
68 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
69 Mode::empty(),
70 )
71 .map_err(io::Error::from)?,
72 );
73 checksum_ic_tree(&directory, metadata)?.verify(expected.hash())?;
74 check_directory_identity(&parent_path, &parent)?;
75 check_directory_identity(&path, &directory)?;
76 self.admit_ic_artifact_original(plan)?;
77 Ok(expected.clone())
78 }
79
80 fn admit_ic_artifact_original(
81 &self,
82 plan: &OperationPlanRecord,
83 ) -> Result<(), DownloadIntegrityError> {
84 self.check_usable()?;
85 read_operation_plan(self.layout, &plan.digest())?;
86 self.require_unchanged_integrity_journal()?;
87 validate(plan, &self.record)?;
88 Ok(())
89 }
90}
91
92fn checksum_ic_tree(
93 directory: &File,
94 metadata: &IcSnapshotMetadataReply<'_>,
95) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
96 checksum_ic_tree_with(directory, metadata, checksum_child)
97}
98
99pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
100 directory: &File,
101 metadata: &IcSnapshotMetadataReply<'_>,
102 mut read_child: impl FnMut(&File, &member::ExpectedMember) -> Result<ArtifactChecksumRecord, E>,
103) -> Result<ArtifactChecksumRecord, E> {
104 let members = member::expected(metadata)?;
105 check_closed_tree(
106 directory,
107 members
108 .iter()
109 .map(|member| std::ffi::OsStr::new(&member.name)),
110 )?;
111 let mut checksums = Vec::with_capacity(members.len());
112 for member in members {
113 let actual = read_child(directory, &member)?;
114 if let Some(expected) = &member.checksum {
115 actual
116 .verify(expected.hash())
117 .map_err(IcSnapshotArtifactError::from)?;
118 }
119 checksums.push((member.name.into(), actual));
120 }
121 check_closed_tree(
122 directory,
123 checksums
124 .iter()
125 .map(|(path, _): &(std::path::PathBuf, _)| path.as_os_str()),
126 )?;
127 checksum_relative_files(checksums)
128 .map_err(ArtifactError::from)
129 .map_err(IcSnapshotArtifactError::from)
130 .map_err(E::from)
131}
132
133fn checksum_child(
134 directory: &File,
135 member: &member::ExpectedMember,
136) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
137 checksum_child_with(
138 directory,
139 &member.name,
140 member.length,
141 member.maximum,
142 |file, length| checksum_exact_reader(file, length),
143 )
144}
145
146pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
147 directory: &File,
148 name: &str,
149 length: Option<u64>,
150 maximum: u64,
151 read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
152) -> Result<ArtifactChecksumRecord, E> {
153 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
154 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
155 let checksum = read(&mut file, original.len())?;
156 let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
157 let current = File::from(
158 unix_fs::openat(directory, name, flags, Mode::empty())
159 .map_err(io::Error::from)
160 .map_err(IcSnapshotArtifactError::from)?,
161 )
162 .metadata()
163 .map_err(IcSnapshotArtifactError::from)?;
164 if !current.is_file()
165 || original.dev() != current.dev()
166 || original.ino() != current.ino()
167 || original.len() != current.len()
168 || original.len() != held.len()
169 {
170 return Err(IcSnapshotArtifactError::CustodyChanged.into());
171 }
172 Ok(checksum)
173}
174
175pub(super) fn checksum_exact_reader<R: Read>(
176 reader: R,
177 length: u64,
178) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
179 let identity = hash_exact_reader(reader, length).map_err(|error| match error {
180 InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
181 IcSnapshotArtifactError::FileShape
182 }
183 error => ArtifactError::Io(error.into()).into(),
184 })?;
185 Ok(ArtifactChecksumRecord::from_digest(
186 *identity.sha256.as_bytes(),
187 ))
188}
189
190pub(super) fn open_regular_child(
191 directory: &File,
192 name: &str,
193 length: Option<u64>,
194 maximum: u64,
195) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
196 let file = File::from(
197 unix_fs::openat(
198 directory,
199 name,
200 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
201 Mode::empty(),
202 )
203 .map_err(io::Error::from)?,
204 );
205 let metadata = file.metadata()?;
206 if !metadata.is_file()
207 || metadata.len() > maximum
208 || length.is_some_and(|length| length != metadata.len())
209 {
210 return Err(IcSnapshotArtifactError::FileShape);
211 }
212 Ok((file, metadata))
213}
214
215#[cfg(test)]
216mod tests;