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 commit = Commit(proposalOrRefs, updatePath)
|
||||||
val commitBytes = commit.toTlsBytes()
|
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 =
|
val commitSecret =
|
||||||
if (pathSecrets.isNotEmpty()) {
|
if (pathSecrets.isNotEmpty()) {
|
||||||
pathSecrets.last().pathSecret
|
MlsCryptoProvider.deriveSecret(pathSecrets.last().pathSecret, "path")
|
||||||
} else {
|
} else {
|
||||||
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
||||||
}
|
}
|
||||||
@@ -1131,15 +1138,18 @@ class MlsGroup private constructor(
|
|||||||
ct.ciphertext,
|
ct.ciphertext,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Derive remaining path secrets from common ancestor up to root.
|
// Derive remaining path secrets from common ancestor up to root,
|
||||||
// pathSecret is the secret AT commonAncestorIdx, so we derive
|
// then one more step to reach the `commit_secret` (RFC 9420 §9.2:
|
||||||
// (directPath.size - commonAncestorIdx - 1) more steps to reach root.
|
// commit_secret = DeriveSecret(root_path_secret, "path")). Openmls
|
||||||
val remainingSteps = directPath.size - commonAncestorIdx - 1
|
// 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
|
var currentSecret = pathSecret
|
||||||
repeat(remainingSteps) {
|
repeat(stepsToRoot) {
|
||||||
currentSecret = MlsCryptoProvider.deriveSecret(currentSecret, "path")
|
currentSecret = MlsCryptoProvider.deriveSecret(currentSecret, "path")
|
||||||
}
|
}
|
||||||
commitSecret = currentSecret
|
commitSecret = MlsCryptoProvider.deriveSecret(currentSecret, "path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2452,10 +2462,11 @@ class MlsGroup private constructor(
|
|||||||
)
|
)
|
||||||
val commitBytes = commit.toTlsBytes()
|
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 =
|
val commitSecret =
|
||||||
if (pathSecrets.isNotEmpty()) {
|
if (pathSecrets.isNotEmpty()) {
|
||||||
pathSecrets.last().pathSecret
|
MlsCryptoProvider.deriveSecret(pathSecrets.last().pathSecret, "path")
|
||||||
} else {
|
} else {
|
||||||
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
ByteArray(MlsCryptoProvider.HASH_OUTPUT_LENGTH)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user