Custom proposals

OpenMLS allows the creation and use of application-defined proposals. To create such a proposal, the application needs to define a Proposal Type in such a way that its value doesn't collide with any Proposal Types defined in Section 17.4. of RFC 9420. If the proposal is meant to be used only inside of a particular application, the value of the Proposal Type is recommended to be in the range between 0xF000 and 0xFFFF, as that range is reserved for private use.

Custom proposals can contain arbitrary octet-strings as defined by the application. Any policy decisions based on custom proposals will have to be made by the application, such as the decision to include a given custom proposal in a commit, or whether to accept a commit that includes one or more custom proposals. To decide the latter, applications can inspect the queued proposals in a ProcessedMessageContent::StagedCommitMesage(staged_commit).

Example on how to use custom proposals:

    // Define a custom proposal type
    let custom_proposal_type = 0xFFFF;

    // Define capabilities supporting the custom proposal type
    let capabilities = Capabilities::new(
        None,
        None,
        None,
        Some(&[ProposalType::Custom(custom_proposal_type)]),
        None,
    );

    // Generate KeyPackage that signals support for the custom proposal type
    let bob_key_package = KeyPackageBuilder::new()
        .leaf_node_capabilities(capabilities.clone())
        .build(ciphersuite, provider, &bob_signer, bob_credential_with_key)
        .unwrap();

    // Create a group that supports the custom proposal type
    let mut alice_group = MlsGroup::builder()
        .with_capabilities(capabilities.clone())
        .ciphersuite(ciphersuite)
        .build(provider, &alice_signer, alice_credential_with_key)
        .unwrap();
    // Create a custom proposal based on an example payload and the custom
    // proposal type defined above
    let custom_proposal_payload = vec![0, 1, 2, 3];
    let custom_proposal =
        CustomProposal::new(custom_proposal_type, custom_proposal_payload.clone());

    let (custom_proposal_message, _proposal_ref) = alice_group
        .propose_custom_proposal_by_reference(provider, &alice_signer, custom_proposal.clone())
        .unwrap();

    // Have bob process the custom proposal.
    let processed_message = bob_group
        .process_message(
            provider,
            custom_proposal_message.into_protocol_message().unwrap(),
        )
        .unwrap();

    let ProcessedMessageContent::ProposalMessage(proposal) = processed_message.into_content()
    else {
        panic!("Unexpected message type");
    };

    bob_group
        .store_pending_proposal(provider.storage(), *proposal)
        .unwrap();

    // Commit to the proposal
    let (commit, _, _) = alice_group
        .commit_to_pending_proposals(provider, &alice_signer)
        .unwrap();

    let processed_message = bob_group
        .process_message(provider, commit.into_protocol_message().unwrap())
        .unwrap();

    let staged_commit = match processed_message.into_content() {
        ProcessedMessageContent::StagedCommitMessage(staged_commit) => staged_commit,
        _ => panic!("Unexpected message type"),
    };

    // Check that the proposal is present in the staged commit
    assert!(staged_commit.queued_proposals().any(|qp| {
        let Proposal::Custom(custom_proposal) = qp.proposal() else {
            return false;
        };
        custom_proposal.proposal_type() == custom_proposal_type
            && custom_proposal.payload() == custom_proposal_payload
    }));