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