A Final Project for CS165 Computer Security designed by Jacob Tan and Richard Duong
Link to the Github Repository here
Implement a secure proxy application using TLS protocol to provide simple authentication and secure file transmission. With this, we can demonstrate how a large scale system with a server caching objects inside of proxies can interact with a client and provide objects upon request in a secure and optimal manner.
The full assignment specifications
- Overview
- Table of Contents
- How to use
- Phase Design
a) Initialization
b) Standard Application Process
c) Nonstandard Application Process with False Positives - Component Design
a) Packet
b) Rendezvous Hashing
c) Bloom Filter - System Design
a) Client
b) Server
c) Proxy
d) Blacklist - Distribution of Work
a) Jacob's Contributions
b) Richard's Contributions - Final Words
- References
This repository contains the starter code for the CS165 project. The directory structure is as follows:
certificates/ // Contains CA and server certificates.
server_proxy/ // Certificates for server(server) and proxy(client)
client_proxy/ // Certificates for proxy(server) and client(client)
scripts/ // Helper scripts.
env.sh // Script helps link libraries (run this before screen)
setup.sh // Run this once to setup the appropriate libraries
reset.sh // Reset the environment
src/ // Source code for the TLSCache Application
components/ // Components used for HRW, Bloom Filter, and Definitions
system/ // Server/Proxy/Client Code and Architecture
tests/ // Unit and Integration Tests
cmake/ // CMake find script.
extern/ // Required third party tools and libraries- LibreSSL & CMake.
licenses/ // Open source licenses for code used.
data/ // Input files used by the client, proxy, and servers
docs/ // Pictures and documents used in the overview design of the site
- Download and extract the code.
- Run the following commands:
Prepare the environment
$ cd TLSCache
$ source scripts/setup.sh
Generate the server/proxy certificates
$ cd TLSCache
$ cd certificates/server_proxy/
$ make
Generate client/proxy certificates
$ cd TLSCache
$ cd certificates/client_proxy/
$ make
Build the TLS Application
$ cd TLSCache/build/
$ make
$ cd TLSCache
$ source scripts/env.sh
Launch another terminal, screen, or tmux
Run the server:
$ ./build/src/server
Launch another 5 terminals, screens, or tmux
Run the proxies on each shell separately:
$ ./build/src/proxy 0
$ ./build/src/proxy 1
$ ./build/src/proxy 2
$ ./build/src/proxy 3
$ ./build/src/proxy 4
Launch another terminal, screen, or tmux
Run the client
$ ./build/src/client 1
Edit files in the data directory
$ cd TLSCache/data/
$ vim client0_requests.min
The default request files are meant to help you with different test cases
- Test your own inputs on file
client0_requests.minwhich can be ran with$ ./build/src/client 0. - To see an example of all found object, not found object, and blacklisted object run
client1_requests.minwith$ ./build/src/client 1 - To see an example of duplications and objects being added and accessed on the cache, run
client2_requests.minwith$ ./build/src/client 2
setup.shshould be run exactly once after you have downloaded code, and never again. It extracts and builds the dependencies in extern/, and builds and links the code in src/ with LibreSSL.reset.shreverts the directory to its initial state. It does not touchsrc/orcertificates/. Runmake cleanincertificates/to delete the generated certificates.env.shshould be from the project directoryTLSCacheeach time you start up a new terminal. If you're using screen, run this script first before starting your screen so all sub-screens will have the libraries linked.
1) Initialization
This phase prepares the server and proxies with the appropriate Blacklists & Bloom Filters
2) Standard Application Process
This phase evaluates our clients' requests without a match on the Bloom Filter
3) Nonstandard Application Process with False Positives
This phase evaluates our clients' requests with a match on the Bloom Filter
Before being able to run the TLS application, we need to be able to set up the system so that we instantiate the Bloom Filters and Blacklists on each proxy. The way the Server, Proxy, and Client interacts and sets up their own systems is important during this time.
- Server Initialization
- Proxy Initialization
- Client Initialization
Has access to:
- Entire object file
- Blacklist object file
- Proxy name/port list
- Appropriate "Server TLS" Root, Key, and Cert
Steps:
- Read and store requestable objects from file
- Read and store blacklisted objects from file
- Distribute blacklisted objects into files for each proxy to read from later
- Configure TLS contexts and setup listening socket

