]>
XWing: generalpurpose hybrid postquantum KEM
SandboxAQ
durumcrustulum@gmail.com
MPISP & Radboud University
peter@cryptojedi.org
Cloudflare
bas@cloudflare.com
IRTF
Crypto Forum
post quantum
kem
PQ/T hybrid
This memo defines XWing, a generalpurpose postquantum/traditional
hybrid key encapsulation mechanism (PQ/T KEM) built on X25519 and
MLKEM768.
About This Document
The latest revision of this draft can be found at .
Status information for this document may be found at .
Discussion of this document takes place on the
Crypto Forum Research Group mailing list (),
which is archived at .
Subscribe at .
Source for this draft and an issue tracker can be found at
.
Introduction
Motivation
There are many choices that can be made when specifying a hybrid KEM:
the constituent KEMs; their security levels; the combiner; and the hash
within, to name but a few. Having too many similar options are a burden
to the ecosystem.
The aim of XWing is to provide a concrete, simple choice for
postquantum hybrid KEM, that should be suitable for the vast majority
of use cases.
Design goals
By making concrete choices, we can simplify and improve many aspects of
XWing.

Simplicity of definition. Because all shared secrets and cipher texts
are fixed length, we do not need to encode the length. Using SHA3256,
we do not need HMACbased construction. For the concrete choice of
MLKEM768, we do not need to mix in its ciphertext, see .

Security analysis. Because MLKEM768 already assumes the Quantum Random
Oracle Model (QROM), we do not need to complicate the analysis
of XWing by considering stronger models.

Performance. Not having to mix in the MLKEM768 ciphertext is a nice
performance benefit. Furthermore, by using SHA3256 in the combiner,
which matches the hashing in MLKEM768, this hash can be computed in
one go on platforms where twoway Keccak is available.
We aim for "128 bits" security (NIST PQC level 1). Although at the
moment there is no peerreviewed evidence that MLKEM512 does not reach
this level, we would like to hedge against future cryptanalytic
improvements, and feel MLKEM768 provides a comfortable margin.
We aim for XWing to be usable for most applications, including
specifically HPKE .
Not an interactive keyagreement
Traditionally most protocols use a DiffieHellman (DH) style
noninteractive keyagreement. In many cases, a DH key agreement can be
replaced by the interactive keyagreement afforded by a KEM without
change in the protocol flow. One notable example is TLS
. However, not all uses of DH can be replaced in a
straightforward manner by a plain KEM.
Not an authenticated KEM
In particular, XWing is not, borrowing the language of , an
authenticated KEM.
Comparisons
With HPKE X25519Kyber768Draft00
XWing is most similar to HPKE's X25519Kyber768Draft00
. The key differences are:

XWing uses the final version of MLKEM768.

XWing hashes the shared secrets, to be usable outside of HPKE.

XWing has a simpler combiner by flattening DHKEM(X25519) into the
final hash.

XWing does not hash in the MLKEM768 ciphertext.
There is also a different KEM called X25519Kyber768Draft00
which is used in TLS. This one should not be used outside of TLS, as it
assumes the presence of the TLS transcript to ensure non malleability.
With generic combiner
The generic combiner of can be
instantiated with MLKEM768 and DHKEM(X25519). That achieves similar
security, but:

XWing is more performant, not hashing in the MLKEM768 ciphertext,
and flattening the DHKEM construction, with the same level of
security.

XWing has a fixed 32 byte shared secret, instead of a variable shared
secret.

XWing does not accept the optional counter and fixedInfo arguments.
Requirements Notation
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED",
"MAY", and "OPTIONAL" in this document are to be interpreted as
described in BCP 14 when, and only when, they
appear in all capitals, as shown here.
Conventions and Definitions
This document is consistent with all terminology defined in
.
The following terms are used throughout this document to describe the
operations, roles, and behaviors of HPKE:

concat(x0, ..., xN): returns the concatenation of byte
strings. concat(0x01, 0x0203, 0x040506) = 0x010203040506.

random(n): return a pseudorandom byte string of length n bytes produced by
a cryptographicallysecure random number generator.
Cryptographic Dependencies
XWing relies on the following primitives:

