Capability-Based Security

Capabilities are a security paradigm in which an ability that is usually handled as a permission (i.e. certain processes are allowed to perform an action if they are saved on an "access control list") are instead handled as a token (i.e. the process that possesses token can perform a certain action). These unforgeable tokens (as enforced by the kernel) can be passed to other owners, held by a given process, and checked for.

On Kinode, each process has an associated set of capabilities, which are each represented internally as an arbitrary JSON object with a source process:

#![allow(unused)]
fn main() {
pub struct Capability {
    pub issuer: Address,
    pub params: String, // JSON-string
}
}

The kernel abstracts away the process of ensuring that a capability is not forged. As a process developer, if a capability comes in on a message or is granted to you by the kernel, you are guaranteed that it is legitimate.

Runtime processes, including the kernel itself, the filesystem, and the HTTP client, issue capabilities to processes. Then, when a request is made by a process, the responder verifies the process's capability. If the process does not have the capability to make such a request, it will be denied.

To give a concrete example: the filesystem can read/write, and it has the capabilities for doing so. The FS may issue capabilities to processes to read/write to certain drives. A process can request to read/write somewhere, and then the FS checks if that process has the required capability. If it does, the FS does the read/write; if not, the request will be denied.

System level capabilities like the above can only be given when a process is first installed.

Startup Capabilities with manifest.json

When developing an application, manifest.json will be your first encounter with capabilties. With this file, capabilities are directly granted to a process on startup. Upon install, the package manager (also referred to as "app store") surfaces these requested capabilities to the user, who can then choose to grant them or not. Here is a manfiest.json example for the chess app:

[
    {
        "process_name": "chess",
        "process_wasm_path": "/chess.wasm",
        "on_exit": "Restart",
        "request_networking": true,
        "request_capabilities": [
            "net:distro:sys"
        ],
        "grant_capabilities": [
            "http_server:distro:sys"
        ],
        "public": true
    }
]

By setting request_networking: true, the kernel will give it the "networking" capability. In the request_capabilities field, chess is asking for the capability to message net:distro:sys. Finally, in the grant_capabilities field, it is giving http_server:distro:sys the ability to message chess.

When booting the chess app, all of these capabilities will be granted throughout your node. If you were to print out chess' capabilities using kinode_process_lib::our_capabilities() -> Vec<Capability>, you would see something like this:

#![allow(unused)]
fn main() {
[
    // obtained because of `request_networking: true`
    Capability { issuer: "our@kernel:distro:sys", params: "\"network\"" },
    // obtained because we asked for it in `request_capabilities`
    Capability { issuer: "our@net:distro:sys", params: "\"messaging\"" }
]
}

Note that userspace capabilities, those created by other processes, can also be requested in a package manifest, though it's not guaranteed that the user will have installed the process that can grant the capability. Therefore, when a userspace process uses the capabilities system, it should have a way to grant capabilities through its body protocol, as described below.

Userspace Capabilities

While the manifest fields are useful for getting a process started, it is not sufficient for creating and giving custom capabilities to other processes. To create your own capabilities, simply declare a new one and attach it to a Request or Response like so:

#![allow(unused)]
fn main() {
let my_new_cap = kinode_process_lib::Capability::new(our, "\"my-new-capability\"");

Request::new()
    .to(a_different_process)
    .capabilities(vec![my_new_cap])
    .send();
}

On the other end, if a process wants to save and reuse that capability, they can do something like this:

#![allow(unused)]
fn main() {
kinode_process_lib::save_capabilities(req.capabilities);
}

This call will automatically save the caps for later use. Next time you attach this cap to a message, whether that is for authentication with the issuer, or to share it with another process, it will reach the other side just fine, and they can check it using the exact same flow.

Get Help: