Merge rust-bitcoin/rust-secp256k1#582: implement `insecure-erase` feature (was: `Zeroize`)

8fffbeab13 implement "non_secure_erase" methods (kwantam)

Pull request description:

  This PR adds [`Zeroize`](https://docs.rs/zeroize) derivations for the following structs:

  - `SecretKey`
  - `KeyPair`
  - `SharedSecret`
  - `Scalar`
  - `DisplaySecret`

  This is *only* a Zeroize impl, and does not make Zeroize happen automatically on drop (doing that would be a breaking change because it would preclude deriving `Copy`). But this is still useful, because it allows downstream libraries to implement `ZeroizeOnDrop` for structs that contain such secrets and/or simply to use the `Zeroizing` container struct.

  Because these new impls are never invoked automatically, performance impact should be zero. Safety-wise, the `Zeroize` library appears to be widely used in cryptographic code. For example, Supranational's [blst](https://github.com/supranational/blst) Rust bindings use it, and in turn are used in one of the most popular eth2 validator implementations.

  Thanks for maintaining a really great library!

ACKs for top commit:
  tcharding:
    FWIW ACK 8fffbeab13
  apoelstra:
    ACK 8fffbeab13

Tree-SHA512: 28d2cdcc6bd2d2d6330b67ae8635561882e869199d8fef9a3ebc3f368a7a0c2c00b818281190133f551b099e9c5226f104a56edc14c9b6f699ceba49e4b24563
This commit is contained in:
Andrew Poelstra 2023-02-22 15:52:37 +00:00
commit c9310884b6
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
8 changed files with 83 additions and 1 deletions

View File

@ -38,6 +38,16 @@ Alternatively add symlinks in your `.git/hooks` directory to any of the githooks
We use a custom Rust compiler configuration conditional to guard the bench mark code. To run the We use a custom Rust compiler configuration conditional to guard the bench mark code. To run the
bench marks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench --features=recovery`. bench marks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench --features=recovery`.
### A note on `non_secure_erase`
This crate's secret types (`SecretKey`, `KeyPair`, `SharedSecret`, `Scalar`, and `DisplaySecret`)
have a method called `non_secure_erase` that *attempts* to overwrite the contained secret. This
method is provided to assist other libraries in building secure secret erasure. However, this
library makes no guarantees about the security of using `non_secure_erase`. In particular,
the compiler doesn't have any concept of secrets and in most cases can arbitrarily move or copy
values anywhere it pleases. For more information, consult the [`zeroize`](https://docs.rs/zeroize)
documentation.
## Fuzzing ## Fuzzing
If you want to fuzz this library, or any library which depends on it, you will If you want to fuzz this library, or any library which depends on it, you will

View File

@ -32,4 +32,3 @@ recovery = []
lowmemory = [] lowmemory = []
std = ["alloc"] std = ["alloc"]
alloc = [] alloc = []

View File

@ -454,6 +454,38 @@ impl KeyPair {
pk pk
} }
} }
/// Attempts to erase the contents of the underlying array.
///
/// Note, however, that the compiler is allowed to freely copy or move the
/// contents of this array to other places in memory. Preventing this behavior
/// is very subtle. For more discussion on this, please see the documentation
/// of the [`zeroize`](https://docs.rs/zeroize) crate.
#[inline]
pub fn non_secure_erase(&mut self) {
non_secure_erase_impl(&mut self.0, DUMMY_KEYPAIR);
}
}
// DUMMY_KEYPAIR is the internal repr of a valid key pair with secret key `[1u8; 32]`
#[cfg(target_endian = "little")]
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];
#[cfg(all(target_endian = "big", target_pointer_width = "32"))]
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];
#[cfg(all(target_endian = "big", target_pointer_width = "64"))]
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];
/// Does a best attempt at secure erasure using Rust intrinsics.
///
/// The implementation is based on the approach used by the [`zeroize`](https://docs.rs/zeroize) crate.
#[inline(always)]
pub fn non_secure_erase_impl<T>(dst: &mut T, src: T) {
use core::sync::atomic;
// overwrite using volatile value
unsafe { ptr::write_volatile(dst, src); }
// prevent future accesses from being reordered to before erasure
atomic::compiler_fence(atomic::Ordering::SeqCst);
} }
#[cfg(not(fuzzing))] #[cfg(not(fuzzing))]

View File

