458 lines
13 KiB
Rust
458 lines
13 KiB
Rust
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
// Copyright by contributors to this project.
|
|
// SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
|
|
/// The example shows how how to create an MLS extension implementing an access control policy
|
|
/// based on the concept of users, similar to
|
|
/// https://bifurcation.github.io/ietf-mimi-protocol/draft-ralston-mimi-protocol.html.
|
|
///
|
|
/// A user, e.g. "bob@b.example", owns zero or more MLS members, e.g. Bob's tablet and PC.
|
|
/// Users do not have MLS cryptographic state, while MLS members do. At any point in time,
|
|
/// the MLS group has a fixed set of users and for each user, zero or more MLS members they
|
|
/// own. Each user also has a role, e.g. a regular user or moderator (which may possibly change
|
|
/// over time).
|
|
///
|
|
/// The goal is to implement the following rule:
|
|
/// 1. Each MLS member belongs to a user in the group.
|
|
///
|
|
/// To this end, we implement the following:
|
|
/// * A GroupContext extension containing the current list of users. MLS guarantees agreement
|
|
/// on the list.
|
|
/// * An AddUser proposal that modifies the user list.
|
|
/// * An MLS credential type for MLS members with the owning user's public key and signature.
|
|
/// When MLS members join using MLS Add proposals, the signature is verified.
|
|
/// * Proposal validation rules that enforce 1. above.
|
|
///
|
|
use assert_matches::assert_matches;
|
|
use mls_rs::{
|
|
client_builder::{MlsConfig, PaddingMode},
|
|
error::MlsError,
|
|
group::{
|
|
proposal::{MlsCustomProposal, Proposal},
|
|
GroupContext, Roster, Sender,
|
|
},
|
|
mls_rules::{
|
|
CommitDirection, CommitOptions, CommitSource, EncryptionOptions, ProposalBundle,
|
|
ProposalSource,
|
|
},
|
|
CipherSuite, CipherSuiteProvider, Client, CryptoProvider, ExtensionList, IdentityProvider,
|
|
MlsRules,
|
|
};
|
|
use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
|
|
use mls_rs_core::{
|
|
crypto::{SignaturePublicKey, SignatureSecretKey},
|
|
error::IntoAnyError,
|
|
extension::{ExtensionError, ExtensionType, MlsCodecExtension},
|
|
group::ProposalType,
|
|
identity::{
|
|
Credential, CredentialType, CustomCredential, MemberValidationContext, MlsCredential,
|
|
SigningIdentity,
|
|
},
|
|
time::MlsTime,
|
|
};
|
|
|
|
use std::fmt::Display;
|
|
|
|
const CIPHER_SUITE: CipherSuite = CipherSuite::CURVE25519_AES128;
|
|
|
|
const ROSTER_EXTENSION_V1: ExtensionType = ExtensionType::new(65000);
|
|
const ADD_USER_PROPOSAL_V1: ProposalType = ProposalType::new(65001);
|
|
const CREDENTIAL_V1: CredentialType = CredentialType::new(65002);
|
|
|
|
fn crypto() -> impl CryptoProvider + Clone {
|
|
mls_rs_crypto_openssl::OpensslCryptoProvider::new()
|
|
}
|
|
|
|
fn cipher_suite() -> impl CipherSuiteProvider {
|
|
crypto().cipher_suite_provider(CIPHER_SUITE).unwrap()
|
|
}
|
|
|
|
#[derive(MlsSize, MlsDecode, MlsEncode)]
|
|
#[repr(u8)]
|
|
enum UserRole {
|
|
Regular = 1u8,
|
|
Moderator = 2u8,
|
|
}
|
|
|
|
#[derive(MlsSize, MlsDecode, MlsEncode)]
|
|
struct UserCredential {
|
|
name: String,
|
|
role: UserRole,
|
|
public_key: SignaturePublicKey,
|
|
}
|
|
|
|
#[derive(MlsSize, MlsDecode, MlsEncode)]
|
|
struct MemberCredential {
|
|
name: String,
|
|
user_public_key: SignaturePublicKey, // Identifies the user
|
|
signature: Vec<u8>,
|
|
}
|
|
|
|
#[derive(MlsSize, MlsEncode)]
|
|
struct MemberCredentialTBS<'a> {
|
|
name: &'a str,
|
|
user_public_key: &'a SignaturePublicKey,
|
|
public_key: &'a SignaturePublicKey,
|
|
}
|
|
|
|
/// The roster will be stored in the custom RosterExtension, an extension in the MLS GroupContext
|
|
#[derive(MlsSize, MlsDecode, MlsEncode)]
|
|
struct RosterExtension {
|
|
roster: Vec<UserCredential>,
|
|
}
|
|
|
|
impl MlsCodecExtension for RosterExtension {
|
|
fn extension_type() -> ExtensionType {
|
|
ROSTER_EXTENSION_V1
|
|
}
|
|
}
|
|
|
|
/// The custom AddUser proposal will be used to update the RosterExtension
|
|
#[derive(MlsSize, MlsDecode, MlsEncode)]
|
|
struct AddUserProposal {
|
|
new_user: UserCredential,
|
|
}
|
|
|
|
impl MlsCustomProposal for AddUserProposal {
|
|
fn proposal_type() -> ProposalType {
|
|
ADD_USER_PROPOSAL_V1
|
|
}
|
|
}
|
|
|
|
/// MlsRules tell MLS how to handle our custom proposal
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct CustomMlsRules;
|
|
|
|
impl MlsRules for CustomMlsRules {
|
|
type Error = CustomError;
|
|
|
|
fn filter_proposals(
|
|
&self,
|
|
_: CommitDirection,
|
|
_: CommitSource,
|
|
_: &Roster,
|
|
context: &GroupContext,
|
|
mut proposals: ProposalBundle,
|
|
) -> Result<ProposalBundle, Self::Error> {
|
|
// Find our extension
|
|
let mut roster: RosterExtension = context
|
|
.extensions
|
|
.get_as()
|
|
.ok()
|
|
.flatten()
|
|
.ok_or(CustomError)?;
|
|
|
|
// Find AddUser proposals
|
|
let add_user_proposals = proposals
|
|
.custom_proposals()
|
|
.iter()
|
|
.filter(|p| p.proposal.proposal_type() == ADD_USER_PROPOSAL_V1);
|
|
|
|
for add_user_info in add_user_proposals {
|
|
let add_user = AddUserProposal::from_custom_proposal(&add_user_info.proposal)?;
|
|
|
|
// Eventually we should check for duplicates
|
|
roster.roster.push(add_user.new_user);
|
|
}
|
|
|
|
// Issue GroupContextExtensions proposal to modify our roster (eventually we don't have to do this if there were no AddUser proposals)
|
|
let mut new_extensions = context.extensions.clone();
|
|
new_extensions.set_from(roster)?;
|
|
let gce_proposal = Proposal::GroupContextExtensions(new_extensions);
|
|
proposals.add(gce_proposal, Sender::Member(0), ProposalSource::Local);
|
|
|
|
Ok(proposals)
|
|
}
|
|
|
|
fn commit_options(
|
|
&self,
|
|
_: &Roster,
|
|
_: &GroupContext,
|
|
_: &ProposalBundle,
|
|
) -> Result<CommitOptions, Self::Error> {
|
|
Ok(CommitOptions::new())
|
|
}
|
|
|
|
fn encryption_options(
|
|
&self,
|
|
_: &Roster,
|
|
_: &GroupContext,
|
|
) -> Result<EncryptionOptions, Self::Error> {
|
|
Ok(EncryptionOptions::new(false, PaddingMode::None))
|
|
}
|
|
}
|
|
|
|
// The IdentityProvider will tell MLS how to validate members' identities. We will use custom identity
|
|
// type to store our User structs.
|
|
impl MlsCredential for MemberCredential {
|
|
type Error = CustomError;
|
|
|
|
fn credential_type() -> CredentialType {
|
|
CREDENTIAL_V1
|
|
}
|
|
|
|
fn into_credential(self) -> Result<Credential, Self::Error> {
|
|
Ok(Credential::Custom(CustomCredential::new(
|
|
Self::credential_type(),
|
|
self.mls_encode_to_vec()?,
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct CustomIdentityProvider;
|
|
|
|
impl IdentityProvider for CustomIdentityProvider {
|
|
type Error = CustomError;
|
|
|
|
fn validate_member(
|
|
&self,
|
|
signing_identity: &SigningIdentity,
|
|
_: Option<MlsTime>,
|
|
context: MemberValidationContext<'_>,
|
|
) -> Result<(), Self::Error> {
|
|
let Some(extensions) = context.new_extensions() else {
|
|
return Ok(());
|
|
};
|
|
|
|
let roster = extensions
|
|
.get_as::<RosterExtension>()
|
|
.ok()
|
|
.flatten()
|
|
.ok_or(CustomError)?;
|
|
|
|
// Retrieve the MemberCredential from the MLS credential
|
|
let Credential::Custom(custom) = &signing_identity.credential else {
|
|
return Err(CustomError);
|
|
};
|
|
|
|
if custom.credential_type != CREDENTIAL_V1 {
|
|
return Err(CustomError);
|
|
}
|
|
|
|
let member = MemberCredential::mls_decode(&mut &*custom.data)?;
|
|
|
|
// Validate the MemberCredential
|
|
|
|
let tbs = MemberCredentialTBS {
|
|
name: &member.name,
|
|
user_public_key: &member.user_public_key,
|
|
public_key: &signing_identity.signature_key,
|
|
}
|
|
.mls_encode_to_vec()?;
|
|
|
|
cipher_suite()
|
|
.verify(&member.user_public_key, &member.signature, &tbs)
|
|
.map_err(|_| CustomError)?;
|
|
|
|
let user_in_roster = roster
|
|
.roster
|
|
.iter()
|
|
.any(|u| u.public_key == member.user_public_key);
|
|
|
|
if !user_in_roster {
|
|
return Err(CustomError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn identity(
|
|
&self,
|
|
signing_identity: &SigningIdentity,
|
|
_: &ExtensionList,
|
|
) -> Result<Vec<u8>, Self::Error> {
|
|
Ok(signing_identity.mls_encode_to_vec()?)
|
|
}
|
|
|
|
fn supported_types(&self) -> Vec<CredentialType> {
|
|
vec![CREDENTIAL_V1]
|
|
}
|
|
|
|
fn valid_successor(
|
|
&self,
|
|
_: &SigningIdentity,
|
|
_: &SigningIdentity,
|
|
_: &ExtensionList,
|
|
) -> Result<bool, Self::Error> {
|
|
Ok(true)
|
|
}
|
|
|
|
fn validate_external_sender(
|
|
&self,
|
|
_: &SigningIdentity,
|
|
_: Option<MlsTime>,
|
|
_: Option<&ExtensionList>,
|
|
) -> Result<(), Self::Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Convenience structs to create users and members
|
|
|
|
struct User {
|
|
credential: UserCredential,
|
|
signer: SignatureSecretKey,
|
|
}
|
|
|
|
impl User {
|
|
fn new(name: &str, role: UserRole) -> Result<Self, CustomError> {
|
|
let (signer, public_key) = cipher_suite()
|
|
.signature_key_generate()
|
|
.map_err(|_| CustomError)?;
|
|
|
|
let credential = UserCredential {
|
|
name: name.into(),
|
|
role,
|
|
public_key,
|
|
};
|
|
|
|
Ok(Self { credential, signer })
|
|
}
|
|
}
|
|
|
|
struct Member {
|
|
credential: MemberCredential,
|
|
public_key: SignaturePublicKey,
|
|
signer: SignatureSecretKey,
|
|
}
|
|
|
|
impl Member {
|
|
fn new(name: &str, user: &User) -> Result<Self, CustomError> {
|
|
let (signer, public_key) = cipher_suite()
|
|
.signature_key_generate()
|
|
.map_err(|_| CustomError)?;
|
|
|
|
let tbs = MemberCredentialTBS {
|
|
name,
|
|
user_public_key: &user.credential.public_key,
|
|
public_key: &public_key,
|
|
}
|
|
.mls_encode_to_vec()?;
|
|
|
|
let signature = cipher_suite()
|
|
.sign(&user.signer, &tbs)
|
|
.map_err(|_| CustomError)?;
|
|
|
|
let credential = MemberCredential {
|
|
name: name.into(),
|
|
user_public_key: user.credential.public_key.clone(),
|
|
signature,
|
|
};
|
|
|
|
Ok(Self {
|
|
credential,
|
|
signer,
|
|
public_key,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Set up Client to use our custom providers
|
|
fn make_client(member: Member) -> Result<Client<impl MlsConfig>, CustomError> {
|
|
let mls_credential = member.credential.into_credential()?;
|
|
let signing_identity = SigningIdentity::new(mls_credential, member.public_key);
|
|
|
|
Ok(Client::builder()
|
|
.identity_provider(CustomIdentityProvider)
|
|
.mls_rules(CustomMlsRules)
|
|
.custom_proposal_type(ADD_USER_PROPOSAL_V1)
|
|
.extension_type(ROSTER_EXTENSION_V1)
|
|
.crypto_provider(crypto())
|
|
.signing_identity(signing_identity, member.signer, CIPHER_SUITE)
|
|
.build())
|
|
}
|
|
|
|
fn main() -> Result<(), CustomError> {
|
|
let alice = User::new("alice", UserRole::Moderator)?;
|
|
let bob = User::new("bob", UserRole::Regular)?;
|
|
|
|
let alice_tablet = Member::new("alice tablet", &alice)?;
|
|
let alice_pc = Member::new("alice pc", &alice)?;
|
|
let bob_tablet = Member::new("bob tablet", &bob)?;
|
|
|
|
// Alice creates the group with our RosterExtension containing her user
|
|
let mut context_extensions = ExtensionList::new();
|
|
let roster = vec![alice.credential];
|
|
context_extensions.set_from(RosterExtension { roster })?;
|
|
|
|
let mut alice_tablet_group =
|
|
make_client(alice_tablet)?.create_group(context_extensions, Default::default())?;
|
|
|
|
// Alice can add her other device
|
|
let alice_pc_client = make_client(alice_pc)?;
|
|
let key_package =
|
|
alice_pc_client.generate_key_package_message(Default::default(), Default::default())?;
|
|
|
|
let welcome = alice_tablet_group
|
|
.commit_builder()
|
|
.add_member(key_package)?
|
|
.build()?
|
|
.welcome_messages
|
|
.remove(0);
|
|
|
|
alice_tablet_group.apply_pending_commit()?;
|
|
let (mut alice_pc_group, _) = alice_pc_client.join_group(None, &welcome)?;
|
|
|
|
// Alice cannot add bob's devices yet
|
|
let bob_tablet_client = make_client(bob_tablet)?;
|
|
let key_package =
|
|
bob_tablet_client.generate_key_package_message(Default::default(), Default::default())?;
|
|
|
|
let res = alice_tablet_group
|
|
.commit_builder()
|
|
.add_member(key_package.clone())?
|
|
.build();
|
|
|
|
assert_matches!(res, Err(MlsError::IdentityProviderError(_)));
|
|
|
|
// Alice can add bob's user and device
|
|
let add_bob = AddUserProposal {
|
|
new_user: bob.credential,
|
|
};
|
|
|
|
let commit = alice_tablet_group
|
|
.commit_builder()
|
|
.custom_proposal(add_bob.to_custom_proposal()?)
|
|
.add_member(key_package)?
|
|
.build()?;
|
|
|
|
bob_tablet_client.join_group(None, &commit.welcome_messages[0])?;
|
|
alice_tablet_group.apply_pending_commit()?;
|
|
alice_pc_group.process_incoming_message(commit.commit_message)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
struct CustomError;
|
|
|
|
impl IntoAnyError for CustomError {
|
|
fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
|
|
Ok(Box::new(self))
|
|
}
|
|
}
|
|
|
|
impl Display for CustomError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("Custom Error")
|
|
}
|
|
}
|
|
|
|
impl From<MlsError> for CustomError {
|
|
fn from(_: MlsError) -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl From<mls_rs_codec::Error> for CustomError {
|
|
fn from(_: mls_rs_codec::Error) -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl From<ExtensionError> for CustomError {
|
|
fn from(_: ExtensionError) -> Self {
|
|
Self
|
|
}
|
|
}
|