Adding message type 5/6 for PQC (was Re: Export noise primitives for additional "chain key ratcheting")

Paul Spooren <[email protected]> Thu, 9 Apr 2026 23:31:55 +0900
Newsgroups com.zx2c4.lists.wireguard
Message-ID <[email protected]>
Hello again,

To follow up my previous email, I gave it a bit more thoughts. Two new =
messages types now strike me as an easier migration path. I wrote the =
design and mangled it through an AI for readability. I=E2=80=99d be =
interested if other people have similar ideas or pursue entirely =
different approaches for PQC.

Heads up, dealing with such big keys (~500kb) breaks this nice and clean =
WireGuard feel. However, to stay compatible with existing scripts, the =
design outlined below still allows addressing peers with their =
=E2=80=9Cclassical=E2=80=9D public key, while peer setup requires the =
full path.

Anyway, for those who care:

## Motivation

A sufficiently powerful quantum computer running Shor's algorithm can
break Curve25519 ECDH, allowing an attacker to:

- Decrypt recorded WireGuard handshakes ("harvest now, decrypt later")
- Recover static keys from captured initiations and impersonate peers

WireGuard's Noise IK handshake could be augmented with post-quantum
key exchange to protect against this threat while retaining full
backward compatibility with existing deployments.

## Approach

We add two new message types (5 and 6) that extend the existing
Noise IKpsk2 handshake with a hybrid post-quantum layer. The classical
Noise IK steps run first, unchanged, followed by two additional KEM
operations that mix post-quantum shared secrets into the session key.

The design is hybrid: even if the PQC algorithms are broken, the
classical X25519 security remains. The PQC layer can only add entropy,
never weaken.

### Algorithm Choice

- **Classic McEliece 460896** for static peer authentication. Code-based
  cryptography with 40+ years of cryptanalysis. Conservative choice for
  long-term identity keys. Decapsulation is fast (~40=CE=BCs); the =
tradeoff
  is a large public key (~524 KB).

- **ML-KEM-512** (FIPS 203) for ephemeral forward secrecy. =
Lattice-based,
  NIST-standardized. Fresh keypair per handshake ensures that =
compromising
  static keys cannot decrypt past sessions. Compact: 800-byte public =
key,
  768-byte ciphertext.

### Wire Format

```
Type 5 =E2=80=94 PQC Initiation (1104 bytes):
  [type:4][sender:4][ephemeral:32][enc_static:48][enc_timestamp:28]
  [mce_ciphertext:156][mlkem_encaps_key:800]
  [mac1:16][mac2:16]

Type 6 =E2=80=94 PQC Response (1032 bytes):
  [type:4][sender:4][receiver:4][ephemeral:32][enc_nothing:16]
  [mce_ciphertext:156][mlkem_ciphertext:768][enc_empty:16]
  [mac1:16][mac2:16]
```

Backward compatible: existing WireGuard implementations silently drop
unknown message types. Classical peers continue using types 1-4 without
modification.

### Protocol Flow

**Type 5 creation (initiator):**

Phase 1 =E2=80=94 classical Noise IK, identical to Type 1:

1. Generate ephemeral X25519 key, mix into chain
2. DH(ephemeral, responder_static) =E2=86=92 encrypt initiator's static =
key
3. DH(initiator_static, responder_static) =E2=86=92 encrypt timestamp

Phase 2 =E2=80=94 post-quantum augmentation:

4. Domain separation: mix PQC construction string into chain key
5. McEliece encapsulate to responder's static McEliece key =E2=86=92 =
156-byte
   ciphertext + 32-byte shared secret, mixed into chain key via KDF
6. ML-KEM keygen =E2=86=92 800-byte encapsulation key sent in message,
   1632-byte decapsulation key held in handshake state

**Type 6 creation (responder):**

Phase 1 =E2=80=94 classical response (e, ee, se, psk, {}), identical to =
Type 2.

Phase 2 =E2=80=94 post-quantum augmentation:

4. McEliece encapsulate to initiator's static McEliece key =E2=86=92 =
shared
   secret mixed into chain key
5. ML-KEM encapsulate using initiator's ephemeral encapsulation key =E2=86=
=92
   shared secret mixed into chain key
6. Encrypt empty payload for transcript authentication

Both sides derive identical session keys from the combined classical +
PQC chain key, then proceed with the normal WireGuard transport.

### PQC-Aware MAC1

For Type 5/6, the mac1 key derivation includes the responder's McEliece
public key hash:

```
pqc_mac1_key =3D BLAKE2s(classical_mac1_key || BLAKE2s(mce_pubkey))
```

