13 Architecting a secure system
\[ \newcommand{\F}{\mathbb{F}} \newcommand{\G}{\mathbb{G}} \newcommand{\N}{\mathbb{N}} \newcommand{\R}{\mathbb{R}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\Zq}{\Z_q} \newcommand{\zo}{\{0,1\}} \newcommand{\bin}{\zo} \newcommand{\zon}{\{0,1\}^n} \newcommand{\A}{\mathcal{A}} \newcommand{\B}{\mathcal{B}} \newcommand{\calA}{\mathcal{A}} \newcommand{\calB}{\mathcal{B}} \newcommand{\calC}{\mathcal{C}} \newcommand{\calD}{\mathcal{D}} \newcommand{\calK}{\mathcal{K}} \newcommand{\calM}{\mathcal{M}} \newcommand{\calT}{\mathcal{T}} \newcommand{\calX}{\mathcal{X}} \newcommand{\calY}{\mathcal{Y}} \newcommand{\xor}{\oplus} \newcommand{\cross}{\times} \newcommand{\deq}{\mathrel{\mathop:}=} \newcommand{\abs}[1]{\left| #1 \right|} \newcommand{\set}[1]{\left\{ #1 \right\}} \renewcommand{\vec}[1]{\mathbf{#1}} \newcommand{\poly}{\operatorname{poly}} \newcommand{\negl}{\operatorname{negl}} \newcommand{\classP}{\mathsf{P}} \newcommand{\classNP}{\mathsf{NP}} \newcommand{\INDCPA}{\text{IND-CPA}} \newcommand{\Enc}{\mathsf{Enc}} \newcommand{\Dec}{\mathsf{Dec}} \newcommand{\Gen}{\mathsf{Gen}} \newcommand{\Derive}{\mathsf{Derive}} \newcommand{\Sign}{\mathsf{Sign}} \newcommand{\Ver}{\mathsf{Ver}} \newcommand{\MAC}{\mathsf{MAC}} \newcommand{\MACSign}{\mathsf{MAC.Sign}} \newcommand{\MACVerify}{\mathsf{MAC.Verify}} \newcommand{\Funs}{\mathsf{Funs}} \newcommand{\ToString}{\mathsf{ToString}} \newcommand{\sk}{\mathsf{sk}} \newcommand{\pk}{\mathsf{pk}} \newcommand{\vk}{\mathsf{vk}} \newcommand{\sig}{\sigma} \newcommand{\rgets}{\mathrel{\overset{\scriptscriptstyle\mathrm{R}}{\leftarrow}}} \newcommand{\getsr}{\rgets} \]
So far, we have been focusing on security for network communication. We have established many tools to achieve this, from message authentication codes to public-key encryption.
Ultimately, however, applications need to make use of these tools. And for our network security tools to provide meaningful security, the applications themselves must be reasonably secure.
In discussing platform and application security, there are two classes of problem that we want to defend against.
1. Mistakes of various types.
- Buggy systems: including hardware and software bugs
- User mistakes: phishing, misconfiguration
2. Malicious people or components.
- Malicious components: malware, supply-chain attacks
- Malicious users: what if the adversary gets the admin’s password?
- Attacker gets access to the system: insider attacks, the adversary guesses credentials,
In computer security, we tend to treat mistakes/bugs and malicious software/components in the same way. We do that because (1) it’s often difficult to specify what it means for a component to be “non-maliciously buggy” and (2) an attacker can often leverage what seems like a benign bug into full-fledged misbehavior.
Thus, a theme that will be present throughout this section is that we will consider mistakes to be malicious: if we are prepared to handle malicious components, we will similarly be prepared to handle our own buggy code. If we are prepared to limit damage of a malicious user with the admin password, we will also be limiting the damage that a mistake-making admin can cause.
This multitude of threats makes designing secure applications quite difficult. To make progress, we will seek to design systems that limit damage when things go wrong.
When we design for security, we have essentially three goals:
- Defend against known attacks.
- Defend against unknown attacks.
- Limiting damage. In the cryptography part of this course, systems are either security or insecure. In systems security, things are much more gray. Attacks we care about often are outside of our threat model—even when this happens, we’d like to somehow contain the damage.
13.1 Isolation
One of the most effective strategies to limit damage is to split a system into isolated components. If one of these components becomes compromised, it should not be able to compromise the other components. For example, if you run code in one virtual machine, it should not be able to tamper with data in another virtual machine.
When choosing what mechanism to use to isolate various components, we think a lot about the performance overhead of an isolation mechanism. The challenge of building a good isolation mechanism is ensuring strong isolation without slowing down the isolation components too much (or taking up too much extra memory).
These components will typically run on top of some host that enforces isolation. Importantly, this host must be correct! If there are bugs in the host, malicious code in a component may be able to exploit a bug to escape its isolation. The success of an isolation mechanism depends on the correctness and configuration of the host.
| Examples | Host |
|---|---|
| Docker Container | Operation System (e.g. Linux) |
| Browser tabs | Browser |
| Language-Level (JavaScript, Wasm) | Language Runtime |
| Process | Linux kernel |
| Virtual machines (VMs) | VM Monitor |
| Physical (“air gap”) | Physics |
In order for these isolated components to be useful, they will need to be able to talk to each other in some form. For example, a client component must be able to make requests to a database component, but we would like to limit the power of the client to do damage. For this, we would like to achieve controlled sharing.
Controlled Sharing
For an isolation mechanism to be useful, it additionally needs to have some way to interact with other isolated components. For example, some JavaScript code isolated in a browser tab still needs some means by which to make requests over the network.
When a host decide whether to allow a request from a particular component, it typically needs to do three things with each request:
Since all three of these actions start with the letters “Au,” we sometimes call this the gold standard for controlled sharing.
- Authenticate: Associate the request with some principal. A principal could be a user name, an “origin” in the web context (e.g.,
google.com), a program, or some other entity in the system. - Authorize: Decide whether that principal is allowed to make the request.
- Audit: Keep track of requests that each principal makes. Auditing is about limiting damage: often a host will mistakenly allow requests it shouldn’t; audit logs make it easier to discover such mistakes and to clean up afterwards.
It is crucial that an isolation mechanism perform these three checks on every single request—a single hold in the isolation boundary is often enough to completely break any benefits isolation that would have provided.
13.2 Authentication
Since we already had an entire module on authentication using signatures, MACs, passwords, and so on, we will not discuss authentication further here.
13.4 Auditing
We have relatively little to say about this. The most important thing to remember about auditing is that a system should store the audit logs in a container that is separate from the container holding application logic. That is important because if the attacker compromises the application, it should be difficult for the attacker to compromise the logs as well.
13.5 Delegation and Chained Requests
Users often interact with systems indirectly. For example, when accessing Gmail, a user’s browser first sends a request to the Gmail server asking for new messages. The Gmail server then sends a request to the database to fetch the message data.
For the first request, it is fairly clear that the principal should be Alice: the request is coming from Alice’s browser, and therefore should have been initiated by Alice directly. Alice will send some credential to the server, and the server can use this credential to verify that it is really Alice on the other end. For the second request, however, it is not as clear who the request should be from.
One option is to have the request come from Alice. This protects against compromise of the Gmail server—the adversary cannot see all user data. Systems like SSH and AFS follow a strategy like this. Another option is for the principal of this second request to be the Gmail server itself. This helps with isolation among services access the same database: if the Google calendar code is buggy and gets compromised, the first plan would allow an adversary to view Alice’s gmail data even is the gmail service was perfectly secure. However, it does not protect other users from a buggy Gmail service.
Compound Principal: “B for A.” To achieve something stronger, we can create a new type of compound principal that combines a service or device with a user. For example, this server-to-database request could carry a principal of “Gmail Server for Alice”. This provides protection against both gmail server compromise and against compromise of other services.
However, it is not as clear how to actually implement this. One option is to continue to have Alice send her credential to the server directly. However, then the server can totally impersonate Alice and we gain little protection. What we would really like is for Alice to give permission to the Gmail server to fetch her emails, but not to do anything else. This is called delegation.
Delegation with cryptography. In interacting with the Gmail server \(B\), we may like for Alice (\(A\)) to give \(B\) permission to authenticate as “\(B\) for \(A\)” and to do so for only 60 seconds into the future. To achieve this, \(A\) can sign a message that outlines the permission it would like to give to \(B\). This signature becomes the proof of authorization. As an example: \[ \Sign(\sk-A, \text{“A delegates to B”}, \text{start}=\text{now}, \text{end}=\text{now}+60) \]
Google indeed uses a strategy like this. They have a global DoS-resilient HTTP front-end that performs initial authentication. This frontend is then responsible for generating these scoped delegation signatures for each operation that the user would like to do and sending them along to the individual services. These signatures are then used for all following operations.
Capabilities. We may want more fine-grained access control. For example, on Android, the Gmail app may like to delegate permission to a PDF viewer to view an attachment. However, if all the attachments are stored in some common database, we would like to avoid giving the PDF viewer access to view everything in the database. To achieve this, Android (and systems more generally) use a plan called cababilities.
A similar strategy can be seen, for example, in cloud file sharing: when you share a file in google drive, it generates a long random link that allows anyone with access to that link to view that file (and no others). This link itself becomes a capability—it allows anyone that posesses it to perform some related action.