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 for reading and updating stored values. Each sort of value has separate methods for accessing or mutating the state. In order to decouple the provider from the OpenMLS implementation, while still having legible types at the provider, there are traits that mirror all the types stored by OpenMLS. The provider methods use values constrained by these traits as as arguments.

/// 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 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> {}
}

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

The traits used as arguments to the storage methods are constrained to implement the Key or Entity traits as well, depending on whether they are only used for addressing (in which case they are a Key) or whether they represent a stored value (in which case they are an Entity).

/// 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 {}

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.

OpenMlsProvider

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 trait OpenMlsProvider {
    type CryptoProvider: crypto::OpenMlsCrypto;
    type RandProvider: random::OpenMlsRand;
    type StorageProvider: storage::StorageProvider<{ storage::CURRENT_VERSION }>;

    // Get the storage provider.
    fn storage(&self) -> &Self::StorageProvider;

    /// Get the crypto provider.
    fn crypto(&self) -> &Self::CryptoProvider;

    /// Get the randomness provider.
    fn rand(&self) -> &Self::RandProvider;
}

Implementation Notes

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