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::{
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 with_artifact_directory(
61 self.layout.root(),
62 entry.artifact_path(),
63 |directory| {
64 checksum_ic_tree(directory, metadata)?.verify(expected.hash())?;
65 Ok::<_, IcSnapshotArtifactError>(())
66 },
67 || Ok(self.admit_ic_artifact_original(plan)?),
68 )?;
69 Ok(expected.clone())
70 }
71
72 pub(super) fn admit_ic_artifact_original(
73 &self,
74 plan: &OperationPlanRecord,
75 ) -> Result<(), DownloadIntegrityError> {
76 self.check_usable()?;
77 read_operation_plan(self.layout, &plan.digest())?;
78 self.require_unchanged_integrity_journal()?;
79 validate(plan, &self.record)?;
80 Ok(())
81 }
82}
83
84fn checksum_ic_tree(
85 directory: &File,
86 metadata: &IcSnapshotMetadataReply<'_>,
87) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
88 checksum_ic_tree_with(directory, metadata, checksum_child)
89}
90
91pub(super) fn checksum_ic_tree_with<E: From<IcSnapshotArtifactError>>(
92 directory: &File,
93 metadata: &IcSnapshotMetadataReply<'_>,
94 mut read_child: impl FnMut(&File, &member::ExpectedMember) -> Result<ArtifactChecksumRecord, E>,
95) -> Result<ArtifactChecksumRecord, E> {
96 let members = member::expected(metadata)?;
97 check_closed_tree(
98 directory,
99 members
100 .iter()
101 .map(|member| std::ffi::OsStr::new(&member.name)),
102 )?;
103 let mut checksums = Vec::with_capacity(members.len());
104 for member in members {
105 let actual = read_child(directory, &member)?;
106 if let Some(expected) = member.checksum() {
107 actual
108 .verify(expected.hash())
109 .map_err(IcSnapshotArtifactError::from)?;
110 }
111 checksums.push((member.name.into(), actual));
112 }
113 check_closed_tree(
114 directory,
115 checksums
116 .iter()
117 .map(|(path, _): &(std::path::PathBuf, _)| path.as_os_str()),
118 )?;
119 checksum_relative_files(checksums)
120 .map_err(ArtifactError::from)
121 .map_err(IcSnapshotArtifactError::from)
122 .map_err(E::from)
123}
124
125fn checksum_child(
126 directory: &File,
127 member: &member::ExpectedMember,
128) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
129 checksum_child_with(
130 directory,
131 &member.name,
132 member.length(),
133 member.maximum(),
134 |file, length| checksum_exact_reader(file, length),
135 )
136}
137
138pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
139 directory: &File,
140 name: &str,
141 length: Option<u64>,
142 maximum: u64,
143 read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
144) -> Result<ArtifactChecksumRecord, E> {
145 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
146 let (mut file, original) = open_regular_child(directory, name, length, maximum)?;
147 let checksum = read(&mut file, original.len())?;
148 let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
149 let current = File::from(
150 unix_fs::openat(directory, name, flags, Mode::empty())
151 .map_err(io::Error::from)
152 .map_err(IcSnapshotArtifactError::from)?,
153 )
154 .metadata()
155 .map_err(IcSnapshotArtifactError::from)?;
156 if !current.is_file()
157 || original.dev() != current.dev()
158 || original.ino() != current.ino()
159 || original.len() != current.len()
160 || original.len() != held.len()
161 {
162 return Err(IcSnapshotArtifactError::CustodyChanged.into());
163 }
164 Ok(checksum)
165}
166
167pub(super) fn checksum_exact_reader<R: Read>(
168 reader: R,
169 length: u64,
170) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
171 #[cfg(test)]
172 let reader = super::read_count::Reader(reader);
173 let identity = hash_exact_reader(reader, length).map_err(|error| match error {
174 InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
175 IcSnapshotArtifactError::FileShape
176 }
177 error => ArtifactError::Io(error.into()).into(),
178 })?;
179 Ok(ArtifactChecksumRecord::from_digest(
180 *identity.sha256.as_bytes(),
181 ))
182}
183
184pub(super) fn open_regular_child(
185 directory: &File,
186 name: &str,
187 length: Option<u64>,
188 maximum: u64,
189) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
190 let file = File::from(
191 unix_fs::openat(
192 directory,
193 name,
194 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
195 Mode::empty(),
196 )
197 .map_err(io::Error::from)?,
198 );
199 let metadata = file.metadata()?;
200 if !metadata.is_file()
201 || metadata.len() > maximum
202 || length.is_some_and(|length| length != metadata.len())
203 {
204 return Err(IcSnapshotArtifactError::FileShape);
205 }
206 Ok((file, metadata))
207}
208
209#[cfg(test)]
210mod tests;