OpenMLS Traits

⚠️ These traits are responsible for all cryptographic operations and randomness within OpenMLS. Please ensure you know what you're doing when implementing your own versions.

Because implementing the OpenMLSCryptoProvider is challenging, requires tremendous care, and is not what the average OpenMLS consumer wants to (or should) do, we provide two implementations that can be used.

Rust Crypto Provider The go-to default at the moment is an implementation using commonly used, native Rust crypto implementations.

Libcrux Crypto Provider A crypto provider backed by the high-assurance cryptography library [libcrux]. Currently only supports relatively modern x86 and amd64 CPUs, as it requires AES-NI, SIMD and AVX.

The Traits

There are 4 different traits defined in the OpenMLS traits crate.

OpenMlsRand

This trait defines two functions to generate arrays and vectors, and is used by OpenMLS to generate randomness for key generation and random identifiers. While there is the commonly used rand crate, not all implementations use it. OpenMLS, therefore, defines its own randomness trait that needs to be implemented by an OpenMLS crypto provider. It simply needs to implement two functions to generate cryptographically secure randomness and store it in an array or vector.

pub trait OpenMlsRand {
    type Error: std::error::Error + Debug;

    /// Fill an array with random bytes.
    fn random_array<const N: usize>(&self) -> Result<[u8; N], Self::Error>;

    /// Fill a vector of length `len` with bytes.
    fn random_vec(&self, len: usize) -> Result<Vec<u8>, Self::Error>;
}

OpenMlsCrypto

This trait defines all cryptographic functions required by OpenMLS. In particular:

  • HKDF
  • Hashing
  • AEAD
  • Signatures
  • HPKE
};

StorageProvider

This trait defines an API for a storage backend that is used for all OpenMLS persistence.

The store provides functions to store, read, and delete values. Note that it does not allow updating values. Instead, entries must be deleted and newly stored.

/// StorageProvider describes the storage backing OpenMLS and persists the state of OpenMLS groups.
///
/// The getters for individual values usually return a `Result<Option<T>, E>`, where `Err(_)`
/// indicates that some sort of IO or internal error occurred, and `Ok(None)` indicates that no
/// error occurred, but no value exists.
/// Many getters for lists return a `Result<Vec<T>, E>`. In this case, if there was no error but
/// the value doesn't exist, an empty vector should be returned.
///
/// More details can be taken from the comments on the respective method.
pub trait StorageProvider<const VERSION: u16> {

The trait is generic over a VERSION, which is used to ensure that the values that are persisted can be upgraded when OpenMLS changes the stored structs.

Every function takes Key and Value arguments.

/// Key is a trait implemented by all types that serve as a key (in the database sense) to in the
/// storage. For example, a GroupId is a key to the stored entities for the group with that id.
/// The point of a key is not to be stored, it's to address something that is stored.
pub trait Key<const VERSION: u16>: Serialize {}
/// Entity is a trait implemented by the values being stored.
pub trait Entity<const VERSION: u16>: Serialize + DeserializeOwned {}

To ensure that each function takes the correct input, they use trait bounds. These are the available traits.

/// Each trait in this module corresponds to a type. Some are used as keys, some as
/// entities, and some both. Therefore, the Key and/or Entity traits also need to be implemented.
pub mod traits {
    use super::{Entity, Key};

    // traits for keys, one per data type
    pub trait GroupId<const VERSION: u16>: Key<VERSION> {}
    pub trait SignaturePublicKey<const VERSION: u16>: Key<VERSION> {}
    pub trait HashReference<const VERSION: u16>: Key<VERSION> {}
    pub trait PskId<const VERSION: u16>: Key<VERSION> {}
    pub trait EncryptionKey<const VERSION: u16>: Key<VERSION> {}
    pub trait EpochKey<const VERSION: u16>: Key<VERSION> {}

