Let's get right into it. First, build your typical Diffie-Hellman key
agreement: Alice and Bob exchange public keys and derive the same
shared secret. Then Bob sends Alice some message with a MAC over
it. Easy as pie.
p =
71997739973919110306099993177739412743227643334286989217363396439283464537000853588
02973900485592910475480089726140708102474957429903531369589969318716771
g =
45653563970957406554368545034838268321361061416395634877324381953436904376061178283
18042418238184896212352329118608100083187535033402010599512641674644143
q = 236234353446506858198510045061214171961
"Order" is a new word, but it just means g^q = 1 mod p. You might
notice that q is a prime, just like p. This isn't mere chance: in
fact, we chose q and p together such that q divides p-1 (the order or
size of the group itself) evenly. This guarantees that an element g of
order q will exist. (In fact, there will be q-1 such elements.)
Back to the protocol. Alice and Bob should choose their secret keys as
random integers mod q. There's no point in choosing them mod p; since
g has order q, the numbers will just start repeating after that. You
can prove this to yourself by verifying g^x mod p = g^(x + k*q) mod p
for any x and k.
How can we attack this protocol? Remember what we said before about
order: the fact that q divides p-1 guarantees the existence of
elements of order q. What if there are smaller divisors of p-1?
j =
30477252323177606811760882179058908038824640750610513771646768011063128035873508507
547741559514324673960576895059570
You don't need to factor it all the way. Just find a bunch of factors
smaller than, say, 2^16. There should be plenty. (Friendly tip: maybe
avoid any repeated factors. They only complicate things.)
Got 'em? Good. Now, we can use these to recover Bob's secret key using
the Pohlig-Hellman algorithm for discrete logarithms. Here's how:
If h = 1, try again.
2. You're Eve. Send Bob h as your public key. Note that h is not a
valid public key! There is no x such that h = g^x mod p. But Bob
doesn't know that.
K := h^x mod p
Where x is his secret key and K is the output shared secret. Bob
then sends back (m, t), with:
Remember how we saw that g^x starts repeating when x > q? h has the
same property with r. This means there are only r possible values
of K that Bob could have generated. We can recover K by doing a
brute-force search over these values until t = MAC(K, m).
x = b1 mod r1
x = b2 mod r2
x = b3 mod r3
...
// ------------------------------------------------------------
So what if we can only recover some fraction of the Bob's secret key?
It feels like there should be some way to use that knowledge to
recover the rest. And there is: Pollard's kangaroo algorithm.
Don't worry about how f is implemented for now. Just know that it's a
function mapping where we are (some y) to the next jump we're going to
take (some x). And it's deterministic: for a given y, it should always
return the same x.
y := y * g^f(y)
The key thing here is that the next step we take is a function whose
sole input is the current element. This means that if our two
sequences ever happen to visit the same element y, they'll proceed in
lockstep from there.
xT := 0
yT := g^b
for i in 1..N:
xT := xT + f(yT)
yT := yT * g^f(yT)
Now: let's catch that wild kangaroo. We'll do a similar loop, this
time starting from y. Our hope is that at some point we'll collide
with the tame kangaroo's path. If we do, we'll eventually end up at
the same place. So on each iteration, we'll check if we're there.
xW := 0
yW := y
if yW = yT:
return b + xT - xW
Take a moment to puzzle out the loop condition. What that relation is
checking is whether we've gone past yT and missed it. In other words,
that we didn't collide. This is a probabilistic algorithm, so it's not
guaranteed to work.
Make sure also that you understand the return statement. If you think
through how we came to the final values for yW and yT, it should be
clear that this value is the index of the input y.
For some k, which you can play around with. Making k bigger will allow
you to take bigger leaps in each loop iteration.
p =
11470374874925275658116663507232161402086650258453896274534991676898999262641581519
101074740642369848233294239851519212341844337347119899874391456329785623
q = 335062023296420808191071248367701059461
j =
34233586850807404623475048381328686211071196701374230492615844865929237417097514638
999377942356150481334217896204702
g =
62295233533396129697815926608474108588988135873845993997829017993606363556674025855
5167783009058567397963466103140082647486611657350811560630587013183357
y =
77600738480326895053950057056773658766546291892980527757545976074466175586003940767
64814236081991643094239886772481052254010323780165093955236429914607119
The index of y is in the range [0, 2^20]. Find it with the kangaroo
algorithm.
Wait, that's small enough to brute force. Here's one whose index is in
[0, 2^40]:
y =
93888974780133995506941146144987906910341874530893552596026140741329188438998332773
97448144245883225611726912025846772975325932794909655215329941809013733
Find that one, too. It might take a couple minutes.
~~ later ~~
Enough about kangaroos, let's get back to Bob. Suppose we know Bob's
secret key x = n mod r for some r < q. It's actually not totally
obvious how to apply this algorithm to get the rest! Because we only
have:
x = n mod r
Which means:
x = n + m*r
For some unknown m. This relation defines a set of values that are
spread out at intervals of r, but Pollard's kangaroo requires a
continuous range!
Actually, this isn't a big deal. Because check it out - we can just
apply the following transformations:
x = n + m*r
y = g^x = g^(n + m*r)
y = g^n * g^(m*r)
y' = y * g^-n = g^(m*r)
g' = g^r
y' = (g')^m
Now simply search for the index m of y' to the base element g'. Notice
that we have a rough bound for m: [0, (q-1)/r]. After you find m, you
can plug it into your existing knowledge of x to recover the rest of
the secret.
Take the above group parameters and generate a key pair for Bob. Use
your subgroup-confinement attack from the last problem to recover as
much of Bob's secret as you can. You'll be able to get a good chunk of
it, but not the whole thing. Then use the kangaroo algorithm to run
down the remaining bits.
// ------------------------------------------------------------
I'm not going to show you any graphs - if you want to see one, you can
find them in, like, every other elliptic curve tutorial on the
internet. Personally, I've never been able to gain much insight from
them.
For the moment, it's not too important to know what a finite field
is. You can basically just think of it as "integers mod p" with all
the usual operations you expect: multiplication, division (via modular
inversion), addition, and subtraction.
We'll use the notation GF(p) to talk about a finite field of size
p. (The "GF" is for "Galois field", another name for a finite field.)
When we take a curve E over field GF(p) (written E(GF(p))), what we're
saying is that only points with both x and y in GF(p) are valid.
(3, 4.7) wouldn't be a valid point on either curve, since 4.7 is not
an integer and thus not a member of either field.
What about (3, -1)? This one is on the curve, but remember we're in
some GF(p). So in GF(7), -1 is actually 6. That means (3, -1) and (3,
6) are the same point. In GF(5), -1 is 4, so blah blah blah you get
what I'm saying.
P + (-P) = P + invert(P) = O
if P2 = O:
return P1
if P1 = invert(P2):
return O
x1, y1 := P1
x2, y2 := P2
if P1 = P2:
m := (3*x1^2 + a) / 2*y1
else:
m := (y2 - y1) / (x2 - x1)
x3 := m^2 - x1 - x2
y3 := m*(x1 - x3) - y1
The first three checks are simple - they pretty much just implement
the rules we have for the identity and inversion.
After that we, uh, use math. You can read more about that part
elsewhere, if you're interested. It's not too important to us, but it
(sort of) makes sense in the context of those graphs I'm not showing
you.
y * y * y * y * y = y^5
Your scalarmult function will look pretty much exactly the same as
your modexp function, except with the primitives swapped out.
Actually, you wanna hear something great? You could define a generic
scale function parameterized over a group that works as a drop-in
implementation for both. Like this:
function generate_keypair():
secret := random(1, baseorder)
public := scale(base, secret)
return (secret, public)
n*G = O
The fact that these two settings share so many similarities (and can
even share a naive implementation) is great news. It means we already
have a lot of the tools we need to reason about (and attack) elliptic
curves!
(182, 85518893674295321206118380980485522083)
It has order 29246302889428143187362802287225875743.
Oh yeah, order. Finding the order of an elliptic curve group turns out
to be a bit tricky, so just trust me when I tell you this one has
order 233970423115425145498902418297807005944. That factors to 2^3 *
29246302889428143187362802287225875743.
Our curve has almost-prime order. There's just that small cofactor of
2^3, which is beneficial for reasons we'll cover later. Don't worry
about it for now.
Wait, what? Yeah, points *not* on the curve. Look closer at our
combine function. Notice anything missing? The b parameter of the
curve is not accounted for anywhere. This is because we have four
inputs to the calculation: the curve parameters (a, b) and the point
coordinates (x, y). Given any three, you can calculate the fourth. In
other words, we don't need b because b is already baked into every
valid (x, y) pair.
233970423115425145550826547352470124412
233970423115425145544350131142039591210
233970423115425145545378039958152057148
They should have a fair few small factors between them. So: find some
points of small order and send them to Alice. You can use the same
trick from before to find points of some prime order r. Suppose the
group has order q. Pick some random point and multiply by q/r. If you
land on the identity, start over.
// ------------------------------------------------------------
All our hard work is about to pay some dividends. Here's a list of
cool-kids jargon you'll be able to deploy after completing this
challenge:
* Montgomery curve
* single-coordinate ladder
* isomorphism
* birational equivalence
* quadratic twist
* trace of Frobenius
Not that you'll understand it all; you won't. But you'll at least be
able to silence crypto-dilettantes on Twitter.
For a long time, this has been the most popular curve form. The NIST
P-curves standardized in the 90s look like this. It's what you'll see
first in most elliptic curve tutorials (including this one).
Although it's almost as old as the Weierstrass form, it's been buried
in the literature until somewhat recently. The Montgomery curve has a
killer feature in the form of a simple and efficient algorithm to
compute scalar multiplication: the Montgomery ladder.
No, really! Most people don't understand it. Instead, they visit the
Explicit-Formulas Database (https://www.hyperelliptic.org/EFD/), the
one-stop shop for state-of-the-art ECC implementation techniques. It's
like cheat codes for elliptic curves. Worth visiting for the
bibliography alone.
With that said, we should try to demystify this a little bit. Here's
the CliffsNotes:
3. cswap is a function that swaps its first two arguments (or not)
depending on whether its third argument is one or zero. Choosy
implementers choose arithmetic implementations of cswap, not
branching ones.
u2, w2 := (1, 0)
u3, w3 := (u, 1)
Go ahead and implement the ladder. Remember that all computations are
in GF(233970423115425145524320034830162017933).
Oh yeah, the curve parameters. You might be thinking that since we're
switching to a new curve format, we also need to pick out a whole new
curve. But you'd be totally wrong! It turns out that some short
Weierstrass curves can be converted into Montgomery curves.
2. Figure out how to map points back and forth between curves.
u = x - 178
v = y
(4, 85518893674295321206118380980485522083)
One nice thing about the Montgomery ladder is its lack of special
cases. Specifically, no special handling of: P1 = O; P2 = O; P1 = P2;
or P1 = -P2. Contrast that with our Weierstrass addition function and
its battalion of ifs.
ladder(76600469441198017145391791613091732004, 11)
You should detect that something is quite wrong. This u does not
represent a point on our curve! Not every u does.
This means that even though we can only submit one coordinate, we
still have a little bit of leeway to find invalid
points. Specifically, an input u such that u^3 + 534*u^2 + u is not a
quadratic residue can never represent a point on our curve. So where
the heck are we?
3. Both the original curve and its twist have a point (0, 0). This is
just a regular point, not the group identity.
4. Both the original curve and its twist have an abstract point at
infinity which serves as the group identity.
The only caveat is that she won't be able to recover the full secret
using off-curve points, only a fraction of it. But we know how to
handle that.
So:
1. Calculate the order of the twist and find its small factors. This
one should have a bunch under 2^24.
4. When you've exhausted all the small subgroups in the twist, recover
the remainder of Alice's secret with the kangaroo attack.
// ------------------------------------------------------------
First, implement ECDSA. If you still have your old DSA implementation
lying around, this should be straightforward. All the same, here's a
refresher if you need it:
Once you've got this implemented, generate a key pair for Alice and
use it to sign some message m.
It would be tough for Eve to find a Q' to verify this signature if all
the domain parameters are fixed. But the domain parameters might not
be fixed - some protocols let the user specify them as part of their
public key.
R = u1*G + u2*Q
R = u1*G + u2*(d*G)
R = (u1 + u2*d)*G
2. Calculate t := u1 + u2*d'.
Note that Eve's public key is totally valid: both the base point and
her public point are members of the subgroup of prime order n. Since
E(GF(p)) and n are unchanged from Alice's public key, they should pass
the same validation rules.
Assuming the role of Eve, derive a public key and domain parameters to
verify Alice's signature over the message.
Let's do the same thing with RSA. Same setup: we have some message and
a signature over it. How do we craft a public key to verify the
signature?
So what we're really looking for is the pair (e', N') to make that
equality hold up. If this is starting to look a little familiar, it
should: what we're doing here is looking for the discrete logarithm of
pad(m) with base s.
a. p-1 should be smooth. How smooth is up to you, but you will need
to find discrete logarithms in each of these subgroups. You can
use something like Shanks or Pollard's rho to compute these in
square-root time.
g^((p-1)/q) != 1 mod p
2. Now pick a prime q. Ensure the same conditions as before, but add these:
Easy as pie. e' will be a lot larger than the typical public exponent,
but that's still legal.
Since RSA signing and decryption are equivalent operations, you can
use this same technique for other surprising results. Try generating a
random (or chosen) ciphertext and creating a key to decrypt it to a
plaintext of your choice!
// ------------------------------------------------------------
Back in set 6 we saw how "nonce" is kind of a misnomer for the k value
in DSA. It's really more like an ephemeral key. And distressingly, the
security of your long-term private key hinges on it.
How far can we take this? Turns out, pretty far: even a slight bias in
nonce generation is enough for an attacker to recover your private
key. Let's see how.
How does this help us? Let's review the signing algorithm:
(Quick note: before we used "n" to mean the order of the base
point. In this problem I'm going to use "q" to avoid naming
collisions. Deal with it.)
Remember that these calculations are all modulo q, the order of the
base point. Now, let's define some stand-ins:
t = r / ( s*2^l)
u = H(m) / (-s*2^l)
Remember that b is small. Whereas t, u, and the secret key d are all
roughly the size of q, b is roughly q/2^l. It's a rounding
error. Since b is so small, we can basically just ignore it and say:
d*t ~ u
d*t ~ u + m*q
0 ~ u + m*q - d*t
We said that a basis is just the right size for the vector space it
spans, but that shouldn't be taken to imply uniqueness. Indeed, any of
the lattices we will care about have infinite possible bases. The only
requirements are that the basis spans the space and the basis is
minimal in size. In that sense, all bases of a given lattice are
equal.
But some bases are more equal than others. In practice, people like to
use bases comprising shorter vectors. Here "shorter" means, roughly,
"containing smaller components on average". A handy measuring stick
here is the Euclidean norm: simply take the dot product of a vector
with itself and take the square root. Or don't take the square root, I
don't care. It won't affect the ordering.
Why do people like these smaller bases? Mostly because they're more
efficient for computation. Honestly, it doesn't matter too much why
people like them. The important thing is that we have relatively
efficient methods for "reducing" a basis. Given an input basis, we can
produce an equivalent-but-with-much-shorter-vectors basis. How much
shorter? Well, maybe not the very shortest possible, but pretty darn
short.
1. Encode your problem space as a set of vectors forming the basis for
a lattice. The lattice you choose should contain the solution
you're looking for as a short vector. You don't need to know the
vector (obviously, since you're looking for it), you just need to
know that it exists as some integral combination of your basis
vectors.
2. Derive a reduced basis for the lattice. We'll come back to this.
4. That's it.
0 ~ u + m*q - d*t
0 ~ u1 + m1*q - d*t1
0 ~ u2 + m2*q - d*t2
0 ~ u3 + m3*q - d*t3
0 ~ u4 + m4*q - d*t4
0 ~ u5 + m5*q - d*t5
0 ~ u6 + m6*q - d*t6
...
0 ~ un + mn*q - d*tn
bt = [ t1 t2 t3 t4 t5 t6 ... tn ]
bu = [ u1 u2 u3 u4 u5 u6 ... un ]
b1 = [ q 0 0 0 0 0 ... 0 ]
b2 = [ 0 q 0 0 0 0 ... 0 ]
b3 = [ 0 0 q 0 0 0 ... 0 ]
b4 = [ 0 0 0 q 0 0 ... 0 ]
b5 = [ 0 0 0 0 q 0 ... 0 ]
b6 = [ 0 0 0 0 0 q ... 0 ]
... ...
bn = [ 0 0 0 0 0 0 ... q ]
bt = [ t0 t1 t2 t3 t4 t5 ... tn ]
bu = [ u0 u1 u2 u3 u4 u5 ... un ]
See how the columns cutting across our row vectors match up with the
approximations we collected above? Notice also that the lattice
defined by this basis contains at least one reasonably short vector
we're interested in:
b1 = [ q 0 0 0 0 0 ... 0 0 0 ]
b2 = [ 0 q 0 0 0 0 ... 0 0 0 ]
b3 = [ 0 0 q 0 0 0 ... 0 0 0 ]
b4 = [ 0 0 0 q 0 0 ... 0 0 0 ]
b5 = [ 0 0 0 0 q 0 ... 0 0 0 ]
b6 = [ 0 0 0 0 0 q ... 0 0 0 ]
... ...
bn = [ 0 0 0 0 0 0 ... q 0 0 ]
bt = [ t0 t1 t2 t3 t4 t5 ... tn ct 0 ]
bu = [ u0 u1 u2 u3 u4 u5 ... un 0 cu ]
We've added two new columns with sentinel values in bt and bu. This
will allow us to determine whether these two vectors are included in
any of the output vectors and in what proportions. (That's not the
only problem this solves. Our last set of vectors wasn't really a
basis, because we had n+2 vectors of degree n, so there were clearly
some redundancies in there.)
We can identify the vector we're looking for by looking for cu in the
last slot of each vector in our reduced basis. Our hunch is that the
adjacent slot will contain -d*ct, and we can divide through by -ct to
recover d.
Okay. To go any further, we need to dig into the nuts and bolts of
basis reduction. There are different strategies for finding a reduced
basis for a lattice, but we're going to focus on a simple and
efficient polynomial-time algorithm: Lenstra-Lenstra-Lovasz (LLL).
Most people don't implement LLL. They use a library, of which there
are several excellent ones. NTL is a popular choice.
n := len(B)
k := 1
while k < n:
for j in reverse(range(k)):
if abs(mu(k, j)) > 1/2:
B[k] := B[k] - round(mu(k, j))*B[j]
Q := gramschmidt(B)
return B
B is our input basis. Delta is a parameter such that 0.25 < delta <=
1. You can just set it to 0.99 and forget about it.
function gramschmidt(B):
Q := []
for i, v in enumerate(B):
Q[i] := v - sum(proj(u, v) for u in Q[:i])
return Q
Back to LLL. The best way to get a sense for how and why it works is
to implement it and test it on some small examples with lots of debug
output. But basically: we walk up and down the basis B, comparing each
vector b against the orthogonalized basis Q. Whenever we find a vector
q in Q that mostly aligns with b, we shave off an integral
approximation of q's projection onto b. Remember that the lattice
deals in integral coefficients, and so must we. After each iteration,
we use some heuristics to decide whether we should move forward or
backward in B, whether we should swap some rows, etc.
One more thing: the above description of LLL is very naive and
inefficient. It probably won't be fast enough for our purposes, so you
may need to optimize it a little. A good place to start would be not
recalculating the entire Q matrix on every update.
b1 = [ -2 0 2 0]
b2 = [ 1/2 -1 0 0]
b3 = [ -1 0 -2 1/2]
b4 = [ -1 1 1 2]
b1 = [ 1/2 -1 0 0]
b2 = [ -1 0 -2 1/2]
b3 = [-1/2 0 1 2]
b4 = [-3/2 -1 2 0]
All that's left is to tie up a few loose ends. First, how do we choose
our sentinel values ct and cu? This is kind of an implementation
detail, but we want to "balance" the size of the entries in our target
vector. And since we expect all of the other entries to be roughly
size q/2^l:
ct = 1/2^l
cu = q/2^l
3. As the attacker, collect your (u, t) pairs. You can experiment with
the amount. With an eight-bit nonce bias, I get good results with
as few as 20 signatures. YMMV.
4. Stuff your values into a matrix and reduce it with LLL. Consider
playing with some smaller matrices to get a sense for how long this
will take to run.
// ------------------------------------------------------------
GCM is the most widely deployed block cipher mode for authenticated
encryption with associated data (AEAD). It's basically just CTR mode
with a weird MAC function wrapped around it. The MAC function works by
evaluating a polynomial over GF(2^128).
Remember how much trouble a repeated nonce causes for CTR mode
encryption? The same thing is true here: an attacker can XOR
ciphertexts together and recover plaintext using statistical methods.
But there's an even more devastating consequence for GCM: it leaks the
authentication key immediately!
2. AD, C, and their respective lengths are known. For a given message,
the attacker knows everything about the MAC polynomial except the
masking block
3. The masking block is generated using only the key and the nonce. If
the nonce is repeated, the mask is the same. If we can collect two
messages encrypted under the same nonce, they'll have used the same
mask.
The last step probably feels a little magical, but you don't actually
need to understand it to implement the attack: you can literally just
plug the right values into a computer algebra system like SageMath and
hit "factor".
But that's not satisfying. You didn't come this far to beat the game
on Easy Mode, did you?
I didn't think so. Now, let's dig into that MAC function. Like I said,
a polynomial over GF(2^128).
So far, all the fields we've worked with have been of prime size p,
i.e. GF(p). It turns out we can construct GF(q) for any q = p^k for
any positive integer k. GF(p) = GF(p^1) is just one form that's common
in cryptography. Another is GF(2^k), and in this case we have
GF(2^128).
0
1
x
x + 1
x^2
x^2 + 1
x^2 + x
x^2 + x + 1
x^3
x^3 + 1
x^3 + x
x^3 + x + 1
x^3 + x^2
x^3 + x^2 + 1
x^3 + x^2 + x
x^3 + x^2 + x + 1
...
And so forth.
If you squint a little they look like the binary expansions of the
integers counting up from zero. This is convenient because it gives us
an obvious choice of representation in unsigned integers.
while a > 0:
if a & 1:
p := p ^ b
a := a >> 1
b := b << 1
return p
return q, r
0
1
x
x + 1
x^2
x^2 + 1
x^2 + x
x^2 + x + 1
x^3
x^3 + 1
x^3 + x
x^3 + x + 1
x^3 + x^2
x^3 + x^2 + 1
x^3 + x^2 + x
x^3 + x^2 + x + 1
while a > 0:
if a & 1:
p := p ^ b
a := a >> 1
b := b << 1
if deg(b) = deg(m):
b := b ^ m
return p
You can implement both versions to prove to yourself that the output
is the same.
You may find yourself in want of other functions you take for granted
in the integer setting, e.g. modexp. Most of these should have
straightforward equivalents in our polynomial setting. Do what you
need to.
Okay, now that you are the master of GF(2^k), we can finally talk
about GCM. Like I said (many words ago): CTR mode for encryption,
weird MAC in GF(2^128).
The size of this field was chosen very specifically to match up with
the width of a 128-bit block cipher. We can convert a block into a
field element trivially; the leftmost bit is the coefficient of x^0,
and so on.
I described the MAC at a very high level. Here's a more detailed view:
h := E(K, 0)
2. Add one last block describing the length of the AD and the length
of the ciphertext. Original lengths, not padded lengths; bit
lengths, not byte lengths. Like this:
len(AD) || len(C)
g := 0
for b in bs:
g := g + b
g := g * h
s := E(K, nonce || 1)
t := g + s
Implement GCM. Use AES-128 as your block cipher. You can probably
reuse whatever you had before for CTR mode. The important new thing to
implement here is the MAC. The above description is brief and
informal; check out the spec for the finer points. Since you've
already got the tools for working in GF(2^k), this shouldn't take too
long.
Okay. Let's rethink our view of the MAC. We'll use our example payload
from above. Here it is:
That's one block of AD and two blocks of ciphertext. The MAC will look
like this:
See how the s masks are identical? They depend only on the nonce and
the encryption key. Since addition is XOR in our field, we can add
these two equations together and that mask will wash right out:
With that out of the way, let's get factoring. Factoring a polynomial
over a finite field means separating it out into smaller polynomials
that are irreducible over the field. Remember that irreducible
polynomials are sort of like prime numbers.
3. Finally, we take each output from the last step and perform an
equal-degree factorization. This is pretty much like it sounds. In
that last example, we'd take that twelfth-degree polynomial and
factor it into its fourth-degree components.
if g = 1:
g := h^((q^d - 1)/3) - 1 mod f
for u in S:
if deg(u) = d:
continue
return S
The only hitch is that we don't know what these moduli are. But we
don't need to! Since f is their product, we can perform these
operations mod f and implicitly apply the Chinese Remainder Theorem.
Just keep doing this until the whole thing is factored into
irreducible parts.
y + c
If you do have more than one candidate, there are two ways to narrow
the list:
// ------------------------------------------------------------
It's somewhat common to use a truncated MAC tag. For instance, you
might be authenticating with HMAC-SHA256 and shorten the tag to 128
bits. The idea is that you can save some bandwidth or storage and
still have an acceptable level of security.
In some protocols, you might take this to the extreme. If two parties
are exchanging lots of small packets, and the value of forging any one
packet is pretty low, they might use a 16-bit tag and expect 16 bits
of security.
In GCM, this is a disaster.
To see how, we'll first review the GCM MAC function. We make a
calculation like this:
We'll also ignore the possibility of AD blocks here, since they don't
matter too much for our purposes.
f(y) = c*y
To construct Mc, just calculate c*1, c*x, c*x^2, ..., c*x^127, convert
each product to a vector, and let the vectors be the columns of your
matrix. You can verify by performing the matrix multiplication against
y and checking the result. I'm going to assume you either know how
matrix multiplication works or have access to Wikipedia to look it up.
f(y) = y^2
Okay, let's put these matrices on the back burner for now. To forge a
ciphertext c', we'll start with a valid ciphertext c and flip some
bits, hoping that:
sum(ci * h^i) = sum(ci' * h^i)
sum(ei * h^i) = 0
sum(di * h^(2^i)) = 0
sum(di * h^(2^i)) = e
sum(Mdi * Ms^i * h) = e
sum(Mdi * Ms^i) * h = e
Ad = sum(Mdi * Ms^i)
Ad * h = e
Let's think about how the bits in the vector e are calculated. This
just falls out of the basic rules of matrix multiplication:
e[0] = Ad[0] * h
e[1] = Ad[1] * h
e[2] = Ad[2] * h
e[3] = Ad[3] * h
...
Suppose the MAC is 16 bits. If we can flip bits and force eight rows
of Ad to zero, that's eight bits of the MAC we know are right. We can
flip whatever bits are left over with a 2^-8 chance of a forgery, way
better than the expected 2^-16!
Actually, let's leave d0 alone. That's the block that encodes the
ciphertext length. Things could get tricky pretty quickly if we start
messing with it.
We still have d1, ..., dn to play with. That means n*128 bits we can
flip. Since the rows of Ad are each 128 bits, we'll have to settle for
forcing n-1 of them to zero. We need some bits left over to play with.
After doing this for each column, T will be full of ones and
zeros. We're looking for sets of bit flips that will zero out those
first n-1 rows. In other words, we're looking for solutions to this
equation:
T * d = 0
Where d is a vector representing all n*128 bits you have to play with.
If you know a little bit of linear algebra, you'll know that what we
really want to find is a basis for N(T), the null space of T. The null
space is exactly that set of vectors that solve the equation
above. Just what we're looking for. Recall that a basis is a minimal
set of vectors whose linear combinations span the whole space. So if
we find a basis for N(T), we can just take random combinations of its
vectors to get viable candidates for d.
Finding a basis for the null space is not too hard. What you want to
do is transpose T (i.e. flip it across its diagonal) and find the
reduced row echelon form using Gaussian elimination. Now perform the
same operations on an identity matrix of size n*128. The rows that
correspond to the zero rows in the reduced row echelon form of T
transpose form a basis for N(T).
Now that we have a basis for N(T), we're ready to start forging
messages. Take a random vector from N(T) and decode it to a bunch of
bit flips in your known good ciphertext C. (Remember that you'll be
flipping bits only in the blocks that are multiplied by h^(2*i) for
some i.) Send the adjusted message C' to the oracle and see if it
passes authentication. If it fails, generate a new vector and try
again.
Pick those rows out and stuff them in a matrix of their own. Call it,
I don't know, K. Here's something neat we know about K:
K * h = 0
h = X * h'
Ad * h = e
We can say:
Ad * X * h' = e
So: start over and build a new T matrix, but this time to nullify rows
of Ad * X. Forge another message and harvest some new linear equations
on h. Stuff them in K and recalculate X.
The endgame comes when K has 127 linearly independent rows. N(K) will
be a 1-dimensional subspace containing exactly one nonzero vector, and
that vector will be h.
1. Build a toy system with a 32-bit MAC. This is the smallest tag
length NIST defines in the GCM specification.
// ------------------------------------------------------------
In the last problem we saw that GCM is very difficult to use safely
with truncated authentication tags. An attacker can greatly improve
their chances of message forgery. After several successful forgeries,
they can recover the authentication key and forge messages at will.
NIST, in turn, absorbed his feedback and then published its official
GCM specification, which says to go ahead and use tags as short as 32
bits, if you feel like it. YOLO.
So what kind of advice does NIST offer to help you avert disaster? Two
pages of considerations culminating in tables specifying:
Let's see how. First, recall the big picture. For a three-block
ciphertext, the MAC is calculated like this:
The c1 block encodes the length, and [c2, c4] are the actual blocks of
ciphertext (in reverse order). s is an unknown mask generated using
the nonce and the encryption key.
Well, not every block. We only used c2 and c4; we left c1 alone, even
though it is a coefficient of h = h^1 = h^(2^0). Keep that in mind,
we'll come back to it.
Remember that with n full blocks we had 128*n free variables, which we
could use to force n-1 rows of our difference matrix Ad to zero. We'd
tweak one block arbitrarily and adjust the others to compensate. If we
tried to force n rows to zero, the only solution would be the all-zero
solution where we tweak nothing.
T * d = 0
T * d = t
Some caveats: