Jump to content
Nytro

Cofactor Explained: Clearing Elliptic Curves' dirty little secret

Recommended Posts

Posted

April 2020

Cofactor Explained: Clearing Elliptic Curves' dirty little secret

Much of public key cryptography uses the notion of prime-order groups. We first relied on the difficulty of the Discrete Logarithm Problem. Problem was, Index Calculus makes DLP less difficult than it first seems. So we used longer and longer keys – up to 4096 bits (512 bytes) in 2020 – to keep up with increasingly efficient attacks.

Elliptic curves solved that. A well chosen, safe curve can only be broken by brute force. In practice, elliptic curve keys can be as small as 32 bytes.

On the other hand, elliptic curves were not exactly fast, and the maths involved many edge cases and subtle death traps. Most of those problems were addressed by Edwards curves, which have a complete addition law with no edge cases, and Montgomery curves, with a simple and fast scalar multiplication method.

Those last curves however did introduced a tiny little problem: their order is not prime.

(Before we dive in, be advised: this is a dense article. Don't hesitate to take the time you need to digest what you've just read and develop an intuitive understanding. Prior experience with elliptic curve scalar multiplication helps too.)

Prime-order groups primer

First things first: what's so great about prime-order groups? What's a group anyway? What does "order" even mean?

A group is the combination of a set of elements "G", and an operation "+". The operation follows what we call the group laws.

  • For all a and b in G, a+b is also in G (closure).
  • For all a, b, and c in G, (a+b)+c = a+(b+c) (associativity).
  • There's an element "0" such that for all a, 0+a = a+0 = a (identity element).
  • For all a in G, there's an element -a such that a + -a = 0 (inverse element).

Basically what you'd expect from good old addition.

The order of a group is simply the number of elements in that group.

To give you an example, let's take G = [0..15], and define + as binary exclusive or. All laws above can be checked. It's order is 16 (there are 16 elements). Note some weird properties. For instance, each element is its own inverse (a xor a is zero).

More interesting are cyclic groups, which have a generator: an element that repeatedly added to itself can walk through the entire group (and back to itself, so it can repeat the cycle all over again).

Cyclic groups are all isomorphic to the group of non-negative integers modulo the same order. Let's take for instance [0..9], with addition modulo 10. The number 1 is a generator of the group:

1 = 1
2 = 1+1
3 = 1+1+1
4 = 1+1+1+1
5 = 1+1+1+1+1
6 = 1+1+1+1+1+1
7 = 1+1+1+1+1+1+1
8 = 1+1+1+1+1+1+1+1
9 = 1+1+1+1+1+1+1+1+1
0 = 1+1+1+1+1+1+1+1+1+1
1 = 1+1+1+1+1+1+1+1+1+1+1  (next cycle starts)
2 = 1+1+1+1+1+1+1+1+1+1+1+1
etc.

Not all numbers are generators of the entire group. 5 for instance can generate only 2 elements: 5, and itself.

5 = 5
0 = 5+5
5 = 5+5+5 (next cycle starts)
0 = 5+5+5+5
etc.

Note: we also use the word "order" to speak of how many elements are generated by a given element. In the group [0..9], 5 "has order 2", because it can generate 2 elements. 1, 3, 7, and 9 have order 10. 2, 4, 6, and 8 have order 5. 0 has order 1.

Finally, prime-order groups are groups with a prime number of elements. They are all cyclic. What's great about them is their uniform structure: every element (except zero) can generate the whole group. Take for instance the group [0..10] (which has order 11). Every element except 0 is a generator:

