Solving ZK Hack Puzzle 3 - Chaos Theory

What follows is a solution to ZK Hack IV, Puzzle 3. ZK Hack is a series of zero-knowledge cryptography CTFs, workshops, hackathons, and study groups. Thanks to the ZK Hack organizers for creating this CTF, Geometry for this puzzle, and the ZK Hack community. You might also be interested in solution for puzzle 2. If reposting elsewhere, find this post on my blog here.

You might be interested in this post if you’re interested in:

  • an thought experiment on how building an authentication scheme based in elliptic curve pairings can go horribly awry!
  • Some background on ElGamal encryption and BLS signatures
  • The process of (cryptographic) puzzle solving

Of the three ZK Hack puzzles, this was by far the most straight-forward, so this will be a shorter post than the prior two.

puzzle.solve()

Background

all the knowledge nuggets you need to solve the puzzle

In this puzzle, we are presented with a novel scheme for the age-old cryptographic primitive, encryption plus authentication. Our Bob combines ElGamal encryption with BLS signatures. The details of each algorithm don’t much matter, but the way Bob re-uses his encryption key for BLS signing very much does. Our goal is to decrypt the ciphertext, without access to the decryption algorithm.

We visit BLS and Elgamal schemes in more depth in the next section, but familiarity with these algorithms is unrequired for working this problem.

Elliptic curve points are denoted in capitals, scalars in lower case. The sender’s secret and private keys are denoted sk,PK=skG, and the receiver’s are denoted sk,PK. We have 3 algorithms:

  • Encryption (send): c=(PK,skPK+M)Esk(M,PK)
  • Authentication (authenticate): σ=skH(c)Ask(c)
  • Auth Verification (check_auth): b0,1=e(G,σ)=?e(PK,H(c))V(PK,c,σ)

Where the final authentication checks satisfies, since: e(G,σ)=e(PK,H(c))ske(G,H(c))=ske(G,H(c))

We also are given a list of messages that Bob may choose to send.

We now demonstrate the exploit.

==🔴Stop! This is the 🚨puzzle solving police🚨! You have been given everything you need to find your own solution! Take ten minutes, pull out your pen, and reap the joy and mathematical gainz of problem solving🔴==

.

.

.

.

.

.

.

.

.

the ‘sploit

The puzzle solution in short

Let’s revisit those 3 algorithms:

  • Encryption (send): c=(PK,skPK+M)Esk(M,PK)
  • Authentication (authenticate): σ=skH(c)Ask(c)
  • Auth Verification (check_auth): b0,1=e(G,σ)=?e(PK,H(c))V(PK,c,σ)

