Complete re-do of Secret and Zeroize#57
Conversation
4876ad1 to
6a2811f
Compare
| // the compiler from eliding this scrub as a dead write. All-zero is a valid bit pattern | ||
| // for the primitive scalar/array types this wrapper is intended to hold. | ||
| unsafe { | ||
| ptr::write_volatile(base_ptr.add(i), 0u8); |
There was a problem hiding this comment.
The overall implementation is sound and way better than what it was. However, this bit requires a final touch.
Having a loop where every byte written into the index is volatile is too much. You just need to prevent the variable from being elided as a whole, so marking the base is just enough.
You can even simplify it further, and use core::hint::black_box; on base_ptr. black_box uses inline assembly and marks base_ptr variable as clobbered memory. The trick is that compiler does not understand asm, hence cannot optimize it.
There was a problem hiding this comment.
I'm aware of the black_box trick, and I considered this also, but the contract on
https://doc.rust-lang.org/std/hint/fn.black_box.html
very clearly says that it is a "best effort"
whereas the contract on
https://doc.rust-lang.org/std/ptr/fn.write_volatile.html
is much stronger.
| // `u8` has no drop glue. `write_volatile` (rather than a plain store) is what forbids | ||
| // the compiler from eliding this scrub as a dead write. All-zero is a valid bit pattern | ||
| // for the primitive scalar/array types this wrapper is intended to hold. | ||
| unsafe { |
There was a problem hiding this comment.
Given this assumption, could the implementation not be simplified to :
unsafe {
ptr::write_volatile(self, core::mem::zeroed());
}Maybe I am missing something but I would've thought that achieves identical results without manually looping.
There was a problem hiding this comment.
Great suggestion!
I have updated to simplify this.
|
I had a look at this earlier this morning, and we had some back and forth in private. My impression is that there aren't substantial difference between the new At this point, I am not strongly advocating against this PR to stick with the RustCrypto zeroize crate, but I'd like to have a discussion about the tradeoffs that come with the choice of implementing "our own The downsides to me seem to be:
On the pro side:
I am not advocating against this PR in favor of the RustCrypto Please, fire away your considerations! Note on
|
romen
left a comment
There was a problem hiding this comment.
One more suggestion:
I believe our previous attempts at using the zeroize crate have been negatively impacted by the lack of "real" examples in its documentation.
The documentation of Secret here has a similar problem, it shows what to do for a trivial (but relevant) "secret" array of u8.
I think we would benefit from adding an example of a more complex struct, with secret and public fields.
Maybe inspired to the MLKEMPrivateKey?
|
Thanks @romen. Very valuable discussion. At some point, side-channel protection -- especially the kind where you are trying to protect your process against a malicious host / OS / root process -- is a losing battle. At some point you have to say "best effort" somewhere in your docs. That's not to say that we can't increase our level of effort here in the future, but for today I think this PR is a dramatic increase over what we had before. |
|
@romen How's this for a toy example of applying this to a more complex struct? use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive};
/// Holds a system user
#[derive(Clone, Copy)]
struct User {
userid: i32,
name: [u8; 64],
}
/// Provide the const ZEROED value for the type.
impl ZeroizablePrimitive for User {
const ZEROED: Self = Self{ userid: 0i32, name: [0u8; 64] };
}
/// We will tag the admins as Secret<User> to give them extra protections against
/// having their info leaked.
struct AllUsers {
users: Vec<User>,
admins: Vec<Secret<User>>,
} |
…et<T> type by being more selective about which components are tagged as secret. This gives about a 5% performance increase over the previous commit.
40c93b0 to
edfe436
Compare
|
The commit above re-works ML-DSA and ML-KEM to make more surgical use of the new Secret type by being more selective about which components are tagged as secret. This gives about a 5% performance increase over the previous commit. As an example, the MLDSAPrivateKey object is now defined as: pub struct MLDSAPrivateKey<..> {
rho: [u8; 32],
K: Secret<[u8; 32]>,
tr: [u8; 64],
s1_hat: Secret<Vector<l>>,
s2_hat: Secret<Vector<k>>,
t0_hat: Vector<k>,
seed: Option<KeyMaterial<32>>,
}which clearly indicates to a human reader which values are secret (ie the actual private key components) and which are not (ie part of the public key). This also gives developers more fine-grained control over which data pays the extra performance penalty associated with zeroization-on-drop. |
…et<T> type by being more selective about which components are tagged as secret. This gives about a 5% performance increase over the previous commit.
f19adae to
d993296
Compare
|
Merged manually, but with a |
The old way we were doing Secret to force a zeroizing Drop was well-intentioned, but likely completely useless since by definition nothing can read a value after it is Drop'd, so the compiler is well within its rights to elide that write. This complete redo of the whole zeroizing architecture does a few things:
bouncycastle-utils::Secret.Secret<T>can be wrapped around anyu8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, and charor arrays thereof. It impl's Deref and DerefMut, so its presence is largely transparent.For example:
Secret<T>doesn't even add any memory overhead ... iesize_of(T) == size_of(Secret<T>), which is cool.bouncycastle-utilscrate is no longer#![forbid(unsafe_code)], which is probably right because eventually we'd need that for constant-time behaviour anyway, but with this architecture, the unsafe rust is contained to the utils crate and does not infect the algorithm crates.If merged, replaces #36 and closes #32.
Reviewers should start with
crypto/utils/src/secret.rsas this is the core new thing that needs to be scrutinized.The rest is a massive refactor to use the new construct (which went surprisingly easily, which I guess validates the design).
This PR is almost a net decrease in lines-of-code since it removes the requirement for all structs to impl Drop, Debug and Display, and instead delegates that to the new
bouncycastle-utils::Secretobject.