Skip to main content

iris_trust/
verify.rs

1//! Getting a decoder module out of a container, which means hashing it.
2
3use std::borrow::Cow;
4
5use iris_format::{Container, DecoderLocation, DecoderRef, Digest};
6
7use crate::error::Untrusted;
8use crate::policy::Policy;
9
10/// A decoder module that hashes to what the container says it should.
11///
12/// The point of this type is that it has no public constructor. [`Policy::decoder`] is the only
13/// thing that makes one, and hashing the bytes is the only thing it does, so a caller holding a
14/// `Verified` is holding proof that the comparison happened. That is a stronger promise than a
15/// function that checks and then returns a slice, because the slice is the same slice whether or
16/// not anybody called the checker.
17///
18/// The module is borrowed from the container when it is embedded and owned when a resolver went and
19/// found it, which is the only difference the two cases make once the hash has matched.
20#[derive(Clone, Debug)]
21pub struct Verified<'a> {
22    record: DecoderRef<'a>,
23    module: Cow<'a, [u8]>,
24    digest: Digest,
25}
26
27impl<'a> Verified<'a> {
28    /// The module, which is the same bytes that were hashed.
29    #[must_use]
30    pub fn module(&self) -> &[u8] {
31        &self.module
32    }
33
34    /// The digest of the module.
35    ///
36    /// This is the decoder's identity: the container names it, this crate recomputed it, and the
37    /// two agreed. A host with a native implementation of this exact module looks it up by this
38    /// value and runs that instead, which is what makes substitution safe rather than a matter of
39    /// trusting a name.
40    #[must_use]
41    pub const fn digest(&self) -> Digest {
42        self.digest
43    }
44
45    /// What the container says about the decoder: its name, its ABI version and what it needs.
46    #[must_use]
47    pub const fn record(&self) -> &DecoderRef<'a> {
48        &self.record
49    }
50}
51
52impl Policy {
53    /// Finds the decoder this container names and hands it over only if it hashes to what the
54    /// container says.
55    ///
56    /// This is the whole of the trust boundary for a decoder, and hashing is not a step it can be
57    /// asked to skip. There is no flag here, and there is nowhere else to get the bytes.
58    ///
59    /// The hash is over the module alone. The container's root digest covers the header and the
60    /// footer, which is what makes a container cheap to open, so a byte changed inside the decoder
61    /// section parses perfectly well and is caught here instead. That is the case this exists for.
62    ///
63    /// A decoder that lives outside the container is refused unless this policy was built with a
64    /// resolver. Whatever the resolver returns is hashed exactly like an embedded module, so a
65    /// registry that hands back the wrong thing fails here rather than at the compiler.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`Untrusted::Missing`] if the container names no decoder, [`Untrusted::External`] if
70    /// the module lives outside the container and this policy has no resolver,
71    /// [`Untrusted::Unresolved`] if it has one and the resolver found nothing, [`Untrusted::Lost`]
72    /// if an embedded module names a section that is not in the file, and [`Untrusted::Digest`],
73    /// carrying both digests, if the bytes are not the module the container names.
74    pub fn decoder<'a>(&self, container: &Container<'a>) -> Result<Verified<'a>, Untrusted> {
75        let record = container.decoder().ok_or(Untrusted::Missing)?;
76        self.check(record, container.decoder_bytes().map(Cow::Borrowed))
77    }
78
79    /// The same check, for a host that read the module out of the file itself.
80    ///
81    /// A host that is not holding the container cannot be handed a slice of it, so it reads the
82    /// section named by [`iris_format::Directory::decoder_section`] and passes the bytes here.
83    /// `embedded` is `None` when there is no such section, which is the same thing as an embedded
84    /// record naming a section the file does not have.
85    ///
86    /// Everything after that point is identical, deliberately. The bytes are hashed and compared
87    /// against the record the same way whether they arrived as a borrow of a resident file, as a
88    /// read through a window, or from a resolver, because how they were obtained is exactly the
89    /// thing the digest exists to stop mattering.
90    ///
91    /// # Errors
92    ///
93    /// See [`Policy::decoder`].
94    pub fn decoder_read<'a>(
95        &self,
96        record: &DecoderRef<'a>,
97        embedded: Option<Vec<u8>>,
98    ) -> Result<Verified<'a>, Untrusted> {
99        self.check(record, embedded.map(Cow::Owned))
100    }
101
102    /// The comparison both entry points end in, written once so they cannot come to differ.
103    fn check<'a>(
104        &self,
105        record: &DecoderRef<'a>,
106        embedded: Option<Cow<'a, [u8]>>,
107    ) -> Result<Verified<'a>, Untrusted> {
108        let expected = record.digest;
109
110        let module: Cow<'a, [u8]> = match record.location {
111            DecoderLocation::Embedded { section } => embedded.ok_or(Untrusted::Lost { section })?,
112            DecoderLocation::External => {
113                let resolver = self.resolver().ok_or_else(|| Untrusted::External {
114                    name: record.name.to_owned(),
115                })?;
116                let found = resolver
117                    .resolve(record)
118                    .ok_or_else(|| Untrusted::Unresolved {
119                        name: record.name.to_owned(),
120                        digest: expected,
121                    })?;
122                Cow::Owned(found)
123            }
124            // `DecoderLocation` is open ended, so this is a file written by something newer than
125            // this build. Failing closed is the only reading of it that cannot be wrong.
126            _ => {
127                return Err(Untrusted::Elsewhere {
128                    name: record.name.to_owned(),
129                });
130            }
131        };
132
133        let found = Digest::of(&module);
134        if found != expected {
135            return Err(Untrusted::Digest { expected, found });
136        }
137
138        Ok(Verified {
139            record: record.clone(),
140            module,
141            digest: found,
142        })
143    }
144}
145
146/// Hashes the decoder embedded in a container and hands it over only if the hash matches.
147///
148/// This is [`Policy::decoder`] under the default policy, which runs embedded decoders and nothing
149/// else. A host that means to run a decoder from somewhere else says so by building a [`Policy`]
150/// with a resolver.
151///
152/// # Errors
153///
154/// See [`Policy::decoder`].
155pub fn decoder<'a>(container: &Container<'a>) -> Result<Verified<'a>, Untrusted> {
156    Policy::embedded_only().decoder(container)
157}
158
159#[cfg(test)]
160mod tests {
161    use iris_abi::CapabilitySet;
162    use iris_format::{Builder, Container, DecoderRef, Digest, SectionKind};
163
164    use super::{Policy, Untrusted, decoder};
165
166    /// Stands in for a decoder. Nothing here compiles it, and that is the point: the digest is
167    /// checked before anything treats these bytes as code, so they do not have to be code.
168    const MODULE: &[u8] = b"a module, as far as this crate is concerned";
169
170    /// A resolver that hands back whatever it was built with, which is how a host that keeps its
171    /// decoders in a directory behaves once the file has been read.
172    #[derive(Debug)]
173    struct Holding(Option<Vec<u8>>);
174
175    impl super::super::Resolve for Holding {
176        fn resolve(&self, _decoder: &DecoderRef<'_>) -> Option<Vec<u8>> {
177            self.0.clone()
178        }
179    }
180
181    fn embedded() -> Vec<u8> {
182        let mut builder = Builder::new("readings", 3);
183        builder.section(SectionKind::Data, b"rows go here".to_vec());
184        builder.embed_decoder("test", (1, 0), CapabilitySet::new(), MODULE.to_vec());
185        builder.build().expect("a container this small always fits")
186    }
187
188    fn external() -> Vec<u8> {
189        let mut builder = Builder::new("readings", 3);
190        builder.section(SectionKind::Data, b"rows go here".to_vec());
191        builder.external_decoder(
192            "elsewhere",
193            (1, 0),
194            CapabilitySet::new(),
195            Digest::of(MODULE),
196        );
197        builder.build().expect("a container this small always fits")
198    }
199
200    /// Where the module sits in the file, which is where a tamperer would be working.
201    fn module_at(bytes: &[u8]) -> usize {
202        bytes
203            .windows(MODULE.len())
204            .position(|window| window == MODULE)
205            .expect("the builder wrote the module into the file")
206    }
207
208    #[test]
209    fn a_module_that_matches_its_digest_is_handed_over() {
210        let bytes = embedded();
211        let container = Container::parse(&bytes).expect("the container parses");
212        let verified = decoder(&container).expect("the module is the one the container names");
213
214        assert_eq!(verified.module(), MODULE);
215        assert_eq!(verified.digest(), Digest::of(MODULE));
216        assert_eq!(verified.record().name, "test");
217    }
218
219    #[test]
220    fn one_flipped_byte_in_the_module_is_refused_with_both_digests() {
221        let mut bytes = embedded();
222        let at = module_at(&bytes) + MODULE.len() / 2;
223        bytes[at] ^= 1;
224
225        // The file still parses, which is the part worth saying out loud. The root digest covers
226        // the header and the footer, so a byte changed inside a section is not something the
227        // container can notice, and the decoder digest is what stands between that byte and the
228        // compiler.
229        let container = Container::parse(&bytes).expect("the container still parses");
230
231        let Err(Untrusted::Digest { expected, found }) = decoder(&container) else {
232            panic!("a module with a flipped byte was accepted");
233        };
234        assert_eq!(expected, Digest::of(MODULE));
235        assert_ne!(found, expected);
236
237        let message = Untrusted::Digest { expected, found }.to_string();
238        assert!(
239            message.contains(&expected.to_string()),
240            "the message does not say which module was expected: {message}"
241        );
242        assert!(
243            message.contains(&found.to_string()),
244            "the message does not say what arrived instead: {message}"
245        );
246    }
247
248    #[test]
249    fn a_container_with_no_decoder_says_so() {
250        let mut builder = Builder::new("readings", 3);
251        builder.section(SectionKind::Data, b"rows go here".to_vec());
252        let bytes = builder.build().expect("a container this small always fits");
253        let container = Container::parse(&bytes).expect("the container parses");
254
255        assert_eq!(decoder(&container).unwrap_err(), Untrusted::Missing);
256    }
257
258    #[test]
259    fn the_default_policy_refuses_a_decoder_that_is_not_in_the_container() {
260        let bytes = external();
261        let container = Container::parse(&bytes).expect("the container parses");
262
263        let error = decoder(&container).unwrap_err();
264        assert_eq!(
265            error,
266            Untrusted::External {
267                name: "elsewhere".to_owned()
268            }
269        );
270
271        // The refusal has to say what would have allowed it, because the alternative is an operator
272        // reading the source of this crate to find out.
273        let message = error.to_string();
274        assert!(
275            message.contains("Policy::with_external_decoders_resolved_by"),
276            "the message does not name the setting that would allow this: {message}"
277        );
278    }
279
280    #[test]
281    fn a_host_that_opted_in_gets_the_module_its_resolver_found() {
282        let bytes = external();
283        let container = Container::parse(&bytes).expect("the container parses");
284        let policy = Policy::with_external_decoders_resolved_by(Holding(Some(MODULE.to_vec())));
285
286        let verified = policy
287            .decoder(&container)
288            .expect("the resolver returned the module the container names");
289        assert_eq!(verified.module(), MODULE);
290        assert_eq!(verified.digest(), Digest::of(MODULE));
291    }
292
293    #[test]
294    fn a_resolver_that_returns_the_wrong_module_is_caught_by_the_digest() {
295        let bytes = external();
296        let container = Container::parse(&bytes).expect("the container parses");
297        let policy = Policy::with_external_decoders_resolved_by(Holding(Some(
298            b"some other module entirely".to_vec(),
299        )));
300
301        let Err(Untrusted::Digest { expected, found }) = policy.decoder(&container) else {
302            panic!("a fetched module nobody checked was accepted");
303        };
304        assert_eq!(expected, Digest::of(MODULE));
305        assert_ne!(found, expected);
306    }
307
308    #[test]
309    fn a_module_read_out_of_a_file_is_checked_the_same_way() {
310        let bytes = embedded();
311        let container = Container::parse(&bytes).expect("the container parses");
312        let record = container.decoder().expect("the container names a decoder");
313
314        // What a windowed host does: it read the decoder section itself and owns the bytes, so it
315        // cannot hand over a borrow of a file it is not holding.
316        let read = container
317            .decoder_bytes()
318            .expect("the section is here")
319            .to_vec();
320        let verified = Policy::embedded_only()
321            .decoder_read(record, Some(read))
322            .expect("the module is the one the container names");
323
324        assert_eq!(verified.module(), MODULE);
325        assert_eq!(verified.digest(), Digest::of(MODULE));
326        assert_eq!(verified.record().name, "test");
327
328        // And the same answer as the resident path, which is the claim worth pinning down. Two
329        // entry points that agree on a good file and differ on a bad one would be worse than one.
330        assert_eq!(
331            decoder(&container)
332                .expect("the resident path agrees")
333                .digest(),
334            verified.digest()
335        );
336    }
337
338    #[test]
339    fn a_module_read_wrong_is_refused_by_the_digest_and_not_by_where_it_came_from() {
340        let bytes = embedded();
341        let container = Container::parse(&bytes).expect("the container parses");
342        let record = container.decoder().expect("the container names a decoder");
343
344        let mut read = container
345            .decoder_bytes()
346            .expect("the section is here")
347            .to_vec();
348        read[MODULE.len() / 2] ^= 1;
349
350        let Err(Untrusted::Digest { expected, found }) =
351            Policy::embedded_only().decoder_read(record, Some(read))
352        else {
353            panic!("a module that was read wrong was accepted");
354        };
355        assert_eq!(expected, Digest::of(MODULE));
356        assert_ne!(found, expected);
357    }
358
359    #[test]
360    fn an_embedded_record_with_nothing_read_says_the_section_is_lost() {
361        let bytes = embedded();
362        let container = Container::parse(&bytes).expect("the container parses");
363        let record = container.decoder().expect("the container names a decoder");
364
365        // A host reaches this by finding no section with the id the record names, which is a file
366        // that points at a decoder it does not contain.
367        assert_eq!(
368            Policy::embedded_only()
369                .decoder_read(record, None)
370                .unwrap_err(),
371            Untrusted::Lost { section: 1 }
372        );
373    }
374
375    #[test]
376    fn a_resolver_that_finds_nothing_is_not_an_attack() {
377        let bytes = external();
378        let container = Container::parse(&bytes).expect("the container parses");
379        let policy = Policy::with_external_decoders_resolved_by(Holding(None));
380
381        assert_eq!(
382            policy.decoder(&container).unwrap_err(),
383            Untrusted::Unresolved {
384                name: "elsewhere".to_owned(),
385                digest: Digest::of(MODULE),
386            }
387        );
388    }
389}