fix: VarInt encoding migration and LeafNode parent_hash for COMMIT

Major interop fixes discovered by IETF test vectors:

1. VarInt migration: All MLS TLS struct serialization now uses
   QUIC-style VarInt encoding for opaque<V> and vector<V> fields,
   matching OpenMLS and mls-rs wire format. Added readVarInt(),
   readOpaqueVarInt(), readVectorVarInt() to TlsReader and
   putVectorVarInt() to TlsWriter.

2. SecretTree left/right derivation: Fixed tree secret splitting
   to use "left"/"right" as context strings per RFC 9420 Section 9,
   instead of byte(0)/byte(1).

3. LeafNode parent_hash: Added parent_hash<V> field for COMMIT
   source per RFC 9420 Section 7.2. The COMMIT case is NOT empty -
   it includes a parent_hash opaque field.

4. MLS-Exporter: Added ByteArray overload for raw byte labels
   (test vectors use non-UTF-8 label bytes).

Test results: 32/41 passing (78%), up from 25/41 (61%).
Newly passing: SecretTree (2), TreeValidation deserialization (1),
TreeValidation resolution (1), TreeKem deserialization (1),
Commit deserialization (1), RatchetTree deserialization (1).

https://claude.ai/code/session_01NocQDWj2Y92FugjfgazzL3
This commit is contained in:
Claude
2026-04-03 20:10:40 +00:00
parent 4fc99021ae
commit 621196b74f
14 changed files with 230 additions and 122 deletions
@@ -139,9 +139,8 @@ class KeyScheduleInteropTest {
val secrets = ks.deriveEpochSecrets(commitSecret, initSecret, pskSecret)
// Test MLS-Exporter
// The exporter label and context in the test vectors are hex-encoded byte arrays.
// The mlsExporter API takes a String label, so decode hex to bytes then to String.
val exporterLabel = String(epoch.exporter.label.hexToByteArray())
// The exporter label in test vectors is hex-encoded raw bytes (may not be valid UTF-8).
val exporterLabel = epoch.exporter.label.hexToByteArray()
val exporterContext = epoch.exporter.context.hexToByteArray()
val exported =