Skip to content

Commit 03db4ba

Browse files
committed
Add support for keylogging.
1 parent 4ec591a commit 03db4ba

File tree

3 files changed

+195
-7
lines changed

3 files changed

+195
-7
lines changed

src/keylog.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use std::env;
2+
use std::fs::{File, OpenOptions};
3+
use std::io;
4+
use std::io::Write;
5+
use std::path::Path;
6+
use std::sync::Mutex;
7+
8+
/// This trait represents the ability to do something useful
9+
/// with key material, such as logging it to a file for debugging.
10+
///
11+
/// Naturally, secrets passed over the interface are *extremely*
12+
/// sensitive and can break the security of past, present and
13+
/// future sessions.
14+
///
15+
/// You'll likely want some interior mutability in your
16+
/// implementation to make this useful.
17+
///
18+
/// See `KeyLogFile` that implements the standard `SSLKEYLOGFILE`
19+
/// environment variable behaviour.
20+
pub trait KeyLog: Send + Sync {
21+
/// Log the given `secret`. `client_random` is provided for
22+
/// session identification. `label` describes precisely what
23+
/// `secret` means:
24+
///
25+
/// - `CLIENT_RANDOM`: `secret` is the master secret for a TLSv1.2 session.
26+
/// - `CLIENT_EARLY_TRAFFIC_SECRET`: `secret` encrypts early data
27+
/// transmitted by a client
28+
/// - `SERVER_HANDSHAKE_TRAFFIC_SECRET`: `secret` encrypts
29+
/// handshake messages from the server during a TLSv1.3 handshake.
30+
/// - `CLIENT_HANDSHAKE_TRAFFIC_SECRET`: `secret` encrypts
31+
/// handshake messages from the client during a TLSv1.3 handshake.
32+
/// - `SERVER_TRAFFIC_SECRET_0`: `secret` encrypts post-handshake data
33+
/// from the server in a TLSv1.3 session.
34+
/// - `CLIENT_TRAFFIC_SECRET_0`: `secret` encrypts post-handshake data
35+
/// from the client in a TLSv1.3 session.
36+
/// - `EXPORTER_SECRET`: `secret` is the post-handshake exporter secret
37+
/// in a TLSv1.3 session.
38+
///
39+
/// These strings are selected to match the NSS key log format:
40+
/// https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
41+
fn log(&self, label: &str, client_random: &[u8], secret: &[u8]);
42+
}
43+
44+
/// `KeyLog` implementation that opens a file whose name is
45+
/// given by the `SSLKEYLOGFILE` environment variable, and writes
46+
/// keys into it.
47+
///
48+
/// If `SSLKEYLOGFILE` is not set, this does nothing.
49+
///
50+
/// If such a file cannot be opened, or cannot be written then
51+
/// this does nothing but logs errors at warning-level.
52+
pub struct KeyLogFile(Mutex<KeyLogFileInner>);
53+
54+
impl KeyLogFile {
55+
/// Makes a new `KeyLogFile`. The environment variable is
56+
/// inspected and the named file is opened during this call.
57+
pub fn new() -> Self {
58+
let var = env::var("SSLKEYLOGFILE");
59+
KeyLogFile(Mutex::new(KeyLogFileInner::new(var)))
60+
}
61+
}
62+
63+
impl KeyLog for KeyLogFile {
64+
fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
65+
match self
66+
.0
67+
.lock()
68+
.unwrap()
69+
.try_write(label, client_random, secret)
70+
{
71+
Ok(()) => {}
72+
Err(e) => {
73+
tracing::warn!("error writing to key log file: {}", e);
74+
}
75+
}
76+
}
77+
}
78+
79+
// Internal mutable state for KeyLogFile
80+
struct KeyLogFileInner {
81+
file: Option<File>,
82+
buf: Vec<u8>,
83+
}
84+
85+
impl KeyLogFileInner {
86+
fn new(var: Result<String, env::VarError>) -> Self {
87+
let path = match var {
88+
Ok(ref s) => Path::new(s),
89+
Err(env::VarError::NotUnicode(ref s)) => Path::new(s),
90+
Err(env::VarError::NotPresent) => {
91+
return KeyLogFileInner {
92+
file: None,
93+
buf: Vec::new(),
94+
};
95+
}
96+
};
97+
98+
let file = match OpenOptions::new().append(true).create(true).open(path) {
99+
Ok(f) => Some(f),
100+
Err(e) => {
101+
tracing::warn!("unable to create key log file {:?}: {}", path, e);
102+
None
103+
}
104+
};
105+
106+
KeyLogFileInner {
107+
file,
108+
buf: Vec::new(),
109+
}
110+
}
111+
112+
fn try_write(&mut self, label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<()> {
113+
let mut file = match self.file {
114+
None => {
115+
return Ok(());
116+
}
117+
Some(ref f) => f,
118+
};
119+
120+
self.buf.truncate(0);
121+
write!(self.buf, "{} ", label)?;
122+
for b in client_random.iter() {
123+
write!(self.buf, "{:02x}", b)?;
124+
}
125+
write!(self.buf, " ")?;
126+
for b in secret.iter() {
127+
write!(self.buf, "{:02x}", b)?;
128+
}
129+
writeln!(self.buf)?;
130+
file.write_all(&self.buf)
131+
}
132+
}
133+
134+
#[cfg(all(test, target_os = "linux"))]
135+
mod test {
136+
use super::*;
137+
138+
#[test]
139+
fn test_env_var_is_not_unicode() {
140+
let mut inner = KeyLogFileInner::new(Err(env::VarError::NotUnicode(
141+
"/tmp/keylogfileinnertest".into(),
142+
)));
143+
assert!(inner.try_write("label", b"random", b"secret").is_ok());
144+
}
145+
146+
#[test]
147+
fn test_env_var_is_not_set() {
148+
let mut inner = KeyLogFileInner::new(Err(env::VarError::NotPresent));
149+
assert!(inner.try_write("label", b"random", b"secret").is_ok());
150+
}
151+
152+
#[test]
153+
fn test_env_var_cannot_be_opened() {
154+
let mut inner = KeyLogFileInner::new(Ok("/dev/does-not-exist".into()));
155+
assert!(inner.try_write("label", b"random", b"secret").is_ok());
156+
}
157+
158+
#[test]
159+
fn test_env_var_cannot_be_written() {
160+
let mut inner = KeyLogFileInner::new(Ok("/dev/full".into()));
161+
assert!(inner.try_write("label", b"random", b"secret").is_err());
162+
}
163+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
mod aead;
22
mod dh;
3+
mod keylog;
34
mod session;
45

6+
pub use crate::aead::ChaCha8PacketKey;
7+
pub use crate::keylog::{KeyLog, KeyLogFile};
58
pub use crate::session::{NoiseConfig, NoiseSession};
69
pub use ed25519_dalek::{Keypair, PublicKey};
710

src/session.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::aead::{ChaCha8PacketKey, PlaintextHeaderKey, HEADER_KEYPAIR};
22
use crate::dh::DiffieHellman;
3+
use crate::keylog::KeyLog;
34
use ed25519_dalek::{Keypair, PublicKey};
45
use quinn_proto::crypto::{
56
ClientConfig, ExportKeyingMaterialError, KeyPair, Keys, ServerConfig, Session,
@@ -12,11 +13,17 @@ use std::sync::Arc;
1213
use subtle::ConstantTimeEq;
1314
use xoodoo::Xoodyak;
1415

16+
/// Noise configuration struct.
1517
#[derive(Default)]
1618
pub struct NoiseConfig {
19+
/// Keypair to use. If none is provided one will be generated.
1720
pub keypair: Option<Keypair>,
21+
/// The remote public key. This needs to be set.
1822
pub remote_public_key: Option<PublicKey>,
23+
/// Optional private shared key usable as a password for private networks.
1924
pub psk: Option<[u8; 32]>,
25+
/// Enables keylogging for debugging purposes to the path provided by `SSLKEYLOGFILE`.
26+
pub keylogger: Option<Arc<dyn KeyLog>>,
2027
}
2128

2229
impl Clone for NoiseConfig {
@@ -29,6 +36,7 @@ impl Clone for NoiseConfig {
2936
keypair,
3037
remote_public_key: self.remote_public_key,
3138
psk: self.psk,
39+
keylogger: self.keylogger.clone(),
3240
}
3341
}
3442
}
@@ -59,12 +67,13 @@ impl ServerConfig<NoiseSession> for Arc<NoiseConfig> {
5967

6068
impl NoiseConfig {
6169
fn start_session(&self, side: Side, params: &TransportParameters) -> NoiseSession {
70+
let mut rng = rand_core::OsRng {};
6271
let s = if let Some(keypair) = self.keypair.as_ref() {
6372
Keypair::from_bytes(&keypair.to_bytes()).unwrap()
6473
} else {
65-
Keypair::generate(&mut rand_core::OsRng {})
74+
Keypair::generate(&mut rng)
6675
};
67-
let e = Keypair::generate(&mut rand_core::OsRng {});
76+
let e = Keypair::generate(&mut rng);
6877
NoiseSession {
6978
xoodyak: Xoodyak::hash(),
7079
state: State::Initial,
@@ -77,6 +86,7 @@ impl NoiseConfig {
7786
remote_e: None,
7887
remote_s: self.remote_public_key,
7988
zero_rtt_key: None,
89+
keylogger: self.keylogger.clone(),
8090
}
8191
}
8292
}
@@ -93,6 +103,16 @@ pub struct NoiseSession {
93103
remote_e: Option<PublicKey>,
94104
remote_s: Option<PublicKey>,
95105
zero_rtt_key: Option<ChaCha8PacketKey>,
106+
keylogger: Option<Arc<dyn KeyLog>>,
107+
}
108+
109+
impl NoiseSession {
110+
fn conn_id(&self) -> Option<&[u8; 32]> {
111+
match self.side {
112+
Side::Client => Some(self.e.public.as_bytes()),
113+
Side::Server => Some(self.remote_e.as_ref()?.as_bytes()),
114+
}
115+
}
96116
}
97117

98118
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -138,9 +158,13 @@ impl Session for NoiseSession {
138158
}
139159
let mut client = [0; 32];
140160
self.xoodyak.squeeze_key(&mut client);
141-
let client = ChaCha8PacketKey::new(client);
142161
let mut server = [0; 32];
143162
self.xoodyak.squeeze_key(&mut server);
163+
if let Some(keylogger) = self.keylogger.as_ref() {
164+
keylogger.log("CLIENT_KEY", self.conn_id().unwrap(), &client[..]);
165+
keylogger.log("SERVER_KEY", self.conn_id().unwrap(), &server[..]);
166+
}
167+
let client = ChaCha8PacketKey::new(client);
144168
let server = ChaCha8PacketKey::new(server);
145169
let key = match self.side {
146170
Side::Client => KeyPair {
@@ -261,8 +285,7 @@ impl Session for NoiseSession {
261285
self.xoodyak.absorb(&self.psk);
262286
let mut transport_parameters = vec![];
263287
self.transport_parameters.write(&mut transport_parameters);
264-
self.xoodyak
265-
.encrypt_in_place(&mut transport_parameters);
288+
self.xoodyak.encrypt_in_place(&mut transport_parameters);
266289
handshake.extend_from_slice(&transport_parameters);
267290
let mut tag = [0; 16];
268291
self.xoodyak.squeeze(&mut tag);
@@ -289,8 +312,7 @@ impl Session for NoiseSession {
289312
self.xoodyak.absorb(&se);
290313
let mut transport_parameters = vec![];
291314
self.transport_parameters.write(&mut transport_parameters);
292-
self.xoodyak
293-
.encrypt_in_place(&mut transport_parameters);
315+
self.xoodyak.encrypt_in_place(&mut transport_parameters);
294316
handshake.extend_from_slice(&transport_parameters);
295317
let mut tag = [0; 16];
296318
self.xoodyak.squeeze(&mut tag);

0 commit comments

Comments
 (0)