(Note: from now on, I will use the notation A.4 to denote A+A+A+A. This is called "scalar multiplication" (in this example, the group element is A and the scalar is 4). Since addition is associative, various tricks can speed up this scalar multiplication. I use a dot instead of "×" so we don't confuse it with ordinary multiplication, and to remind that the group element on the left of the dot is not necessarily a number.)

1.1  =  1    2.1  =  2    3.1  =  3    4.1  =  4
1.2  =  2    2.2  =  4    3.2  =  6    4.2  =  8
1.3  =  3    2.3  =  6    3.3  =  9    4.3  =  1
1.4  =  4    2.4  =  8    3.4  =  1    4.4  =  5
1.5  =  5    2.5  = 10    3.5  =  4    4.5  =  9
1.6  =  6    2.6  =  1    3.6  =  7    4.6  =  2   etc.
1.7  =  7    2.7  =  3    3.7  = 10    4.7  =  6
1.8  =  8    2.8  =  5    3.8  =  2    4.8  = 10
1.9  =  9    2.9  =  7    3.9  =  5    4.9  =  3
1.10 = 10    2.10 =  9    3.10 =  8    4.10 =  7
1.11 =  0    2.11 =  0    3.11 =  0    4.11 =  0

You get the idea.

In practice, we can't distinguish group elements from each other: apart from zero, they all have the same properties. That's why discrete logarithm is so difficult: there is no structure to latch on to, so an attacker would mostly have to resort to brute force.

(Okay, I lied. Natural numbers do have some structure to latch on to, which is why RSA keys need to be so damn huge. Elliptic curves – besides treacherous exceptions – don't have a known exploitable structure.)

The cofactor

So what we want is a group of order P, where P is a big honking prime. Unfortunately, the simplest and most efficient elliptic curves out there – those that can be expressed in Montgomery and Edwards form – don't give us that. Instead, they have order P×H, where P is suitably large, and H is a small number (often 4 or 8): the cofactor.

Let's illustrate this with the cyclic group [0..43], of order 44. How much structure can we latch on to?

 0  1  2  3
 4  5  6  7
 8  9 10 11
12 13 14 15
16 17 18 19
20 21 22 23
24 25 26 27
28 29 30 31
32 33 34 35
36 37 38 39
40 41 42 43

Since 44 is not prime, not all elements will have order 44. For instance:

  • 1 has order 44
  • 2 has order 22
  • 3 has order 44
  • 4 has order 11

You get the idea. When we go over all numbers, we notice that the order of each element is not arbitrary. It is either 1, 2, 4, 11, 22, or 44. Note that 44 = 11 × 2 × 2. We can see were the various orders come from: 1, 2, 2×2, 11, 11×2, 11×2×2.

The order of an element is easy to test: just multiply by the order to test, see if it yields zero. For instance (remember A.4 is a short hand for A+A+A+A, and we're working modulo 44 – the order of the group):

 8 . 11 =  0  -- prime order
24 . 11 =  0  -- prime order
25 . 11 = 33  -- not prime order
11 .  4 =  0  -- low order
33 .  4 =  0  -- low order
25 .  4 = 12  -- not low order

("Low order" means orders below 11: 1, 2, 4. Not much lower than 11, but if we replace 11 by a large prime P, the term "low order" makes more sense.)

Understandably, there are only few elements of low order: 0, 11, 22, and 33. 0 has order 1, 22 has order 2, 11 and 33 have order 4. Like the column on the left, they form a proper subgroup. It's easier to see by swapping and rotating the columns a bit:

 0 11 22 33
 4 15 26 37
 8 19 30 41
12 23 34  1
16 27 38  5
20 31 42  9
24 35  2 13
28 39  6 17
32 43 10 21
36  3 14 25
40  7 18 29

The low order subgroup is shown on the first line. And now we can finally see the structure of this group: a narrow rectangle, with 11 lines and 4 columns, where each element in this rectangle is the sum of an element of prime order, and an element of low order. For instance:

30 =  8 + 22 = 4.2 + 11.2
35 = 24 + 11 = 4.6 + 11.1
 1 = 12 + 33 = 4.3 + 11.3

You just have to look left and up to know which elements to sum. To do this algebraically, you need to multiply by the right scalar. You need to clear the (co)factor.

Let's first look left. How do we clear the cofactor? We start by an element that can be expressed thus:

E = 4.a + 11.b

What we want to find is the scalar s such that

E.s = 4.a

Note that

E.s = (4.a + 11.b).s
E.s = 4.a.s + 11.b.s
E.s = 4.(a×s) + 11.(b×s)
E.s = 4.(a×1) + 11.(b×0)

Recall that 4 has order 11, and 11 has order 4. So s must follow these equations:

s = 1 mod 11  -- preserve a
s = 0 mod  4  -- absorb b

There's only one such scalar between 0 and 44: 12. So, multiplying by 12 clears the cofactor, and preserves the prime factor. For instance:

13.12 = 24
27.12 = 16
42.12 = 20

Now we know how to look left. To look up, we follow the same reasoning, except this time, our scalar s must follow these equations:

s = 0 mod 11  -- absorb a
s = 1 mod  4  -- preserve b

That's 33. For instance:

13.33 = 33
27.33 = 11
42.33 = 22

Now we can look up as well.

(Note: This "looking up" and "looking left" terminology isn't established mathematical terminology, but rather a hopefully helpful illustration. Do not ask established mathematicians or cryptographers about "looking up" and "looking left" without expecting them to be at least somewhat confused.)

Torsion safe representatives

We now have an easy way to project elements in the prime-order subgroup. Just look left by multiplying the element by the appropriate scalar (in the case of our [0..43] group, that's 12). That lets us treat each line of the rectangle as an equivalence class, with one canonical representative: the leftmost element – the one that is on the prime-order subgroup. This effectively gets us what we want: a prime-order group.

Let's say we have a scalar s and an element E, which are not guaranteed to be on the prime-order subgroup. We want to know in which line their scalar multiplication will land, and we want to represent that line by its leftmost element. To do so, we just need to perform the scalar multiplication, then look left. For instance:

s      =  7  -- some random scalar
E      = 31  -- some random point
E.s    = 41
result = (E.s).12 = 8

Or we could first project E to the left, then perform the scalar multiplication. It will stay on the left column, and give us the same result:

E      = 31
E . 12 = 20
result = (E.12).s = 8

The problem with this approach is that it is slow. We're performing two scalar multiplications instead of just one. That kind of defeats the purpose of choosing a fast curve to begin with. We need something better.

Let us look at our result one more time:

result = (E.s).12 = 8
result = (E.12).s = 8

It would seem the order in which we do the scalar multiplications does not matter. Indeed, the associativity of group addition means we can rely on the following:

(E.s).t = E.(s×t)
        = E.(t×s)
        = (E.t).s

Now you can see that we can avoid performing two scalar multiplications, and multiply the two scalars instead. To go back to our example:

s      =  7       -- our random scalar
E      = 31       -- our random point
result = E.(7×12) -- scalarmult and look left
result = E.(84)
result = E.(40)   -- because 84 % 44 = 40
result = 8

Remember we are working with a cyclic group of order 44: adding an element to itself 84 times is like adding it to itself 44 times (the result is zero), and again 40 times. So better reduce the scalar modulo the group order so we can have a cheaper scalar multiplication.

Let's recap:

s        =  7  -- our random scalar
E        = 31  -- our random point
E.s      = 41
E.(s×12) =  8

41 and 8 are on the same line, and 8 is in the prime-order subgroup. Multiplying by s×12 instead of s preserved the main factor, and cleared the cofactor. Because of this, we call s×12 the torsion safe representative of s.

Now computing (s×12) % 44 may be simple, but it doesn't look very cheap. Thankfully, we're not out of performance tricks:

(s×12) % 44 = (s×(11+1)) % 44
            = (s×(11+1)) % 44
            = (s + (s   × 11)) % 44
            =  s + (s   × 11) % 44  -- we assume s < 11
            =  s + (s%4 × 11)       -- we can remove %44

We only need to add s and and a multiple of 11 (0, 11, 2×11, or 3×11). The result is guaranteed to be under the group order (44). The total cost is:

  • Multiplying the prime order by a small number.
  • Adding two scalars together.

Compared to the scalar multiplication, that's practically free.

Decoupling main factor and cofactor

We just found the torsion safe representative of a scalar, that after scalar multiplication preserves the line, and only chooses the left column. In some cases, we may want to reverse roles: preserve the column, and and only chose the top line. In other words, we want to do the equivalent of performing the scalar multiplication, then looking up.

The reasoning is the same as for torsion safe representatives, only flipped on its head. Instead of multiplying our scalar by 12 (which is a multiple of the low order, equals 1 modulo the prime order), we want to multiply it by 33: a multiple of the prime order, which equals 1 modulo the low order.

Again, modular multiplication by 33 is not cheap, so we repeat our performance trick:

(s×33) % 44 = (s×11×3) % (11×4)
            = ((s×3) % 4) × 11

In this case, we just have to compute (s×3) % 4 and multiply the prime order by that small number. The total cost is just the multiplication of the order by a very small number.

Now we can do the decoupling:

s      =  7  -- some random scalar
E      = 31  -- some random point
E.s    = 41  -- normal result

E.(s×12) =  8
E.(s×33) = 33
E.s      = 8 + 33 = 41 -- decoupled result

Cofactor and discrete logarithm

Time to start applying our knowledge.

Let's say I have elements A and B, and a scalar s such that B = A.s. The discrete logarithm problem is about finding s when you already know A and B.

If we're working in a prime-order group with a sufficiently big prime, that problem is intractable. But we're not working with a prime-order group. We have a cofactor to deal with. Let's go back to our group of order 44:

 0 11 22 33
 4 15 26 37
 8 19 30 41
12 23 34  1
16 27 38  5
20 31 42  9
24 35  2 13
28 39  6 17
32 43 10 21
36  3 14 25
40  7 18 29

We can easily recover the line and the column of A and B, so let's do that. Take for instance A = 27 and B = 15. Now let's find s.

A = 27
A = 16 + 11
A = 4.4 + 11.1

B = 15
B = 4 + 11
B = 4.1 + 11.1

B = A . s
4.1 + 11.1 = (4.4 + 11.1) . s
4.1 + 11.1 =  (4.4).s + (11.1).s

4 .1 = (4 .4).s
11.1 = (11.1).s

Now look at that last line. Since 11 has only order 4, there are only 4 possible solutions, which are easy to brute force. We can try them all and easily see that:

11 . 1  = (11.1).1  (mod 44)
s mod 4 = 1

The other equation however is more of a problem. There are 11 possible solutions, and trying them all is more expensive:

4 . 1    ≠ (4.4).1
4 . 1    ≠ (4.4).2
4 . 1    = (4.4).3
s mod 11 = 3

Now that we know both moduli, we can deduce that s = 25:

A . s = B
27.25 = 15

What we just did here is reducing the discrete logarithm into two, easier discrete logarithms. Such divide and conquer is the reason why we want prime-order groups, where the difficulty of the discrete logarithm is the square root of the prime order (square root because there are cleverer brute force methods than just trying all possibilities). Here however, the difficulty wasn't the square root of 44. It was sqrt(11) + sqrt(2) + sqrt(2), which is significantly lower.

The elliptic curves we are interested in however have much bigger orders. Curve25519 for instance has order 8 × L where L is 2²⁵² + something (and the cofactor is 8). So the difficulty of solving discrete logarithm for Curve25519 is sqrt(2)×3 + sqrt(2²⁵²), or approximately 2¹²⁶. Still lower than sqrt(8×L) (about 2¹²⁷), but not low enough to be worrying: discrete logarithm is still intractable.

Cofactor and X25519

Elliptic curves can have a small cofactor, and still guarantee we can't solve discrete logarithm. There's still a problem however: the attacker can still solve the easy half of discrete logarithm, and deduce the value of s, modulo the cofactor. In the case of Curve25519, that means 3 bits of information, that could be read, or even controlled by the attacker.

That's not ideal, so DJB rolled two tricks up his sleeve:

  1. The chosen base point of the curve has order L, not 8×L. Multiplying that point by our secret scalar, can then only generate points on the prime-order subgroup (the leftmost column). The three bits of information the attacker could have had are just absorbed by that base point.

  2. The scalar is clamped. Bit 255 is cleared and bit 254 is set to prevent timing leaks in poor implementations, and put a lower bound on standard attacks. More importantly, bits 0, 1, and 2 are cleared, to make sure the scalar is a multiple of 8. This guarantees that the low order component of the point, if any, will be absorbed by the scalar multiplication, such that the resulting point will be on the prime-order subgroup.

The second trick is especially important: it guarantees that the scalar multiplication between your secret key and an attacker controlled point on the curve can only yield two kinds of results:

  • A point on the curve, that has prime order. The attacker don't learn anything about your private key (at least not without solving discrete logarithm first), and they can't control which point on the curve you are computing. We're good.

  • Zero. Okay, the attacker did manage to force this output, but this only (and always) happens when they gave you a low order point. So again, they learned nothing about your private key. And if you needed to check for low order output (some protocols, like CPace, require this check), you only need to make sure it's not zero (use a constant-time comparison, please).

(I'm ignoring what happens if the point you're multiplying is not on the curve. Failing to account for that has broken systems in the past. X25519 however only transmits the x-coordinate of the point, so the worst you can have is a point on the "twist". Since the twist of Curve25519 also has a big prime order (2²⁵³ minus something) and a small cofactor (4), the results will be similar, and the attacker will learn nothing. Curve25519 is thus "twist secure".)

Cofactor and Elligator

Elligator was developed to help with censorship circumvention. It encodes points on the curve so that if the point is selected at random, its encoding will be indistinguishable from random. With that tool, the entire cryptographic communication is fully random, including initial ephemeral keys. This enables the construction of fully random formats, and facilitates steganography.

Communication protocols often require ephemeral keys to initiate communications. In practice, they need to generate a random key pair, and send the public half over the network. Public keys however will not necessarily look random, even though the private half was indeed random. Curve25519 for instance have a number of biases:

  • Curve25519 points are encoded in 255 bits (we only encode the x-coordinate). Since all communication happens at the byte level, there is one unused bit, which is usually cleared. This is easily remedied by simply randomising the unused bit.

  • The x-coordinate of Curve25519 points does not span all values from 0 to 2²⁵⁵-1. Values 2²⁵⁵-19 and above never occur. In practice though, this bias is small enough to be utterly undetectable.

  • Curve25519 points satisfy the equation y² = x³ + 486662 x² + x. All the attacker has to do is take the suspected x-coordinate, compute the right hand side of the equation, and check whether this is a square in the finite field GF(2²⁵⁵-19). For random strings, it will be about half the time. if it is a Curve25519 x-coordinate, it will be all the time.

    The remedy is Elligator mappings, which can decode all numbers from 0 to 2²⁵⁵-20 into a point on the curve. Encoding fails about half the time, but we can try generating key pair until we find one that can be turned into a random looking encoding.

  • X25519 keys aren't selected at random to begin with: because of cofactor clearing, they are taken from the prime-order subgroup. Even with Elligator mappings, an attacker can decode the point, and check whether it belongs to the prime-order subgroup. Again, with X25519, this would happen all the time, unlike random representatives.

    When I first discovered this problem, I didn't know what to do. The remedy is fairly simple once I understood the cofactor. The real problem was reaching that understanding.

So, our problem is to generate a key pair, where the public half is a random point on the whole curve, not just the prime-order subgroup. Let's again illustrate it with the [0..43] group:

 0 11 22 33
 4 15 26 37
 8 19 30 41
12 23 34  1
16 27 38  5
20 31 42  9
24 35  2 13
28 39  6 17
32 43 10 21
36  3 14 25
40  7 18 29

Recall that the prime-order subgroup is the column on the left, and the low order subgroup is the first line. X25519 is designed in such a way that:

  • The chosen generator of the curve has prime order.
  • The private key is a multiple of the cofactor.

For us here, this means our generator is 4, and our private key is a multiple of 4. You can check that multiplying 4 by a multiple of 4 will always yield a multiple of… 4. An element on the left column. (Remember, we're working modulo 44).

But that's no good. I want to select a random element on the whole rectangle, not just the left column. If we recall our cofactor lessons, the solution is simple: add a random low order element. The random prime-order element selects the line, and the random low order element selects the column. Adding them together gives us a random element over the whole group.

To add icing on the cake, this method is compatible X25519. Let's take an example. Let's first have a regular key exchange:

B   = 4                  -- Generator of the prime-order group
sa  = 20                 -- Alice's private key
sb  = 28                 -- Bob's   private key
SA  = B.sa  = 4.20  = 36 -- Alice's public key
SB  = B.sb  = 4.28  = 24 -- Bob's   public key
ssa = SB.sa = 24.20 = 40 -- Shared secret (computed by Alice)
ssb = SA.sb = 36.28 = 40 -- Shared secret (computed by Bob)

As expected of Diffie–Hellman, Alice and Bob compute the same shared secret. Now, what happens if Alice adds a low order element to properly hide her key? Let's say she adds 11 (an element of order 4).

LO  = 11                 -- random low order point
HA  = SA+LO = 36+11 =  3 -- Alice's "hidden" key
ssb = HA.sb =  3.28 = 40

Bob still computes the same shared secret! Which by now should not be a big surprise: scalars that are a multiple of the cofactor absorb the low order component, effectively projecting the result back in the prime order subgroup.

Applying this to Curve25519 is straightforward:

  • Select a random number, then clamp it as usual. It is now a multiple of the cofactor of the curve (8).
  • Multiply the Curve25519 base point by that scalar.
  • Add a low order point at random.

There's a little complication, though. X25519 works on Montgomery curves, which are optimised for an x-coordinate only ladder. That ladder takes advantage of differential addition. Adding a low order point requires arbitrary addition, whose code is neither trivial nor available.

We can work around that problem by starting from Edwards25519 instead:

  • Select a random number, then clamp it as usual. It is now a multiple of the cofactor of the curve (8).
  • Multiply the Edwards25519 base point by that scalar.
  • Add a low order point at random. (By the way, be sure the selection of the low order point happens in constant time. Avoid naive array lookup tables.)
  • Convert the result to Curve25519 (clamping guarantees we do not hit the treacherous exceptions of the birational map).

The main advantage here is speed: Edwards25519 scalar multiplication by the base point often takes advantage of pre-computed tables, making it much faster than the Montgomery ladder in practice. (Note: pre-computed tables don't really apply to key exchange, which is why X22519 uses the Montgomery ladder instead.)

This has a price however: we now depend on EdDSA code, which is not ideal if we don't compute signatures as well. Moreover, some libraries, like TweetNaCl avoid pre-computed tables to simplify the code. This makes Edwards scalar multiplication slower than the Montgomery ladder.

Alternatively, there is a way to stay in Montgomery space: change the base point. Let's try it with the [0..43] group. Instead of using 4 as the base point, we'll add 11, a point whose order is the same as the cofactor (4). Our "dirty" base point is 15 (4+11). Now let's multiply that by Alice's public key:

O  = 11                -- low order point (order 4)
B  = 4                 -- base point      (prime order)
D  = B+LO = 11+4 = 15  -- "dirty" base point
sa = 20                -- Alice's private key
SA = B.sa = 4.20  = 36 -- Alice's public key
HA = D.sa = 15.20 = 36 -- ???

Okay we have a problem: even with the dirty base point, we get the same result. That's because Alice's private key is still a multiple of the cofactor, and absorbs the low order component. But we don't want to absorb it, we want to use it, to select a column at random. Here's the trick:

  • Use a multiple of the cofactor (4) to select the line.
  • Use a multiple of the prime order (11) to select the column.
  • Add those two numbers.
  • Multiply the dirty base point by it.

Note the parallel with EdDSA: we were adding points, now we add scalars. But the result is the same:

d  = 33               -- random multiple of the prime order
da = sa+d = 20+33 = 9 -- Alice's "dirty" secret key
HA = D.da = 15. 9 = 3 -- Alice's hidden key

Note that we can ensure both methods yield the same results by properly decoupling the main factor and the cofactor.

Now we can apply the method to Curve25519:

  • Add a low order point of order 8 to the base point. That's our new, "dirty" base point. That can be done offline, and the result hard coded. (I personally added the Edwards25519 to a low order Edwards point, then converted the result to Montgomery.)
  • Select a random number, then clamp it as usual. It is now a multiple of the cofactor of the curve (8). Note that if we multiply the dirty base point by that scalar, we'd absorb the low order point all over again.
  • Select a random multiple of the prime order. For Curve25519, that means, 0, L, 2L… up to 7L.
  • Add that multiple of L to the clamped random number above.
  • Multiply the resulting scalar by the dirty base point.

That way we no longer need EdDSA code.

(Note: you can look at actual code in the implementation of the crypto_x25519_dirty_*() functions in my Monocypher cryptographic library.)

Cofactor and scalar inversion

Scalar inversion is useful for exponential blinding to implement Oblivious Pseudo-Random Functions (OPRF). (It will make sense soon, promise).

Let's say Alice wants to connect to Bob's server, with her password. To maximise security, they use the latest and greatest in authentication technology (augmented PAKE). One important difference between that and your run of the mill password based authentication, is that Alice doesn't want to transmit her password. And Bob certainly doesn't want to transmit the salt to anyone but Alice, that would be the moral equivalent of publishing his password database.

Yet we must end up somehow with Alice knowing something she can use as the salt: a blind salt, which must be a function of the password and the salt only:

Blind_salt = f(password, salt)

One way to do this is with exponential blinding. We start by having Alice compute a random point on the curve, which is a function of the password:

P = Hash_to_point(Hash(password))

That will act as a kind of secret, hidden base point. The Hash_to_point() function can use the Elligator mappings. Note that even though P is the base point multiplied by some scalar, we cannot recover the value of that scalar (if we could, it would mean Elligator mappings could be used to solve discrete logarithm). Now Alice computes an ephemeral key pair with that base point:

r = random()
R = P . r

She sends R to Bob. Note that as far as Bob (or any attacker) is concerned, that point R is completely random, and has no correlation with the password, or its hash. The difficulty of discrete logarithm prevents them to recover P from R alone. Now Bob uses R to transmit the blind salt:

S = R . salt

He sends S back to Alice, who then computes the blind salt:

Blind_salt = S . (1/r)  -- assuming r × (1/r) = 1

Let's go over the whole protocol:

P          = Hash_to_point(Hash(password))
r          = random()
R          = P . r
S          = R . salt
Blind_salt = S . (1/r)
Blind_salt = R . salt . (1/r)
Blind_salt = P .  r . salt . (1/r)
Blind_salt = P . (r × salt × (1/r))
Blind_salt = P .      salt
Blind_salt = Hash_to_point(Hash(password)) . salt

And voila, our blind salt depends solely on the password and salt. You need to know the password to compute it from the salt, and if Mallory tries to connect to Bob, guessing the wrong password will give her a totally different, unexploitable blind salt. Offline dictionary attack is not possible without having hacked the database first.

Now this is all very well, if we are working on a prime-order subgroup. Scalars do have an inverse modulo the order of the curve, hash to point will give us what we want… except nope, our group does not have prime order. We need to deal with the cofactor, somehow.

The first problem with the cofactor comes from Hash_to_point(). When Elligator decodes a representative into a point on the curve, that point is not guaranteed to belong to the prime-order subgroup. There's the potential to leak up to 3 bits of information about the password (the cofactor of Curve25519 is 8). Fortunately, the point P is not transmitted over the network. Only R is. And this gives us the opportunity to clear the cofactor:

R = P . r

If the random scalar r is a multiple of 8, then R will be guaranteed to be on the prime-order subgroup, and we won't leak anything. X25519 has us covered: after clamping, r is a multiple of 8.

This guarantee however goes out the window as soon as R is transmitted across the network: Mallory could instead send a bogus key that is not on the prime-order subgroup. (They could also send a point on the twist, but Curve25519 is twist secure, so let's ignore that.) Again though, X25519 takes care of this:

S = R . salt

Just clamp salt, and we again have a multiple of 8, and S will be guaranteed to be on the prime-order subgroup.

Of course, Alice might receive some malicious S instead, so she can't assume it's the correct one. And this time, X25519 does not have us covered:

Blind_salt = S . (1/r)

See, X25519 clamping has a problem: while it clears the cofactor all right, it does not preserve the main factor. Which means clamping neither survives nor preserves algebraic transformations.

  • Inverting r then clamping does not work.
  • Clamping then inverting r does not clear the cofactor.

The solution is torsion safe representatives:

c = clamp(r)
i = 1/c    -- modulo L
s = i × t  -- t%L == 1  and  t%8 == 0

Where L is the prime order of the curve. For Curve25519, t = 3×L + 1. The performance trick explained in the torsion safe representatives section apply as expected.

(Note: you can look at actual code in the implementation of the crypto_x25519_inverse() function in Monocypher.)

Conclusion

Phew, done. I confess didn't think I'd need such a long post. But you get the idea: properly dealing with a cofactor these days is delicate. It's doable, but the cleaner solution these days is to use the Ristretto Group: you get modern curves and a prime order group.

(Discuss on Hacker News, Reddit, Lobsters)

 

Sursa: http://loup-vaillant.fr/tutorials/cofactor

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...