mirror of
https://github.com/asterinas/asterinas.git
synced 2025-06-08 21:06:48 +00:00
Add chapter Everything is a Capability
This commit is contained in:
parent
82da7b3e78
commit
e473b43d38
@ -71,7 +71,8 @@ Here is an overview of the architecture of KxOS.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
**1. Security by design.** Security is our top priority in the design of KxOS. As such, we adopt the widely acknowledged security best practice of [least privilege principle](https://en.wikipedia.org/wiki/Principle_of_least_privilege) and enforce it in a fashion that leverages the full strengths of Rust. To do so, we partition KxOS into two halves: a _privileged_ OS core and _unprivileged_ OS components. As a result, we can write the OS components almost entirely in _safe_ Rust, while taking extra cautions with the _unsafe_ Rust code in the OS core. Furthermore, we propose the idea of _everything-is-a-capability_, which elevates the status of [capabilities](https://en.wikipedia.org/wiki/Capability-based_security) to the level of a ubiquitous security primitive used throughout the OS. We make novel use of Rust's advanced features (e.g., [type-level programming](https://willcrichton.net/notes/type-level-programming/)) to make capabilities more accessible and efficient. The net result is improved security and uncompromised performance.
|
**1. Security by design.** Security is our top priority in the design of KxOS. As such, we adopt the widely acknowledged security best practice of [least privilege principle](https://en.wikipedia.org/wiki/Principle_of_least_privilege) and enforce it in a fashion that leverages the full strengths of Rust. To do so, we partition KxOS into two halves: a _privileged_ OS core and _unprivileged_ OS components. All OS components are written entirely in _safe_ Rust and only the privileged OS core
|
||||||
|
is allowed to have _unsafe_ Rust code. Furthermore, we propose the idea of _everything-is-a-capability_, which elevates the status of [capabilities](https://en.wikipedia.org/wiki/Capability-based_security) to the level of a ubiquitous security primitive used throughout the OS. We make novel use of Rust's advanced features (e.g., [type-level programming](https://willcrichton.net/notes/type-level-programming/)) to make capabilities more accessible and efficient. The net result is improved security and uncompromised performance.
|
||||||
|
|
||||||
**2. Trustworthy OS-level virtualization.** OS-level virtualization mechanisms (like Linux's cgroups and namespaces) enable containers, a more lightweight and arguably more popular alternative to virtual machines (VMs). But there is one problem with containers: they are not as secure as VMs (see [StackExchange](https://security.stackexchange.com/questions/169642/what-makes-docker-more-secure-than-vms-or-bare-metal), [LWN](https://lwn.net/Articles/796700/), and [AWS](https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/security-tasks-containers.html)). There is a real risk that malicious containers may exploit privilege escalation bugs in the OS kernel to attack the host. [A study](https://dl.acm.org/doi/10.1145/3274694.3274720) found that 11 out of 88 kernel exploits are effective in breaking the container sandbox. The seemingly inherent insecurity of OS kernels leads to a new breed of container implementations (e.g., [Kata](https://katacontainers.io/) and [gVisor](https://gvisor.dev/)) that are based on VMs, instead of kernels, for isolation and sandboxing. We argue that this unfortunate retreat from OS-level virtualization to VM-based one is unwarranted---if the OS kernels are secure enough. And this is exactly what we plan to achieve with KxOS. We aim to provide a trustworthy OS-level virtualization mechanism on KxOS.
|
**2. Trustworthy OS-level virtualization.** OS-level virtualization mechanisms (like Linux's cgroups and namespaces) enable containers, a more lightweight and arguably more popular alternative to virtual machines (VMs). But there is one problem with containers: they are not as secure as VMs (see [StackExchange](https://security.stackexchange.com/questions/169642/what-makes-docker-more-secure-than-vms-or-bare-metal), [LWN](https://lwn.net/Articles/796700/), and [AWS](https://docs.aws.amazon.com/AmazonECS/latest/bestpracticesguide/security-tasks-containers.html)). There is a real risk that malicious containers may exploit privilege escalation bugs in the OS kernel to attack the host. [A study](https://dl.acm.org/doi/10.1145/3274694.3274720) found that 11 out of 88 kernel exploits are effective in breaking the container sandbox. The seemingly inherent insecurity of OS kernels leads to a new breed of container implementations (e.g., [Kata](https://katacontainers.io/) and [gVisor](https://gvisor.dev/)) that are based on VMs, instead of kernels, for isolation and sandboxing. We argue that this unfortunate retreat from OS-level virtualization to VM-based one is unwarranted---if the OS kernels are secure enough. And this is exactly what we plan to achieve with KxOS. We aim to provide a trustworthy OS-level virtualization mechanism on KxOS.
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
- [Case Study 1: Syscall Workflow](privilege_separation/syscall_workflow.md)
|
- [Case Study 1: Syscall Workflow](privilege_separation/syscall_workflow.md)
|
||||||
- [Case Study 2: Drivers for Virtio Devices on PCI](privilege_separation/pci_virtio_drivers.md)
|
- [Case Study 2: Drivers for Virtio Devices on PCI](privilege_separation/pci_virtio_drivers.md)
|
||||||
- [Everything as a Capability](capabilities/README.md)
|
- [Everything as a Capability](capabilities/README.md)
|
||||||
- [What are Capabilities?](capabilities/what_are_capabilities.md)
|
|
||||||
- [Type-Level Programming in Rust](capabilities/type_level_programming.md)
|
- [Type-Level Programming in Rust](capabilities/type_level_programming.md)
|
||||||
- [CapComp: Zero-Cost Capabilities and Components](capabilities/capcomp.md)
|
- [CapComp: Zero-Cost Capabilities and Components](capabilities/capcomp.md)
|
||||||
- [Trustworthy Containers]()
|
- [Trustworthy Containers]()
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
# Everything is a Capability
|
||||||
|
|
||||||
|
> A capability is a token, ticket, or key that gives the possessor permission to access an entity or object in a computer system. ---Dennis and Van Horn of MIT, 1966
|
||||||
|
|
||||||
|
Capabilities are a classic approach to security and access control in OSes,
|
||||||
|
especially microkernels. For example, capabilities are known as handles in [Zircon](https://fuchsia.dev/fuchsia-src/concepts/kernel). From the users' perspective, a handle is just an ID. But inside the kernel, a handle is a C++ object that contains three logical fields:
|
||||||
|
|
||||||
|
* A reference to a kernel object;
|
||||||
|
* The rights to the kernel object;
|
||||||
|
* The process it is bound to (or if it's bound to the kernel).
|
||||||
|
|
||||||
|
Capabilities have a few nice properties in terms of security.
|
||||||
|
|
||||||
|
* Non-forgeability. New capabilities can only be constructed or derived from existing, valid capabilities. Capabilities cannot be created out of thin air.
|
||||||
|
* Monotonicity. A new capability cannot have more permissions than the original capability from which the new one is derived.
|
||||||
|
* Transferability. A capability may be transferred to or borrowed by another user or security domain to grant access to the resource behind the capability.
|
||||||
|
|
||||||
|
Existing capability-based systems, e.g., [seL4](https://docs.sel4.systems/Tutorials/capabilities.html), [Zircon](https://fuchsia.dev/fuchsia-src/concepts/kernel/handles), and [WASI](https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-capabilities.md), use
|
||||||
|
capabilities in a limited fashion, mostly as a means to limit the access from
|
||||||
|
external users (e.g., via syscall), rather than a mechanism to enforce advanced
|
||||||
|
security policies internally (e.g., module-level isolation).
|
||||||
|
|
||||||
|
So we ask this question: is it possible to use capabilities as a _ubitiquous_ security primitive throughout KxOS to enhance the security and robustness of the
|
||||||
|
OS? Specifically, we propose a new principle called "_everything is a capability_".
|
||||||
|
Here, "everything" refers to any type of OS resource, internal or external alike.
|
||||||
|
In traditional OSes, treating everything as a capability is unrewarding
|
||||||
|
because (1) capabilities themselves are unreliable due to memory safety problems
|
||||||
|
, and (2) capabilities are no free lunch as they incur memory and CPU overheads. But these arguments may no longer stand in a well-designed Rust OS like KxOS.
|
||||||
|
Because the odds of memory safety bugs are minimized and
|
||||||
|
advanced Rust features like type-level programming allow us to implement
|
||||||
|
capabilities as a zero-cost abstraction.
|
||||||
|
|
||||||
|
In the rest of this chapter, we first introduce the advanced Rust technique
|
||||||
|
of [type-level programming (TLP)](type_level_programming.md) and then describe how we leverage TLP as well as
|
||||||
|
other Rust features to [implement zero-cost capabilities](capcomp.md).
|
@ -0,0 +1,403 @@
|
|||||||
|
|
||||||
|
# CapComp: Towards a Zero-Cost Capability-Based Component System for Rust
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**CapComp** is a zero-cost capability-based component system for Rust. With CapComp, Rust developers can now build _component-based systems_ that follow the _principle of least privilege_, which is effective in improving system security and stability. CapComp can also make formal verification of Rust systems more feasible since it reduces the Trusted Computing Base (TCB) for a specific set of properties. We believe CapComp is useful for building complex systems of high security or stability requirements, e.g., operating systems.
|
||||||
|
|
||||||
|
CapComp features a novel _zero-cost access control_ mechanism. Unlike the traditional approach to capability-based access control (e.g., seL4), CapComp enforces access control at compile time: no runtime check for access rights, no hardware isolation overheads, or extra runtime overheads of Inter-Procedural Calls (IPC). To achieve this, CapComp has realized the full potential of the Rust language by making a joint and clever use of new type primitives, type-level programming, procedural macros, and program analysis techniques.
|
||||||
|
|
||||||
|
CapComp is designed to be _pragmatic_. To enjoy the benefits of CapComp, developers need to design new systems with CapComp in mind or refactor existing systems accordingly. But these efforts are minimized thanks to the intuitive and user-friendly APIs provided by CapComp. Furthermore, CapComp supports incremental adoption: it is totally ok to protect only selected parts of a system with CapComp. Lastly, CapComp does not require changes to the Rust language or compiler.
|
||||||
|
|
||||||
|
## Motivating examples
|
||||||
|
|
||||||
|
To illustrate the motivation of CapComp, let's first examine two examples. While both examples originate in the context of OS, the motivations behind them make sense in other contexts.
|
||||||
|
|
||||||
|
### Example 1: Capabilities in a microkernel-based OS
|
||||||
|
|
||||||
|
A capability is a communicable, unforgeable token of authority. Conceptually, a capability is a reference to an object along with an associated set of access rights. Capabilities are chosen to be the core primitive that underpins the security of many systems, most notably, several microkernels like seL4, zircon, and Capsicum.
|
||||||
|
|
||||||
|
In Rust, the most straightforward way to represent a capability is like this (as is done in zCore, a Rust rewrite of zircon). In other languages like C and C++, the representation is similar.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A handle = a capability.
|
||||||
|
pub struct Handle<O> {
|
||||||
|
/// The kernel object referred to by the handle.
|
||||||
|
pub object: Arc<O>,
|
||||||
|
/// The access rights associated with the handle.
|
||||||
|
pub rights: Rights,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The problem with this approach is three-fold. First, a capability (i.e., `Handle`) consumes more memory than a raw pointer. Second, enforcing access control according to the associated rights requires runtime checking. Third, due to the overhead of runtime checking, users of `Handle` have the incentive to skip the runtime checking whenever possible and use the object directly. Thus, we cannot rule out the odds that some buggy code fails to enforce the access control, leading to security loopholes.
|
||||||
|
|
||||||
|
So, here is our question: **is it possible to implement capabilities in Rust without runtime overheads and (potential) security loopholes?**
|
||||||
|
|
||||||
|
### Example 2: Key retention in a monolithic OS kernel
|
||||||
|
|
||||||
|
Let's consider a key retention subsystem named `keyring` in a monolithic kernel (such a subsystem exists in the Linux kernel). The subsystem serves as a secure and centralized place to manage secrets used in the OS kernel. The users of the `keyring` subsystem include both user programs and kernel services.
|
||||||
|
|
||||||
|
Here is an (overly-)simplified version of `keyring` in Rust.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A key is some secret data.
|
||||||
|
pub struct Key(Vec<u8>);
|
||||||
|
|
||||||
|
impl Key {
|
||||||
|
pub fn new(bytes: &[u8]) -> Self {
|
||||||
|
Self(Vec::from(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> &[u8] {
|
||||||
|
self.0.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, bytes: &[u8]) -> &[u8] {
|
||||||
|
self.0.clear();
|
||||||
|
self.0.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a key with a name.
|
||||||
|
pub fn insert_key(name: &str, key: Key) {
|
||||||
|
KEYS.lock().unwrap().insert(name.to_string(), Arc::new(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a key by a name.
|
||||||
|
pub fn get_key(name: &str) -> Option<Arc<Key>> {
|
||||||
|
KEYS.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&name.to_string())
|
||||||
|
.map(|key| key.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref KEYS: Mutex<HashMap<String, Arc<Key>> = {
|
||||||
|
Mutex::new(HashMap::new())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While any components (or modules/subsystems/drivers) in a monolithic kernel are part of the TCB and thus trusted, it is still highly desirable to constraint the access of keys to only the components that have some legitimate reasons to use them. This minimizes the odds of misusing or leaking the highly sensitive keys maintained by `keyring`.
|
||||||
|
|
||||||
|
Our question is that **how to enforce inter-component access control (as demanded here by `keyring`) in a complex system like a monolithic kernel and without runtime overheads?**
|
||||||
|
|
||||||
|
## Core concepts
|
||||||
|
|
||||||
|
Before demonstrating how we approach the above problems in CapComp, we need to first introduce some core concepts in CapComp.
|
||||||
|
|
||||||
|
In CapComp, **components** are Rust crates that are governed by our compile-time capability-based access control mechanism. Crates that maintain mutable states or have global impacts on a system are good candidates to be protected as components.
|
||||||
|
Unlike a regular crate, a component can decide how many functionalities to expose on a per-component or per-object basis; that is, only granting to its users the bare minimal privileges necessary to do their jobs.
|
||||||
|
|
||||||
|
For our purpose of access control, we define the **entry points** of a component as the following types of _public Rust language items_ exposed by a component.
|
||||||
|
* Structs, enumerations, and unions;
|
||||||
|
* Functions and associated functions;
|
||||||
|
* Static items;
|
||||||
|
* Constant items.
|
||||||
|
|
||||||
|
We consider these items as entry points because a user who gets access to such items can directly _execute_ the code within the component. And also for this reason, other types of Rust language items (e.g., modules, type aliases, use declarations, traits, etc.) are not considered as entry points: they either do not involve direct code execution (like modules) or only involves _indirect_ code execution (like type aliases, use declarations, and traits) via other entry points.
|
||||||
|
|
||||||
|
But not all entry points are created equal. Some are actually safe to use arbitrarily without jeopardizing other parts of the system, while some are considered **privileged** in the sense that _uncontrolled_ access to such entry points may have undesirable impacts on the system. The opposite of privileged entry points is the **unprivileged** ones. CapComp has a set of built-in rules to identify some common patterns of naive or harmless code that can be safely considered as unprivileged. In the meantime, developers can decide which entry points they believe are unprivileged and point them out for CapComp. The rest of the entry points are thus the privileged ones. Put it in another way, all entry points are considered privileged by default; unprivileged ones are the exceptions. Privileged entry points are the targets of the access control mechanism of CapComp.
|
||||||
|
|
||||||
|
With the concepts explained, we can now articulate the three main properties of CapComp.
|
||||||
|
* **Object-level access control.** All public methods of an object behind a capability are access controlled.
|
||||||
|
* **Component-level access control.** All privileged entry points of a component are accessed controlled.
|
||||||
|
* **Mandatory access control.** All users of components cannot penetrate the access control without using the `unsafe` keyword.
|
||||||
|
|
||||||
|
These properties are mostly enforced using compile-time techniques, thus incurring zero runtime overheads.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To give you a concrete idea of CapComp, we revisit the two motivating examples and show how CapComp can meet their demands.
|
||||||
|
|
||||||
|
### Example 1: Capabilities in a microkernel-based OS
|
||||||
|
|
||||||
|
One fundamental primitive provided by CapComp is capabilities. The type that represents a capability is `Cap<O, R>`, where `O` is the type of the capability's associated object and `R` is the type of the capability's associated rights.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: cap_comp/cap.rs
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
/// A capability.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Cap<O: ?Sized, R> {
|
||||||
|
rights: PhantomData<R>,
|
||||||
|
object: O,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O, R> Cap<O, R> {
|
||||||
|
pub fn new(object: O) -> Self {
|
||||||
|
Self {
|
||||||
|
rights: PhantomData,
|
||||||
|
object,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how the Rust representation of a capability in CapComp differs from the traditional approach. The key difference is that CapComp represents rights in _types_, not values. This enables CapComp to enforce access control at compile time.
|
||||||
|
|
||||||
|
So how do users use CapComp's capabilities? Imagine you are developing a kernel object named `Endpoint`, which can be used to send and receive bytes between threads or processes.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: user_project/endpoint.rs
|
||||||
|
|
||||||
|
pub struct Endpoint;
|
||||||
|
|
||||||
|
impl Endpoint {
|
||||||
|
pub fn recv(&self) -> u8 {
|
||||||
|
todo!("impl recv")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, _byte: u8) {
|
||||||
|
todo!("impl send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You want to ensure that the sender side of an `Endpoint` (could be a process or thread) can only use it to send bytes, while the receiver side can only receive bytes. We can do so easily with CapComp.
|
||||||
|
|
||||||
|
First, add `cap_comp` crate as a dependency in your project's `Cargo.toml`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
[dependencies]
|
||||||
|
cap_comp = { version = "0.1", features = ["read_right", "write_right"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
The `features` field specifies which kinds of access rights that your project finds interesting. There are a rich set of general-purpose access rights built into CapComp and you can select those that make sense to your project. (We may find a way to support customized access rights in the future.)
|
||||||
|
|
||||||
|
Then, we can refactor the code of `Endpoint` to leverage CapComp to protect `Endpoint` with capabilities.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: user_project/endpoint.rs
|
||||||
|
|
||||||
|
use cap_comp::{
|
||||||
|
rights::{Read, Write},
|
||||||
|
impl_cap, require,
|
||||||
|
Cap, Rights,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Endpoint;
|
||||||
|
|
||||||
|
// Instruct CapComp to implement the right set of methods for
|
||||||
|
// `Cap<Endpoint, R>`, depending upon rights specified by `R`.
|
||||||
|
#[impl_cap]
|
||||||
|
impl Endpoint {
|
||||||
|
// Implement the recv method for `Cap<Endpoint, R: Read>`.
|
||||||
|
#[require(rights = [Read])]
|
||||||
|
pub fn recv(&self) -> u8 {
|
||||||
|
todo!("impl recv")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the send method for `Cap<Endpoint, R: Write>`.
|
||||||
|
#[require(rights = [Write])]
|
||||||
|
pub fn send(&self, _byte: u8) {
|
||||||
|
todo!("impl send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recv() {
|
||||||
|
// Create an endpoint capability with the write right.
|
||||||
|
let endpoint_cap: Cap<Endpoint, Rights![Write]> = Cap::new(Endpoint);
|
||||||
|
endpoint_cap.send(0);
|
||||||
|
//endpoint_cap.recv(); // compiler error!
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn send() {
|
||||||
|
// Create an endpoint capability with the read right.
|
||||||
|
let endpoint_cap: Cap<Endpoint, Rights![Read]> = Cap::new(Endpoint);
|
||||||
|
endpoint_cap.recv();
|
||||||
|
//endpoint_cap.send(0); // compiler error!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code shows the most basic usage of capabilities and rights in CapComp and CapComp's ability to enforce object-level access control at compile time.
|
||||||
|
|
||||||
|
### Example 2: Key retention in a monolithic OS kernel
|
||||||
|
|
||||||
|
#### What are tokens?
|
||||||
|
|
||||||
|
Just like the methods of an object can be access-controlled wtih capabilities, the entry points of a component can be access-controlled with tokens. In CapComp, a **token** is owned by a component or a module inside a component; such a component or module is called **token owner**. A token represents the access rights of the token owner to other components. So by presenting its token, a token owner can prove to a recipient component which access rights it possesses; and based on token, the recipient component can decide whether to allow a specific operation accordingly.
|
||||||
|
|
||||||
|
Anywhere inside a component, a user can get access to the token that belongs to the current textual scope via `Token!()`, a special macro provided by CapComp. The macro returns a _zero-sized_ Rust object of type `T: Token`, where `Token` is a marker trait for all token types. Type `T` is encoded with the access rights info via type-programming techniques. Thus, transferring and checking tokens can be done entirely at the compile time, without incurring any runtime cost.
|
||||||
|
|
||||||
|
#### Refactor `keyring` with tokens
|
||||||
|
|
||||||
|
To make our `keyring` example more realistic, let's consider a system that consists of a `keyring` component and three other components.
|
||||||
|
* The `keyring` component is responsible for key management.
|
||||||
|
* The `boot` component parses arguments and configurations (including encryption keys) from users and starts up the entire system.
|
||||||
|
* The `encrypted_fs` component implements encrypted file I/O, which uses encryption keys.
|
||||||
|
* The `proc_fs` component implements a Linux-like procfs that facilitate inspecting the status of the system (e.g., all available keys).
|
||||||
|
|
||||||
|
To leverage the component-level access control mechanism of CapComp, one needs to provide a configuration file for CapComp, which specifies all components in the target system and their access rights.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# File: CapComp.toml
|
||||||
|
|
||||||
|
# There are four components in total
|
||||||
|
[components]
|
||||||
|
keyring = { path = "keyring/" }
|
||||||
|
boot = { path = "boot/" }
|
||||||
|
encrypted_fs = { path = "encrypted_fs/" }
|
||||||
|
procfs = { path = "procfs/" }
|
||||||
|
|
||||||
|
# A centralized place to claim access to the keyring component
|
||||||
|
[access_to.keyring]
|
||||||
|
# The boot component has the write right.
|
||||||
|
boot = { rights = ["Write"] }
|
||||||
|
|
||||||
|
# The init module inside the encrypted_fs component needs
|
||||||
|
# the read right
|
||||||
|
'encrypted_fs.init' = { rights = ["Read", "Inspect"] }
|
||||||
|
|
||||||
|
# The proc_fs component has the inspect right
|
||||||
|
proc_fs = { rights = ["Inspect"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we refactor the `keyring` component with CapComp.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: keyring/lib.rs
|
||||||
|
|
||||||
|
use cap_comp::{
|
||||||
|
load_config, impl_cap, require, unprivileged
|
||||||
|
Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
load_config!("../CapComp.toml");
|
||||||
|
|
||||||
|
/// A key is some secret data.
|
||||||
|
// Inform CapComp that this type is unprivileged, which means
|
||||||
|
// it does not have to be protected by capabilities.
|
||||||
|
#[unprivileged]
|
||||||
|
pub struct Key {
|
||||||
|
name: String,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[impl_cap]
|
||||||
|
impl Key {
|
||||||
|
pub fn new(name: &str, bytes: &[u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
payload: Vec::from(bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[require(rights = [Inspect])]
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[require(rights = [Read])]
|
||||||
|
pub fn payload(&self) -> &Vec<u8> {
|
||||||
|
&self.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
#[require(rights = [Write])]
|
||||||
|
pub fn payload_mut(&mut self) -> &mut Vec<u8> {
|
||||||
|
&mut self.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A component can access this operation only if it has a token
|
||||||
|
// with the write right.
|
||||||
|
#[require(bounds = { Tk: Write })]
|
||||||
|
pub fn insert_key<Tk>(key: Key, token: &Tk) {
|
||||||
|
todo!("omitted...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A component can access this operation only if it has a token
|
||||||
|
// with the write right.
|
||||||
|
#[require(bounds = { Tk: Read })]
|
||||||
|
pub fn find_key<Tk>(name: &str, token: &Tk) -> Option<Arc<Cap<Key, Rights![Read]>>> {
|
||||||
|
todo!("omitted...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A component can access this operation only if it has a token
|
||||||
|
// with the inspect right.
|
||||||
|
#[require(bounds = { Tk: Inspect })]
|
||||||
|
pub fn list_all_keys<Tk>(name: &str, token: &Tk) -> Option<Arc<Cap<Key, Rights![Inspect]>>> {
|
||||||
|
todo!("omitted...")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `boot` component can insert keys into `keyring` since it has the write right.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: boot/lib.rs
|
||||||
|
|
||||||
|
use capcomp::{load_config, Token};
|
||||||
|
use keyring::{insert_key, Key};
|
||||||
|
|
||||||
|
load_config!("../CapComp.toml");
|
||||||
|
|
||||||
|
fn parse_cli_arguments(args: &[&str]) {
|
||||||
|
let key = parse_encryption_key(args);
|
||||||
|
insert_key("encryption_key", Token!());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_encryption_key(args: &[&str]) {
|
||||||
|
todo!("omitted...")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `encrypted_fs` component can fetch and use its encryption key from `keyring` since it has the read right.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: mount_fs/lib.rs
|
||||||
|
capcomp::load_config!("../CapComp.toml");
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: mount_fs/init.rs
|
||||||
|
use capcomp::Token;
|
||||||
|
use keyring::find_key;
|
||||||
|
|
||||||
|
pub fn init_fs() {
|
||||||
|
let key = find_key("encryption_key", Token!());
|
||||||
|
// use the key to init fs...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For debug purposes, the `proc_fs` component is allowed to list the metadata of keys since it has the inspect right, but not read or update keys.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// File: proc_fs/lib.rs
|
||||||
|
|
||||||
|
use capcomp::{load_config, Token};
|
||||||
|
use keyring::list_all_keys;
|
||||||
|
|
||||||
|
load_config!("../CapComp.toml");
|
||||||
|
|
||||||
|
pub fn list_all_key_names() -> Vec<String> {
|
||||||
|
let keys = list_all_keys(Token!());
|
||||||
|
key.iter().map(|key| key.name().to_string()).collect()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, the CapComp-powered system has greatly narrowed the scope of code that has access to keys. In fact, the scope is clearly defined in `CapComp.toml`. This greatly reduces the odds of misusing or leaking keys and facilitates security auditing for the codebase.
|
||||||
|
|
||||||
|
## Prototype
|
||||||
|
|
||||||
|
We have implemented a prototype of CapComp that can support the two examples above. More specifically, the prototype demonstrates that the following key design points of CapComp are feasible.
|
||||||
|
|
||||||
|
* Using Rust's type-level programming to encode access rights with types and enforce access control at compile time (`Cap<O, R>`, `Rights!`, `Token!`).
|
||||||
|
* Using Rust's procedure macros to minimize the boilerplate code required (`#[impl_cap]`).
|
||||||
|
* Using program analysis technique (a lint pass) to check if all privileged entry points of a component are access-controlled (`#[unprivileged]`).
|
||||||
|
|
||||||
|
## Discussions
|
||||||
|
|
||||||
|
* Figure out the killer apps of CapComp
|
||||||
|
* How to demonstrate the security benefits brought by CapComp?
|
||||||
|
* Is it too complex for developers to write Rust code with CapComp?
|
||||||
|
* What is it like to build a component-based OS with CapComp?
|
||||||
|
* Can we formally verify the security guarantees of CapComp?
|
@ -0,0 +1,794 @@
|
|||||||
|
# Type-Level Programming (TLP) in Rust
|
||||||
|
|
||||||
|
## What is TLP?
|
||||||
|
|
||||||
|
TLP is, in short, _computation over types_, where
|
||||||
|
* **Types** are used as *values*
|
||||||
|
* **Generic parameters** are used as *variables*
|
||||||
|
* **Trait bounds** are used as *types*
|
||||||
|
* **Traits with associated types** are as used as *functions*
|
||||||
|
|
||||||
|
Let's see some examples of Rust TLP crates.
|
||||||
|
|
||||||
|
Example 1: TLP integers.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use typenum::{Sum, Exp, Integer, N2, P3, P4};
|
||||||
|
|
||||||
|
type X = Sum<P3, P4>;
|
||||||
|
assert_eq!(<X as Integer>::to_i32(), 7);
|
||||||
|
|
||||||
|
type Y = Exp<N2, P3>;
|
||||||
|
assert_eq!(<Y as Integer>::to_i32(), -8);
|
||||||
|
```
|
||||||
|
|
||||||
|
Example 2: TLP lists.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use type_freak::{TListType, list::*};
|
||||||
|
|
||||||
|
type List1 = TListType![u8, u16, u32];
|
||||||
|
|
||||||
|
type List2 = LPrepend<List1, u64>;
|
||||||
|
// List2 ~= TListType![u64, u8, u16, u32]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this document, we will first explain how TLP works in Rust and
|
||||||
|
then will demonstrate the value of TLP by giving a good application---
|
||||||
|
implementing zero-cost capablities!
|
||||||
|
|
||||||
|
## How TLP Works?
|
||||||
|
|
||||||
|
### Case Study: TLP bools
|
||||||
|
|
||||||
|
Let's define type-level bools.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A marker trait for type-level bools.
|
||||||
|
pub trait Bool {} //
|
||||||
|
impl Bool for True {}
|
||||||
|
impl Bool for False {}
|
||||||
|
|
||||||
|
/// Type-level "true".
|
||||||
|
pub struct True;
|
||||||
|
/// Type-level "false".
|
||||||
|
pub struct False;
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's compute over type-level bools.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A trait operator for logical negation on type-level bools.
|
||||||
|
pub trait Not {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Not for True {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Not for False {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type alias to make using the `Not` operator easier.
|
||||||
|
pub type NotOp<B> = <B as Not>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's test the type-level bools.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::bool::{True, False, NotOp};
|
||||||
|
use crate::assert_type_same;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not_op() {
|
||||||
|
assert_type_same!(True, NotOp<False>);
|
||||||
|
assert_type_same!(False, NotOp<True>);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's define more operations for type-level bools.
|
||||||
|
```rust
|
||||||
|
/// A trait operator for logical and on type-level bools.
|
||||||
|
pub trait And<B: Bool> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Bool> And<B> for True {
|
||||||
|
type Output = B;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Bool> And<B> for False {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type alias to make using the `And` operator easier.
|
||||||
|
pub type AndOp<B0, B1> = <B0 as And<B1>>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's test the and operation.
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_and_op() {
|
||||||
|
assert_type_same!(AndOp<True, True>, True);
|
||||||
|
assert_type_same!(AndOp<True, False>, False);
|
||||||
|
assert_type_same!(AndOp<False, True>, False);
|
||||||
|
assert_type_same!(AndOp<False, False>, False);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mnemonic for TLP functions
|
||||||
|
|
||||||
|
#### Defining a function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl SelfType {
|
||||||
|
pub fn method(&self, arg0: Type0, arg1: Type1, /* ... *) -> RetType {
|
||||||
|
/* function body */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
v.s.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl<Arg0, Arg1, /* ... */> Method<Arg0, Arg1, /* ... */> for SelfType
|
||||||
|
where
|
||||||
|
Arg0: ArgBound0,
|
||||||
|
Arg1: ArgBound1,
|
||||||
|
/* ... */
|
||||||
|
{
|
||||||
|
type Output/*: RetBound */ = /* function body */;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Traits like `Method` are called _trait operators_.
|
||||||
|
|
||||||
|
#### Calling a function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
object.method(arg0, arg1, /* ... */>);
|
||||||
|
```
|
||||||
|
|
||||||
|
v.s.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
<Object as Method<Arg0, Arg1, /* ... */>>::Output
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```rust
|
||||||
|
ObjectType::method(&object, arg0, arg1, /* ... */>);
|
||||||
|
```
|
||||||
|
|
||||||
|
v.s.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
MethodOp<Object, Arg0, Arg1, /* ... */>
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLP equality assertions
|
||||||
|
|
||||||
|
This is what we want.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_assert() {
|
||||||
|
assert_type_same!(u16, u16);
|
||||||
|
assert_type_same!((), ());
|
||||||
|
// assert_same_type!(u16, bool); // Compiler error!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is how it is implemented.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A trait that is intended to check if two types are the same in trait bounds.
|
||||||
|
pub trait IsSameAs<T> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IsSameAs<T> for T {
|
||||||
|
type Output = ();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub type AssertTypeSame<Lhs, Rhs> = <Lhs as IsSameAs<Rhs>>::Output;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_type_same {
|
||||||
|
($lhs:ty, $rhs:ty) => {
|
||||||
|
const _: AssertTypeSame<$lhs, $rhs> = ();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLP equality checks
|
||||||
|
|
||||||
|
The `SameAsOp` (and `SameAs`) is a very useful primitive.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_same_as() {
|
||||||
|
assert_type_same!(SameAsOp<True, True>, True);
|
||||||
|
assert_type_same!(SameAsOp<False, True>, False);
|
||||||
|
assert_type_same!(SameAsOp<True, NotOp<False>>, True);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how it gets implemented.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A trait operator to check if two types are the same, returning a Bool.
|
||||||
|
pub trait SameAs<T> {
|
||||||
|
type Output: Bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SameAsOp<T, U> = <T as SameAs<U>>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl SameAs<True> for True {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
impl SameAs<False> for True {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SameAs<True> for False {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<False> for False {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Can be simplified in the future (through the unstablized feature of specialization)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(specialization)]
|
||||||
|
|
||||||
|
impl<T> SameAs<T> for True {
|
||||||
|
default type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<True> for True {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SameAs<T> for False {
|
||||||
|
default type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<False> for False {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLP control flow
|
||||||
|
|
||||||
|
#### Conditions
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_if() {
|
||||||
|
assert_type_same!(IfOp<True, u32, ()>, u32);
|
||||||
|
assert_type_same!(IfOp<False, (), bool>, bool);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait If<Cond: Bool, T1, T2> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T1, T2> If<True, T1, T2> for () {
|
||||||
|
type Output = T1;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T1, T2> If<False, T1, T2> for () {
|
||||||
|
type Output = T2;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type IfOp<Cond, T1, T2> = <() as If<Cond, T1, T2>>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Loops
|
||||||
|
|
||||||
|
You don't write loops in TLP; you write _recursive_ functions.
|
||||||
|
|
||||||
|
### TLP collections
|
||||||
|
|
||||||
|
Take TLP sets as an example.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
/// A marker trait for type-level sets.
|
||||||
|
pub trait Set {}
|
||||||
|
|
||||||
|
/// An non-empty type-level set.
|
||||||
|
pub struct Cons<T, S: Set>(PhantomData<(T, S)>);
|
||||||
|
/// An empty type-level set.
|
||||||
|
pub struct Nil;
|
||||||
|
|
||||||
|
impl<T, S: Set> Set for Cons<T, S> {}
|
||||||
|
impl Set for Nil {}
|
||||||
|
```
|
||||||
|
|
||||||
|
How to write a _contain_ operator?
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn test_set_contain() {
|
||||||
|
struct A;
|
||||||
|
struct B;
|
||||||
|
struct C;
|
||||||
|
struct D;
|
||||||
|
|
||||||
|
type AbcSet = Cons<A, Cons<B, Cons<C, Nil>>>;
|
||||||
|
assert_type_same!(SetContainOp<AbcSet, A>, True);
|
||||||
|
assert_type_same!(SetContainOp<AbcSet, B>, True);
|
||||||
|
assert_type_same!(SetContainOp<AbcSet, C>, True);
|
||||||
|
assert_type_same!(SetContainOp<AbcSet, D>, False);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can implement the operator with recursion!
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A trait operator to check if `T` is a member of a type set;
|
||||||
|
pub trait SetContain<T> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SetContain<T> for Nil {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U, S> SetContain<T> for Cons<U, S>
|
||||||
|
where
|
||||||
|
S: Set,
|
||||||
|
U: SameAs<T>,
|
||||||
|
S: SetContain<T>,
|
||||||
|
SameAsOp<U, T>: Or<SetContainOp<S, T>>,
|
||||||
|
{
|
||||||
|
type Output = OrOp<SameAsOp<U, T>, SetContainOp<S, T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SetContainOp<Set, Item> = <Set as SetContain<Item>>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: needs to implement `SameAs` for all possible item types (e.g., among `A` through `D`).
|
||||||
|
|
||||||
|
### Where are the boundaries for TLP?
|
||||||
|
|
||||||
|
#### Expressiveness: practically unlimited.
|
||||||
|
|
||||||
|
![[Pasted image 20210825015426.png]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
![[Pasted image 20210825015401.png]]
|
||||||
|
|
||||||
|
#### Ergonomics: probably fixable.
|
||||||
|
|
||||||
|
An example from the `typ` crate.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
typ! {
|
||||||
|
fn BinaryGcd<lhs, rhs>(lhs: Unsigned, rhs: Unsigned) -> Unsigned {
|
||||||
|
if lhs == rhs {
|
||||||
|
lhs
|
||||||
|
} else if lhs == 0u {
|
||||||
|
rhs
|
||||||
|
} else if rhs == 0u {
|
||||||
|
lhs
|
||||||
|
} else {
|
||||||
|
if lhs % 2u == 1u {
|
||||||
|
if rhs % 2u == 1u {
|
||||||
|
if lhs > rhs {
|
||||||
|
let sub: Unsigned = lhs - rhs;
|
||||||
|
BinaryGcd(sub, rhs)
|
||||||
|
} else {
|
||||||
|
let sub: Unsigned = rhs - lhs;
|
||||||
|
BinaryGcd(sub, lhs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let div: Unsigned = rhs / 2u;
|
||||||
|
BinaryGcd(lhs, div)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if rhs % 2u == 1u {
|
||||||
|
let div: Unsigned = lhs / 2u;
|
||||||
|
BinaryGcd(div, rhs)
|
||||||
|
} else {
|
||||||
|
let ldiv: Unsigned = lhs / 2u;
|
||||||
|
let rdiv: Unsigned = rhs / 2u;
|
||||||
|
BinaryGcd(ldiv, rdiv) * 2u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## An Application of TLP
|
||||||
|
|
||||||
|
### Capabilities in Rust
|
||||||
|
|
||||||
|
A simplified example from zCore:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Handle<O> {
|
||||||
|
/// The object referred to by the handle.
|
||||||
|
pub object: Arc<O>,
|
||||||
|
|
||||||
|
/// The handle's associated rights.
|
||||||
|
pub rights: Rights,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The limitations
|
||||||
|
* CPU cost: check for rights cost CPU cycles;
|
||||||
|
* Memory cost: access rights cost memory space;
|
||||||
|
* Security weakness: it is easy to bypass access rights internally.
|
||||||
|
|
||||||
|
### Our idea: _zero-cost_ capabilities
|
||||||
|
|
||||||
|
Encoding the access rights in the type `R`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Handle<O, R> {
|
||||||
|
/// The object referred to by the handle.
|
||||||
|
object: Arc<O>,
|
||||||
|
|
||||||
|
/// The handle's associated rights.
|
||||||
|
rights: PhantomData<R>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Compile-time* access control with trait bounds on `R`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::my_dummy_channel::{Channel, Msg};
|
||||||
|
use crate::rights::Read;
|
||||||
|
|
||||||
|
impl<R: Read> Handle<Channel, R> {
|
||||||
|
pub fn read(&self) -> Msg {
|
||||||
|
self.object.read()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Write> Handle<Channel, R> {
|
||||||
|
pub fn write(&self, msg: Msg) {
|
||||||
|
self.object.write(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**--> Requirement 1. Construct a set of types that _satisfies all possible combinations of the trait bounds_.**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::rights::{Rights, RightsIsSuperSetOf};
|
||||||
|
|
||||||
|
impl<O, R: Rights> Handle<O, R> {
|
||||||
|
/// Create a new handle for the given object.
|
||||||
|
pub fn new(object: O) -> Self {
|
||||||
|
Self {
|
||||||
|
object: Arc::new(object),
|
||||||
|
rights: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a duplicate of the handle, refering to the same object.
|
||||||
|
///
|
||||||
|
/// The duplicate is guaranteed---by the compiler---to have the same or
|
||||||
|
/// lesser rights.
|
||||||
|
pub fn duplicate<R1>(&self) -> Handle<O, R1>
|
||||||
|
where
|
||||||
|
R1: Rights,
|
||||||
|
R: RightsIsSuperSetOf<R1>,
|
||||||
|
{
|
||||||
|
Handle {
|
||||||
|
object: self.object.clone(),
|
||||||
|
rights: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**--> Requirement 2. Implement the `RightsIsSuperSetOf<Sub>` trait for all `Super`, where _the rights represented by the `Super` type is a superset of the rights represented by the `Sub` type_.**
|
||||||
|
|
||||||
|
(Question: why a naive implementation won't work?)
|
||||||
|
|
||||||
|
### Our solution: the TLP-powered `trait_flags` crate
|
||||||
|
|
||||||
|
Our `trait_flags` crate is similar to `bitflags`, but it uses traits (or types) as flags, instead of bits.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Define a set of traits that represent different rights and
|
||||||
|
/// a macro that outputs types with the desired combination of rights.
|
||||||
|
trait_flags! {
|
||||||
|
trait Rights {
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
// many more...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn define_rights() {
|
||||||
|
type MyNoneRights = Rights![];
|
||||||
|
type MyReadRights = Rights![Read];
|
||||||
|
type MyWriteRights = Rights![Write];
|
||||||
|
type MyReadWriteRights = Rights![Read, Write];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn has_rights() {
|
||||||
|
fn has_read_right<T: Read>() {}
|
||||||
|
fn has_write_right<T: Write>() {}
|
||||||
|
|
||||||
|
has_read_right::<Rights![Read]>();
|
||||||
|
has_read_right::<Rights![Read, Write]>();
|
||||||
|
//has_read_right::<Rights![]>(); // Compiler error!
|
||||||
|
|
||||||
|
has_write_right::<Rights![Write]>();
|
||||||
|
has_write_right::<Rights![Read, Write]>();
|
||||||
|
//has_write_right::<Rights![]>(); // Compiler error!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn rights_is_superset_of() {
|
||||||
|
fn is_superset_of<Super: Rights + RightsIsSuperSetOf<Sub>, Sub: Rights>() {}
|
||||||
|
|
||||||
|
is_superset_of::<Rights![Read], Rights![]>();
|
||||||
|
is_superset_of::<Rights![Write], Rights![]>();
|
||||||
|
is_superset_of::<Rights![Read], Rights![Read]>();
|
||||||
|
is_superset_of::<Rights![Read, Write], Rights![Write]>();
|
||||||
|
is_superset_of::<Rights![Read, Write], Rights![Write]>();
|
||||||
|
|
||||||
|
//is_superset_of::<Rights![Read], Rights![Write]>(); // Compiler error!
|
||||||
|
//is_superset_of::<Rights![], Rights![Read]>(); // Compiler error!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How `trait_flags` works?
|
||||||
|
|
||||||
|
Let's walkthrough the Rust code generated by the `trait_flags` macro. Without loss of generality, we assume that the input given to the macro consists of only two rights: the read and write rights.
|
||||||
|
|
||||||
|
#### For requirement 1
|
||||||
|
|
||||||
|
**Step 1.** Generate a set of the types that can represent all possible combinations of rights.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Marker traits
|
||||||
|
pub trait Rights {}
|
||||||
|
pub trait Read: Rights {}
|
||||||
|
pub trait Write: Rights {}
|
||||||
|
|
||||||
|
pub struct RightSet<B0, B1>(
|
||||||
|
PhantomData<B0>, // If B0 == True, then the set contains the read right
|
||||||
|
PhantomData<B1>, // If B1 == True, then the set contains the write right
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2.** .
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl<B0, B1> Rights for RightSet<B0, B1> {}
|
||||||
|
impl<B1> Read for RightSet<True, B1> {}
|
||||||
|
impl<B0> Write for RightSet<B0, True> {}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For requirement 2
|
||||||
|
|
||||||
|
**Step 1.** Reduce the problem of `RightsIsSuperSetOf` trait to implementing a trait operator named `RightsIncludeOp`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A marker trait that marks any pairs of `Super: Rights` and `Sub: Rights` types,
|
||||||
|
/// where the rights of `Super` is a superset of that of `Sub`.
|
||||||
|
pub trait RightsIsSuperSetOf<R: Rights> {}
|
||||||
|
|
||||||
|
impl<Super, Sub> RightsIsSuperSetOf<Sub> for Super
|
||||||
|
where
|
||||||
|
Super: Rights + RightsInclude<Sub>,
|
||||||
|
Sub: Rights,
|
||||||
|
True: IsSameAs<RightsIncludeOp<Super, Sub>>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type alias for `RightsInclude`.
|
||||||
|
pub type RightsIncludeOp<Super, Sub> = <Super as RightsInclude<Sub>>::Output;
|
||||||
|
|
||||||
|
/// A trait operator that "calculates" if `Super: Rights` is a superset of `Sub: Rights`.
|
||||||
|
/// If yes, the result is `True`; otherwise, the result is `False`.
|
||||||
|
pub trait RightsInclude<Sub: Rights> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2. ** Implement `RightsIncludeOp`.
|
||||||
|
|
||||||
|
Here is a simplified version.
|
||||||
|
```rust
|
||||||
|
impl<Super0, Super1, Sub0, Sub1> RightsInclude<RightSet<Sub0, Sub1>>
|
||||||
|
for RightSet<Super0, Super1>
|
||||||
|
where
|
||||||
|
Super0: Bool,
|
||||||
|
Super1: Bool,
|
||||||
|
Sub0: Bool,
|
||||||
|
Sub1: Bool,
|
||||||
|
BoolGreaterOrEqual<Super0, Sub0>: And<BoolGreaterOrEqual<Super1, Sub1>>,
|
||||||
|
// For brevity, we omit many less important trait bounds...
|
||||||
|
{
|
||||||
|
type Output = AndOp<BoolGreaterOrEqual<Super0, Sub0>, BoolGreaterOrEqual<Super1, Sub1>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoolGreaterOrEqual<B0, B1> = NotOp<AndOp<SameAsOp<B0, False>, SameAsOp<B1, True>>>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### An alternative implementation of `trait_flags`
|
||||||
|
|
||||||
|
Let's walkthrough the Rust code generated by this alternative implementation of `trait_flags`, which is based on variable-length TLP sets that we have described in part 1 of this talk. Again, we assume the input to the macro is only the read and write rights.
|
||||||
|
|
||||||
|
#### For requirement 1
|
||||||
|
|
||||||
|
**Step 1.** Generate a set of the types that can represent all possible combinations of rights.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::set::{Set, Cons, Nil};
|
||||||
|
|
||||||
|
pub trait Rights {}
|
||||||
|
pub trait Read: Rights {}
|
||||||
|
pub trait Write: Rights {}
|
||||||
|
|
||||||
|
impl<S: Set + Rights> Rights for Cons<ReadRight, S> {}
|
||||||
|
impl<S: Set + Rights> Rights for Cons<WriteRight, S> {}
|
||||||
|
impl Rights for Nil {}
|
||||||
|
|
||||||
|
pub struct ReadRight;
|
||||||
|
pub struct WriteRight;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2.** Define the `SameAs` relationship between right types.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl SameAs<ReadRight> for ReadRight {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
impl SameAs<WriteRight> for ReadRight {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SameAs<ReadRight> for WriteRight {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<WriteRight> for WriteRight {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code is only part that results in a complexity greater than O(N), where N is the number of types. In the future, it can be reduced to the complexity of O(N) with the help of an unstable feature named _specialization_.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(specialization)]
|
||||||
|
|
||||||
|
impl<T> SameAs<T> for ReadRight {
|
||||||
|
default type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<ReadRight> for ReadRight {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SameAs<T> for WriteRight {
|
||||||
|
default type Output = False;
|
||||||
|
}
|
||||||
|
impl SameAs<WriteRight> for WriteRight {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3.** Implement the marker traits for _right_ types.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl<R> Read for R
|
||||||
|
where
|
||||||
|
R: Rights + SetContain<ReadRight>,
|
||||||
|
True: IsSameAs<SetContainOp<R, ReadRight>>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> Write for R
|
||||||
|
where
|
||||||
|
R: Rights + SetContain<WriteRight>,
|
||||||
|
True: IsSameAs<SetContainOp<R, WriteRight>>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we have implemented the `SetContain` and `SetContainOp` trait operators in part 1.
|
||||||
|
|
||||||
|
#### For requirement 2
|
||||||
|
|
||||||
|
**Step 1.** Reduce the problem of `RightsIsSuperSetOf` trait to implementing a trait operator named `RightsIncludeOp`, which in turn can be implemented with `SetIncludeOp`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A marker trait that marks any pairs of `Super: Rights` and `Sub: Rights` types,
|
||||||
|
/// where the rights of `Super` is a superset of that of `Sub`.
|
||||||
|
pub trait RightsIsSuperSetOf<R: Rights> {}
|
||||||
|
|
||||||
|
impl<Super, Sub> RightsIsSuperSetOf<Sub> for Super
|
||||||
|
where
|
||||||
|
Super: Rights + RightsInclude<Sub>,
|
||||||
|
Sub: Rights,
|
||||||
|
True: IsSameAs<RightsIncludeOp<Super, Sub>>,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::set::{SetInclude as RightsInclude, SetIncludeOp as RightsIncludeOp}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2.** Implement `SetIncludeOp`, which is a trait operator that checks if a set A includes a set B, i.e., the set A is a superset of the set B.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A trait operator to check if a set A includes a set B, i.e., A is a superset of B.
|
||||||
|
pub trait SetInclude<S> {
|
||||||
|
type Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SetInclude<Nil> for Nil {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, S: Set> SetInclude<Cons<T, S>> for Nil {
|
||||||
|
type Output = False;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, S: Set> SetInclude<Nil> for Cons<T, S> {
|
||||||
|
type Output = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SuperT, SuperS, SubT, SubS> SetInclude<Cons<SubT, SubS>> for Cons<SuperT, SuperS>
|
||||||
|
where
|
||||||
|
SuperS: Set + SetInclude<SubS> + SetContain<SubT> + SetInclude<SubS>,
|
||||||
|
SubS: Set,
|
||||||
|
SuperT: SameAs<SubT>,
|
||||||
|
// For brevity, we omit some trait bounds...
|
||||||
|
{
|
||||||
|
type Output = IfOp<
|
||||||
|
SameAsOp<SuperT, SubT>, // The if condition
|
||||||
|
SetIncludeOp<SuperS, SubS>, // The if branch
|
||||||
|
AndOp< // The else branch
|
||||||
|
SetContainOp<SuperS, SubT>,
|
||||||
|
SetIncludeOp<SuperS, SubS>,
|
||||||
|
>,
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SetIncludeOp<SuperSet, SubSet> = <SuperSet as SetInclude<SubSet>>::Output;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrapup
|
||||||
|
|
||||||
|
* Part 1: the magic of TLP
|
||||||
|
* TLP bools
|
||||||
|
* TLP equality assertions and checks
|
||||||
|
* TLP control flow (if and ~~loops~~)
|
||||||
|
* TLP collections (type sets)
|
||||||
|
* Part 2: the application of TLP
|
||||||
|
* Traditional capability-based access control
|
||||||
|
* Zero-cost capability-based access control
|
||||||
|
* How to implement `trait_flags` with TLP
|
||||||
|
* Fixed-length set version (`RightSet<R0, R1, ...>`)
|
||||||
|
* Variable-length set version (`Cons<R0, Cons<R1, ...>`)
|
@ -0,0 +1 @@
|
|||||||
|
# What are Capabilities?
|
Loading…
x
Reference in New Issue
Block a user