-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implement insecure-erase
feature (was: Zeroize
)
#582
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this an optional, default-off feature so we're not adding a dependency for downstream crates that don't use this?
Ah, excellent point. Done! |
See also discussion on #553 . Aside from issues of feature-gating, I don't understand how |
I agree that in many cases Zeroize isn't much help because of the Copy impls---but it doesn't hurt, and as long as it's feature gated it also doesn't add any dependency overhead. Speaking to my particular application: I have a SecretKey field in a non-Copy struct, and I'm perfectly happy to pin that struct in order to ensure that Zeroize does its thing. I'd even be willing to manually implement Zeroize for that struct, but there's just no way that I can see (other than In other words: as far as I can tell, it's currently impossible to securely erase SecretKeys other than by using unsafe transmutations, even for libraries that are happy to pin, use std, avoid Copy, etc. This PR doesn't fully solve the problems, but it does change the situation from "impossible" to "possible but hard to do right". Would it help to address your concerns if I added a paragraph in the crate docs with a brief rundown of the limitations and a pointer to the Zeroize documents for more info? |
Cargo.toml
Outdated
@@ -35,6 +35,7 @@ global-context = ["std"] | |||
# if you are doing a no-std build, then this feature does nothing | |||
# and is not necessary.) | |||
global-context-less-secure = ["global-context"] | |||
zeroizable = ["zeroize", "secp256k1-sys/zeroizable"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Features should be named the same as crates. You can rename the crate if you need to activate additional features. (see how it's done for serde
in bitcoin
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Unfortunately, renaming the crate doesn't seem to work here: it seems like the renaming breaks the derive macro. (I could easily be doing something wrong, though!)
I'll leave this open for now. If the direction of the conversation changes and it starts to look like this PR has a chance at being merged I can revisit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If zeroize
doesn't support an attribute similar to serde(crate = "...")
then I suggest to open an issue there first.
@kwantam using unsafe code there are a couple ways you can modify the underlying backing store:
I agree that from a purely technical point of view, there's "no harm" in adding a feature-gated Instead I think we should just make it possible to get the underlying backing store out of a
|
Great point @apoelstra! I personally like Also I just realized that if we do not provide this then the only sound |
@apoelstra I completely understand the maintenance burden, particularly regarding MSRV, and @Kixunil thanks very much for the helpful review and for your thoughts on alternatives. Of the alternatives @apoelstra lists, my preference would be I understand @Kixunil's point about how this leaks an abstraction, meaning that future optimizations might become breaking changes. Y'all obviously know much better than I do, but as far as I can tell the underlying representation of If that works for you, I'll just transmute this PR into an Thanks again for the very helpful discussion on this! |
Apologies for the double comment. Coming back to correct myself. I just realized that in fact // *** borrowed from zeroize ***
#[inline(always)]
fn atomic_fence() {
std::atomic::compiler_fence(std::atomic::Ordering::SeqCst);
}
#[inline(always)]
fn volatile_write<T: Copy + Sized>(dst: &mut T, src: T) {
unsafe { std::ptr::write_volatile(dst, src) }
}
// *** end borrowed from zeroize ***
impl SecretKey {
/// big warning goes here
pub fn insecure_erase(&mut self) {
volatile_write(&mut self.0, [1u8; constants::SECRET_KEY_SIZE]);
atomic_fence();
}
}
// and likewise for other secret-containing structs |
I'd be fine with this. I agree that it seems extremely unlikely that we'll ever change
I like it!! Thank you for keeping the name |
I specifically said
Not a native but to my knowledge "insecure" means having insecurity, essentially being afraid of something. So we should either say And can someone explain why the atomic fence is used? I don't think it should be needed. |
Zeroize
impl for all secret-related structsinsecure-erase
feature (was: Zeroize
)
OK, I've taken a first cut at |
README.md
Outdated
@@ -38,6 +38,10 @@ 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 | |||
bench marks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench --features=recovery`. | |||
|
|||
### A note on the `insecure-erase` feature | |||
|
|||
The `insecure-erase` feature is provided to assist other libraries in building secure secret erasure. When the `insecure-erase` feature is enabled, secret types (`SecretKey`, `KeyPair`, `SharedSecret`, `Scalar`, and `DisplaySecret`) have a method called `insecure_erase` that *attempts* to overwrite the contained secret. This library makes no guarantees about the security of using `insecure_erase`. In particular, since all of these types are `Copy`, the compiler is free to move and copy them, thereby making additional copies in memory. For more information, consult the [`zeroize`](https://docs.rs/zeroize) documentation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since all of these types are
Copy
, the compiler is free to move and copy them
This is misleading, even non-Copy
types can be copied by the compiler. Copy
literally means "doesn't run destructors", nothing else.
It should say something like "In particular, the compiler doesn't know that this is a secret so it may arbitrarily copy it anywhere it pleases."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point. Reworded!
Apologies for misunderstanding! What you say here makes total sense.
The word "insecure" can be used in the way that's meant here. For example, one could talk about an "insecure hash function". "Unsecure" is not in modern use (the Oxford English Dictionary lists it as obsolete). The closest is the adjective "unsecured", which (to my knowledge) is most commonly used to refer to financial products: "an unsecured loan". "non_secure_erase" would be totally fine and is, as far as I can tell, almost perfectly synonymous with "insecure_erase". The only disadvantage is the slightly longer name.
The zeroize crate has a comment saying that the atomic fence is to prevent later reads from being reordered. I'm guessing this is because the semantics of |
Oh, I just realized this whole PR is not needed to implement zeroize! Just write this code: let dummy = Secret::from_slice(&[1; 32]).unwrap();
volatile_write(&mut the_keypair_you_want_to_erase, dummy);
// fence, if it's actually needed |
The code in question should run in destructor, so there should be no more reads afterwards (other than some horrible UB accessing uninitialized memory - pretty certain the memory will get corrupted anyway). Even if we wanted to prevent those |
Yes, I think you're 100% right if we assume that erasure will only ever happen in a destructor, but I think that's not an assumption the zeroize authors are making, hence their use of the memory fence. Regarding memory ordering: I think you're right that it's possible to choose a slightly weaker constraint than SeqCst. I'm less sure that trying to weaken the fence here is worthwhile: if the goal of this change is to enable folks to implement |
Yes, 100% agreed. I guess this is an instance of the "zeroize with unsafe code" approach discussed earlier in the thread. On the other hand, it would be nice to provide this implementation in one place so that consuming libraries don't all have to implement it themselves. This is especially handy since the only use of |
I don't think we should feature-gate this. The point of calling it I don't have any strong feelings about the memory fence. I guess we should have a clear justification for why we do what we do, so we're not cargo-culting, but I also feel like using anything but Other than that, ACK 6b0961c |
Oh, and I do mildly prefer My reason for choosing |
Just pushed a commit that removes the feature gates and rewords the text in the README accordingly. We can easily revert if the discussion pushes us back in the other direction.
If it would help, I can try to ping the authors of the |
My vote is to just stick with @kwantam oof, these CI failures do not look fun. I believe the reason is that you've hardcoded an internal representation of the all-1s keypair, but this internal representation is different on systems of different endianness. The most straightforward approach may be to hardcode two versions, switched based on |
Ah, hrm. Makes sense. I need to step away from this PR for the next few hours, but I'll take another look tonight. Thanks for the pointer! |
OK! All the cross tests work locally for me now. BE32 and BE64 needed different constants. If having three different constants feels too ugly, a couple obvious alternatives:
I'm in favor of keeping the constants; seems like the least bad option. |
I think I'd prefer the ugliness of the multiple constants to this. I may consider PR'ing upstream to see if they'll expose some "dummy key" constants.
Heh, no, I think this is a really bad idea. It would be literally a 10000x performance hit.
:) I did consider this actually, but I am convinced that it's computationally impossible. |
Could you squash/rebase this so that each PR is reviewable individually? |
I think probably you could just squash everything into one commit. |
Done! Thanks a lot for thinking through this with me. |
Since you didn't mention it, I assume this PR should not bump the version number. But please let me know if you'd like me to do so. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All my comments are superficial documentation improvements. Feel free to ignore them, the code changes look good to me. I've pushed the changes to my tree and I can do a follow up PR if you want me to (branch 02-21-non-secure-erase-docs
branch on my tree) or you can grab them from there.
ACK 2865863
Thanks @tcharding! I squashed in the branch |
Do they just blindly stick (overly-strong) fence everywhere? Sounds wrong. I would suggest we leave it off and document it.
I wouldn't say so. Without any support the code would either have to be unsound or the secret keys would have to be stored inside
That seems to promote an unhealthy relationship with Looking at the implementation I find it highly concerning that we are manually writing the contents of structs that are supposed to be internal to Also I noticed there are still several instances mentioning |
Thanks again @Kixunil for the helpful feedback!
The default implementations use a memory fence. It's possible that it's overly strong. It seems more likely to me that the zeroize authors have thought about this a lot longer than I have, and have a reason for choosing Stepping back: the goal, from my perspective, is to provide library implementors with the tool they need to implement a conforming zeroize implementation in structs containing types from this library. Given that, I have a strong bias towards remaining consistent with what zeroize would provide if we'd instead just added
I completely agree with you! We should certainly not be scared of
👍 absolutely crucial, as you say. There's currently a test in
Great catch, thank you. I'll fix those right now. |
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]
People who want it can just call the fence right after calling our function. People who don't (e.g. they are only calling it in destructors) don't have to pay the cost. Libraries should strive to be flexible and this is more flexible without affecting ergonomics greatly. |
I think there are two important questions here: what are we optimizing for, and what will the result of that optimization be? The second one is easier to answer: there will be almost exactly zero performance improvement as a result of removing this fence. Every other zeroize implementation under the sun already has a fence, and on every processor I'm aware of the performance impact of multiple fences stacked up is deeply sublinear in (or even negligibly dependent on) the number of fences. So when we call Case in point: fn _bench_fence<const FENCE_PERIOD: usize>() {
let mut foo = [16u8; 32];
for i in 0..(1 << 25) {
let j = (i % 256) as u8;
unsafe { ptr::write_volatile(&mut foo, [j; 32]); }
if i % FENCE_PERIOD == 0 {
atomic::compiler_fence(atomic::Ordering::SeqCst);
}
}
unsafe { ptr::write_volatile(&mut foo, [17u8; 32]); }
atomic::compiler_fence(atomic::Ordering::SeqCst);
} Results are below.
In sum: stacking up more fences just doesn't matter. This is partly because the volatile writes are far more expensive than the fences, and partly because the performance impact of going from zero fences to one fence completely swamps the performance impacts of additional fences. Removing this fence just isn't a meaningful optimization in the vast majority of expected uses of this function, i.e., as part of a larger zeroize implementation of a containing struct. Back to the first question: what are we optimizing for? If we're optimizing for performance of clearing secrets out of memory, the above benchmarks give pretty good evidence that this optimization isn't doing much. On the other hand, if we're optimizing for the fewest ways for a user to shoot themselves in the foot, it seems pretty obvious that adding a memory fence is the right way to go. So as far as I can tell, removing the fence gains us nothing (performance-wise) and potentially loses a lot (in terms of foot-guns). |
Nice analysis! OK, let's keep the fence in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW ACK 8fffbea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 8fffbea
This PR adds
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 implementZeroizeOnDrop
for structs that contain such secrets and/or simply to use theZeroizing
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 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!