@ -46,6 +46,7 @@ const SHARED_SECRET_SIZE: usize = constants::SECRET_KEY_SIZE;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SharedSecret([u8; SHARED_SECRET_SIZE]); pub struct SharedSecret([u8; SHARED_SECRET_SIZE]);
impl_display_secret!(SharedSecret); impl_display_secret!(SharedSecret);
impl_non_secure_erase!(SharedSecret, 0, [0u8; SHARED_SECRET_SIZE]);
impl SharedSecret { impl SharedSecret {
/// Creates a new shared secret from a pubkey and secret key. /// Creates a new shared secret from a pubkey and secret key.

View File

@ -66,6 +66,7 @@ use crate::{hashes, ThirtyTwoByteHash};
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct SecretKey([u8; constants::SECRET_KEY_SIZE]); pub struct SecretKey([u8; constants::SECRET_KEY_SIZE]);
impl_display_secret!(SecretKey); impl_display_secret!(SecretKey);
impl_non_secure_erase!(SecretKey, 0, [1u8; constants::SECRET_KEY_SIZE]);
impl PartialEq for SecretKey { impl PartialEq for SecretKey {
/// This implementation is designed to be constant time to help prevent side channel attacks. /// This implementation is designed to be constant time to help prevent side channel attacks.
@ -992,6 +993,15 @@ impl KeyPair {
pub fn sign_schnorr(&self, msg: Message) -> schnorr::Signature { pub fn sign_schnorr(&self, msg: Message) -> schnorr::Signature {
SECP256K1.sign_schnorr(&msg, self) SECP256K1.sign_schnorr(&msg, self)
} }
/// Attempts to erase the secret within the underlying array.
///
/// Note, however, that the compiler is allowed to freely copy or move the contents
/// of this array to other places in memory. Preventing this behavior is very subtle.
/// For more discussion on this, please see the documentation of the
/// [`zeroize`](https://docs.rs/zeroize) crate.
#[inline]
pub fn non_secure_erase(&mut self) { self.0.non_secure_erase(); }
} }
impl From<KeyPair> for SecretKey { impl From<KeyPair> for SecretKey {
@ -1603,6 +1613,17 @@ mod test {
assert_eq!(PublicKey::from_slice(&pk1.serialize_uncompressed()[..]), Ok(pk1)); assert_eq!(PublicKey::from_slice(&pk1.serialize_uncompressed()[..]), Ok(pk1));
} }
#[test]
#[cfg(all(feature = "std", not(fuzzing)))]
fn erased_keypair_is_valid() {
let s = Secp256k1::new();
let kp = KeyPair::from_seckey_slice(&s, &[1u8; constants::SECRET_KEY_SIZE])
.expect("valid secret key");
let mut kp2 = kp;
kp2.non_secure_erase();
assert!(kp.eq_fast_unstable(&kp2));
}
#[test] #[test]
#[rustfmt::skip] #[rustfmt::skip]
fn invalid_secret_key() { fn invalid_secret_key() {

View File

@ -91,6 +91,23 @@ macro_rules! impl_pretty_debug {
}; };
} }
macro_rules! impl_non_secure_erase {
($thing:ident, $target:tt, $value:expr) => {
impl $thing {
/// Attempts to erase the contents of the underlying array.
///
/// Note, however, that the compiler is allowed to freely copy or move the
/// contents of this array to other places in memory. Preventing this behavior
/// is very subtle. For more discussion on this, please see the documentation
/// of the [`zeroize`](https://docs.rs/zeroize) crate.
#[inline]
pub fn non_secure_erase(&mut self) {
secp256k1_sys::non_secure_erase_impl(&mut self.$target, $value);
}
}
};
}
/// Formats error. If `std` feature is OFF appends error source (delimited by `: `). We do this /// Formats error. If `std` feature is OFF appends error source (delimited by `: `). We do this
/// because `e.source()` is only available in std builds, without this macro the error source is /// because `e.source()` is only available in std builds, without this macro the error source is
/// lost for no-std builds. /// lost for no-std builds.

View File

@ -23,6 +23,7 @@ use crate::constants;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Scalar([u8; 32]); pub struct Scalar([u8; 32]);
impl_pretty_debug!(Scalar); impl_pretty_debug!(Scalar);
impl_non_secure_erase!(Scalar, 0, [0u8; 32]);
const MAX_RAW: [u8; 32] = [ const MAX_RAW: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,

View File

@ -86,6 +86,7 @@ macro_rules! impl_display_secret {
pub struct DisplaySecret { pub struct DisplaySecret {
secret: [u8; SECRET_KEY_SIZE], secret: [u8; SECRET_KEY_SIZE],
} }
impl_non_secure_erase!(DisplaySecret, secret, [0u8; SECRET_KEY_SIZE]);
impl fmt::Debug for DisplaySecret { impl fmt::Debug for DisplaySecret {
#[inline] #[inline]