Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Latest commit

 

History

History
95 lines (73 loc) · 4.39 KB

challenge4solution.md

File metadata and controls

95 lines (73 loc) · 4.39 KB

To solve for the flag of the Xyber CTF challenge, we will have to successfully connect to the TLS server. From the prompt, we see that the server only accepts the X25519Kyber768 key exchange specified in the X25519Kyber768Draft00 IETF draft. This key exchange, as mentioned in the draft, is only for TLS 1.3.

What we need to know in order to solve this challenge is that, during a TLS 1.3 handshake, the following pieces of information gets exchanged:

  1. First, inside the ClientHello message sent by the client, there is a key_share extension that carries some client-side key exchange data.

  2. Inside the ServerHello message returned by the server, there is also a key_share extension that carries some server-side key exchange data.

  3. Upon seeing both key_share extensions, both parties (client and server) are able to derive a shared secret from them, and use that shared secret to derive the actual session key.

With this knowledge in mind, we take a look at a few snippets of tlslite/keyexchange.py which contains all the key exchange code and with X25519Kyber768Draft00 handshake already implemented with placeholders.

def get_random_private_key(self):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    return getRandomBytes(X25519_ORDER_SIZE)
# ...

def calc_public_value(self, private):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    return x25519(private, bytearray(X25519_G))
# ...

def calc_shared_key(self, private, peer_share):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    S = x25519(private, peer_share)
    self._non_zero_check(s)
    return S
# ...

We see that there are three function calls that involve handling this particular type of key exchange. Get_random_private_key generates the key pair used for key exchange, calc_public_value outputs what key_share payload goes into the ClientHello, and lastly calc_shared_key computes the shared secret from the two key_share extensions in ClientHello and ServerHello. Right now they just implement a plain X25519 key exchange as a placeholder.

Fortunately, we only need to implement the client side here as the server side already supports the new key exchange, therefore, we just need to finish implementing the three function calls and it should just work.

Taking a closer look at Section 3 of the draft, we see that the construction is extremely simple.

For the client's share, the key_exchange value contains the concatenation of the client's X25519 ephemeral share (32 bytes) and the client's Kyber768Draft00 public key (1184 bytes). The resulting key_exchange value is 1216 bytes in length.

The shared secret is calculated as the concatenation of the X25519 shared secret (32 bytes) and the Kyber768Draft00 shared secret (32 bytes). The resulting shared secret value is 64 bytes in length.

Basically, we keep the original X25519 key exchange placeholder, and do another Kyber768 key exchange in parallel, and concatenate the data to the end of the payloads.

def get_random_private_key(self):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    # Random X25519 sk
    xsk = getRandomBytes(X25519_ORDER_SIZE)
    # Random Kyber768 sk
    _, ksk = Kyber768.keygen()
    # Contatenating two sk
    return xsk + ksk
# ...

def calc_public_value(self, private):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    # X25519 pk
    xpk = x25519(private[:X25519_ORDER_SIZE], bytearray(X25519_G))

 # Kyber768 pk
    kpk = Kyber768.get_pk_from_sk(private[X25519_ORDER_SIZE:])
    # Concatenating to pk
    return xpk + kpk
# ...

def calc_shared_key(self, private, peer_share):
# ...
elif self.group == GroupName.x25519kyber768draft00:
    # Parse the first part of sk as X25519 private key
    xsk = private[:X25519_ORDER_SIZE]
    peer_xpk = peer_share[:X25519_ORDER_SIZE]
    # Compute X25519 shared secret
    xsecret = x25519(xsk, peer_xpk)
    self._non_zero_check(xsecret)

    # Parse the second part of sk as Kyber768 private key
    ksk = private[X25519_ORDER_SIZE:]
    peer_kencaps = peer_share[X25519_ORDER_SIZE:]
    # Compute Kyber768 shared secret (client side calls Decaps)
    ksecret = Kyber768.dec(peer_kencaps, ksk)

    # Return the concatenation of the two shared secrets
    return xsecret + ksecret
# ...

Upon fixing the tlslite implementation, running the client will output success and the flag.

References

https://www.ietf.org/archive/id/draft-tls-westerbaan-xyber768d00-03.html