MLKEM768 postquantum keyencapsulation mechanism (KEM) :

MLKEM768.KeyGen(): Randomized algorithm to generate an
MLKEM768 key pair (pk_M, sk_M) of an encapsulation key pk_M
and decapsulation key sk_M.
Note that MLKEM768.KeyGen() returns the keys in reverse
order of GenerateKeyPair() defined below.

MLKEM768.Encaps(pk_M): Randomized algorithm to generate (ss_M,
ct_M), an ephemeral 32 byte shared key ss_M, and a fixedlength
encapsulation (ciphertext) of that key ct_M for encapsulation key pk_M.
MLKEM768.Encaps(pk_M) MUST perform the encapsulation key check
of §7.2 and raise an error if it fails.

MLKEM768.Decap(ct_M, sk_M): Deterministic algorithm using the
decapsulation key sk_M to recover the shared key from ct_M.
To generate deterministic test vectors, we also use

MLKEM768.KeyGen_internal(d, z): Algorithm to generate an
MLKEM768 key pair (pk_M, sk_M) of an encapsulation key pk_M
and decapsulation key sk_M.
Note that MLKEM768.KeyGen() returns the keys in reverse
order of GenerateKeyPair() defined below.
d and z are both 32 byte strings.

MLKEM768.Encaps_internal(pk_M, m): Algorithm to generate (ss_M, ct_M),
an ephemeral 32 byte shared key ss_M, and a fixedlength
encapsulation (ciphertext) of that key ct_M for encapsulation key
pk_M. m is a 32 byte string.
MLKEM768.Encaps_internal(pk_M) MUST perform the encapsulation key check
of §7.2 and raise an error if it fails.

X25519 elliptic curve DiffieHellman keyexchange defined in :

X25519(k,u): takes 32 byte strings k and u representing a
Curve25519 scalar and curvepoint respectively, and returns
the 32 byte string representing their scalar multiplication.

X25519_BASE: the 32 byte string representing the standard base point
of Curve25519. In hex
it is given by 0900000000000000000000000000000000000000000000000000000000000000.
Note that 9 is the standard basepoint for X25519, cf .

Symmetric cryptography.

SHAKE128(message, outlen): The extendableoutput function (XOF)
defined in Section 6.2 of .

SHA3256(message): The hash defined in Section 6.1 of .
XWing Construction
Encoding and sizes
XWing encapsulation key, decapsulation key, ciphertexts and shared secrets are all
fixed length byte strings.
 Decapsulation key (private):

32 bytes
 Encapsulation key (public):

1216 bytes
 Ciphertext:

1120 bytes
 Shared secret:

32 bytes
Key generation
An XWing keypair (decapsulation key, encapsulation key) is generated as
follows.
GenerateKeyPair() returns the 32 byte secret decapsulation key sk
and the 1216 byte encapsulation key pk.
Here and in the balance of the document for clarity we use
the M and Xsubscripts for MLKEM768 and X25519 components respectively.
Key derivation
For testing, it is convenient to have a deterministic version
of key generation. An XWing implementation MAY provide the following
derandomized variant of key generation.
seed must be 32 bytes.
GenerateKeyPairDerand() returns the 32 byte secret encapsulation key
sk and the 32 byte decapsulation key pk.
Combiner
Given 32 byte strings ss_M, ss_X, ct_X, pk_X, representing the
MLKEM768 shared secret, X25519 shared secret, X25519 ciphertext
(ephemeral public key) and X25519 public key respectively, the 32 byte
combined shared secret is given by:
where XWingLabel is the following 6 byte ASCII string
In hex XWingLabel is given by 5c2e2f2f5e5c.
Encapsulation
Given an XWing encapsulation key pk, encapsulation proceeds as follows.
pk is a 1216 byte XWing encapsulation key resulting from GeneratePublicKey()
Encapsulate() returns the 32 byte shared secret ss and the 1120 byte
ciphertext ct.
Note that Encapsulate() may raise an error if the MLKEM encapsulation
does not pass the check of §7.2.
Derandomized
For testing, it is convenient to have a deterministic version
of encapsulation. An XWing implementation MAY provide
the following derandomized function.
pk is a 1216 byte XWing encapsulation key resulting from GeneratePublicKey()
eseed MUST be 64 bytes.
EncapsulateDerand() returns the 32 byte shared secret ss and the 1120 byte
ciphertext ct.
Decapsulation
ct is the 1120 byte ciphertext resulting from Encapsulate()
sk is a 32 byte XWing decapsulation key resulting from GenerateKeyPair()
Decapsulate() returns the 32 byte shared secret.
Keeping expanded decapsulation key around
For efficiency, an implementation MAY cache the result of expandDecapsulationKey.
This is useful in two cases:

