pamoja_update/verify.rs
1//! Checking an image against the manifest that describes it.
2//!
3//! The image is hashed as it arrives rather than after it lands, so a device with
4//! kilobytes of RAM can verify a payload of megabytes. Nothing is trusted until
5//! [`ImageVerifier::finish`] agrees on both the length and the digest, which is
6//! why the caller is handed a receipt it cannot forge rather than a boolean it
7//! might forget to read.
8
9use sha2::{Digest, Sha256};
10
11use crate::error::{Refusal, Result};
12use crate::manifest::{Manifest, DIGEST_LEN};
13
14/// Proof that an image matched its manifest.
15///
16/// Only [`ImageVerifier::finish`] produces one, so a function that asks for a
17/// [`Verified`] cannot be handed an unchecked image by mistake.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Verified {
20 size: u32,
21 digest: [u8; DIGEST_LEN],
22}
23
24impl Verified {
25 /// Returns the verified image's length in bytes.
26 ///
27 /// # Returns
28 ///
29 /// The length the manifest declared and the image turned out to have.
30 pub fn size(&self) -> u32 {
31 self.size
32 }
33
34 /// Returns the verified image's digest.
35 ///
36 /// # Returns
37 ///
38 /// The SHA-256 the manifest committed to and the image hashed to.
39 pub fn digest(&self) -> [u8; DIGEST_LEN] {
40 self.digest
41 }
42}
43
44/// Hashes a complete image, for a publisher filling in a manifest.
45///
46/// The manifest commits to a SHA-256 over the image, and this is that hash, so a
47/// publisher does not have to add a hashing crate of its own just to name the image it
48/// is releasing. A device receiving the image checks the same hash through
49/// [`ImageVerifier`], which streams it rather than holding the whole image.
50///
51/// # Arguments
52///
53/// * `image` - the complete image the release carries.
54///
55/// # Returns
56///
57/// The SHA-256 of `image`, ready to put in a [`Manifest`](crate::Manifest).
58///
59/// # Examples
60///
61/// ```
62/// use pamoja_update::image_digest;
63///
64/// let digest = image_digest(b"firmware");
65/// assert_eq!(digest.len(), 32);
66/// ```
67pub fn image_digest(image: &[u8]) -> [u8; DIGEST_LEN] {
68 Sha256::digest(image).into()
69}
70
71/// Hashes an image as it arrives and checks it against a manifest.
72///
73/// # Examples
74///
75/// ```
76/// use pamoja_update::{ImageVerifier, Manifest, PayloadFormat, STRUCTURE_VERSION};
77/// use sha2::{Digest, Sha256};
78///
79/// let image = b"firmware bytes";
80/// let manifest = Manifest {
81/// structure_version: STRUCTURE_VERSION,
82/// sequence: 1,
83/// vendor_id: [0; 16],
84/// class_id: [0; 16],
85/// format: PayloadFormat::Raw,
86/// storage: 0,
87/// digest: Sha256::digest(image).into(),
88/// size: image.len() as u32,
89/// expires: 0,
90/// };
91///
92/// let mut verifier = ImageVerifier::new(&manifest);
93/// for chunk in image.chunks(4) {
94/// verifier.update(chunk).unwrap();
95/// }
96/// assert!(verifier.finish().is_ok());
97/// ```
98pub struct ImageVerifier {
99 hasher: Sha256,
100 expected_digest: [u8; DIGEST_LEN],
101 expected_size: u32,
102 seen: u64,
103}
104
105impl ImageVerifier {
106 /// Starts verifying an image against `manifest`.
107 ///
108 /// # Arguments
109 ///
110 /// * `manifest` - the manifest whose digest and size the image must match.
111 ///
112 /// # Returns
113 ///
114 /// A verifier awaiting the image.
115 pub fn new(manifest: &Manifest) -> Self {
116 Self {
117 hasher: Sha256::new(),
118 expected_digest: manifest.digest,
119 expected_size: manifest.size,
120 seen: 0,
121 }
122 }
123
124 /// Folds the next chunk of the image in.
125 ///
126 /// # Arguments
127 ///
128 /// * `chunk` - the next bytes of the image, in order.
129 ///
130 /// # Returns
131 ///
132 /// `Ok(())` once the chunk is hashed.
133 ///
134 /// # Errors
135 ///
136 /// Returns [`Refusal::Size`] as soon as more bytes arrive than the manifest
137 /// declared, so an oversized payload is stopped while it is arriving rather
138 /// than after it has filled the slot.
139 pub fn update(&mut self, chunk: &[u8]) -> Result<()> {
140 self.seen += chunk.len() as u64;
141 if self.seen > u64::from(self.expected_size) {
142 return Err(Refusal::Size);
143 }
144 self.hasher.update(chunk);
145 Ok(())
146 }
147
148 /// Finishes the check and reports whether the image is the one described.
149 ///
150 /// # Returns
151 ///
152 /// A [`Verified`] receipt when the image is exactly the length and content
153 /// the manifest committed to.
154 ///
155 /// # Errors
156 ///
157 /// Returns [`Refusal::Size`] if fewer bytes arrived than declared, or
158 /// [`Refusal::Digest`] if the content does not hash to the manifest's digest.
159 pub fn finish(self) -> Result<Verified> {
160 if self.seen != u64::from(self.expected_size) {
161 return Err(Refusal::Size);
162 }
163
164 let digest: [u8; DIGEST_LEN] = self.hasher.finalize().into();
165 // A wrong digest and a right one take the same work to reject here, but the
166 // comparison is over a hash the attacker cannot steer, so an early return
167 // leaks nothing worth having.
168 if digest != self.expected_digest {
169 return Err(Refusal::Digest);
170 }
171
172 Ok(Verified {
173 size: self.expected_size,
174 digest,
175 })
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::manifest::{PayloadFormat, STRUCTURE_VERSION};
183
184 /// A manifest describing `image` exactly.
185 fn manifest_for(image: &[u8]) -> Manifest {
186 Manifest {
187 structure_version: STRUCTURE_VERSION,
188 sequence: 1,
189 vendor_id: [0; 16],
190 class_id: [0; 16],
191 format: PayloadFormat::Raw,
192 storage: 0,
193 digest: Sha256::digest(image).into(),
194 size: image.len() as u32,
195 expires: 0,
196 }
197 }
198
199 /// Feeds an image through a verifier in small chunks.
200 fn run(manifest: &Manifest, image: &[u8]) -> Result<Verified> {
201 let mut verifier = ImageVerifier::new(manifest);
202 for chunk in image.chunks(7) {
203 verifier.update(chunk)?;
204 }
205 verifier.finish()
206 }
207
208 #[test]
209 fn the_described_image_verifies() {
210 let image = b"the firmware, arriving in pieces";
211 let manifest = manifest_for(image);
212 let verified = run(&manifest, image).expect("verify");
213 assert_eq!(verified.size(), image.len() as u32);
214 assert_eq!(verified.digest(), manifest.digest);
215 }
216
217 #[test]
218 fn an_empty_image_verifies_when_that_is_what_was_described() {
219 let manifest = manifest_for(b"");
220 assert!(run(&manifest, b"").is_ok());
221 }
222
223 #[test]
224 fn a_tampered_image_is_refused() {
225 let image = b"the firmware, arriving in pieces";
226 let manifest = manifest_for(image);
227 let mut altered = *image;
228 altered[3] ^= 0x01;
229 assert_eq!(run(&manifest, &altered), Err(Refusal::Digest));
230 }
231
232 #[test]
233 fn a_short_image_is_refused() {
234 let image = b"the firmware, arriving in pieces";
235 let manifest = manifest_for(image);
236 assert_eq!(
237 run(&manifest, &image[..image.len() - 1]),
238 Err(Refusal::Size)
239 );
240 }
241
242 #[test]
243 fn an_oversized_image_is_stopped_while_it_arrives() {
244 let image = b"short";
245 let manifest = manifest_for(image);
246 let mut verifier = ImageVerifier::new(&manifest);
247 // The refusal lands on the chunk that crosses the declared length, not at
248 // the end, so nothing keeps writing past the slot.
249 assert!(verifier.update(image).is_ok());
250 assert_eq!(verifier.update(b"more"), Err(Refusal::Size));
251 }
252
253 #[test]
254 fn reordered_chunks_are_refused() {
255 let image = b"order matters to a hash";
256 let manifest = manifest_for(image);
257 let mut verifier = ImageVerifier::new(&manifest);
258 verifier.update(&image[10..]).expect("update");
259 verifier.update(&image[..10]).expect("update");
260 assert_eq!(verifier.finish(), Err(Refusal::Digest));
261 }
262}