This keeps coming up, but I think it's a very, very bad idea. It's false security.
If you are running in an environment where you don't trust code running in the same compartment/sandbox/process, then it's futile to zero out memory. The caller could have prepared things such that the memset doesn't work, if the key material went somewhere else.
If you ever find yourself thinking you need to do this, what you instead need is a helper process who's only purpose is to do primitive operations with sensitive key material.
Particularly as Rust is already a "safe" language - it doesn't even make sense to zero memory which by definition another piece of code can't access. Unless there's declared "unsafe" code lying around, but you wouldn't put that in the same process, would you? At which point, what are you even protecting against? If an in-process threat is that advanced, then you're not achieving anything.
Zeroing memory protects against future compromise. Sure, if you were already compromised, you already lost; but if you don't zero cryptographic material and you are compromised in the future (which can even be a physical attack like cold boot), you lose.
It's the same idea as in "forward secrecy": once the key material is discarded, it's gone, and no future compromise can bring it back. Being able to say "after this point in time these values don't exist anymore" is a powerful cryptographic primitive.
This: exactly this. Zeroisation is a fundamental cryptographic primitive, probably the most fundamental. Conceptually simple, but very easy to screw up, and has catastrophic consequences if the assumption is violated.
I want to be able to have an ephemeral secret, and then trust the code to do its very best to get rid of it when it's no longer needed. That doesn't mean just leave it rotting on the heap and promising it doesn't get accesed again, it means burning it to make sure. That's the underpinning of any possible proof of forward security.
Sure, you say, I want a helper process? Fine idea: compartmentation. Now that helper process needs secure zeroisation. And since I want it to be secure, surely I want to write that process in Rust. See where I'm going here?
Whether 'safe' code I trust within my environment can read it again is totally irrelevant, if the machine later gets rooted or booted, nonstopped or whatever.
Zeroing out memory after use is mitigation against security flaws like heart bleed. It's not a security feature in it self. Although you are right that the secure process is probably just the better solution anyway.
It's sort of like the arguments for DRM. Of course pirates will always break it, but we can still mitigate it in a number of ways.
Not false sense of security: suspending VMs to disk. The contents of previously used, free memory that has not been reused by the kernel are written to disk. Now it has a lifetime far longer than RAM.
So you're essentially trying to protect against a hypervisor attack? That's never going to work unless you put the primitives in the hypervisor. Assuming you own it.
No, it wouldn't. For example, the "Heartbleed in Rust" blog post [1] re-used a buffer without freeing it. No destructor runs in between the two uses, so a zeroing destructor could not possibly prevent the bug.
Maybe zeroing destructors make sense as defense-in-depth, but I don't see how they can fix a Heartbleed-style exploit in Rust. In code where the buffer is freed and its destructor runs, Rust's memory safety guarantees already prevent it from being accessed after free. In vulnerable code that just uses the same buffer twice, the destructor never has a chance to run so its behavior doesn't matter.
The real Heartbleed vulnerability (CVE-2014-0160 in OpenSSL) involved reading into uninitialized memory in a newly-allocated buffer, which safe Rust code already prevents [2].
Thanks - that's a good example of what I was trying to convey.
The point is Rust already provides safety guarantees. If you don't trust the runtime, then why would you trust the built-in zero'ing? I get the "defense in depth" argument, but it feels a bit like doing this:
{
int a = secret; // Get secret.
assert(a == secret); // Check "a" is actually that.
a = 0; // Ensure "a" is zero'd on exit.
assert(a == 0); // Just because.
}
And yes, I get that you can build this into the language so it's not quite as ridiculous - you actually wipe tainted stack, for example.
But the point is: the runtime has an ABI and a machine model. Information is allowed to leak across function boundaries, beacuse it doesn't matter. Without using the "unsafe" keyword, there are no methods of getting around the machine model and dipping into the underlying actual machine.
Even if you don't have a "safe" language and runtime, it's still of limited value. It protects against threats involving data or control flow corruption after key usage, and where there isn't sufficient control of the program to perturb the secret-consuming functions. That's more of an annoyance than prevention. On the other hand, it gave the programmer a false sense that it was properly wiping secrets.
It is very probable that a sufficiently smart optimizer could see the assertion was always true and delete it. Then see no one reads "a" and delete it as well. In certain circumstances this can cause a secret to be leaked in, say a register, making our safe function unsafe. You need to be very careful writing secure code, and probably need to go down to the level of writing assembly to be sure the optimizer isn't turning your safe code into unsafe code.
We actually have an interesting project in rust where someone is writing a syntax extension to take rust like code and generate assembly [0]. It's probably unsafe to use right now but if sufficiently well implemented it could be the foundation of a lot of interesting cryptography work.
Sorry, I wasn't clear enough that my code was intended as sarcastic. It's obviously silly to zero variables because the compiler is free to ignore you. The point is, the underlying machine is going to do the same.
There are many ways to dig out stale memory if you're running at sufficient privilege, for example. Direct cache introspection, for example, or bypass. Zero-izing alone is not sufficiently strong to mitigate the threats people imagine it works against.
The heartbleed "issue" in Rust involved code that passed the same array to two functions, making it apparent that the array was never getting destroyed. Since they used a custom memory allocator, secure destructors won't help you since your array might not even get destroyed. Lessons: Don't use custom allocators.
If you are running in an environment where you don't trust code running in the same compartment/sandbox/process, then it's futile to zero out memory. The caller could have prepared things such that the memset doesn't work, if the key material went somewhere else.
If you ever find yourself thinking you need to do this, what you instead need is a helper process who's only purpose is to do primitive operations with sensitive key material.
Particularly as Rust is already a "safe" language - it doesn't even make sense to zero memory which by definition another piece of code can't access. Unless there's declared "unsafe" code lying around, but you wouldn't put that in the same process, would you? At which point, what are you even protecting against? If an in-process threat is that advanced, then you're not achieving anything.