In this document, we will walk through a scenario with five users initializing and running Aranya. The users will create a team using Aranya and send messages to each other using Aranya Fast Channels. To run this scenario using Rust, see the Rust template. To run this scenario using our C API wrappers, see the C example.
There are a few things to note:
The following walkthrough offers a detailed explanation of Aranya’s internals, written in Rust, to assist in setting up an example usage with the client and daemon. Examples for the Rust API and the C API wrappers are both displayed.
Any policy actions are determined by the implemented policy. This walkthrough will use the default policy defined here.
Security tip: This walkthrough is intended as an example to be run on a single machine. As such, a single machine is used to build all key bundles and run all daemons under a single user’s profile. In production, each Aranya user’s key bundle should be created under separate Linux users on their respective machines and preferably all private keys should be stored in a protected partition, such as an HSM, for maximum security. This avoids a single access point for all Aranya user keys in case a machine is compromised.
See the overview for more details on the components used in this walkthrough.
The walkthrough includes five users who will be referred to by their user role. The actions performed by each user are based on the permissions assigned to each role in the default policy. There will be five users, Owner
, Admin
, Operator
, Member A
and Member B
. We will use the daemon
implementation for this example.
Step 1. Prepare the device environment
Step 2: Configure, build and run the daemon for each user
Step 3. Submit an action to the Owner
’s daemon to create a team
Step 4. Submit actions to populate the team with the rest of the users
Step 5. Submit an action to the Admin
’s daemon to
create an Aranya Fast Channels label
Step 6. Submit actions to the Operator
’s daemon to
assign the Fast Channels label to Member A
and Member B
Step 7. Submit an action to Member A
’s daemon to
create an Aranya Fast Channel
Step 8. Call the Fast Channels API from Member A
’s daemon to
send a message. Optionally, call the Fast Channels API
from Member B
’s daemon to send a message back.
The following dependencies must be present on the device building Aranya:
The following platforms are not supported:
The daemon provides functionality for the client library to maintain Aranya state, including interacting with the graph and syncing with peers, and send off-graph messages using Aranya Fast Channels. The daemon and client interact through the Daemon API. The following sections will walk through configuring and starting a long-running daemon process.
For more details, see the Aranya Daemon’s README.
At runtime, the daemon takes in a configuration file with information used by the daemon to network and operate. This includes a folder that contains non-volatile information used by the daemon to operate, including private cryptographic material belonging to the user, key storage accessed by Aranya and graph storage for holding all fully processed commands. Additionally, the daemon’s config file also includes networking for syncing and off-graph messaging. A complete example of a daemon configuration file can be found here.
Based on this example, create a configuration file for each user. Remember to change the ports and other user-specific values for each user.
Or, directly use the daemon configuration files from the C example. This example has configs for each user in this tutorial.
Now that the daemons have been configured, we can build and run them!
To build the daemon, invoke cargo build
:
$ cargo build --bin aranya-daemon --release
Since we have separate configuration files for each user, we only need one build of the daemon. This step only needs to be performed once.
To start the daemon for the owner, we run the following:
$ target/release/aranya-daemon <path to owner's daemon config>
Repeat this step for all users, substituting the associated configuration file for each user:
$ target/release/aranya-daemon <path to admin's daemon config>
$ target/release/aranya-daemon <path to operator's daemon config>
$ target/release/aranya-daemon <path to member a's daemon config>
$ target/release/aranya-daemon <path to member b's daemon config>
Internally, the daemon is instantiated by loading the specified configuration
file. Once created, the daemon starts using its run
method.
let daemon = Daemon::load(config).await?;
daemon.run().await
We will walk through the steps performed by the run
method to set up Aranya
and Aranya Fast Channels next.
The run
method will first create the necessary objects to interact with
Aranya, including tools for storage, cryptography and syncing. The daemon’s
setup_aranya
method uses these items to instantiate the Aranya client for
submitting actions. Then, the daemon will call setup_afc
to set up the
networking required to send messages to peers using Fast Channels. We will walk
through these setup methods in the following sections.
Dependencies of Aranya include a crypto engine, policy engine, and storage
provider. Once running and before aranya can be setup, the daemon will
instantiate, by default, a key store and crypto engine. These are then passed
into the setup_aranya
method, along with an external address for syncing and
the public portions of the device keys. The device keys are three asymmetric
cryptographic keys used to identify the device (IdentityKey
), validate their
operations (SigningKey
), and send encrypted data to them (EncryptionKey
).
This input is then used to instantiate a policy engine and storage provider.
use aranya_crypto::{
default::{DefaultCipherSuite, DefaultEngine},
keystore::fs_keystore::Store as KeyStore,
};
use aranya_runtime::{
storage::linear::libc::{
FileManager,
LinearStorageProvider
},
ClientState,
};
use crate::vm_policy::PolicyEngine;
const TEST_POLICY: &str = "/path/to/policy.md";
/// Creates the Aranya client and server.
async fn setup_aranya(
&self,
eng: DefaultEngine,
store: KeyStore,
pk: &PublicKeys<DefaultCipherSuite>,
external_sync_addr: Addr,
) -> Result<(Client, Server)> {
let user_id = pk.ident_pk.id()?;
let aranya = ClientState::new(
PolicyEngine<DefaultEngine, KeyStore>::new(
TEST_POLICY, eng, store, user_id
)?,
LinearStorageProvider<FileManager>::new(
FileManager::new(self.cfg.storage_path())
.context("unable to create `FileManager`")?,
),
);
}
NB: This is an abbreviated version of the daemon’s setup_aranya
method, see
here
for the current implementation details.
The daemon receives actions from the user via the user client API. When the client makes a call in the client library, it may invoke a command in the daemon using an internal API. For more details on this API, see the aranya-daemon-api crate.
Once the Aranya client has been created by setup_aranya
, the run
method
will instantiate a Syncer
for the device to be able to update their local
state of the DAG with its peers from the same team. In the process of syncing,
it will send a message, which holds some of the most recently seen state, to a
peer as a request to sync its local DAG with any missing commands that the peer
has. The Syncer
uses its sync
method which calls the Aranya client’s
sync_peer
method to send this request. Upon receiving the request, the peer
will compare these commands against their own version of state. If they have
seen new commands, the peer will respond with this new state. The Syncer
iterates over the list of peers and goes through this process at some
configured interval. Meanwhile, the Syncer
also listens for and responds to
incoming sync requests. This is all done automatically by the daemon once a
team ID has been configured. The full implementation of this struct can be found
here.
Before the client library can send data, the router component uses Fast
Channels to encrypt the data with the encryption key for that data’s label.
On startup, the daemon uses the setup_afc
method to initialize memory shared
by the daemon to write and the client to access channel keys used to
encrypt and decrypt Fast Channels data.
Before the client library can send data, the router component uses Fast Channels to encrypt the data with the encryption key for that data’s label. On the other side of the channel, the peer’s router receives the traffic on its external network socket and uses Fast Channels to decrypt the data with the key corresponding to the data’s label. The keys used to encrypt and decrypt are accessed by the client in the shared memory initialized by the daemon. The router component then forwards the data as plaintext to the user’s application.
Now that Aranya and Fast Channels are running, the daemon is ready to submit actions!
All Aranya operations, except for syncing, require users to join a team first. There are two ways a user may join a team: creating a team or being added to one. We will walk through each of these, first creating the team and then adding users.
To create a team, the first user submits a create_team
action which will
initialize a new graph for the team to operate on. This user is automatically
assigned the Owner
role as part of the command.
let client = Client::connect(owner_sock_path)?;
let team_id = client.create_team()?;
The following snippet has been modified for simplicity. To see actual usage, see the C example.
// have owner create the team.
err = aranya_create_team(&team->clients.owner.client, &team->id);
EXPECT("error creating team", err);
NB: the team ID should be stored as it will be used for updating the team later in the walkthrough.
This will cause Owner
’s daemon application to invoke the following action:
aranya_client.create_team()
A CreateTeam
command is submitted on behalf of the first user to the daemon
to be processed by Aranya. If valid, the command will be added to the user’s
graph of commands (i.e., their DAG), as the first node (or root) of the graph,
and returns to the user the team ID that uniquely identifies the team they
created. Additionally, a fact will be stored that associates Owner
in the new
team with the Owner
role. The team has now been created and the Owner
can
add peers to sync with.
Peers are added to sync with using the client’s add_sync_peer
command. It
takes in the peer’s external network address that the daemon can communicate
with. This method also takes in a time interval that tells the daemon how
often to attempt syncing with that peer. All team members must call the
add_sync_peer
method for each team member in order to sync state with the
rest of the team.
// Create an instance of a Team API to add sync peers.
let mut team = client.team(team_id);
let interval = Duration::from_millis(100);
let admin_sync_addr = "127.0.0.1:10002";
let operator_sync_addr = "127.0.0.1:10003";
let member_a_sync_addr = "127.0.0.1:10004";
let member_b_sync_addr = "127.0.0.1:10005";
team.add_sync_peer(admin_sync_addr, interval).await?;
team.add_sync_peer(operator_sync_addr, interval).await?;
team.add_sync_peer(member_a_sync_addr, interval).await?;
team.add_sync_peer(member_b_sync_addr, interval).await?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
const char *admin_sync_addr = "127.0.0.1:10002";
const char *operator_sync_addr = "127.0.0.1:10003";
const char *member_a_sync_addr = "127.0.0.1:10004";
const char *member_b_sync_addr = "127.0.0.1:10005";
const u64 interval = ARANYA_DURATION_MILLISECONDS * 100;
err = aranya_add_sync_peer(&team->owner.client, &team->id,
admin_sync_addr, interval);
EXPECT("Failed to add admin sync peer", err);
err = aranya_add_sync_peer(&team->owner.client, &team->id,
operator_sync_addr, interval);
EXPECT("Failed to add operator sync peer", err);
err = aranya_add_sync_peer(&team->owner.client, &team->id,
member_a_sync_addr, interval);
EXPECT("Failed to add member a sync peer", err);
err = aranya_add_sync_peer(&team->owner.client, &team->id,
member_b_sync_addr, interval);
EXPECT("Failed to add member b sync peer", err);
Now, the Owner
can start adding other team members!
To be added to the team, a user first needs to send the public portion of their
user keys, the user key bundle, to an existing user in the team. This key
exchange is done outside of the daemon using something like scp
. Further, the
existing user must have permission to add a user to the team. Based on the
implemented policy, all new users, except the Owner
, are added to the team
with the Member
role. Only users with the Owner
role or Operator
role may
add a new user to the team.
// Get the keybundle of the user that should be added as an admin:
let admin_kb = client.get_key_bundle()?;
// Send the keybundle to the Owner
// ...
The following snippet has been modified for simplicity. See the actual usage in the C example.
err = aranya_get_key_bundle(&owner_client->client, &owner_client->pk);
CLIENT_EXPECT("error getting key bundle", owner_client->name, err);
Let’s assume Owner
has received the second user’s keys and can add them to
the team and assign the Admin
role. This involves two commands, AddMember
and AssignAdmin
. The first is published by the add_member
action which adds
the second user to the team and the second is published by the assign_role
action which assigns the passed in role. In this case, the Owner
is assigning
the Admin
role, so an AssignAdmin
command will be added to the graph. The
daemon’s add_device_to_team
method submits the add_member
action and the
assign_role
method submits the assign_role
action:
// Create an instance of a Team API to add the admin using
// the ID of the team.
let team = client.team(team_id);
team.add_device_to_team(admin_kb)?;
team.assign_role(admin_device_id, Role::Admin)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// add admin to team.
err = aranya_add_device_to_team(&team->clients.owner.client, &team->id,
&team->clients.admin.pk);
EXPECT("error adding admin to team", err);
// upgrade role to admin.
err = aranya_assign_role(&team->clients.owner.client, &team->id,
&team->clients.admin.id, ARANYA_ROLE_ADMIN);
EXPECT("error assigning admin role", err);
If processed successfully, new AddMember
and AssignAdmin
commands will be
added to the graph and associates Admin to the team and their role.
NB: Remember that users must process commands locally before they can act
upon them. Thus, Admin
must sync with a peer to receive the commands Owner
performed to onboard them onto the team before they can perform any commands
themself. The Admin
can begin syncing with peers using the add_sync_peer
method described above. Remember, every user will have to add each team member
to sync with using this method.
Owner
can repeat these steps to add the rest of the users to the team. So,
after receiving the key bundle from the third, fourth, and fifth user, the
Owner
will perform the following:
team.add_device_to_team(operator_kb)?;
team.assign_role(operator_device_id, Role::Operator)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// add operator to team.
err = aranya_add_device_to_team(&team->clients.owner.client, &team->id,
&team->clients.operator.pk);
EXPECT("error adding operator to team", err);
// upgrade role to operator.
err = aranya_assign_role(&team->clients.owner.client, &team->id,
&team->clients.operator.id, ARANYA_ROLE_OPERATOR);
EXPECT("error assigning operator role", err);
This subcommand will submit two actions, add_member
and assign_operator
.
The first will add the user to the team as a Member
and the second will
assign them the Operator
role. The last two users will only be added to the
team as Member
s.
team.add_device_to_team(member_a_kb)?;
team.assign_role(member_a_device_id, Role::Member)?;
team.add_device_to_team(member_b_kb)?;
team.assign_role(member_b_device_id, Role::Member)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// add membera to team.
err = aranya_add_device_to_team(&team->clients.owner.client, &team->id,
&team->clients.membera.pk);
EXPECT("error adding membera to team", err);
// add memberb to team.
err = aranya_add_device_to_team(&team->clients.owner.client, &team->id,
&team->clients.memberb.pk);
EXPECT("error adding memberb to team", err);
If these actions are processed successfully, new commands exist on the graph that associate the team members with their newly assigned roles. Before the new team members can submit actions, they must retrieve the team ID (remember this happens externally) to sync state and receive the commands that have associated them with the team. Once associated with the team and assigned a role, the users can begin submitting actions!
Finally, network identifiers need to be assigned for the members that will use
Fast Channels. The network identifers are used by Fast Channels to properly
translate between network names and users. The Operator
will perform the next
actions:
let member_a_afc_addr = "127.0.0.1:11004";
let member_b_afc_addr = "127.0.0.1:11005";
operator_client.assign_net_identifier(member_a_device_id, member_a_afc_addr)?;
operator_client.assign_net_identifier(member_b_device_id, member_b_afc_addr)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
const char *member_a_afc_addr = "127.0.0.1:11004";
const char *member_b_afc_addr = "127.0.0.1:11005";
// assign AFC network addresses.
err = aranya_assign_net_identifier(&team->clients.operator.client, &team->id,
&team->clients.membera.id,
member_a_afc_addr);
EXPECT("error assigning net name to membera", err);
err = aranya_assign_net_identifier(&team->clients.operator.client, &team->id,
&team->clients.memberb.id,
member_b_afc_addr);
EXPECT("error assigning net name to memberb", err);
Now that all users have been added to the team, they can begin sending encrypted messages to each other, facilitated by Aranya Fast Channels. When using Fast Channels, messages are not stored on the graph and are only one to one between two users. We will walk through how users can send messages using each of these methods.
Aranya Fast Channels provides functionality for encrypted peer to peer
messaging via channels. This section will walk through a bidirectional channel
being set up between Member A
and Member B
.
As mentioned, a channel label must be created so it can be associated with the
users and channel. Based on the default policy, an Operator
can create a Fast
Channels label. So, Operator
, who was assigned the Operator
role, will
submit an action to the daemon to create the label.
// Have operator create the label using the team instance
// from the operator_client.team(team_id)
let label = 42;
team.create_label(label)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// operator creates AFC labels and assigns them to team members.
AranyaLabel label = 42;
err = aranya_create_label(&team->clients.operator.client, &team->id, label);
EXPECT("error creating afc label", err);
This is an Aranya command, so the daemon passes it into the policy to be processed. If it is successful, the label is stored as a fact on the graph.
Now that it exists, the label can be assigned to Member A
and Member B
.
Based on the default policy, the Operator
role can assign Fast Channels
labels. So, Operator
, who was assigned the Operator
role, will submit the
action to assign the label. If processed successfully, the Aranya command for
assigning a Fast Channels label will create a fact that associates the label,
the user’s user_id
, and a channel operation. Since this is a bidirectional
channel, each user will be given the ReadWrite
channel operation.
// Assign label (42) to Member A and Member B using the operator's
// team instance.
team.assign_label(member_a_device_id, label)?;
team.assign_label(member_b_device_id, label)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
err = aranya_assign_label(&team->clients.operator.client, &team->id,
&team->clients.membera.id, label);
EXPECT("error assigning afc label to membera", err);
err = aranya_assign_label(&team->clients.operator.client, &team->id,
&team->clients.memberb.id, label);
EXPECT("error assigning afc label to memberb", err);
Once these commands are submitted, they are processed by the policy. If found
valid, Member A
and Member B
are now both assigned the same label with the
ability to interact over a bidirectional channel.
The action for creating a bidirectional Fast Channel,
create_bidi_channel
, is called within an Aranya session to produce the
ephemeral command, CreateBidiChannel
, which contains the Fast Channels
channel keys.
Note: An ephemeral command is processed by Aranya but not added to the graph.
Now, Member A
or Member B
could create a Fast Channel.
let channel_id = member_a_client.create_bidi_channel(team_id, member_b_afc_addr, label_num)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// create AFC channel between membera and memberb.
AranyaChannelId chan_id;
err = aranya_create_bidi_channel(&team->clients.membera.client, &team->id,
member_b_afc_addr, label, &chan_id);
EXPECT("error creating afc channel", err);
The client library and daemon will handle the required communication to
transfer the ephemeral command to Member B
. Once the command is received by
the Router
on Member B
’s device it is then evaluated by the recipient’s
policy. If valid, the channel keys are stored in this user’s shared memory
database. At this point, the channel is created and can be used for messaging
between Member A
and Member B
.
To send a message, Member A
will call Fast Channels’s API, send_data
, with
the message data and the channel ID.
// Sample data, can be any bytes
let data = vec![1, 1, 2, 3, 5];
// Send the data to over the channel to member A. The client library handles
// the transport.
member_a_client.send_data(channel_id, data)?;
The following snippet has been modified for simplicity. See the actual usage in the C example.
// send AFC data.
const char *send = "hello world";
err = aranya_send_data(&team->clients.membera.client, chan_id,
(const uint8_t *)send, (int)strlen(send));
EXPECT("error sending data", err);
This Fast Channels command will use the channel’s encryption key to encrypt
the message and attach headers related to the contents. Member B
can now read
the message!
Since this channel is bidirectional, Member B
may also send a message to
Member A
. In this case, Member B
will submit a similar action with
the channel ID.
// Sample data, can be any bytes
let data = vec![8, 13, 21, 34, 55];
// Send the data to over the channel to member B. The client library handles
// the transport.
member_b_client.send_data(channel_id, data)?;
The following snippet has been modified for simplicity. See the actual usage in
the C example. Note the example does not
include Member B
sending a message to Member A
. However, the action will
look similar to the linked usage, but Member B
’s client would be passed in
instead.
// send AFC data.
const char *send = "hello world";
err = aranya_send_data(&team->clients.memberb.client, chan_id,
(const uint8_t *)send, (int)strlen(send));
Great job, you’ve now successfully stood up Aranya daemons, created an Aranya team, and sent messages using Aranya Fast Channels!