Has access to:
- Relevant blacklist file
- Appropriate "Client TLS" Root
- Appropriate "Server TLS" Root, Key, and Cert
Steps:
- Read blacklist for the specified proxy
- Create a local bloom filter with blacklisted objects as elements
- Prepare a cache for objects to be stored on
- Configure TLS contexts and setup listening/connection sockets
- Prepare a file descriptor pipe for updating the cache

Has access to:
- Object Requests file
Steps:
- Read in all object requests file from the corresponding client file
- Configure TLS contexts and setup connection socket

This phase is the standard application process. In the standard application process, we are anticipating an object request that does not produce a false positive in the Bloom Filter. If the client makes a request to the application, they're expected to encounter one of these four scenarios.
- Scenario 1: Client requests object on proxy
- Scenario 2: Client requests object on server
- Scenario 3: Client requests nonexistent object
- Scenario 4: Client requests blacklisted object
- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter for blacklisted objects, finds no match
- Proxy checks local cache for object, finds object
- Proxy returns object to Client

- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter for blacklisted objects, finds no match
- Proxy checks local cache for object, finds no match
- Proxy requests object from server
- Server checks locally for object, finds object
- Server returns object to Proxy
- Proxy returns object to Client

- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter of blacklisted objects, finds no match
- Proxy checks local cache for object, finds no match
- Proxy requests object from server
- Server checks locally for object, finds no match
- Server returns [NON] to Proxy
- Proxy returns [NON] to Client

- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter of blacklisted objects, finds match
- Proxy does an additional validation check to see if object is on the local set of blacklisted objects, finds match
- Proxy returns [DEN] to Client

This phase is the nonstandard application process. In the nonstandard application process, we are anticipating an object request that produces a false positive on the Bloom Filter. With a false positive, that means the situation where the client requests a blacklisted object is excluded. If the client makes a request to the application, they're expected to encounter one of these three scenarios.
- Scenario 1: Client requests object on proxy
- Scenario 2: Client requests object on server
- Scenario 3: Client requests nonexistent object
- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter for blacklisted objects, finds match
- Proxy does an additional validation check to see if object is on the local proxy set of blacklisted objects, finds no match (false positive)
- Proxy checks local cache for object, finds object
- Proxy returns object to Client

- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter for blacklisted objects, finds match
- Proxy does an additional validation check to see if object is on the local proxy set of blacklisted objects, finds no match (false positive)
- Proxy checks local cache for object, finds object
- Proxy requests object from server
- Server checks locally for object, finds object
- Server returns object to Proxy
- Proxy returns object to Client

- Client runs HRW on object to determine which proxy holds the cached object
- Client requests object from result proxy
- Proxy checks if object is on Bloom Filter for blacklisted objects, finds match
- Proxy does an additional validation check to see if object is on the local proxy set of blacklisted objects, finds no match (false positive)
- Proxy checks local cache for object, finds no match
- Proxy requests object from server
- Server checks locally for object, finds no match
- Server returns [NON] to Proxy
- Proxy returns [NON] to Client

