File: secret-leak-detector.rs

package info (click to toggle)
rust-sequoia-openpgp 2.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 9,372 kB
  • sloc: sh: 6; makefile: 2
file content (214 lines) | stat: -rw-r--r-- 8,349 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! Scans a process memory for secret leaks.
//!
//! These tests attempt to detect secrets leaking into the stack and
//! heap without being zeroed after use.  To that end, we do the
//! following:
//!
//! 1. We break free(3) by using a custom allocator that leaks all
//!    heap allocations, so that memory will not be reused and we
//!    can robustly detect secret leaking into the heap because
//!    there is no risk of them being overwritten.
//!
//! 2. We do an operation involving secrets.  We use a fixed secret
//!    with a simple pattern (currently repeated '@'s) that is easy
//!    to find in memory later.
//!
//! 3. After the operation, we scan the processes memory (only
//!    readable and writable regions) for the secret.
//!
//! # Example of a test failure
//!
//! This shows the secret leaking into the stack:
//!
//! ```
//! test_ed25519: running test
//! [stack]: 139264 bytes
//! 7ffed1a8ee10  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a8ee20  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90080  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90090  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a900a0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a900e0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90110  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90120  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90130  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffed1a90170  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! test_ed25519: secret leaked
//! test leak_tests::test_ed25519 ... FAILED
//! ```
//!
//! # Debugging secret leaks
//!
//! To find what code leaks the secret, we use rr, the lightweight
//! recording & deterministic debugging tool.  Deterministic is key
//! here, we can see where the secret leaks to, and then replay the
//! execution and set a watchpoint on the address, and know that it
//! will leak to the exact same address again:
//!
//! ```sh
//! $ rr record target/debug/examples/secret-leak-detector test_ed25519
//! [stack]: 139264 bytes
//! 7ffc9119dab0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119dac0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ece0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ecf0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed00  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed40  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed70  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed80  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed90  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119edd0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! test_ed25519: secret leaked
//! $ rr replay -d rust-gdb
//! [...]
//! (rr) watch *0x7ffc9119dab0 if *0x7ffc9119dab0 == 0x40404040
//! Hardware watchpoint 1: *0x7ffc9119dab0
//! (rr) c
//! Continuing.
//! test_ed25519: running test
//!
//! Hardware watchpoint 1: *0x7ffc9119dab0
//!
//! Old value = 0
//! New value = 1077952576
//! 0x000055a463e40969 in sha2::sha512::x86::sha512_update_x_avx (x=0x7ffc9119eba0, k64=...)
//!     at src/sha512/x86.rs:260
//! 260                 let mut t2 = $SRL64(t0, 1);
//! (rr) c
//! Continuing.
//!
//! Hardware watchpoint 1: *0x7ffc9119dab0
//!
//! Old value = -997709592
//! New value = 1077952576
//! 0x000055a463e3bde0 in sha2::sha512::x86::load_data_avx (x=0x7ffc9119e200, ms=0x7ffc9119e180,
//!     data=0x7ffc911a1658) at src/sha512/x86.rs:89
//! 89          unrolled_iterations!(0, 1, 2, 3, 4, 5, 6, 7);
//! (rr) c
//! Continuing.
//! [stack]: 139264 bytes
//! 7ffc9119dab0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119dac0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ece0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ecf0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed00  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed40  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed70  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed80  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119ed90  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! 7ffc9119edd0  40 40 40 40 40 40 40 40  40 40 40 40 40 40 40 40   !!!!!!!!!!!!!!!!
//! test_ed25519: secret leaked
//!
//! Program received signal SIGKILL, Killed.
//! 0x0000000070000002 in syscall_traced ()
//! ```

#![allow(dead_code)]

use std::{
    env,
    io::{self, Write},
    path::{Path, PathBuf},
    process::Command,
    sync::OnceLock,
};
use anyhow::Result;

/// Locates the detector program.
///
/// The detector program is built as an example.  This has the
/// advantage that it is built using the same feature flags as the
/// test is invoked, and it is built with as little overhead as
/// possible.  The downside is that it may not have been built: if
/// cargo test --test secret-leak-detector is invoked to only run this
/// integration test, then the examples are not implicitly built
/// before running this test.
fn locate_detector() -> Option<&'static Path> {
    static NOFREE: OnceLock<Option<PathBuf>> = OnceLock::new();
    Some(NOFREE.get_or_init(|| -> Option<PathBuf> {
        let mut p = PathBuf::from(env::var_os("OUT_DIR")?);
        loop {
            let q = p.join("examples").join("secret-leak-detector");
            if q.exists() {
                break Some(q);
            }

            if let Some(parent) = p.parent() {
                p = parent.to_path_buf();
            } else {
                break None;
            }
        }
    }).as_ref()?.as_path())
}

/// Emits a message to stderr that is not captured by the test
/// framework, and returns success.
fn skip() -> Result<()> {
    // Write directly to stderr.  This way, we can emit the message
    // even though the test output is captured.
    writeln!(&mut io::stderr(),
             "Detector not built, skipping test: \
              run cargo build -p sequoia-openpgp \
              --example secret-leak-detector first")?;
    Ok(())
}

macro_rules! make_test {
    ($name: ident) => {
        #[cfg_attr(target_os = "linux", test)]
        fn $name() -> Result<()> {
            if let Some(d) = locate_detector() {
                let result = Command::new(d)
                    .arg(stringify!($name))
                    .output()?;

                if result.status.success() {
                    Ok(())
                } else {
                    io::stderr().write_all(&result.stderr)?;
                    Err(anyhow::anyhow!("leak detected"))
                }
            } else {
                skip()
            }
        }
    }
}

mod leak_tests {
    use super::*;

    /// Tests that we actually detect leaks.
    #[cfg_attr(target_os = "linux", test)]
    fn leak_basecase() -> Result<()> {
        if let Some(d) = locate_detector() {
            let result = Command::new(d)
                .arg("leak_basecase")
                .output()?;

            if ! result.status.success() {
                Ok(())
            } else {
                Err(anyhow::anyhow!("base case failed: no leak detected"))
            }
        } else {
            skip()
        }
    }

    // The tests.
    make_test!(clean_basecase);
    make_test!(test_memzero);
    make_test!(test_libc_memset);
    make_test!(test_protected);
    make_test!(test_protected_mpi);
    make_test!(test_session_key);
    make_test!(test_encrypted);
    make_test!(test_password);
    make_test!(test_ed25519);
    #[cfg(not(feature = "crypto-rust"))]
    make_test!(test_aes_256_encryption);
    #[cfg(not(feature = "crypto-rust"))]
    make_test!(test_aes_256_decryption);
}