Post-Quantum Cryptography
LP-6462
DraftLuxChat Group Chat (MLS)
LuxChat Group Chat (MLS) specification for LuxDA Bus
Abstract
This LP defines group chat encryption for LuxDA using MLS (Messaging Layer Security, RFC 9420) semantics. Groups support efficient member addition/removal, forward secrecy, and post-compromise security with post-quantum key encapsulation.
Motivation
Group messaging requires:
- Scalable Key Management: Efficient for large groups
- Dynamic Membership: Add/remove without re-keying all
- Forward Secrecy: Past messages protected
- Post-Compromise Security: Recovery after member compromise
- Transcript Consistency: All members see same history
Specification
1. Group Namespace
1.1 Group ID and Namespace
type GroupID [32]byte
func GenerateGroupID() GroupID {
var id GroupID
rand.Read(id[:])
return id
}
func GroupNamespace(groupID GroupID) [20]byte {
return DeriveNamespace("lux.group.v1", groupID[:])
}
1.2 Group Policy
type GroupPolicy struct {
// Membership control
AddMemberPolicy MembershipPolicy
RemoveMemberPolicy MembershipPolicy
// Message policies
AllowExternalSenders bool
RequireEncryption bool
// Crypto requirements
MinCipherSuite CipherSuite
RequirePQ bool
// Administrative
Admins []Identity
}
type MembershipPolicy uint8
const (
PolicyAdminOnly MembershipPolicy = 1
PolicyAnyMember MembershipPolicy = 2
PolicyInviteOnly MembershipPolicy = 3
)
2. MLS Core Concepts
2.1 Ratchet Tree
type RatchetTree struct {
// Tree structure (binary tree)
Nodes []TreeNode
// Leaf count
LeafCount uint32
}
type TreeNode struct {
// Node type
Type NodeType
// For leaf nodes
KeyPackage *KeyPackage
MemberID *Identity
// For parent nodes
PublicKey []byte
UnmergedLeaves []uint32
}
type NodeType uint8
const (
NodeLeaf NodeType = 1
NodeParent NodeType = 2
NodeBlank NodeType = 3
)
2.2 Group State
type GroupState struct {
// Group identification
GroupID GroupID
Epoch uint64
// Tree
Tree *RatchetTree
// Secrets
GroupSecret []byte
EpochSecret []byte
// Derived keys
SenderDataSecret []byte
EncryptionSecret []byte
ExporterSecret []byte
ConfirmationKey []byte
MembershipKey []byte
// Transcript hash
ConfirmedTranscriptHash []byte
InterimTranscriptHash []byte
}
2.3 Key Schedule
func DeriveEpochSecrets(groupSecret, commitSecret []byte, context *GroupContext) *EpochSecrets {
// Joiner secret
joinerSecret := HKDF(groupSecret, "joiner")
// Combine with commit
epochSecret := HKDF(joinerSecret, commitSecret)
// Derive application secrets
return &EpochSecrets{
SenderDataSecret: HKDF(epochSecret, "sender data", context),
EncryptionSecret: HKDF(epochSecret, "encryption", context),
ExporterSecret: HKDF(epochSecret, "exporter", context),
ConfirmationKey: HKDF(epochSecret, "confirm", context),
MembershipKey: HKDF(epochSecret, "membership", context),
ResumptionSecret: HKDF(epochSecret, "resumption", context),
EpochAuthenticator: HMAC(confirmationKey, confirmedTranscriptHash),
}
}
3. MLS Messages
3.1 Message Types
type MLSMessage struct {
Version uint8
MessageType MLSMessageType
Payload []byte
}
type MLSMessageType uint8
const (
MLSWelcome MLSMessageType = 1
MLSGroupInfo MLSMessageType = 2
MLSKeyPackage MLSMessageType = 3
MLSProposal MLSMessageType = 4
MLSCommit MLSMessageType = 5
MLSApplication MLSMessageType = 6
)
3.2 Proposal Types
type Proposal struct {
Type ProposalType
Payload []byte
}
type ProposalType uint8
const (
ProposalAdd ProposalType = 1
ProposalUpdate ProposalType = 2
ProposalRemove ProposalType = 3
ProposalPreSharedKey ProposalType = 4
ProposalReInit ProposalType = 5
ProposalExternalInit ProposalType = 6
ProposalGroupContextExt ProposalType = 7
)
3.3 Commit Message
type Commit struct {
// Proposals included in this commit
Proposals []ProposalOrRef
// Path update (new ratchet keys)
Path *UpdatePath
}
type UpdatePath struct {
// Sender's new leaf
LeafNode *LeafNode
// Path from leaf to root
Nodes []UpdatePathNode
}
type UpdatePathNode struct {
EncryptionKey []byte // HPKE public key
Ciphertexts [][]byte // Encrypted path secret for each resolution
}
4. Group Operations
4.1 Create Group
func CreateGroup(creator *KeyPackage, groupID GroupID, policy *GroupPolicy) (*GroupState, error) {
// Initialize tree with creator as only leaf
tree := NewRatchetTree()
tree.AddLeaf(creator)
// Initialize secrets
initSecret := RandomBytes(32)
groupSecret := HKDF(initSecret, "init")
// Create initial state
state := &GroupState{
GroupID: groupID,
Epoch: 0,
Tree: tree,
GroupSecret: groupSecret,
}
// Derive epoch secrets
state.DeriveEpochSecrets()
return state, nil
}
4.2 Add Member
func (g *GroupState) AddMember(newMember *KeyPackage) (*Commit, *Welcome, error) {
// Create Add proposal
addProposal := &Proposal{
Type: ProposalAdd,
Payload: newMember.Encode(),
}
// Generate commit
commit, err := g.GenerateCommit([]*Proposal{addProposal})
if err != nil {
return nil, nil, err
}
// Generate Welcome for new member
welcome, err := g.GenerateWelcome(newMember, commit)
if err != nil {
return nil, nil, err
}
return commit, welcome, nil
}
4.3 Remove Member
func (g *GroupState) RemoveMember(memberIndex uint32) (*Commit, error) {
// Create Remove proposal
removeProposal := &Proposal{
Type: ProposalRemove,
Payload: encodeUint32(memberIndex),
}
// Generate commit
return g.GenerateCommit([]*Proposal{removeProposal})
}
4.4 Update Keys
func (g *GroupState) UpdateKeys() (*Commit, error) {
// Create Update proposal with new key package
newKeyPackage := GenerateKeyPackage(g.MyIdentity)
updateProposal := &Proposal{
Type: ProposalUpdate,
Payload: newKeyPackage.Encode(),
}
return g.GenerateCommit([]*Proposal{updateProposal})
}
5. Welcome Message
5.1 Welcome Structure
type Welcome struct {
CipherSuite CipherSuite
Secrets []EncryptedGroupSecrets
EncryptedGroupInfo []byte
}
type EncryptedGroupSecrets struct {
NewMemberKeyPackageHash []byte
HPKECiphertext []byte // Encrypted to new member's init key
}
5.2 Join Group
func JoinGroup(welcome *Welcome, myKeyPackage *KeyPackage, myPrivKey []byte) (*GroupState, error) {
// Find my encrypted secrets
var mySecrets *EncryptedGroupSecrets
myHash := KeyPackageHash(myKeyPackage)
for _, secrets := range welcome.Secrets {
if bytes.Equal(secrets.NewMemberKeyPackageHash, myHash) {
mySecrets = &secrets
break
}
}
if mySecrets == nil {
return nil, ErrNotInWelcome
}
// Decrypt group secrets using HPKE
groupSecrets, err := HPKEDecrypt(
myKeyPackage.InitKey,
myPrivKey,
mySecrets.HPKECiphertext,
)
if err != nil {
return nil, err
}
// Decrypt group info
groupInfo, err := DecryptGroupInfo(
welcome.EncryptedGroupInfo,
groupSecrets,
)
if err != nil {
return nil, err
}
// Build state from group info
return BuildStateFromGroupInfo(groupInfo, groupSecrets)
}
6. Application Messages
6.1 Message Encryption
type MLSApplicationMessage struct {
GroupID GroupID
Epoch uint64
ContentType ContentType
Sender LeafIndex
Ciphertext []byte
}
func (g *GroupState) EncryptMessage(content []byte) (*MLSApplicationMessage, error) {
// Get sender data key
senderDataKey := g.GetSenderDataKey(g.MyLeafIndex)
// Derive message key (secret tree ratchet)
messageKey := g.RatchetMessageKey(g.MyLeafIndex)
// Build authenticated data
aad := buildAAD(g.GroupID, g.Epoch, g.MyLeafIndex)
// AEAD encrypt
nonce := DeriveNonce(messageKey, g.SendCounter)
ciphertext := AESGCMEncrypt(messageKey, nonce, content, aad)
g.SendCounter++
return &MLSApplicationMessage{
GroupID: g.GroupID,
Epoch: g.Epoch,
ContentType: ContentApplication,
Sender: g.MyLeafIndex,
Ciphertext: ciphertext,
}, nil
}
6.2 Message Decryption
func (g *GroupState) DecryptMessage(msg *MLSApplicationMessage) ([]byte, error) {
// Verify epoch
if msg.Epoch != g.Epoch {
return nil, ErrWrongEpoch
}
// Get message key for sender
messageKey := g.GetMessageKey(msg.Sender)
// Build AAD
aad := buildAAD(msg.GroupID, msg.Epoch, msg.Sender)
// Decrypt
return AESGCMDecrypt(messageKey, nonce, msg.Ciphertext, aad)
}
7. Secret Tree
7.1 Tree Derivation
type SecretTree struct {
Secrets []TreeSecret
}
type TreeSecret struct {
Secret []byte
Generation uint32
}
func (st *SecretTree) GetMessageKey(leafIndex uint32) []byte {
// Get secret for leaf
secret := st.Secrets[leafIndex]
// Derive message key
messageKey := HKDF(secret.Secret, "message", secret.Generation)
// Ratchet
st.Secrets[leafIndex] = TreeSecret{
Secret: HKDF(secret.Secret, "next"),
Generation: secret.Generation + 1,
}
return messageKey
}
8. PQ Extensions
8.1 PQ Cipher Suites
const (
// Classical (for comparison)
MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 CipherSuite = 0x0001
// PQ/Hybrid
MLS_256_XWING_AES256GCM_SHA384_Ed25519_MLDSA CipherSuite = 0x0101
MLS_256_MLKEM768_AES256GCM_SHA384_MLDSA65 CipherSuite = 0x0102
)
8.2 Hybrid HPKE
func HybridHPKEEncap(classicalPub, pqPub []byte) (ciphertext, sharedSecret []byte) {
// X25519 DH
ephPriv, ephPub := GenerateX25519()
dhSecret := X25519(ephPriv, classicalPub)
// ML-KEM encapsulation
pqCiphertext, pqSecret := MLKEMEncapsulate(pqPub)
// Combine secrets
sharedSecret = HKDF(concat(dhSecret, pqSecret), "hybrid")
ciphertext = concat(ephPub, pqCiphertext)
return
}
9. Bus Integration
9.1 Message Flow
┌─────────────────────────────────────────────────────────────┐
│ Group Namespace │
│ /lux/mainnet/group/{groupID} │
├─────────────────────────────────────────────────────────────┤
│ Seq 1: Welcome (to new member namespace) │
│ Seq 2: Commit (Add member) │
│ Seq 3: Application message │
│ Seq 4: Application message │
│ Seq 5: Commit (Update keys) │
│ Seq 6: Application message │
│ ... │
└─────────────────────────────────────────────────────────────┘
9.2 Header AAD Binding
func (g *GroupState) BuildAAD(header *MsgHeader) []byte {
return concat(
header.NamespaceId[:],
uint64ToBytes(header.Seq),
g.GroupID[:],
uint64ToBytes(g.Epoch),
)
}
Rationale
Why MLS?
- IETF standard (RFC 9420)
- Designed for asynchronous messaging
- Efficient for large groups
- Built-in PQ support path
Why Ratchet Tree?
- O(log n) complexity for updates
- Forward secrecy per message
- Post-compromise security via commits
Why Epoch-Based Security?
- Clean security boundaries
- All state changes via commits
- Auditable security evolution
Security Considerations
Forward Secrecy
- Each epoch has independent secrets
- Past epochs unrecoverable after key deletion
- Message keys ratchet within epoch
Post-Compromise Security
- Update proposal refreshes member's keys
- Commit forces new epoch
- Compromised member loses access after removal
Transcript Consistency
- Transcript hash chains messages
- All members verify same transcript
- Divergence detectable
Test Plan
Unit Tests
- Tree Operations: Add, remove, update nodes
- Key Derivation: Verify against test vectors
- Encrypt/Decrypt: Round-trip messages
Integration Tests
- Group Lifecycle: Create → Add → Message → Remove
- Multi-Device: Same user, multiple devices
- Large Groups: 100+ member groups
Interop Tests
- RFC 9420 Vectors: Test against official vectors
- Cross-Implementation: Test with other MLS implementations
References
LP-6462 v1.0.0 - 2026-01-02