boltlabs-inc / tss-ecdsa

An implementation of a threshold ECDSA signature scheme
Other
11 stars 5 forks source link

Implement Proper Zeroization for Sensitive and Secret Data #517

Open gatoWololo opened 6 months ago

gatoWololo commented 6 months ago

Note: This Epic description is WIP. I may add more information later. But in general, I cannot write here, exhaustively, everything that should be said about zeroizing.

This library generates and uses critical, secret values. We need to carefully and thoughtfully implement zeroization for this data. There are several challenges to making this happen:

Which data is sensitive and should be zeroized?

This is not clearly labeled in the code. We do know of some structs that contain secret values, e.g. the Private structs (There are three of these, one for each round of the sub-protocol). Even then, not every field in this struct requires zeroization.

The round one struct currently looks like:

/// Private data used in round one of the presign protocol.
#[derive(ZeroizeOnDrop)]
pub(crate) struct Private {
    pub k: BigNumber,
    pub rho: Nonce,
    pub gamma: BigNumber,
    pub nu: Nonce,
    #[zeroize(skip)]
    pub G: Ciphertext, // Technically can be public but is only one per party
    #[zeroize(skip)]
    pub K: Ciphertext, // Technically can be public but is only one per party
}

Note: One way to identify these structs is by their ZeroizeOnDrop derive line. If something has ZeroizeOnDrop there is a good chance some of the data in there needs to be zeroized.

Our current approach is to replace the former struct with a new version something like:

pub(crate) struct Private {
    pub k: SecretBigNumber,
    pub rho: SecretNonce,
    pub gamma: BigNumber,
    pub nu: SecretNonce,
    pub G: Ciphertext, // Technically can be public but is only one per party
    pub K: Ciphertext, // Technically can be public but is only one per party
}

We can notice several changes here:

Use Secret* variants of the existing types we work with. These types have several advantages:
We remove the ZeroizeOnDrop derivation.

Having ZeroizeOnDrop in these "compound" structs makes it so that the individual fields cannot be "moved out" (in the Rust language sense). Types cannot have fields moved out if they implement a destructor.

This makes it hard to "reuse" or "move" this data as it flows through the code. You end up having to make copies. To avoid this, and leverage Rust's powerful move semantics, we do not implement ZeroizeOnDrop directly. Instead, the sub-types (the type of the fields that make up our compound types) should implement ZeroizeOnDrop. This will allow the fields to be moved into other structs or functions.

Example implementation:

Here is what SecretBigNumber would look like:

#[derive(Clone, ZeroizeOnDrop)]
pub(crate) struct SecretBigNumber(Box<BigNumber>);

This type, SecretBigNumber should encapsulate (via methods) all the functionality and operations necessary for it to accomplish it's work. This type should avoid exposing the underlying data to the user. It should also be careful to to accidentally make copies that are not then zeroized.

When it comes to zeroizing, our solutions is:

On 100% Zeroizing

Even with this work, I do not think we can reach 100% zeroization. There are some places where our secret data may still find itself: registers, CPU-caches, etc. There is not a good way for us to handle this changes and not really necessary.

Data will also momentarily live on the stack until we get it in the heap. Consider this method which creates a new SecretBigNumber:

fn random_positive_bn<R: RngCore + CryptoRng>(
        rng: &mut R,
        n: &BigNumber,
    ) -> SecretBigNumber {
        // Note: We don't zeroize the stack allocated value here...
        let bn = BigNumber::from_rng(n, rng);
        SecretBigNumber(Box::new(bn))
    }

Before boxing (heap-allocating) this value, it momentarily lives on the stack. Once we have heap allocated it, it can not be zeroized since it has already been "moved" into the Box constructor. So this value remains on the stack, ideally, another function call would quickly clobber that stack memory, effectively deleting the value. This is the best we can do since Rust does not support allocating values directly in the heap. Check out the unstable box syntax for more information.

More Background

See tickets: