Skip to content

Commit 8c076d4

Browse files
committed
global: add PQC ML-KEM to handshake as PSK
This commit extends the handshake to generate a PQC-based PSK. The NIST-approved ML-KEM (formerly Kyber) is included in the initiator and responder messages to transport the encapsulation key and ciphertext, respectively. The generated shared secrets are directly injected as a pre-shared key (PSK), since PQC resilience is the intended purpose. The ML-KEM encapsulation key and ciphertext are piggybacked onto WireGuard message types 1 and 2, without altering the handshake itself. As a result, the initiation and response messages grow by about 1 kB (~10x) and the handshake takes ~5x longer (0.21s vs 0.93s[^1]), however, likely negligible, since the transported data stream is unaffected. This commit does not address PQC authentication. However, it offers a practical solution to mitigate retrospective decryption using quantum computers—namely, "store now, decrypt later" attacks. While more comprehensive approaches like "Post-quantum WireGuard"[^2] include PQC authentication and a full PQC handshake, the changes proposed here aim to be as minimal as possible, usable as soon as possible. [^1]: Naively running `go test -bench=TestNoiseHandshake -count=100` [^2]: https://eprint.iacr.org/2020/379.pdf Signed-off-by: Paul Spooren <[email protected]>
1 parent 91a0587 commit 8c076d4

File tree

3 files changed

+80
-30
lines changed

3 files changed

+80
-30
lines changed

device/noise-protocol.go

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"sync"
1212
"time"
1313

14+
"filippo.io/mlkem768"
1415
"golang.org/x/crypto/blake2s"
1516
"golang.org/x/crypto/chacha20poly1305"
1617
"golang.org/x/crypto/poly1305"
@@ -60,8 +61,8 @@ const (
6061
)
6162

