Auth

Theme Song: Who Am I?

In this assignment, you'll extend the secure communication protocol from Signal to build a secure server-client authentication system. In particular, you'll explore the ways in which we can use digital signatures to expand our circle of trust and be sure that nobody is pretending to be someone they're not.


Background Knowledge

In this assignment, you'll build a secure authentication platform. There are two programs involved: a server and a client. The server acts as a central verification authority that clients will interact with to obtain certificates. The server has its own globally-recognized public key, and signs the certificates of clients who properly log in. The clients will then use these certificates to communicate with each other, protected against impersonation attacks. In order to verify with the server, each client must provide a valid password and 2FA response.

Digital Signatures

You've interacted with digital signatures briefly in the first (warm-up) assignment, but we will go over them again here. Digital signatures are essentially the public-key equivalent to MACs, allowing one party to sign a message, and all other parties to verify that this message was signed by that party. To do so, we first generate a keypair consisting of a public verification key \(vk\) and a secret signing key \(sk\), then use \(sk\) to sign various messages \(\sigma_i = Sign_{sk}(m_i)\). When another party is given a message and signature, they can use \(vk\) to verify that the message was signed correctly: \(Verify_{vk}(\sigma_i, m_i) ?= true\). We want it to be the case that it is hard to forge signatures; that is, given \(vk\) but not \(sk\), finding valid signatures for any message, even when given valid signatures for other messages, is hard.

Using digital signatures, we can achieve authenticated key exchange that is secure against man-in-the-middle attacks which we have seen in project Signal.

Password Authentication

You probably authenticate by password every day. Password authentication relies on both a server and a user knowing some shared secret \(pw\), and the user proves that they know this secret by sending \(pw\) (or some altered version of it) to the server. With the cryptographic primitives we've explored thus far in mind, there are a number of naive ways that one might implement password authentication. One might encrypt the password and send it to the server, who will then decrypt it and store the plaintext password in a database for later verification. While encryption makes this protocol safe from eavesdropping attacks, storing the plaintext password doesn't protect against cases where the server or database are compromised. Even if we stored a hash of the password, adversaries that have access to the database can mount an offline brute-force dictionary attack or consult a rainbow table to crack the passwords We want to be careful to protect against a variety of attacks against all parts of our system.

We propose a heavily redundant but secure password authentication scheme so that you get a sense of the techniques you may see out in the wild. On registration, the server generates and sends a random (say, 128-bit) salt to the user. A salt is a random string appended to a password before hashing it to prevent dictionary attacks. The user sends the hash of their password with the salt appended: \(h := H(pw || salt)\) to the server, which then computes a random short (say, 8-bit) pepper and hashes the user's message with the pepper appended yet again: \(h' := H(h || pepper)\). Finally, the server stores the salt, but not the pepper. On verification, the server sends the stored salt to the user, who then sends the hash of their password. Then, the server tries all \(2^8\) possible pepper values and verifies if any one of them succeeds. We avoid sending the password in plaintext form over the wire, and we avoid storing a version of the password that can be used for authentication in the database.

Pseudorandom Functions and 2FA

Random numbers are convenient because they introduce a level of unpredictability to systems which can be very useful for keeping secret values secret (e.g. ElGamal encryption uses random values to ensure that even two ciphertexts of the same message are distinct). However, sometimes you want both you and your partner to experience the same randomness, or you may want to cheaply generate more randomness from some base seed of randomness. Pseudorandom functions (PRFs) are deterministic but unpredictable functions that take some value and output a pseudorandom value. We want that the distribution of PRF outputs to be indistinguishable from that of a truly random function.

PRFs are useful in many ways. For one, they allow you to securely generate an infinite amount of seemingly random values deterministically for use in other cryptographic protocols. In this assignment, we'll use a PRF to implement two-factor authentication by using PRF outputs as a way of proving that we know the value of given a shared seed \(s\). We can generate a short-lived login token by inputting this seed alongside the current time. The server can then validate that our values are correct by running the same function.

Putting it all together

The following diagrams explain how the protocols work together.

We'll first cover interactions between a user and a server. There are three main component: 1) key exchange 2) registration and 3) login. A user must either register or login with the server to retrieve their certificate. It is assumed that the server's public verification key, \(vk_s\), is known. In either case, they must first run a key exchange protocol illustrated below. Note that from this project on we won't be using Diffie-Hellman ratchet just for ease of implementation.

Architecture Key Exchange User to Server