If multiple ciphertexts for the same key are decapsulated.

If a ciphertext is decapsulated for a key that has just been generated.
This happen on the clientside for TLS.
A typical API pattern to achieve this optimization is to have an
opaque decapsulation key object that hides the cached values.
For instance, such an API could have the following functions.

UnpackDecapsulationKey(sk) takes a decapsulation key, and returns
an opaque object that contains the expanded decapsulation key.

Decapsulate(ct, esk) takes a ciphertext and an expanded decapsulation key.

GenerateKeyPair() returns an encapsulation key and an expanded
decapsulation key.

PackDecapsulationKey(sk) takes an expanded decapsulation key,
and returns the packed decapsulation key.
The expanded decapsulation key could cache more computation,
such as the expanded matrix A in MLKEM.
Any such expanded decapsulation key MUST NOT be transmitted between
implementations, as this could break the security analysis of XWing.
In particular, the MALBINDKPK and MALBINDKCT binding
properties of XWing do not hold when transmitting the regular MLKEM
decapsulation key.
Use in HPKE
XWing satisfies the HPKE KEM interface as follows.
The SerializePublicKey, SerializePrivateKey,
and DeserializePrivateKey are the identity functions,
as XWing keys are fixedlength byte strings, see .
DeriveKeyPair() is given by
where the HPKE private key and public key are the XWing decapsulation
key and encapsulation key respectively.
Encap() is Encapsulate() from , where an
MLKEM encapsulation key check failure causes an HPKE EncapError.
Decap() is Decapsulate() from .
XWing is not an authenticated KEM: it does not support AuthEncap()
and AuthDecap(), see .
Nsecret, Nenc, Npk, and Nsk are defined in .
Use in TLS 1.3
For the client's share, the key_exchange value contains
the XWing encapsulation key.
For the server's share, the key_exchange value contains
the XWing ciphertext.
On MLKEM encapsulation key check failure, the server MUST
abort with an illegal_parameter alert.
Security Considerations
Informally, XWing is secure if SHA3 is secure, and either X25519 is
secure, or MLKEM768 is secure.
More precisely, if SHA3256, SHA3512, SHAKE128, and SHAKE256 may be
modelled as a random oracle, then the INDCCA security of XWing is
bounded by the INDCCA security of MLKEM768, and the gapCDH security
of Curve25519, see .
The security of XWing relies crucially on the specifics of the
FujisakiOkamoto transformation used in MLKEM768: the XWing
combiner cannot be assumed to be secure, when used with different
KEMs. In particular it is not known to be safe to leave
out the postquantum ciphertext from the combiner in the general case.
Binding properties
Some protocols rely on further properties of the KEM.
XWing satisfies the binding properties MALBINDKPK and MALBINDKCT
(TODO: reference to proof).
This implies XWing also satisfies

MALBINDK,CTPK

MALBINDK,PKCT

LEAKBINDKPK

LEAKBINDKCT

LEAKBINDK,CTPK

LEAKBINDK,PKCT

HONBINDKPK

HONBINDKCT

HONBINDK,CTPK

HONBINDK,PKCT
In contrast, MLKEM on its own does not achieve
MALBINDKPK, MALBINDKCT, nor MALBINDK,PKCT.
IANA Considerations
This document requests/registers a new entry to the "HPKE KEM Identifiers"
registry.
 Value:

TBD (please)
 KEM:

XWing
 Nsecret:

32
 Nenc:

1120
 Npk:

1216
 Nsk:

32
 Auth:

no
 Reference:

