summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--openpgp/Cargo.toml4
-rw-r--r--openpgp/tests/secret-leak-detector.rs216
-rw-r--r--openpgp/tests/secret-leak-detector/detector.rs378
3 files changed, 598 insertions, 0 deletions
diff --git a/openpgp/Cargo.toml b/openpgp/Cargo.toml
index 71a52df2..08719619 100644
--- a/openpgp/Cargo.toml
+++ b/openpgp/Cargo.toml
@@ -147,3 +147,7 @@ required-features = ["compression-deflate"]
[[bench]]
name = "run_benchmarks"
harness = false
+
+[[example]]
+name = "secret-leak-detector"
+path = "tests/secret-leak-detector/detector.rs"
diff --git a/openpgp/tests/secret-leak-detector.rs b/openpgp/tests/secret-leak-detector.rs
new file mode 100644
index 00000000..b5c4a55a
--- /dev/null
+++ b/openpgp/tests/secret-leak-detector.rs
@@ -0,0 +1,216 @@
+//! 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 re-used 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,
+};
+use anyhow::Result;
+use once_cell::sync::OnceCell;
+
+/// 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: OnceCell<Option<PathBuf>> = OnceCell::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);
+
+ // These find leaks in the RustCrypto backend, so we cannot enable
+ // them yet:
+ //
+ //make_test!(test_aes_256_encryption);
+ //make_test!(test_aes_256_decryption);
+}
diff --git a/openpgp/tests/secret-leak-detector/detector.rs b/openpgp/tests/secret-leak-detector/detector.rs
new file mode 100644
index 00000000..3d752657
--- /dev/null
+++ b/openpgp/tests/secret-leak-detector/detector.rs
@@ -0,0 +1,378 @@
+//! Scans a process memory for secret leaks.
+//!
+//! This is the code doing the experiments and scanning itself for
+//! leaks. See ../secret-leak-detector.rs for details.
+
+use std::{
+ alloc::{GlobalAlloc, System, Layout},
+ collections::HashMap,
+ io::{Read, Write},
+};
+
+use sequoia_openpgp::{
+ crypto::{mem, mpi::{MPI, ProtectedMPI}, Password, SessionKey, Signer},
+ fmt::hex,
+ packet::{
+ key::{Key4, PrimaryRole},
+ PKESK,
+ SKESK,
+ },
+ serialize::stream::{
+ Message, Encryptor2, LiteralWriter,
+ },
+ parse::{
+ stream::*,
+ Parse,
+ },
+ policy::StandardPolicy,
+ types::{
+ HashAlgorithm,
+ SymmetricAlgorithm,
+ },
+ Cert,
+ Fingerprint,
+ KeyHandle,
+ Result,
+
+};
+
+/// An allocator that leaks all allocations making it easier to spot
+/// secret leaks.
+struct LeakingAllocator;
+
+unsafe impl GlobalAlloc for LeakingAllocator {
+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
+ System.alloc(layout)
+ }
+
+ unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
+ // Leaking.
+ }
+}
+
+#[global_allocator]
+static GLOBAL: LeakingAllocator = LeakingAllocator;
+
+/// How often to repeat a test.
+const N: usize = 1;
+
+/// The secret to use and scan for.
+const NEEDLE: &[u8] = b"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";
+const N0: u8 = NEEDLE[0];
+
+/// A clean base case that does nothing.
+fn clean_basecase() {
+}
+
+/// Constructs a vector carefully, without leaking the secret during
+/// construction.
+fn careful_to_vec<B: AsRef<[u8]>>(b: B) -> Vec<u8> {
+ let b = b.as_ref();
+ let mut r = vec![0; b.len()];
+ b.iter().zip(r.iter_mut()).for_each(|(f, t)| *t = *f);
+ r
+}
+
+/// Checks that a secret has been written.
+///
+/// Notably, this also prevents optimizing the secret creation away.
+fn check_secret(v: &[u8]) {
+ assert_eq!(v.iter().cloned().map(usize::from).sum::<usize>(),
+ NEEDLE.len() * N0 as usize);
+ std::hint::black_box(v); // Avoid optimizing away.
+ unsafe { // Likewise.
+ assert_eq!(libc::memcmp(v.as_ptr() as *const _,
+ NEEDLE.as_ptr() as *const _,
+ NEEDLE.len()), 0);
+ }
+}
+
+/// A leaky base case that allocates a Vector.
+fn leak_basecase() {
+ let v = NEEDLE.to_vec();
+ check_secret(&v);
+}
+
+/// A test case that allocates a Vector and securely overwrites it
+/// using [`memsec::memzero`].
+fn test_memzero() {
+ let mut v = careful_to_vec(NEEDLE);
+ check_secret(&v);
+ let len = v.len();
+ unsafe {
+ memsec::memzero(v.as_mut_ptr(), len);
+ }
+}
+
+/// A test case that allocates a Vector and securely overwrites it
+/// using [`libc::memset`].
+fn test_libc_memset() {
+ let mut v = careful_to_vec(NEEDLE);
+ check_secret(&v);
+ let len = v.len();
+ let p = unsafe {
+ libc::memset(v.as_mut_ptr() as _, 0, len)
+ };
+ std::hint::black_box(p); // Avoid optimizing the memset away.
+}
+
+/// A test case that allocates a mem::Protected and drops it.
+fn test_protected() {
+ let v: mem::Protected = NEEDLE.into();
+ check_secret(&v);
+ let v: mem::Protected = NEEDLE.to_vec().into();
+ check_secret(&v);
+}
+
+/// A test case that allocates a mem::Protected and drops it.
+fn test_protected_mpi() {
+ let v: ProtectedMPI = NEEDLE.to_vec().into_boxed_slice().into();
+ check_secret(v.value());
+ let v: ProtectedMPI = NEEDLE.to_vec().into();
+ check_secret(v.value());
+ let v: ProtectedMPI = mem::Protected::from(NEEDLE).into();
+ check_secret(v.value());
+ let v: ProtectedMPI = MPI::new(NEEDLE).into();
+ check_secret(v.value());
+}
+
+/// A test case that allocates a SessionKey and drops it.
+fn test_session_key() {
+ let v: SessionKey = NEEDLE.into();
+ check_secret(&v);
+ let v: SessionKey = NEEDLE.to_vec().into();
+ check_secret(&v);
+}
+
+/// A test case that allocates a mem::Encrypted, uses it once, then
+/// drops it.
+fn test_encrypted() {
+ let m = mem::Encrypted::new(NEEDLE.into());
+ m.map(|v| check_secret(&v));
+}
+
+/// A test case that allocates a Password, uses it once, then drops
+/// it.
+fn test_password() {
+ let p = Password::from(NEEDLE);
+ p.map(|v| check_secret(&v));
+}
+
+/// A test case that allocates a Key4, uses it once, then
+/// drops it.
+fn test_ed25519() {
+ let k = Key4::<_, PrimaryRole>::import_secret_ed25519(NEEDLE, None)
+ .unwrap();
+ let mut kp = k.into_keypair().unwrap();
+ kp.sign(HashAlgorithm::SHA256, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ .unwrap();
+}
+
+/// A test case that encrypts a message using AES-256.
+fn test_aes_256_encryption() {
+ aes_256_encryption().unwrap();
+}
+fn aes_256_encryption() -> Result<Vec<u8>> {
+ let mut sink = Vec::new();
+ let message = Message::new(&mut sink);
+ let message = Encryptor2::with_session_key(
+ message, SymmetricAlgorithm::AES256, NEEDLE.into())?
+ .add_passwords(Some(Password::from(NEEDLE)))
+ .build()?;
+ let mut w = LiteralWriter::new(message).build()?;
+ w.write_all(b"Hello world.")?;
+ w.finalize()?;
+ Ok(sink)
+}
+
+/// A test case that decrypts a message using AES-256.
+fn test_aes_256_decryption() {
+ let ciphertext = aes_256_encryption().unwrap();
+ aes_256_decryption(&ciphertext).unwrap();
+}
+fn aes_256_decryption(ciphertext: &[u8]) -> Result<()> {
+ let p = &StandardPolicy::new();
+
+ struct Helper {}
+ impl VerificationHelper for Helper {
+ fn get_certs(&mut self, _ids: &[KeyHandle]) -> Result<Vec<Cert>> {
+ Ok(Vec::new())
+ }
+ fn check(&mut self, _structure: MessageStructure) -> Result<()> {
+ Ok(())
+ }
+ }
+ impl DecryptionHelper for Helper {
+ fn decrypt<D>(&mut self, _: &[PKESK], skesks: &[SKESK],
+ _sym_algo: Option<SymmetricAlgorithm>,
+ mut decrypt: D) -> Result<Option<Fingerprint>>
+ where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool
+ {
+ skesks[0].decrypt(&Password::from(NEEDLE))
+ .map(|(algo, session_key)| decrypt(algo, &session_key))?;
+ Ok(None)
+ }
+ }
+
+ let h = Helper {};
+ let mut v = DecryptorBuilder::from_bytes(ciphertext)?
+ .with_policy(p, None, h)?;
+
+ let mut content = Vec::new();
+ v.read_to_end(&mut content)?;
+ assert_eq!(content, b"Hello world.");
+ Ok(())
+}
+
+type Test = fn() -> ();
+
+fn main() {
+ let tests: HashMap<&'static str, Test> = [
+ ("clean_basecase", clean_basecase as Test),
+ ("leak_basecase", leak_basecase as Test),
+ ("test_memzero", test_memzero as Test),
+ ("test_libc_memset", test_libc_memset as Test),
+ ("test_protected", test_protected as Test),
+ ("test_protected_mpi", test_protected_mpi as Test),
+ ("test_session_key", test_session_key as Test),
+ ("test_encrypted", test_encrypted as Test),
+ ("test_password", test_password as Test),
+ ("test_ed25519", test_ed25519 as Test),
+ ("test_aes_256_encryption", test_aes_256_encryption as Test),
+ ("test_aes_256_decryption", test_aes_256_decryption as Test),
+ ].into_iter().collect();
+
+ let test = if let Some(t) = std::env::args().nth(1) {
+ t
+ } else {
+ eprintln!("Usage: {} <TEST>\n\nAvailable tests:\n",
+ std::env::args().nth(0).unwrap());
+ for t in tests.keys() {
+ println!("{}", t);
+ }
+ return;
+ };
+
+ eprintln!("{}: running test", test);
+ for _ in 0..N {
+ if let Some(test_fn) = tests.get(test.as_str()) {
+ (test_fn)();
+ } else {
+ panic!("unknown test case {:?}", test);
+ }
+ }
+
+ scan(&test).unwrap();
+}
+
+fn scan(name: &str) -> Result<()> {
+ let mut found_secret = false;
+ let mut sink = std::io::stderr();
+ for map in Map::iter()? {
+ let map = map?;
+ if map.read && map.write {
+ let mut header_printed = false;
+ let view = map.as_bytes()?;
+ const CS: usize = 16;
+ for (i, c) in view.chunks(CS).enumerate() {
+ if c.iter().filter(|&b| *b == N0).count() > 7 {
+ found_secret = true;
+
+ if ! header_printed {
+ eprintln!("{}: {} bytes", map.pathname, map.len);
+ header_printed = true;
+ }
+
+ let mut d = hex::Dumper::with_offset(
+ &mut sink, "", map.start as usize + i * CS);
+ d.write_labeled(c, |_, buf| {
+ let mut s = String::with_capacity(16);
+ for b in buf {
+ assert!(N0 != b'!');
+ s.push(if *b == N0 {
+ '!'
+ } else {
+ '.'
+ });
+ }
+ Some(s)
+ })?;
+ }
+ }
+ }
+ }
+
+ if found_secret {
+ eprintln!("{}: secret leaked", name);
+ std::process::exit(1);
+ } else {
+ eprintln!("{}: passed", name);
+ Ok(())
+ }
+}
+
+use std::{
+ fs::File,
+ io::{BufReader, BufRead},
+};
+
+#[derive(Debug)]
+#[allow(dead_code)]
+struct Map {
+ start: u64,
+ len: u64,
+ read: bool,
+ write: bool,
+ execute: bool,
+ offset: u64,
+ device: String,
+ inode: u64,
+ pathname: String,
+}
+
+impl Map {
+ fn iter() -> Result<impl Iterator<Item = Result<Map>>> {
+ let f = File::open("/proc/self/maps")?;
+ let f = BufReader::new(f);
+ Ok(f.lines().filter_map(|l| l.ok()).map(Self::parse_line))
+ }
+
+ fn parse_line(l: String) -> Result<Self> {
+ let f = l.splitn(6, ' ').collect::<Vec<_>>();
+ let a = f[0].splitn(2, '-').collect::<Vec<_>>();
+ let parse_hex = |s| -> Result<u64> {
+ let b = hex::decode(s)?;
+ let mut a = [0; 8]; // XXX word size <= u64
+ let l = a.len().min(b.len());
+ a[8 - l..].copy_from_slice(&b[..l]);
+ Ok(u64::from_be_bytes(a))
+ };
+ let start = parse_hex(&a[0])?;
+ let end = parse_hex(&a[1])?;
+ assert!(start <= end);
+
+ Ok(Map {
+ start,
+ len: end - start,
+ read: f[1].as_bytes()[0] == b'r',
+ write: f[1].as_bytes()[1] == b'w',
+ execute: f[1].as_bytes()[2] == b'x',
+ offset: parse_hex(&f[2])?,
+ device: f[3].into(),
+ inode: f[4].parse()?,
+ pathname: f[5].trim_start().into(),
+ })
+ }
+
+ fn as_bytes(&self) -> Result<&[u8]> {
+ if self.read {
+ let s = unsafe {
+ std::slice::from_raw_parts(self.start as usize as *const _,
+ self.len as usize)
+ };
+ Ok(s)
+ } else {
+ Err(anyhow::anyhow!("No read permissions"))
+ }
+ }
+}