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::{image_digest, ImageVerifier, Manifest, PayloadFormat, STRUCTURE_VERSION};
77///
78/// const VENDOR: [u8; 16] = [10; 16];
79/// const GATE_CONTROLLER: [u8; 16] = [15; 16];
80///
81/// let image = b"firmware for a gate controller";
82/// let manifest = Manifest {
83/// structure_version: STRUCTURE_VERSION,
84/// sequence: 1,
85/// vendor_id: VENDOR,
86/// class_id: GATE_CONTROLLER,
87/// format: PayloadFormat::Raw,
88/// storage: 0,
89/// digest: image_digest(image),
90/// size: image.len() as u32,
91/// expires: 0,
92/// };
93///
94/// let mut verifier = ImageVerifier::new(&manifest);
95/// for chunk in image.chunks(4) {
96/// verifier.update(chunk).unwrap();
97/// }
98/// let verified = verifier.finish().unwrap();
99/// assert_eq!(verified.size(), manifest.size);
100/// ```
101pub struct ImageVerifier {
102 hasher: Sha256,
103 expected_digest: [u8; DIGEST_LEN],
104 expected_size: u32,
105 seen: u64,
106}
107
108impl ImageVerifier {
109 /// Starts verifying an image against `manifest`.
110 ///
111 /// # Arguments
112 ///
113 /// * `manifest` - the manifest whose digest and size the image must match.
114 ///
115 /// # Returns
116 ///
117 /// A verifier awaiting the image.
118 pub fn new(manifest: &Manifest) -> Self {
119 Self {
120 hasher: Sha256::new(),
121 expected_digest: manifest.digest,
122 expected_size: manifest.size,
123 seen: 0,
124 }
125 }
126
127 /// Folds the next chunk of the image in.
128 ///
129 /// # Arguments
130 ///
131 /// * `chunk` - the next bytes of the image, in order.
132 ///
133 /// # Returns
134 ///
135 /// `Ok(())` once the chunk is hashed.
136 ///
137 /// # Errors
138 ///
139 /// Returns [`Refusal::Size`] as soon as more bytes arrive than the manifest
140 /// declared, so an oversized payload is stopped while it is arriving rather
141 /// than after it has filled the slot.
142 pub fn update(&mut self, chunk: &[u8]) -> Result<()> {
143 self.seen += chunk.len() as u64;
144 if self.seen > u64::from(self.expected_size) {
145 return Err(Refusal::Size);
146 }
147 self.hasher.update(chunk);
148 Ok(())
149 }
150
151 /// Finishes the check and reports whether the image is the one described.
152 ///
153 /// # Returns
154 ///
155 /// A [`Verified`] receipt when the image is exactly the length and content
156 /// the manifest committed to.
157 ///
158 /// # Errors
159 ///
160 /// Returns [`Refusal::Size`] if fewer bytes arrived than declared, or
161 /// [`Refusal::Digest`] if the content does not hash to the manifest's digest.
162 pub fn finish(self) -> Result<Verified> {
163 if self.seen != u64::from(self.expected_size) {
164 return Err(Refusal::Size);
165 }
166
167 let digest: [u8; DIGEST_LEN] = self.hasher.finalize().into();
168 // A wrong digest and a right one take the same work to reject here, but the
169 // comparison is over a hash the attacker cannot steer, so an early return
170 // leaks nothing worth having.
171 if digest != self.expected_digest {
172 return Err(Refusal::Digest);
173 }
174
175 Ok(Verified {
176 size: self.expected_size,
177 digest,
178 })
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::manifest::{PayloadFormat, STRUCTURE_VERSION};
186
187 /// A manifest describing `image` exactly.
188 fn manifest_for(image: &[u8]) -> Manifest {
189 Manifest {
190 structure_version: STRUCTURE_VERSION,
191 sequence: 1,
192 vendor_id: [0; 16],
193 class_id: [0; 16],
194 format: PayloadFormat::Raw,
195 storage: 0,
196 digest: Sha256::digest(image).into(),
197 size: image.len() as u32,
198 expires: 0,
199 }
200 }
201
202 /// Feeds an image through a verifier in small chunks.
203 fn run(manifest: &Manifest, image: &[u8]) -> Result<Verified> {
204 let mut verifier = ImageVerifier::new(manifest);
205 for chunk in image.chunks(7) {
206 verifier.update(chunk)?;
207 }
208 verifier.finish()
209 }
210
211 #[test]
212 fn the_described_image_verifies() {
213 let image = b"the firmware, arriving in pieces";
214 let manifest = manifest_for(image);
215 let verified = run(&manifest, image).expect("verify");
216 assert_eq!(verified.size(), image.len() as u32);
217 assert_eq!(verified.digest(), manifest.digest);
218 }
219
220 #[test]
221 fn an_empty_image_verifies_when_that_is_what_was_described() {
222 let manifest = manifest_for(b"");
223 assert!(run(&manifest, b"").is_ok());
224 }
225
226 #[test]
227 fn a_tampered_image_is_refused() {
228 let image = b"the firmware, arriving in pieces";
229 let manifest = manifest_for(image);
230 let mut altered = *image;
231 altered[3] ^= 0x01;
232 assert_eq!(run(&manifest, &altered), Err(Refusal::Digest));
233 }
234
235 #[test]
236 fn a_short_image_is_refused() {
237 let image = b"the firmware, arriving in pieces";
238 let manifest = manifest_for(image);
239 assert_eq!(
240 run(&manifest, &image[..image.len() - 1]),
241 Err(Refusal::Size)
242 );
243 }
244
245 #[test]
246 fn an_oversized_image_is_stopped_while_it_arrives() {
247 let image = b"short";
248 let manifest = manifest_for(image);
249 let mut verifier = ImageVerifier::new(&manifest);
250 // The refusal lands on the chunk that crosses the declared length, not at
251 // the end, so nothing keeps writing past the slot.
252 assert!(verifier.update(image).is_ok());
253 assert_eq!(verifier.update(b"more"), Err(Refusal::Size));
254 }
255
256 #[test]
257 fn reordered_chunks_are_refused() {
258 let image = b"order matters to a hash";
259 let manifest = manifest_for(image);
260 let mut verifier = ImageVerifier::new(&manifest);
261 verifier.update(&image[10..]).expect("update");
262 verifier.update(&image[..10]).expect("update");
263 assert_eq!(verifier.finish(), Err(Refusal::Digest));
264 }
265}