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