Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7383234
Add ML-KEM EnvelopedCms API surface
vcsjones Aug 28, 2026
87e2c29
Add managed KEM recipient decoding
vcsjones Aug 28, 2026
0fe6aab
Implement ML-KEM decryption
vcsjones Aug 31, 2026
4d76327
Add more test documents
vcsjones Aug 31, 2026
a99a998
Expand ML-KEM decryption tests
vcsjones Aug 31, 2026
4db7523
Support certificate-backed ML-KEM decryption
vcsjones Aug 31, 2026
fa99155
Implement managed ML-KEM encryption
vcsjones Aug 31, 2026
4a3cadd
Harden ML-KEM EnvelopedCms handling
vcsjones Sep 1, 2026
3a6ef34
Refactor managed KEM decryption dispatch
vcsjones Sep 1, 2026
72f2365
Expand ML-KEM EnvelopedCms coverage
vcsjones Sep 1, 2026
87df405
Expand ML-KEM EnvelopedCms edge-case coverage
vcsjones Sep 1, 2026
aed5ccd
Add platform guard
vcsjones Sep 1, 2026
b15f9cb
Refine KEM helpers and decode coverage
vcsjones Sep 1, 2026
0b2926a
Test custom and unsupported ML-KEM implementations
vcsjones Sep 1, 2026
f88b21c
Merge remote-tracking branch 'ms/main' into ml-kem-enveloped-cms-managed
vcsjones Sep 14, 2026
df48d79
First code review feedback round
vcsjones Sep 14, 2026
7e78b81
Change union to be RSA
vcsjones Sep 14, 2026
560c9fb
Rename files
vcsjones Sep 14, 2026
4251ce0
More code review feedback
vcsjones Sep 14, 2026
36f9010
Use CoreLib's source directly
vcsjones Sep 14, 2026
6db3738
More code review feedback
vcsjones Sep 15, 2026
c72fa9e
Shim CryptographicOperations
vcsjones Sep 15, 2026
7f59dc4
Test RSA can decrypt with a KEM recipient
vcsjones Sep 15, 2026
2e725d1
Assert version
vcsjones Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/libraries/Common/src/System/Security/Cryptography/Oids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,23 @@ internal static partial class Oids
internal const string MsPkcs12MachineKeySet = "1.3.6.1.4.1.311.17.2";

// Key wrap algorithms
internal const string Aes128Wrap = "2.16.840.1.101.3.4.1.5";
internal const string Aes192Wrap = "2.16.840.1.101.3.4.1.25";
internal const string Aes256Wrap = "2.16.840.1.101.3.4.1.45";
internal const string CmsRc2Wrap = "1.2.840.113549.1.9.16.3.7";
internal const string Cms3DesWrap = "1.2.840.113549.1.9.16.3.6";

// Key derivation algorithms
internal const string HkdfWithSha256 = "1.2.840.113549.1.9.16.3.28";
internal const string HkdfWithSha384 = "1.2.840.113549.1.9.16.3.29";
internal const string HkdfWithSha512 = "1.2.840.113549.1.9.16.3.30";
internal const string HkdfWithSha3_256 = "1.2.840.113549.1.9.16.3.33";
internal const string HkdfWithSha3_384 = "1.2.840.113549.1.9.16.3.34";
internal const string HkdfWithSha3_512 = "1.2.840.113549.1.9.16.3.35";

// OtherRecipientInfo types
internal const string IdSmimeOriKem = "1.2.840.113549.1.9.16.13.3";

