Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d808604
Add ML-DSA (FIPS 204) post-quantum signature support
iNinja May 8, 2026
fc2be48
Refactor X509 adapter routing, fix telemetry, improve PFX loading
iNinja May 8, 2026
71cccec
Route ECDSA X509 certs correctly, broaden PrivateKeyStatus handling, …
iNinja May 8, 2026
3b17575
Fix HasPrivateKey/PrivateKeyStatus interaction, align AKP HasPrivateK…
iNinja May 8, 2026
5bc48ff
Clear ConvertedSecurityKey on AKP x5c validation failure
iNinja May 8, 2026
b4b8d0d
Dispose X509Certificate2 instances in tests
iNinja May 8, 2026
cede17f
Merge remote-tracking branch 'origin/dev8x' into iinglese/ml-dsa-8x-v2
iNinja May 18, 2026
71fe548
Clean up InternalAPI.Unshipped.txt: keep only ML-DSA entries
iNinja May 19, 2026
897f707
Move ML-DSA PublicAPI entries from per-TFM to shared root file
iNinja May 19, 2026
8b48106
Merge remote-tracking branch 'origin/dev8x' into iinglese/ml-dsa-8x-v2
iNinja May 19, 2026
785d8d3
Merge branch 'dev8x' into iinglese/ml-dsa-8x-v2
iNinja May 23, 2026
31e1aac
Make MlDsaSecurityKey disposable and dispose MlDsaPublicKey on AKP re…
May 28, 2026
ed49730
Update ComputeJwkThumbprint() to properly dispose ML-DSA key object
May 28, 2026
94c9f8c
Clone MLDsa per AsymmetricAdapter for thread safety
May 28, 2026
d607a42
Remove TFM condition compilation of ML-DSA and use sign operation as …
May 28, 2026
445d578
Share ML-DSA between adapters in case of non-exportable seed or priva…
May 28, 2026
e934f35
Merge branch 'dev8x' into iinglese/ml-dsa-8x-v2
cpp11nullptr May 28, 2026
f803041
Merge branch 'dev8x' into iinglese/ml-dsa-8x-v2
cpp11nullptr May 29, 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
18 changes: 18 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- Directory.Build.targets is imported after NuGet package .targets files,
so target overrides here take effect over package-defined InitialTargets. -->
<Project>

<!-- Microsoft.Bcl.Cryptography 10.0.2 and its transitive dependency System.Formats.Asn1
emit TFM support warnings on net6.0. ML-DSA functionality is validated on net6.0
via the compatibility package and covered by the cross-TFM test suite.
These overrides suppress only these specific package warnings — any new package
that introduces TFM warnings will still surface normally.
Note: Condition="false" applies unconditionally, but the package .targets files
only fire on unsupported TFMs (net6.0), so this has no effect on net8.0+.
Remove when net6.0 is dropped from SrcTargets. -->
<Target Name="NETStandardCompatError_Microsoft_Bcl_Cryptography_net8_0"
Condition="false" />
<Target Name="NETStandardCompatError_System_Formats_Asn1_net8_0"
Condition="false" />