Bob’s only innovation is to re-use the encryption secret key for signing. Because we have the list of messages he may have sent, we can try to peel the message off of c2. By testing each possible message Mi, we may compute $skPK':$ X_i = skPK' = c_2 - M_i = (sk*PK' + M) - M_i $$

We have a line-up. One of these Ai’s is our guy. How will we differentiate them? We haven’t done anything creative or unusual yet. But unlike secure encryption+authentication, now we have a pairing at our disposal.

We have that, for some i, X=Xi=skPK=skskG. That looks a lot like a public key, corresponding to secret key x=sksk.

Can we shove X into our verification step? e(G,σ)=?e(Xi,H(c))V(Xi,c,σ)ske(G,H(c))skske(G,H(c)) Almost. We need to find a way to squeeze sk onto the left-hand-side. If we can multiply G or σ by sk, we’re done, and the former is just the public key, PK.

We can’t re-use the provided auth verification algorithm, but we can easily write that new auth-verification ourselves:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub fn check_auth_evil(s1s2_G: G1Affine, c: &ElGamal, s: G2Affine, receiver_pk: G1Affine) -> bool {
  // let lhs = { Bls12_381::pairing(G1Projective::generator(), s) };
  // (sk_r * G1, sk_s * H(.))
  let lhs = { Bls12_381::pairing(receiver_pk, s) };
  // let lhs = { receiver_pk, s) };

  let hash_c = c.hash_to_curve();
  // (sk_r*sk_s * G1, H(.))
  let rhs = { Bls12_381::pairing(s1s2_G, hash_c) };

  lhs == rhs
}

repo link

Plug that bad-boy in, and we’re done:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  let sender_pk = blob.sender_pk;
  let c = blob.c.clone();
  let c1 = blob.c.1.clone();
  let s = blob.s;
  let rec_pk = blob.rec_pk;

  for (i, m) in _messages.into_iter().enumerate() {
    let a = (c1 - m.0).into_affine();
    let e = check_auth_evil(a, &c, s, rec_pk);
    if e {
      warn!("found message at index {}", i);
      warn!("win");
      break;
    }
  }

line em up

puzzle.deets()

Extended background on BLS and ElGamal encryption. This isn’t technically required to solve the problem, but the interested reader may enjoy the background context.

BLS

The BLS signature scheme is a simple signature aggregation scheme, relying on a elliptic curve pairing. It consists of 3 algorithms, (sign, aggregate_signatures, verify), where verify checks either any individual’s signature, or the whole aggregated signature. We don’t use aggregation at all in this puzzle.

  • Individual signing - Each individual signature σi=skiH(M) is curve point on G2. Each public key pki=skiG1 is a curve point on G1.

  • Individual signature verification and aggregated verification - Verification for any individual signature σi is the same as for the entire signature, so we drop the indices: e(pk,H(M))=e(G1,σ) Note that we don’t typically verify the individual signatures, only the final aggregated signature. The point of a signature aggregation scheme is that the aggregated signature is valid if and only if each component signature is valid.

  • Signature aggregation - Simply compute the sum of the signatures and sum of public keys:

σagg=i=1nσi=(ski)H(M)pkagg=pki=(ski)G1

ElGamal

ElGamal encryption is a secure but non-standard public-key cryptosystem based on the Diffie-Hellman key exchange. It was described by Taher Elgamal in 1985. ElGamal can be used for both encryption and digital signatures. The security of the ElGamal encryption system is based on the difficulty of solving the discrete logarithm problem. ElGamal encryption generally produces larger ciphertexts compared to the original plaintext size, effectively doubling the message size. This can be inefficient in terms of bandwidth and storage.

Denoting scalars in lower case, and elliptic curve points in upper case:

Key Generation:

  • Private Key: Select a random number x as the recipient private key.
  • Public Key: Multiply the generator point G of the curve by x to get Q=xG. The recipient public key is Q.

Encryption: (message, pk) -> ciphertext

  • Given a plaintext message M represented as a point on the curve:
  • Choose a random number r.
  • Compute C1=rG.
  • Compute C2=M+rQ.
  • The ciphertext is the pair C=(C1,C2).

Decryption: (ciphertext, sk) -> message

  • Compute M=C2xC1.
  • This works because: C2xC1=M+rQx(rG)=M+r(xG)xrG=M.

puzzle.log()

My puzzle solving log, lightly edited for readability. Keeping a log helps navigate overwhelming context dumps, and to avoid doing wrong and/or stupid things repeatedly. I use Obsidian to take these notes, for which I have written an extensive setup and usage guide. My repo can be found here. For taking math notes, I generally use a whiteboard or ipad for my scratch space before writing down my clean notes here, which helps to think n scratch faster. This puzzle in particular may have been faster to just work out on a whiteboard, without a log at all.

it be time for logging

Code runthrough

  • hasher - same hash as last puzzle it looks like, sha256 based
  • struct ElGamal representing ciphertext. Over a pair of G1Affine elements, let’s tag it with Debug for convenience.
    • hash_to_curve(self) to create a G2 element
    • we’re implementing our own hash to curve? check pre-image resistance (not collision resistance based on goal context). Sha256 over 128 bits is still pretty strong, so it seems fine
  • struct Message over G1Affine
  • struct Sender over a secret key in Fr and a pk
    • verify that pk is correctly constructed
    • send a message to receiver
    • authenticate generate the tag s in G2Affine: s=H(c)sk
  • struct Receiver gets a pk
  • Auditor
    • check_auth takes a the sender pk, the cipher-text, and the authentication tag s
      • encrypt-then-sign is fine, unless the encryption and authentication are malleable?
      • lhs: e(G1,s)
      • rhs: e(pk,H(c)) , where H is our sketchy hash function

I think we have everything we need already to work this out. To the whiteboard!

See above notes for whiteboard observations.

updatedupdated2024-02-052024-02-05