Skip to content

Commit 4bb8027

Browse files
committed
implement "non_secure_erase" methods
This PR implements a `non_secure_erase()` method on SecretKey, KeyPair, SharedSecret, Scalar, and DisplaySecret. The purpose of this method is to (attempt to) overwrite secret data with valid default values. This method can be used by libraries to implement Zeroize on structs containing secret values. `non_secure_erase()` attempts to avoid being optimized away or reordered using the same mechanism as the zeroize crate: first, using `std::ptr::write_volatile` (which will not be optimized away) to overwrite the memory, then using a memory fence to prevent subtle issues due to load or store reordering. Note, however, that this method is *very unlikely* to do anything useful on its own. Effective use involves carefully placing these values inside non-Copy structs and pinning those structs in place. See the [`zeroize`](https://docs.rs/zeroize) documentation for tips and tricks, and for further discussion. [this commit includes a squashed-in commit from tcharding to fix docs and helpful suggestions from apoelstra and Kixunil]
1 parent 6ec968a commit 4bb8027

File tree

8 files changed

+82
-1
lines changed

8 files changed

+82
-1
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ Alternatively add symlinks in your `.git/hooks` directory to any of the githooks
3838
We use a custom Rust compiler configuration conditional to guard the bench mark code. To run the
3939
bench marks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench --features=recovery`.
4040

41+
### A note on `non_secure_erase`
42+
43+
This crate's secret types (`SecretKey`, `KeyPair`, `SharedSecret`, `Scalar`, and `DisplaySecret`)
44+
have a method called `non_secure_erase` that *attempts* to overwrite the contained secret. This
45+
method is provided to assist other libraries in building secure secret erasure. However, this
46+
library makes no guarantees about the security of using `non_secure_erase`. In particular,
47+
the compiler doesn't have any concept of secrets and in most cases can arbitrarily move or copy
48+
values anywhere it pleases. For more information, consult the [`zeroize`](https://docs.rs/zeroize)
49+
documentation.
50+
4151
## Fuzzing
4252

4353
If you want to fuzz this library, or any library which depends on it, you will

secp256k1-sys/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,3 @@ recovery = []
3232
lowmemory = []
3333
std = ["alloc"]
3434
alloc = []
35-

secp256k1-sys/src/lib.rs

+32
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,38 @@ impl KeyPair {
454454
pk
455455
}
456456
}
457+
458+
/// Attempts to erase the contents of the underlying array.
459+
///
460+
/// Note, however, that since this type is `Copy`, ensuring that the contents
461+
/// of this array don't end up elsewhere in memory is very subtle. For more
462+
/// discussion on this, please see the documentation of the
463+
/// [`zeroize`](https://docs.rs/zeroize) crate.
464+
#[inline]
465+
pub fn non_secure_erase(&mut self) {
466+
non_secure_erase_impl(&mut self.0, DUMMY_KEYPAIR);
467+
}
468+
}
469+
470+
// DUMMY_KEYPAIR is the internal repr of a valid key pair with secret key `[1u8; 32]`
471+
#[cfg(target_endian = "little")]
472+
const DUMMY_KEYPAIR: [c_uchar; 96] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 143, 7, 221, 213, 233, 245, 23, 156, 255, 25, 72, 96, 52, 24, 30, 215, 101, 5, 186, 170, 213, 62, 93, 153, 64, 100, 18, 123, 86, 197, 132, 27, 209, 232, 168, 105, 122, 212, 34, 81, 222, 57, 246, 167, 32, 129, 223, 223, 66, 171, 197, 66, 166, 214, 254, 7, 21, 84, 139, 88, 143, 175, 190, 112];
473+
#[cfg(all(target_endian = "big", target_pointer_width = "32"))]
474+
const DUMMY_KEYPAIR: [c_uchar; 96] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 213, 221, 7, 143, 156, 23, 245, 233, 96, 72, 25, 255, 215, 30, 24, 52, 170, 186, 5, 101, 153, 93, 62, 213, 123, 18, 100, 64, 27, 132, 197, 86, 105, 168, 232, 209, 81, 34, 212, 122, 167, 246, 57, 222, 223, 223, 129, 32, 66, 197, 171, 66, 7, 254, 214, 166, 88, 139, 84, 21, 112, 190, 175, 143];
475+
#[cfg(all(target_endian = "big", target_pointer_width = "64"))]
476+
const DUMMY_KEYPAIR: [c_uchar; 96] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 156, 23, 245, 233, 213, 221, 7, 143, 215, 30, 24, 52, 96, 72, 25, 255, 153, 93, 62, 213, 170, 186, 5, 101, 27, 132, 197, 86, 123, 18, 100, 64, 81, 34, 212, 122, 105, 168, 232, 209, 223, 223, 129, 32, 167, 246, 57, 222, 7, 254, 214, 166, 66, 197, 171, 66, 112, 190, 175, 143, 88, 139, 84, 21];
477+
478+
/// Does a best attempt at secure erasure using Rust intrinsics.
479+
///
480+
/// The implementation is based on the approach used by the [`zeroize`](https://docs.rs/zeroize) crate.
481+
#[inline(always)]
482+
pub fn non_secure_erase_impl<T>(dst: &mut T, src: T) {
483+
use core::sync::atomic;
484+
// overwrite using volatile value
485+
unsafe { ptr::write_volatile(dst, src); }
486+
487+
// prevent future accesses from being reordered to before erasure
488+
atomic::compiler_fence(atomic::Ordering::SeqCst);
457489
}
458490

459491
#[cfg(not(fuzzing))]

src/ecdh.rs

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const SHARED_SECRET_SIZE: usize = constants::SECRET_KEY_SIZE;
4646
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4747
pub struct SharedSecret([u8; SHARED_SECRET_SIZE]);
4848
impl_display_secret!(SharedSecret);
49+
impl_non_secure_erase!(SharedSecret, 0, [0u8; SHARED_SECRET_SIZE]);
4950

5051
impl SharedSecret {
5152
/// Creates a new shared secret from a pubkey and secret key.

src/key.rs

+20
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ use crate::{hashes, ThirtyTwoByteHash};
6666
#[derive(Copy, Clone)]
6767
pub struct SecretKey([u8; constants::SECRET_KEY_SIZE]);
6868
impl_display_secret!(SecretKey);
69+
impl_non_secure_erase!(SecretKey, 0, [1u8; constants::SECRET_KEY_SIZE]);
6970

7071
impl PartialEq for SecretKey {
7172
/// This implementation is designed to be constant time to help prevent side channel attacks.
@@ -992,6 +993,14 @@ impl KeyPair {
992993
pub fn sign_schnorr(&self, msg: Message) -> schnorr::Signature {
993994
SECP256K1.sign_schnorr(&msg, self)
994995
}
996+
997+
/// Attempts to erase the secret within the underlying array.
998+
///
999+
/// Note, however, that since [`KeyPair`] implements `Copy`, ensuring that the secret data
1000+
/// doesn't end up elsewhere in memory is very subtle. For more discussion on this, please see
1001+
/// the documentation of the [`zeroize`](https://docs.rs/zeroize) crate.
1002+
#[inline]
1003+
pub fn non_secure_erase(&mut self) { self.0.non_secure_erase(); }
9951004
}
9961005

9971006
impl From<KeyPair> for SecretKey {
@@ -1603,6 +1612,17 @@ mod test {
16031612
assert_eq!(PublicKey::from_slice(&pk1.serialize_uncompressed()[..]), Ok(pk1));
16041613
}
16051614

1615+
#[test]
1616+
#[cfg(all(feature = "std", not(fuzzing)))]
1617+
fn erased_keypair_is_valid() {
1618+
let s = Secp256k1::new();
1619+
let kp = KeyPair::from_seckey_slice(&s, &[1u8; constants::SECRET_KEY_SIZE])
1620+
.expect("valid secret key");
1621+
let mut kp2 = kp;
1622+
kp2.non_secure_erase();
1623+
assert!(kp.eq_fast_unstable(&kp2));
1624+
}
1625+
16061626
#[test]
16071627
#[rustfmt::skip]
16081628
fn invalid_secret_key() {

src/macros.rs

+17
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,23 @@ macro_rules! impl_pretty_debug {
9191
};
9292
}
9393

94+
macro_rules! impl_non_secure_erase {
95+
($thing:ident, $target:tt, $value:expr) => {
96+
impl $thing {
97+
/// Attempts to erase the contents of the underlying array.
98+
///
99+
/// Note, however, that since this type is `Copy`, ensuring that the contents
100+
/// of this array don't end up elsewhere in memory is very subtle. For more
101+
/// discussion on this, please see the documentation of the
102+
/// [`zeroize`](https://docs.rs/zeroize) crate.
103+
#[inline]
104+
pub fn non_secure_erase(&mut self) {
105+
secp256k1_sys::non_secure_erase_impl(&mut self.$target, $value);
106+
}
107+
}
108+
};
109+
}
110+
94111
/// Formats error. If `std` feature is OFF appends error source (delimited by `: `). We do this
95112
/// because `e.source()` is only available in std builds, without this macro the error source is
96113
/// lost for no-std builds.

src/scalar.rs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::constants;
2323
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
2424
pub struct Scalar([u8; 32]);
2525
impl_pretty_debug!(Scalar);
26+
impl_non_secure_erase!(Scalar, 0, [0u8; 32]);
2627

2728
const MAX_RAW: [u8; 32] = [
2829
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,

src/secret.rs

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ macro_rules! impl_display_secret {
8686
pub struct DisplaySecret {
8787
secret: [u8; SECRET_KEY_SIZE],
8888
}
89+
impl_non_secure_erase!(DisplaySecret, secret, [0u8; SECRET_KEY_SIZE]);
8990

9091
impl fmt::Debug for DisplaySecret {
9192
#[inline]

0 commit comments

Comments
 (0)