use openmls_traits::{
crypto::OpenMlsCrypto,
types::{Ciphersuite, CryptoError},
OpenMlsProvider,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tls_codec::{
DeserializeBytes, SecretVLByteVec, Serialize as _, Size as _, TlsDeserializeBytes,
TlsSerialize, TlsSize, VLByteSlice, VLByteVec,
};
use crate::{
binary_tree::{array_representation::TreeSize, LeafNodeIndex},
ciphersuite::{hash_ref::KeyPackageRef, Secret},
group::{GroupEpoch, GroupId},
messages::PathSecret,
treesync::node::encryption_keys::EncryptionKeyPair,
};
pub const VC_COMPONENT_ID: u16 = 0x667A;
const ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
const PATH_GENERATION_LABEL: &str = "Path Generation";
const INIT_KEY_LABEL: &str = "Init Key";
const KEY_PACKAGE_SEED_LABEL: &str = "vc key package seed";
const TARGET_OPERATION_LABEL: &str = "vc target operation";
const GROUP_CREATION_LABEL: &str = "Group Creation";
const DERIVATION_INFO_KEY_LABEL: &str = "key";
const DERIVATION_INFO_NONCE_LABEL: &str = "nonce";
const EPOCH_ID_LABEL: &str = "Epoch ID";
const EPOCH_ENCRYPTION_KEY_LABEL: &str = "Encryption Key";
const EPOCH_BASE_SECRET_LABEL: &str = "Base Secret";
const REUSE_GUARD_LABEL: &str = "Reuse Guard";
const GENERATION_ID_LABEL: &str = "Generation ID Secret";
const GENERATION_ID_EXPAND_LABEL: &str = "generation id";
const REUSE_GUARD_PRP_KEY_LABEL: &str = "reuse guard";
const PRP_KEY_LEN: usize = 16;
#[derive(Error, Debug, PartialEq, Clone)]
pub enum VirtualClientsError {
#[error("Failed to deserialize derivation info.")]
DerivationInfoMalformed,
#[error("Failed to decrypt derivation info.")]
DerivationInfoDecryptionFailed,
#[error("No virtual-clients operation secret tree for this epoch.")]
MissingOperationTree,
#[error("No virtual-clients emulation-epoch state for this epoch.")]
MissingEmulationEpochState,
#[error("Virtual-clients storage error")]
StorageError,
#[error("Leaf encryption key from path does not match the derived key.")]
EncryptionKeyMismatch,
#[error("Cryptographic operation failed.")]
CryptoError(#[from] CryptoError),
#[error(
"Hash function produced output of length {actual_length}, expected {expected_length}."
)]
HashOutputLengthMismatch {
actual_length: usize,
expected_length: usize,
},
#[error("TLS codec error: {0}")]
Tls(#[from] tls_codec::Error),
#[error("Leaf does not declare AppDataDictionary support in its capabilities.")]
AppDataDictionaryNotSupported,
#[error("Leaf's AppComponents entry does not list the virtual-clients component id.")]
VcComponentNotListed,
#[error("Leaf index is outside the operation secret tree.")]
IndexOutOfBounds,
#[error("The operation secret for this generation was already consumed.")]
OperationGenerationConsumed,
#[error("The requested operation generation is too far beyond the ratchet head.")]
OperationGenerationTooDistant,
#[error("Operation ratchet generation has reached `u32::MAX`.")]
OperationRatchetTooLong,
#[error("An unrecoverable error has occurred due to a bug in the implementation.")]
LibraryError,
#[error("KeyPackageUpload contains a duplicate key_package_index: {0}.")]
DuplicateKeyPackageIndex(u32),
#[error("KeyPackageUpload contains a duplicate KeyPackageRef.")]
DuplicateKeyPackageRef,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct EmulatorEpochSecret(Secret);
impl EmulatorEpochSecret {
pub(crate) fn new(bytes: &[u8]) -> Self {
Self(Secret::from_slice(bytes))
}
pub(crate) fn derive_epoch_id(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EpochId, VirtualClientsError> {
let secret = self.0.derive_secret(crypto, ciphersuite, EPOCH_ID_LABEL)?;
Ok(EpochId(secret.as_slice().to_vec().into()))
}
pub(crate) fn derive_epoch_encryption_key(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EpochEncryptionKey, VirtualClientsError> {
let secret = self
.0
.derive_secret(crypto, ciphersuite, EPOCH_ENCRYPTION_KEY_LABEL)?;
Ok(EpochEncryptionKey(secret))
}
pub(crate) fn derive_epoch_base_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<Secret, VirtualClientsError> {
Ok(self
.0
.derive_secret(crypto, ciphersuite, EPOCH_BASE_SECRET_LABEL)?)
}
pub(crate) fn derive_reuse_guard_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<ReuseGuardSecret, VirtualClientsError> {
let secret = self
.0
.derive_secret(crypto, ciphersuite, REUSE_GUARD_LABEL)?;
Ok(ReuseGuardSecret(secret))
}
pub(crate) fn derive_generation_id_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<GenerationIdSecret, VirtualClientsError> {
let secret = self
.0
.derive_secret(crypto, ciphersuite, GENERATION_ID_LABEL)?;
Ok(GenerationIdSecret(secret))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ReuseGuardSecret(Secret);
impl ReuseGuardSecret {
#[cfg(test)]
pub(crate) fn from_secret_for_tests(secret: Secret) -> Self {
Self(secret)
}
pub(crate) fn derive_prp_key(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
key_schedule_nonce: &[u8],
) -> Result<[u8; PRP_KEY_LEN], VirtualClientsError> {
let key = self.0.kdf_expand_label(
crypto,
ciphersuite,
REUSE_GUARD_PRP_KEY_LABEL,
key_schedule_nonce,
PRP_KEY_LEN,
)?;
key.as_slice()
.try_into()
.map_err(|_| VirtualClientsError::HashOutputLengthMismatch {
actual_length: key.as_slice().len(),
expected_length: PRP_KEY_LEN,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct GenerationIdSecret(Secret);
impl GenerationIdSecret {
fn derive_generation_id(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
context: &PrivateMessageContext<'_>,
) -> Result<GenerationId, VirtualClientsError> {
let context_bytes = context.tls_serialize_detached()?;
let generation_id = self.0.kdf_expand_label(
crypto,
ciphersuite,
GENERATION_ID_EXPAND_LABEL,
&context_bytes,
ciphersuite.hash_length(),
)?;
Ok(GenerationId(generation_id.as_slice().to_vec().into()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize)]
#[repr(u8)]
pub enum RatchetType {
Application = 1,
Handshake = 2,
}
#[derive(Debug, TlsSize, TlsSerialize)]
pub(crate) struct PrivateMessageContext<'a> {
group_id: VLByteSlice<'a>,
epoch: u64,
generation: u32,
ratchet_type: RatchetType,
}
#[derive(Debug, Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
pub struct GenerationId(VLByteVec);
impl GenerationId {
pub fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
}
#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
pub(crate) struct DerivationInfo {
epoch_id: EpochId,
ciphertext: VLByteVec,
}
impl DerivationInfo {
pub(crate) fn encrypt(
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
key: &EpochEncryptionKey,
epoch_id: EpochId,
leaf_encryption_key: &[u8],
tbe: &DerivationInfoTbe,
) -> Result<Self, VirtualClientsError> {
let (aead_key, aead_nonce) =
key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
let payload = tbe.tls_serialize_detached()?;
let ciphertext = crypto.aead_encrypt(
ciphersuite.aead_algorithm(),
aead_key.as_slice(),
payload.as_slice(),
aead_nonce.as_slice(),
epoch_id.0.as_slice(),
)?;
Ok(Self {
epoch_id,
ciphertext: ciphertext.into(),
})
}
pub(crate) fn epoch_id(&self) -> &EpochId {
&self.epoch_id
}
pub(crate) fn decrypt(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
key: &EpochEncryptionKey,
leaf_encryption_key: &[u8],
operation_type: VirtualClientOperationType,
) -> Result<DerivationInfoTbe, VirtualClientsError> {
let (aead_key, aead_nonce) =
key.derive_key_nonce(crypto, ciphersuite, leaf_encryption_key)?;
let plaintext = crypto
.aead_decrypt(
ciphersuite.aead_algorithm(),
aead_key.as_slice(),
self.ciphertext.as_slice(),
aead_nonce.as_slice(),
self.epoch_id.0.as_slice(),
)
.map_err(|e| {
log::error!("vc: aead decrypt derivation info failed: {e:?}");
VirtualClientsError::DerivationInfoDecryptionFailed
})?;
DerivationInfoTbe::deserialize_for_operation(&plaintext, operation_type)
}
}
#[derive(
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TlsSize, TlsSerialize, TlsDeserializeBytes,
)]
pub struct EpochId(VLByteVec);
impl EpochId {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes.into())
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
}
#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
pub struct KeyPackageInfo {
pub key_package_ref: KeyPackageRef,
pub cipher_suite: Ciphersuite,
pub key_package_index: u32,
}
#[derive(Debug, TlsSize, TlsSerialize, TlsDeserializeBytes)]
pub struct KeyPackageUpload {
pub epoch_id: EpochId,
pub leaf_index: LeafNodeIndex,
pub generation: u32,
pub key_package_info: Vec<KeyPackageInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RetainedKeyPackageMaterial {
pub epoch_id: EpochId,
pub leaf_index: LeafNodeIndex,
pub generation: u32,
pub key_package_ciphersuite: Ciphersuite,
pub key_package_index: u32,
pub key_package_seed_secret: KeyPackageSeedSecret,
}
fn validate_key_package_infos(infos: &[KeyPackageInfo]) -> Result<(), VirtualClientsError> {
let mut seen_indices = std::collections::BTreeSet::new();
let mut seen_refs = std::collections::BTreeSet::new();
for info in infos {
if !seen_indices.insert(info.key_package_index) {
return Err(VirtualClientsError::DuplicateKeyPackageIndex(
info.key_package_index,
));
}
if !seen_refs.insert(&info.key_package_ref) {
return Err(VirtualClientsError::DuplicateKeyPackageRef);
}
}
Ok(())
}
pub fn assemble_vc_key_package_upload<Storage: crate::storage::StorageProvider>(
storage: &Storage,
epoch_id: EpochId,
generation: u32,
key_package_info: Vec<KeyPackageInfo>,
) -> Result<KeyPackageUpload, VirtualClientsError> {
validate_key_package_infos(&key_package_info)?;
let state: EmulationEpochState = storage
.vc_emulation_epoch_state(&epoch_id)
.map_err(|e| {
log::error!("vc: load emulation epoch state in assemble upload failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingEmulationEpochState)?;
Ok(KeyPackageUpload {
epoch_id,
leaf_index: state.leaf_index,
generation,
key_package_info,
})
}
pub fn process_vc_key_package_upload<Provider: OpenMlsProvider>(
provider: &Provider,
upload: &KeyPackageUpload,
) -> Result<(), VirtualClientsError> {
use crate::components::vc_operation_tree::OperationSecretTree;
use openmls_traits::storage::StorageProvider as _;
validate_key_package_infos(&upload.key_package_info)?;
let storage = provider.storage();
let crypto = provider.crypto();
let state: EmulationEpochState = storage
.vc_emulation_epoch_state(&upload.epoch_id)
.map_err(|e| {
log::error!("vc: load emulation epoch state in process upload failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingEmulationEpochState)?;
let mut operation_tree: OperationSecretTree = storage
.vc_operation_tree(&upload.epoch_id)
.map_err(|e| {
log::error!("vc: load operation tree in process upload failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingOperationTree)?;
let emulation_ciphersuite = state.emulation_ciphersuite;
let operation_secret = operation_tree.derive_operation_secret(
crypto,
emulation_ciphersuite,
&upload.epoch_id,
upload.leaf_index,
VirtualClientOperationType::KeyPackage,
upload.generation,
b"",
)?;
let mut materials = Vec::with_capacity(upload.key_package_info.len());
for info in &upload.key_package_info {
let key_package_seed_secret = operation_secret.derive_key_package_seed_secret(
crypto,
info.cipher_suite,
info.key_package_index,
)?;
let material = RetainedKeyPackageMaterial {
epoch_id: upload.epoch_id.clone(),
leaf_index: upload.leaf_index,
generation: upload.generation,
key_package_ciphersuite: info.cipher_suite,
key_package_index: info.key_package_index,
key_package_seed_secret,
};
materials.push((info.key_package_ref.clone(), material));
}
storage
.write_retained_key_package_material_batch(&upload.epoch_id, &operation_tree, &materials)
.map_err(|e| {
log::error!("vc: persist batch key package material in process upload failed: {e:?}");
VirtualClientsError::StorageError
})?;
Ok(())
}
#[derive(Debug)]
pub(crate) struct VcWelcomeMaterial {
pub(crate) key_package_ref: KeyPackageRef,
pub(crate) epoch_id: EpochId,
pub(crate) leaf_index: LeafNodeIndex,
pub(crate) generation: u32,
pub(crate) key_package_index: u32,
pub(crate) init_private_key: openmls_traits::types::HpkePrivateKey,
pub(crate) encryption_keypair: EncryptionKeyPair,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct RegisteredVcEmulationEpoch {
pub(crate) group_epoch: crate::group::GroupEpoch,
pub(crate) epoch_id: EpochId,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct VcEmulationBindings {
bindings: std::collections::VecDeque<(crate::group::GroupEpoch, EpochId)>,
}
impl VcEmulationBindings {
pub fn get(&self, epoch: crate::group::GroupEpoch) -> Option<&EpochId> {
for (bound_epoch, epoch_id) in &self.bindings {
if *bound_epoch == epoch {
return Some(epoch_id);
}
}
None
}
pub(crate) fn insert(
&mut self,
epoch: crate::group::GroupEpoch,
epoch_id: EpochId,
max_entries: usize,
) {
self.bindings
.retain(|(bound_epoch, _)| *bound_epoch != epoch);
self.bindings.push_back((epoch, epoch_id));
while self.bindings.len() > max_entries {
self.bindings.pop_front();
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct EpochEncryptionKey(Secret);
impl EpochEncryptionKey {
fn derive_key_nonce(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
leaf_encryption_key: &[u8],
) -> Result<(Secret, Secret), VirtualClientsError> {
let key = self.0.kdf_expand_label(
crypto,
ciphersuite,
DERIVATION_INFO_KEY_LABEL,
leaf_encryption_key,
ciphersuite.aead_key_length(),
)?;
let nonce = self.0.kdf_expand_label(
crypto,
ciphersuite,
DERIVATION_INFO_NONCE_LABEL,
leaf_encryption_key,
ciphersuite.aead_nonce_length(),
)?;
Ok((key, nonce))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmulationEpochState {
pub(crate) leaf_index: LeafNodeIndex,
pub(crate) epoch_encryption_key: EpochEncryptionKey,
pub(crate) reuse_guard_secret: ReuseGuardSecret,
pub(crate) generation_id_secret: GenerationIdSecret,
pub(crate) emulation_group_size: TreeSize,
pub(crate) emulation_ciphersuite: Ciphersuite,
}
impl EmulationEpochState {
pub(crate) fn new(
leaf_index: LeafNodeIndex,
epoch_encryption_key: EpochEncryptionKey,
reuse_guard_secret: ReuseGuardSecret,
generation_id_secret: GenerationIdSecret,
emulation_group_size: TreeSize,
emulation_ciphersuite: Ciphersuite,
) -> Self {
Self {
leaf_index,
epoch_encryption_key,
reuse_guard_secret,
generation_id_secret,
emulation_group_size,
emulation_ciphersuite,
}
}
pub(crate) fn into_parts(self) -> (LeafNodeIndex, EpochEncryptionKey, Ciphersuite) {
(
self.leaf_index,
self.epoch_encryption_key,
self.emulation_ciphersuite,
)
}
pub(crate) fn derive_generation_id(
&self,
crypto: &impl OpenMlsCrypto,
group_id: &GroupId,
epoch: GroupEpoch,
generation: u32,
ratchet_type: RatchetType,
) -> Result<GenerationId, VirtualClientsError> {
let context = PrivateMessageContext {
group_id: VLByteSlice(group_id.as_slice()),
epoch: epoch.as_u64(),
generation,
ratchet_type,
};
self.generation_id_secret
.derive_generation_id(crypto, self.emulation_ciphersuite, &context)
}
pub(crate) fn reuse_guard_inputs(&self) -> crate::framing::EmulatorReuseGuardCtx<'_> {
crate::framing::EmulatorReuseGuardCtx {
reuse_guard_secret: &self.reuse_guard_secret,
emulation_ciphersuite: self.emulation_ciphersuite,
emulation_group_size: self.emulation_group_size,
emulation_leaf_index: self.leaf_index,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OperationSecret(Secret);
impl From<Secret> for OperationSecret {
fn from(secret: Secret) -> Self {
Self(secret)
}
}
fn import_secret(
crypto: &impl OpenMlsCrypto,
target_ciphersuite: Ciphersuite,
source_secret: &Secret,
label: &str,
context: &[u8],
) -> Result<Secret, CryptoError> {
let salt = Secret::from_slice(&[]);
let target_prk = salt.hkdf_extract(crypto, target_ciphersuite, source_secret)?;
target_prk.kdf_expand_label(
crypto,
target_ciphersuite,
label,
context,
target_ciphersuite.hash_length(),
)
}
impl OperationSecret {
#[cfg(test)]
pub(crate) fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
pub(crate) fn derive_target_operation_secret(
&self,
crypto: &impl OpenMlsCrypto,
target_ciphersuite: Ciphersuite,
group_id: &GroupId,
) -> Result<TargetOperationSecret, VirtualClientsError> {
let context = TargetOperationContext {
cipher_suite: target_ciphersuite,
group_id: VLByteSlice(group_id.as_slice()),
}
.tls_serialize_detached()?;
let secret = import_secret(
crypto,
target_ciphersuite,
&self.0,
TARGET_OPERATION_LABEL,
&context,
)?;
Ok(TargetOperationSecret(secret))
}
pub(crate) fn derive_key_package_seed_secret(
&self,
crypto: &impl OpenMlsCrypto,
target_ciphersuite: Ciphersuite,
key_package_index: u32,
) -> Result<KeyPackageSeedSecret, VirtualClientsError> {
let context = KeyPackageSeedContext {
cipher_suite: target_ciphersuite,
key_package_index,
}
.tls_serialize_detached()?;
let seed = import_secret(
crypto,
target_ciphersuite,
&self.0,
KEY_PACKAGE_SEED_LABEL,
&context,
)?;
Ok(KeyPackageSeedSecret(seed))
}
}
#[derive(Debug, TlsSize, TlsSerialize)]
struct KeyPackageSeedContext {
cipher_suite: Ciphersuite,
key_package_index: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeyPackageSeedSecret(Secret);
impl KeyPackageSeedSecret {
pub(crate) fn derive_init_key_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<InitKeySecret, VirtualClientsError> {
let init_key_secret = self.0.derive_secret(crypto, ciphersuite, INIT_KEY_LABEL)?;
Ok(InitKeySecret(init_key_secret))
}
pub(crate) fn derive_encryption_key_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EncryptionKeySecret, VirtualClientsError> {
let encryption_key_secret =
self.0
.derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
Ok(EncryptionKeySecret(encryption_key_secret))
}
pub(crate) fn derive_group_creation_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<Secret, VirtualClientsError> {
Ok(self
.0
.derive_secret(crypto, ciphersuite, GROUP_CREATION_LABEL)?)
}
}
pub(crate) struct EncryptionKeySecret(Secret);
impl EncryptionKeySecret {
pub(crate) fn generate_encryption_key_pair(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EncryptionKeyPair, VirtualClientsError> {
let hpke_config = ciphersuite.hpke_config();
let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
Ok(EncryptionKeyPair::from(key_pair))
}
}
pub(crate) struct InitKeySecret(Secret);
impl InitKeySecret {
pub(crate) fn generate_init_key_pair(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<openmls_traits::types::HpkeKeyPair, VirtualClientsError> {
let hpke_config = ciphersuite.hpke_config();
let key_pair = crypto.derive_hpke_keypair(hpke_config, self.0.as_slice())?;
Ok(key_pair)
}
}
pub(crate) struct PathGenerationSecret(Secret);
impl From<PathGenerationSecret> for PathSecret {
fn from(value: PathGenerationSecret) -> Self {
value.0.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
#[repr(u8)]
pub enum VirtualClientOperationType {
KeyPackage = 1,
LeafNode = 2,
Application = 3,
}
#[derive(Clone, PartialEq, Eq, TlsSize, TlsSerialize, TlsDeserializeBytes)]
pub(crate) struct ExternalInitSecret(SecretVLByteVec);
impl std::fmt::Debug for ExternalInitSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalInitSecret")
.field("init_secret", &"<redacted>")
.finish()
}
}
impl ExternalInitSecret {
pub(crate) fn from_slice(bytes: &[u8]) -> Self {
Self(bytes.to_vec().into())
}
pub(crate) fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
}
#[derive(Debug, TlsSize, TlsSerialize)]
struct TargetOperationContext<'a> {
cipher_suite: Ciphersuite,
group_id: VLByteSlice<'a>,
}
#[derive(Debug)]
pub(crate) struct TargetOperationSecret(Secret);
impl TargetOperationSecret {
pub(crate) fn derive_encryption_key_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<EncryptionKeySecret, VirtualClientsError> {
let encryption_key_secret =
self.0
.derive_secret(crypto, ciphersuite, ENCRYPTION_KEY_LABEL)?;
Ok(EncryptionKeySecret(encryption_key_secret))
}
pub(crate) fn derive_path_generation_secret(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
) -> Result<PathGenerationSecret, VirtualClientsError> {
let path_generation_secret =
self.0
.derive_secret(crypto, ciphersuite, PATH_GENERATION_LABEL)?;
Ok(PathGenerationSecret(path_generation_secret))
}
}
#[derive(Debug)]
pub(crate) struct VcCommitMaterial {
pub(crate) epoch_id: EpochId,
pub(crate) operation_secret: OperationSecret,
pub(crate) external_init_secret: Option<ExternalInitSecret>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DerivationInfoTbe {
LeafNode {
leaf_index: LeafNodeIndex,
generation: u32,
external_init_secret: Option<ExternalInitSecret>,
},
KeyPackage {
leaf_index: LeafNodeIndex,
generation: u32,
key_package_index: u32,
},
}
impl DerivationInfoTbe {
pub(crate) fn leaf_index(&self) -> LeafNodeIndex {
match self {
Self::LeafNode { leaf_index, .. } | Self::KeyPackage { leaf_index, .. } => *leaf_index,
}
}
pub(crate) fn generation(&self) -> u32 {
match self {
Self::LeafNode { generation, .. } | Self::KeyPackage { generation, .. } => *generation,
}
}
pub(crate) fn external_init_secret(&self) -> Option<&ExternalInitSecret> {
match self {
Self::LeafNode {
external_init_secret,
..
} => external_init_secret.as_ref(),
Self::KeyPackage { .. } => None,
}
}
fn tls_serialize_detached(&self) -> Result<Vec<u8>, tls_codec::Error> {
match self {
Self::LeafNode {
leaf_index,
generation,
external_init_secret,
} => {
let mut out = Vec::with_capacity(
leaf_index.tls_serialized_len()
+ generation.tls_serialized_len()
+ external_init_secret.tls_serialized_len(),
);
leaf_index.tls_serialize(&mut out)?;
generation.tls_serialize(&mut out)?;
external_init_secret.tls_serialize(&mut out)?;
Ok(out)
}
Self::KeyPackage {
leaf_index,
generation,
key_package_index,
} => {
let mut out = Vec::with_capacity(
leaf_index.tls_serialized_len()
+ generation.tls_serialized_len()
+ key_package_index.tls_serialized_len(),
);
leaf_index.tls_serialize(&mut out)?;
generation.tls_serialize(&mut out)?;
key_package_index.tls_serialize(&mut out)?;
Ok(out)
}
}
}
fn deserialize_for_operation(
bytes: &[u8],
operation_type: VirtualClientOperationType,
) -> Result<Self, VirtualClientsError> {
let (leaf_index, rest) = LeafNodeIndex::tls_deserialize_bytes(bytes)?;
let (generation, rest) = u32::tls_deserialize_bytes(rest)?;
let (tbe, rest) = match operation_type {
VirtualClientOperationType::KeyPackage => {
let (key_package_index, rest) = u32::tls_deserialize_bytes(rest)?;
(
Self::KeyPackage {
leaf_index,
generation,
key_package_index,
},
rest,
)
}
VirtualClientOperationType::LeafNode => {
let (external_init_secret, rest) =
Option::<ExternalInitSecret>::tls_deserialize_bytes(rest)?;
(
Self::LeafNode {
leaf_index,
generation,
external_init_secret,
},
rest,
)
}
VirtualClientOperationType::Application => {
return Err(VirtualClientsError::DerivationInfoMalformed);
}
};
if !rest.is_empty() {
return Err(VirtualClientsError::DerivationInfoMalformed);
}
Ok(tbe)
}
}
pub(crate) fn load_vc_epoch_state_and_tree<Provider: OpenMlsProvider>(
provider: &Provider,
epoch_id: &EpochId,
) -> Result<
(
EmulationEpochState,
crate::components::vc_operation_tree::OperationSecretTree,
),
VirtualClientsError,
> {
use openmls_traits::storage::StorageProvider as _;
let storage = provider.storage();
let state = storage
.vc_emulation_epoch_state(epoch_id)
.map_err(|e| {
log::error!("vc: load emulation epoch state failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingEmulationEpochState)?;
let operation_tree = storage
.vc_operation_tree(epoch_id)
.map_err(|e| {
log::error!("vc: load operation tree failed: {e:?}");
VirtualClientsError::StorageError
})?
.ok_or(VirtualClientsError::MissingOperationTree)?;
Ok((state, operation_tree))
}
pub(crate) fn resolve_vc_leaf_dictionary(
caller_capabilities: Option<&crate::treesync::node::leaf_node::Capabilities>,
caller_extensions: Option<
&crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
>,
current_leaf: Option<&crate::treesync::node::leaf_node::LeafNode>,
) -> Result<crate::extensions::AppDataDictionary, VirtualClientsError> {
use crate::{
component::{ComponentId, ComponentType},
extensions::ExtensionType,
};
use tls_codec::DeserializeBytes as _;
let supports_app_data_dictionary = match caller_capabilities {
Some(c) => c.extensions().contains(&ExtensionType::AppDataDictionary),
None => current_leaf
.map(|leaf| {
leaf.capabilities()
.extensions()
.contains(&ExtensionType::AppDataDictionary)
})
.unwrap_or(false),
};
if !supports_app_data_dictionary {
return Err(VirtualClientsError::AppDataDictionaryNotSupported);
}
let mut resolved_dictionary = current_leaf
.and_then(|leaf| leaf.extensions().app_data_dictionary())
.map(|ext| ext.dictionary().clone())
.unwrap_or_default();
if let Some(caller_dict) = caller_extensions.and_then(|exts| exts.app_data_dictionary()) {
for entry in caller_dict.dictionary().entries() {
resolved_dictionary.insert(entry.id(), entry.data().to_vec());
}
}
let app_components_bytes = resolved_dictionary
.get(&ComponentId::from(ComponentType::AppComponents))
.map(<[u8]>::to_vec);
let Some(app_components_bytes) = app_components_bytes else {
return Err(VirtualClientsError::VcComponentNotListed);
};
let supported_components = Vec::<u16>::tls_deserialize_exact_bytes(&app_components_bytes)
.map_err(|e| {
log::error!("vc: AppComponents body failed to deserialize: {e:?}");
VirtualClientsError::VcComponentNotListed
})?;
if !supported_components.contains(&VC_COMPONENT_ID) {
return Err(VirtualClientsError::VcComponentNotListed);
}
Ok(resolved_dictionary)
}
pub(crate) fn merge_vc_derivation_info(
caller_extensions: Option<
&crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
>,
mut resolved_dictionary: crate::extensions::AppDataDictionary,
derivation_info_bytes: Vec<u8>,
) -> Result<
crate::extensions::Extensions<crate::treesync::node::leaf_node::LeafNode>,
crate::error::LibraryError,
> {
use crate::extensions::{AppDataDictionaryExtension, Extension, Extensions};
resolved_dictionary.insert(VC_COMPONENT_ID, derivation_info_bytes);
let vc_extension =
Extension::AppDataDictionary(AppDataDictionaryExtension::new(resolved_dictionary));
let other_extensions = caller_extensions
.map(|exts| {
exts.iter()
.filter(|ext| !matches!(ext, Extension::AppDataDictionary(_)))
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let new_extensions: Vec<Extension> = other_extensions
.into_iter()
.chain(std::iter::once(vc_extension))
.collect();
Extensions::from_vec(new_extensions)
.map_err(|_| crate::error::LibraryError::custom("Failed to build VC leaf-node extensions"))
}
#[cfg(test)]
mod tests {
use super::*;
use openmls_rust_crypto::{MemoryStorage, OpenMlsRustCrypto};
use openmls_traits::{
random::OpenMlsRand,
storage::{StorageProvider, CURRENT_VERSION},
OpenMlsProvider,
};
const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
fn register_epoch_state(provider: &OpenMlsRustCrypto, leaf_index: LeafNodeIndex) -> EpochId {
use crate::components::vc_operation_tree::OperationSecretTree;
let emulator = EmulatorEpochSecret::new(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
);
let epoch_id = emulator
.derive_epoch_id(provider.crypto(), CIPHERSUITE)
.expect("derive epoch id");
let epoch_encryption_key = emulator
.derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
.expect("derive epoch encryption key");
let reuse_guard_secret = emulator
.derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
.expect("derive reuse guard secret");
let generation_id_secret = emulator
.derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
.expect("derive generation id secret");
let epoch_base_secret = emulator
.derive_epoch_base_secret(provider.crypto(), CIPHERSUITE)
.expect("derive epoch base secret");
let emulation_group_size = TreeSize::new(2);
let state = EmulationEpochState::new(
leaf_index,
epoch_encryption_key,
reuse_guard_secret,
generation_id_secret,
emulation_group_size,
CIPHERSUITE,
);
<MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_emulation_epoch_state(
provider.storage(),
&epoch_id,
&state,
)
.expect("write emulation epoch state");
let operation_tree = OperationSecretTree::new(epoch_base_secret, emulation_group_size);
<MemoryStorage as StorageProvider<CURRENT_VERSION>>::write_vc_operation_tree(
provider.storage(),
&epoch_id,
&operation_tree,
)
.expect("write operation tree");
epoch_id
}
#[test]
fn assemble_upload_reads_leaf_index_from_state() {
let provider = OpenMlsRustCrypto::default();
let leaf_index = LeafNodeIndex::new(5);
let epoch_id = register_epoch_state(&provider, leaf_index);
let infos = vec![
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
cipher_suite: CIPHERSUITE,
key_package_index: 1,
},
];
let upload = assemble_vc_key_package_upload(provider.storage(), epoch_id.clone(), 4, infos)
.expect("assemble upload");
assert_eq!(upload.epoch_id, epoch_id);
assert_eq!(upload.leaf_index, leaf_index);
assert_eq!(upload.generation, 4);
assert_eq!(upload.key_package_info.len(), 2);
}
#[test]
fn assemble_upload_without_state_fails() {
let provider = OpenMlsRustCrypto::default();
let epoch_id = EpochId(b"unregistered-epoch".to_vec().into());
let err = assemble_vc_key_package_upload(provider.storage(), epoch_id, 0, Vec::new())
.expect_err("assemble must fail without registered state");
assert_eq!(err, VirtualClientsError::MissingEmulationEpochState);
}
#[test]
fn process_upload_stores_records() {
let provider = OpenMlsRustCrypto::default();
let leaf_index = LeafNodeIndex::new(0);
let epoch_id = register_epoch_state(&provider, leaf_index);
let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
let upload = KeyPackageUpload {
epoch_id: epoch_id.clone(),
leaf_index,
generation: 0,
key_package_info: vec![
KeyPackageInfo {
key_package_ref: ref_a.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: ref_b.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 1,
},
],
};
process_vc_key_package_upload(&provider, &upload).expect("process upload");
let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &ref_a
)
.expect("read material a")
.expect("material a present");
assert_eq!(material_a.epoch_id, epoch_id);
assert_eq!(material_a.leaf_index, leaf_index);
assert_eq!(material_a.generation, 0);
assert_eq!(material_a.key_package_index, 0);
assert_eq!(material_a.key_package_ciphersuite, CIPHERSUITE);
let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &ref_b
)
.expect("read material b")
.expect("material b present");
assert_eq!(material_b.epoch_id, epoch_id);
assert_eq!(material_b.leaf_index, leaf_index);
assert_eq!(material_b.generation, 0);
assert_eq!(material_b.key_package_index, 1);
assert_eq!(material_b.key_package_ciphersuite, CIPHERSUITE);
}
#[test]
fn delete_key_package_removes_vc_record() {
let provider = OpenMlsRustCrypto::default();
let leaf_index = LeafNodeIndex::new(0);
let epoch_id = register_epoch_state(&provider, leaf_index);
let kp_ref = KeyPackageRef::from_slice(b"kp-ref");
let upload = KeyPackageUpload {
epoch_id,
leaf_index,
generation: 0,
key_package_info: vec![KeyPackageInfo {
key_package_ref: kp_ref.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
}],
};
process_vc_key_package_upload(&provider, &upload).expect("process upload");
let present: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &kp_ref
)
.expect("read material");
assert!(present.is_some());
<MemoryStorage as StorageProvider<CURRENT_VERSION>>::delete_key_package(
provider.storage(),
&kp_ref,
)
.expect("delete key package");
let after: Option<RetainedKeyPackageMaterial> = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &kp_ref
)
.expect("read material after delete");
assert!(after.is_none());
}
fn setup_key_and_epoch_id(provider: &OpenMlsRustCrypto) -> (EpochEncryptionKey, EpochId) {
let emulator = EmulatorEpochSecret::new(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
);
let key = emulator
.derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
.expect("derive ek");
let epoch_id = emulator
.derive_epoch_id(provider.crypto(), CIPHERSUITE)
.expect("derive epoch id");
(key, epoch_id)
}
#[test]
fn derivation_info_tbe_roundtrip() {
let provider = OpenMlsRustCrypto::default();
let (key, epoch_id) = setup_key_and_epoch_id(&provider);
let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
let key_package_tbe = DerivationInfoTbe::KeyPackage {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
key_package_index: 5,
};
let leaf_node_tbe = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: None,
};
let external_commit_tbe = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: Some(ExternalInitSecret::from_slice(b"external init secret")),
};
let key_package_bytes = key_package_tbe
.tls_serialize_detached()
.expect("serialize key package tbe");
let leaf_node_bytes = leaf_node_tbe
.tls_serialize_detached()
.expect("serialize leaf node tbe");
assert_eq!(key_package_bytes.len(), leaf_node_bytes.len() + 3);
for (original, operation_type) in [
(key_package_tbe, VirtualClientOperationType::KeyPackage),
(leaf_node_tbe, VirtualClientOperationType::LeafNode),
(external_commit_tbe, VirtualClientOperationType::LeafNode),
] {
let derivation_info = DerivationInfo::encrypt(
provider.crypto(),
CIPHERSUITE,
&key,
epoch_id.clone(),
&leaf_encryption_key,
&original,
)
.expect("encrypt");
assert_eq!(derivation_info.epoch_id(), &epoch_id);
let decrypted = derivation_info
.decrypt(
provider.crypto(),
CIPHERSUITE,
&key,
&leaf_encryption_key,
operation_type,
)
.expect("decrypt");
assert_eq!(original, decrypted);
}
}
#[test]
fn derivation_info_tbe_wire_format_matches_spec() {
let absent = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: None,
}
.tls_serialize_detached()
.expect("serialize");
assert_eq!(
absent,
[0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00]
);
let present = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: Some(ExternalInitSecret::from_slice(b"init")),
}
.tls_serialize_detached()
.expect("serialize");
assert_eq!(
present,
[0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x01, 0x04, b'i', b'n', b'i', b't']
);
let key_package = DerivationInfoTbe::KeyPackage {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
key_package_index: 5,
}
.tls_serialize_detached()
.expect("serialize");
assert_eq!(
key_package,
[0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05]
);
}
#[test]
fn derivation_info_tbe_rejects_trailing_data() {
let variants = [
(
DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: None,
},
VirtualClientOperationType::LeafNode,
),
(
DerivationInfoTbe::KeyPackage {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
key_package_index: 5,
},
VirtualClientOperationType::KeyPackage,
),
];
for (tbe, operation_type) in variants {
let mut bytes = tbe.tls_serialize_detached().expect("serialize");
bytes.push(0x00);
let result = DerivationInfoTbe::deserialize_for_operation(&bytes, operation_type);
assert_eq!(result, Err(VirtualClientsError::DerivationInfoMalformed));
}
}
#[test]
fn external_init_secret_debug_is_redacted() {
let tbe = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(7),
generation: 3,
external_init_secret: Some(ExternalInitSecret::from_slice(b"very secret bytes")),
};
let debug = format!("{tbe:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("secret bytes"));
assert!(!debug.to_lowercase().contains("76657279"));
}
#[test]
fn decryption_fails_with_wrong_leaf_encryption_key() {
let provider = OpenMlsRustCrypto::default();
let (key, epoch_id) = setup_key_and_epoch_id(&provider);
let leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
let tbe = DerivationInfoTbe::LeafNode {
leaf_index: LeafNodeIndex::new(1),
generation: 0,
external_init_secret: None,
};
let derivation_info = DerivationInfo::encrypt(
provider.crypto(),
CIPHERSUITE,
&key,
epoch_id,
&leaf_encryption_key,
&tbe,
)
.expect("encrypt");
let other_leaf_encryption_key = provider.rand().random_vec(32).expect("randomness");
let err = derivation_info
.decrypt(
provider.crypto(),
CIPHERSUITE,
&key,
&other_leaf_encryption_key,
VirtualClientOperationType::LeafNode,
)
.expect_err("decryption with the wrong context must fail");
assert_eq!(err, VirtualClientsError::DerivationInfoDecryptionFailed);
}
#[test]
fn key_package_seed_derivation_is_indexed_and_label_separated() {
let provider = OpenMlsRustCrypto::default();
let operation_secret = OperationSecret::from(Secret::from_slice(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
));
let seed_zero = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
.expect("derive seed 0");
let seed_zero_again = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
.expect("derive seed 0 again");
let seed_one = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
.expect("derive seed 1");
let init_zero = seed_zero
.derive_init_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive init key 0")
.generate_init_key_pair(provider.crypto(), CIPHERSUITE)
.expect("generate init pair 0");
let init_zero_again = seed_zero_again
.derive_init_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive init key 0 again")
.generate_init_key_pair(provider.crypto(), CIPHERSUITE)
.expect("generate init pair 0 again");
let init_one = seed_one
.derive_init_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive init key 1")
.generate_init_key_pair(provider.crypto(), CIPHERSUITE)
.expect("generate init pair 1");
assert_eq!(init_zero.public, init_zero_again.public);
assert_ne!(init_zero.public, init_one.public);
let encryption_zero = seed_zero
.derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive encryption key 0")
.generate_encryption_key_pair(provider.crypto(), CIPHERSUITE)
.expect("generate encryption pair 0");
assert_ne!(
init_zero.public.as_slice(),
encryption_zero.public_key().as_slice()
);
}
#[test]
fn key_package_seed_binds_target_ciphersuite() {
let provider = OpenMlsRustCrypto::default();
let operation_secret = OperationSecret::from(Secret::from_slice(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
));
let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
let seed = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
.expect("derive seed");
let seed_other_suite = operation_secret
.derive_key_package_seed_secret(provider.crypto(), other_ciphersuite, 0)
.expect("derive seed under other target ciphersuite");
assert_ne!(seed.0.as_slice(), seed_other_suite.0.as_slice());
}
#[test]
fn target_operation_secret_binds_ciphersuite_and_group_id() {
let provider = OpenMlsRustCrypto::default();
let operation_secret = OperationSecret::from(Secret::from_slice(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
));
let group_id = GroupId::from_slice(b"group-a");
let other_ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519;
let target = operation_secret
.derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
.expect("derive target operation secret");
let target_again = operation_secret
.derive_target_operation_secret(provider.crypto(), CIPHERSUITE, &group_id)
.expect("derive target operation secret again");
let target_other_group = operation_secret
.derive_target_operation_secret(
provider.crypto(),
CIPHERSUITE,
&GroupId::from_slice(b"group-b"),
)
.expect("derive target operation secret for other group");
let target_other_suite = operation_secret
.derive_target_operation_secret(provider.crypto(), other_ciphersuite, &group_id)
.expect("derive target operation secret under other target ciphersuite");
assert_eq!(target.0.as_slice(), target_again.0.as_slice());
assert_ne!(target.0.as_slice(), target_other_group.0.as_slice());
assert_ne!(target.0.as_slice(), target_other_suite.0.as_slice());
let encryption_key_secret = target
.derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive encryption key secret");
let path_generation_secret = target
.derive_path_generation_secret(provider.crypto(), CIPHERSUITE)
.expect("derive path generation secret");
assert_ne!(
encryption_key_secret.0.as_slice(),
path_generation_secret.0.as_slice()
);
}
#[test]
fn group_creation_secret_derivation_is_deterministic_and_label_separated() {
let provider = OpenMlsRustCrypto::default();
let operation_secret = OperationSecret::from(Secret::from_slice(
&provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness"),
));
let seed_zero = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 0)
.expect("derive seed 0");
let seed_one = operation_secret
.derive_key_package_seed_secret(provider.crypto(), CIPHERSUITE, 1)
.expect("derive seed 1");
let epoch_secret_zero = seed_zero
.derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
.expect("derive group creation secret 0");
let epoch_secret_zero_again = seed_zero
.derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
.expect("derive group creation secret 0 again");
let epoch_secret_one = seed_one
.derive_group_creation_secret(provider.crypto(), CIPHERSUITE)
.expect("derive group creation secret 1");
assert_eq!(
epoch_secret_zero.as_slice(),
epoch_secret_zero_again.as_slice()
);
assert_ne!(epoch_secret_zero.as_slice(), epoch_secret_one.as_slice());
let encryption_key_secret = seed_zero
.derive_encryption_key_secret(provider.crypto(), CIPHERSUITE)
.expect("derive encryption key 0");
assert_ne!(
epoch_secret_zero.as_slice(),
encryption_key_secret.0.as_slice()
);
}
#[test]
fn validate_rejects_duplicate_index() {
let infos = vec![
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
cipher_suite: CIPHERSUITE,
key_package_index: 2,
},
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
cipher_suite: CIPHERSUITE,
key_package_index: 2,
},
];
let err = validate_key_package_infos(&infos).expect_err("duplicate index must be rejected");
assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(2));
}
#[test]
fn validate_rejects_duplicate_ref() {
let infos = vec![
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
cipher_suite: CIPHERSUITE,
key_package_index: 1,
},
];
let err = validate_key_package_infos(&infos).expect_err("duplicate ref must be rejected");
assert_eq!(err, VirtualClientsError::DuplicateKeyPackageRef);
}
#[test]
fn validate_accepts_distinct_infos() {
let infos = vec![
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-a"),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: KeyPackageRef::from_slice(b"kp-ref-b"),
cipher_suite: CIPHERSUITE,
key_package_index: 1,
},
];
validate_key_package_infos(&infos).expect("distinct infos must pass");
}
#[test]
fn process_upload_rejects_malformed_without_consuming_generation() {
let provider = OpenMlsRustCrypto::default();
let leaf_index = LeafNodeIndex::new(0);
let epoch_id = register_epoch_state(&provider, leaf_index);
let ref_a = KeyPackageRef::from_slice(b"kp-ref-a");
let ref_b = KeyPackageRef::from_slice(b"kp-ref-b");
let malformed = KeyPackageUpload {
epoch_id: epoch_id.clone(),
leaf_index,
generation: 0,
key_package_info: vec![
KeyPackageInfo {
key_package_ref: ref_a.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: ref_b.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
],
};
let err = process_vc_key_package_upload(&provider, &malformed)
.expect_err("malformed upload must be rejected");
assert_eq!(err, VirtualClientsError::DuplicateKeyPackageIndex(0));
let valid = KeyPackageUpload {
epoch_id: epoch_id.clone(),
leaf_index,
generation: 0,
key_package_info: vec![
KeyPackageInfo {
key_package_ref: ref_a.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 0,
},
KeyPackageInfo {
key_package_ref: ref_b.clone(),
cipher_suite: CIPHERSUITE,
key_package_index: 1,
},
],
};
process_vc_key_package_upload(&provider, &valid)
.expect("valid upload reusing the same generation must succeed");
let material_a: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &ref_a
)
.expect("read material a")
.expect("material a present");
assert_eq!(material_a.epoch_id, epoch_id);
assert_eq!(material_a.generation, 0);
assert_eq!(material_a.key_package_index, 0);
let material_b: RetainedKeyPackageMaterial = <MemoryStorage as StorageProvider<
CURRENT_VERSION,
>>::retained_key_package_material(
provider.storage(), &ref_b
)
.expect("read material b")
.expect("material b present");
assert_eq!(material_b.key_package_index, 1);
}
fn state_from_secret_bytes(
provider: &OpenMlsRustCrypto,
secret_bytes: &[u8],
leaf_index: LeafNodeIndex,
) -> EmulationEpochState {
let emulator = EmulatorEpochSecret::new(secret_bytes);
let epoch_encryption_key = emulator
.derive_epoch_encryption_key(provider.crypto(), CIPHERSUITE)
.expect("derive epoch encryption key");
let reuse_guard_secret = emulator
.derive_reuse_guard_secret(provider.crypto(), CIPHERSUITE)
.expect("derive reuse guard secret");
let generation_id_secret = emulator
.derive_generation_id_secret(provider.crypto(), CIPHERSUITE)
.expect("derive generation id secret");
EmulationEpochState::new(
leaf_index,
epoch_encryption_key,
reuse_guard_secret,
generation_id_secret,
TreeSize::new(2),
CIPHERSUITE,
)
}
#[test]
fn generation_id_is_deterministic_and_context_sensitive() {
let provider = OpenMlsRustCrypto::default();
let secret_bytes = provider
.rand()
.random_vec(CIPHERSUITE.hash_length())
.expect("randomness");
let state = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(0));
let group_id = GroupId::from_slice(b"higher-level-group");
let epoch = GroupEpoch::from(7);
let derive = |group_id: &GroupId, epoch, generation, ratchet_type| {
state
.derive_generation_id(provider.crypto(), group_id, epoch, generation, ratchet_type)
.expect("derive generation id")
};
let base = derive(&group_id, epoch, 3, RatchetType::Application);
assert_eq!(base.as_slice().len(), CIPHERSUITE.hash_length());
assert_eq!(base, derive(&group_id, epoch, 3, RatchetType::Application));
assert_ne!(base, derive(&group_id, epoch, 4, RatchetType::Application));
assert_ne!(
base,
derive(&group_id, GroupEpoch::from(8), 3, RatchetType::Application)
);
assert_ne!(
base,
derive(
&GroupId::from_slice(b"other-group"),
epoch,
3,
RatchetType::Application
)
);
assert_ne!(base, derive(&group_id, epoch, 3, RatchetType::Handshake));
let sibling = state_from_secret_bytes(&provider, &secret_bytes, LeafNodeIndex::new(5));
let sibling_id = sibling
.derive_generation_id(
provider.crypto(),
&group_id,
epoch,
3,
RatchetType::Application,
)
.expect("sibling derive generation id");
assert_eq!(base, sibling_id);
}
}