This tightens the DoS boundary: only peers who possess the responder's
full PQC public key (~524 KB) can send valid Type 5 messages. Classical
Type 1/2 mac1 is unchanged.

## Key Management

### No Separate PQC Private Key

The McEliece keypair is derived deterministically from the existing
WireGuard private key:

```
seed =3D BLAKE2s(wg_private_key || "wg-pqc-mceliece-seed")
mce_sk, mce_pk =3D McEliece460896_KeyGen(seed)
```

One secret to protect. No additional key storage. Changing the WireGuard
private key automatically rotates the PQC identity.

### Unified PublicKey Field

There is no separate PQC configuration key. The standard `PublicKey`
field handles both classical and PQC peers:

- **44 base64 characters** (32 bytes decoded): classical Curve25519 key
- **~700 KB base64** (524,192 bytes decoded): classical key + McEliece
  public key, concatenated

Since McEliece public keys are too large for config file lines or
netlink messages, if a PublicKey value starts with `/`, it is read from
that file path:

```ini
[Interface]
PrivateKey =3D yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=3D

[Peer]
PublicKey =3D /etc/wireguard/peer1.pub
AllowedIPs =3D 10.0.0.2/32
Endpoint =3D 203.0.113.1:51820
```

A new `wg pqcpubkey` command generates the key file:

```sh
wg genkey | tee /etc/wireguard/private | wg pqcpubkey > =
/etc/wireguard/peer1.pub
```

This reads a private key from stdin (same UX as `wg pubkey`) and outputs
the combined base64 blob.

### PQC Enforcement

When a peer is configured with a PQC public key, classical Type 1
initiations from that peer are silently rejected. There is no way to
downgrade a PQC-configured peer to classical-only. No explicit toggle
is needed =E2=80=94 the presence of the McEliece key implies PQC-only.

## What We Explicitly Do Not Do

- No separate PQC configuration keys or toggles
- No modification to the classical Noise IK protocol
- No changes to the transport data path (Type 4)


> On 1. Apr 2026, at 05:14, Paul Spooren <[email protected]> wrote:
>=20
> Hi,
>=20
> I=E2=80=99m looking into hardening WireGuard against quantum =
computers, specifically how to extend the Noise-based handshake.=20
>=20
> A bit of background, while PSK injection with external daemons for =
forward secrecy, exists, that=E2=80=99s extra software running, exposing =
extra ports etc. Modifying a Rust/Go implementation is easier than the =
Kernel, but my background is running things on WiFi routers, so Kernel =
is preferred. That said, I=E2=80=99d like to extend the current =
WireGuard message format and attach PQC without exceeding the IPv6 MTU =
of 1280 bytes. In literature I don=E2=80=99t find definitive a PQC =
handshake standard, however combining ML-KEM512 for forward secrecy and =
McEliece460896 for static long-term keys does exist in practice[1] and =
fits into WireGuard init and response messages. Alternatively, sntrup653 =
could be used for forward secrecy without PQ authentication, similar to =
Signal's PQXDH[2]. Possibly new primitives/schemes are developed, with =
smaller public keys and ciphertexts.=20
>=20
> Just today I read an email about ML-KEM and the Kernel; none of these =
cryptographic primitives are (to my knowledge) part of the Linux Kernel, =
making a =E2=80=9Cnew=E2=80=9D protocol version of WireGuard more =
difficult to implement and further out in the future.
>=20
> I wondered if WireGuard could export some of the noise primitives like =
mix_hash and mix_key to allow =E2=80=9Cone way=E2=80=9D (ratchet) =
modifications of the chaining key and =E2=80=9Cappend only" bytes to the =
outgoing packet (PQ pubkey, ciphertext). After appending bytes, =
WireGuard takes over again and calculates the MAC, proceeding as usual. =
The "ratcheting=E2=80=9D Kernel module would work similar to the =
existing PSK approach, it adds additional data to the chaining key, =
however it can=E2=80=99t downgrade it (except crashing the Kernel). =
Another approach could be to add netlink handling for an active daemon, =
this would at least reduce the open ports and network complexity.=20
>=20
> Yet another way to have PQ WireGuard could be to produce one's own =
modules like WolfGuard did the recently[3] with AES/FIPS, however I =
thought to ask anyway.
>=20
> Thanks,
> Paul
>=20
> [1]: https://rosenpass.eu/docs/rosenpass-project/whitepaper/
> [2]: https://signal.org/docs/specifications/pqxdh/
> [3]: https://github.com/wolfSSL/wolfGuard