Skip to main content

rustls_util/
key_log_file.rs

1use core::fmt::{Debug, Formatter};
2use std::env::var_os;
3use std::ffi::OsString;
4use std::fs::{File, OpenOptions};
5use std::io;
6use std::io::Write;
7use std::sync::Mutex;
8
9#[cfg(feature = "log")]
10use log::warn;
11use rustls::KeyLog;
12
13// Internal mutable state for KeyLogFile
14struct KeyLogFileInner {
15    file: Option<File>,
16    buf: Vec<u8>,
17}
18
19impl KeyLogFileInner {
20    fn new(var: Option<OsString>) -> Self {
21        let Some(path) = &var else {
22            return Self {
23                file: None,
24                buf: Vec::new(),
25            };
26        };
27
28        #[cfg_attr(not(feature = "log"), expect(clippy::manual_ok_err))]
29        let file = match OpenOptions::new()
30            .append(true)
31            .create(true)
32            .open(path)
33        {
34            Ok(f) => Some(f),
35            #[cfg_attr(not(feature = "log"), expect(unused_variables))]
36            Err(e) => {
37                #[cfg(feature = "log")]
38                warn!("unable to create key log file {path:?}: {e}");
39                None
40            }
41        };
42
43        Self {
44            file,
45            buf: Vec::new(),
46        }
47    }
48
49    fn try_write(&mut self, label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<()> {
50        let Some(file) = &mut self.file else {
51            return Ok(());
52        };
53
54        self.buf.clear();
55        write!(self.buf, "{label} ")?;
56        for b in client_random.iter() {
57            write!(self.buf, "{b:02x}")?;
58        }
59        write!(self.buf, " ")?;
60        for b in secret.iter() {
61            write!(self.buf, "{b:02x}")?;
62        }
63        writeln!(self.buf)?;
64        file.write_all(&self.buf)
65    }
66}
67
68impl Debug for KeyLogFileInner {
69    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
70        f.debug_struct("KeyLogFileInner")
71            // Note: we omit self.buf deliberately as it may contain key data.
72            .field("file", &self.file)
73            .finish_non_exhaustive()
74    }
75}
76
77/// [`KeyLog`] implementation that opens a file whose name is
78/// given by the `SSLKEYLOGFILE` environment variable, and writes
79/// keys into it.
80///
81/// If `SSLKEYLOGFILE` is not set, this does nothing.
82///
83/// If such a file cannot be opened, or cannot be written then
84/// this does nothing but logs errors at warning-level.
85pub struct KeyLogFile(Mutex<KeyLogFileInner>);
86
87impl KeyLogFile {
88    /// Makes a new `KeyLogFile`.  The environment variable is
89    /// inspected and the named file is opened during this call.
90    pub fn new() -> Self {
91        let var = var_os("SSLKEYLOGFILE");
92        Self(Mutex::new(KeyLogFileInner::new(var)))
93    }
94}
95
96impl KeyLog for KeyLogFile {
97    fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
98        match self
99            .0
100            .lock()
101            .unwrap()
102            .try_write(label, client_random, secret)
103        {
104            Ok(()) => {}
105            #[cfg_attr(not(feature = "log"), expect(unused_variables))]
106            Err(e) => {
107                #[cfg(feature = "log")]
108                warn!("error writing to key log file: {e}");
109            }
110        }
111    }
112}
113
114impl Debug for KeyLogFile {
115    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
116        match self.0.try_lock() {
117            Ok(key_log_file) => write!(f, "{key_log_file:?}"),
118            Err(_) => write!(f, "KeyLogFile {{ <locked> }}"),
119        }
120    }
121}
122
123#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
124mod tests {
125    use super::*;
126
127    fn init() {
128        let _ = env_logger::builder()
129            .is_test(true)
130            .try_init();
131    }
132
133    #[test]
134    fn test_env_var_is_not_set() {
135        init();
136        let mut inner = KeyLogFileInner::new(None);
137        assert!(
138            inner
139                .try_write("label", b"random", b"secret")
140                .is_ok()
141        );
142    }
143
144    #[test]
145    fn test_env_var_cannot_be_opened() {
146        init();
147        let mut inner = KeyLogFileInner::new(Some("/dev/does-not-exist".into()));
148        assert!(
149            inner
150                .try_write("label", b"random", b"secret")
151                .is_ok()
152        );
153    }
154
155    #[test]
156    fn test_env_var_cannot_be_written() {
157        init();
158
159        #[cfg(target_os = "linux")]
160        const UNWRITABLE_FILE: &str = "/dev/full";
161
162        #[cfg(target_os = "macos")]
163        const UNWRITABLE_FILE: &str = "/dev/urandom";
164
165        let mut inner = KeyLogFileInner::new(Some(UNWRITABLE_FILE.into()));
166        assert!(
167            inner
168                .try_write("label", b"random", b"secret")
169                .is_err()
170        );
171    }
172}