6263
const (
63-
MessageInitiationSize = 148 // size of handshake initiation message
64-
MessageResponseSize = 92 // size of response message
64+
MessageInitiationSize = 1332 // size of handshake initiation message
65+
MessageResponseSize = 1180 // size of response message
6566
MessageCookieReplySize = 64 // size of cookie reply message
6667
MessageTransportHeaderSize = 16 // size of data preceding content in transport message
6768
MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport
@@ -82,23 +83,25 @@ const (
8283
*/
8384

8485
type MessageInitiation struct {
85-
Type uint32
86-
Sender uint32
87-
Ephemeral NoisePublicKey
88-
Static [NoisePublicKeySize + poly1305.TagSize]byte
89-
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte
90-
MAC1 [blake2s.Size128]byte
91-
MAC2 [blake2s.Size128]byte
86+
Type uint32
87+
Sender uint32
88+
Ephemeral NoisePublicKey
89+
EphemeralPQ [mlkem768.EncapsulationKeySize]byte
90+
Static [NoisePublicKeySize + poly1305.TagSize]byte
91+
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte
92+
MAC1 [blake2s.Size128]byte
93+
MAC2 [blake2s.Size128]byte
9294
}
9395

9496
type MessageResponse struct {
95-
Type uint32
96-
Sender uint32
97-
Receiver uint32
98-
Ephemeral NoisePublicKey
99-
Empty [poly1305.TagSize]byte
100-
MAC1 [blake2s.Size128]byte
101-
MAC2 [blake2s.Size128]byte
97+
Type uint32
98+
Sender uint32
99+
Receiver uint32
100+
Ephemeral NoisePublicKey
101+
CiphertextPQ [mlkem768.CiphertextSize]byte
102+
Empty [poly1305.TagSize]byte
103+
MAC1 [blake2s.Size128]byte
104+
MAC2 [blake2s.Size128]byte
102105
}
103106

104107
type MessageTransport struct {
@@ -118,14 +121,16 @@ type MessageCookieReply struct {
118121
type Handshake struct {
119122
state handshakeState
120123
mutex sync.RWMutex
121-
hash [blake2s.Size]byte // hash value
122-
chainKey [blake2s.Size]byte // chain key
123-
presharedKey NoisePresharedKey // psk
124-
localEphemeral NoisePrivateKey // ephemeral secret key
125-
localIndex uint32 // used to clear hash-table
126-
remoteIndex uint32 // index for sending
127-
remoteStatic NoisePublicKey // long term key, never changes, can be accessed without mutex
128-
remoteEphemeral NoisePublicKey // ephemeral public key
124+
hash [blake2s.Size]byte // hash value
125+
chainKey [blake2s.Size]byte // chain key
126+
presharedKey NoisePresharedKey // psk
127+
localEphemeral NoisePrivateKey // ephemeral secret key
128+
localEphemeralPQ [mlkem768.SeedSize]byte // ephemeral secret PQC key
129+
localIndex uint32 // used to clear hash-table
130+
remoteIndex uint32 // index for sending
131+
remoteStatic NoisePublicKey // long term key, never changes, can be accessed without mutex
132+
remoteEphemeral NoisePublicKey // ephemeral public key
133+
remoteEphemeralPQ [mlkem768.EncapsulationKeySize]byte
129134
precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret
130135
lastTimestamp tai64n.Timestamp
131136
lastInitiationConsumption time.Time
@@ -193,9 +198,16 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
193198

194199
handshake.mixHash(handshake.remoteStatic[:])
195200

201+
dk, err := mlkem768.GenerateKey()
202+
if err != nil {
203+
return nil, err
204+
}
205+
handshake.localEphemeralPQ = [64]byte(dk.Bytes())
206+
196207
msg := MessageInitiation{
197-
Type: MessageInitiationType,
198-
Ephemeral: handshake.localEphemeral.publicKey(),
208+
Type: MessageInitiationType,
209+
Ephemeral: handshake.localEphemeral.publicKey(),
210+
EphemeralPQ: [mlkem768.EncapsulationKeySize]byte(dk.EncapsulationKey()),
199211
}
200212

201213
handshake.mixKey(msg.Ephemeral[:])
@@ -331,6 +343,8 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
331343
handshake.chainKey = chainKey
332344
handshake.remoteIndex = msg.Sender
333345
handshake.remoteEphemeral = msg.Ephemeral
346+
handshake.remoteEphemeralPQ = msg.EphemeralPQ
347+
334348
if timestamp.After(handshake.lastTimestamp) {
335349
handshake.lastTimestamp = timestamp
336350
}
@@ -392,6 +406,16 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
392406
}
393407
handshake.mixKey(ss[:])
394408

409+
// set preshared key
410+
411+
ciphertext, sspq, err := mlkem768.Encapsulate(handshake.remoteEphemeralPQ[:])
412+
if err != nil {
413+
return nil, err
414+
}
415+
msg.CiphertextPQ = [mlkem768.CiphertextSize]byte(ciphertext)
416+
417+
handshake.presharedKey = NoisePresharedKey(sspq)
418+
395419
// add preshared key
396420

397421
var tau [blake2s.Size]byte
@@ -468,6 +492,21 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
468492
mixKey(&chainKey, &chainKey, ss[:])
469493
setZero(ss[:])
470494

495+
// set preshared key
496+
497+
dk, err := mlkem768.NewKeyFromSeed(handshake.localEphemeralPQ[:])
498+
if err != nil {
499+
return false
500+
}
501+
sspq, err := mlkem768.Decapsulate(dk, msg.CiphertextPQ[:])
502+
if err != nil {
503+
return false
504+
}
505+
handshake.presharedKey = NoisePresharedKey(sspq)
506+
507+
setZero(sspq[:])
508+
setZero(handshake.localEphemeralPQ[:])
509+
471510
// add preshared key (psk)
472511

473512
var tau [blake2s.Size]byte

go.mod

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
module github.com/tailscale/wireguard-go
22

3-
go 1.20
3+
go 1.21.3
4+
5+
toolchain go1.24.3
46

57
require (
6-
golang.org/x/crypto v0.13.0
7-
golang.org/x/net v0.15.0
8-
golang.org/x/sys v0.12.0
8+
golang.org/x/crypto v0.26.0
9+
golang.org/x/net v0.21.0
10+
golang.org/x/sys v0.24.0
911
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
1012
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
1113
)
1214

1315
require (
16+
filippo.io/mlkem768 v0.0.0-20241021091500-d85de16e2039 // indirect
1417
github.com/google/btree v1.0.1 // indirect
1518
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
1619
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
filippo.io/mlkem768 v0.0.0-20241021091500-d85de16e2039 h1:I/alPPIVzEkPeQKVU7Sl5gv/sQ0IC4zgqHiACrSgUW8=
2+
filippo.io/mlkem768 v0.0.0-20241021091500-d85de16e2039/go.mod h1:IkpYfciLz5fI/S4/Z0NlhR4cpv6ubCMDnIwAe0XiojA=
13
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
24
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
35
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
46
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
7+
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
8+
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
59
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
610
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
11+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
12+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
713
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
814
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15+
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
16+
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
917
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
1018
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
1119
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=

0 commit comments

Comments
 (0)