// PKCS7 Content Types.
internal const string Pkcs7Data = "1.2.840.113549.1.7.1";
internal const string Pkcs7Signed = "1.2.840.113549.1.7.2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ namespace System.Runtime.CompilerServices
/// </para>
/// </remarks>
/// <seealso cref="UnionAttribute" />
public interface IUnion
#if SYSTEM_PRIVATE_CORELIB
public
#else
internal
#endif
interface IUnion
{
/// <summary>
/// Gets the value contained in the union, or <see langword="null" /> if the union has no value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ namespace System.Runtime.CompilerServices
/// </remarks>
/// <seealso cref="IUnion" />
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class UnionAttribute : Attribute
#if SYSTEM_PRIVATE_CORELIB
public
#else
internal
#endif
sealed class UnionAttribute : Attribute
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public CmsRecipient(System.Security.Cryptography.Pkcs.SubjectIdentifierType reci
public CmsRecipient(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { }
public System.Security.Cryptography.X509Certificates.X509Certificate2 Certificate { get { throw null; } }
public System.Security.Cryptography.Pkcs.SubjectIdentifierType RecipientIdentifierType { get { throw null; } }
#if NET11_0_OR_GREATER
public static System.Security.Cryptography.Pkcs.CmsRecipient CreateForKeyEncapsulation(System.Security.Cryptography.Pkcs.SubjectIdentifierType recipientIdentifierType, System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.ReadOnlySpan<byte> userKeyingMaterial) { throw null; }
public static System.Security.Cryptography.Pkcs.CmsRecipient CreateForKeyEncapsulation(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, System.ReadOnlySpan<byte> userKeyingMaterial) { throw null; }
#endif
}
public sealed partial class CmsRecipientCollection : System.Collections.ICollection, System.Collections.IEnumerable
{
Expand Down Expand Up @@ -120,6 +124,11 @@ public EnvelopedCms(System.Security.Cryptography.Pkcs.ContentInfo contentInfo, S
public int Version { get { throw null; } }
public void Decode(byte[] encodedMessage) { }
public void Decrypt() { }
#if NET11_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public void Decrypt(System.Security.Cryptography.Pkcs.KemRecipientInfo recipientInfo, System.Security.Cryptography.CompositeMLKem privateKey) { }
public void Decrypt(System.Security.Cryptography.Pkcs.KemRecipientInfo recipientInfo, System.Security.Cryptography.MLKem privateKey) { }
#endif
public void Decrypt(System.Security.Cryptography.Pkcs.RecipientInfo recipientInfo) { }
public void Decrypt(System.Security.Cryptography.Pkcs.RecipientInfo recipientInfo, System.Security.Cryptography.X509Certificates.X509Certificate2Collection extraStore) { }
public void Decrypt(System.Security.Cryptography.X509Certificates.X509Certificate2Collection extraStore) { }
Expand All @@ -138,6 +147,21 @@ internal KeyAgreeRecipientInfo() { }
public override System.Security.Cryptography.Pkcs.SubjectIdentifier RecipientIdentifier { get { throw null; } }
public override int Version { get { throw null; } }
}
#if NET11_0_OR_GREATER
public sealed partial class KemRecipientInfo : System.Security.Cryptography.Pkcs.RecipientInfo
{
internal KemRecipientInfo() { }
public override byte[] EncryptedKey { get { throw null; } }
public System.Security.Cryptography.Pkcs.AlgorithmIdentifier KeyEncapsulationAlgorithm { get { throw null; } }
public System.ReadOnlyMemory<byte> KeyEncapsulationCiphertext { get { throw null; } }
public System.Security.Cryptography.Pkcs.AlgorithmIdentifier KeyDerivationAlgorithm { get { throw null; } }
public override System.Security.Cryptography.Pkcs.AlgorithmIdentifier KeyEncryptionAlgorithm { get { throw null; } }
public int KeyEncryptionKeyLengthInBytes { get { throw null; } }
public override System.Security.Cryptography.Pkcs.SubjectIdentifier RecipientIdentifier { get { throw null; } }
public System.ReadOnlyMemory<byte>? UserKeyingMaterial { get { throw null; } }
public override int Version { get { throw null; } }
}
#endif
public sealed partial class KeyTransRecipientInfo : System.Security.Cryptography.Pkcs.RecipientInfo
{
internal KeyTransRecipientInfo() { }
Expand Down Expand Up @@ -231,6 +255,9 @@ public enum RecipientInfoType
Unknown = 0,
KeyTransport = 1,
KeyAgreement = 2,
#if NET11_0_OR_GREATER
KeyEncapsulation = 3,
#endif
}
public sealed partial class SignedCms
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal DecryptorPal(RecipientInfoCollection recipientInfos)
public abstract ContentInfo? TryDecrypt(
RecipientInfo recipientInfo,
X509Certificate2? cert,
AsymmetricAlgorithm? privateKey,
EnvelopedCmsKey privateKey,
X509Certificate2Collection originatorCerts,
X509Certificate2Collection extraStore,
out Exception? exception);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography;

namespace Internal.Cryptography
{
internal union EnvelopedCmsKey(
RSA,
#if NET11_0_OR_GREATER
MLKem,
CompositeMLKem,
#endif
EnvelopedCmsKey.None)
{
internal sealed record None
{
internal static None Instance { get; } = new None();

private None()
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Security.Cryptography.Pkcs;

namespace Internal.Cryptography
{
internal abstract class KemRecipientInfoPal : RecipientInfoPal
{
internal abstract AlgorithmIdentifier KeyDerivationAlgorithm { get; }
internal abstract AlgorithmIdentifier KeyEncapsulationAlgorithm { get; }
internal abstract ReadOnlyMemory<byte> KeyEncapsulationCiphertext { get; }
internal abstract int KeyEncryptionKeyLengthInBytes { get; }
internal abstract ReadOnlyMemory<byte>? UserKeyingMaterial { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ public override DecryptorPal Decode(
new KeyAgreeRecipientInfo(new ManagedKeyAgreePal(recipientInfo.Kari.Value, i)));
}
}
else if (recipientInfo.Ori.HasValue)
{
#if NET11_0_OR_GREATER
if (recipientInfo.Ori.Value.OriType == Oids.IdSmimeOriKem)
{
KemRecipientInfoAsn kemRecipientInfo = KemRecipientInfoAsn.Decode(
recipientInfo.Ori.Value.OriValue,
AsnEncodingRules.BER);

recipientInfos.Add(new KemRecipientInfo(new ManagedKemRecipientInfoPal(kemRecipientInfo)));
continue;
}
#endif

throw new CryptographicException();
}
else
{
Debug.Fail($"{nameof(RecipientInfoAsn)} deserialized with an unknown recipient type");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,53 +32,56 @@ public ManagedDecryptorPal(
public override unsafe ContentInfo? TryDecrypt(
RecipientInfo recipientInfo,
X509Certificate2? cert,
AsymmetricAlgorithm? privateKey,
EnvelopedCmsKey privateKey,
X509Certificate2Collection originatorCerts,
X509Certificate2Collection extraStore,
out Exception? exception)
{
// When encryptedContent is null Windows seems to decrypt the CEK first,
// then return a 0 byte answer.

Debug.Assert((cert != null) ^ (privateKey != null));
Debug.Assert((cert is not null) ^ (privateKey is not EnvelopedCmsKey.None));

byte[]? cek;

if (recipientInfo.Pal is ManagedKeyTransPal ktri)
{
RSA? key = privateKey as RSA;
RSA? key = privateKey is RSA rsa ? rsa : null;

if (privateKey != null && key == null)
if (privateKey is not EnvelopedCmsKey.None && key is null)
{
exception = new CryptographicException(SR.Cryptography_Cms_Ktri_RSARequired);
return null;
}

byte[]? cek = ktri.DecryptCek(cert, key, out exception);
// Pin CEK to prevent it from getting copied during heap compaction.
fixed (byte* pinnedCek = cek)
cek = ktri.DecryptCek(cert, key, out exception);
}
#if NET11_0_OR_GREATER
else if (recipientInfo.Pal is ManagedKemRecipientInfoPal kemRecipientInfo)
{
if (privateKey is CompositeMLKem compositeMLKem)
{
try
{
if (exception != null)
{
return null;
}

return TryDecryptCore(
cek!,
_envelopedData.EncryptedContentInfo.ContentType,
_envelopedData.EncryptedContentInfo.EncryptedContent,
_envelopedData.EncryptedContentInfo.ContentEncryptionAlgorithm,
out exception);
}
finally
{
if (cek != null)
{
Array.Clear(cek, 0, cek.Length);
}
}
cek = kemRecipientInfo.DecryptCek(compositeMLKem, out exception);
}
else if (privateKey is MLKem mlKem)
{
cek = kemRecipientInfo.DecryptCek(mlKem, out exception);
}
else if (privateKey is EnvelopedCmsKey.None)
{
Debug.Assert(cert is not null);
cek = kemRecipientInfo.DecryptCek(cert, out exception);
}
else
{
exception = new CryptographicException(
SR.Cryptography_Cms_RecipientType_NotSupported,
recipientInfo.Type.ToString());

return null;
}
}
#endif
else
{
exception = new CryptographicException(
Expand All @@ -87,6 +90,32 @@ public ManagedDecryptorPal(

return null;
}

// Pin CEK to prevent it from getting copied during heap compaction.
fixed (byte* pinnedCek = cek)
Comment thread
vcsjones marked this conversation as resolved.
{
try
{
if (exception is not null)
{
return null;
}

return TryDecryptCore(
cek!,
_envelopedData.EncryptedContentInfo.ContentType,
_envelopedData.EncryptedContentInfo.EncryptedContent,
_envelopedData.EncryptedContentInfo.ContentEncryptionAlgorithm,
out exception);
}
finally
{
if (cek is not null)
{
CryptographicOperations.ZeroMemory(cek);
}
}
}
}

public static ContentInfo? TryDecryptCore(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public override unsafe byte[] Encrypt(
}
}

private byte[] Encrypt(
private static byte[] Encrypt(
CmsRecipientCollection recipients,
ContentInfo contentInfo,
AlgorithmIdentifier contentEncryptionAlgorithm,
Expand Down Expand Up @@ -102,12 +102,22 @@ private byte[] Encrypt(
envelopedData.RecipientInfos = new RecipientInfoAsn[recipients.Count];

bool allRecipientsVersion0 = true;
bool hasOtherRecipientInfo = false;

for (var i = 0; i < recipients.Count; i++)
{
CmsRecipient recipient = recipients[i];
bool v0Recipient;

#if NET11_0_OR_GREATER
if (PkcsHelpers.IsKeyEncapsulationAlgorithm(recipient.Certificate.GetKeyAlgorithm()))
{
envelopedData.RecipientInfos[i] = MakeKemRecipientInfo(cek, recipient);
hasOtherRecipientInfo = true;
continue;
}
#endif

envelopedData.RecipientInfos[i].Ktri = recipient.Certificate.GetKeyAlgorithm() switch
{
Oids.Rsa => MakeKtri(cek, recipient, out v0Recipient),
Expand All @@ -126,7 +136,7 @@ private byte[] Encrypt(
// v3 (RFC 3369):
// * OriginatorInfo contains v2 attribute certificates (not supported)
// * Any PWRI (password) recipients are present (not supported)
// * Any ORI (other) recipients are present (not supported)
// * Any ORI (other) recipients are present
// v2 (RFC 2630):
// * OriginatorInfo is present
// * Any RecipientInfo has a non-zero version number
Expand All @@ -135,7 +145,11 @@ private byte[] Encrypt(
// v0 (RFC 2315):
// * Anything not already matched

if (envelopedData.OriginatorInfo != null ||
if (hasOtherRecipientInfo)
{
envelopedData.Version = 3;
}
else if (envelopedData.OriginatorInfo != null ||
!allRecipientsVersion0 ||
envelopedData.UnprotectedAttributes != null)
{
Expand Down
Loading
Loading