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(directory, member, |file, length| {
130 checksum_exact_reader(file, length)
131 })
132}
133
134pub(super) fn checksum_child_with<E: From<IcSnapshotArtifactError>>(
135 directory: &File,
136 member: &member::ExpectedMember,
137 read: impl FnOnce(&mut File, u64) -> Result<ArtifactChecksumRecord, E>,
138) -> Result<ArtifactChecksumRecord, E> {
139 let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
140 let (mut file, original) = member.open(directory)?;
141 let checksum = read(&mut file, original.len())?;
142 let held = file.metadata().map_err(IcSnapshotArtifactError::from)?;
143 let current = File::from(
144 unix_fs::openat(directory, member.name.as_str(), flags, Mode::empty())
145 .map_err(io::Error::from)
146 .map_err(IcSnapshotArtifactError::from)?,
147 )
148 .metadata()
149 .map_err(IcSnapshotArtifactError::from)?;
150 if !current.is_file()
151 || original.dev() != current.dev()
152 || original.ino() != current.ino()
153 || original.len() != current.len()
154 || original.len() != held.len()
155 {
156 return Err(IcSnapshotArtifactError::CustodyChanged.into());
157 }
158 Ok(checksum)
159}
160
161pub(super) fn checksum_exact_reader<R: Read>(
162 reader: R,
163 length: u64,
164) -> Result<ArtifactChecksumRecord, IcSnapshotArtifactError> {
165 #[cfg(test)]
166 let reader = super::read_count::Reader(reader);
167 let identity = hash_exact_reader(reader, length).map_err(|error| match error {
168 InputError::LimitExceeded { .. } | InputError::LengthMismatch { .. } => {
169 IcSnapshotArtifactError::FileShape
170 }
171 error => ArtifactError::Io(error.into()).into(),
172 })?;
173 Ok(ArtifactChecksumRecord::from_digest(
174 *identity.sha256.as_bytes(),
175 ))
176}
177
178pub(super) fn open_regular_child(
179 directory: &File,
180 name: &str,
181 length: Option<u64>,
182 maximum: u64,
183) -> Result<(File, std::fs::Metadata), IcSnapshotArtifactError> {
184 let file = File::from(
185 unix_fs::openat(
186 directory,
187 name,
188 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
189 Mode::empty(),
190 )
191 .map_err(io::Error::from)?,
192 );
193 let metadata = file.metadata()?;
194 if !metadata.is_file()
195 || metadata.len() > maximum
196 || length.is_some_and(|length| length != metadata.len())
197 {
198 return Err(IcSnapshotArtifactError::FileShape);
199 }
200 Ok((file, metadata))
201}
202
203#[cfg(test)]
204mod tests;