Repository navigation
[API Proposal] Composite ML-KEM #129633
Description
Activity
- addedapi-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementation
on Jun 19, 2026 dotnet-policy-service commented
on Jun 19, 2026 ContributorMore actionsTagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security
See info in area-owners.md if you want to be subscribed.public byte[] ExportDecapsulationKey();
I'm of two minds here. For ML-KEM that's what the spec called the private key, e.g.
Algorithm 19 ML-KEM.KeyGen()
Generates an encapsulation key and a corresponding decapsulation key.
Output: encapsulation key ek ∈ 𝔹384𝑘+32 .
Output: decapsulation key dk ∈ 𝔹768𝑘+96 .For Composite ML-KEM they're just called "public key" and "private key" or "composite public" and (by extension) "composite private"
Key Generation Process:
- Generate component keys
mlkemSeed = Random(64)
(mlkemPK, mlkemSK) = ML-KEM.KeyGen_internal(
mlkemSeed[:32],
mlkemSeed[32:] )
(tradPK, tradSK) = Trad.KeyGen()-
Check for component key gen failure
if NOT (mlkemPK, mlkemSK) or NOT (tradPK, tradSK):
output "Key generation error" -
Output the composite public and private keys
pk = SerializePublicKey(mlkemPK, tradPK)
sk = SerializePrivateKey(mlkemSeed, tradSK)
return (pk, sk)Likening it to Composite ML-DSA, that might mean the spec-correctest answer is
ExportCompositeMLKemPublicKeyandExportCompositeMLKemPrivateKey.On the other hand, encapsulation and decapsulation do describe their role, and a sort of... primitiveness.
public static bool IsSupported { get; }andpublic static bool IsAlgorithmSupported(CompositeMLKemAlgorithm algorithm);For HPKE the current thinking is to get rid of the big "IsSupported" and only have the one with HpkeSuite... but I think that's because so long as we provide a managed fallback on environments that the OS can't offload for us DHKEM_P256_HKDF_SHA256_AES_128_GCM will always work, and thus IsSupported is just "!browser" (okay, so not "always", but "always").
Here it's less universal, since we need MLKem.IsSupported to be true, which isn't guaranteed. I could go either way.
[Experimental("SYSLIB5006")]
I was expecting we'd close 5006 this year, but since we're still at one provider for SLH-DSA, at least that one stays Experimental... and HashML-DSA is A Thing. So, yeah, 5006 is probably reasonable for this. Since it's currently still at draft stage we should expect Experimental.
MaxEncapsulationKeySizeInBytes
The RSA keys still mess this up. If the spec were authoritative and said only e=65537 was allowed, then sure. I don't want to add API that calls something "Max" and then it be possible to violate that max, and also don't want to make keys not exportable because they violate the max.
I'm of two minds here. For ML-KEM that's what the spec called the private key, e.g.
I think that is largely because this is being driven by IETF and they are sticking with their terms.
Private and Public are widely just considered bad terms these days - that's precisely why FIPS 203 avoided it.
In order to reduce confusion, this standard will not use the terms “public key” or “private key.” Instead, keys will be referred to only using the more specific terms, i.e., one of “encapsulation key”, “decapsulation key”, “encryption key”, “decryption key”, and “shared secret key”.
It's still a KEM, and I think NIST and the broader cryptography community have good reasons for avoiding public and private key as terminology. The SPKI and PKCS#8 are unfortunate, but those terms have existed for many years.
PranavSenthilnathan commented
on Jun 19, 2026 MemberAuthorMore actionsRemoved
MaxEncapsulationKeySizeInBytesand changed the core method toExportEncapsulationKeyCoreinstead ofTryExportEncapsulationKeyCore- likeCompositeMLDsait would give the implementor a sufficiently large buffer. IIRC we wanted to avoid cases where incorrect implementations wrote off the end of an insufficiently large buffer by mistake.Removing the property makes the
public int ExportDecapsulationKey(Span<byte> destination);overload harder to use since the user won't know the buffer size to provide. We can consider removing that overload since theTryExportDecapsulationKeydoes a similar operation although it's not as compositional (e.g. can't dospan.Slice(0, ExportDecapsulationKey(span))).Removed
IsSupportedbased on the managed implementation argument.I'd prefer the encapsulation/decapsulation terminology to be internally consistent with ML-KEM, but we can decide this in the API design review meeting.
namespace System.Security.Cryptography.X509Certificates
I don't know how much sense it makes to put any of this on
PublicKeyorX509Certificate2. Remember that hybrids / composites are here mostly to support a transitionary time. Ideally, composites wouldn't even exist. The only reason they do is to hedge against improved cryptanalysis in PQC1.So let's play out some scenarios
- ML-KEM is sound. Everyone is happy with it, so there is no point in using a Composite when the cryptographic community feels it's no longer needed. Use ML-KEM directly - it will cut down on complexity significantly.
- ML-KEM is not sound. Everyone realizes that ML-KEM shouldn't be used. In that case, using the Composites doesn't make sense either and everyone moves on to the next other composite, thing, or whatever.
Either way - the point is that Composites are not meant to exist for a very long time (indeed, CNSA 2.0 even said don't bother with Composites)
Combined with the fact that Composite ML-KEM is squints keyAgreement in X.509 sense, and using X.509 with keyAgreement is already a weird scenario - I don't think it will get much use, either.
All that to say: my sense is that we want to try and minimize the amount of public API surface for something that is - by design - not supposed to exist long.
Especially in the case of
PublicKey- it supports arbitrary SubjectPublicKeyInfo's. The APIs should really only exist for things that are expected to really be core, common, use cases. For example, one could just doPublicKey key = PublicKey.CreateFromSubjectPublicKeyInfo(compositeKem.ExportSubjectPublicKeyInfo());
1There are technically other reasons people like Composites, but that is the primary one
PranavSenthilnathan commented
on Jun 20, 2026 MemberAuthorMore actionsMy understanding was that the main use case of the
MLKemclass would be for certificates in things like Enveloped CMS. AndCompositeMLKemwould be an alternative to it (even if hybrids have questionable utility). If we don't support certificates, what would be the use case forCompositeMLKem? Or are you suggesting keeping all of that an internal implementation instead of making it public?My understanding was that the main use case of the MLKem class would be for certificates in things like Enveloped CMS.
Within .NET itself, possibly. A much more common use of ML-KEM, the algorithm, is in protocols that do ephemeral key agreement like SSH or TLS. It can be used for EnvelopedCms.
If we don't support certificates, what would be the use case for CompositeMLKem?
Enveloped CMS, as a specification, does not require X.509 certificates. We just designed it in such a way that it did. We haven't added any support for KEMs in EnvelopedCms. When we get around to adding KEMRecipientInfo there is no reason for us to require the KEM keys to be shrouded in a certificate.
Or are you suggesting keeping all of that an internal implementation instead of making it public?
I am not exactly sure what I am suggesting, unfortunately. I am just pointing out that this is a lot of new API surface that is by design living on borrowed time. Maybe all of it is necessary now.
I guess we can keep the certificate stuff. I just want to be very mindful that composites don't need to have as much API surface as possible.
Seems like conversation has run out, so marking api-ready-for-review. (Feel free to continue discussing)
- addedapi-ready-for-reviewAPI is ready for review, it is NOT ready for implementationAPI is ready for review, it is NOT ready for implementationand removedapi-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementation
on Jun 29, 2026 (Disclaimer: I am not officially stating a Microsoft policy here, just my understanding of it. Anything officially written replaces anything I say here.)
FWIW, my current understanding/projection of the Microsoft viewpoint (which may, very well, be unique), is that even in the face of a CRQC, composites are "better" than pure PQC until closer to 2050 than 2030. Mainly it comes down to "a CRQC existing doesn't mean that using it is fast or free, so buttressing new algorithms with a pre-Q algorithm just gives an extra element of defense". When the new algorithms are old enough that implementation gotchas have had a chance to be found out and fixed, and no one has found a fundamental flaw in them, then the addition of the traditional component become an "unnecessary" extra element of defense... or when the time and money for running a break becomes negligible enough...
At the very least, I took to the MS Crypto Board the idea of giving composites a sort of second-class treatment (e.g. moving them to a different assembly, reducing integration touchpoints)... and of everyone who spoke, I got a unanimous plea to treat them first-class. At least the current ML-DSA and ML-KEM ones.
Just to formalize some of my thoughts here
plea to treat them first-class
There are of course always going to be differing opinions about what someone thinks is important. I don't think I mean treat it as second-class, but every new API we introduce incurs hundreds of lines of code, tests, and documentation that will live forever.
We offer first-class APIs for developers. We offer public APIs per-algorithm because it gives a much better developer experience than, say,
EVP_DigestSignand hoping you passed the right strings and parameters to get the algorithm you want. That however means additional Public API surface.Putting aside the CRQC relevance questions in terms of "obsolete" or not:
public PublicKey(CompositeMLKem key);
[Experimental("SYSLIB5006")] [UnsupportedOSPlatform("browser")] public CompositeMLKem? GetCompositeMLKemPublicKey();Personally I think we should just stop adding things to
PublicKey. We havePublicKey.CreateFromSubjectPublicKeyInfoandPublicKey.ExportSubjectPublicKeyInfoto support arbitrary public keys. All those "first class" APIs do is defer to those APIs, anyway:Line 72 in 0a8142e
public PublicKey(MLKem key) : this(key.ExportSubjectPublicKeyInfo()) So anyone can do
PublicKey.CreateFromSubjectPublicKeyInfo(mlKemCompositeInstance.ExportSubjectPublicKeyInfo()).If I had a time machine I would go back and recommend we don't add it for the other PQC types, but they are there now. I don't like using "but we already did it" to justify keep doing things we shouldn't do.
GetCompositeMLKemPublicKeyGetCompositeMLKemPrivateKeyCopyWithPrivateKey
I'm torn on these because I generally don't expect these APIs to get any reasonable amount of traction. That said if you do actually need these APIs, you can't easily work around not having them except for
GetCompositeMLKemPublicKey.The most obvious use case for it is
EnvelopedCmsbut I would counter-
I think a lot of
EnvelopedCmsuse is because it is simply the only tool available in-the-box that does "asymmetric encryption". That will change with HPKE, and I expect HPKE will be used more commonly in that regard. -
EnvelopedCmsis a type-forward in .NET Framework configurations of S.S.C.Pkcs. So we can't add APIs to itLine 19 in 0a8142e
[assembly: TypeForwardedTo(typeof(System.Security.Cryptography.Pkcs.EnvelopedCms))] Even if we decide we want these APIs, I don't know that we should have
X509CertificateKeyAccessorsOOB them. They will only be present for .NET Framework, and, sinceEnvelopedCmscan't add Public API surface (unless we have extension methods do CryptoConfig or some stuff) I'm not really sure who the OOB APIs are for.
2 remaining items
- Looks good as proposed.
- Leaving out the PublicKey members is fine, as well as the other certificate-attachment API (if there's not currently use for them)
namespace System.Security.Cryptography { // System.Security.Cryptography and Microsoft.Bcl.Cryptography [Experimental("SYSLIB5006")] public abstract class CompositeMLKem : IDisposable { protected CompositeMLKem(CompositeMLKemAlgorithm algorithm); public static bool IsAlgorithmSupported(CompositeMLKemAlgorithm algorithm); public CompositeMLKemAlgorithm Algorithm { get; } public byte[] Decapsulate(byte[] ciphertext); public void Decapsulate(ReadOnlySpan<byte> ciphertext, Span<byte> sharedSecret); protected abstract void DecapsulateCore(ReadOnlySpan<byte> ciphertext, Span<byte> sharedSecret); public void Encapsulate(out byte[] ciphertext, out byte[] sharedSecret); public void Encapsulate(Span<byte> ciphertext, Span<byte> sharedSecret); protected abstract void EncapsulateCore(Span<byte> ciphertext, Span<byte> sharedSecret); public byte[] ExportDecapsulationKey(); public int ExportDecapsulationKey(Span<byte> destination); public bool TryExportDecapsulationKey(Span<byte> destination, out int bytesWritten); protected abstract int ExportDecapsulationKeyCore(Span<byte> destination); public byte[] ExportEncapsulationKey(); public int ExportEncapsulationKey(Span<byte> destination); public bool TryExportEncapsulationKey(Span<byte> destination, out int bytesWritten); protected abstract int ExportEncapsulationKeyCore(Span<byte> destination); public static CompositeMLKem GenerateKey(CompositeMLKemAlgorithm algorithm); public static CompositeMLKem ImportEncapsulationKey(CompositeMLKemAlgorithm algorithm, ReadOnlySpan<byte> source); public static CompositeMLKem ImportEncapsulationKey(CompositeMLKemAlgorithm algorithm, byte[] source); public static CompositeMLKem ImportDecapsulationKey(CompositeMLKemAlgorithm algorithm, ReadOnlySpan<byte> source); public static CompositeMLKem ImportDecapsulationKey(CompositeMLKemAlgorithm algorithm, byte[] source); public void Dispose(); protected virtual void Dispose(bool disposing); // PKCS#8 / SPKI / PEM import/export public bool TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten); public bool TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten); public bool TryExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters, Span<byte> destination, out int bytesWritten); public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters); public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, PbeParameters pbeParameters); public byte[] ExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters); public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<byte> passwordBytes, PbeParameters pbeParameters); public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<char> password, PbeParameters pbeParameters); public string ExportEncryptedPkcs8PrivateKeyPem(string password, PbeParameters pbeParameters); public byte[] ExportPkcs8PrivateKey(); public string ExportPkcs8PrivateKeyPem(); public bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten); protected abstract bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten); public byte[] ExportSubjectPublicKeyInfo(); public string ExportSubjectPublicKeyInfoPem(); public bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten); public static CompositeMLKem ImportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte> passwordBytes, ReadOnlySpan<byte> source); public static CompositeMLKem ImportEncryptedPkcs8PrivateKey(ReadOnlySpan<char> password, ReadOnlySpan<byte> source); public static CompositeMLKem ImportEncryptedPkcs8PrivateKey(string password, byte[] source); public static CompositeMLKem ImportFromEncryptedPem(ReadOnlySpan<char> source, ReadOnlySpan<byte> passwordBytes); public static CompositeMLKem ImportFromEncryptedPem(ReadOnlySpan<char> source, ReadOnlySpan<char> password); public static CompositeMLKem ImportFromEncryptedPem(string source, byte[] passwordBytes); public static CompositeMLKem ImportFromEncryptedPem(string source, string password); public static CompositeMLKem ImportFromPem(ReadOnlySpan<char> source); public static CompositeMLKem ImportFromPem(string source); public static CompositeMLKem ImportPkcs8PrivateKey(byte[] source); public static CompositeMLKem ImportPkcs8PrivateKey(ReadOnlySpan<byte> source); public static CompositeMLKem ImportSubjectPublicKeyInfo(byte[] source); public static CompositeMLKem ImportSubjectPublicKeyInfo(ReadOnlySpan<byte> source); } // System.Security.Cryptography and Microsoft.Bcl.Cryptography [Experimental("SYSLIB5006")] public sealed class CompositeMLKemAlgorithm : IEquatable<CompositeMLKemAlgorithm> { internal CompositeMLKemAlgorithm(); public static CompositeMLKemAlgorithm MLKem768WithRsaOaep2048 { get; } public static CompositeMLKemAlgorithm MLKem768WithRsaOaep3072 { get; } public static CompositeMLKemAlgorithm MLKem768WithRsaOaep4096 { get; } public static CompositeMLKemAlgorithm MLKem768WithX25519 { get; } public static CompositeMLKemAlgorithm MLKem768WithECDiffieHellmanP256 { get; } public static CompositeMLKemAlgorithm MLKem768WithECDiffieHellmanP384 { get; } public static CompositeMLKemAlgorithm MLKem768WithECDiffieHellmanBrainpoolP256r1 { get; } public static CompositeMLKemAlgorithm MLKem1024WithRsaOaep3072 { get; } public static CompositeMLKemAlgorithm MLKem1024WithECDiffieHellmanP384 { get; } public static CompositeMLKemAlgorithm MLKem1024WithECDiffieHellmanBrainpoolP384r1 { get; } public static CompositeMLKemAlgorithm MLKem1024WithX448 { get; } public static CompositeMLKemAlgorithm MLKem1024WithECDiffieHellmanP521 { get; } public string Name { get; } public int CiphertextSizeInBytes { get; } public int SharedSecretSizeInBytes { get; } public bool Equals([NotNullWhen(true)] CompositeMLKemAlgorithm? other); public override bool Equals([NotNullWhen(true)] object? obj); public override int GetHashCode(); public override string ToString(); public static bool operator ==(CompositeMLKemAlgorithm? left, CompositeMLKemAlgorithm? right); public static bool operator !=(CompositeMLKemAlgorithm? left, CompositeMLKemAlgorithm? right); } // System.Security.Cryptography and Microsoft.Bcl.Cryptography [Experimental("SYSLIB5006")] [SupportedOSPlatform("windows")] public sealed partial class CompositeMLKemCng : CompositeMLKem { public CompositeMLKemCng(CngKey key); public CngKey GetKey(); } } namespace System.Security.Cryptography.X509Certificates { // System.Security.Cryptography only public sealed partial class PublicKey { [Experimental("SYSLIB5006")] public PublicKey(CompositeMLKem key); [Experimental("SYSLIB5006")] [UnsupportedOSPlatform("browser")] public CompositeMLKem? GetCompositeMLKemPublicKey(); } // System.Security.Cryptography only public partial class X509Certificate2 { [Experimental("SYSLIB5006")] public CompositeMLKem? GetCompositeMLKemPublicKey(); [Experimental("SYSLIB5006")] public CompositeMLKem? GetCompositeMLKemPrivateKey(); [Experimental("SYSLIB5006")] public X509Certificate2 CopyWithPrivateKey(CompositeMLKem privateKey); } // Microsoft.Bcl.Cryptography only public static partial class X509CertificateKeyAccessors { [Experimental("SYSLIB5006")] public static CompositeMLKem? GetCompositeMLKemPublicKey(this X509Certificate2 certificate); [Experimental("SYSLIB5006")] public static CompositeMLKem? GetCompositeMLKemPrivateKey(this X509Certificate2 certificate); [Experimental("SYSLIB5006")] public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, CompositeMLKem privateKey); } }
- addedapi-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedand removedapi-ready-for-reviewAPI is ready for review, it is NOT ready for implementationAPI is ready for review, it is NOT ready for implementation
on Aug 11, 2026 - added a commit that references this issue
on Aug 19, 2026 PranavSenthilnathan commented
on Aug 24, 2026 MemberAuthorMore actionsWe should move
[SupportedOSPlatform("windows")]fromCompositeMLKemCngto its constructor, matching the other CNG algorithm types.[Experimental("SYSLIB5006")] -[SupportedOSPlatform("windows")] public sealed partial class CompositeMLKemCng : CompositeMLKem { + [SupportedOSPlatform("windows")] public CompositeMLKemCng(CngKey key); }@bartonjs I don't think we need a separate review for this update
Reacted by Jeremy Barton- added a commit that references this issue
on Aug 28, 2026 - added a commit that references this issue
on Sep 10, 2026 - added a commit that references this issue
on Sep 20, 2026
Background and motivation
Composite ML-KEM is a Post-Quantum Cryptography key encapsulation mechanism that combines ML-KEM with a traditional key agreement algorithm (ECDH or X25519/X448). The latest draft specification is: https://datatracker.ietf.org/doc/html/draft-ietf-lamps-pq-composite-kem
This follows the same composite design philosophy as Composite ML-DSA (#118320), providing hybrid post-quantum security for key establishment. Both component algorithms must succeed for the combined operation to succeed, ensuring security even if one algorithm is compromised.
Related: #114453 (ML-KEM, api-approved), #118320 (Composite ML-DSA, api-approved)
API Proposal
The API follows the pattern established by
CompositeMLDsa(#118320) for the composite type, andMLKem(#114453) for the KEM operations (encapsulate/decapsulate).Open questions
Key size properties: MLKem exposes exact key sizes as properties (e.g.
EncapsulationKeySizeInBytes). CompositeMLDsa has variable, bounded key sizes but doesn't expose these values (yet). The reasoning was that we can add it later if needed. We should decide which approach to take for Composite ML-KEM. The asterisked values below are max sizes instead of exact sizes:CNG classes: This assumes Windows will support Composite ML-KEM in NCrypt. Composite ML-KEM support in BCrypt has already shipped in a preview, so it seems likely.