@@ -131,23 +131,26 @@ func pqEncapsulate(pk []byte) (kemSS, ct []byte, err error) {
131131 return xwing .Encapsulate (pk , nil )
132132}
133133
134- // pqResumptionState holds the most recent resumption ticket for a server .
135- type pqResumptionState struct {
134+ // pqSessionState holds a server's reusable PQ key material .
135+ type pqSessionState struct {
136136 mu sync.Mutex
137137 ticket []byte
138138 resumeSecret [32 ]byte
139139 expiry time.Time
140140 epoch uint64
141+ encap []byte
142+ encapKey [32 ]byte
143+ encapEpoch uint64
141144}
142145
143- func newPqResumptionState (c CryptoConstruction ) * pqResumptionState {
146+ func newPqSessionState (c CryptoConstruction ) * pqSessionState {
144147 if c != XWingPQ {
145148 return nil
146149 }
147- return & pqResumptionState {}
150+ return & pqSessionState {}
148151}
149152
150- func (s * pqResumptionState ) store (ticket []byte , resumeSecret [32 ]byte , expiry time.Time , epoch uint64 ) {
153+ func (s * pqSessionState ) store (ticket []byte , resumeSecret [32 ]byte , expiry time.Time , epoch uint64 ) {
151154 if s == nil {
152155 return
153156 }
@@ -159,7 +162,7 @@ func (s *pqResumptionState) store(ticket []byte, resumeSecret [32]byte, expiry t
159162 s .epoch = epoch
160163}
161164
162- func (s * pqResumptionState ) get (currentEpoch uint64 ) (ticket []byte , resumeSecret [32 ]byte , ok bool ) {
165+ func (s * pqSessionState ) get (currentEpoch uint64 ) (ticket []byte , resumeSecret [32 ]byte , ok bool ) {
163166 if s == nil {
164167 return nil , [32 ]byte {}, false
165168 }
@@ -177,8 +180,37 @@ func (s *pqResumptionState) get(currentEpoch uint64) (ticket []byte, resumeSecre
177180 return append ([]byte (nil ), s .ticket ... ), s .resumeSecret , true
178181}
179182
183+ func (s * pqSessionState ) getCachedEncapsulation (currentEpoch uint64 ) (ct []byte , key [32 ]byte , ok bool ) {
184+ if s == nil {
185+ return nil , [32 ]byte {}, false
186+ }
187+ s .mu .Lock ()
188+ defer s .mu .Unlock ()
189+ if s .encap == nil {
190+ return nil , [32 ]byte {}, false
191+ }
192+ if s .encapEpoch != currentEpoch {
193+ s .encap = nil
194+ s .encapKey = [32 ]byte {}
195+ return nil , [32 ]byte {}, false
196+ }
197+ return append ([]byte (nil ), s .encap ... ), s .encapKey , true
198+ }
199+
200+ func (s * pqSessionState ) storeEncapsulation (ct []byte , key [32 ]byte , epoch uint64 ) {
201+ if s == nil {
202+ return
203+ }
204+ s .mu .Lock ()
205+ defer s .mu .Unlock ()
206+ s .encap = append ([]byte (nil ), ct ... )
207+ s .encapKey = key
208+ s .encapEpoch = epoch
209+ }
210+
180211// encryptPQ builds a PQ query: a resumed query when a valid ticket is held,
181- // otherwise a query that carries a fresh X-Wing ciphertext.
212+ // otherwise a query that carries an X-Wing ciphertext, reusing the cached
213+ // encapsulation when one is available for the current network epoch.
182214func (proxy * Proxy ) encryptPQ (
183215 serverInfo * ServerInfo ,
184216 packet []byte ,
@@ -192,7 +224,7 @@ func (proxy *Proxy) encryptPQ(
192224 }
193225 copy (nonce , clientNonce )
194226
195- if ticket , resumeSecret , ok := serverInfo .pqResumption .get (queryEpoch ); ok {
227+ if ticket , resumeSecret , ok := serverInfo .pqSession .get (queryEpoch ); ok {
196228 key := pqResumedSharedKey (resumeSecret , serverInfo .MagicQuery , clientNonce , ticket )
197229 padded := pqPad (packet , 256 )
198230 ct := xsecretbox .Seal (nil , nonce , padded , key [:])
@@ -207,11 +239,15 @@ func (proxy *Proxy) encryptPQ(
207239 return & key , out , clientNonce , queryEpoch , nil
208240 }
209241
210- kemSS , ctKem , err := pqEncapsulate (serverInfo .PqPublicKey )
211- if err != nil {
212- return nil , nil , nil , queryEpoch , err
242+ ctKem , key , ok := serverInfo .pqSession .getCachedEncapsulation (queryEpoch )
243+ if ! ok {
244+ var kemSS []byte
245+ if kemSS , ctKem , err = pqEncapsulate (serverInfo .PqPublicKey ); err != nil {
246+ return nil , nil , nil , queryEpoch , err
247+ }
248+ key = pqDeriveSharedKey (kemSS , serverInfo .MagicQuery , serverInfo .PqCertContext , ctKem )
249+ serverInfo .pqSession .storeEncapsulation (ctKem , key , queryEpoch )
213250 }
214- key := pqDeriveSharedKey (kemSS , serverInfo .MagicQuery , serverInfo .PqCertContext , ctKem )
215251 padded := pqPad (packet , 64 )
216252 ct := xsecretbox .Seal (nil , nonce , padded , key [:])
217253 out := make ([]byte , 0 , PQClientMagicLen + len (ctKem )+ HalfNonceSize + len (ct ))
@@ -269,6 +305,6 @@ func (proxy *Proxy) pqProcessControl(
269305 }
270306 resumeSecret := pqResumeSecret (* sharedKey , serverInfo .MagicQuery , clientNonce )
271307 expiry := time .Now ().Add (time .Duration (lifetime ) * time .Second )
272- serverInfo .pqResumption .store (ticket , resumeSecret , expiry , queryEpoch )
308+ serverInfo .pqSession .store (ticket , resumeSecret , expiry , queryEpoch )
273309 dlog .Debugf ("[%v] stored a PQ resumption ticket (lifetime %ds)" , serverInfo .Name , lifetime )
274310}
0 commit comments