- Packet Design
- Rendezvous Hashing Design
- Bloom Filter Design
The packet design when using TLS is much simpler than if one were to incorporate a checksum and manually encrypt, where you can just send . However, since proxies and clients will be receiving different types of packets, we need some way of specifying the type of specification. Therefore we've reduced it down to 4 prefixes on the packet that will specify what to anticipate with packet requests coming in and packets sent out. The 4 packet prefixes are: GET, PUT, NON, DEN.
- The client prefixes the object request with GET during Standard Application Process and Nonstandard Application Process when sending a request to the proxy
- The proxy prefixes the object request with GET during Standard Application Process and Nonstandard Application Process when sending a request to the server
- The proxy prefixes the object data with PUT during Standard Application Process and Nonstandard Application Process when returning the object to the client
- The server prefixes the object data with PUT during Standard Application Process and Nonstandard Application Process when returning the object to the proxy
- The server returns a NON to the proxy during Standard Client requests nonexistent object and Nonstandard Client requests nonexistent object if the object requested does not exist on the server
- The proxy returns a NON to the client during Standard Client requests nonexistent object and Nonstandard Client requests nonexistent object if the object requested does not exist on the proxy or the server
- The proxy returns a DEN to the client during Standard Client requests blacklisted object if the object requested was a blacklisted object
Rendezvous (Highest Random Weight) Hash is an algorithm that is a solution to the distributed hash table problem. HRW is a general form of Consistent Hashing and is far less complex and practical in application. We will be using HRW to distribute objects across all proxies for the specifications of this assignment.
Given an object to distribute and a list of proxies, we can generate a set of strings by adding the name of the object to each proxy.
Here's an example
Object = "object1". Proxy 1 = "proxy1", Proxy 2 = "proxy2", Proxy 3 = "proxy3"
Then the strings would be
String1 = "object1proxy1", String2 = "object1proxy2", String3 = "object1proxy3"
We hash each of the strings and order them based on greatest to least
OrderedStrings = String2, String1, String3
And now we can determine an order for how we want to distribute the object. In the list, String2 has the highest random weight and correlates to proxy2. Therefore we would distribute the object to Proxy 2. However, if Proxy 2 is down, we would distribute it to the next correlated proxy on the list, which would be proxy1. And so on and so forth. As a result of this design, Rendezvous Hashing is very good when it comes to Load Balancing efficiently as well as minimizing disruption in case a proxy goes down, since all objects of the downed proxy will just be redistributed to the next largest hash.<br
Let O denote the object name, P denote the set of proxy names for object distribution, S be a string, and h(S) be a hash function.
We can say that P = {P1, P2, ... , Pn - 1, Pn} where any Pi ∈ P s.t. 0 < i ≤ n
Let L be the set of all strings that can be made by concatenating O to each Pi ∀ i
Then L = {(O || P1), (O || P2), ... , (O || Pn - 1), (O || Pn)} or
L = {L1, L2, ... , Ln - 1, Ln} and each term can be denoted as Li ∀ i
Then, we have the set of values V represent *h(Li) ∀ i
If we order set V by greatest to least, we pick the largest and available hash value as the proxy to distribute the object to.
The Rendezvous Hash is implemented inside of /src/components/hrw.h and has the function signature of HRW(string objname) and 5 proxy names hardcoded into the implementation. We used MurmurHash3 as the hash function of choice with a seed value of 1. The resulting value from calling the function would be the index of the proxy to distribute the object to.
Bloom Filters are a probabilitistic data structure that can test for existence of an element in a set. This is improved compared to testing for existence using a hash table that requires that we store all elements locally. With a bloom filter, we store a fraction of the memory while functioning at the same efficiency as a standard hash table.
Let's say we have objects that we want to check for existence. We produce an array of bits flagged 0 and several hash functions that will be used on the elements.
Here's an example
objects[] = "item1", "item2", "item3", "item4", "item5"
bloom_filter = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
bloom_filter_size = 15
hashes h1, h2, h3
When inserting objects, we fill in the indexes based on the value of the hashes.
Putting h1("item1") we may get a value 2
Putting h2("item1") we may get a value 5
Putting h3("item1") we may get a value 12
The resulting bloom filter will look like this:
bloom_filter = 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0
Providing the indexes to fill, we would get the following bloom filter:
indexes of hash(item1): 2, 5, 12
indexes of hash(item2): 3, 7, 14
indexes of hash(item3): 1, 3, 7
indexes of hash(item4): 2, 3, 9
indexes of hash(item5): 0, 3, 5
bloom_filter = 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1
When checking for existence of an object, we just check the indexes of the hashes to see if all values in the indexes of the bloom filter are filled out
For example, checking for item3, we would check the indexes of the hashes:
h1("item3") = 1
h2("item3") = 3
h3("item3") = 7
bloom_filter = 1, (1), 1, (1), 0, 1, 0, (1), 0, 1, 0, 0, 1, 0, 1
We can see that "item3" was inserted into the bloom filter by the hashes, and checking for existence shows that it does exist.
When checking for existence with an item that doesn't exist, we will find at least 1 index from the hash that has a 0.
For example, checking item6, we would check the indexes of the hashes:
h1("item6") = 1
h2("item6") = 6
h3("item6") = 8
bloom_filter = 1, (1), 1, 1, 0, 1, (0), 1, (0), 1, 0, 0, 1, 0, 1
We can see that "item6" has a 0 at indexes 6 and 8, therefore it does not exist in the bloom filter.
The Bloom Filter is implemented inside of '/src/components/bf.h' and has 2 class methods. One is to initialize the bloom filter, and the other is to check for existence. For our hash functions, we used MurmurHash3 with seed values ranging from 1-5 as our different hash functions, and did bitwise operations
- Created Rendezvous hashing design
- Created Bloom Filter design
- Designed the README
- Created Server/Proxy/Client Architecture
- Created the Packet design