</Project>
3 changes: 2 additions & 1 deletion build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
<PropertyGroup>
<AspNetCoreMinSupportedVersion>2.1.1</AspNetCoreMinSupportedVersion>
<BannedApiAnalyzersVersion>4.14.0</BannedApiAnalyzersVersion>
<MicrosoftBclCryptographyVersion>10.0.2</MicrosoftBclCryptographyVersion>
<MicrosoftBclTimeProviderVersion>8.0.1</MicrosoftBclTimeProviderVersion>
<MicrosoftCSharpVersion>4.5.0</MicrosoftCSharpVersion>
<MicrosoftSourceLinkGitHubVersion>1.0.0</MicrosoftSourceLinkGitHubVersion>
<NetStandardVersion>2.0.3</NetStandardVersion>
<NewtonsoftVersion>13.0.3</NewtonsoftVersion>
<SystemDiagnosticSourceVersion>6.0.2</SystemDiagnosticSourceVersion>
<SystemMemoryVersion>4.5.5</SystemMemoryVersion>
<SystemMemoryVersion>4.6.3</SystemMemoryVersion>
Comment thread
cpp11nullptr marked this conversation as resolved.
<SystemSecurityCryptographyCngVersion>4.5.0</SystemSecurityCryptographyCngVersion>
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
</PropertyGroup>
Expand Down
155 changes: 155 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/AsymmetricAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ internal AsymmetricAdapter(
InitializeUsingX509SecurityKey(x509SecurityKeyFromJsonWebKey, algorithm, requirePrivateKey);
else if (securityKey is ECDsaSecurityKey edcsaSecurityKeyFromJsonWebKey)
InitializeUsingEcdsaSecurityKey(edcsaSecurityKeyFromJsonWebKey);
else if (securityKey is MlDsaSecurityKey mlDsaSecurityKeyFromJsonWebKey)
InitializeUsingMlDsaSecurityKey(mlDsaSecurityKeyFromJsonWebKey);
else
throw LogHelper.LogExceptionMessage(
new NotSupportedException(
Expand All @@ -99,6 +101,10 @@ internal AsymmetricAdapter(
{
InitializeUsingEcdsaSecurityKey(ecdsaKey);
}
else if (key is MlDsaSecurityKey mlDsaKey)
{
InitializeUsingMlDsaSecurityKey(mlDsaKey);
}
else
throw LogHelper.LogExceptionMessage(
new NotSupportedException(
Expand Down Expand Up @@ -138,6 +144,9 @@ protected virtual void Dispose(bool disposing)
{
if (ECDsa != null)
ECDsa.Dispose();

if (MLDsa != null)
MLDsa.Dispose();
#if DESKTOP
if (RsaCryptoServiceProviderProxy != null)
RsaCryptoServiceProviderProxy.Dispose();
Expand All @@ -151,6 +160,8 @@ protected virtual void Dispose(bool disposing)

private ECDsa ECDsa { get; set; }

private MLDsa MLDsa { get; set; }

internal byte[] Encrypt(byte[] data)
{
return _encryptFunction(data);
Expand All @@ -176,6 +187,23 @@ private void InitializeUsingEcdsaSecurityKey(ECDsaSecurityKey ecdsaSecurityKey)
_verifyUsingOffsetFunction = VerifyUsingOffsetECDsa;
}

private void InitializeUsingMlDsaSecurityKey(MlDsaSecurityKey mlDsaSecurityKey)
{
InitializeUsingMlDsa(mlDsaSecurityKey.MLDsa);
}

private void InitializeUsingMlDsa(MLDsa mlDsa)
{
MLDsa = mlDsa;
Comment thread
cpp11nullptr marked this conversation as resolved.
Outdated
_signFunction = SignMlDsa;
_signUsingOffsetFunction = SignUsingOffsetMlDsa;
#if NET6_0_OR_GREATER
_signUsingSpanFunction = SignUsingSpanMlDsa;
#endif
_verifyFunction = VerifyMlDsa;
_verifyUsingOffsetFunction = VerifyUsingOffsetMlDsa;
}

private void InitializeUsingRsa(RSA rsa, string algorithm)
{
// The return value for X509Certificate2.GetPrivateKey OR X509Certificate2.GetPublicKey.Key is a RSACryptoServiceProvider
Expand Down Expand Up @@ -257,11 +285,71 @@ private void InitializeUsingX509SecurityKey(
X509SecurityKey x509SecurityKey,
string algorithm,
bool requirePrivateKey)
{
if (x509SecurityKey.MlDsaPublicKey != null)
{
InitializeUsingX509MlDsa(x509SecurityKey, algorithm, requirePrivateKey);
}
else if (x509SecurityKey.PublicKey is RSA)
{
InitializeUsingX509Rsa(x509SecurityKey, algorithm, requirePrivateKey);
}
else if (x509SecurityKey.PublicKey is ECDsa ecDsa)
{
InitializeUsingEcdsaSecurityKey(new ECDsaSecurityKey(ecDsa));
}
else
{
// Certificate key type is not recognized (not RSA, ECDSA, or ML-DSA).
throw LogHelper.LogExceptionMessage(
new NotSupportedException(
LogHelper.FormatInvariant(
LogMessages.IDX10725,
LogHelper.MarkAsNonPII(algorithm),
LogHelper.MarkAsNonPII(x509SecurityKey.KeyId))));
}
}

private void InitializeUsingX509MlDsa(
X509SecurityKey x509SecurityKey,
string algorithm,
bool requirePrivateKey)
{
// Borrow the MLDsa instance from X509SecurityKey.
// The X509SecurityKey retains ownership; _disposeCryptoOperators remains
// false so the adapter will not dispose it. Same pattern as RSA/ECDsa.
MLDsa mlDsa = requirePrivateKey ? x509SecurityKey.MlDsaPrivateKey : x509SecurityKey.MlDsaPublicKey;
if (mlDsa == null)
throw LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(
LogMessages.IDX10723,
LogHelper.MarkAsNonPII(algorithm),
LogHelper.MarkAsNonPII(x509SecurityKey.KeyId))));

InitializeUsingMlDsa(mlDsa);
}

private void InitializeUsingX509Rsa(
X509SecurityKey x509SecurityKey,
string algorithm,
bool requirePrivateKey)
{
if (requirePrivateKey)
{
if (x509SecurityKey.PrivateKey == null)
throw LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(
LogMessages.IDX10638,
LogHelper.MarkAsNonPII(x509SecurityKey.KeyId))));

InitializeUsingRsa(x509SecurityKey.PrivateKey as RSA, algorithm);
}
else
{
InitializeUsingRsa(x509SecurityKey.PublicKey as RSA, algorithm);
Comment thread
iNinja marked this conversation as resolved.
}
}

private RSA RSA { get; set; }
Expand Down Expand Up @@ -342,6 +430,51 @@ private byte[] SignUsingOffsetECDsa(byte[] bytes, int offset, int count)
return ECDsa.SignHash(HashAlgorithm.ComputeHash(bytes, offset, count));
}

private byte[] SignMlDsa(byte[] bytes)
{
return MLDsa.SignData(bytes, context: null);
}

#if NET6_0_OR_GREATER
internal bool SignUsingSpanMlDsa(
ReadOnlySpan<byte> data,
Span<byte> destination,
out int bytesWritten)
{
int signatureSize = MLDsa.Algorithm.SignatureSizeInBytes;
if (destination.Length < signatureSize)
{
bytesWritten = 0;
return false;
}

// MLDsa.SignData requires destination to be exactly SignatureSizeInBytes.
MLDsa.SignData(data, destination.Slice(0, signatureSize), context: default);
bytesWritten = signatureSize;
return true;
}
#endif

private byte[] SignUsingOffsetMlDsa(byte[] bytes, int offset, int count)
{
#if NET6_0_OR_GREATER
Comment thread
cpp11nullptr marked this conversation as resolved.
Outdated
int signatureSize = MLDsa.Algorithm.SignatureSizeInBytes;
byte[] signature = new byte[signatureSize];
MLDsa.SignData(
new ReadOnlySpan<byte>(bytes, offset, count),
signature.AsSpan(),
context: default);
return signature;
#else
if (offset == 0 && count == bytes.Length)
return MLDsa.SignData(bytes, context: null);

byte[] slice = new byte[count];
Buffer.BlockCopy(bytes, offset, slice, 0, count);
return MLDsa.SignData(slice, context: null);
#endif
}

internal bool Verify(byte[] bytes, byte[] signature)
{
return _verifyFunction(bytes, signature);
Expand Down Expand Up @@ -382,6 +515,28 @@ private bool VerifyUsingOffsetECDsa(byte[] bytes, int offset, int count, byte[]
#endif
}

private bool VerifyMlDsa(byte[] bytes, byte[] signature)
{
return MLDsa.VerifyData(bytes, signature, context: null);
}

private bool VerifyUsingOffsetMlDsa(byte[] bytes, int offset, int count, byte[] signature)
{
#if NET6_0_OR_GREATER
return MLDsa.VerifyData(
new ReadOnlySpan<byte>(bytes, offset, count),
signature.AsSpan(),
context: default);
#else
if (offset == 0 && count == bytes.Length)
return MLDsa.VerifyData(bytes, signature, context: null);

byte[] slice = new byte[count];
Buffer.BlockCopy(bytes, offset, slice, 0, count);
return MLDsa.VerifyData(slice, signature, context: null);
#endif
}

private byte[] DecryptWithRsa(byte[] bytes)
{
return RSA.Decrypt(bytes, RSAEncryptionPadding);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ public class AsymmetricSignatureProvider : SignatureProvider
{ SecurityAlgorithms.RsaSsaPssSha512, 1040 },
{ SecurityAlgorithms.RsaSsaPssSha256Signature, 528 },
{ SecurityAlgorithms.RsaSsaPssSha384Signature, 784 },
{ SecurityAlgorithms.RsaSsaPssSha512Signature, 1040 }
{ SecurityAlgorithms.RsaSsaPssSha512Signature, 1040 },
{ SecurityAlgorithms.MlDsa44, 10496 },
{ SecurityAlgorithms.MlDsa65, 15616 },
{ SecurityAlgorithms.MlDsa87, 20736 }
};

/// <summary>
Expand All @@ -66,7 +69,10 @@ public class AsymmetricSignatureProvider : SignatureProvider
{ SecurityAlgorithms.RsaSsaPssSha512, 1040 },
{ SecurityAlgorithms.RsaSsaPssSha256Signature, 528 },
{ SecurityAlgorithms.RsaSsaPssSha384Signature, 784 },
{ SecurityAlgorithms.RsaSsaPssSha512Signature, 1040 }
{ SecurityAlgorithms.RsaSsaPssSha512Signature, 1040 },
{ SecurityAlgorithms.MlDsa44, 10496 },
{ SecurityAlgorithms.MlDsa65, 15616 },
{ SecurityAlgorithms.MlDsa87, 20736 }
};

internal AsymmetricSignatureProvider(
Expand Down Expand Up @@ -190,6 +196,12 @@ protected virtual HashAlgorithmName GetHashAlgorithmName(string algorithm)

private AsymmetricAdapter CreateAsymmetricAdapter()
{
// ML-DSA and other pure-signing algorithms do not use an external hash.
if (SupportedAlgorithms.IsSupportedMlDsaAlgorithm(Algorithm))
return new AsymmetricAdapter(Key, Algorithm, WillCreateSignatures);

// Preserve the protected virtual GetHashAlgorithmName extensibility point
// for hash-based algorithms (RSA, ECDSA).
HashAlgorithmName hashAlgorithmName = GetHashAlgorithmName(Algorithm);
return new AsymmetricAdapter(
Key,
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToSymmetricSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToX509SecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryCreateToRsaSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used for try pattern", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.JsonWebKeyConverter.TryConvertToMlDsaSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey,Microsoft.IdentityModel.Tokens.SecurityKey@)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as platform test", Scope = "member", Target = "~P:Microsoft.IdentityModel.Tokens.RsaSecurityKey.PrivateKeyStatus")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as platform test", Scope = "member", Target = "~P:Microsoft.IdentityModel.Tokens.MlDsaSecurityKey.PrivateKeyStatus")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as platform test", Scope = "member", Target = "~P:Microsoft.IdentityModel.Tokens.MlDsaSecurityKey.HasPrivateKey")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Appropriate exception will be caught.", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.InMemoryCryptoProviderCache.TryRemove(Microsoft.IdentityModel.Tokens.SignatureProvider)~System.Boolean")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Used as validation", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedSaml(Microsoft.IdentityModel.Tokens.SecurityToken,System.Nullable{System.DateTime},System.Nullable{System.DateTime},System.String,Microsoft.IdentityModel.Tokens.TokenValidationParameters,System.Text.StringBuilder)")]
[assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Not using Globalization", Scope = "member", Target = "~M:Microsoft.IdentityModel.Tokens.Interop.Kernel32.GetMessage(System.Int32,System.IntPtr)~System.String")]
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,16 @@ virtual Microsoft.IdentityModel.Tokens.TokenHandler.CreateClaimsIdentityInternal
virtual Microsoft.IdentityModel.Tokens.TokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.Experimental.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.Experimental.ValidationResult<Microsoft.IdentityModel.Tokens.Experimental.ValidatedToken, Microsoft.IdentityModel.Tokens.Experimental.ValidationError>>
virtual Microsoft.IdentityModel.Tokens.TokenHandler.ValidateTokenAsync(string token, Microsoft.IdentityModel.Tokens.Experimental.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.Experimental.ValidationResult<Microsoft.IdentityModel.Tokens.Experimental.ValidatedToken, Microsoft.IdentityModel.Tokens.Experimental.ValidationError>>
virtual Microsoft.IdentityModel.Tokens.ValidationError.CreateException() -> System.Exception
~Microsoft.IdentityModel.Tokens.MlDsaSecurityKey.MlDsaSecurityKey(Microsoft.IdentityModel.Tokens.JsonWebKey webKey, bool usePrivateKey) -> void
static Microsoft.IdentityModel.Tokens.MlDsaAdapter.CreateMlDsa(Microsoft.IdentityModel.Tokens.JsonWebKey jsonWebKey, bool usePrivateKey) -> System.Security.Cryptography.MLDsa
static Microsoft.IdentityModel.Tokens.MlDsaAdapter.GetMLDsaAlgorithm(string algorithm) -> System.Security.Cryptography.MLDsaAlgorithm
static Microsoft.IdentityModel.Tokens.SupportedAlgorithms.IsSupportedMlDsaAlgorithm(string algorithm) -> bool
static Microsoft.IdentityModel.Tokens.SupportedAlgorithms.TryGetHashAlgorithmName(string algorithm, out System.Security.Cryptography.HashAlgorithmName hashAlgorithmName) -> bool
internal static Microsoft.IdentityModel.Tokens.MlDsaSecurityKey.GetAlgorithmName(System.Security.Cryptography.MLDsaAlgorithm algorithm) -> string
Microsoft.IdentityModel.Tokens.X509SecurityKey.MlDsaPrivateKey.get -> System.Security.Cryptography.MLDsa
Microsoft.IdentityModel.Tokens.X509SecurityKey.MlDsaPublicKey.get -> System.Security.Cryptography.MLDsa
const Microsoft.IdentityModel.Tokens.LogMessages.IDX10721 = "IDX10721: Unable to create a key from the AKP JsonWebKey (alg: '{0}'). The required parameter '{1}' is missing or empty." -> string
const Microsoft.IdentityModel.Tokens.LogMessages.IDX10722 = "IDX10722: The AKP JsonWebKey (alg: '{0}') has inconsistent key material. The 'pub' parameter does not match the public key derived from 'priv'." -> string
const Microsoft.IdentityModel.Tokens.LogMessages.IDX10723 = "IDX10723: Unable to extract the private key from the X.509 certificate for algorithm '{0}' (Key: '{1}'). Private key extraction may not be supported on this platform." -> string
const Microsoft.IdentityModel.Tokens.LogMessages.IDX10724 = "IDX10724: Unable to compute a JWK thumbprint, public key extraction from the X.509 certificate is not supported on this platform (Key: '{0}')." -> string
const Microsoft.IdentityModel.Tokens.LogMessages.IDX10725 = "IDX10725: Unable to create a SignatureProvider for algorithm '{0}' (Key: '{1}'). The X.509 certificate key could not be extracted. This may indicate the platform does not support the certificate's key type." -> string
Loading