sbat/
image.rs

1// Copyright 2023 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! SBAT metadata associated with an executable.
10//!
11//! Typically this data is read from the `.sbat` section of a UEFI PE
12//! executable. See the crate documentation for details of how it is
13//! used.
14
15use crate::csv::{CsvIter, Record, trim_ascii_at_null};
16use crate::{Component, ParseError};
17use ascii::AsciiStr;
18use core::ptr;
19
20/// Standard PE section name for SBAT metadata.
21pub const SBAT_SECTION_NAME: &str = ".sbat";
22
23/// Vendor data. This is optional human-readable data that is not used
24/// for SBAT comparison.
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub struct Vendor<'a> {
27    /// Human-readable vendor name.
28    pub name: Option<&'a AsciiStr>,
29
30    /// Human-readable package name.
31    pub package_name: Option<&'a AsciiStr>,
32
33    /// Human-readable package version.
34    pub version: Option<&'a AsciiStr>,
35
36    /// Url to look stuff up, contact, etc.
37    pub url: Option<&'a AsciiStr>,
38}
39
40/// Entry in image SBAT metadata. This contains a [`Component`], which
41/// is what gets used for revocation comparisons, as well as [`Vendor`]
42/// data, which is extra data that serves as a human-readable comment.
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
44pub struct Entry<'a> {
45    /// Component data. This is used for SBAT comparison.
46    pub component: Component<'a>,
47
48    /// Vendor data. This is human-readable and not used for SBAT
49    /// comparison.
50    pub vendor: Vendor<'a>,
51}
52
53const NUM_ENTRY_FIELDS: usize = 6;
54
55impl<'a> Entry<'a> {
56    /// Make a new `Entry`.
57    #[must_use]
58    pub fn new(component: Component<'a>, vendor: Vendor<'a>) -> Entry<'a> {
59        Entry { component, vendor }
60    }
61
62    /// Parse an `Entry` from a `Record`.
63    fn from_record(
64        record: &Record<'a, NUM_ENTRY_FIELDS>,
65    ) -> Result<Self, ParseError> {
66        Ok(Self::new(
67            Component::from_record(record)?,
68            Vendor {
69                name: record.get_field(2),
70                package_name: record.get_field(3),
71                version: record.get_field(4),
72                url: record.get_field(5),
73            },
74        ))
75    }
76}
77
78/// Iterator over entries in [`ImageSbat`].
79///
80/// See [`ImageSbat::entries`].
81pub struct Entries<'a>(CsvIter<'a, NUM_ENTRY_FIELDS>);
82
83impl<'a> Iterator for Entries<'a> {
84    type Item = Entry<'a>;
85
86    fn next(&mut self) -> Option<Self::Item> {
87        let next = self.0.next()?;
88
89        // These unwraps will always succeed, because the validity of
90        // the data was already checked in ImageSbat::parse.
91        let record = next.unwrap();
92        Some(Entry::from_record(&record).unwrap())
93    }
94}
95
96/// Image SBAT metadata.
97///
98/// Typically this data comes from the `.sbat` section of a UEFI PE
99/// executable.
100#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
101#[repr(transparent)]
102pub struct ImageSbat(AsciiStr);
103
104impl ImageSbat {
105    /// Parse SBAT metadata from raw CSV. This data typically comes from
106    /// the `.sbat` section of a UEFI PE executable. Each record is
107    /// parsed as an [`Entry`].
108    ///
109    /// Any data past the first null in `input` is ignored. A null byte
110    /// is not required to be present.
111    pub fn parse(input: &[u8]) -> Result<&Self, ParseError> {
112        let input = trim_ascii_at_null(input)?;
113
114        // Ensure that all entries are valid.
115        let iter = CsvIter::<NUM_ENTRY_FIELDS>::new(input);
116        for record in iter {
117            let record = record?;
118            // Check that the first two fields are valid. The other
119            // fields are optional.
120            Component::from_record(&record)?;
121        }
122
123        Ok(Self::from_ascii_str_unchecked(input))
124    }
125
126    /// Internal method to create `&Self` from `&AsciiStr`. This is
127    /// essentially a cast, it does not check the validity of the
128    /// data. It is only used in the deref implementation for
129    /// `ImageSbatOwned`. Note that although unchecked, this method is
130    /// not unsafe; invalid data passed in could lead to a panic, but no
131    /// UB.
132    #[allow(unsafe_code)]
133    pub(crate) fn from_ascii_str_unchecked(s: &AsciiStr) -> &Self {
134        // SAFETY: `Self` is a `repr(transparent)` wrapper around
135        // `AsciiStr`, so the types are compatible.
136        unsafe { &*(ptr::from_ref(s) as *const Self) }
137    }
138
139    /// Get the underlying ASCII CSV string.
140    #[must_use]
141    pub fn as_csv(&self) -> &AsciiStr {
142        &self.0
143    }
144
145    /// Get an iterator over the entries.
146    #[must_use]
147    pub fn entries(&self) -> Entries<'_> {
148        Entries(CsvIter::new(&self.0))
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::Generation;
156
157    #[cfg(feature = "alloc")]
158    use crate::ImageSbatOwned;
159
160    const VALID_SBAT: &[u8] = b"sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
161shim,1,UEFI shim,shim,1,https://github.com/rhboot/shim";
162
163    fn parse_success_helper(image_sbat: &ImageSbat) {
164        let ascii = |s| AsciiStr::from_ascii(s).unwrap();
165
166        assert_eq!(
167            image_sbat.entries().collect::<Vec<_>>(),
168            [
169                Entry::new(
170                    Component {
171                        name: ascii("sbat"),
172                        generation: Generation::new(1).unwrap(),
173                    },
174                    Vendor {
175                        name: Some(ascii("SBAT Version")),
176                        package_name: Some(ascii("sbat")),
177                        version: Some(ascii("1")),
178                        url: Some(ascii(
179                            "https://github.com/rhboot/shim/blob/main/SBAT.md"
180                        )),
181                    },
182                ),
183                Entry::new(
184                    Component {
185                        name: ascii("shim"),
186                        generation: Generation::new(1).unwrap(),
187                    },
188                    Vendor {
189                        name: Some(ascii("UEFI shim")),
190                        package_name: Some(ascii("shim")),
191                        version: Some(ascii("1")),
192                        url: Some(ascii("https://github.com/rhboot/shim")),
193                    }
194                )
195            ]
196        );
197    }
198
199    #[test]
200    fn parse_success_array() {
201        parse_success_helper(ImageSbat::parse(VALID_SBAT).unwrap());
202    }
203
204    #[cfg(feature = "alloc")]
205    #[test]
206    fn parse_success_vec() {
207        parse_success_helper(&ImageSbatOwned::parse(VALID_SBAT).unwrap());
208    }
209
210    #[test]
211    fn invalid_record_array() {
212        assert_eq!(ImageSbat::parse(b"a"), Err(ParseError::TooFewFields));
213    }
214
215    #[cfg(feature = "alloc")]
216    #[test]
217    fn invalid_record_vec() {
218        assert_eq!(ImageSbatOwned::parse(b"a"), Err(ParseError::TooFewFields));
219    }
220}