On February 20, 2018 at 4:21:32 AM, Andrew W. Donoho (
Thank you for your fine answers. They comport with my expectations.
Without boring you with my details, I am doing something similar to an
ephemeral ECDH exchange. The secrets have a very short lifetime. Hence, I’m
not worried about swapping to disk. (Though most of the cloud vendors claim
the swap vm is encrypted.)
secret_bytes = os.urandom(384//8)
secret_int = int.from_bytes(secret_bytes, byteorder='big')
private_key = ec.derive_private_key(secret_int, ec.SECP384R1(), backend)
I would like secret_bytes, secret_int, and private_key to be zeroed-out
before I exit the function. I want as precise an object lifetime as
possible. While I fully understand the values of a library maintainer
wishing to have a robust solution for all supported versions and runtimes,
I have explicit control of this function and its execution environment. I’m
more concerned about revealing the secret in a hostile cloud. In
particular, I don’t have the string aliasing problem. Yes, this is a
fragile and extremely hazardous solution. OTOH, the industry just freaked
out about Meltdown. It behooves us all to manage key lifetimes carefully.
As I dig into examining this problem further, I assume the
ec.derive_private_key() function takes a conservative approach and doesn’t
ever copy the secret_int? If I’m living well, banging the bytes in
secret_bytes might hit the only copy of the secret. Depends upon whether
int.from_bytes() copies the storage of an immutable source.
Access to Python's memory (via side channel or dumping as root) is not part
of pyca/cryptography's threat model at this time so we don't attempt to
protect against it. Making it part of our threat model would be difficult
due in part to the reasons you stated above as well as the difficulty in
writing tests to prevent regression, but let's talk about what CPython does
in this case.
int.from_bytes will unfortunately make a copy (to a Python integer). That
int will then be copied into a BN via _int_to_bn (
when you call derive_private_key. It will actually be converted twice (a
thing we should fix) (
Although the resulting BNs will themselves be zeroed as freed, this means a
secret scalar bytestring created in Python will be resident in memory no
less than 5 times (3 byte strings, 2 numbers).
Obviously the next logical question is why you'd provide a Python integer
when we're just going to convert it back to big endian bytes anyway.
Disregarding the memory clearing issue it's also inefficient. When
originally designing some of the APIs we made a mistake and chose integers
instead of big endian bytes (see: numbers classes). We have not yet added
alternate APIs to potentially enable us to deprecate numbers because the
improvement in efficiency probably isn't worth the pain of trying to
convert the huge number of users of those classes.
ec.derive_private_key_from_bytes(secret_bytes, ec.SECP384R1(), backend)
could potentially be a way to do this specific operation while reducing the
number of copies (to zero in Python and 2-3 in OpenSSL, although the latter
are zeroed), but without tests that can detect non-required copies of
secret material it would be extremely hard to prevent regression in the
long term as the code is updated.
Given your chosen constraints have you considered deriving a key in
subprocess, serializing it, and reading it from stdout in the parent
process? By doing this you'd have a precisely defined intermediate object
lifetime and the only secret in the parent process's memory would be a
single DER or PEM bytestring containing the EC key.
-Paul Kehrer (reaperhulk)
Cryptography-dev mailing list