@@ -25,10 +25,17 @@ type ConnectionState struct {
2525 messageCounter atomic.Uint64
2626 window * Bits
2727 writeLock sync.Mutex
28+
29+ // Post-quantum KEM state (only used for Curve_PQ)
30+ pqKemPubKey []byte // Our ephemeral ML-KEM-1024 public key
31+ pqKemPrivKey []byte // Our ephemeral ML-KEM-1024 private key
32+ pqKemSS []byte // Shared secret from KEM exchange
2833}
2934
3035func NewConnectionState (l * logrus.Logger , cs * CertState , crt cert.Certificate , initiator bool , pattern noise.HandshakePattern ) (* ConnectionState , error ) {
3136 var dhFunc noise.DHFunc
37+ var pqKemPub , pqKemPriv []byte
38+
3239 switch crt .Curve () {
3340 case cert .Curve_CURVE25519 :
3441 dhFunc = noise .DH25519
@@ -38,6 +45,19 @@ func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, i
3845 } else {
3946 dhFunc = noiseutil .DHP256
4047 }
48+ case cert .Curve_PQ :
49+ // Hybrid mode: X25519 DH for classical security + ML-KEM-1024 for PQ security.
50+ // The Noise IX handshake uses X25519 for the DH tokens. The ML-KEM-1024
51+ // exchange is layered on top via the handshake payload (KemPublicKey/KemCiphertext).
52+ // Both shared secrets are mixed into the final symmetric keys via HKDF.
53+ dhFunc = noise .DH25519
54+
55+ // Generate ephemeral ML-KEM-1024 keypair for this handshake
56+ var err error
57+ pqKemPub , pqKemPriv , err = noiseutil .PQKEMKeypair ()
58+ if err != nil {
59+ return nil , fmt .Errorf ("NewConnectionState: ML-KEM-1024 keygen failed: %s" , err )
60+ }
4161 default :
4262 return nil , fmt .Errorf ("invalid curve: %s" , crt .Curve ())
4363 }
@@ -49,7 +69,24 @@ func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, i
4969 ncs = noise .NewCipherSuite (dhFunc , noiseutil .CipherAESGCM , noise .HashSHA256 )
5070 }
5171
52- static := noise.DHKey {Private : cs .privateKey , Public : crt .PublicKey ()}
72+ // For PQ hybrid mode, use a fresh X25519 keypair as the Noise "static" key.
73+ // The actual identity authentication comes from the ML-DSA-87 cert signature,
74+ // not from the DH static key. This is safe because:
75+ // 1. The cert is verified against the CA in the handshake payload
76+ // 2. The KEM exchange provides the PQ-secure key agreement
77+ // 3. The X25519 DH provides defense-in-depth
78+ var static noise.DHKey
79+ if crt .Curve () == cert .Curve_PQ {
80+ // Generate ephemeral X25519 keypair (cert's public key is ML-KEM, not X25519)
81+ var err error
82+ static , err = noise .DH25519 .GenerateKeypair (rand .Reader )
83+ if err != nil {
84+ return nil , fmt .Errorf ("NewConnectionState: X25519 keygen failed: %s" , err )
85+ }
86+ } else {
87+ static = noise.DHKey {Private : cs .privateKey , Public : crt .PublicKey ()}
88+ }
89+
5390 hs , err := noise .NewHandshakeState (noise.Config {
5491 CipherSuite : ncs ,
5592 Random : rand .Reader ,
@@ -67,10 +104,12 @@ func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, i
67104 // The queue and ready params prevent a counter race that would happen when
68105 // sending stored packets and simultaneously accepting new traffic.
69106 ci := & ConnectionState {
70- H : hs ,
71- initiator : initiator ,
72- window : NewBits (ReplayWindow ),
73- myCert : crt ,
107+ H : hs ,
108+ initiator : initiator ,
109+ window : NewBits (ReplayWindow ),
110+ myCert : crt ,
111+ pqKemPubKey : pqKemPub ,
112+ pqKemPrivKey : pqKemPriv ,
74113 }
75114 // always start the counter from 2, as packet 1 and packet 2 are handshake packets.
76115 ci .messageCounter .Add (2 )
0 commit comments