Add Ed448 (Goldilocks) curve support#61
Add Ed448 (Goldilocks) curve support#61sridhar-panigrahi wants to merge 4 commits intoLFDT-Lockness:mfrom
Conversation
Adds Curve448 as a new curve backend using ed448-goldilocks-plus. Follows the same manual trait implementation approach as Ed25519 since Curve448 uses Edwards encoding rather than SEC1. - 57-byte compressed point encoding (CompressedEdwardsY) - 56-byte scalar encoding with Reduce<56> and Reduce<112> - Cofactor-4 torsion checking via CofactorGroup - All existing tests instantiated for the new curve Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com> Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
@survived , please let me know you thoughts on this ! |
Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com> Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com>
96ef316 to
e963cf0
Compare
|
Hey @sridhar-panigrahi, sorry, it will take me some time to review the PR as I'm pressed by some other work. I should be able to get on this in a couple of weeks unless @maurges gets to it first! |
|
it's ok @survived , I'll try to find and work on something even more valuable on this repo . |
|
@maurges , please let me know your thoughts on this ! |
|
I left a couple of comments, but the big question is whether we want this curve to be curve448 (56 byte encoded scalar) or ed448 (57 byte encoded scalar). I'm tending towards the second, since the biggest usage of this curve is for signatures after all, but maybe curve448 is more general and is easy enough to adapt to signatures too. Or maybe it's possible to have both |
|
We def need ed448 not curve448, to align with existing ed25519 I still haven't had time to look at PR, but curve448 is in a twisted form in which there's no point addition defined. The only way around is by mapping points from curve448 to ed448, perform point addition, and map back. This is extremely inefficient, we should not do that. |
|
Sorry, it's my bad, I wrote curve448 in #58. I meant ed448 |
If I understand right, here internally everything is treated as ed448 values, and is only encoded as curve448 |
|
Hey @survived and @maurges, I've updated the PR to address your feedback:
Let me know if there's anything else to adjust! |
9261f3c to
61e45bb
Compare
- Rename Curve448 to Ed448, CURVE_NAME = ed448 - Scalar encoding changed to 57 bytes (RFC 8032) - Derive Zeroize on Ed448, Point, and Scalar structs - Use GenericArray::into() in to_bytes_compressed - Fix FromUniformBytes to use 84 bytes (k=224 per RFC 9380) - Replace Reduce<56>/Reduce<112> with Reduce<57>/Reduce<114> - Fix Reduce<57> to use from_bytes_mod_order_wide - Fix Straus multiscalar for non-multiple-of-8 scalar byte lengths - Rename feature flags: curve448 to ed448, curve-curve448 to curve-ed448 Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com>
61e45bb to
0081221
Compare
maurges
left a comment
There was a problem hiding this comment.
I marked three places with the usage of GenericArray::from_slice, but there are others, and all of them should be replaced
| use group::GroupEncoding; | ||
| self.0.to_bytes().into() |
There was a problem hiding this comment.
Nitpick: it's inconsistent that above you use a fully qualified call to group::cofactor::CofactorGroup::is_torsion_free, but here you import instead.
My personal preference is to use fully-qualified calls. But importing traits does make the implementation look more similar to ed25519, which makes this review easier. So use your judgement
There was a problem hiding this comment.
Used the inherent self.0.is_torsion_free() method directly — avoids both the verbose fully-qualified call and any trait import.
| impl core::fmt::Debug for Point { | ||
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| use generic_ec_core::CompressedEncoding; | ||
| f.debug_tuple("Point") | ||
| .field(&self.to_bytes_compressed()) | ||
| .finish() | ||
| } | ||
| } |
There was a problem hiding this comment.
What's the reasoning for an explicit Debug impl? Does it make it more similar to how the other curves in this crate behave? Or something else?
There was a problem hiding this comment.
Removed both the explicit Debug impls for Point and Scalar — looking at ed25519.rs, it has no custom Debug on its raw types either, so this keeps things consistent.
| let mut wide = [0u8; 114]; | ||
| wide[..57].copy_from_slice(bytes); | ||
| Self(ed448_goldilocks_plus::Scalar::from_bytes_mod_order_wide( | ||
| GenericArray::from_slice(&wide), | ||
| )) |
There was a problem hiding this comment.
You can create wide as let mut wide = GenericArray::default(), and then there's no need to convert
There was a problem hiding this comment.
Fixed — now uses GenericArray::default() directly for all the wide-buffer cases.
| impl generic_ec_core::Decode for Point { | ||
| fn decode(bytes: &[u8]) -> Option<Self> { | ||
| use group::GroupEncoding; | ||
| let ga = GenericArray::from_slice(bytes); |
There was a problem hiding this comment.
This will panic if the slice is too short. Better to use try_from_slice and return None
There was a problem hiding this comment.
Fixed — added an explicit length check before calling from_slice, returning None on mismatch. (generic-array 0.14 doesn't have try_from_slice, so the length check achieves the same panic-safe result.)
| let mut bytes_le = [0u8; 114]; | ||
| bytes_le[..84].copy_from_slice(bytes); | ||
| Self(ed448_goldilocks_plus::Scalar::from_bytes_mod_order_wide( | ||
| GenericArray::from_slice(&bytes_le), | ||
| )) |
There was a problem hiding this comment.
You can create a GenericArray directly instead of first writing to an array
There was a problem hiding this comment.
Fixed — now creates GenericArray::default() and fills it with copy_from_slice.
|
|
||
| fn from_le_bytes_exact(bytes: &Self::Bytes) -> Option<Self> { | ||
| Option::from(ed448_goldilocks_plus::Scalar::from_canonical_bytes( | ||
| GenericArray::from_slice(bytes), |
There was a problem hiding this comment.
This can panic in runtime if we ever mistake the lengths. But there is an instance impl<'a, T, const N: usize> From<&'a [T; N]> for &'a GenericArray<T, ConstArrayLength<N>>, so you should be able to simply write bytes.into() here
There was a problem hiding this comment.
Fixed for the [u8; 57] case using bytes.into(). For [u8; 114], generic-array 0.14 only implements From<&[T; N]> for specific sizes and 114 isn't among them, so used GenericArray::default() + copy_from_slice there instead.
|
Thank you for the detailed review, @maurges! I've addressed all the feedback in the latest commit:
Let me know if anything else needs attention! |
Signed-off-by: Shridhar Panigrahi <sridharpanigrahi2006@gmail.com>
c2b114e to
882b678
Compare
Following up on #58 — updated based on reviewer feedback to use ed448 instead of curve448.
What changed
Ed448,CURVE_NAME = "ed448", feature flagcurve-ed448Zeroizederived on the curve struct,Point, andScalarFromUniformBytesnow uses 84 bytes (k = 224 per RFC 9380)Reduce<57>andReduce<114>replacing the oldReduce<56>/Reduce<112>All tests pass.