    // traits for entity, one per type
    pub trait QueuedProposal<const VERSION: u16>: Entity<VERSION> {}
    pub trait TreeSync<const VERSION: u16>: Entity<VERSION> {}
    pub trait GroupContext<const VERSION: u16>: Entity<VERSION> {}
    pub trait InterimTranscriptHash<const VERSION: u16>: Entity<VERSION> {}
    pub trait ConfirmationTag<const VERSION: u16>: Entity<VERSION> {}
    pub trait SignatureKeyPair<const VERSION: u16>: Entity<VERSION> {}
    pub trait PskBundle<const VERSION: u16>: Entity<VERSION> {}
    pub trait HpkeKeyPair<const VERSION: u16>: Entity<VERSION> {}
    pub trait GroupState<const VERSION: u16>: Entity<VERSION> {}
    pub trait GroupEpochSecrets<const VERSION: u16>: Entity<VERSION> {}
    pub trait LeafNodeIndex<const VERSION: u16>: Entity<VERSION> {}
    pub trait GroupUseRatchetTreeExtension<const VERSION: u16>: Entity<VERSION> {}
    pub trait MessageSecrets<const VERSION: u16>: Entity<VERSION> {}
    pub trait ResumptionPskStore<const VERSION: u16>: Entity<VERSION> {}
    pub trait KeyPackage<const VERSION: u16>: Entity<VERSION> {}
    pub trait MlsGroupJoinConfig<const VERSION: u16>: Entity<VERSION> {}
    pub trait LeafNode<const VERSION: u16>: Entity<VERSION> {}

    // traits for types that implement both
    pub trait ProposalRef<const VERSION: u16>: Entity<VERSION> + Key<VERSION> {}
}

An implementation of the storage trait should ensure that it can address and efficiently handle values.

Example: Key packages

This is only an example, but it illustrates that the application may need to do more when it comes to implementing storage.

Key packages are only deleted by OpenMLS when they are used and not last resort key packages (which may be used multiple times). The application needs to implement some logic to manage last resort key packages.

    fn write_key_package<
        HashReference: traits::HashReference<VERSION>,
        KeyPackage: traits::KeyPackage<VERSION>,
    >(
        &self,
        hash_ref: &HashReference,
        key_package: &KeyPackage,
    ) -> Result<(), Self::Error>;

The application may store the hash references in a separate list with a validity period.

fn write_key_package<
    HashReference: traits::HashReference<VERSION>,
    KeyPackage: traits::KeyPackage<VERSION>,
>(
    &self,
    hash_ref: &HashReference,
    key_package: &KeyPackage,
) -> Result<(), Self::Error> {
    // Get the validity from the application in some way.
    let validity = self.get_validity(hash_ref);

    // Store the reference and its validity period.
    self.store_hash_ref(hash_ref, validity);

    // Store the actual key package.
    self.store_key_package(hash_ref, key_package);
}

This allows the application to iterate over the hash references and delete outdated key packages.

OpenMlsCryptoProvider

Additionally, there's a wrapper trait defined that is expected to be passed into the public OpenMLS API. Some OpenMLS APIs require only one of the sub-traits, though.

    pub use super::random::OpenMlsRand as _;
    pub use super::signatures::Signer as _;
    pub use super::storage::StorageProvider as _;
    pub use super::types as openmls_types;
    pub use super::OpenMlsProvider as _;
}

/// The OpenMLS Crypto Provider Trait
///
/// An implementation of this trait must be passed in to the public OpenMLS API
/// to perform randomness generation, cryptographic operations, and key storage.
pub trait OpenMlsProvider {
    type CryptoProvider: crypto::OpenMlsCrypto;
    type RandProvider: random::OpenMlsRand;

Implementation Notes

It is not necessary to implement all sub-traits if one functionality is missing. Suppose you want to use a persisting key store. In that case, it is sufficient to do a new implementation of the key store trait and combine it with one of the provided crypto and randomness trait implementations.