fix(marmot): derive commit_secret one step past the root path_secret
RFC 9420 §9.2: `commit_secret = DeriveSecret(path_secret_at_root, "path")`. Openmls / mdk implement this exactly — after deriving the ratchet-tree's own path_secrets up to (and including) the root, they advance one more step and use THAT as the commit_secret contribution to the new epoch's key schedule. Quartz was using the root's own path_secret as commit_secret on both the encryption side (MlsGroup.commit) and the decryption side (MlsGroup.processCommit), plus in externalJoin's commit construction. Internally consistent, so quartz↔quartz worked; but every cross-impl commit (quartz→openmls or openmls→quartz) derived a different epoch_secret, cascaded into a different confirmation_key, and failed at `InvalidCommit(ConfirmationTagMismatch)` — the blocker behind every test that actually needed an openmls peer to accept a quartz-produced commit or vice-versa once the UpdatePath-layer bugs were out of the way. Fix: add one `DeriveSecret(pathSecrets.last().pathSecret, "path")` step on the sender side, and one extra `DeriveSecret(..., "path")` past the root on the receiver side, mirroring openmls's `ParentNode::derive_path` loop post-condition. https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
This commit is contained in:
+21
-10
@@ -574,10 +574,17 @@ class MlsGroup private constructor(
|
||||
val commit = Commit(proposalOrRefs, updatePath)
|
||||
val commitBytes = commit.toTlsBytes()
|
||||
|
||||
// Advance epoch
|
||||
// Advance epoch.
|
||||
// RFC 9420 §9.2: commit_secret is the path_secret for the "virtual" node
|
||||
// one step past the root — i.e. `DeriveSecret(path_secret_at_root, "path")`,
|
||||
// NOT the path_secret at the root itself. Openmls/mdk derives exactly this
|
||||
// value; quartz was using the root's own path_secret, which is the
|
||||
// encryption-key seed rather than the key-schedule contribution. That
|
||||
// one-step gap silently diverged the two sides' epoch_secret and made
|
||||
// every cross-impl commit fail `ConfirmationTagMismatch`.
|
||||
val commitSecret =
|
||||
if (pathSecrets.isNotEmpty()) {
|
||||
pathSecrets.last().pathSecret
|
||||
MlsCryptoProvider.deriveSecret(pathSecrets.last().pathSecret, "path")
|
||||
} else {
|
||||
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
||||
}
|
||||
@@ -1131,15 +1138,18 @@ class MlsGroup private constructor(
|
||||
ct.ciphertext,
|
||||
)
|
||||
|
||||
// Derive remaining path secrets from common ancestor up to root.
|
||||
// pathSecret is the secret AT commonAncestorIdx, so we derive
|
||||
// (directPath.size - commonAncestorIdx - 1) more steps to reach root.
|
||||
val remainingSteps = directPath.size - commonAncestorIdx - 1
|
||||
// Derive remaining path secrets from common ancestor up to root,
|
||||
// then one more step to reach the `commit_secret` (RFC 9420 §9.2:
|
||||
// commit_secret = DeriveSecret(root_path_secret, "path")). Openmls
|
||||
// advances one step past the root; quartz was stopping at the root
|
||||
// and diverging — that's what caused `ConfirmationTagMismatch` on
|
||||
// every cross-impl commit.
|
||||
val stepsToRoot = directPath.size - commonAncestorIdx - 1
|
||||
var currentSecret = pathSecret
|
||||
repeat(remainingSteps) {
|
||||
repeat(stepsToRoot) {
|
||||
currentSecret = MlsCryptoProvider.deriveSecret(currentSecret, "path")
|
||||
}
|
||||
commitSecret = currentSecret
|
||||
commitSecret = MlsCryptoProvider.deriveSecret(currentSecret, "path")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2452,10 +2462,11 @@ class MlsGroup private constructor(
|
||||
)
|
||||
val commitBytes = commit.toTlsBytes()
|
||||
|
||||
// Derive epoch secrets using external init_secret
|
||||
// Derive epoch secrets using external init_secret.
|
||||
// commit_secret = DeriveSecret(root_path_secret, "path") (RFC 9420 §9.2).
|
||||
val commitSecret =
|
||||
if (pathSecrets.isNotEmpty()) {
|
||||
pathSecrets.last().pathSecret
|
||||
MlsCryptoProvider.deriveSecret(pathSecrets.last().pathSecret, "path")
|
||||
} else {
|
||||
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user