mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-30 19:46:17 +00:00
fix(marmot): align MLS extension IDs and sender-data sample size with RFC 9420
Three latent spec divergences that only surfaced once we fed the Marmot stack with a Welcome authored by openmls 0.8 (the MLS backend under MDK / whitenoise): * ratchet_tree extension type was 0x0001 (RFC 9420 §13.3 assigns 0x0001 to application_id; ratchet_tree is 0x0002). Welcomes and GroupInfos Amethyst emitted were unreadable to any spec-compliant peer, and Welcomes from peers couldn't be joined because the tree lookup failed. * external_pub extension type was 0x0003 (that slot is required_capabilities; external_pub is 0x0004). External commits would have failed interop the same way. * sender-data ciphertext_sample used AEAD.Nk (16 B) bytes of the ciphertext; RFC 9420 §6.3.2 mandates KDF.Nh (32 B). Every cross-implementation PrivateMessage would fail sender-data AEAD. Amethyst ↔ Amethyst round-trip tests passed because both sides were wrong the same way — the existing MlsConformanceTest hard-coded the wrong extension IDs, so it even asserted the broken invariant. Conformance test assertions updated to the spec-correct values. https://claude.ai/code/session_01HfHdd5S5rvxUW2ihEpLGJr
This commit is contained in:
@@ -610,8 +610,9 @@ class MlsGroup private constructor(
|
||||
senderDataWriter.putBytes(reuseGuard)
|
||||
val senderDataPlain = senderDataWriter.toByteArray()
|
||||
|
||||
// Derive sender data key/nonce using ciphertext sample (RFC 9420 §6.3.1)
|
||||
val ciphertextSample = ciphertext.copyOfRange(0, minOf(ciphertext.size, MlsCryptoProvider.AEAD_KEY_LENGTH))
|
||||
// Derive sender data key/nonce using ciphertext sample (RFC 9420 §6.3.2:
|
||||
// "the first KDF.Nh bytes of the ciphertext").
|
||||
val ciphertextSample = ciphertext.copyOfRange(0, minOf(ciphertext.size, MlsCryptoProvider.HASH_OUTPUT_LENGTH))
|
||||
val senderDataKey =
|
||||
MlsCryptoProvider.expandWithLabel(
|
||||
epochSecrets.senderDataSecret,
|
||||
@@ -627,7 +628,7 @@ class MlsGroup private constructor(
|
||||
MlsCryptoProvider.AEAD_NONCE_LENGTH,
|
||||
)
|
||||
|
||||
// Build SenderDataAAD (RFC 9420 §6.3.1)
|
||||
// Build SenderDataAAD (RFC 9420 §6.3.2)
|
||||
val senderDataAad = buildSenderDataAAD(groupId, epoch, ContentType.APPLICATION)
|
||||
val encryptedSenderData =
|
||||
MlsCryptoProvider.aeadEncrypt(senderDataKey, senderDataNonce, senderDataAad, senderDataPlain)
|
||||
@@ -676,8 +677,11 @@ class MlsGroup private constructor(
|
||||
}
|
||||
|
||||
// Derive sender data key/nonce using ciphertext sample (RFC 9420 §6.3.1)
|
||||
// RFC 9420 §6.3.2: ciphertext_sample is the first KDF.Nh bytes
|
||||
// (32 for HKDF-SHA256), not AEAD.Nk (16). Using AEAD.Nk here made
|
||||
// sender-data decryption fail against every spec-compliant sender.
|
||||
val ciphertextSample =
|
||||
privMsg.ciphertext.copyOfRange(0, minOf(privMsg.ciphertext.size, MlsCryptoProvider.AEAD_KEY_LENGTH))
|
||||
privMsg.ciphertext.copyOfRange(0, minOf(privMsg.ciphertext.size, MlsCryptoProvider.HASH_OUTPUT_LENGTH))
|
||||
val senderDataKey =
|
||||
MlsCryptoProvider.expandWithLabel(
|
||||
epochSecrets.senderDataSecret,
|
||||
@@ -1501,7 +1505,11 @@ class MlsGroup private constructor(
|
||||
companion object {
|
||||
private const val MAX_SENT_KEYS = 10_000
|
||||
private const val REUSE_GUARD_LENGTH = 4
|
||||
private const val RATCHET_TREE_EXTENSION_TYPE = 0x0001
|
||||
|
||||
// RFC 9420 §13.3 IANA registry: 0x0002 is ratchet_tree.
|
||||
// (0x0001 is application_id — using it here makes Welcomes
|
||||
// unreadable to OpenMLS/MDK/whitenoise.)
|
||||
private const val RATCHET_TREE_EXTENSION_TYPE = 0x0002
|
||||
|
||||
/**
|
||||
* Wrap a raw [Commit] (as [commitBytes]) in an MlsMessage(PublicMessage(...))
|
||||
@@ -1572,7 +1580,11 @@ class MlsGroup private constructor(
|
||||
}
|
||||
|
||||
private const val REQUIRED_CAPABILITIES_EXTENSION_TYPE = 0x0002
|
||||
private const val EXTERNAL_PUB_EXTENSION_TYPE = 0x0003
|
||||
|
||||
// RFC 9420 §13.3 IANA registry: 0x0004 is external_pub.
|
||||
// (0x0003 is required_capabilities — using it here makes
|
||||
// external-join GroupInfos unreadable to OpenMLS/MDK.)
|
||||
private const val EXTERNAL_PUB_EXTENSION_TYPE = 0x0004
|
||||
private const val EXTERNAL_SENDERS_EXTENSION_TYPE = 0x0004
|
||||
|
||||
/** MLS self_remove proposal type (MIP-00 / MIP-03). */
|
||||
|
||||
+6
-5
@@ -146,12 +146,12 @@ class MlsConformanceTest {
|
||||
assertEquals(0, decoded.signer, "Signer should be leaf 0 (group creator)")
|
||||
assertTrue(decoded.signature.isNotEmpty(), "GroupInfo must be signed")
|
||||
|
||||
// Must contain ratchet_tree extension (type 0x0001)
|
||||
val ratchetTreeExt = decoded.extensions.find { it.extensionType == 0x0001 }
|
||||
// Must contain ratchet_tree extension (RFC 9420 §13.3: type 0x0002)
|
||||
val ratchetTreeExt = decoded.extensions.find { it.extensionType == 0x0002 }
|
||||
assertTrue(ratchetTreeExt != null, "GroupInfo must contain ratchet_tree extension")
|
||||
|
||||
// Must contain external_pub extension (type 0x0003)
|
||||
val externalPubExt = decoded.extensions.find { it.extensionType == 0x0003 }
|
||||
// Must contain external_pub extension (RFC 9420 §13.3: type 0x0004)
|
||||
val externalPubExt = decoded.extensions.find { it.extensionType == 0x0004 }
|
||||
assertTrue(externalPubExt != null, "GroupInfo must contain external_pub extension")
|
||||
assertEquals(32, externalPubExt.extensionData.size, "external_pub must be 32 bytes (X25519)")
|
||||
}
|
||||
@@ -394,7 +394,8 @@ class MlsConformanceTest {
|
||||
// Verify it's derived from external_secret via DeriveKeyPair
|
||||
// (This is an internal consistency check)
|
||||
val groupInfo = group.groupInfo()
|
||||
val extPubFromGI = groupInfo.extensions.find { it.extensionType == 0x0003 }
|
||||
// RFC 9420 §13.3: external_pub is extension type 0x0004.
|
||||
val extPubFromGI = groupInfo.extensions.find { it.extensionType == 0x0004 }
|
||||
assertContentEquals(externalPub, extPubFromGI!!.extensionData)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user