fix: transcript hash computation using AuthenticatedContent decomposition

ConfirmedTranscriptHashInput = AuthenticatedContent minus the last 33
bytes (confirmation_tag = VarInt(32) + 32-byte HMAC-SHA256 MAC).
InterimTranscriptHashInput = those last 33 bytes (the confirmation_tag).

confirmed_hash = Hash(interim_before || ConfirmedTranscriptHashInput)
interim_hash = Hash(confirmed_hash || InterimTranscriptHashInput)

Test results: 40/41 passing (98%). Only remaining failure is
EncryptWithLabel (HPKE X25519 DH computation discrepancy).

https://claude.ai/code/session_01NocQDWj2Y92FugjfgazzL3
This commit is contained in:
Claude
2026-04-03 21:45:38 +00:00
parent b01ea3f567
commit 34cb887076
@@ -55,14 +55,16 @@ class TranscriptHashInteropTest {
val interimBefore = v.interimTranscriptHashBefore.hexToByteArray() val interimBefore = v.interimTranscriptHashBefore.hexToByteArray()
val authenticatedContent = v.authenticatedContent.hexToByteArray() val authenticatedContent = v.authenticatedContent.hexToByteArray()
// confirmed_transcript_hash = // ConfirmedTranscriptHashInput = wire_format || FramedContent || signature
// Hash(interim_transcript_hash_before || AuthenticatedContent_tbm) // (everything in AuthenticatedContent EXCEPT the confirmation_tag at the end)
// AuthenticatedContent_tbm is the wire_format || content || auth fields // For SHA-256, confirmation_tag = VarInt(32) + 32 bytes = 33 bytes
// In the test vectors, authenticated_content is already TLS-encoded val confirmationTagSize = 1 + MlsCryptoProvider.HASH_OUTPUT_LENGTH
val confirmedInput = authenticatedContent.copyOfRange(0, authenticatedContent.size - confirmationTagSize)
// confirmed_transcript_hash = Hash(interim_before || ConfirmedTranscriptHashInput)
val writer = TlsWriter() val writer = TlsWriter()
writer.putBytes(interimBefore) writer.putBytes(interimBefore)
writer.putBytes(authenticatedContent) writer.putBytes(confirmedInput)
val confirmedHash = MlsCryptoProvider.hash(writer.toByteArray()) val confirmedHash = MlsCryptoProvider.hash(writer.toByteArray())
assertEquals( assertEquals(
@@ -79,21 +81,16 @@ class TranscriptHashInteropTest {
for ((idx, v) in vectors.withIndex()) { for ((idx, v) in vectors.withIndex()) {
val confirmedAfter = v.confirmedTranscriptHashAfter.hexToByteArray() val confirmedAfter = v.confirmedTranscriptHashAfter.hexToByteArray()
val confirmationKey = v.confirmationKey.hexToByteArray() val authenticatedContent = v.authenticatedContent.hexToByteArray()
// confirmation_tag = HMAC(confirmation_key, confirmed_transcript_hash) // InterimTranscriptHashInput = confirmation_tag (last 33 bytes of AuthenticatedContent)
val mac = val confirmationTagSize = 1 + MlsCryptoProvider.HASH_OUTPUT_LENGTH
com.vitorpamplona.quartz.utils.mac val confirmationTag = authenticatedContent.copyOfRange(authenticatedContent.size - confirmationTagSize, authenticatedContent.size)
.MacInstance("HmacSHA256", confirmationKey)
mac.update(confirmedAfter)
val confirmationTag = mac.doFinal()
// interim_transcript_hash = // interim_transcript_hash = Hash(confirmed_hash || InterimTranscriptHashInput)
// Hash(confirmed_transcript_hash || confirmation_tag)
val writer = TlsWriter() val writer = TlsWriter()
writer.putBytes(confirmedAfter) writer.putBytes(confirmedAfter)
writer.putBytes(confirmationTag) writer.putBytes(confirmationTag)
val interimHash = MlsCryptoProvider.hash(writer.toByteArray()) val interimHash = MlsCryptoProvider.hash(writer.toByteArray())
assertEquals( assertEquals(