f9c6a4711f
Two bugs in MlsGroup were blocking 12 of 209 jvmTest cases (all RFC 9420 interop vectors already passed; only our higher-level lifecycle code diverged from the spec). Bug A — write/read asymmetry on member commits. processCommit requires signature + confirmation_tag as separate parameters, but CommitResult only exposed framedCommitBytes (the full PublicMessage envelope). Tests called processCommit(commitBytes, ..., ByteArray(0)) and immediately hit "FramedContentTBS signature missing on commit from leaf N". Add MlsGroup.processFramedCommit(framedCommitBytes) that unpacks the MlsMessage(PublicMessage(Commit)) envelope, verifies membership_tag for member senders (RFC 9420 §6.2), and delegates to processCommit. Mirrors the unwrap MarmotInboundProcessor already does in production. Updates the 9 affected tests to use the new entry point. Bug B — externalJoin used the wrong HPKE info bytes. The joiner encrypted UpdatePath path-secrets against groupContext.toTlsBytes() (pre-mutation), but receivers HPKE-Open against the post-mutation context (epoch+1, new tree_hash) per RFC 9420 §7.6. Result: AEADBadTagException on every external join. Refactor externalJoin to follow the same staging pattern as commit(): stage public keys, applyUpdatePath, patch parent_hash, replace placeholder leaf — then compute pathEncContextBytes from the post- mutation tree, then HPKE-encrypt path-secrets with that context. To make the framed pipeline available to external commits as well, introduce ExternalJoinResult exposing both commitBytes and framedCommitBytes (PublicMessage with sender = new_member_commit, no membership_tag). processFramedCommit resolves NEW_MEMBER_COMMIT senders to the receiver-side leaf index using the same first-blank-or-append algorithm as RatchetTree.addLeaf, so wire decoding doesn't need to carry the joiner's chosen index in the empty Sender field. After: 209/209 MLS tests pass.