At this point, all communication is encrypted. If a user wishes to register themselves, they run the following protocol:

Architecture Register

Likewise, the following diagram illustrates the login protocol.

Architecture Login

After registration or login, a user has obtained a certificate and are able to communicate with other users. We'll now cover interactions between a user and another user: the main complex part is key exchange. The protocol for key exchange is illustrated below:

Architecture Key Exchange User to User

After key exchange all communication can be encrypted just like signal (without the ratchet).

In short, we proceed in the following steps: login or registration, then communication.

Registration

Login

Communication


Assignment Specification

Please note: you may NOT change any of the function headers defined in the stencil. Doing so will break the autograder; if you don't understand a function header, please ask us what it means and we'll be happy to clarify.

Functionality

You will primarily need to edit src/drivers/crypto_driver.cxx, src/pkg/client.cxx, and src/pkg/server.cxx. The following is an overview of relevant files:

The following roadmap should help you organize concerns into a sequence:

Some tips:

DSA Signatures

Implement DSA Signatures by editing the following functions. Once you do so, your clients will be able to verify the integrity of messages sent between them.

Cryptographic functions:

Revamped Diffie-Hellman

Implement our updated Diffie-Hellman key exchange protocol by editing the following functions. Once you do so, your clients will be able to come to a shared secret without risk of a man-in-the-middle attack occuring.

Application functions:

Register/Login

Implement registration and login by editing the following functions. Once you do so, your clients will be able to register and verify new users.

Application functions:

Communication

Implement communication by editing the following functions. Note: You should be able to reuse a lot of code from Signal, especially in communicating between users.

Application functions:

Support Code

Read the support code header files before coding so you have a sense of what functionality we provide. This isn't a networking class, nor is it a software engineering class, so we try to abstract away as many of these details as we can so you can focus on the cryptography.

The following is an overview of the functionality that each support code file provides.

Messaging

The following example shows how to use our new encryption helpers alongside our networking library; we'll be using this pattern for the entire course, so it's good to get it down now.

// Declare the message struct that we want to send and populate its fields
Message msg_s;
msg_s.value = "foo";

// Encrypt and tag the message; this serializes the message, encrypts it using AES, tags it with an HMAC, and rolls the IV into one convenient vector.
std::vector<unsigned char> message_bytes =
    crypto_driver->encrypt_and_tag(AES_key, HMAC_key, &msg_s);

// Send it away!
network_driver->send(message_bytes);

// -----

// Receive the message
std::vector<unsigned char> raw_data = network_driver->read();
auto msg_data = crypto_driver->decrypt_and_verify(AES_key, HMAC_key, raw_data);

// Deserialize the data into a message.
Message msg_s;
msg_s.deserialize(msg_data);

Libraries: CryptoPP

You may find the following wiki pages useful during this assignment:


Getting Started

To get started, get your stencil repository here and clone it into the devenv/home folder. From here you can access the code from both your computer and from the Docker container.

To prevent crypto_driver.cxx solutions to earlier assignments being leaked in later assignments, we ask that you copy your code from crypto_driver functions implemented in the last assignment into this one. The functions you copy over now will not need to be copied over in the following assignment.

Running

To build the project, cd into the build folder and run cmake ... This will generate a set of Makefiles building the whole project. From here, you can run make to generate a binary you can run, and you can run make check to run any tests you write in the test folder.

To run the user binary, run ./auth_user <config file>. We have provided user config files for you to use; you shouldn't need to change them unless you would like to experiment with more users. Afterwards, you can either choose to login, register, connect, or listen; the former two deal with other server binaries, the latter two deal with other user binaries. They call the corresponding Handle functions in code.

To run the server binary, run ./auth_server <port> <config file>. We have provided server config files for you to use; you shouldn't need to change them. Afterwards, the server will start listening for connections and handle them in separate threads.

Testing

You may write tests in any of the test/**.cxx files in the Doctest format. We provide test/test_provided.cxx, feel free to write more tests in this file directly. If you want to add any new test files, make sure to add the file to the cmake variable, TESTFILES, on line 7 of test/CMakeLists.txt so that cmake can pick up on the new files. Examples have been included in the assignment stencil. To build and run the tests, run make check in the build directory. If you'd like to see if your code can interoperate with our code (which is what it will be tested against), feel free to download our binaries here - we try to keep these up to date, so if you're unsure about the functionality of our binaries, please ask us on Ed!


FAQ