I support using a seed as the primary portable private key format.
Primarily it's simpler, smaller, and saves us from thinking about private key validation.
To make things concrete, we could go for the changes to FIPS 203 at the end of the e-mail [1].
To put things into perspective, on my Ice Lake laptop, keygen, encapsulation and decapsulation for ML-KEM-768-ipd currently run in 18,000, 19,000 and 21,000 cycles respectively.
Using a seed as private key makes decapsulation directly using this seed more expensive: 31,000 cycles in total.
Expanding the seed is essentially the same as keygen: 18,000 cycles.
Decapsulation using fully expanded private key is cheaper: 13,000 cycles.
In practice TLS stacks already have this split: they keep A around after generating client keyshare, and use that for faster decapsulation when they receive the server keyshare.
Best,
Bas
[1]
1. Rename K-PKE.KeyGen to K-PKE.ExpandPrivate; remove lines 1, 2, 20, and 21; add rho and sigma as arguments; and return (\hat{A}, \hat{t}, \hat{s}).
2. Add a new function ML-KEM.UnpackPrivate that takes a 32-byte dk as argument, and actas as follows:
(rho, sigma, z) = J(dk)
\hat{A}, \hat{t}, \hat{s} = K-PKE.ExpandPrivate(rho, sigma)
return (\hat{A}, \hat{t}, \hat{s}, rho, z)
Note that the other place J is used includes the ciphertext as input, which makes them input-length separated.
Also note that the call to J here has a capacity of 104 bytes, making it in essence a "SHA3-832".
3. Change ML-KEM.KeyGen to:
dk <$- B^32
(\hat{A}, \hat{t}, \hat{s}, rho, z) = ML-KEM.UnpackPrivate(dk)
ek = ByteEncode_12(\hat{t}) || rho
return (ek, dk, (\hat{A}, \hat{t}, \hat{s}, rho, z))
4. Change K-PKE.Encrypt to accept \hat{A}, and \hat{t} directly instead of ek_PKE, removing lines 2–8.
5. Change K-PKE.Decrypt to accept \hat{s} as argument directly instead of dk_PKE.
6. Change ML-KEM.Decaps to accept (\hat{A}, \hat{t}, \hat{s}, rho, z) instead of dk, removing lines 1–4. Pass \hat{s} into K-PKE.Decrypt instead of dk_PKE. Pass \hat{A} and \hat{t} into K-PKE.Encrypt instead of ek_PKE.
7. Change ML-KEM.Encaps to include the lines 2–8 removed from K-PKE.Encrypt before the call to K-PKE.Encrypt, to which \hat{A} and \hat{t} are passed instead of ek.