-
Notifications
You must be signed in to change notification settings - Fork 35
Refresh, validate, and migrate invitation KeyPackages #1781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
82c5ee1
520bcd4
3352dac
49d589d
55a73be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,6 +323,7 @@ impl<S: StorageProvider> Engine<S> { | |
|
|
||
| let provider = EngineOpenMlsProvider::<S>::new(&self.crypto, self.storage.mls_storage()); | ||
| let key_package = validate_key_package(kp_in, provider.crypto())?; | ||
| validate_invitee_capabilities(&key_package)?; | ||
| // foundation/key-packages.md: reject a KeyPackage whose credential | ||
| // identity is not a valid Marmot account identity. This single gate | ||
| // covers both the create-group and invite invitee paths. | ||
|
|
@@ -336,6 +337,31 @@ impl<S: StorageProvider> Engine<S> { | |
| } | ||
| } | ||
|
|
||
| /// Enforce the RFC 9420 section 7.2 advertisement rule before using a | ||
| /// KeyPackage for a new membership operation. Keep this out of the shared | ||
| /// storage/maintenance validator: old private bundles may still be needed to | ||
| /// process Welcomes sent before the peer refreshed its public KeyPackage. | ||
| fn validate_invitee_capabilities(key_package: &MlsKeyPackage) -> Result<(), EngineError> { | ||
| use crate::capabilities::{DEFAULT_MLS_EXTENSION_TYPES, DEFAULT_MLS_PROPOSAL_TYPES}; | ||
|
|
||
| let capabilities = key_package.leaf_node().capabilities(); | ||
| if capabilities | ||
| .extensions() | ||
| .iter() | ||
| .any(|kind| DEFAULT_MLS_EXTENSION_TYPES.contains(&u16::from(*kind))) | ||
| || capabilities | ||
| .proposals() | ||
| .iter() | ||
| .any(|kind| DEFAULT_MLS_PROPOSAL_TYPES.contains(&u16::from(*kind))) | ||
| { | ||
| return Err(EngineError::Backend( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Separately, the message names no member. An app creating a ten-person group gets "default capabilities must not be advertised" with no way to tell the user which invitee must republish. A dedicated variant carrying the credential identity (or a per-member rejection at the app resolution boundary, where |
||
| "key_package validate: default capabilities must not be advertised (RFC 9420 section 7.2)" | ||
| .into(), | ||
| )); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn ensure_key_package_profile( | ||
| key_package: &KeyPackage, | ||
| wire_profile: ProtocolProfile, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,9 +79,15 @@ separate cache reads, membership, provider response, profile hydration, and netw | |
| queries or identities. | ||
|
|
||
| Group creation and invites still take pubkeys at the action boundary. The app canonicalizes and deduplicates the | ||
| requested roster, reuses current cached KeyPackages, and resolves cold members in bounded multi-author relay batches | ||
| before building the MLS add. Hosts may prewarm that same bounded composition lookup without reserving packages or | ||
| durably admitting strangers; the final mutation revalidates every package. New Nostr-routed groups generate | ||
| requested roster and fetches current KeyPackages in bounded multi-author relay batches before building the MLS add. | ||
| Cached packages remain useful for discovery, but cannot authorize an invitation or substitute for a failed relay | ||
| lookup. Hosts may prewarm that same bounded composition lookup without reserving packages or durably admitting | ||
| strangers; the final action reuses discovery routes but fetches packages again before the mutation validates them. This | ||
| also applies to another account on the same installation: its local package record is not an invitation shortcut, | ||
| and its published package must be reachable on relays. Relay freshness is not proof that the recipient still owns | ||
| private material; it avoids authorizing from a stale local copy. Each prewarm call requests a fresh readiness signal, | ||
| so hosts should debounce roster changes. The process-local prewarm cache retains only bounded relay metadata. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc drift missed in this pass: |
||
| New Nostr-routed groups generate | ||
| `marmot.transport.nostr.routing.v1` at creation, store the component bytes in | ||
| signed MLS app data, and project the decoded `nostr_group_id` plus relay list into group subscriptions and publish | ||
| targets. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,7 +60,6 @@ pub(crate) enum AppPerformanceOperation { | |
| GroupCreateQueueWait, | ||
| GroupCreateKeyPackageLookup, | ||
| GroupMemberKeyPackagePrewarm, | ||
| GroupCreateKeyPackageCacheReuse, | ||
| GroupCreateKeyPackageNetworkResolution, | ||
| GroupCreateImagePreprocess, | ||
| GroupCreateImageUpload, | ||
|
|
@@ -318,6 +317,7 @@ pub struct AppPerformanceSnapshot { | |
| #[serde(default)] | ||
| pub group_member_key_package_prewarm: AppPerformanceOperationSnapshot, | ||
| #[serde(default)] | ||
| /// Retired counter retained for export/API compatibility; no new samples. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the doc comment sits after |
||
| pub group_create_key_package_cache_reuse: AppPerformanceOperationSnapshot, | ||
| #[serde(default)] | ||
| pub group_create_key_package_network_resolution: AppPerformanceOperationSnapshot, | ||
|
|
@@ -813,11 +813,6 @@ impl AppPerformanceTelemetry { | |
| .group_member_key_package_prewarm | ||
| .record(duration, success); | ||
| } | ||
| AppPerformanceOperation::GroupCreateKeyPackageCacheReuse => { | ||
| inner | ||
| .group_create_key_package_cache_reuse | ||
| .record(duration, success); | ||
| } | ||
| AppPerformanceOperation::GroupCreateKeyPackageNetworkResolution => { | ||
| inner | ||
| .group_create_key_package_network_resolution | ||
|
|
@@ -1502,7 +1497,6 @@ mod tests { | |
| AppPerformanceOperation::GroupCreateQueueWait, | ||
| AppPerformanceOperation::GroupCreateKeyPackageLookup, | ||
| AppPerformanceOperation::GroupMemberKeyPackagePrewarm, | ||
| AppPerformanceOperation::GroupCreateKeyPackageCacheReuse, | ||
| AppPerformanceOperation::GroupCreateKeyPackageNetworkResolution, | ||
| AppPerformanceOperation::GroupCreateImagePreprocess, | ||
| AppPerformanceOperation::GroupCreateImageUpload, | ||
|
|
@@ -1520,11 +1514,11 @@ mod tests { | |
| } | ||
|
|
||
| let snapshot = telemetry.snapshot(); | ||
| assert_eq!(snapshot.group_create_key_package_cache_reuse.attempts, 0); | ||
| for stage in [ | ||
| snapshot.group_create_queue_wait, | ||
| snapshot.group_create_key_package_lookup, | ||
| snapshot.group_member_key_package_prewarm, | ||
| snapshot.group_create_key_package_cache_reuse, | ||
| snapshot.group_create_key_package_network_resolution, | ||
| snapshot.group_create_image_preprocess, | ||
| snapshot.group_create_image_upload, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1183,9 +1183,9 @@ impl AppClient { | |
| Ok(self.runtime.publish_fresh_key_package().await?) | ||
| } | ||
|
|
||
| /// Resolve and cache the current composition roster without reserving or | ||
| /// consuming any KeyPackage. Group creation revalidates the cached bytes | ||
| /// and the MLS mutation boundary retains its ordinary validation. | ||
| /// Fetch current relay KeyPackages for the composition roster without | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documented create-time telemetry contract is now false and one metric is dead.
All three now describe behavior that cannot occur. Please either retire the |
||
| /// reserving or consuming them. Group creation fetches again before the | ||
| /// MLS mutation; cached packages only inform discovery. | ||
| pub async fn prewarm_group_member_key_packages( | ||
| &self, | ||
| member_refs: &[&str], | ||
|
|
@@ -1529,16 +1529,14 @@ impl AppClient { | |
| key_packages.is_ok(), | ||
| ); | ||
| let resolved = key_packages?; | ||
| record_app_performance( | ||
| telemetry, | ||
| if resolved.stats.network_resolved_members == 0 { | ||
| AppPerformanceOperation::GroupCreateKeyPackageCacheReuse | ||
| } else { | ||
| AppPerformanceOperation::GroupCreateKeyPackageNetworkResolution | ||
| }, | ||
| key_package_elapsed, | ||
| true, | ||
| ); | ||
| if resolved.stats.unique_members > 0 { | ||
| record_app_performance( | ||
| telemetry, | ||
| AppPerformanceOperation::GroupCreateKeyPackageNetworkResolution, | ||
| key_package_elapsed, | ||
| true, | ||
| ); | ||
| } | ||
| let members = resolved.key_packages; | ||
| self.refresh_routing()?; | ||
| let nostr_routing = self.app.new_nostr_routing()?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bundled change with the largest blast radius in this PR.
mdk itself advertised
ExtensionType::RequiredCapabilities(0x0003) in every leaf until #1709 landed on 2026-09-06 (first shipped in v0.9.19). Those packages stay lifetime-valid for up to ~3 months (OpenMLS default3 * 28 days + 1h, not overridden here), andrepublish_key_packagedeliberately reuses the current artifact rather than rotating, so they stay published. After this check, any peer whose published package predates that fix -- including everyone still on v0.9.18 or earlier -- cannot be created-with or invited at all. The same PR removes the cached-package fallback, so there is no second path around it.RFC 9420 section 7.3 leaf-node validation does not require this check, and tolerating a stray 0x0003 advertisement is harmless: the group's required-capabilities computation is unaffected. So the strictness buys conformance tidiness at a real availability cost against our own recent releases.
Two separable asks: (1) land this as its own PR rather than inside a caching fix, since it needs its own compatibility decision; (2) state the rollout -- warn + telemetry for a release, or accept-and-ignore per RFC extensibility -- before failing closed on packages mdk generated four days ago.