fix(marmot): sign commits under pre-proposal extensions (fix InvalidMembershipTag)
For a GroupContextExtensions commit, openmls signs the `FramedContentTBS` over `group.public_group.group_context()` — the UNMUTATED context, before the `diff` applies the GCE proposal. The post-proposal extensions only make it into the HPKE path-encryption context and the subsequent key schedule, not into the TBS / signature / membership_tag. Quartz's `applyProposal` handler for `GroupContextExtensions` mutates `groupContext.extensions` inline — by the time `commit()` captured `preCommitContextBytes`, the extensions were already updated. The signature and membership_tag were then minted over the post-GCE context, and openmls's `verify_membership` recomputed the MAC over the pre-GCE context and reported `ValidationError(InvalidMembershipTag)` on every cross-impl rename / promote / demote / leave. Fix: snapshot `groupContext.extensions` before any proposal is applied, and rebuild `preCommitContextBytes` with those original extensions. The path-encryption context and post-commit key schedule still use the updated extensions, matching openmls. https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
This commit is contained in:
@@ -411,6 +411,15 @@ class MlsGroup private constructor(
|
|||||||
val preCommitExporterSecret =
|
val preCommitExporterSecret =
|
||||||
exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
||||||
|
|
||||||
|
// Snapshot the pre-proposal extensions. GroupContextExtensions proposals
|
||||||
|
// mutate `groupContext.extensions` the moment they're applied, but
|
||||||
|
// openmls signs the commit's FramedContentTBS over the UNMUTATED
|
||||||
|
// context (the one in `public_group` before `diff` applies proposals).
|
||||||
|
// Without this snapshot, a GCE commit on the quartz side uses the new
|
||||||
|
// extensions for TBS + membership_tag, and openmls rejects it with
|
||||||
|
// `ValidationError(InvalidMembershipTag)`.
|
||||||
|
val preCommitExtensions = groupContext.extensions
|
||||||
|
|
||||||
val proposalOrRefs = proposals.map { ProposalOrRef.Inline(it.proposal) }
|
val proposalOrRefs = proposals.map { ProposalOrRef.Inline(it.proposal) }
|
||||||
|
|
||||||
// Check if we need an UpdatePath. RFC 9420 §12.4.1: the path value
|
// Check if we need an UpdatePath. RFC 9420 §12.4.1: the path value
|
||||||
@@ -601,8 +610,10 @@ class MlsGroup private constructor(
|
|||||||
// is sent — the one we're about to leave. Receivers (openmls/mdk)
|
// is sent — the one we're about to leave. Receivers (openmls/mdk)
|
||||||
// strict-verify both, so we must use the leaf signing key that's
|
// strict-verify both, so we must use the leaf signing key that's
|
||||||
// still in the pre-commit tree and the membership_key derived from
|
// still in the pre-commit tree and the membership_key derived from
|
||||||
// the pre-commit epoch secrets.
|
// the pre-commit epoch secrets. Extensions need explicit rewind to
|
||||||
val preCommitContextBytes = groupContext.toTlsBytes()
|
// pre-proposal state for GroupContextExtensions commits.
|
||||||
|
val preCommitContextBytes =
|
||||||
|
groupContext.copy(extensions = preCommitExtensions).toTlsBytes()
|
||||||
val preCommitMembershipKey = epochSecrets.membershipKey
|
val preCommitMembershipKey = epochSecrets.membershipKey
|
||||||
val preCommitSigningKey = signingPrivateKey
|
val preCommitSigningKey = signingPrivateKey
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user