-
Notifications
You must be signed in to change notification settings - Fork 200
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
AEAD Trait #40
AEAD Trait #40
Conversation
aead/src/lib.rs
Outdated
/// An enum describing possible failure modes | ||
#[cfg(not(feature = "serde"))] | ||
#[derive(Clone, Copy, Debug, Eq, Fail, Hash, Ord, PartialEq, PartialOrd)] | ||
pub enum AeadError { |
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 it might be worth considering an opaque error type here, and in general look at some papers, resources, and case studies on misuse resistance in the design of AEAD APIs.
This sort of information, leaked to the attacker, can potentially be used for attacks. Whether or not that is possible will depend on the code which impl's this trait, but I think it's very much worth considering.
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.
Even beyond that, AEADs built using the Encode-then-Encipher paradigm (HCTR, HCH, AEZ, and various others), which provides the maximal misuse resistance (RAE, in Rogaway's terminology), are incapable of distinguishing many of these. As a result, there exist AEADs that will have to choose which kind of misrepresentation they will perform, because this error type forces them to distinguish between things they cannot.
aead/src/lib.rs
Outdated
+ Display | ||
+ Into<Vec<u8>> | ||
+ Send | ||
+ Sized |
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.
This bound disallows the use of variable-width nonces. Whether these are a good idea is debatable, but I'll point out this trait is incompatible with Miscreant in its current form:
https://github.com/miscreant/miscreant.rs/blob/master/src/aead.rs#L57
aead/src/lib.rs
Outdated
} | ||
|
||
/// A standard nonce implementation for AEAD algorithms which do not use | ||
/// explicit nonces. |
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.
Which algorithms are these? Is there precedent for these being used as part of an AEAD interface?
When I generally think of implicit nonces, or algorithms that do not need nonces (e.g. the many SIV algorithms implemented by Miscreant), I think of them as using an interface which lies below the AEAD abstraction layer (e.g. the SIV interface), or protocols which exist above the AEAD layer (TLS/Noise)
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.
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.
Well, that answers my question, but this seems like a single, draft, nonstandard AEAD construction (which self-identifies as such, per 2.1) which probably shouldn't figure into the design of the trait as a whole, except that the bounds on Nonce
are sufficiently broad to allow concretely representing an empty nonce..
aead/src/lib.rs
Outdated
/// encryption is meant to be managed internally by the implementation itself. | ||
pub trait AuthenticatedBlockCipher: Sized + Send + Sync | ||
where | ||
for<'ciphertext> Vec<u8>: From<&'ciphertext Self::Ciphertext>, |
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'm seeing a lot of HRTBs in this PR and having used them in the past I think they're often unnecessary and desirable to avoid if possible. Just a thought.
|
||
#![no_std] | ||
|
||
extern crate alloc; |
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.
This precludes usage on platforms without a heap. It'd be nice to gate this on an alloc
feature and make the core APIs no_std
-friendly with allocating wrappers. See Misreant's seal_in_place
:
https://github.com/miscreant/miscreant.rs/blob/master/src/aead.rs#L57
...vs its seal
wrapper which allocates:
https://github.com/miscreant/miscreant.rs/blob/master/src/aead.rs#L74
aead/src/lib.rs
Outdated
/// Encrypts the given plaintext into a new ciphertext object and the nonce | ||
fn encrypt<'ad, RngType: CryptoRng + RngCore>( | ||
&mut self, | ||
csprng: &mut RngType, |
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 assume this is to randomly select the nonce. That's great from a misuse resistance perspective, however for protocols that use implicit nonces, they need to explicitly specify it.
I guess that's what you were going for with EmptyNonce
? However if you're using an empty nonce, this parameter is irrelevant.
Overall I think this API is mixing concerns that belong at different layers.
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.
It was originally added to randomly select an IV for the draft linked above.
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.
It seems a lot of the eccentricities of this draft are leaking out in this API design, whereas a generalized AEAD trait needs to be broad enough to cover much more than just that.
aead/src/lib.rs
Outdated
additional_data: impl Iterator<Item = &'ad [u8]>, | ||
nonce: Self::Nonce, | ||
ciphertext: Self::Ciphertext, | ||
) -> Result<Vec<u8>, AeadError>; |
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.
As mentioned earlier, I think it'd be nice for the core trait API that algorithm providers impl
be no-std friendly. The rest of the RustCrypto crates are, so forcing allocation here is "missed it by that much"
This seems like a good start, but it'd sure be nice if it were truly |
I think being truly I agree with @tarcieri that we should work out doing AEAD without alloc though. Two questions: Is a low-level "detached" interface that treats head, body, and tail separately wise? Are we happy with some reinvention of We could reduce the number of offsets by creating a new wrapper type for each message, so you'd have a builder for encryption, and ultimately leave the Any convenience methods for use with alloc should prefer |
I could imagine some ATC tricks being helpful, like
|
@burdges even that seems more complex and with more lifetimes than are necessary. I still don't see the need for anything resembling |
Yes, if you know head and tail lengths at compile time, then you can handle them detached:
I think this only punts the problem though because now the user must reassemble them correctly. Right now, Noise explorer makes user's magically supply a correctly padded buffer, which makes debugging impossible. In Noise, In the scheme I provided, It's possible the detached scheme provides a internal foundational layer, from which once builds various convenience methods. I'd maybe do that via inherent impl though, not traits, not sure. |
Some other options:
The first seems like a simple and straightforward approach to me. |
I took roughly that second buffer approach in my linked partial pull to noise explorer, but it's actually way messier than I wanted to go. You cannot make the user know their own message size when they supply the buffer. If you adapt I have not considered all AEAD use cases, only network handshakes, so maybe detached comes up more in a raw AEAD context? |
In fact, we could combine everything so the impl provides the interface you describe, but a wrapper type provides a more friendly builder pattern, thus avoiding ATCs.
We need to figure out the best sugar for types like Bytes too though. |
So to summarize the comments, I see these as the primary things:
For the no-vec change, the suggestion is to make This was part of the original intent of Each AEAD implementation ships with a pair of That is, you take a mutable slice and a length, you give it to a Conversely, a Plaintext getting spit out from the So something like this:
How does that sound? |
It's okay to There are two reasonable options, the You could supply a buffer and a separate length in one call, like C code might do, but such an interface is awkward, error prone, and not idiomatic rust. Do not do this. |
One option I think is worth considering is punting on the question of an "attached" This dramatically simplifies the API and covers the most common cases. The only AEAD-ish modes I can even think of which don't fit this are AES-SIV and AES-PMAC-SIV (but, as it were, AES-GCM-SIV was amended to use postfix tags like other common AEAD modes). This postpones the bikeshed over what exactly the "buffer" API looks like, which is really just much ado about |
I think the prefix vs postfix tags largely comes down to whether the same trait should handle handshakes too. I think noise needs only postfix after the handshake, but noise requires messages during the handshake, ala certs, etc., so you want the same interface there. I've no idea if this even makes any sense for say rustls. |
I don't think doing only the "detached" methods proposed by @burdges actually helps implement RFC5116-compliant AEADs. The ciphertext format is entirely up to the algorithm, which is why you can have the CBC-HMAC draft prepend a 16-byte IV to the ciphertext, and append a MAC, miscreant prepend the tag, and both of them are still well within the lines of RFC5116. Making the user care about what goes where seems like it's just making a lot of pain. The My takeaway from all this is that you really want a mutable slice with some additional constraints (at minimum, it must have at least X bytes of unused space for the algo to fill stuff in). In my experience, "I want this existing type, but I want extra constraints on it" seems like exactly the place to create a new type, hence my description of the Is there a limitation around the |
The detached modes allow you to reassemble the ciphertext however you want. You can override the default impl of the
I think we're in violent agreement about this, but there's lots of bikeshedding happening about the precise way in which to implement it.
You say I think that can all be done with one I'm seeing a lot of bikeshedding and talking past each other here, which I think can be addressed by punting on what that newtype (or types, or associated types, or a type generic around a trait), having an actual concrete trait to think about things in terms of, and it will still fit everyone's use cases, just not be the absolute optimal Make sense? |
I'm suggesting |
Yes, I understand what you're suggesting. I think an alternative worth considering is if instead of creating two newtypes per algorithm, if instead that can be expressed as a single type which is generic around the single But also, I'm suggesting we punt on that debate for now, and move forward on a simple trait with the least common denominator of our many needs. If I'm not expressing myself clearly enough in prose, perhaps I can open an alternative PR? |
I proposed the Ciphertext/Plaintext types approach in https://github.com/SymbolicSoft/noiseexplorer/pull/34 After @georgio ran into troubles doing it, I looked closer and realized I needed like 4ish offsets, not just two. I think the
Yes, you can reduce the offsets by making the buffer type dependent upon I suspect an It's true
I think either attached or detached modes simplify backing the buffers with Vec, Bytes, SmallVec, future VLAs, I agree with @jcape that's you're not actually complying with RFCs when doing detached mode. We all accept that detached mode exists only as glue to simplify everything else.
Any types satisfying these traits would still accepts arbitrary slices though? I do not believe you require so much flexibility, but who knows. Are any AEAD modes commonly used on rope data structures? There is another interesting approach like
which requires the AEAD allocate enough space for the prefix and postfix itself. We must tie the lifetimes of self and the buffers together though, which sounds annoying. And you need yet another allocation before merging, so this works poorly for building |
The big question here is whether this is expected to be a streaming/online AEAD where each element of the iterator can be decrypted as it is received (which is a very fraught topic; see Hoang, Reyhanitabar, Rogaway, & Vizár 2015), or if it's effectively just feeding discontiguous input to the AEAD (which should specify (Edit: added missing EDIT: Additionally, the interface as-written is not an AEAD - it is at most an AE, but even then it has no way to represent failure. It would need something more like this to be an AEAD, and to represent failure: struct DecryptionError;
pub trait AEAD {
fn encrypt<'a, 'd, A, P>(
&mut self,
ad: A,
plaintext: P
) -> impl Iterator<Item = &'d mut [u8]>
where
A: Iterator<Item = &'a [u8]> + Clone + DoubleEndedIterator,
P: Iterator<Item = &'d mut [u8]> + Clone + DoubleEndedIterator;
fn decrypt<'a, 'd, A, C>(
&mut self,
ad: A,
ciphertext: C,
) -> Result<impl Iterator<Item = &'d mut [u8]>, DecryptionError>
where
A: Iterator<Item = &'a [u8]> + Clone + DoubleEndedIterator,
C: Iterator<Item = &'d mut [u8]> + Clone + DoubleEndedIterator;
} (This becomes somewhat unwieldy, yes.) Even so, this still has some key problems:
The In addition, an Furthermore, the Some of this, such as the statefulness, is due to aspects of the draft that diverge from the common definition of "AEAD" in the literature - that is, a deterministic encryption function from a tuple If we take the standard definition at face value (and borrow a bit from Rogaway, in the use of struct DecryptionError;
trait Aead {
fn nonce_len() -> Option<usize>;
fn tau_len() -> Option<usize>;
fn encrypt(&self, nonce: &[u8], ad: &[u8], pt: &mut [u8], tau: &mut [u8]);
fn decrypt<'a>(&self, nonce: &[u8], ad: &[u8], ct: &'a mut [u8], tau: &mut [u8])
-> Result<&'a mut [u8], DecryptionError>;
} (the A Higher-level constructs - such as the draft - can then be cleanly implemented in terms of this, maintaining and framing |
So following the common abstract definition of AEAD, I think the simplest API would look like this: // note: I would prefer to also have methods which will use fixed sizes
// for key and nonce, and ideally methods should be panic-free
trait Aead {
fn new(key: &[u8]) -> Result<Self, CreationError>;
// Encryption result is written to `buf`. Note that we probably should be able to
// support AEAD schemes which prepend and append AD.
fn encrypt<'a>(&self, nonce: &[u8], ad: &[u8], pt: &mut [u8], buf: &'a mut [u8])
-> Result<&'a [u8], EncryptionError>;
// IIUC not all AEAD schemes include nonce into AD, so we may have to
// provide nonce. If scheme does include nonce into ciphertext, nonce argument
// should be ignored. First returned slice is AD, second one is plaintext. We could
// add a helper structure to make it less error-prone.
fn decrypt<'a>(&self, ct: &'a mut [u8], nonce: Option<&[u8]>)
-> Result<(&'a [u8], &'a [u8]), DecryptionError>;
} The problem here is a memcpy of plaintext into the provided buffer, which could unnecessary bound performance of implementation. Also providing An alternative could look like this: trait Aead: Sized {
fn new(key: &[u8]) -> Result<Self, CreationError>;
// Here `f` will be called several times with AD, MAC, ciphertext and paddings.
// If it returns an error, it will be bubbled to the caller.
fn encrypt(
&self, nonce: &[u8], ad: &[u8], pt: &mut [u8],
f: impl FnMut(&[u8]) -> Result<(), EncryptionError>,
) -> Result<(), EncryptionError>;
// Resulting ciphertext length must be equal or less of the hint length
fn ciphertext_len_hint(nonce: &[u8], ad: &[u8], pt: &[u8]) -> usize;
fn encrypt_to_slice(&self, nonce: &[u8], ad: &[u8], pt: &mut [u8], buf: &mut [u8])
-> Result<(), EncryptionError>
{
let mut offset = 0;
self.encrypt(nonce, ad, pt, |v| {
// we can't use split_at_mut inside this closure, so we have to perform
// a small dance to convince compiler to remove panic branch originating
// from the potential overflow of offset after addition
let n = v.len();
if offset > buf.len() { return Err(EncryptionError) }
let buf = &mut buf[offset..]
if n > buf.len() { return Err(EncryptionError) }
buf[..n].copy_from_slice(v);
offset += n;
Ok(())
})?;
Ok(())
}
#[cfg(feature="alloc")]
fn encrypt_to_vec(&self, nonce: &[u8], ad: &[u8], pt: &'a mut [u8])
-> Result<Vec<u8>, EncryptionError>
{
let n = Self::ciphertext_len_hint(nonce, ad, pt);
let mut buf = Vec::with_capacity(n);
self.encrypt(nonce, ad, pt, |v| {
buf.extend_from_slice(v);
Ok(())
})?;
Ok(buf)
}
fn decrypt<'a>(&self, ct: &'a mut [u8], nonce: Option<&[u8]>)
-> Result<(&'a [u8], &'a [u8]), DecryptionError>;
} What do you think? Have I missed something? |
@newpavlov My main issue with doing it precisely that way is that, by having In addition, there are use cases where keeping them contiguous isn't useful to the user - for example, bcachefs' encryption keeps only the non-expanded portion of the ciphertext in the data extents, and stores |
Here's what I'd suggest (I'll just write this with const generics syntax): pub struct Error;
pub trait Aead {
const KEY_SIZE: usize;
const TAG_SIZE: usize;
type Nonce;
fn new(key: &[u8; KEY_SIZE]) -> Self;
fn seal_in_place_detached(&self, nonce: Self::Nonce, associated_data: &[u8], buffer: &mut [u8], tag: &mut [u8; TAG_SIZE]);
fn open_in_place_detached(&self, nonce: Self::Nonce, associated_data: &[u8], buffer: &mut [u8], tag: &[u8; TAG_SIZE])) -> Result<(), Error>;
#[cfg(feature = "alloc")]
fn seal(&self, nonce: Self::Nonce, associated_data: &[u8], plaintext: &[u8]) -> Vec<u8> {
let mut buffer = vec![0; plaintext.len() + Self::TAG_SIZE];
buffer[Self::TAG_SIZE..].copy_from_slice(plaintext);
let (buf, tag) = buffer.split_at_mut(plaintext.len() - Self::TAG_SIZE);
self.seal_in_place_detached(nonce, associated_data, buf, tag);
buffer
}
#[cfg(feature = "alloc")]
fn open(&self, nonce: Self::Nonce, associated_data: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
let mut buffer = Vec::from(ciphertext);
if ciphertext.len() >= Self::TAG_SIZE {
let (buf, tag) = buffer.split_at_mut(ciphertext.len() - Self::TAG_SIZE);
self.open_in_place_detached(nonce, associated_data, buf, tag)?;
} else {
return Err(Error);
}
buffer.drain(..Self::TAG_SIZE);
Ok(buffer)
}
} What I like about this:
I think it could still use a "buffer" type to handle the actual ciphertext assembly part, supplied as a parameter to hypothetical non-detached |
@jcape I agree, and would be happy with just |
We can use vector-based |
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.
Nice! Looks good to me now (aside from @newpavlov’s nit)
@jcape I think you'll need to edit
to
...and then add a build matrix entry specifically for AEAD on Rust 1.36 that does:
|
aead/src/lib.rs
Outdated
extern crate alloc; | ||
|
||
use alloc::vec::Vec; | ||
use generic_array::{GenericArray, ArrayLength}; |
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.
It'd probably be good to have a pub use generic_array
like the other trait crates
I implemented the ChaCha20Poly1305 AEAD using this trait: A few thoughts:
One last thought: in implementing RFC 8439, the specification and test vectors are "detached" in several places. I ended up internally implementing a detached API in order to implement the attached one. I think a detached API would be extremely helpful for AEAD implementations to write their core For example, here is the pseudocode for the ChaCha20Poly1305 AEAD, which is defined in the RFC in terms of a detached API: https://tools.ietf.org/html/rfc8439#section-2.8.1
|
If data is bigger than register, than it may be better to pass it by reference. Plus other crates pass both key and nonce by reference, so I think it will be better to be consistent. |
I'd be fine with them both being references too |
Hm, it's indeed not a great property, but I am not sure about using an associated type to fix it. I see two alternatives: Cipher.encrypt(nonce, Payload { ad, msg })
// builder pattern
Cipher.with_ad(ad).encrypt(nonce, msg) |
Between the two, I dislike the builder pattern less, but why not go all the way (naming notwithstanding):
|
@jcape that's an interesting idea. One nit: I think How about: CiphertextBuilder.ad(ad).nonce(nonce).encrypt(msg); |
@tarcieri The interesting bit is when you do |
@jcape it's an idea worth pursuing, but perhaps after we land this PR to get the ball rolling? |
Because it's significantly more typing without sufficient improvements on being less error-prone? The problem originates from To make it nicer to read we could use |
@jcape looks like you need to modify |
|
Can we land this? 😉 |
Yeah, lets merge it and continue work in a separate PR. Before publishing I think we should implement a solution for the |
Hi,
I've previously spoken with @newpavlov via e-mail, and he asked me to create this PR and made some cursory comments on the original, internal-to-MobileCoin, source code for this crate. I've attempted to address the comments and clarify the Nonce object's purpose. My goal here is to get this interface into widespread usage, so I'm amenable to changing whatever needs to be changed to make that a reality.
The crate itself is intended to provide a misuse-resistant, rust-ish way to use RFC5116 AEAD ciphers.