-
Notifications
You must be signed in to change notification settings - Fork 504
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
Elligator2 Forward and Reverse Mappings #612
base: main
Are you sure you want to change the base?
Conversation
Thank you! It will take a bit for me to take a look at this (trying to make some paper deadlines). Re the IETF standard, hasn't that officially been standardized now? RFC 9380. Would be good to have test vectors from there. |
I will get those added. I haven't spent much time looking at that RFC yet, so I can try to work in the interface there as well (and get the rest of the tests passing). |
b4ad93f
to
31ca8dd
Compare
I have added the RFC9380 test vectors for the elligator2 implementation, and an interface that should work if someone wanted to use this to implement the The CI checks should also be passing now. |
a972ecf
to
fcee93b
Compare
Any chance of this landing soon? |
fcee93b
to
c69bda8
Compare
I am working on adding some final tests to ensure that the bits of the ellgator2 representatives appear as uniform random. TLDR: The The specific issue that this is testing for can be described as: An instantiation of Elligator is parameterized by what might be called
a “canonical” square root function, one with the property that
`√a^2 = √(−a)^2` for all field elements `a`. That is, we designate just
over half the field elements as “non-negative,” and the image of the
square root function consists of exactly those elements. A convenient
definition of “non-negative” for Curve25519, suggested by its authors,
is the lower half of the field, the elements `{0, 1, …, (q − 1)+/+2}`.
When there are two options for a square root, take the smaller of the two. Any Elligator implementation that does not do this canonicalization of the final square root, and instead maps a given input systematically to either its negative or non-negative root is vulnerable to the following computational distinguisher. [An adversary could] observe a representative, interpret it as a field
element, square it, then take the square root using the same
non-canonical square root algorithm. With representatives produced by
an affected version of [the elligator2 implementation], the output of
the square-then-root operation would always match the input. With
random strings, the output would match only half the time. The solution from the For a more in-depth explanation see:
This should not impact the general interface of the PR, and I am hoping to have the changes finished within the week. |
The latest commit fixes several issues.
I have no other changes planned for this PR without review / input. |
43ecd08
to
2226611
Compare
I have added another refactor to the elligator2 implementation motivated by feedback based on issues encountered with encoded key distinguishability in obfs4. The changes required to fix the distinguisher resulted in two versions of the elligator2 algorithm which are not interchangeable. More information on the issue can be found here, the solution added to the A test variant exists for legacy implementations that do not use least-square-root value of the representative (i.e. kleshni & signal), but it is not exposed by default. For now I have published my fork as its own crate (see curve25519-elligator2), but my intention is to hopefully get this merged here and eventually yank the forked crate. |
Hi, thank you for this! I played around with this and had some notes:
I'll be reviewing the rest of the PR, but I think these should be addressed before merging. Thanks again! |
8d96835
to
15ad0b3
Compare
You are definitely correct about the implementation of the gt function. I was relying on the idea that other implementations were using a reliable compare, but it turns out they fall victim to the exact issue that you describe. Really what they are trying to check is whether the value is negative, but they do so with by subtracting
For the As an aside, is the implementation of |
Is there anything blocking this PR? I would love to have it included in a future release as I find myself needing this feature. |
I'm gonna spend some time this week going thru this. I really appreciate this PR @jmwample. It's been tough co-maintaining this while having a full time job. I don't know a lot about Elligator2, and especially the distinguishing attacks mentioned here, so I will have to read a lot here, and probably ask a bunch of questions. Thank u in advance! |
Thanks @rozbb! |
with agl/ed25519/extra25519, the kleshni C implementation, and rfc9380.
Edwards rfc9380 tests and elligator representative randomness using tweaks.
fix for subgroup based computational distinguisher and updates / simplifications to the elligator2 interface as a result
15ad0b3
to
924988a
Compare
Great to hear! I rebased onto the latest changes in Okay, I am working on getting myself back up to speed as well. I think the current sticking point regards the following
Things to consider here are:
|
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.
Ok this was a lot but I left a first round of comments. The high level comment is that I'm not sure where any of this math comes from. It seems like a mix of the RFC, the paper, Signal's impl, and agl's impl. If that's actually the case, then this will need a lot of references and explanations.
One thing I realized is the inverse maps are not standardized. So the RFC cannot be followed, and in fact it looks like it is not followed (eg using a range check versus x mod 2
for the is-nonneg function). These choices should probably be documented somewhere public. We cannot interoperate with other Elligator implementations if we don't have a reference.
Sorry for the wall of text. This was a bunch of reading and I learned a lot
/// The `tweak` parameter is used to adjust the computed representative making | ||
/// it computationally indistinguishable from uniform random. If this property | ||
/// is not required then the provided tweak value does not matter. | ||
fn to_representative(point: &[u8; 32], tweak: u8) -> CtOption<[u8; 32]>; |
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.
Should probably be called sk_to_pubkey_representative
or something. And the input should be called sk
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.
Also, why is this the API and not the actual point -> repr mapping?
// ============================================================================ | ||
// ============================================================================ | ||
|
||
fn map_to_curve_parts( |
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 function looks like it's supposed to be map_to_curve_elligator2
or map_to_curve_elligator2_curve25519
, but it doesn't appear to follow RFC 9380 F.3 or G.2.1. Where is it from? Can you comment it clearer so I can follow along?
/// [elligator paper](https://elligator.cr.yp.to/elligator-20130828.pdf) | ||
/// [elligator site](https://elligator.org/) | ||
/// | ||
pub(crate) fn point_to_representative( |
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.
/// Determines whether a point is encodable as a representative. Approximately | ||
/// 50% of points are not encodable. | ||
#[inline] | ||
fn is_encodable(u: &FieldElement) -> Choice { |
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.
Ditto where did this function come from? According to the site, you only need to check u ≠ −A
, −Zu(u + A)
is a square, and v = 0 => u = 0
// [`PublicRepresentative`] in the [`x25519_dalek`] crate. AFAIK there is no | ||
// need for anyone with only the public key to be able to generate the | ||
// representative. | ||
pub(crate) fn v_in_sqrt(key_input: &[u8; 32]) -> Choice { |
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.
Why is this function called this? Shouldn't it be pubkey_v_is_nonneg
? Confused me a lot while reading.
Also it'd be nice to refactor to remove this function entirely. IMO it's doing too many ops here (mask, compute pubkey, extract v coord, return is-nonneg)
/// See <https://datatracker.ietf.org/doc/rfc9380/> | ||
pub fn map_to_point(r: &[u8; 32]) -> MontgomeryPoint { | ||
let mut clamped = *r; | ||
clamped[31] &= MASK_UNSET_BYTE; |
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 isn't clamping, right? This is just clearing the top 2 bits which are random
/// See <https://datatracker.ietf.org/doc/rfc9380/> | ||
pub fn map_to_point(r: &[u8; 32]) -> EdwardsPoint { | ||
let mut clamped = *r; | ||
clamped[31] &= MASK_UNSET_BYTE; |
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.
Ditto
/// | ||
/// For a more in-depth explanation see: | ||
/// <https://github.com/agl/ed25519/issues/27> | ||
/// <https://www.bamsoftware.com/papers/fep-flaws/> |
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 don't understand the distinction. The two things you linked above do not seem to point out any flaw in the RFC, rather implementation errors in some libraries. Is there something inherent in the RFC that requires modification? The impl above already appears to do randomization.
It appears that the actual issue is that the above RFC 9380 mapping always clears the cofactor, which is a leaky thing to do. If that's the issue, why not change the API to do the real mappings point -> field rather than scalar -> point -> field?
(&x * &d_1, d_1, y, one) | ||
} | ||
|
||
pub(crate) fn map_fe_to_montgomery(r: &FieldElement) -> (FieldElement, FieldElement) { |
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.
Comment needed
(&xmn * &(xmd.invert()), y) | ||
} | ||
|
||
pub(crate) fn map_fe_to_edwards(r: &FieldElement) -> (FieldElement, FieldElement) { |
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.
Should mention this is map_to_curve_elligator2_edwards25519
from RFC 9380 section G.2.2
Hello,
This is an implementation of the Elligator2 forward and reverse mappings --- points to representative values, as well as representative values to points. The specific goal is to make Elligator mapping for x25519 handshakes possible in pure rust.
This implementation is gated behind a feature flag
"elligator2"
in thecurve25519_dalek
crate for point transforms. I have tested this against the test vectors from theKleshni/Elligator-2
C implementation as well as (a fork of) theagl/ed25519/extra25519
golang implementation. These can be seen in the test cases for this PR.I also added a feature flag
"elligator2"
to thex25519_dalek
crate that exposes aPublicRepresentative
type that mirrors thePublicKey
type in order to exchange only Elligator Representatives when performing a diffie-hellman handshake.This implementation of the Elligator2 transforms (to the best of my ability):
agl/ed25519/extra25519
Kleshni/Elligator-2
I have seen a couple issues and PRs dealing with elligator2 mappings and this library, so I apologize if this is muddying the water . This is a feature that I really needed so I have implemented the functionality based on several existing implementations in other languages.
I am also not an expert in crypto implementations and would greatly appreciate any feedback. I hope this is helpful and potentially solid enough to include in the library. 👍
Related to: