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.
At this point, all communication is encrypted. If a user wishes to register themselves, they run the following protocol:
Likewise, the following diagram illustrates the login protocol.
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:
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
- On setup, the server has access to a DSA keypair \(vk_s, sk_s\), and the user has access to \(vk_s\).
- On registration, the user initiates a connection with the server and sends their DH public value \(g^a\).
- The server will respond with both DH public values \((g^a, g^b)\) and a signature on both values, \(\sigma_s = Sign(g^a, g^b, sk_s)\).
- All communication past this point takes place using secret-key authenticated encryption; note that we do not implement the ratchet in this or future assignments.
- Next, the user will send their \(id_i\) to the server.
- The server will generate a random 128-bit \(salt_i\) for this user and send it to the user.
- The user will use the salt to generate \(h_i := H(password \ || \ salt_i)\) and send \(h_i\) to the server.
- The server then generates a random 8-bit \(pepper_i\) and generates \(h_i' = H(h_i \ ||\ pepper_i)\).
- The server then generates a PRG seed \(seed_i\) and sends it to the user for use in 2FA.
- The user generates a 2FA response \(r := PRG_{seed_i}(now)\), where \(now\) is rounded down to the nearest second.
- The server verifies that this response is valid by checking the past 60 seconds of PRG responses.
- The user generates a DSA keypair \(vk_i, sk_i\) and sends \(vk_i\) to the server for signing.
- The server generates a certificate for this user \(\sigma_i\) over the fields \((vk_i, \ id_i)\), then sends it to the user.
- The server stores \((id_i, h_i', salt_i, seed_i)\) in the database.
Login
- On setup, the server has access to a DSA keypair \(vk_s, sk_s\), and the user has access to \(vk_s\).
- On login, the user initiates a connection with the server and sends their DH public value \(g^a\).
- The server will respond with both DH public values \((g^a, g^b)\) and a signature on both values, \(\sigma_s = Sign(g^a, g^b, sk_s)\).
- All communication past this point takes place using secret-key authenticated encryption; note that we do not implement the ratchet in this or future assignments.
- Next, the user will send their \(id\) to the server.
- The server will retrieve \((id_i, h_i', salt_i, seed_i)\) from the database and sends \(salt_i\) to the user.
- The user will use the salt to generate \(h_i := H(password || salt_i)\) and send \(h_i\) to the server.
- The server then tries all possible 8-bit \(pepper_i\) and generates \(\hat{h}_i' = H(h_i || pepper_i)\) until one matches \(h'_i\).
- The user sends a 2FA response \(r := PRG_{seed_i}(now)\), where \(now\) is rounded down to the nearest second.
- The server verifies that this response is valid by checking the past 60 seconds of PRG responses.
- The user generates a DSA keypair \(vk_i, sk_i\) and sends \(vk_i\) to the server for signing.
- The server generates a certificate for this user \(\sigma_i\) over the fields \((vk_i, \ id_i)\), then sends it to the user.
Communication
- On setup, both users should have registered and obtained a certficate, and both users have access to \(vk_s\).
- On startup, both users run Diffie-Hellman, signing every message with the certificate they received from the server.
- Upon receipt of a DH public value from the other party, each user verifies that the server's signature on the certificate is valid, and that the user's signature on the public vaue is valid.
- Following these steps, the users have come to a shared secret and use symmetric key encryption using these values.
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:
src/cmd/user.cxx
is the main entrypoint for theauth_user
binary. It calls theUser
class.src/cmd/server.cxx
is the main entrypoint for theauth_server
binary. It calls theServer
class.src/drivers/crypto_driver.cxx
contains all of the cryptographic protocols we use in this assignment.src/pkg/user.cxx
Implements theUser
class.src/pkg/server.cxx
Implements theServer
class.
The following roadmap should help you organize concerns into a sequence:
- DSA Signatures: Implement DSA key generation, signing, and verification.
- Revamped Diffie-Hellman: Implement our modified DH key exchange protocol in registration and login.
- Register/Login: Implement register functionality to add new users to the system and login functionality to verify old users.
- Communication: Implement communication functionality to allow users to talk to each other
Some tips:
- The
encrypt_and_tag
anddecrypt_and_verify
functions are wrapper functions that should cut down on the amount of repetitive code in your implementation. **Use these functions to save yourself a lot of debugging time - do not callAES_*
orHMAC_*
functions raw! - You might notice that a lot of messages don't have
signature
fields; we use a generic wrapperDSASignedWrapper
to handle signatures. Generate signatures over the serialization of the unsigned message. - Remember to call
network_driver->disconnect()
at the end of handler functions. - You don't need to replicate our CLI functionality; however, using it as a debugging tool is helpful.
- If a protocol fails for any reason (e.g. invalid signature, incorrect keys, decryption failed, etc.), throw an
std::runtime_error
. - Use our constants from
include-shared/constants.hpp
where applicable. In particular from now on, Diffie-Hellman parameters are now hard-coded here instead of exchanged. - Use the
chvec2str
andstr2chvec
functions to convert to and from strings and bytevecs, andbyteblock_to_string
andstring_to_byteblock
to convert to and from strings and byteblocks. - Use our function
crypto_driver->nowish()
to get the time rounded down to the nearest second. - Use our function
crypto_driver->hash()
to hash values. - If you're debugging and want a fresh database, you are free to simply delete the database file at
./keys/server.db
. Alternatively, the database driver has areset_tables
function which will do just this. - The server functions should not return runtime errors if the user has failed somehow; we want the server to stay alive after all! So, the server should simply return from the function and terminate the connection.
- Remember to use the functions in
keyloaders.hpp
to save any keys, seeds, or certificates you may have obtained.
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:
CryptoDriver::DSA_generate()
CryptoDriver::DSA_sign(...)
CryptoDriver::DSA_verify(...)
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:
ServerClient::HandleConnection(...)
ServerClient::HandleKeyExchange(...)
UserClient::HandleServerKeyExchange()
UserClient::HandleUserKeyExchange()
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:
ServerClient::HandleConnection(...)
ServerClient::HandleLogin(...)
ServerClient::HandleRegister(...)
UserClient::HandleServerKeyExchange()
DoLoginOrRegister(...)
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:
UserClient::HandleUserKeyExchange()
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.
src/drivers/storage_driver
implements a class to manage database connections and operations; use this instead of interacting with the database directly. We usesqlite3
under the hood, so you can runsqlite3 <dbpath>
to debug the database directory if necessary.src/drivers/repl_driver
implements a convenience class to run different REPL commands.src-shared/config.cxx
contains loaders for our configuration files.src-shared/keyloaders.cxx
contains loader for our keys.- Everything else from prior assignments is unchanged.
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
- None yet!