fix: correct key schedule welcome_secret derivation order

The welcome_secret must be derived from member_secret (the Extract of
joiner_secret and psk_secret), not directly from joiner_secret. This
matches the RFC 9420 key schedule diagram where welcome_secret is
derived after the psk extraction step.

Before: welcome = DeriveSecret(joiner_secret, "welcome")
After:  member = Extract(joiner_secret, psk_secret)
        welcome = DeriveSecret(member, "welcome")
        epoch = ExpandWithLabel(member, "epoch", GroupContext, Nh)

This fix was discovered and validated by the IETF interop test vectors.
Key schedule tests now pass all 11 derived secrets across multiple epochs.

https://claude.ai/code/session_01NocQDWj2Y92FugjfgazzL3
This commit is contained in:
Claude
2026-04-03 19:51:54 +00:00
parent d1409c84bc
commit 4fc99021ae
@@ -69,14 +69,14 @@ class KeySchedule(
val prk = MlsCryptoProvider.hkdfExtract(initSecret, commitSecret)
val joinerSecret = MlsCryptoProvider.expandWithLabel(prk, "joiner", groupContext, MlsCryptoProvider.HASH_OUTPUT_LENGTH)
// welcome_secret = DeriveSecret(joiner_secret, "welcome")
val welcomeSecret = MlsCryptoProvider.deriveSecret(joinerSecret, "welcome")
// member_secret = HKDF-Extract(joiner_secret, psk_secret)
val memberSecret = MlsCryptoProvider.hkdfExtract(joinerSecret, pskSecret)
// epoch_secret = ExpandWithLabel(
// HKDF-Extract(joiner_secret, psk_secret),
// "epoch", GroupContext, Nh)
val epochPrk = MlsCryptoProvider.hkdfExtract(joinerSecret, pskSecret)
val epochSecret = MlsCryptoProvider.expandWithLabel(epochPrk, "epoch", groupContext, MlsCryptoProvider.HASH_OUTPUT_LENGTH)
// welcome_secret = DeriveSecret(member_secret, "welcome")
val welcomeSecret = MlsCryptoProvider.deriveSecret(memberSecret, "welcome")
// epoch_secret = ExpandWithLabel(member_secret, "epoch", GroupContext, Nh)
val epochSecret = MlsCryptoProvider.expandWithLabel(memberSecret, "epoch", groupContext, MlsCryptoProvider.HASH_OUTPUT_LENGTH)
// Derive individual secrets from epoch_secret
val senderDataSecret = MlsCryptoProvider.deriveSecret(epochSecret, "sender data")