This document
Furthermore, this document requests/registers a new entry to the TLS
Named Group (or Supported Group) registry, according to the procedures
in .
 Value:

26287 (please)
 Description:

XWing
 DTLSOK:

Y
 Recommended:

Y
 Reference:

This document
 Comment:

PQ/T hybrid of X25519 and MLKEM768
References
Normative References
Key words for use in RFCs to Indicate Requirement Levels
In many standards track documents several words are used to signify the requirements in the specification. These words are often capitalized. This document defines these words as they should be interpreted in IETF documents. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.
Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words
RFC 2119 specifies common key words that may be used in protocol specifications. This document aims to reduce the ambiguity by clarifying that only UPPERCASE usage of the key words have the defined special meanings.
Informative References
Terminology for PostQuantum Traditional Hybrid Schemes
UK National Cyber Security Centre
One aspect of the transition to postquantum algorithms in
cryptographic protocols is the development of hybrid schemes that
incorporate both postquantum and traditional asymmetric algorithms.
This document defines terminology for such schemes. It is intended
to be used as a reference and, hopefully, to ensure consistency and
clarity across different protocols, standards, and organisations.
Combiner function for hybrid key encapsulation mechanisms (Hybrid KEMs)
Entrust Limited
Proton AG
BSI
The migration to postquantum cryptography often calls for performing
multiple key encapsulations in parallel and then combining their
outputs to derive a single shared secret.
This document defines a comprehensible and easy to implement Keccak
based KEM combiner to join an arbitrary number of key shares, that is
compatible with NIST SP 80056Cr2 [SP80056C] when viewed as a key
derivation function. The combiners defined here are practical split
key PRFs and are CCAsecure as long as at least one of the ingredient
KEMs is.
FIPS 202: SHA3 Standard: PermutationBased Hash and ExtendableOutput Functions
n.d.
FIPS 203: ModuleLatticeBased KeyEncapsulation Mechanism Standard
n.d.
Hybrid Public Key Encryption
This document describes a scheme for hybrid public key encryption (HPKE). This scheme provides a variant of public key encryption of arbitrarysized plaintexts for a recipient public key. It also includes three authenticated variants, including one that authenticates possession of a preshared key and two optional ones that authenticate possession of a key encapsulation mechanism (KEM) private key. HPKE works for any combination of an asymmetric KEM, key derivation function (KDF), and authenticated encryption with additional data (AEAD) encryption function. Some authenticated variants may not be supported by all KEMs. We provide instantiations of the scheme using widely used and efficient primitives, such as Elliptic Curve DiffieHellman (ECDH) key agreement, HMACbased key derivation function (HKDF), and SHA2.
This document is a product of the Crypto Forum Research Group (CFRG) in the IRTF.
Elliptic Curves for Security
This memo specifies two elliptic curves over prime fields that offer a high level of practical security in cryptographic applications, including Transport Layer Security (TLS). These curves are intended to operate at the ~128bit and ~224bit security level, respectively, and are generated deterministically based on a list of required properties.
Hybrid key exchange in TLS 1.3
University of Waterloo
Cisco Systems
University of Haifa and Amazon Web Services
Hybrid key exchange refers to using multiple key exchange algorithms
simultaneously and combining the result with the goal of providing
security even if all but one of the component algorithms is broken.
It is motivated by transition to postquantum cryptography. This
document provides a construction for hybrid key exchange in the
Transport Layer Security (TLS) protocol version 1.3.
Discussion of this work is encouraged to happen on the TLS IETF
mailing list tls@ietf.org or on the GitHub repository which contains
the draft: https://github.com/dstebila/draftstebilatlshybrid
design.
X25519Kyber768Draft00 hybrid postquantum KEM for HPKE
Cloudflare
Cloudflare
This memo defines X25519Kyber768Draft00, a hybrid postquantum KEM,
for HPKE (RFC9180). This KEM does not support the authenticated
modes of HPKE.
X25519Kyber768Draft00 hybrid postquantum key agreement
Cloudflare
University of Waterloo
This memo defines X25519Kyber768Draft00, a hybrid postquantum key
exchange for TLS 1.3.
IANA Registry Updates for TLS and DTLS
Venafi
sn3rd
This document updates the changes to TLS and DTLS IANA registries
made in RFC 8447. It adds a new value "D" for discouraged to the
recommended column of the selected TLS registries.
This document updates the following RFCs: 3749, 5077, 4680, 5246,
5705, 5878, 6520, 7301, and 8447.
Unbindable Kemmy Schmidt: MLKEM is neither MALBINDKCT nor MALBINDKPK
n.d.
Binding Security of ImplicitlyRejecting KEMs and Application to BIKE and HQC
n.d.
XWing: The Hybrid KEM You’ve Been Looking For
n.d.
Implementations

