-
Notifications
You must be signed in to change notification settings - Fork 11
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
feature: implement the RustCrypto KEM API for ClassicMcEliece #16
Conversation
src/kem_api.rs
Outdated
|
||
/// A wrapper wrapping the bytes of a ClassicMcEliece public key. | ||
#[derive(Debug)] | ||
pub struct PublicKey([u8; CRYPTO_PUBLICKEYBYTES]); |
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.
See my issue and comments in #11. I really don't think these should be treated as second class just needed to be able to implement the KEM API. I think these should replace all usage of [u8; SOME_CONSTANT]
in the public API of this crate.
I don't think we should have a separate ClassicMcEliece::keypair
associated function that is basically just a less flexible copy of classic_mceliece_rust::crypto_kem_keypair
. I think we are good with only the standalone function. But it should instead deal with nice PublicKey
and SecretKey
types. These types can exist even with the kem
feature turned off.
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.
(I think crypto_kem_keypair
is a strange name. So I'm going to use keypair
for the rest of this message/PR review 😛 )
I would love to see something like this exposed at the crate root:
pub fn keypair<R: CryptoRng + RngCore>(
public_key: &mut PublicKey,
secret_key: &mut SecretKey,
rng: &mut R
) -> Result<(), Error>
Yeah, that also involves not using Box<dyn Error>
as return type as well. This will never reach no_std
while depending on Box
🙈. But that is a separate topic.
src/kem_api.rs
Outdated
impl ClassicMcEliece { | ||
pub fn keypair<R: CryptoRng + RngCore>(&self, rng: &mut R) -> (SecretKey, PublicKey) { | ||
let mut sk = [0u8; CRYPTO_SECRETKEYBYTES]; | ||
let mut pk = [0u8; CRYPTO_PUBLICKEYBYTES]; |
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.
I really think we should maintain the ability to control where this is stored, and not be forced to have it on the stack.
Just storing the large PK in a wrapper struct also has enormous stack usage, nothing to be done for no_std, but there should be an option to box the KeyPair/Ciphertexts.
There is hope for no_std
also. But if you simply store it on the stack like this, then no. Then unoptimized code will have to pay the huge stack size for every call. Imagine if you abstract this into some other type, so keypair
is a few levels down, and the pubkey is returned up a few functions. That is going to blow your stack away. BUT even without alloc
there are better solutions for no_std
. One solution that is not elegant, but works, is the output arguments public_key: &mut [u8; CRYPTO_PUBLICKEYBYTES]
.
I think that this PR is currently not breaking the API, right? Was that so we can get something out faster? The main branch in this repository has already broken the API compared to the last release. So the next release will be breaking anyway. So I think we better clean as much as possible up at once instead. But that's just my opinion. There will be yet another breaking release once the crate can support all variants at the same time instead of having feature flags also. Unless we want to roll that into |
I'd also be partial to yanking 1.0, since it is hard to use correctly at all and publishing this under 1.0.1. @prokls any input on this? Maybe a simple option would be to have a second |
1.0.1 is already published. So 1.0.2 is the lowest we can go. But personally I think that's a bit dirty. To completely break the API and make it look like some kind of patch release. There is no known bug/issue with the 1.0.x branch, except it being hard to use and should not be used 😂. I don't see any reason against going
I definitely don't like out parameters. But I'm also not a fan of this suggestion. It's simply less flexible. It only allows stack and heap allocated buffers. Consider this potential way of storing your buffer as a global static: static BUF: Mutex<[u8; 1024]> = Mutex::new([0; 1024]);
fn main() {
let mut public_key = BUF.lock().unwrap();
keypair(&mut public_key);
// do something with `public_key`
} The best would IMHO be if we could better encode the difference between storage to be used for key material and a filled in key. To make it harder to accidentally use an uninitialized buffer as key material. This should be a fairly common problem to solve for out parameters, so I'm surprised if there is no almost-ready solution for this. But otherwise I guess it's some boilerplate with traits etc. Where |
I don't think the "boxed variant" should be This library should not define a special heap type. That should be on the user to construct by just combining existing types and heap allocation methods. I don't think this library should get involved in how the allocation happens, at all. It should just take in a mutable reference to where to put the data. |
I'd kind of hate if one could create uninitialized objects even with the new API. I think Out-Parameters are just not used if at all possible most of the time and I also don't like them. I also don't know how well the creation of boxed Arrays or Boxed array-wrapper structs works in practice right now, I think there were still some cases where a copy was created on the stack as an intermediate step when boxing an array, will have to look into that in more detail. |
Maybe I misunderstood you. But the standard library I really don't like them either. But the opposite is worse IMO.
Only in completely unoptimized builds. When you turn on compiler optimizations these copies are removed is my experience. |
How will you avoid that? Even with |
What I have in mind is something like the following. We still take a mutable buffer as the argument to the function. Nothing changes there! But the return type is changed from nothing to typed keys. Then we implement the appropriate traits and methods on those types (like you have already done). The difference is that those types don't contain the owned array. They just contain references to the key material. That way they can be constructed from a versatile set of buffer sources. And only this crate can instantiate the key types. use std::error::Error;
use std::sync::Mutex;
pub const PUBLIC_KEY_SIZE: usize = 1024;
pub const SECRET_KEY_SIZE: usize = 9876;
pub const CIPHERTEXT_SIZE: usize = 1235;
pub struct PublicKey<'a>(&'a [u8; PUBLIC_KEY_SIZE]);
pub struct SecretKey<'a>(&'a [u8; SECRET_KEY_SIZE]);
pub struct Ciphertext<'a>(&'a [u8; CIPHERTEXT_SIZE]);
impl Decapsulator<Ciphertext> for SecretKey {
/*...*/
}
impl EncappedKey for Ciphertext {
/*...*/
}
fn keypair<'a>(
secret_key_buffer: &'a mut [u8; SECRET_KEY_SIZE],
public_key_buffer: &'a mut [u8; PUBLIC_KEY_SIZE],
) -> Result<(SecretKey<'a>, PublicKey<'a>), Box<dyn Error>> {
// Compute the keys
secret_key_buffer[0] = 1;
public_key_buffer[0] = 1;
Ok((SecretKey(secret_key_buffer), PublicKey(public_key_buffer)))
}
// Demonstrate how versatile it becomes to use the above function:
fn main() -> Result<(), Box<dyn Error>> {
let mut stack_secret_buffer = [0u8; SECRET_KEY_SIZE];
let mut stack_public_buffer = [0u8; PUBLIC_KEY_SIZE];
let (stack_privkey, stack_pubkey) = keypair(&mut stack_secret_buffer, &mut stack_public_buffer)?;
let mut heap_secret_buffer = Box::new([0u8; SECRET_KEY_SIZE]);
let mut heap_public_buffer = Box::new([0u8; PUBLIC_KEY_SIZE]);
let (heap_privkey, heap_pubkey) = keypair(&mut heap_secret_buffer, &mut heap_public_buffer)?;
static STATIC_SECRET_BUFFER: Mutex<[u8; SECRET_KEY_SIZE]> = Mutex::new([0; SECRET_KEY_SIZE]);
static STATIC_PUBLIC_BUFFER: Mutex<[u8; PUBLIC_KEY_SIZE]> = Mutex::new([0; PUBLIC_KEY_SIZE]);
let mut static_secret_buffer_lock = STATIC_SECRET_BUFFER.lock().unwrap();
let mut static_public_buffer_lock = STATIC_PUBLIC_BUFFER.lock().unwrap();
let (static_privkey, static_pubkey) = keypair(&mut static_secret_buffer_lock, &mut static_public_buffer_lock)?;
Ok(())
} |
There will be a new Regarding your proposal, I had something similar in mind as well, however the ergonomics of that solution are also not great, since you may need to keep/pass both the buffer and the Key object around. A different variant would be to have the above types, but wrap the inner storage in |
Are there any updates on this? That keyword has been very experimental since Rust 1.0 and last time I read about it it did not sound like it would ever be stabilized, rather be removed. |
Seems like you are right based on rust-lang/rust#49733 |
The |
So which of these options would you (@faern @prokls) prefer:
Personally I feel structs wrapping arrays or slices is the best compromise to address most of the pain points. |
I'd like to avoid compromises if possible :) If we can figure out something that:
That would be golden. What about below. You need to pass in a storage object to the functions. But instead of being just a use std::mem::{size_of, size_of_val};
pub const PUBLIC_KEY_SIZE: usize = 98765;
pub struct PublicKey<S: KeyStorage<PUBLIC_KEY_SIZE>>(S);
impl<S: KeyStorage<PUBLIC_KEY_SIZE>> PublicKey<S> {
/// Helper to move a key backed in any possible way to the stack
pub fn to_owned(&self) -> PublicKey<[u8; PUBLIC_KEY_SIZE]> {
PublicKey(*self.0.as_ref())
}
/// Helper to move a key backed in any possible way to the heap
#[cfg(feature = "alloc")]
pub fn to_heap(&self) -> PublicKey<Box<[u8; PUBLIC_KEY_SIZE]>> {
PublicKey(Box::new(*self.0.as_ref()))
}
}
pub trait KeyStorage<const SIZE: usize> {
fn as_ref(&self) -> &[u8; SIZE];
fn as_mut(&mut self) -> &mut [u8; SIZE];
}
impl<const SIZE: usize> KeyStorage<SIZE> for [u8; SIZE] {
fn as_ref(&self) -> &[u8; SIZE] {
self
}
fn as_mut(&mut self) -> &mut [u8; SIZE] {
self
}
}
impl<const SIZE: usize> KeyStorage<SIZE> for &mut [u8; SIZE] {
fn as_ref(&self) -> &[u8; SIZE] {
self
}
fn as_mut(&mut self) -> &mut [u8; SIZE] {
self
}
}
#[cfg(feature = "alloc")]
impl<const SIZE: usize> KeyStorage<SIZE> for Box<[u8; SIZE]> {
fn as_ref(&self) -> &[u8; SIZE] {
self
}
fn as_mut(&mut self) -> &mut [u8; SIZE] {
self
}
}
fn keypair<'a, S: KeyStorage<PUBLIC_KEY_SIZE> + 'a>(mut public_key_storage: S) -> PublicKey<S> {
// Compute the key and fill in the buffer
let key_buffer = public_key_storage.as_mut();
key_buffer[0] = 1;
PublicKey(public_key_storage)
}
fn main() {
// If we give the ownership of the array we get it back owned on the stack.
// Takes large stack space, but the lifetime is 'static, so it can be passed around
let mut stack_pubkey_buffer = [0u8; PUBLIC_KEY_SIZE];
let pubkey = keypair(stack_pubkey_buffer);
assert_eq!(size_of_val(&pubkey), PUBLIC_KEY_SIZE);
std::thread::spawn(move || drop(pubkey));
// If we lend the buffer to `keypair` we don't eat up our stack, but the returned
// type has a lifetime bound to the buffer, so it can't be passed around as smoothly.
let pubkey = keypair(&mut stack_pubkey_buffer);
assert_eq!(size_of_val(&pubkey), size_of::<usize>());
//std::thread::spawn(move || drop(pubkey)); <- Does not compile
// However, we can always convert it into an owned version of itself
let stack_pubkey = pubkey.to_owned();
std::thread::spawn(move || drop(stack_pubkey));
drop(pubkey);
#[cfg(feature = "alloc")]
{
// If we allocate the buffer on the heap and transfer ownership to `keypair`, it
// can use the heap buffer and return the ownership of the same data without cloning
// or moving. The resulting `PublicKey<Box<...>>` is 'static
let heap_pubkey_buffer = Box::new([0u8; PUBLIC_KEY_SIZE]);
let pubkey = keypair(heap_pubkey_buffer);
assert_eq!(size_of_val(&pubkey), size_of::<usize>());
std::thread::spawn(move || drop(pubkey));
}
#[cfg(feature = "alloc")]
{
// We can even use the heap stored buffer in a borrowing way
let mut heap_pubkey_buffer = Box::new([0u8; PUBLIC_KEY_SIZE]);
let pubkey = keypair(&mut *heap_pubkey_buffer);
assert_eq!(size_of_val(&pubkey), size_of::<usize>());
// std::thread::spawn(move || drop(pubkey)); <- Does not compile
drop(pubkey);
}
}
|
This seems quite nice, I guess I'll try to integrate this in this PR later today. Maybe also exposing a second keypair method without parameters on Since the ciphertext (<=240 bytes) and shared secret (32 bytes) are small anyway, it is probably ok to not do this for them, but keep them as arrays or |
This sadly not that compatible with the KEM API: impl<S: KeyStorage<CRYPTO_PUBLICKEYBYTES>> EncappedKey for Ciphertext {
type EncappedKeySize = CryptoCiphertextBytesTypenum;
type SharedSecretSize = typenum::U32;
type SenderPublicKey = ();
type RecipientPublicKey = PublicKey<S>; // <-- this is not constrained in the impl
fn from_bytes(bytes: &GenericArray<u8, Self::EncappedKeySize>) -> Result<Self, kem::Error> {
let mut data = [0u8; CRYPTO_CIPHERTEXTBYTES];
data.copy_from_slice(bytes.as_slice());
Ok(Ciphertext(data))
}
} Since it wants the public key to be a concrete type, we would also need the
would work, but this also does not seem that nice. |
Would you be able to push your WIP branch somewhere? So I can play with it? |
@faern see the latest commit, I tried it with the The example in the (EDIT: I guess the Readme test still stack-overflows on the github-runners, but it works locally, I guess the default stack size of those runners is configured lower). |
I now question whether or not we actually want to allow passing the huge public keys on the stack at all. I mean the use case enabled by
So in other words: The public keys probably never want to be on the stack, because it costs too much memory. And the secret keys should probably be kept separate as well, to make it possible to |
I was thinking this as well, since most users do not actually want to have this on the stack ever. Make the crate use Regarding fallible RNGs, I don't know if we want this or not, since it is the only thing requiring error handling atm, and I most callers will not use fallible RNGs or can just seed one from the fallible one. |
I'm currently working on an experiment with this. I'm trying to represent the keys and key storage buffers in a different way. I hope to have something WIP to show today or in a few days. |
Basically I don't think we need to represent all keys/buffers in the same way. Because they have different properties.
|
I'm fighting with My impl of the keys basically work now. The only thing left is this wart with zeroing the keys when using the kem interface. But maybe that interface is simply not compatible with clearing secrets from memory 🤷 |
Seems like this is an inherent problem with Rust/Zeroize: https://docs.rs/zeroize/latest/zeroize/#stackheap-zeroing-notes |
Exactly. To implement the KEM traits one has to pass around the values owned on the stack a lot. So yeah, we can ignore that part. |
See #20 for my attempt at doing this. Or rather this, plus some other ideas plus I felt like I had to do it all in one go. To prove to myself, and you, that it would all fit together and actually work. |
Superseded by #20 |
This is the first attempt for the RustCrypto KEM API.
A few growing pains:
generic_array
since const generics are not far enough yet, is a bit of a hassle with typenum.no_std
, but there should be an option to box the KeyPair/Ciphertexts.Any comments @faern @prokls ?