Skip to content

Commit b1e7ddb

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 65cd6ee commit b1e7ddb

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
@@ -12,6 +12,7 @@ import (
1212
"sync"
1313
"time"
1414

15+
"filippo.io/mlkem768"
1516
"golang.org/x/crypto/blake2s"
1617
"golang.org/x/crypto/chacha20poly1305"
1718
"golang.org/x/crypto/poly1305"
@@ -61,8 +62,8 @@ const (
6162
)
6263

6364
const (
64-
MessageInitiationSize = 148 // size of handshake initiation message
65-
MessageResponseSize = 92 // size of response message
65+
MessageInitiationSize = 1332 // size of handshake initiation message
66+
MessageResponseSize = 1180 // size of response message
6667
MessageCookieReplySize = 64 // size of cookie reply message
6768
MessageTransportHeaderSize = 16 // size of data preceding content in transport message
6869
MessageEncapsulatingTransportSize = 8 // size of optional, free (for use by conn.Bind.Send()) space preceding the transport header
@@ -84,23 +85,25 @@ const (
8485
*/
8586

8687
type MessageInitiation struct {
87-
Type uint32
88-
Sender uint32
89-
Ephemeral NoisePublicKey
90-
Static [NoisePublicKeySize + poly1305.TagSize]byte
91-
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte
92-
MAC1 [blake2s.Size128]byte
93-
MAC2 [blake2s.Size128]byte
88+
Type uint32
89+
Sender uint32
90+
Ephemeral NoisePublicKey
91+
EphemeralPQ [mlkem768.EncapsulationKeySize]byte
92+
Static [NoisePublicKeySize + poly1305.TagSize]byte
93+
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte
94+
MAC1 [blake2s.Size128]byte
95+
MAC2 [blake2s.Size128]byte
9496
}
9597

9698
type MessageResponse struct {
97-
Type uint32
98-
Sender uint32
99-
Receiver uint32
100-
Ephemeral NoisePublicKey
101-
Empty [poly1305.TagSize]byte
102-
MAC1 [blake2s.Size128]byte
103-
MAC2 [blake2s.Size128]byte
99+
Type uint32
100+
Sender uint32
101+
Receiver uint32
102+
Ephemeral NoisePublicKey
103+
CiphertextPQ [mlkem768.CiphertextSize]byte
104+
Empty [poly1305.TagSize]byte
105+
MAC1 [blake2s.Size128]byte
106+
MAC2 [blake2s.Size128]byte
104107
}
105108

106109
type MessageTransport struct {
@@ -212,14 +215,16 @@ func (msg *MessageCookieReply) marshal(b []byte) error {
212215
type Handshake struct {
213216
state handshakeState
214217
mutex sync.RWMutex
215-
hash [blake2s.Size]byte // hash value
216-
chainKey [blake2s.Size]byte // chain key
217-
presharedKey NoisePresharedKey // psk
218-
localEphemeral NoisePrivateKey // ephemeral secret key
219-
localIndex uint32 // used to clear hash-table
220-
remoteIndex uint32 // index for sending
221-
remoteStatic NoisePublicKey // long term key, never changes, can be accessed without mutex
222-
remoteEphemeral NoisePublicKey // ephemeral public key
218+
hash [blake2s.Size]byte // hash value
219+
chainKey [blake2s.Size]byte // chain key
220+
presharedKey NoisePresharedKey // psk
221+
localEphemeral NoisePrivateKey // ephemeral secret key
222+
localEphemeralPQ [mlkem768.SeedSize]byte // ephemeral secret PQC key
223+
localIndex uint32 // used to clear hash-table
224+
remoteIndex uint32 // index for sending
225+
remoteStatic NoisePublicKey // long term key, never changes, can be accessed without mutex
226+
remoteEphemeral NoisePublicKey // ephemeral public key
227+
remoteEphemeralPQ [mlkem768.EncapsulationKeySize]byte
223228
precomputedStaticStatic [NoisePublicKeySize]byte // precomputed shared secret
224229
lastTimestamp tai64n.Timestamp
225230
lastInitiationConsumption time.Time
@@ -287,9 +292,16 @@ func (device *Device) CreateMessageInitiation(peer *Peer) (*MessageInitiation, e
287292

288293
handshake.mixHash(handshake.remoteStatic[:])
289294

295+
dk, err := mlkem768.GenerateKey()
296+
if err != nil {
297+
return nil, err
298+
}
299+
handshake.localEphemeralPQ = [64]byte(dk.Bytes())
300+
290301
msg := MessageInitiation{
291-
Type: MessageInitiationType,
292-
Ephemeral: handshake.localEphemeral.publicKey(),
302+
Type: MessageInitiationType,
303+
Ephemeral: handshake.localEphemeral.publicKey(),
304+
EphemeralPQ: [mlkem768.EncapsulationKeySize]byte(dk.EncapsulationKey()),
293305
}
294306

295307
handshake.mixKey(msg.Ephemeral[:])
@@ -425,6 +437,8 @@ func (device *Device) ConsumeMessageInitiation(msg *MessageInitiation) *Peer {
425437
handshake.chainKey = chainKey
426438
handshake.remoteIndex = msg.Sender
427439
handshake.remoteEphemeral = msg.Ephemeral
440+
handshake.remoteEphemeralPQ = msg.EphemeralPQ
441+
428442
if timestamp.After(handshake.lastTimestamp) {
429443
handshake.lastTimestamp = timestamp
430444
}
@@ -486,6 +500,16 @@ func (device *Device) CreateMessageResponse(peer *Peer) (*MessageResponse, error
486500
}
487501
handshake.mixKey(ss[:])
488502

503+
// set preshared key
504+
505+
ciphertext, sspq, err := mlkem768.Encapsulate(handshake.remoteEphemeralPQ[:])
506+
if err != nil {
507+
return nil, err
508+
}
509+
msg.CiphertextPQ = [mlkem768.CiphertextSize]byte(ciphertext)
510+
511+
handshake.presharedKey = NoisePresharedKey(sspq)
512+
489513
// add preshared key
490514

491515
var tau [blake2s.Size]byte
@@ -562,6 +586,21 @@ func (device *Device) ConsumeMessageResponse(msg *MessageResponse) *Peer {
562586
mixKey(&chainKey, &chainKey, ss[:])
563587
setZero(ss[:])
564588

589+
// set preshared key
590+
591+
dk, err := mlkem768.NewKeyFromSeed(handshake.localEphemeralPQ[:])
592+
if err != nil {
593+
return false
594+
}
595+
sspq, err := mlkem768.Decapsulate(dk, msg.CiphertextPQ[:])
596+
if err != nil {
597+
return false
598+
}
599+
handshake.presharedKey = NoisePresharedKey(sspq)
600+
601+
setZero(sspq[:])
602+
setZero(handshake.localEphemeralPQ[:])
603+
565604
// add preshared key (psk)
566605

567606
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)