diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs index 6d8324b7eef531..825fab16bf1104 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs @@ -20,25 +20,26 @@ internal static partial class BCryptNative /// internal static class AlgorithmName { - public const string DSA = "DSA"; // BCRYPT_DSA_ALGORITHM - public const string ECDH = "ECDH"; // BCRYPT_ECDH_ALGORITHM - public const string ECDHP256 = "ECDH_P256"; // BCRYPT_ECDH_P256_ALGORITHM - public const string ECDHP384 = "ECDH_P384"; // BCRYPT_ECDH_P384_ALGORITHM - public const string ECDHP521 = "ECDH_P521"; // BCRYPT_ECDH_P521_ALGORITHM - public const string ECDsa = "ECDSA"; // BCRYPT_ECDSA_ALGORITHM - public const string ECDsaP256 = "ECDSA_P256"; // BCRYPT_ECDSA_P256_ALGORITHM - public const string ECDsaP384 = "ECDSA_P384"; // BCRYPT_ECDSA_P384_ALGORITHM - public const string ECDsaP521 = "ECDSA_P521"; // BCRYPT_ECDSA_P521_ALGORITHM - public const string HKDF = "HKDF"; // BCRYPT_HKDF_ALGORITHM - public const string MD5 = "MD5"; // BCRYPT_MD5_ALGORITHM - public const string MLDsa = "ML-DSA"; // BCRYPT_MLDSA_ALGORITHM - public const string MLKem = "ML-KEM"; // BCRYPT_MLKEM_ALGORITHM - public const string RSA = "RSA"; // BCRYPT_RSA_ALGORITHM - public const string Sha1 = "SHA1"; // BCRYPT_SHA1_ALGORITHM - public const string Sha256 = "SHA256"; // BCRYPT_SHA256_ALGORITHM - public const string Sha384 = "SHA384"; // BCRYPT_SHA384_ALGORITHM - public const string Sha512 = "SHA512"; // BCRYPT_SHA512_ALGORITHM - public const string Pbkdf2 = "PBKDF2"; // BCRYPT_PBKDF2_ALGORITHM + public const string CompositeMLDsa = "Composite-ML-DSA"; // BCRYPT_COMPOSITE_MLDSA_ALGORITHM + public const string DSA = "DSA"; // BCRYPT_DSA_ALGORITHM + public const string ECDH = "ECDH"; // BCRYPT_ECDH_ALGORITHM + public const string ECDHP256 = "ECDH_P256"; // BCRYPT_ECDH_P256_ALGORITHM + public const string ECDHP384 = "ECDH_P384"; // BCRYPT_ECDH_P384_ALGORITHM + public const string ECDHP521 = "ECDH_P521"; // BCRYPT_ECDH_P521_ALGORITHM + public const string ECDsa = "ECDSA"; // BCRYPT_ECDSA_ALGORITHM + public const string ECDsaP256 = "ECDSA_P256"; // BCRYPT_ECDSA_P256_ALGORITHM + public const string ECDsaP384 = "ECDSA_P384"; // BCRYPT_ECDSA_P384_ALGORITHM + public const string ECDsaP521 = "ECDSA_P521"; // BCRYPT_ECDSA_P521_ALGORITHM + public const string HKDF = "HKDF"; // BCRYPT_HKDF_ALGORITHM + public const string MD5 = "MD5"; // BCRYPT_MD5_ALGORITHM + public const string MLDsa = "ML-DSA"; // BCRYPT_MLDSA_ALGORITHM + public const string MLKem = "ML-KEM"; // BCRYPT_MLKEM_ALGORITHM + public const string RSA = "RSA"; // BCRYPT_RSA_ALGORITHM + public const string Sha1 = "SHA1"; // BCRYPT_SHA1_ALGORITHM + public const string Sha256 = "SHA256"; // BCRYPT_SHA256_ALGORITHM + public const string Sha384 = "SHA384"; // BCRYPT_SHA384_ALGORITHM + public const string Sha512 = "SHA512"; // BCRYPT_SHA512_ALGORITHM + public const string Pbkdf2 = "PBKDF2"; // BCRYPT_PBKDF2_ALGORITHM } internal static class KeyDerivationFunction diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSignHash.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSignHash.cs index 376a133244b17a..5cfac08815aeeb 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSignHash.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSignHash.cs @@ -76,7 +76,7 @@ internal static unsafe NTSTATUS BCryptSignHashPss( } } - internal static unsafe void BCryptSignHashPqcPure( + internal static unsafe int BCryptSignHashPqcPure( SafeBCryptKeyHandle key, ReadOnlySpan data, ReadOnlySpan context, @@ -104,12 +104,14 @@ internal static unsafe void BCryptSignHashPqcPure( BCryptSignVerifyFlags.BCRYPT_PAD_PQDSA); } - Debug.Assert(bytesWritten == destination.Length); - if (status != Interop.BCrypt.NTSTATUS.STATUS_SUCCESS) { throw Interop.BCrypt.CreateCryptographicException(status); } + + Debug.Assert(bytesWritten <= destination.Length); + + return bytesWritten; } internal static unsafe void BCryptSignHashPqcPreHash( diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs index eaa384bacd11f3..b66c1d29f0becb 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs @@ -116,6 +116,9 @@ internal enum KeyBlobMagicNumber : int BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC = 0x50444345, BCRYPT_ECDSA_PRIVATE_GENERIC_MAGIC = 0x56444345, + BCRYPT_COMPOSITE_MLDSA_PUBLIC_MAGIC = 0x4B504D43, + BCRYPT_COMPOSITE_MLDSA_PRIVATE_MAGIC = 0x4B534D43, + BCRYPT_MLDSA_PUBLIC_MAGIC = 0x4B505344, BCRYPT_MLDSA_PRIVATE_MAGIC = 0x4B535344, BCRYPT_MLDSA_PRIVATE_SEED_MAGIC = 0x53535344, diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs index 8145cdcdcd6e4a..1d67ed324aaaa2 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs @@ -674,6 +674,11 @@ static void SubjectPublicKeyReader(ReadOnlySpan key, in ValueAlgorithmIden { CompositeMLDsaAlgorithm algorithm = GetAlgorithmIdentifier(in identifier); + if (!IsAlgorithmSupported(algorithm)) + { + throw new CryptographicException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + } + if (!algorithm.IsValidPublicKeySize(key.Length)) { throw new CryptographicException(SR.Argument_PublicKeyWrongSizeForAlgorithm); @@ -867,6 +872,11 @@ static void PrivateKeyReader( { CompositeMLDsaAlgorithm algorithm = GetAlgorithmIdentifier(in algorithmIdentifier); + if (!IsAlgorithmSupported(algorithm)) + { + throw new CryptographicException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + } + if (!algorithm.IsValidPrivateKeySize(privateKeyContents.Length)) { throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); @@ -1795,6 +1805,50 @@ protected virtual void Dispose(bool disposing) { } + private protected bool TryExportPkcs8FromExportedPrivateKey(Span destination, out int bytesWritten) + { + AsnWriter? writer = null; + + try + { + using (CryptoPoolLease lease = CryptoPoolLease.Rent(Algorithm.MaxPrivateKeySizeInBytes)) + { + int privateKeySize = ExportCompositeMLDsaPrivateKeyCore(lease.Span); + + if (!Algorithm.IsValidPrivateKeySize(privateKeySize)) + { + bytesWritten = 0; + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } + + // Add some overhead for the ASN.1 structure. + int initialCapacity = 32 + privateKeySize; + + writer = new AsnWriter(AsnEncodingRules.DER, initialCapacity); + + using (writer.PushSequence()) + { + writer.WriteInteger(0); // Version + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(Algorithm.Oid); + } + + writer.WriteOctetString(lease.Span.Slice(0, privateKeySize)); + } + + Debug.Assert(writer.GetEncodedLength() <= initialCapacity); + + return writer.TryEncode(destination, out bytesWritten); + } + } + finally + { + writer?.Reset(); + } + } + private AsnWriter WriteEncryptedPkcs8PrivateKeyToAsnWriter(ReadOnlySpan passwordBytes, PbeParameters pbeParameters) { AsnWriter? tmp = null; diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaImplementation.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaImplementation.Windows.cs index dc0fa563d9f7be..df2c2e664b2d4e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaImplementation.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaImplementation.Windows.cs @@ -1,81 +1,219 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; +using System.Security.Cryptography.Asn1; using Internal.Cryptography; +using Internal.NativeCrypto; +using Microsoft.Win32.SafeHandles; + +using NTSTATUS = Interop.BCrypt.NTSTATUS; namespace System.Security.Cryptography { internal sealed partial class CompositeMLDsaImplementation : CompositeMLDsa { - private CompositeMLDsaImplementation(CompositeMLDsaAlgorithm algorithm) + private static readonly SafeBCryptAlgorithmHandle? s_algHandle = OpenAlgorithmHandle(); + + private readonly bool _hasPrivateKey; + private SafeBCryptKeyHandle _key; + + private CompositeMLDsaImplementation( + CompositeMLDsaAlgorithm algorithm, + SafeBCryptKeyHandle key, + bool hasPrivateKey) : base(algorithm) { - throw new PlatformNotSupportedException(); + _key = key; + _hasPrivateKey = hasPrivateKey; } - internal static partial bool SupportsAny() + [MemberNotNullWhen(true, nameof(s_algHandle))] + internal static partial bool SupportsAny() => s_algHandle is not null; + + [MemberNotNullWhen(true, nameof(s_algHandle))] + internal static partial bool IsAlgorithmSupportedImpl(CompositeMLDsaAlgorithm algorithm) => + SupportsAny() && PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out _); + + protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) { - if (!Helpers.IsOSPlatformWindows) + if (!_hasPrivateKey) { - return false; + throw new CryptographicException(SR.Cryptography_NoPrivateKeyAvailable); } - return CompositeMLDsaManaged.SupportsAny(); + return Interop.BCrypt.BCryptSignHashPqcPure(_key, data, context, destination); } - internal static partial bool IsAlgorithmSupportedImpl(CompositeMLDsaAlgorithm algorithm) + protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => + Interop.BCrypt.BCryptVerifySignaturePqcPure(_key, data, context, signature); + + internal static partial CompositeMLDsa GenerateKeyImpl(CompositeMLDsaAlgorithm algorithm) { - if (!Helpers.IsOSPlatformWindows) + Debug.Assert(SupportsAny()); + + if (!PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out string? parameterSet)) { - return false; + Debug.Fail("Base class should have validated algorithm support."); + throw new CryptographicException(); } - return CompositeMLDsaManaged.IsAlgorithmSupportedImpl(algorithm); - } + SafeBCryptKeyHandle keyHandle = Interop.BCrypt.BCryptGenerateKeyPair(s_algHandle, keyLength: 0); - internal static partial CompositeMLDsa GenerateKeyImpl(CompositeMLDsaAlgorithm algorithm) - { - if (!Helpers.IsOSPlatformWindows) + try { - throw new PlatformNotSupportedException(); + Interop.BCrypt.BCryptSetSZProperty(keyHandle, Interop.BCrypt.BCryptPropertyStrings.BCRYPT_PARAMETER_SET_NAME, parameterSet); + Interop.BCrypt.BCryptFinalizeKeyPair(keyHandle); + } + catch + { + keyHandle?.Dispose(); + throw; } - return CompositeMLDsaManaged.GenerateKeyImpl(algorithm); + return new CompositeMLDsaImplementation(algorithm, keyHandle, hasPrivateKey: true); } internal static partial CompositeMLDsa ImportCompositeMLDsaPublicKeyImpl(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) { - if (!Helpers.IsOSPlatformWindows) + Debug.Assert(SupportsAny()); + + if (!PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out string? parameterSet)) { - throw new PlatformNotSupportedException(); + Debug.Fail("Base class should have validated algorithm support."); + throw new CryptographicException(); } - return CompositeMLDsaManaged.ImportCompositeMLDsaPublicKeyImpl(algorithm, source); + const string PublicBlobType = Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PUBLIC_BLOB; + + SafeBCryptKeyHandle key = + PqcBlobHelpers.EncodeCompositeMLDsaBlob( + parameterSet, + source, + PublicBlobType, + static blob => Interop.BCrypt.BCryptImportKeyPair(s_algHandle, PublicBlobType, blob)); + + return new CompositeMLDsaImplementation(algorithm, key, hasPrivateKey: false); } internal static partial CompositeMLDsa ImportCompositeMLDsaPrivateKeyImpl(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) { - if (!Helpers.IsOSPlatformWindows) + Debug.Assert(SupportsAny()); + + if (!PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out string? parameterSet)) { - throw new PlatformNotSupportedException(); + Debug.Fail("Base class should have validated algorithm support."); + throw new CryptographicException(); } - return CompositeMLDsaManaged.ImportCompositeMLDsaPrivateKeyImpl(algorithm, source); - } + const string PrivateBlobType = Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB; - protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) => - throw new PlatformNotSupportedException(); + SafeBCryptKeyHandle key = + PqcBlobHelpers.EncodeCompositeMLDsaBlob( + parameterSet, + source, + PrivateBlobType, + static blob => Interop.BCrypt.BCryptImportKeyPair(s_algHandle, PrivateBlobType, blob)); - protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => - throw new PlatformNotSupportedException(); + return new CompositeMLDsaImplementation(algorithm, key, hasPrivateKey: true); + } - protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) => - throw new PlatformNotSupportedException(); + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + if (!_hasPrivateKey) + { + throw new CryptographicException(SR.Cryptography_NoPrivateKeyAvailable); + } + + return TryExportPkcs8FromExportedPrivateKey(destination, out bytesWritten); + } protected override int ExportCompositeMLDsaPublicKeyCore(Span destination) => - throw new PlatformNotSupportedException(); + ExportKey(Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PUBLIC_BLOB, destination); - protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) => - throw new PlatformNotSupportedException(); + protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) + { + if (!_hasPrivateKey) + { + throw new CryptographicException(SR.Cryptography_NoPrivateKeyAvailable); + } + + return ExportKey(Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB, destination); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key?.Dispose(); + _key = null!; + } + + base.Dispose(disposing); + } + + private int ExportKey(string keyBlobType, Span destination) + { + ArraySegment keyBlob = Interop.BCrypt.BCryptExportKey(_key, keyBlobType); + + try + { + ReadOnlySpan keyBytes = PqcBlobHelpers.DecodeCompositeMLDsaBlob( + keyBlob, + out ReadOnlySpan parameterSet, + out string blobType); + + if (!PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(Algorithm, out string? expectedParameterSet)) + { + Debug.Fail("Unsupported algorithm."); + throw new CryptographicException(); + } + + if (blobType != keyBlobType || + keyBytes.Length > destination.Length || + !parameterSet.SequenceEqual(expectedParameterSet)) + { + Debug.Fail( + $"{nameof(blobType)}: {blobType}, " + + $"{nameof(parameterSet)}: {parameterSet.ToString()}, " + + $"{nameof(keyBytes)}.Length: {keyBytes.Length} / {destination.Length}"); + + throw new CryptographicException(); + } + + keyBytes.CopyTo(destination); + return keyBytes.Length; + } + finally + { + CryptoPool.Return(keyBlob); + } + } + + private static SafeBCryptAlgorithmHandle? OpenAlgorithmHandle() + { + if (!Helpers.IsOSPlatformWindows) + { + return null; + } + + NTSTATUS status = Interop.BCrypt.BCryptOpenAlgorithmProvider( + out SafeBCryptAlgorithmHandle hAlgorithm, + BCryptNative.AlgorithmName.CompositeMLDsa, + pszImplementation: null, + Interop.BCrypt.BCryptOpenAlgorithmProviderFlags.None); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + hAlgorithm.Dispose(); + return null; + } + else + { + return hAlgorithm; + } + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index a4844646a065b2..578ff6c96ae332 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -335,43 +335,8 @@ protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan destination, out int bytesWritten) - { - AsnWriter? writer = null; - - try - { - using (CryptoPoolLease lease = CryptoPoolLease.Rent(Algorithm.MaxPrivateKeySizeInBytes)) - { - int privateKeySize = ExportCompositeMLDsaPrivateKeyCore(lease.Span); - - // Add some overhead for the ASN.1 structure. - int initialCapacity = 32 + privateKeySize; - - writer = new AsnWriter(AsnEncodingRules.DER, initialCapacity); - - using (writer.PushSequence()) - { - writer.WriteInteger(0); // Version - - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier(Algorithm.Oid); - } - - writer.WriteOctetString(lease.Span.Slice(0, privateKeySize)); - } - - Debug.Assert(writer.GetEncodedLength() <= initialCapacity); - } - - return writer.TryEncode(destination, out bytesWritten); - } - finally - { - writer?.Reset(); - } - } + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) => + TryExportPkcs8FromExportedPrivateKey(destination, out bytesWritten); protected override int ExportCompositeMLDsaPublicKeyCore(Span destination) { diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs index dcbd34ed8a2015..4ef3c7b9c3b8c0 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs @@ -44,7 +44,13 @@ protected override void SignDataCore(ReadOnlySpan data, ReadOnlySpan throw new CryptographicException(SR.Cryptography_NoPrivateKeyAvailable); } - Interop.BCrypt.BCryptSignHashPqcPure(_key, data, context, destination); + int written = Interop.BCrypt.BCryptSignHashPqcPure(_key, data, context, destination); + + if (written != destination.Length) + { + Debug.Fail($"Expected {destination.Length} bytes, but {nameof(Interop.BCrypt.BCryptSignHashPqcPure)} wrote {written} bytes."); + throw new CryptographicException(); + } } protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => diff --git a/src/libraries/Common/src/System/Security/Cryptography/PqcBlobHelpers.cs b/src/libraries/Common/src/System/Security/Cryptography/PqcBlobHelpers.cs index 6ebd7ef45d4560..3cb718748230b6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/PqcBlobHelpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/PqcBlobHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -22,6 +23,11 @@ internal static partial class PqcBlobHelpers internal const string BCRYPT_MLKEM_PARAMETER_SET_768 = "768"; internal const string BCRYPT_MLKEM_PARAMETER_SET_1024 = "1024"; + internal const string BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_44_ECDSA_P256_SHA256 = "44-ECDSA-P256-SHA256"; + internal const string BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P256_SHA512 = "65-ECDSA-P256-SHA512"; + internal const string BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P384_SHA512 = "65-ECDSA-P384-SHA512"; + internal const string BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_87_ECDSA_P384_SHA512 = "87-ECDSA-P384-SHA512"; + internal static string GetMLDsaParameterSet(MLDsaAlgorithm algorithm) { if (algorithm == MLDsaAlgorithm.MLDsa44) @@ -41,6 +47,35 @@ internal static string GetMLDsaParameterSet(MLDsaAlgorithm algorithm) throw new PlatformNotSupportedException(); } + internal static bool TryGetCompositeMLDsaParameterSet( + CompositeMLDsaAlgorithm algorithm, + [NotNullWhen(true)] out string? parameterSet) + { + if (algorithm == CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256) + { + parameterSet = BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_44_ECDSA_P256_SHA256; + return true; + } + else if (algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256) + { + parameterSet = BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P256_SHA512; + return true; + } + else if (algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384) + { + parameterSet = BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P384_SHA512; + return true; + } + else if (algorithm == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384) + { + parameterSet = BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_87_ECDSA_P384_SHA512; + return true; + } + + parameterSet = null; + return false; + } + internal delegate TResult EncodeBlobFunc(ReadOnlySpan blob); internal static TResult EncodeMLDsaBlob( @@ -70,6 +105,30 @@ internal static TResult EncodeMLDsaBlob( return EncodePQDsaBlob(magic, parameterSet, data, callback); } + internal static TResult EncodeCompositeMLDsaBlob( + ReadOnlySpan parameterSet, + ReadOnlySpan data, + string blobType, + EncodeBlobFunc callback) + { + KeyBlobMagicNumber magic; + + switch (blobType) + { + case Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PUBLIC_BLOB: + magic = KeyBlobMagicNumber.BCRYPT_COMPOSITE_MLDSA_PUBLIC_MAGIC; + break; + case Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB: + magic = KeyBlobMagicNumber.BCRYPT_COMPOSITE_MLDSA_PRIVATE_MAGIC; + break; + default: + Debug.Fail("Unknown blob type."); + throw new CryptographicException(); + } + + return EncodePQDsaBlob(magic, parameterSet, data, callback); + } + internal static ReadOnlySpan DecodeMLDsaBlob( ReadOnlySpan blob, out ReadOnlySpan parameterSet, @@ -96,6 +155,29 @@ internal static ReadOnlySpan DecodeMLDsaBlob( return data; } + internal static ReadOnlySpan DecodeCompositeMLDsaBlob( + ReadOnlySpan blob, + out ReadOnlySpan parameterSet, + out string blobType) + { + ReadOnlySpan data = DecodePQDsaBlob(blob, out KeyBlobMagicNumber magic, out parameterSet); + + switch (magic) + { + case KeyBlobMagicNumber.BCRYPT_COMPOSITE_MLDSA_PUBLIC_MAGIC: + blobType = Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PUBLIC_BLOB; + break; + case KeyBlobMagicNumber.BCRYPT_COMPOSITE_MLDSA_PRIVATE_MAGIC: + blobType = Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB; + break; + default: + Debug.Fail("Unknown blob type."); + throw new CryptographicException(); + } + + return data; + } + private static unsafe TResult EncodePQDsaBlob( KeyBlobMagicNumber magic, ReadOnlySpan parameterSet, @@ -110,11 +192,9 @@ private static unsafe TResult EncodePQDsaBlob( data.Length); // Key // For ML-DSA we need 12 bytes for header, 6 bytes for parameter set, and 32 bytes for seed. Round up to 64. + // Other PQC algorithms may need over 256 bytes, so fall back to renting. const int StackAllocThreshold = 64; - // If there are new algorithms that require more than 64 bytes, we should increase the threshold. - Debug.Assert(blobSize is > 256 or <= StackAllocThreshold, "Increase stackalloc threshold"); - byte[]? rented = null; Span blobBytes = blobSize <= StackAllocThreshold diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs index 97118669d7de84..57611fb2cb878e 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Formats.Asn1; +using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; using Microsoft.DotNet.RemoteExecutor; using Test.Cryptography; @@ -12,6 +13,11 @@ namespace System.Security.Cryptography.Tests { public static class CompositeMLDsaFactoryTests { + // The Ed448 composite is commonly not supported, so we can use it as a representative test case for scenarios + // where Composite ML-DSA is supported but a specific composite is not. + public static bool CompositeMLDsaSupportedAndEd448CompositeNotSupported => + CompositeMLDsa.IsSupported && !CompositeMLDsa.IsAlgorithmSupported(CompositeMLDsaAlgorithm.MLDsa87WithEd448); + [Fact] public static void NullArgumentValidation() { @@ -332,7 +338,7 @@ private static void AssertImportBadPrivateKey(CompositeMLDsaAlgorithm algorithm, { CompositeMLDsaTestHelpers.AssertImportPrivateKey( import => AssertThrowIfNotSupported( - () => AssertExtensions.Throws(() => import()), + () => Assert.ThrowsAny(() => import()), algorithm), algorithm, key); @@ -463,7 +469,7 @@ private static void AssertImportBadPublicKey(CompositeMLDsaAlgorithm algorithm, { CompositeMLDsaTestHelpers.AssertImportPublicKey( import => AssertThrowIfNotSupported( - () => AssertExtensions.Throws(() => import()), + () => Assert.ThrowsAny(() => import()), algorithm), algorithm, key); @@ -571,6 +577,24 @@ public static void ImportSubjectPublicKeyInfo_AlgorithmErrorsInAsn() CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo(import => AssertThrowIfNotSupported(() => import(spki.Encode()))); } + [ConditionalFact(nameof(CompositeMLDsaSupportedAndEd448CompositeNotSupported))] + public static void ImportSubjectPublicKeyInfo_SupportedButHasUnsupportedAlgorithm() + { + // Create an unsupported Composite ML-DSA SPKI + SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn + { + Algorithm = new AlgorithmIdentifierAsn + { + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa87WithEd448), + Parameters = null, + }, + SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).PublicKey, + }; + + CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo( + import => Assert.Throws(() => import(spki.Encode()))); + } + [Fact] public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn() { @@ -609,6 +633,23 @@ public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn() CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => AssertThrowIfNotSupported(() => import(pkcs8.Encode()))); } + [ConditionalFact(nameof(CompositeMLDsaSupportedAndEd448CompositeNotSupported))] + public static void ImportPkcs8PrivateKey_SupportedButHasUnsupportedAlgorithm() + { + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa87WithEd448), + Parameters = null, + }, + PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).SecretKey, + }; + + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => Assert.Throws(() => import(pkcs8.Encode()))); + } + [Fact] public static void ImportFromPem_MalformedPem() { @@ -685,23 +726,63 @@ public static void AlgorithmMatches_Import(CompositeMLDsaTestData.CompositeMLDsa [Fact] public static void IsSupported_AgreesWithPlatform() { - // Composites are supported everywhere MLDsa is supported - Assert.Equal(MLDsa.IsSupported, CompositeMLDsa.IsSupported); + bool supported; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (PlatformDetection.IsWindows10Version28120OrGreater) + { + // Windows supports: https://learn.microsoft.com/en-us/windows/win32/seccng/bcrypt/ns-bcrypt-bcrypt_pqdsa_key_blob#cbparameterset + supported = true; + } + else + { + // Do not fall back to managed implementation on Windows versions that do not support Composite ML-DSA. + supported = false; + } + } + else + { + // Non-Windows uses the managed implementation, so the support is the same as for MLDsa. + supported = MLDsa.IsSupported; + } + + Assert.Equal(supported, CompositeMLDsa.IsSupported); } [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void IsAlgorithmSupported_AgreesWithPlatform(CompositeMLDsaAlgorithm algorithm) { - bool supported = CompositeMLDsaTestHelpers.ExecuteComponentFunc( - algorithm, - rsa => MLDsa.IsSupported, - ecdsa => ecdsa.IsSec && MLDsa.IsSupported, - eddsa => false); + bool supported; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (PlatformDetection.IsWindows10Version28120OrGreater) + { + // Windows supports: https://learn.microsoft.com/en-us/windows/win32/seccng/bcrypt/ns-bcrypt-bcrypt_pqdsa_key_blob#cbparameterset + supported = + algorithm == CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256 || + algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256 || + algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384 || + algorithm == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384; + } + else + { + // Do not fall back to managed implementation on Windows versions that do not support Composite ML-DSA. + supported = false; + } + } + else + { + supported = CompositeMLDsaTestHelpers.ExecuteComponentFunc( + algorithm, + rsa => MLDsa.IsSupported, + ecdsa => ecdsa.IsSec && MLDsa.IsSupported, + eddsa => false); + } - Assert.Equal( - supported, - CompositeMLDsa.IsAlgorithmSupported(algorithm)); + Assert.Equal(supported, CompositeMLDsa.IsAlgorithmSupported(algorithm)); } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -743,6 +824,10 @@ private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorit { Assert.Contains("CompositeMLDsa", pnse.Message); } + catch (ThrowsAnyException te) when (te.InnerException is PlatformNotSupportedException pnse) + { + Assert.Contains("CompositeMLDsa", pnse.Message); + } } } diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Windows.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Windows.cs index ac6ef2d9f63bcc..2ba5561cb420d2 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Windows.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Windows.cs @@ -69,6 +69,11 @@ public static partial class PlatformDetection // Windows 11 2024 Update (24H2) public static bool IsWindows10Version26100OrGreater => IsWindowsVersionOrLater(10, 0, 26100); + // TODO (https://github.com/dotnet/runtime/issues/129787): This build has Composite ML-DSA support but is a preview build. + // This should be changed to the next official release build once it is available. + // Windows 11 Preview Build 28120 (Experimental Channel) + public static bool IsWindows10Version28120OrGreater => IsWindowsVersionOrLater(10, 0, 28120); + public static bool IsWindowsIoTCore { get diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 6670adf751a160..958817d7ce2a85 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -427,12 +427,6 @@ Link="Common\System\Security\Cryptography\CompositeMLDsaImplementation.Windows.cs" /> - - - - - -