Post-Quantum Cryptography
LP-6461
DraftLuxChat DM Sessions (PQXDH)
LuxChat DM Sessions (PQXDH) specification for LuxDA Bus
Abstract
This LP defines the direct message (DM) encryption protocol for LuxDA, providing end-to-end encrypted conversations between two parties with forward secrecy and post-compromise security through key ratcheting.
Motivation
Private 1:1 messaging requires:
- Confidentiality: Only participants can read messages
- Forward Secrecy: Past messages safe if keys compromised
- Post-Compromise Security: Recovery after compromise
- Offline Delivery: Send to offline recipients
- Post-Quantum Safety: Resist future quantum attacks
Specification
1. DM Namespace Derivation
1.1 Deterministic Namespace
DM conversations use a deterministic namespace:
func DMNamespace(identity1, identity2 *Identity) [20]byte {
// Sort identities for determinism
if bytes.Compare(identity1.Identifier, identity2.Identifier) > 0 {
identity1, identity2 = identity2, identity1
}
// Derive namespace
return DeriveNamespace(
"lux.dm.v1",
identity1.Identifier,
identity2.Identifier,
)
}
1.2 Namespace Policy
DM namespaces have implicit policy:
var DMNamespacePolicy = NamespacePolicy{
WriterMode: WriterModeAllowlist,
Writers: []Identity{identity1, identity2},
EncryptionMode: EncryptionE2EE_DM,
PQMode: PQModeHybrid,
// Other fields derived from participants
}
2. Session Establishment (PQXDH)
2.1 PQXDH Protocol
Post-Quantum Extended Diffie-Hellman:
type PQXDHKeys struct {
// Classical X25519
IdentityKey *X25519KeyPair
SignedPreKey *X25519KeyPair
OneTimePreKey *X25519KeyPair // Optional
// PQ ML-KEM
PQPreKey *MLKEMKeyPair
}
type PQXDHMessage struct {
// Sender identity
SenderIdentityKey []byte
// Ephemeral keys
EphemeralKey []byte // X25519 ephemeral
PQCiphertext []byte // ML-KEM ciphertext
// Pre-key identifiers
SignedPreKeyID uint32
OneTimePreKeyID uint32 // 0 if not used
// Encrypted initial message
Ciphertext []byte
}
2.2 Session Initiation (Alice → Bob)
func InitiateSession(alice *PQXDHKeys, bobKeyPackage *KeyPackage) (*Session, *PQXDHMessage, error) {
// Generate ephemeral X25519
ephPriv, ephPub := GenerateX25519()
// Encapsulate to Bob's PQ key
pqCiphertext, pqSharedSecret := MLKEMEncapsulate(bobKeyPackage.PQPreKey)
// X25519 DH computations
dh1 := X25519(alice.IdentityKey.Private, bobKeyPackage.SignedPreKey)
dh2 := X25519(ephPriv, bobKeyPackage.IdentityKey)
dh3 := X25519(ephPriv, bobKeyPackage.SignedPreKey)
// Optional one-time pre-key
var dh4 []byte
if bobKeyPackage.OneTimePreKey != nil {
dh4 = X25519(ephPriv, bobKeyPackage.OneTimePreKey)
}
// Combine secrets (hybrid: classical + PQ)
ikm := concat(dh1, dh2, dh3, dh4, pqSharedSecret)
masterSecret := HKDF(ikm, "PQXDH")
// Derive root key and chain keys
rootKey, sendChainKey := DeriveChainKeys(masterSecret)
session := &Session{
RemoteIdentity: bobKeyPackage.Credential.Identity,
RootKey: rootKey,
SendChainKey: sendChainKey,
SendCounter: 0,
}
return session, &PQXDHMessage{
SenderIdentityKey: alice.IdentityKey.Public,
EphemeralKey: ephPub,
PQCiphertext: pqCiphertext,
SignedPreKeyID: bobKeyPackage.SignedPreKeyID,
}, nil
}
2.3 Session Reception (Bob)
func ReceiveSession(bob *PQXDHKeys, msg *PQXDHMessage) (*Session, error) {
// Decapsulate PQ ciphertext
pqSharedSecret := MLKEMDecapsulate(bob.PQPreKey.Private, msg.PQCiphertext)
// X25519 DH computations
dh1 := X25519(bob.SignedPreKey.Private, msg.SenderIdentityKey)
dh2 := X25519(bob.IdentityKey.Private, msg.EphemeralKey)
dh3 := X25519(bob.SignedPreKey.Private, msg.EphemeralKey)
// One-time pre-key
var dh4 []byte
if msg.OneTimePreKeyID != 0 {
otpk := bob.GetOneTimePreKey(msg.OneTimePreKeyID)
dh4 = X25519(otpk.Private, msg.EphemeralKey)
bob.DeleteOneTimePreKey(msg.OneTimePreKeyID)
}
// Same derivation as sender
ikm := concat(dh1, dh2, dh3, dh4, pqSharedSecret)
masterSecret := HKDF(ikm, "PQXDH")
rootKey, recvChainKey := DeriveChainKeys(masterSecret)
return &Session{
RemoteIdentity: deriveIdentity(msg.SenderIdentityKey),
RootKey: rootKey,
RecvChainKey: recvChainKey,
RecvCounter: 0,
}, nil
}
3. Double Ratchet
3.1 Session State
type Session struct {
// Identities
LocalIdentity Identity
RemoteIdentity Identity
// Ratchet state
RootKey [32]byte
SendChainKey [32]byte
RecvChainKey [32]byte
// DH ratchet keys
SendRatchetKey *X25519KeyPair
RecvRatchetKey []byte // Public only
// Message counters
SendCounter uint32
RecvCounter uint32
PrevSendCounter uint32
// Skipped message keys (for out-of-order)
SkippedKeys map[SkippedKeyID][32]byte
}
type SkippedKeyID struct {
RatchetPub []byte
Counter uint32
}
3.2 DH Ratchet Step
func (s *Session) DHRatchet(remoteRatchetPub []byte) {
// DH with current and new remote ratchet key
dhOutput := X25519(s.SendRatchetKey.Private, remoteRatchetPub)
// Derive new root key and receive chain
s.RootKey, s.RecvChainKey = KDFRootKey(s.RootKey, dhOutput)
// Generate new send ratchet key
s.SendRatchetKey = GenerateX25519KeyPair()
// DH with new keys
dhOutput = X25519(s.SendRatchetKey.Private, remoteRatchetPub)
// Derive new send chain
s.RootKey, s.SendChainKey = KDFRootKey(s.RootKey, dhOutput)
// Update remote key
s.RecvRatchetKey = remoteRatchetPub
s.PrevSendCounter = s.SendCounter
s.SendCounter = 0
s.RecvCounter = 0
}
3.3 Symmetric Ratchet
func KDFChainKey(chainKey [32]byte) (nextChainKey, messageKey [32]byte) {
// HKDF for chain key update
output := HKDF(chainKey[:], "chain", 64)
copy(nextChainKey[:], output[:32])
copy(messageKey[:], output[32:])
return
}
func (s *Session) GetSendMessageKey() [32]byte {
messageKey := [32]byte{}
s.SendChainKey, messageKey = KDFChainKey(s.SendChainKey)
s.SendCounter++
return messageKey
}
4. Message Encryption
4.1 Message Format
type EncryptedDMMessage struct {
// Header (plaintext, authenticated)
Header DMMessageHeader
// Ciphertext
Ciphertext []byte
// Authentication tag
Tag []byte
}
type DMMessageHeader struct {
// DH ratchet public key
RatchetPub []byte
// Message counter
Counter uint32
// Previous chain counter
PrevCounter uint32
// Timestamp
Timestamp uint64
}
4.2 Encryption
func (s *Session) Encrypt(plaintext []byte) (*EncryptedDMMessage, error) {
// Get message key
messageKey := s.GetSendMessageKey()
// Derive encryption and MAC keys
encKey, macKey := DeriveAEADKeys(messageKey)
// Build header
header := DMMessageHeader{
RatchetPub: s.SendRatchetKey.Public,
Counter: s.SendCounter - 1,
PrevCounter: s.PrevSendCounter,
Timestamp: uint64(time.Now().Unix()),
}
// AAD = header || namespace_id
aad := append(header.Encode(), s.NamespaceID[:]...)
// AEAD encryption
nonce := DeriveNonce(messageKey, header.Counter)
ciphertext, tag := AESGCMEncrypt(encKey, nonce, plaintext, aad)
return &EncryptedDMMessage{
Header: header,
Ciphertext: ciphertext,
Tag: tag,
}, nil
}
4.3 Decryption
func (s *Session) Decrypt(msg *EncryptedDMMessage) ([]byte, error) {
// Check for skipped message key
skippedID := SkippedKeyID{
RatchetPub: msg.Header.RatchetPub,
Counter: msg.Header.Counter,
}
if messageKey, ok := s.SkippedKeys[skippedID]; ok {
delete(s.SkippedKeys, skippedID)
return s.decryptWithKey(msg, messageKey)
}
// Check if DH ratchet needed
if !bytes.Equal(msg.Header.RatchetPub, s.RecvRatchetKey) {
// Skip remaining messages in current chain
s.skipMessages(s.RecvRatchetKey, msg.Header.PrevCounter)
// Perform DH ratchet
s.DHRatchet(msg.Header.RatchetPub)
}
// Skip messages in this chain
s.skipMessages(msg.Header.RatchetPub, msg.Header.Counter)
// Get message key
messageKey := s.GetRecvMessageKey()
return s.decryptWithKey(msg, messageKey)
}
func (s *Session) skipMessages(ratchetPub []byte, until uint32) {
for s.RecvCounter < until {
messageKey := s.GetRecvMessageKey()
skippedID := SkippedKeyID{
RatchetPub: ratchetPub,
Counter: s.RecvCounter - 1,
}
s.SkippedKeys[skippedID] = messageKey
}
}
5. Message Types
5.1 Content Types
type DMContent struct {
Type ContentType
Payload []byte
}
type ContentType uint8
const (
ContentText ContentType = 1
ContentImage ContentType = 2
ContentFile ContentType = 3
ContentReaction ContentType = 4
ContentReply ContentType = 5
ContentRead ContentType = 6
ContentTyping ContentType = 7
ContentKeyUpdate ContentType = 8
)
5.2 Text Message
type TextContent struct {
Text string
}
5.3 Attachment Reference
type AttachmentContent struct {
// Reference to encrypted file (LP-6463)
CID *CID
MimeType string
FileName string
Size uint64
Thumbnail []byte // Optional encrypted thumbnail
Key []byte // Decryption key
}
6. Read Receipts and Typing
6.1 Read Receipt
type ReadReceipt struct {
MessageSeqs []uint64 // Sequence numbers read
Timestamp uint64
}
6.2 Typing Indicator
type TypingIndicator struct {
IsTyping bool
// Short TTL, not persisted
}
7. Session Management
7.1 Session Storage
type SessionStore interface {
// Get session for identity
GetSession(remoteIdentity *Identity) (*Session, error)
// Save session
SaveSession(session *Session) error
// Delete session
DeleteSession(remoteIdentity *Identity) error
// List all sessions
ListSessions() ([]*Session, error)
}
7.2 Session Reset
func (s *Session) Reset() error {
// Clear ratchet state
s.RootKey = [32]byte{}
s.SendChainKey = [32]byte{}
s.RecvChainKey = [32]byte{}
// Clear skipped keys
s.SkippedKeys = make(map[SkippedKeyID][32]byte)
// New session will be established on next message
s.Initialized = false
return nil
}
8. Wire Format
8.1 DM Bus Message
DMBusMessageV1 := {
version: uint8 [1 byte]
type: uint8 [1 byte] // Session init, message, etc.
headerLen: uint16 [2 bytes]
header: bytes [headerLen bytes]
ciphertextLen: uint32 [4 bytes]
ciphertext: bytes [ciphertextLen bytes]
tag: bytes16 [16 bytes]
}
8.2 Message Envelope
type DMEnvelope struct {
// Sender info
SenderKeyRef [32]byte
// Message type
MessageType DMMessageType
// Encrypted content
Encrypted *EncryptedDMMessage
}
type DMMessageType uint8
const (
DMTypeSessionInit DMMessageType = 1
DMTypeMessage DMMessageType = 2
DMTypeAck DMMessageType = 3
)
Rationale
Why PQXDH?
- Combines classical and PQ key exchange
- Safe against both classical and quantum attacks
- Based on proven Signal X3DH
Why Double Ratchet?
- Provides forward secrecy
- Post-compromise security
- Battle-tested protocol
Why Deterministic Namespace?
- Both parties can derive same namespace
- No coordination needed
- Privacy-preserving (hash-based)
Security Considerations
Forward Secrecy
- Each message uses unique key
- Compromise of current key doesn't reveal past messages
- DH ratchet provides long-term FS
Post-Compromise Security
- New DH ratchet after compromise
- Attacker loses access after ratchet
- One-time pre-keys enhance initial security
Replay Protection
- Sequence numbers prevent replay
- Namespace binding in AAD
- Timestamp bounds
Test Plan
Unit Tests
- PQXDH: Key exchange produces matching secrets
- Double Ratchet: Encrypt/decrypt round-trip
- Out-of-Order: Handle message reordering
Integration Tests
- Session Establishment: Alice → Bob conversation start
- Full Conversation: Multi-message exchange
- Recovery: Session reset and re-establishment
Security Tests
- Ciphertext Malleability: Reject modified ciphertext
- Replay: Reject replayed messages
- Wrong Session: Messages decrypt only in correct session
References
LP-6461 v1.0.0 - 2026-01-02