Skip to content

Commit d5fb766

Browse files
committed
xmtp_proto: bump to xmtp/proto feature/external-invite
Bumps proto pin to ab83a2b on the feature/external-invite branch. New types added by the regen: - ExternalInvitePayload, EncryptedGroupInfoBlob (invite wire format). - JoinPolicyV1 + JoinPolicyEntry (JOIN_POLICY well-known component). - external_committer_permissions field on ComponentMetadata (per-component authorization for external committers). Removes the previously-attempted allow_external_commit field on the legacy PolicySet proto — that approach is superseded by JoinPolicy + external_committer_permissions, which are AppData-native and survive the migration.
1 parent 60dc90d commit d5fb766

5 files changed

Lines changed: 799 additions & 2 deletions

File tree

crates/xmtp_mls_common/src/app_data/component_registry.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub fn new_component_metadata(
4141
ComponentMetadata {
4242
permissions: Some(permissions),
4343
component_type: component_type as i32,
44+
external_committer_permissions: None,
4445
}
4546
}
4647

@@ -633,6 +634,7 @@ mod tests {
633634
// the negative case we're testing here.
634635
let meta = ComponentMetadata {
635636
permissions: None,
637+
external_committer_permissions: None,
636638
component_type: ComponentType::Bytes as i32,
637639
};
638640
// Applies to ALL components, not just constrained ones.
@@ -828,6 +830,7 @@ mod tests {
828830
// The set() path catches this; from_bytes() must catch it too.
829831
let meta = ComponentMetadata {
830832
permissions: None,
833+
external_committer_permissions: None,
831834
component_type: ComponentType::Bytes as i32,
832835
};
833836
let bytes = raw_bytes_with_entry(ComponentId::GROUP_NAME, meta.encode_to_vec());

crates/xmtp_proto/proto_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7b8260c3041389bf706fb665460adbbd520d2be7
1+
424fdd061d41bdd7e81185735329de414dfd8e4c
7.11 KB
Binary file not shown.

crates/xmtp_proto/src/gen/xmtp.mls.message_contents.rs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,87 @@ impl WelcomeWrapperAlgorithm {
186186
}
187187
}
188188
}
189+
/// ExternalInvitePayload is the shareable invite blob for QR-code or link-based
190+
/// joining of an XMTP group via an MLS external commit. The application embeds
191+
/// the serialized bytes in whatever transport it prefers (hex, base64, raw QR,
192+
/// NFC, etc.) and stores the encrypted GroupInfo blob on an external service
193+
/// keyed by `group_id_hash`.
194+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
195+
pub struct ExternalInvitePayload {
196+
#[prost(uint32, tag = "1")]
197+
pub version: u32,
198+
/// Application-defined opaque bytes identifying the service location.
199+
#[prost(bytes = "vec", tag = "2")]
200+
pub service_pointer: ::prost::alloc::vec::Vec<u8>,
201+
/// sha256(group_id); service lookup key for the encrypted blob.
202+
#[prost(bytes = "vec", tag = "3")]
203+
pub group_id_hash: ::prost::alloc::vec::Vec<u8>,
204+
/// 32 bytes; ChaCha20Poly1305 key used to wrap the GroupInfo.
205+
#[prost(bytes = "vec", tag = "4")]
206+
pub symmetric_key: ::prost::alloc::vec::Vec<u8>,
207+
/// Nanoseconds since UNIX epoch; 0 means no expiry.
208+
#[prost(uint64, tag = "5")]
209+
pub expires_at_ns: u64,
210+
/// MLS ciphersuite identifier of the target group.
211+
#[prost(uint32, tag = "6")]
212+
pub ciphersuite: u32,
213+
}
214+
impl ::prost::Name for ExternalInvitePayload {
215+
const NAME: &'static str = "ExternalInvitePayload";
216+
const PACKAGE: &'static str = "xmtp.mls.message_contents";
217+
fn full_name() -> ::prost::alloc::string::String {
218+
"xmtp.mls.message_contents.ExternalInvitePayload".into()
219+
}
220+
fn type_url() -> ::prost::alloc::string::String {
221+
"/xmtp.mls.message_contents.ExternalInvitePayload".into()
222+
}
223+
}
224+
/// EncryptedGroupInfoBlob wraps a single GroupInfo TLS-serialized bytes under
225+
/// ChaCha20Poly1305 with a fresh nonce per re-encryption. Stored on the
226+
/// external service and replaced by joiners (with a fresh nonce) after each
227+
/// successful join.
228+
///
229+
/// `epoch` and `group_state_hash` are plaintext metadata used by the
230+
/// external service for ordering and fork detection. The service should
231+
/// accept an uploaded blob iff its `(epoch, group_state_hash)` is strictly
232+
/// newer than the currently-stored blob's — concurrent uploads at the same
233+
/// epoch with different state hashes signal a fork and should be rejected.
234+
/// The joiner verifies on download that the blob's `epoch` and
235+
/// `group_state_hash` match the decrypted GroupInfo before attempting to
236+
/// join — closes the "malicious service swapped ciphertext" gap.
237+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
238+
pub struct EncryptedGroupInfoBlob {
239+
#[prost(uint32, tag = "1")]
240+
pub version: u32,
241+
/// 12 bytes; ChaCha20Poly1305 nonce specific to this ciphertext.
242+
#[prost(bytes = "vec", tag = "2")]
243+
pub nonce: ::prost::alloc::vec::Vec<u8>,
244+
/// wrap_payload_symmetric output: AEAD ciphertext over the serialized
245+
/// MlsMessageOut(GroupInfo).
246+
#[prost(bytes = "vec", tag = "3")]
247+
pub ciphertext: ::prost::alloc::vec::Vec<u8>,
248+
/// MLS group epoch of the wrapped GroupInfo. Plaintext; service uses
249+
/// for monotonic-ordering. Joiner verifies against the decrypted
250+
/// GroupInfo before joining.
251+
#[prost(uint64, tag = "4")]
252+
pub epoch: u64,
253+
/// Tree-hash (or equivalent group-state digest) of the wrapped
254+
/// GroupInfo. Plaintext; service uses alongside `epoch` for fork
255+
/// detection. Joiner verifies against the decrypted GroupInfo before
256+
/// joining.
257+
#[prost(bytes = "vec", tag = "5")]
258+
pub group_state_hash: ::prost::alloc::vec::Vec<u8>,
259+
}
260+
impl ::prost::Name for EncryptedGroupInfoBlob {
261+
const NAME: &'static str = "EncryptedGroupInfoBlob";
262+
const PACKAGE: &'static str = "xmtp.mls.message_contents";
263+
fn full_name() -> ::prost::alloc::string::String {
264+
"xmtp.mls.message_contents.EncryptedGroupInfoBlob".into()
265+
}
266+
fn type_url() -> ::prost::alloc::string::String {
267+
"/xmtp.mls.message_contents.EncryptedGroupInfoBlob".into()
268+
}
269+
}
189270
/// Message for group mutable metadata
190271
#[derive(Clone, PartialEq, ::prost::Message)]
191272
pub struct GroupMutableMetadataV1 {
@@ -520,6 +601,66 @@ impl Compression {
520601
}
521602
}
522603
}
604+
/// v1 external-commit-policy payload.
605+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
606+
pub struct ExternalCommitPolicyV1 {
607+
/// Master switch for MLS External Commits adding new members.
608+
/// Required for the QR-invite flow. Defaults to false; admins
609+
/// (super-admin by default) opt in via
610+
/// AppDataUpdate(EXTERNAL_COMMIT_POLICY).
611+
#[prost(bool, tag = "1")]
612+
pub allow_external_commit: bool,
613+
/// Wall-clock auto-disable timestamp (ns since UNIX epoch).
614+
/// 0 = no automatic expiry. After this timestamp the validator
615+
/// rejects all external commits regardless of `allow_external_commit`.
616+
/// Lets admins issue time-bounded invite campaigns without having to
617+
/// come back and flip the bit manually.
618+
#[prost(uint64, tag = "2")]
619+
pub expires_at_ns: u64,
620+
/// Maximum staleness of the GroupInfo referenced by an external
621+
/// commit, in nanoseconds since GroupInfo export. 0 = no staleness
622+
/// limit. External commits whose referenced GroupInfo was exported
623+
/// more than `expire_in_ns` ago are rejected. Narrows the replay
624+
/// window for stolen-blob attacks and forces re-export frequency.
625+
#[prost(uint64, tag = "3")]
626+
pub expire_in_ns: u64,
627+
}
628+
impl ::prost::Name for ExternalCommitPolicyV1 {
629+
const NAME: &'static str = "ExternalCommitPolicyV1";
630+
const PACKAGE: &'static str = "xmtp.mls.message_contents";
631+
fn full_name() -> ::prost::alloc::string::String {
632+
"xmtp.mls.message_contents.ExternalCommitPolicyV1".into()
633+
}
634+
fn type_url() -> ::prost::alloc::string::String {
635+
"/xmtp.mls.message_contents.ExternalCommitPolicyV1".into()
636+
}
637+
}
638+
/// Versioned envelope. New variants are added as new oneof variants;
639+
/// readers that don't recognize a variant treat the policy as default
640+
/// (all fields zero) per the standard unknown-variant tolerance rules.
641+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
642+
pub struct ExternalCommitPolicyEntry {
643+
#[prost(oneof = "external_commit_policy_entry::Version", tags = "1")]
644+
pub version: ::core::option::Option<external_commit_policy_entry::Version>,
645+
}
646+
/// Nested message and enum types in `ExternalCommitPolicyEntry`.
647+
pub mod external_commit_policy_entry {
648+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Oneof)]
649+
pub enum Version {
650+
#[prost(message, tag = "1")]
651+
V1(super::ExternalCommitPolicyV1),
652+
}
653+
}
654+
impl ::prost::Name for ExternalCommitPolicyEntry {
655+
const NAME: &'static str = "ExternalCommitPolicyEntry";
656+
const PACKAGE: &'static str = "xmtp.mls.message_contents";
657+
fn full_name() -> ::prost::alloc::string::String {
658+
"xmtp.mls.message_contents.ExternalCommitPolicyEntry".into()
659+
}
660+
fn type_url() -> ::prost::alloc::string::String {
661+
"/xmtp.mls.message_contents.ExternalCommitPolicyEntry".into()
662+
}
663+
}
523664
/// Message for group mutable metadata
524665
#[derive(Clone, PartialEq, ::prost::Message)]
525666
pub struct GroupMutablePermissionsV1 {
@@ -927,9 +1068,18 @@ pub struct ComponentMetadata {
9271068
/// The data structure type of the component's value
9281069
#[prost(enumeration = "ComponentType", tag = "1")]
9291070
pub component_type: i32,
930-
/// Permission policies for this component
1071+
/// Permission policies for this component, evaluated against regular
1072+
/// (member-issued) commits.
9311073
#[prost(message, optional, tag = "2")]
9321074
pub permissions: ::core::option::Option<ComponentPermissions>,
1075+
/// Permission policies for this component, evaluated against MLS External
1076+
/// Commits (RFC 9420 §12.4.3.2). Absent / unset is equivalent to all-Deny:
1077+
/// external committers cannot touch this component. Each component opts in
1078+
/// explicitly by setting this field. Combined with the JOIN_POLICY master
1079+
/// switch (`allow_external_commit`), this is the per-component declarative
1080+
/// authorization for external-commit-driven joins.
1081+
#[prost(message, optional, tag = "3")]
1082+
pub external_committer_permissions: ::core::option::Option<ComponentPermissions>,
9331083
}
9341084
impl ::prost::Name for ComponentMetadata {
9351085
const NAME: &'static str = "ComponentMetadata";

0 commit comments

Comments
 (0)