Encrypt CMS messages
You encrypt data for a recipient whose certificate carries an ML-KEM key, for example for an archive whose content must stay confidential even against future quantum computers.
Solution overview
| Element | Purpose |
|---|---|
PostQuantumConfig.Exemptions | with PurePostQuantum, allows pure ML-KEM recipients and the ML-DSA issuer |
MLKemKey.Generate | generates an ML-KEM key pair (ML-KEM-768 or ML-KEM-1024; ML-KEM-512 only with ReducedParameterSets) |
X509CertificateBuilder with an ML-KEM key | issues the recipient certificate |
AttachPrivateMLKemKey | associates the recipient certificate with its private key |
KemEnvelopedCms.Encrypt, Encode | encrypts the content for one or more recipients |
KemEnvelopedCms.Decode, Decrypt | decrypts at the recipient |
KemEnvelopedCms is modeled on the .NET EnvelopedCms class.
Implementation
BSI TR-02102-1 recommends ML-KEM only in combination with a classical algorithm, using the CatKDF or
KeyCombine combiners. A KEMRecipientInfo as defined in RFC 9629 uses ML-KEM on its own, and composite
ML-KEM combines with SHA3-256. Both kinds of recipients therefore require an exemption, which you set once
when your app starts. This example uses PurePostQuantum for the ML-KEM recipient and for the pure ML-DSA
certification authority:
PostQuantumConfig.Exemptions |= PostQuantumExemptions.PurePostQuantum;
Create the recipient certificate. An ML-KEM key can't sign, so an ML-DSA issuer issues the certificate. In this example, that's a CA of your own:
var now = DateTimeOffset.UtcNow; using var caKey = MLDsaKey.Generate(MLDsaParameterSet.MLDsa65); var caRequest = new MLDsaX509SignatureGenerator(caKey).CreateCertificateRequest("CN=Contoso PQ CA"); caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); caRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign, true)); using var ca = caRequest.CreateSelfSigned(now, now.AddYears(10), CryptoMode.PostQuantum); using var archiveKey = MLKemKey.Generate(MLKemParameterSet.MLKem768); var archiveBuilder = new X509CertificateBuilder( new X500DistinguishedName("CN=Contoso Archive"), archiveKey.ExportSubjectPublicKeyInfo(), default); archiveBuilder.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, true)); using var archive = archiveBuilder.Create( ca, now, now.AddYears(1), RandomNumberGenerator.GetBytes(16), CryptoMode.PostQuantum); archive.AttachPrivateMLKemKey(archiveKey);
Encrypt and decrypt:
var outgoing = new KemEnvelopedCms(new ContentInfo("Contract 2026-17"u8.ToArray())); outgoing.Encrypt(archive); byte[] envelope = outgoing.Encode(); var incoming = new KemEnvelopedCms(); incoming.Decode(envelope); incoming.Decrypt(archive); Console.WriteLine(System.Text.Encoding.UTF8.GetString(incoming.ContentInfo.Content));
Remarks
- Readable only with
KemEnvelopedCms. The .NETEnvelopedCmsclass can't decode messages that contain a KEMRecipientInfo, not even for an RSA recipient of the same message. - Hybrid through composite ML-KEM. If the recipient certificate carries a composite ML-KEM key, the content stays confidential as long as either part holds. In that case, associate the private key with
AttachPrivateCompositeMLKemKey. Encrypting for such a recipient requiresPostQuantumExemptions.NonRecommendedCombiners. Decrypting existing messages doesn't require an exemption and raises thePolicyDeviationevent. - Hybrid without CMS. For your own protocol without CMS,
HybridKeyAgreementagrees on a key from ECDH and ML-KEM with KeyCombine, without any exemption; see Agree on a hybrid key. - RSA recipients in the same message. A recipient with an RSA certificate gets a KeyTransRecipientInfo with RSAES-OAEP. That recipient also opens the message with
KemEnvelopedCms. - The private key must be in the process.
Decryptneeds the recipient's private ML-KEM key. If it's missing, the call fails with aCryptographicException.