awslabs / mls-rs

An implementation of Messaging Layer Security (RFC 9420)
Apache License 2.0
99 stars 19 forks source link

[WIP] [1.x] Externalize KeyPackageStorage and update join and write to storage generation API #209

Open mulmarta opened 4 hours ago

mulmarta commented 4 hours ago

Background:

As laid out in #207, we want to externalize all storage objects that interact with mls-rs so internal functionality is not directly dependent on a user-provided storage mechanism.

Description of feature:

Key Package Storage

Currently when a Client joins a group with Client::join_group, it will retrieve the private key that corresponds with the key package that was used to add it to the group. Later when the created Group is saved with Group::write_to_storage, it will delete that private key in the KeyPackageStorage implementation.

Before (0.x)

Join Group API

// Make a key package store that conforms to the KeyPackageStorage trait
let key_package_store = MyKeyPackageStore::new();

let client = Client::builder() 
        .key_package_repo(key_package_store) // Transfer the ownership of the key package repo to a client via the ClientBuilder
        ....
        .build();

// Join a group.
let (group, new_member_info) = client.join_group(tree_data, welcome_message).unwrap();

In the above, join_group internally finds the key package private key by calling KeyPackageStorage::get on (a clone of) the key_package_store with all key package references included in the welcome_message.

Write to Storage API

// Join a group
let (group, new_member_info) = client.join_group(tree_data, welcome_message).unwrap();

// Store state of the joined group.
group.write_to_storage().unwrap();

In the above, write_to_storage internally deletes the key package private key used to join by calling KeyPackageStorage::delete on (a clone of) the key_package_store owned by client.

After (1.x)

Join Group API

Client joins a group in three steps. First, it parses the Welcome message which returns information needed to fetch the private key from the storage. The same function will be used to parse other MLSMessage types like Commit, Proposal. Second, Client retrieves the private key and, third, it joins using the private key.

// Make a key package store that conforms to the KeyPackageStorage trait
let key_package_store = MyKeyPackageStore::new();

let client = Client::builder() 
        .... // No key package specific configuration 
        .build();

// Parse the Welcome message
let parsed_message = client.parse_message(welcome_message);

let ParsedMessage::Welcome {
    key_package_refs, // List of key package refs found in the message
    cipher_suite,
} = parsed_message
else {
    // Handle the case where this is not a Welcome message
};

// Independently retrieve key package private key
let private_kp_data = key_package_store.get(key_package_refs, cipher_suite).unwrap();

// Join group
let (group, new_member_info) = client.join_group(tree_data, welcome_message, private_kp_data).unwrap();

Write to Storage API

Instead of calling various storage interfaces internally, the write_to_storage function outputs a diff between the last write and the current state, which is used by the application to update the storage. Here we focus on key packages.

// Join a group
let (group, new_member_info) = client.join_group(tree_data, welcome_message).unwrap();

// Export a diff between the current group state and the last group state written to storage
let group_state_diff = group.write_to_storage().unwrap();

// The diff contains (group state update and) a possibly a key package to delete.
if let Some(key_package_ref) = group_state_diff.key_package_to_delete {
    key_package_store.delete(key_package_ref).unwrap();
}
mgeisler commented 3 hours ago

Thanks for the write-up, Marta! This looks very nice to me.

I believe that this will overall make all method calls to Client and Group be "idempotent" (if that's the right word?)? That is, if I call it Client::join_group twice with the same arguments, I get the same (group, new_member_info) values back?

(Module any randomness that might be picked when the values are created, but semantically the values would compare equal after two calls.)