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        let expected = record.digest;
77
78        let module: Cow<'a, [u8]> = match record.location {
79            DecoderLocation::Embedded { section } => Cow::Borrowed(
80                container
81                    .decoder_bytes()
82                    .ok_or(Untrusted::Lost { section })?,
83            ),
84            DecoderLocation::External => {
85                let resolver = self.resolver().ok_or_else(|| Untrusted::External {
86                    name: record.name.to_owned(),
87                })?;
88                let found = resolver
89                    .resolve(record)
90                    .ok_or_else(|| Untrusted::Unresolved {
91                        name: record.name.to_owned(),
92                        digest: expected,
93                    })?;
94                Cow::Owned(found)
95            }
96            // `DecoderLocation` is open ended, so this is a file written by something newer than
97            // this build. Failing closed is the only reading of it that cannot be wrong.
98            _ => {
99                return Err(Untrusted::Elsewhere {
100                    name: record.name.to_owned(),
101                });
102            }
103        };
104
105        let found = Digest::of(&module);
106        if found != expected {
107            return Err(Untrusted::Digest { expected, found });
108        }
109
110        Ok(Verified {
111            record: record.clone(),
112            module,
113            digest: found,
114        })
115    }
116}
117
118/// Hashes the decoder embedded in a container and hands it over only if the hash matches.
119///
120/// This is [`Policy::decoder`] under the default policy, which runs embedded decoders and nothing
121/// else. A host that means to run a decoder from somewhere else says so by building a [`Policy`]
122/// with a resolver.
123///
124/// # Errors
125///
126/// See [`Policy::decoder`].
127pub fn decoder<'a>(container: &Container<'a>) -> Result<Verified<'a>, Untrusted> {
128    Policy::embedded_only().decoder(container)
129}
130
131#[cfg(test)]
132mod tests {
133    use iris_abi::CapabilitySet;
134    use iris_format::{Builder, Container, DecoderRef, Digest, SectionKind};
135
136    use super::{Policy, Untrusted, decoder};
137
138    /// Stands in for a decoder. Nothing here compiles it, and that is the point: the digest is
139    /// checked before anything treats these bytes as code, so they do not have to be code.
140    const MODULE: &[u8] = b"a module, as far as this crate is concerned";
141
142    /// A resolver that hands back whatever it was built with, which is how a host that keeps its
143    /// decoders in a directory behaves once the file has been read.
144    #[derive(Debug)]
145    struct Holding(Option<Vec<u8>>);
146
147    impl super::super::Resolve for Holding {
148        fn resolve(&self, _decoder: &DecoderRef<'_>) -> Option<Vec<u8>> {
149            self.0.clone()
150        }
151    }
152
153    fn embedded() -> Vec<u8> {
154        let mut builder = Builder::new("readings", 3);
155        builder.section(SectionKind::Data, b"rows go here".to_vec());
156        builder.embed_decoder("test", (1, 0), CapabilitySet::new(), MODULE.to_vec());
157        builder.build().expect("a container this small always fits")
158    }
159
160    fn external() -> Vec<u8> {
161        let mut builder = Builder::new("readings", 3);
162        builder.section(SectionKind::Data, b"rows go here".to_vec());
163        builder.external_decoder(
164            "elsewhere",
165            (1, 0),
166            CapabilitySet::new(),
167            Digest::of(MODULE),
168        );
169        builder.build().expect("a container this small always fits")
170    }
171
172    /// Where the module sits in the file, which is where a tamperer would be working.
173    fn module_at(bytes: &[u8]) -> usize {
174        bytes
175            .windows(MODULE.len())
176            .position(|window| window == MODULE)
177            .expect("the builder wrote the module into the file")
178    }
179
180    #[test]
181    fn a_module_that_matches_its_digest_is_handed_over() {
182        let bytes = embedded();
183        let container = Container::parse(&bytes).expect("the container parses");
184        let verified = decoder(&container).expect("the module is the one the container names");
185
186        assert_eq!(verified.module(), MODULE);
187        assert_eq!(verified.digest(), Digest::of(MODULE));
188        assert_eq!(verified.record().name, "test");
189    }
190
191    #[test]
192    fn one_flipped_byte_in_the_module_is_refused_with_both_digests() {
193        let mut bytes = embedded();
194        let at = module_at(&bytes) + MODULE.len() / 2;
195        bytes[at] ^= 1;
196
197        // The file still parses, which is the part worth saying out loud. The root digest covers
198        // the header and the footer, so a byte changed inside a section is not something the
199        // container can notice, and the decoder digest is what stands between that byte and the
200        // compiler.
201        let container = Container::parse(&bytes).expect("the container still parses");
202
203        let Err(Untrusted::Digest { expected, found }) = decoder(&container) else {
204            panic!("a module with a flipped byte was accepted");
205        };
206        assert_eq!(expected, Digest::of(MODULE));
207        assert_ne!(found, expected);
208
209        let message = Untrusted::Digest { expected, found }.to_string();
210        assert!(
211            message.contains(&expected.to_string()),
212            "the message does not say which module was expected: {message}"
213        );
214        assert!(
215            message.contains(&found.to_string()),
216            "the message does not say what arrived instead: {message}"
217        );
218    }
219
220    #[test]
221    fn a_container_with_no_decoder_says_so() {
222        let mut builder = Builder::new("readings", 3);
223        builder.section(SectionKind::Data, b"rows go here".to_vec());
224        let bytes = builder.build().expect("a container this small always fits");
225        let container = Container::parse(&bytes).expect("the container parses");
226
227        assert_eq!(decoder(&container).unwrap_err(), Untrusted::Missing);
228    }
229
230    #[test]
231    fn the_default_policy_refuses_a_decoder_that_is_not_in_the_container() {
232        let bytes = external();
233        let container = Container::parse(&bytes).expect("the container parses");
234
235        let error = decoder(&container).unwrap_err();
236        assert_eq!(
237            error,
238            Untrusted::External {
239                name: "elsewhere".to_owned()
240            }
241        );
242
243        // The refusal has to say what would have allowed it, because the alternative is an operator
244        // reading the source of this crate to find out.
245        let message = error.to_string();
246        assert!(
247            message.contains("Policy::with_external_decoders_resolved_by"),
248            "the message does not name the setting that would allow this: {message}"
249        );
250    }
251
252    #[test]
253    fn a_host_that_opted_in_gets_the_module_its_resolver_found() {
254        let bytes = external();
255        let container = Container::parse(&bytes).expect("the container parses");
256        let policy = Policy::with_external_decoders_resolved_by(Holding(Some(MODULE.to_vec())));
257
258        let verified = policy
259            .decoder(&container)
260            .expect("the resolver returned the module the container names");
261        assert_eq!(verified.module(), MODULE);
262        assert_eq!(verified.digest(), Digest::of(MODULE));
263    }
264
265    #[test]
266    fn a_resolver_that_returns_the_wrong_module_is_caught_by_the_digest() {
267        let bytes = external();
268        let container = Container::parse(&bytes).expect("the container parses");
269        let policy = Policy::with_external_decoders_resolved_by(Holding(Some(
270            b"some other module entirely".to_vec(),
271        )));
272
273        let Err(Untrusted::Digest { expected, found }) = policy.decoder(&container) else {
274            panic!("a fetched module nobody checked was accepted");
275        };
276        assert_eq!(expected, Digest::of(MODULE));
277        assert_ne!(found, expected);
278    }
279
280    #[test]
281    fn a_resolver_that_finds_nothing_is_not_an_attack() {
282        let bytes = external();
283        let container = Container::parse(&bytes).expect("the container parses");
284        let policy = Policy::with_external_decoders_resolved_by(Holding(None));
285
286        assert_eq!(
287            policy.decoder(&container).unwrap_err(),
288            Untrusted::Unresolved {
289                name: "elsewhere".to_owned(),
290                digest: Digest::of(MODULE),
291            }
292        );
293    }
294}