configfs_tsm/
lib.rs

1// Copyright (C) 2023 Entropy Cryptography Inc.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! This can generate quotes for remote attestation on confidential computing platforms using Linux's
17//! [configfs-tsm](https://www.kernel.org/doc/Documentation/ABI/testing/configfs-tsm) filesystem
18//! interface.
19//!
20//! This is designed for and tested with Intel TDX, but since the `configfs-tsm` is a platform-agnostic
21//! interface, this could potentially work with other platforms such as Intel SGX, or AMD SEV.
22//!
23//! This crate has no dependencies and generates quotes only by reading and writing local files.
24//!
25//! Warning: This crate is in early stages of development and has not been audited
26use std::{
27    fmt::{self, Display},
28    fs::{create_dir, read_to_string, File},
29    hash::{DefaultHasher, Hash, Hasher},
30    io::{ErrorKind, Read, Write},
31    num::ParseIntError,
32    path::PathBuf,
33};
34
35/// The path of the configfs-tsm interface
36const CONFIGFS_TSM_PATH: &str = "/sys/kernel/config/tsm/report";
37
38/// Create a quote with given input, using the input data as quote directory name
39pub fn create_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
40    let quote_name = create_quote_name(&input);
41    let mut quote = OpenQuote::new(&quote_name)?;
42    quote.write_input(input)?;
43    quote.read_output()
44}
45
46/// Same as create_quote, but check that the provider (the TEE platform) matches one of a given set
47/// of values
48pub fn create_quote_with_providers(
49    input: [u8; 64],
50    accepted_providers: Vec<&str>,
51) -> Result<Vec<u8>, QuoteGenerationError> {
52    let quote_name = create_quote_name(&input);
53    let mut quote = OpenQuote::new(&quote_name)?;
54    quote.check_provider(accepted_providers)?;
55    quote.write_input(input)?;
56    quote.read_output()
57}
58
59/// Convenience function for creating a quote and checking the provider is tdx_guest
60pub fn create_tdx_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
61    create_quote_with_providers(input, vec!["tdx_guest"])
62}
63
64/// Represents a pending quote
65pub struct OpenQuote {
66    /// The path of the quote files
67    path: PathBuf,
68    /// What generation number we expect the quote to have when reading.
69    /// This is used to detect conflicts when another process modifies the quote.
70    expected_generation: u32,
71}
72
73impl OpenQuote {
74    /// Create a new open quote
75    pub fn new(quote_name: &str) -> Result<Self, QuoteGenerationError> {
76        let mut quote_path = PathBuf::from(CONFIGFS_TSM_PATH);
77        quote_path.push(quote_name);
78
79        if let Err(error) = create_dir(quote_path.clone()) {
80            match error.kind() {
81                // If a quote with the same name has already been made, we ignore the error as we can still
82                // re-use the quote
83                ErrorKind::AlreadyExists => {}
84                ErrorKind::NotFound => return Err(QuoteGenerationError::CannotFindTsmDir),
85                _ => return Err(QuoteGenerationError::IO(error)),
86            }
87        }
88
89        Ok(Self {
90            path: quote_path,
91            expected_generation: 0,
92        })
93    }
94
95    /// Write input data to quote
96    pub fn write_input(&mut self, input: [u8; 64]) -> Result<(), QuoteGenerationError> {
97        self.update_generation()?;
98        let mut inblob_path = self.path.clone();
99        inblob_path.push("inblob");
100        let mut inblob_file = File::create(inblob_path)?;
101        inblob_file.write_all(&input)?;
102
103        self.expected_generation += 1;
104        Ok(())
105    }
106
107    /// Generate the quote
108    pub fn read_output(&self) -> Result<Vec<u8>, QuoteGenerationError> {
109        let mut outblob_path = self.path.clone();
110        outblob_path.push("outblob");
111        let mut outblob_file = File::open(outblob_path)?;
112        let mut output = Vec::new();
113        outblob_file.read_to_end(&mut output)?;
114
115        let actual = self.read_generation()?;
116        if self.expected_generation != actual {
117            return Err(QuoteGenerationError::Generation(
118                self.expected_generation,
119                actual,
120            ));
121        }
122        Ok(output)
123    }
124
125    /// Read the current generation number
126    pub fn read_generation(&self) -> Result<u32, QuoteGenerationError> {
127        let mut generation_path = self.path.clone();
128        generation_path.push("generation");
129        let mut current_generation = read_to_string(generation_path)?;
130        trim_newline(&mut current_generation);
131        Ok(current_generation.parse()?)
132    }
133
134    /// Check that the provider matches given accepted values
135    pub fn check_provider(&self, accepted_values: Vec<&str>) -> Result<(), QuoteGenerationError> {
136        let mut provider_path = self.path.clone();
137        provider_path.push("provider");
138        let mut provider = read_to_string(provider_path)?;
139        trim_newline(&mut provider);
140        if !accepted_values.contains(&provider.as_str()) {
141            return Err(QuoteGenerationError::BadProvider(provider));
142        }
143        Ok(())
144    }
145
146    /// Update the expected generation number
147    fn update_generation(&mut self) -> Result<(), QuoteGenerationError> {
148        self.expected_generation = self.read_generation()?;
149        Ok(())
150    }
151}
152
153/// Derive a name for the quote directory from the input data by hashing and encoding as hex
154fn create_quote_name(input: &[u8]) -> String {
155    let mut s = DefaultHasher::new();
156    input.hash(&mut s);
157    let hash_bytes = s.finish().to_le_bytes();
158    bytes_to_hex(&hash_bytes)
159}
160
161fn bytes_to_hex(input: &[u8]) -> String {
162    input
163        .iter()
164        .map(|b| format!("{:02x}", b).to_string())
165        .collect::<Vec<String>>()
166        .join("")
167}
168
169/// Remove a trailing newline character from a given string if present
170fn trim_newline(input: &mut String) {
171    if input.ends_with('\n') {
172        input.pop();
173        if input.ends_with('\r') {
174            input.pop();
175        }
176    }
177}
178
179/// An error when parsing a quote
180#[derive(Debug)]
181pub enum QuoteGenerationError {
182    Generation(u32, u32),
183    IO(std::io::Error),
184    ParseInt,
185    BadProvider(String),
186    CannotFindTsmDir,
187}
188
189impl Display for QuoteGenerationError {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        match self {
192            QuoteGenerationError::Generation(expected, actual) => f.write_str(&format!(
193                "Wrong generation number - possible conflict. Expected: {} Actual: {}",
194                expected, actual
195            )),
196            QuoteGenerationError::IO(error) => f.write_str(&error.to_string()),
197            QuoteGenerationError::ParseInt => {
198                f.write_str("Could not parse integer when reading generation value")
199            }
200            QuoteGenerationError::BadProvider(provider) => f.write_str(&format!(
201                "Quote has provider which is not allowed: {}",
202                provider
203            )),
204            QuoteGenerationError::CannotFindTsmDir => f.write_str(
205                "Cannot find configfs-tsm directory - maybe your hardware does not support it",
206            ),
207        }
208    }
209}
210
211impl From<std::io::Error> for QuoteGenerationError {
212    fn from(error: std::io::Error) -> QuoteGenerationError {
213        QuoteGenerationError::IO(error)
214    }
215}
216
217impl From<ParseIntError> for QuoteGenerationError {
218    fn from(_: ParseIntError) -> QuoteGenerationError {
219        QuoteGenerationError::ParseInt
220    }
221}