fix(marmot): thread actual wire_format into ConfirmedTranscriptHashInput
RFC 9420 §8.2 says ConfirmedTranscriptHashInput carries the wire_format of the AuthenticatedContent being committed, not a hard-coded value. openmls/mdk pass mls_content.wire_format() through, so B's PrivateMessage commits use wire_format=2 in their transcript-hash input. Quartz always wrote wire_format=1 (PublicMessage), so amy's receiver-side new_confirmed_transcript_hash diverged from B's — and every tag derived from it (confirmation_key, confirmation_tag, epoch secrets) diverged too, surfacing as "Confirmation tag verification failed" once my error-surfacing fix stopped hiding it. Plumb the real wire format through processCommit and its decrypt() dispatch so PrivateMessage commits from B are hashed as PrivateMessage on amy's side. https://claude.ai/code/session_1469d7f4-bb66-4ffa-a44d-1dfa4b526484
This commit is contained in:
@@ -967,9 +967,11 @@ class MlsGroup private constructor(
|
||||
}
|
||||
}
|
||||
// Apply the commit inline — after this returns, the caller
|
||||
// only needs to know the epoch advanced. The signature rides
|
||||
// into the transcript-hash computation inside processCommit.
|
||||
processCommit(commitBytes, senderLeafIndex, confirmationTag, signature)
|
||||
// only needs to know the epoch advanced. The signature + wire
|
||||
// format ride into the transcript-hash computation inside
|
||||
// processCommit (RFC 9420 §8.2 requires the real wire format
|
||||
// for ConfirmedTranscriptHashInput).
|
||||
processCommit(commitBytes, senderLeafIndex, confirmationTag, signature, WireFormat.PRIVATE_MESSAGE)
|
||||
|
||||
return DecryptedMessage(
|
||||
senderLeafIndex = senderLeafIndex,
|
||||
@@ -1039,6 +1041,7 @@ class MlsGroup private constructor(
|
||||
senderLeafIndex: Int,
|
||||
confirmationTag: ByteArray,
|
||||
signature: ByteArray = ByteArray(0),
|
||||
wireFormat: WireFormat = WireFormat.PUBLIC_MESSAGE,
|
||||
) {
|
||||
val commit = Commit.decodeTls(TlsReader(commitBytes))
|
||||
|
||||
@@ -1215,7 +1218,7 @@ class MlsGroup private constructor(
|
||||
|
||||
// Update transcript hashes (RFC 9420 Section 8.2)
|
||||
// ConfirmedTranscriptHashInput = wire_format || FramedContent || signature
|
||||
val confirmedTranscriptHashInput = buildConfirmedTranscriptHashInput(commit, senderLeafIndex, signature)
|
||||
val confirmedTranscriptHashInput = buildConfirmedTranscriptHashInput(commit, senderLeafIndex, signature, wireFormat)
|
||||
val confirmedInput = TlsWriter()
|
||||
confirmedInput.putBytes(interimTranscriptHash)
|
||||
confirmedInput.putBytes(confirmedTranscriptHashInput)
|
||||
@@ -1393,7 +1396,8 @@ class MlsGroup private constructor(
|
||||
commit: Commit,
|
||||
senderLeafIndex: Int,
|
||||
signature: ByteArray = ByteArray(0),
|
||||
): ByteArray = buildConfirmedTranscriptHashInput(commit, senderLeafIndex, groupId, epoch, signature)
|
||||
wireFormat: WireFormat = WireFormat.PUBLIC_MESSAGE,
|
||||
): ByteArray = buildConfirmedTranscriptHashInput(commit, senderLeafIndex, groupId, epoch, signature, wireFormat)
|
||||
|
||||
/**
|
||||
* Compute the RFC 9420 §7.9.2 parent_hash chain for a commit we are
|
||||
@@ -1992,9 +1996,17 @@ class MlsGroup private constructor(
|
||||
groupId: ByteArray,
|
||||
epoch: Long,
|
||||
signature: ByteArray = ByteArray(0),
|
||||
wireFormat: WireFormat = WireFormat.PUBLIC_MESSAGE,
|
||||
): ByteArray {
|
||||
// RFC 9420 §8.2: ConfirmedTranscriptHashInput carries the wire_format
|
||||
// of the actual AuthenticatedContent — openmls passes
|
||||
// `mls_content.wire_format()` through. When B sends a commit as a
|
||||
// PrivateMessage (mdk default = AlwaysCiphertext outgoing), amy
|
||||
// MUST recompute the transcript hash with wire_format=2, or the
|
||||
// resulting confirmed_transcript_hash — and thus the
|
||||
// confirmation_tag derived from it — silently diverges from B's.
|
||||
val writer = TlsWriter()
|
||||
writer.putUint16(WireFormat.PUBLIC_MESSAGE.value)
|
||||
writer.putUint16(wireFormat.value)
|
||||
writer.putOpaqueVarInt(groupId)
|
||||
writer.putUint64(epoch)
|
||||
writer.putUint8(1) // SenderType.MEMBER
|
||||
|
||||
+2
-1
@@ -289,6 +289,7 @@ class MlsGroupManager(
|
||||
senderLeafIndex: Int,
|
||||
confirmationTag: ByteArray,
|
||||
signature: ByteArray = ByteArray(0),
|
||||
wireFormat: com.vitorpamplona.quartz.marmot.mls.framing.WireFormat = com.vitorpamplona.quartz.marmot.mls.framing.WireFormat.PUBLIC_MESSAGE,
|
||||
) = mutex.withLock {
|
||||
val group = requireGroup(nostrGroupId)
|
||||
|
||||
@@ -299,7 +300,7 @@ class MlsGroupManager(
|
||||
// the current epoch key, wasting the finite retention slots.
|
||||
val retainedBefore = group.retainedSecrets()
|
||||
|
||||
group.processCommit(commitBytes, senderLeafIndex, confirmationTag, signature)
|
||||
group.processCommit(commitBytes, senderLeafIndex, confirmationTag, signature, wireFormat)
|
||||
|
||||
pushRetainedEpoch(nostrGroupId, retainedBefore)
|
||||
persistGroup(nostrGroupId)
|
||||
|
||||
Reference in New Issue
Block a user