mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
docs(mls): record the engine boundary; last hardcoded profile bits
Three leftovers from the extraction, plus the documentation it needs to
survive.
`KeyPackageRotationManager` still built its leaf capabilities from bare hex
(`0x000A`, `0xF2EE`, `0x000A`) with the two meanings of `0x000A` -- last_resort
as an extension, self_remove as a proposal -- distinguishable only by which
list they were in. That set is now `MarmotCapabilities.mipKeyPackageLeaf()`,
beside the other two profiles, with the ordering marked load-bearing: those
bytes go into published KeyPackages and define KeyPackageBundleStore's v4
snapshot format.
`MarmotManager` and `MarmotConvergenceEngine` still spelled
`exporterSecret("marmot", "group-event", 32)` literally at three call sites, so
the binding's key derivation was stated in four places. They read it off
`MarmotGroupPolicy.commitExporter` now, which is where the engine reads it too.
`CurrentProfileWelcomeTest` and `CommitPreservesLeafIdentityTest` sat in the
`mls/` test tree with ten and three Marmot imports between them; they test
Marmot's current profile, so they move to `marmot/appComponents/`.
The README states the invariant as a command you can run, and scopes it
honestly: shipped code only. Two tests under `mls/components/` do decode real
Marmot components, because an interop test is worth more against payloads that
actually exist -- a fixture is data, an import in the engine is a dependency.
It also writes down the three things a second binding has to know, the one
that will bite (a policy is behaviour, not state, so a restore that forgets it
silently drops the rules), and why the default is permissive rather than
closed.
The plan's Stage 1 is marked landed with what it cost and what Stage 3 still
owes: MlsGroupManager names `nostrGroupId` 147 times, so cordn needs its own
manager over the same MlsGroup rather than reusing that one. §5.1's coupling
measurements are marked superseded rather than deleted -- they are what the
stage was scoped against.
Verified: :quartz:jvmTest 5082, :commons:jvmTest 2202, both green;
:quic, :cli, :marmotBench and :amethyst all compile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
@@ -207,8 +207,8 @@ import com.vitorpamplona.quartz.experimental.profileGallery.hash
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.image
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.cli.stores
|
||||
import com.vitorpamplona.amethyst.cli.SecureFileIO
|
||||
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
|
||||
import com.vitorpamplona.quartz.marmot.MarmotIngestDedupStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
+7
-4
@@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotGroupSnapshot
|
||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotMessageEdit
|
||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemEvent
|
||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemRowDiff
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotGroupPolicy
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupManager
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
@@ -90,14 +91,14 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
/**
|
||||
* Central coordinator for Marmot MLS group messaging.
|
||||
@@ -327,9 +328,11 @@ class MarmotManager(
|
||||
): ByteArray? =
|
||||
try {
|
||||
val preCommitKey =
|
||||
MarmotGroupPolicy.commitExporter.let { exporter ->
|
||||
MlsGroup
|
||||
.restore(obligation.priorState)
|
||||
.exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
||||
.restore(obligation.priorState, MarmotGroupPolicy)
|
||||
.exporterSecret(exporter.label, exporter.context, exporter.length)
|
||||
}
|
||||
GroupEventEncryption.decrypt(event.content, preCommitKey)
|
||||
} catch (e: Exception) {
|
||||
Log.w(
|
||||
|
||||
+2
-2
@@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.PreviewStat
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicException
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicStream
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.marmot
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.marmot
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.marmot
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
|
||||
+1
-1
@@ -20,8 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.marmot
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.protocolCore.GroupLifecycleState
|
||||
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
+1
-1
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.marmot
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
// In-memory stand-ins for the durable stores a MarmotManager needs.
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
package com.vitorpamplona.marmotbench
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.marmot.MarmotPublisher
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
// In-memory stores, matching the commons test doubles byte for byte.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Cordn interop: extract the MLS core, then add a second binding
|
||||
|
||||
Status: Stage 2 landed. `:contextvm` implements the core spec plus all 12 CEPs on the client
|
||||
side, with the Tier C fixture server, at 172 tests. Stages 0 (cordn-side vectors), 1 (extract the
|
||||
MLS engine) and 3-4 (the cordn binding and app integration) are open. Stage 3 is the one gated on
|
||||
the §4.1 upstream decision.
|
||||
Status: Stages 1 and 2 landed. The RFC 9420 engine is `quartz/…/mls/` and imports nothing from
|
||||
`marmot/` — a binding supplies its rules through `MlsGroupPolicy`. `:contextvm` implements the
|
||||
core spec plus all 12 CEPs on the client side, with the Tier C fixture server, at 172 tests.
|
||||
Stages 0 (cordn-side vectors) and 3-4 (the cordn binding and app integration) are open; Stage 3
|
||||
is the one gated on the §4.1 upstream decision.
|
||||
|
||||
Correction to an earlier gate in this plan: §4.1 does **not** block Stage 2. ContextVM is
|
||||
credential-agnostic and has no MLS dependency at all, so the transport was safe to build first;
|
||||
@@ -201,7 +202,11 @@ wire format. There is nothing to implement against. Do not attempt it.
|
||||
|
||||
### 5.1 The engine, and how Marmot-clean it is
|
||||
|
||||
`quartz/…/marmot/mls/` is **11,964 LOC**. Measured coupling to the Marmot layer:
|
||||
**Superseded by Stage 1, which landed.** The measurements below are what the engine looked like
|
||||
before the extraction; they are kept because they are what the stage was scoped against. The
|
||||
engine is now `quartz/…/mls/` with zero `marmot/` imports.
|
||||
|
||||
`quartz/…/marmot/mls/` was **11,964 LOC**. Measured coupling to the Marmot layer:
|
||||
|
||||
- **3 files**, **10 imports** total:
|
||||
- `group/MlsGroup.kt` (7): `MarmotGroupData`, `MarmotGroupState`, `AdminPolicyV1`,
|
||||
@@ -637,29 +642,57 @@ No production code. Two deliverables.
|
||||
|
||||
**Gate:** do not start Stage 2 until §4.1 has an answer. Stage 1 is safe to do regardless.
|
||||
|
||||
### Stage 1 — Extract a binding-agnostic RFC 9420 engine
|
||||
### Stage 1 — Extract a binding-agnostic RFC 9420 engine — LANDED
|
||||
|
||||
Worth doing whether or not cordn ever ships. Today the engine is one protocol's private detail;
|
||||
this makes it a library.
|
||||
Done in four commits. The engine is `quartz/…/mls/` and imports nothing from `quartz/…/marmot/`.
|
||||
|
||||
- Move `quartz/…/marmot/mls/` → `quartz/…/mls/`. Packages change; `marmot/` keeps everything
|
||||
else.
|
||||
- Parameterize what §5.1 lists as hardcoded: group id, credential identity bytes,
|
||||
`required_capabilities`, leaf `capabilities`, and the exporter label/context all become
|
||||
constructor or call-site inputs. `exporterSecret(label, context, length)` already exists —
|
||||
stop calling it with a literal `"marmot"` from inside the engine.
|
||||
- Push the Marmot-specific reads out to `marmot/`: `currentMarmotData()`, `currentGroupState()`,
|
||||
`currentNostrGroupId()`, the `AdminPolicyV1`/`GroupLifecycleV1` hooks and the agent-text-stream
|
||||
helpers. Where the engine needs a policy decision, it takes an interface; `marmot/` supplies the
|
||||
Marmot implementation.
|
||||
- Move `group/MarmotMessageStore.kt` out (it is named for Marmot and keyed on
|
||||
`nostrGroupId` — it is a binding concern).
|
||||
- Behaviour must not change. The existing MLS + Marmot test suites are the contract; they pass
|
||||
unmodified except for import lines.
|
||||
| What | Where it went |
|
||||
| ---- | ------------- |
|
||||
| The engine (28 files, 11,964 LOC) | `quartz/…/marmot/mls/` → `quartz/…/mls/` |
|
||||
| `MlsGroupManager`, `MlsGroupStateStore`, `MarmotMessageStore` | → `marmot/groups/` (all keyed on `nostrGroupId`) |
|
||||
| MIP-03 authorization, depletion guard, join role check, self-remove gate | → `marmot/groups/MarmotGroupPolicy` |
|
||||
| Leaf + `required_capabilities` profiles | → `marmot/groups/MarmotCapabilities` |
|
||||
| `currentMarmotData/GroupState/NostrGroupId`, `agentTextStreamSecret` | → `marmot/groups/MarmotGroupViews` (extension functions) |
|
||||
| `last_resort_key_package` (0x0004) | → `mls/components/ComponentsList` — it is the extensions draft's, not Marmot's |
|
||||
|
||||
Risk: `MlsGroup.kt` is 4,505 lines and carries the convergence/lifecycle logic. Keep this stage
|
||||
strictly mechanical — extraction and parameterization, no logic edits — so the diff stays
|
||||
reviewable and the test suite is a real check.
|
||||
**The seam is `MlsGroupPolicy`**: three hooks the engine calls where RFC 9420 defers to the
|
||||
application (`authorizeCommit`, `authorizeSelfRemove`, `validateJoin`) and four values it reads
|
||||
(leaf capabilities, `required_capabilities`, extra known extension types, the commit exporter
|
||||
label). One argument selects a whole profile — `MlsGroup.create(id, policy = MarmotGroupPolicy)`
|
||||
brings the rules, the capabilities and `MLS-Exporter("marmot", "group-event", 32)` together — so
|
||||
adopting it cost one added argument per call site rather than five.
|
||||
|
||||
Policies receive a read-only `GroupView`, not the `MlsGroup`: a policy holding the group could
|
||||
commit or rotate keys from inside the check meant to gate those things.
|
||||
|
||||
**The default is permissive**, which is a trade worth naming. Closed would make the engine
|
||||
unusable without a policy and would push callers into writing an allow-everything one anyway.
|
||||
The cost is that a group restored without its policy silently drops the binding's rules — a
|
||||
policy is behaviour, not state, so it is deliberately not in `MlsGroupState`. All ten production
|
||||
construction sites are in `marmot/` and all ten name it.
|
||||
|
||||
Two findings from doing it:
|
||||
|
||||
1. **`:quic` was already a second consumer, reaching through the wrong package.**
|
||||
`quic/tls/TlsClient.kt` imported `quartz.marmot.mls.crypto.X25519` for its TLS 1.3 handshake —
|
||||
neither Marmot nor MLS. That is the argument for this stage independent of cordn.
|
||||
2. **The test suite found every site that had been relying on Marmot defaults.** The first run
|
||||
after `MlsGroup.create` stopped defaulting to Marmot's profile failed 13 tests, each one a
|
||||
Marmot test that had been getting a Marmot-shaped group for free. The other ~180 construction
|
||||
sites kept passing on the permissive default — they are engine tests, and they now prove the
|
||||
engine runs without Marmot at all.
|
||||
|
||||
8 tests were added for the seam itself (`MlsGroupPolicySeamTest`, `MarmotPolicySeamTest`),
|
||||
including the half no existing test covered: the same group at the same state accepts the same
|
||||
commit once the policy is gone. Verified by mutation — ignoring the policy in `commit()` and
|
||||
re-hardcoding the exporter label each kill exactly their guarding tests.
|
||||
|
||||
Suite: `:quartz:jvmTest` 5074 → 5082, `:commons:jvmTest` 2202, both green.
|
||||
|
||||
What Stage 3 still owes: `MlsGroupManager` is keyed on `nostrGroupId` 147 times, so cordn cannot
|
||||
reuse it and needs its own manager over the same `MlsGroup`. Class names were left alone in the
|
||||
move — `MlsGroupManager` under `marmot/groups/` reads correctly and renaming four classes would
|
||||
have churned 22 files across five modules for clarity the package path already gives.
|
||||
|
||||
### Stage 2 — `:contextvm` module (clean-room) — LANDED
|
||||
|
||||
|
||||
+19
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.mls.codec.TlsWriter
|
||||
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
||||
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage
|
||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
||||
import com.vitorpamplona.quartz.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.mls.tree.Extension
|
||||
@@ -59,6 +60,24 @@ object MarmotCapabilities {
|
||||
proposals = listOf(MlsGroup.SELF_REMOVE_PROPOSAL_TYPE),
|
||||
)
|
||||
|
||||
/**
|
||||
* The MIP-era leaf set as a published KeyPackage advertises it.
|
||||
*
|
||||
* Same as [mipLeaf] plus `0x000A` as an EXTENSION, which OpenMLS validation
|
||||
* requires on a last-resort KeyPackage. Note `0x000A` appears in both lists
|
||||
* meaning different things: as an extension it is `last_resort`, as a
|
||||
* proposal it is `self_remove`.
|
||||
*
|
||||
* The order is load-bearing and must not be tidied: these bytes go into
|
||||
* published KeyPackages, and `KeyPackageBundleStore`'s v4 snapshot format
|
||||
* is defined by them.
|
||||
*/
|
||||
fun mipKeyPackageLeaf(): Capabilities =
|
||||
Capabilities(
|
||||
extensions = listOf(MlsKeyPackage.LAST_RESORT_EXTENSION_TYPE, MARMOT_GROUP_DATA_EXTENSION_TYPE),
|
||||
proposals = listOf(MlsGroup.SELF_REMOVE_PROPOSAL_TYPE),
|
||||
)
|
||||
|
||||
/**
|
||||
* Build an MLS `required_capabilities` extension that marks Marmot's
|
||||
* mandatory interop set as required for all members (RFC 9420 §7.2):
|
||||
|
||||
+2
-13
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||
import com.vitorpamplona.quartz.marmot.groups.MarmotCapabilities
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.SNAPSHOT_VERSION
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MlsCiphersuite
|
||||
import com.vitorpamplona.quartz.mls.codec.TlsReader
|
||||
@@ -30,7 +31,6 @@ import com.vitorpamplona.quartz.mls.crypto.MlsCryptoProvider
|
||||
import com.vitorpamplona.quartz.mls.crypto.X25519
|
||||
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
||||
import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage
|
||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
||||
import com.vitorpamplona.quartz.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.mls.tree.Extension
|
||||
import com.vitorpamplona.quartz.mls.tree.LeafNode
|
||||
@@ -654,18 +654,7 @@ class KeyPackageRotationManager(
|
||||
encryptionKey = encryptionKey,
|
||||
signatureKey = signatureKey,
|
||||
credential = Credential.Basic(identity),
|
||||
capabilities =
|
||||
Capabilities(
|
||||
extensions =
|
||||
listOf(
|
||||
0x000A, // LastResort (required by OpenMLS validation)
|
||||
0xF2EE, // NostrGroupData (required by group's RequiredCapabilities)
|
||||
),
|
||||
proposals =
|
||||
listOf(
|
||||
0x000A, // SelfRemove (required by group's RequiredCapabilities)
|
||||
),
|
||||
),
|
||||
capabilities = MarmotCapabilities.mipKeyPackageLeaf(),
|
||||
leafNodeSource = LeafNodeSource.KEY_PACKAGE,
|
||||
lifetime = Lifetime(notBefore = now, notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS),
|
||||
extensions = emptyList(),
|
||||
|
||||
+3
-1
@@ -423,7 +423,9 @@ class MarmotConvergenceEngine(
|
||||
mutex.withLock {
|
||||
contexts[groupId]?.candidateStates?.values?.mapNotNull { state ->
|
||||
try {
|
||||
MlsGroup.restore(state, MarmotGroupPolicy).exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
||||
MarmotGroupPolicy.commitExporter.let {
|
||||
MlsGroup.restore(state, MarmotGroupPolicy).exporterSecret(it.label, it.context, it.length)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# `mls/` — the RFC 9420 engine
|
||||
|
||||
A binding-agnostic MLS (RFC 9420) implementation: TLS presentation-language
|
||||
codec, HPKE, X25519/Ed25519, the ratchet tree, the key schedule, the secret
|
||||
tree, message framing, proposals, commits, Welcome, and the `app_data_dictionary`
|
||||
component carrier from `draft-ietf-mls-extensions-10`.
|
||||
|
||||
**It knows nothing about Marmot, Nostr, or cordn, and must stay that way.** The
|
||||
invariant, over the shipped code:
|
||||
|
||||
```bash
|
||||
grep -rn 'import com.vitorpamplona.quartz.marmot' \
|
||||
--include='*.kt' quartz/src/{commonMain,jvmAndroid,appleMain,linuxMain}/kotlin/com/vitorpamplona/quartz/mls/
|
||||
```
|
||||
|
||||
That must print nothing. Tests are deliberately outside it: a couple under
|
||||
`mls/components/` decode real Marmot components as fixtures, because the point
|
||||
of an interop test is to exercise the engine against payloads that actually
|
||||
exist. A fixture is data; an import in the engine is a dependency.
|
||||
|
||||
It lived under `marmot/` until Stage 1 of
|
||||
`quartz/plans/2026-09-17-cordn-interop.md`, and `:quic` was already importing
|
||||
`crypto/X25519` from there for its TLS 1.3 handshake — a use that is neither
|
||||
Marmot nor MLS, and the reason the extraction was overdue.
|
||||
|
||||
## Using it
|
||||
|
||||
```kotlin
|
||||
val group = MlsGroup.create(identity) // a plain RFC 9420 group
|
||||
val group = MlsGroup.create(identity, policy = MarmotGroupPolicy) // Marmot's profile
|
||||
```
|
||||
|
||||
The default group requires nothing beyond RFC 9420: no `required_capabilities`,
|
||||
no leaf capabilities (§7.2 forbids advertising DEFAULT types), no commit
|
||||
exporter, and no limit on who may commit what.
|
||||
|
||||
## The `MlsGroupPolicy` seam
|
||||
|
||||
RFC 9420 says who may *send* a proposal and never who may *commit* one. Real
|
||||
deployments need more — Marmot's MIP-03 names admin accounts and allows everyone
|
||||
else only a self-Update or a SelfRemove — so the engine asks a policy wherever
|
||||
the spec defers to the application:
|
||||
|
||||
| Hook | Called from | Marmot uses it for |
|
||||
| ---- | ----------- | ------------------ |
|
||||
| `authorizeCommit` | `commit()` and both inbound commit paths | MIP-03 proposal-set rules + admin depletion |
|
||||
| `authorizeSelfRemove` | `proposeSelfRemove`, `buildSelfRemoveProposalMessage` | admin must self-demote first |
|
||||
| `validateJoin` | `processWelcome` | `required_member_roles` on the agent-text-stream component |
|
||||
| `defaultLeafCapabilities` | every leaf the engine builds | `0xF2EE` + `self_remove` |
|
||||
| `defaultRequiredCapabilities` | `create()`, epoch 0 | the MIP-era interop set |
|
||||
| `knownExtensionTypes` | GroupContextExtensions validation | `0xF2EE` |
|
||||
| `commitExporter` | `CommitResult.preCommitExporterSecret` | `MLS-Exporter("marmot", "group-event", 32)` |
|
||||
|
||||
Three things to know before you add a binding:
|
||||
|
||||
1. **One argument selects a whole profile.** `policy` supplies the rules *and*
|
||||
the capability defaults *and* the exporter binding, so `capabilities` and
|
||||
`requiredCapabilities` default from it. Adopting a profile is one argument,
|
||||
not five.
|
||||
|
||||
2. **A policy is behaviour, not state.** It is deliberately absent from
|
||||
`MlsGroupState`, so whatever restores a group must pass the same policy it
|
||||
was created with. A restore that forgets it gets a group that silently skips
|
||||
the binding's rules. In Marmot only `MlsGroupManager` restores a group in
|
||||
order to commit with it, which is what keeps that narrow.
|
||||
|
||||
3. **A policy gets a `GroupView`, not the `MlsGroup`.** A policy holding the
|
||||
group could commit or rotate keys from inside the check meant to gate exactly
|
||||
that.
|
||||
|
||||
The default is permissive rather than closed. Closed would make the engine
|
||||
unusable without a policy and would push callers into writing an
|
||||
allow-everything one anyway; open puts each restriction in the binding that
|
||||
documents it. The cost is item 2 above.
|
||||
|
||||
## Where the bindings live
|
||||
|
||||
- `quartz/…/marmot/groups/` — `MarmotGroupPolicy`, `MarmotCapabilities`,
|
||||
`MarmotGroupViews` (Marmot's reads of a GroupContext, as extension functions),
|
||||
plus `MlsGroupManager` and the two stores, all keyed on `nostrGroupId`.
|
||||
- `quartz/…/marmot/mipXX…/` — the Nostr event layer.
|
||||
|
||||
## Tests
|
||||
|
||||
`quartz/src/commonTest/…/mls/` runs everywhere; `quartz/src/jvmAndroidTest/…/mls/`
|
||||
holds the tests needing real secp256k1/JNI. `mls/interop/` replays the RFC 9420
|
||||
test vectors. `group/MlsGroupPolicySeamTest` pins the seam itself with a
|
||||
recording policy; `marmot/groups/MarmotPolicySeamTest` pins that Marmot's rules
|
||||
travel with `MarmotGroupPolicy` and not with the engine.
|
||||
+2
-1
@@ -18,12 +18,13 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.mls.group
|
||||
package com.vitorpamplona.quartz.marmot.appComponents
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.AppComponentIds
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
||||
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
||||
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.runBlocking
|
||||
+2
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.mls.group
|
||||
package com.vitorpamplona.quartz.marmot.appComponents
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.marmot.groups.currentMarmotData
|
||||
import com.vitorpamplona.quartz.marmot.groups.currentNostrGroupId
|
||||
import com.vitorpamplona.quartz.mls.crypto.Ed25519
|
||||
import com.vitorpamplona.quartz.mls.crypto.Ed25519KeyPair
|
||||
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
Reference in New Issue
Block a user