Go

Rust

xwingkem.rs
Note: implements the older 00 version of this memo at the time of
writing.
Machinereadable specification
For the convenience of implementors, we provide a reference specification
in Python. This is a specification; not production ready code:
it should not be deployed asis, as it leaks the private key by its runtime.
x25519.py
> t) & 1
swap ^= kt
if swap == 1:
x3, x2 = x2, x3
z3, z2 = z2, z3
swap = kt
A = x2 + z2
AA = (A*A) % p
B = x2  z2
BB = (B*B) % p
E = AA  BB
C = x3 + z3
D = x3  z3
DA = (D*A) % p
CB = (C*B) % p
x3 = DA + CB
x3 = (x3 * x3) % p
z3 = DA  CB
z3 = (x1 * z3 * z3) % p
x2 = (AA * BB) % p
z2 = (E * (AA + (a24 * E) % p)) % p
if swap == 1:
x3, x2 = x2, x3
z2, z3 = z3, z2
ret = (x2 * pow(z2, p2, p)) % p
return bytes((ret >> 8*i) & 255 for i in range(32))
]]>
mlkem.py
(q1)//2:
r = q
return r
# Rounds to nearest integer with ties going up
def Round(x):
return int(floor(x + 0.5))
def Compress(x, d):
return Round((2**d / q) * x) % (2**d)
def Decompress(y, d):
assert 0 <= y and y <= 2**d
return Round((q / 2**d) * y)
def BitsToWords(bs, w):
assert len(bs) % w == 0
return [sum(bs[i+j] * 2**j for j in range(w))
for i in range(0, len(bs), w)]
def WordsToBits(bs, w):
return sum([[(b >> i) % 2 for i in range(w)] for b in bs], [])
def Encode(a, w):
return bytes(BitsToWords(WordsToBits(a, w), 8))
def Decode(a, w):
return BitsToWords(WordsToBits(a, 8), w)
def brv(x):
""" Reverses a 7bit number """
return int(''.join(reversed(bin(x)[2:].zfill(nBits1))), 2)
class Poly:
def __init__(self, cs=None):
self.cs = (0,)*n if cs is None else tuple(cs)
assert len(self.cs) == n
def __add__(self, other):
return Poly((a+b) % q for a,b in zip(self.cs, other.cs))
def __neg__(self):
return Poly(qa for a in self.cs)
def __sub__(self, other):
return self + other
def __str__(self):
return f"Poly({self.cs}"
def __eq__(self, other):
return self.cs == other.cs
def NTT(self):
cs = list(self.cs)
layer = n // 2
zi = 0
while layer >= 2:
for offset in range(0, nlayer, 2*layer):
zi += 1
z = pow(zeta, brv(zi), q)
for j in range(offset, offset+layer):
t = (z * cs[j + layer]) % q
cs[j + layer] = (cs[j]  t) % q
cs[j] = (cs[j] + t) % q
layer //= 2
return Poly(cs)
def RefNTT(self):
# Slower, but simpler, version of the NTT.
cs = [0]*n
for i in range(0, n, 2):
for j in range(n // 2):
z = pow(zeta, (2*brv(i//2)+1)*j, q)
cs[i] = (cs[i] + self.cs[2*j] * z) % q
cs[i+1] = (cs[i+1] + self.cs[2*j+1] * z) % q
return Poly(cs)
def InvNTT(self):
cs = list(self.cs)
layer = 2
zi = n//2
while layer < n:
for offset in range(0, nlayer, 2*layer):
zi = 1
z = pow(zeta, brv(zi), q)
for j in range(offset, offset+layer):
t = (cs[j+layer]  cs[j]) % q
cs[j] = (inv2*(cs[j] + cs[j+layer])) % q
cs[j+layer] = (inv2 * z * t) % q
layer *= 2
return Poly(cs)
def MulNTT(self, other):
""" Computes self o other, the multiplication of self and other
in the NTT domain. """
cs = [None]*n
for i in range(0, n, 2):
a1 = self.cs[i]
a2 = self.cs[i+1]
b1 = other.cs[i]
b2 = other.cs[i+1]
z = pow(zeta, 2*brv(i//2)+1, q)
cs[i] = (a1 * b1 + z * a2 * b2) % q
cs[i+1] = (a2 * b1 + a1 * b2) % q
return Poly(cs)
def Compress(self, d):
return Poly(Compress(c, d) for c in self.cs)
def Decompress(self, d):
return Poly(Decompress(c, d) for c in self.cs)
def Encode(self, d):
return Encode(self.cs, d)
def sampleUniform(stream):
cs = []
while True:
b = stream.read(3)
d1 = b[0] + 256*(b[1] % 16)
d2 = (b[1] >> 4) + 16*b[2]
assert d1 + 2**12 * d2 == b[0] + 2**8 * b[1] + 2**16*b[2]
for d in [d1, d2]:
if d >= q:
continue
cs.append(d)
if len(cs) == n:
return Poly(cs)
def CBD(a, eta):
assert len(a) == 64*eta
b = WordsToBits(a, 8)
cs = []
for i in range(n):
cs.append((sum(b[:eta])  sum(b[eta:2*eta])) % q)
b = b[2*eta:]
return Poly(cs)
def XOF(seed, j, i):
h = SHAKE128.new()
h.update(seed + bytes([j, i]))
return h
def PRF1(seed, nonce):
assert len(seed) == 32
h = SHAKE256.new()
h.update(seed + bytes([nonce]))
return h
def PRF2(seed, msg):
assert len(seed) == 32
h = SHAKE256.new()
h.update(seed + msg)
return h.read(32)
def G(seed):
h = hashlib.sha3_512(seed).digest()
return h[:32], h[32:]
def H(msg): return hashlib.sha3_256(msg).digest()
class Vec:
def __init__(self, ps):
self.ps = tuple(ps)
def NTT(self):
return Vec(p.NTT() for p in self.ps)
def InvNTT(self):
return Vec(p.InvNTT() for p in self.ps)
def DotNTT(self, other):
""" Computes the dot product in NTT domain. """
return sum((a.MulNTT(b) for a, b in zip(self.ps, other.ps)),
Poly())
def __add__(self, other):
return Vec(a+b for a,b in zip(self.ps, other.ps))
def Compress(self, d):
return Vec(p.Compress(d) for p in self.ps)
def Decompress(self, d):
return Vec(p.Decompress(d) for p in self.ps)
def Encode(self, d):
return Encode(sum((p.cs for p in self.ps), ()), d)
def __eq__(self, other):
return self.ps == other.ps
def EncodeVec(vec, w):
return Encode(sum([p.cs for p in vec.ps], ()), w)
def DecodeVec(bs, k, w):
cs = Decode(bs, w)
return Vec(Poly(cs[n*i:n*(i+1)]) for i in range(k))
def DecodePoly(bs, w):
return Poly(Decode(bs, w))
class Matrix:
def __init__(self, cs):
""" Samples the matrix uniformly from seed rho """
self.cs = tuple(tuple(row) for row in cs)
def MulNTT(self, vec):
""" Computes matrix multiplication A*vec in the NTT domain. """
return Vec(Vec(row).DotNTT(vec) for row in self.cs)
def T(self):
""" Returns transpose of matrix """
k = len(self.cs)
return Matrix((self.cs[j][i] for j in range(k))
for i in range(k))
def sampleMatrix(rho, k):
return Matrix([[sampleUniform(XOF(rho, j, i))
for j in range(k)] for i in range(k)])
def sampleNoise(sigma, eta, offset, k):
return Vec(CBD(PRF1(sigma, i+offset).read(64*eta), eta)
for i in range(k))
def constantTimeSelectOnEquality(a, b, ifEq, ifNeq):
# WARNING! In production code this must be done in a
# dataindependent constanttime manner, which this implementation
# is not. In fact, many more lines of code in this
# file are not constanttime.
return ifEq if a == b else ifNeq
def InnerKeyGen(seed, params):
assert len(seed) == 32
rho, sigma = G(seed + bytes([params.k]))
A = sampleMatrix(rho, params.k)
s = sampleNoise(sigma, params.eta1, 0, params.k)
e = sampleNoise(sigma, params.eta1, params.k, params.k)
sHat = s.NTT()
eHat = e.NTT()
tHat = A.MulNTT(sHat) + eHat
pk = EncodeVec(tHat, 12) + rho
sk = EncodeVec(sHat, 12)
return (pk, sk)
def InnerEnc(pk, msg, seed, params):
assert len(msg) == 32
tHat = DecodeVec(pk[:32], params.k, 12)
if EncodeVec(tHat, 12) != pk[:32]:
raise Exception("MLKEM public key not normalized")
rho = pk[32:]
A = sampleMatrix(rho, params.k)
r = sampleNoise(seed, params.eta1, 0, params.k)
e1 = sampleNoise(seed, eta2, params.k, params.k)
e2 = sampleNoise(seed, eta2, 2*params.k, 1).ps[0]
rHat = r.NTT()
u = A.T().MulNTT(rHat).InvNTT() + e1
m = Poly(Decode(msg, 1)).Decompress(1)
v = tHat.DotNTT(rHat).InvNTT() + e2 + m
c1 = u.Compress(params.du).Encode(params.du)
c2 = v.Compress(params.dv).Encode(params.dv)
return c1 + c2
def InnerDec(sk, ct, params):
split = params.du * params.k * n // 8
c1, c2 = ct[:split], ct[split:]
u = DecodeVec(c1, params.k, params.du).Decompress(params.du)
v = DecodePoly(c2, params.dv).Decompress(params.dv)
sHat = DecodeVec(sk, params.k, 12)
return (v  sHat.DotNTT(u.NTT()).InvNTT()).Compress(1).Encode(1)
def KeyGen(seed, params):
assert len(seed) == 64
z = seed[32:]
pk, sk2 = InnerKeyGen(seed[:32], params)
h = H(pk)
return (pk, sk2 + pk + h + z)
def Enc(pk, seed, params):
assert len(seed) == 32
K, r = G(seed + H(pk))
ct = InnerEnc(pk, seed, r, params)
return (ct, K)
def Dec(sk, ct, params):
sk2 = sk[:12 * params.k * n//8]
pk = sk[12 * params.k * n//8 : 24 * params.k * n//8 + 32]
h = sk[24 * params.k * n//8 + 32 : 24 * params.k * n//8 + 64]
z = sk[24 * params.k * n//8 + 64 : 24 * params.k * n//8 + 96]
m2 = InnerDec(sk, ct, params)
K2, r2 = G(m2 + h)
ct2 = InnerEnc(pk, m2, r2, params)
return constantTimeSelectOnEquality(
ct2, ct,
K2, # if ct == ct2
PRF2(z, ct), # if ct != ct2
)
]]>
Test vectors # TODO: replace with test vectors that reuse MLKEM, X25519 values
Acknowledgments
TODO acknowledge.
Change log

RFC Editor's Note: Please remove this section prior to publication of a
final version of this document.
Since draftconnollycfrgxwingkem03

Mandate MLKEM encapsulation key check, and stipulate effect
on TLS and HPKE integration.

Add provisional TLS codepoint.
Since draftconnollycfrgxwingkem02

Use seed as private key.

Expand on caching decapsulation key values.

Expand on binding properties.
Since draftconnollycfrgxwingkem01

Add list of implementations.

Miscellaneous editorial improvements.

Add Python reference specification.

Correct definition of MLKEM768.KeyGenDerand(seed).
Since draftconnollycfrgxwingkem00

A copy of the X25519 public key is now included in the XWing
decapsulation (private) key, so that decapsulation does not
require separate access to the XWing public key. See #2.