fix: align Marmot implementation with MIP specs for interop with MDK
Brings Amethyst's Marmot code into wire-level interoperability with the reference MDK (Rust) implementation by resolving four spec violations and tightening the epoch lookback window: - MIP-01 (NostrGroupData extension): switch to QUIC-style VarInt length prefixes (RFC 9000 §16) instead of fixed uint16. MIP-01 mandates this encoding (the one produced by the Rust `tls_codec` crate v0.4+), so Amethyst's previous uint16 framing could not round-trip with any MDK peer. admin_pubkeys, relays (outer + each inner), name, description, image_*, and disappearing_message_secs now all use VarInt prefixes. - MIP-01: enforce "admin_pubkeys MUST NOT contain duplicates" in the MarmotGroupData constructor. - MIP-05 (Push Notifications): token plaintext padded size was 220 (→ 280 encrypted); spec MUSTs are exactly 1024 bytes plaintext (1084 encrypted). A server expecting MIP-05 tokens would reject Amethyst's frames outright. - MIP-05: HKDF IKM was sha256(shared_point); spec requires the raw 32-byte ECDH x-coordinate. The extra sha256 produced a different PRK, so token ciphertext authenticated only within Amethyst. Removed the hash; ECDH already returns the x-only compact form via pubKeyTweakMulCompact. - MIP-03: bump EPOCH_RETENTION_WINDOW from 2 to 5 to match MDK's DEFAULT_EPOCH_LOOKBACK, so late-arriving GroupEvents whose ChaCha20 outer key was derived from a prior epoch's exporter secret can still be decrypted after a Commit advances the group. Hand-crafted MarmotGroupData test blobs were updated to emit VarInt length prefixes.
This commit is contained in:
+11
-13
@@ -115,27 +115,24 @@ class MarmotMipComplianceTest {
|
||||
|
||||
@Test
|
||||
fun marmotGroupData_rejectsZeroDisappearingSecsOnDecode() {
|
||||
// Hand-crafted TLS blob: version=3, group_id=32x0, empty opaque2 for
|
||||
// name/description/admins/relays/images, then disappearing_message_secs
|
||||
// = 8 bytes of zero (invalid).
|
||||
// Hand-crafted TLS blob (MIP-01 QUIC VarInt length prefixes):
|
||||
// uint16 version=3 | opaque group_id[32] | 8x empty VarInt(0) fields
|
||||
// (name..image_upload_key) | disappearing_message_secs = VarInt(8) + 8
|
||||
// zero bytes (invalid per MIP-01).
|
||||
val header =
|
||||
ByteArray(2 + 32) {
|
||||
// version + groupId
|
||||
when (it) {
|
||||
0 -> 0
|
||||
|
||||
1 -> 3
|
||||
|
||||
// version=3
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
// 8x opaque2 fields of length 0, each encoded as two zero bytes:
|
||||
// 8 empty VarInt-prefixed opaque fields, each a single 0x00 byte:
|
||||
// name, description, admin_pubkeys, relays, image_hash, image_key,
|
||||
// image_nonce, image_upload_key
|
||||
val zeroFields = ByteArray(8 * 2) // all zeros
|
||||
// disappearing_message_secs opaque2 with 8 zero bytes
|
||||
val disappearingField = ByteArray(2 + 8).also { it[1] = 8 }
|
||||
val zeroFields = ByteArray(8) // all 0x00
|
||||
// disappearing_message_secs: VarInt(8) = 0x08, then 8 zero bytes
|
||||
val disappearingField = ByteArray(1 + 8).also { it[0] = 0x08 }
|
||||
val blob = header + zeroFields + disappearingField
|
||||
|
||||
// decodeTls catches any exception and returns null
|
||||
@@ -150,8 +147,9 @@ class MarmotMipComplianceTest {
|
||||
it[0] = 0
|
||||
it[1] = 99
|
||||
}
|
||||
val zeroFields = ByteArray(8 * 2) // name..image_upload_key
|
||||
val disappearingField = ByteArray(2) // zero-length
|
||||
val zeroFields = ByteArray(8) // 8x VarInt(0) for name..image_upload_key
|
||||
val disappearingField = ByteArray(1) // VarInt(0) — zero-length field
|
||||
|
||||
val blob = header + zeroFields + disappearingField
|
||||
|
||||
assertNull(MarmotGroupData.decodeTls(blob))
|
||||
|
||||
Reference in New Issue
Block a user