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    error::Error,
28    fmt::{self, Display},
29    fs::{create_dir, read_to_string, File},
30    hash::{DefaultHasher, Hash, Hasher},
31    io::{ErrorKind, Read, Write},
32    num::ParseIntError,
33    path::PathBuf,
34};
35
36/// The path of the configfs-tsm interface
37const CONFIGFS_TSM_PATH: &str = "/sys/kernel/config/tsm/report";
38
39/// Create a quote with given input, using the input data as quote directory name
40pub fn create_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
41    let quote_name = create_quote_name(&input);
42    let mut quote = OpenQuote::new(&quote_name)?;
43    quote.write_input(input)?;
44    quote.read_output()
45}
46
47/// Same as create_quote, but check that the provider (the TEE platform) matches one of a given set
48/// of values
49pub fn create_quote_with_providers(
50    input: [u8; 64],
51    accepted_providers: Vec<&str>,
52) -> Result<Vec<u8>, QuoteGenerationError> {
53    let quote_name = create_quote_name(&input);
54    let mut quote = OpenQuote::new(&quote_name)?;
55    quote.check_provider(accepted_providers)?;
56    quote.write_input(input)?;
57    quote.read_output()
58}
59
60/// Convenience function for creating a quote and checking the provider is tdx_guest
61pub fn create_tdx_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
62    create_quote_with_providers(input, vec!["tdx_guest"])
63}
64
65/// Represents a pending quote
66pub struct OpenQuote {
67    /// The path of the quote files
68    path: PathBuf,
69    /// What generation number we expect the quote to have when reading.
70    /// This is used to detect conflicts when another process modifies the quote.
71    expected_generation: u32,
72}
73
74impl OpenQuote {
75    /// Create a new open quote
76    pub fn new(quote_name: &str) -> Result<Self, QuoteGenerationError> {
77        let mut quote_path = PathBuf::from(CONFIGFS_TSM_PATH);
78        quote_path.push(quote_name);
79
80        if let Err(error) = create_dir(quote_path.clone()) {
81            match error.kind() {
82                // If a quote with the same name has already been made, we ignore the error as we can still
83                // re-use the quote
84                ErrorKind::AlreadyExists => {}
85                ErrorKind::NotFound => return Err(QuoteGenerationError::CannotFindTsmDir),
86                _ => return Err(QuoteGenerationError::IO(error)),
87            }
88        }
89
90        Ok(Self {
91            path: quote_path,
92            expected_generation: 0,
93        })
94    }
95
96    /// Write input data to quote
97    pub fn write_input(&mut self, input: [u8; 64]) -> Result<(), QuoteGenerationError> {
98        self.update_generation()?;
99        let mut inblob_path = self.path.clone();
100        inblob_path.push("inblob");
101        let mut inblob_file = File::create(inblob_path)?;
102        inblob_file.write_all(&input)?;
103
104        self.expected_generation += 1;
105        Ok(())
106    }
107
108    /// Generate the quote
109    pub fn read_output(&self) -> Result<Vec<u8>, QuoteGenerationError> {
110        let mut outblob_path = self.path.clone();
111        outblob_path.push("outblob");
112        let mut outblob_file = File::open(outblob_path)?;
113        let mut output = Vec::new();
114        outblob_file.read_to_end(&mut output)?;
115
116        let actual = self.read_generation()?;
117        if self.expected_generation != actual {
118            return Err(QuoteGenerationError::Generation(
119                self.expected_generation,
120                actual,
121            ));
122        }
123
124        if output.is_empty() {
125            return Err(QuoteGenerationError::EmptyQuote);
126        }
127        Ok(output)
128    }
129
130    /// Read the current generation number
131    pub fn read_generation(&self) -> Result<u32, QuoteGenerationError> {
132        let mut generation_path = self.path.clone();
133        generation_path.push("generation");
134        let mut current_generation = read_to_string(generation_path)?;
135        trim_newline(&mut current_generation);
136        Ok(current_generation.parse()?)
137    }
138
139    /// Check that the provider matches given accepted values
140    pub fn check_provider(&self, accepted_values: Vec<&str>) -> Result<(), QuoteGenerationError> {
141        let mut provider_path = self.path.clone();
142        provider_path.push("provider");
143        let mut provider = read_to_string(provider_path)?;
144        trim_newline(&mut provider);
145        if !accepted_values.contains(&provider.as_str()) {
146            return Err(QuoteGenerationError::BadProvider(provider));
147        }
148        Ok(())
149    }
150
151    /// Update the expected generation number
152    fn update_generation(&mut self) -> Result<(), QuoteGenerationError> {
153        self.expected_generation = self.read_generation()?;
154        Ok(())
155    }
156}
157
158/// Derive a name for the quote directory from the input data by hashing and encoding as hex
159fn create_quote_name(input: &[u8]) -> String {
160    let mut s = DefaultHasher::new();
161    input.hash(&mut s);
162    let hash_bytes = s.finish().to_le_bytes();
163    bytes_to_hex(&hash_bytes)
164}
165
166fn bytes_to_hex(input: &[u8]) -> String {
167    input
168        .iter()
169        .map(|b| format!("{:02x}", b).to_string())
170        .collect::<Vec<String>>()
171        .join("")
172}
173
174/// Remove a trailing newline character from a given string if present
175fn trim_newline(input: &mut String) {
176    if input.ends_with('\n') {
177        input.pop();
178        if input.ends_with('\r') {
179            input.pop();
180        }
181    }
182}
183
184/// An error when parsing a quote
185#[derive(Debug)]
186pub enum QuoteGenerationError {
187    Generation(u32, u32),
188    IO(std::io::Error),
189    ParseInt,
190    BadProvider(String),
191    CannotFindTsmDir,
192    EmptyQuote,
193}
194
195impl Display for QuoteGenerationError {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match self {
198            QuoteGenerationError::Generation(expected, actual) => f.write_str(&format!(
199                "Wrong generation number - possible conflict. Expected: {} Actual: {}",
200                expected, actual
201            )),
202            QuoteGenerationError::IO(error) => f.write_str(&error.to_string()),
203            QuoteGenerationError::ParseInt => {
204                f.write_str("Could not parse integer when reading generation value")
205            }
206            QuoteGenerationError::BadProvider(provider) => f.write_str(&format!(
207                "Quote has provider which is not allowed: {}",
208                provider
209            )),
210            QuoteGenerationError::CannotFindTsmDir => f.write_str(
211                "Cannot find configfs-tsm directory - maybe your hardware does not support it",
212            ),
213            QuoteGenerationError::EmptyQuote => f.write_str("Empty quote. This could be an authorization issue with the quote generation socket."),
214        }
215    }
216}
217
218impl Error for QuoteGenerationError {
219    fn source(&self) -> Option<&(dyn Error + 'static)> {
220        match self {
221            Self::IO(e) => Some(e),
222            _ => None,
223        }
224    }
225}
226
227impl From<std::io::Error> for QuoteGenerationError {
228    fn from(error: std::io::Error) -> QuoteGenerationError {
229        QuoteGenerationError::IO(error)
230    }
231}
232
233impl From<ParseIntError> for QuoteGenerationError {
234    fn from(_: ParseIntError) -> QuoteGenerationError {
235        QuoteGenerationError::ParseInt
236    }
237}