diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptExportKey.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptExportKey.cs index 3be63fdfe175a1..a9c3da451f636d 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptExportKey.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptExportKey.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -36,10 +37,78 @@ internal static ArraySegment BCryptExportKey(SafeBCryptKeyHandle key, stri if (ntStatus != NTSTATUS.STATUS_SUCCESS) { + CryptoPool.Return(rented); throw CreateCryptographicException(ntStatus); } return new ArraySegment(rented, 0, numBytesNeeded); } + + internal static T BCryptExportKey(SafeBCryptKeyHandle key, string blobType, Func callback) + { + int numBytesNeeded; + NTSTATUS ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, null, 0, out numBytesNeeded, 0); + + if (ntStatus != NTSTATUS.STATUS_SUCCESS) + { + throw CreateCryptographicException(ntStatus); + } + + // Array must be precisely-sized, so no renting. + byte[] destination = new byte[numBytesNeeded]; + + using (PinAndClear.Track(destination)) + { + ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, destination, numBytesNeeded, out numBytesNeeded, 0); + + if (ntStatus != NTSTATUS.STATUS_SUCCESS) + { + throw CreateCryptographicException(ntStatus); + } + + if (numBytesNeeded != destination.Length) + { + Debug.Fail("Written byte count does not match required byte count."); + throw new CryptographicException(); + } + + return callback(destination); + } + } + + internal delegate T ExportKeyCallback(ReadOnlySpan keyBytes); + + internal static T BCryptExportKey(SafeBCryptKeyHandle key, string blobType, ExportKeyCallback callback) + { + int numBytesNeeded; + NTSTATUS ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, null, 0, out numBytesNeeded, 0); + + if (ntStatus != NTSTATUS.STATUS_SUCCESS) + { + throw CreateCryptographicException(ntStatus); + } + + byte[] rented = CryptoPool.Rent(numBytesNeeded); + + try + { + using (PinAndClear.Track(rented)) + { + ntStatus = BCryptExportKey(key, IntPtr.Zero, blobType, rented, numBytesNeeded, out numBytesNeeded, 0); + + if (ntStatus != NTSTATUS.STATUS_SUCCESS) + { + throw CreateCryptographicException(ntStatus); + } + + return callback(rented.AsSpan(0, numBytesNeeded)); + } + } + finally + { + // PinAndClear will clear + CryptoPool.Return(rented, clearSize: 0); + } + } } } diff --git a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.SignVerify.cs b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.SignVerify.cs index efa93181594a45..4d9c69139d117c 100644 --- a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.SignVerify.cs +++ b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.SignVerify.cs @@ -23,6 +23,15 @@ internal static unsafe ErrorCode NCryptSignHash(SafeNCryptKeyHandle hKey, void* } [LibraryImport(Libraries.NCrypt, StringMarshalling = StringMarshalling.Utf16)] - internal static unsafe partial ErrorCode NCryptVerifySignature(SafeNCryptKeyHandle hKey, void* pPaddingInfo, ReadOnlySpan pbHashValue, int cbHashValue, ReadOnlySpan pbSignature, int cbSignature, AsymmetricPaddingMode dwFlags); + private static unsafe partial ErrorCode NCryptVerifySignature(SafeNCryptKeyHandle hKey, void* pPaddingInfo, byte* pbHashValue, int cbHashValue, byte* pbSignature, int cbSignature, AsymmetricPaddingMode dwFlags); + + internal static unsafe ErrorCode NCryptVerifySignature(SafeNCryptKeyHandle hKey, void* pPaddingInfo, ReadOnlySpan pbHashValue, int cbHashValue, ReadOnlySpan pbSignature, int cbSignature, AsymmetricPaddingMode dwFlags) + { + fixed (byte* pHash = &Helpers.GetNonNullPinnableReference(pbHashValue)) + fixed (byte* pSignature = &Helpers.GetNonNullPinnableReference(pbSignature)) + { + return NCryptVerifySignature(hKey, pPaddingInfo, pHash, cbHashValue, pSignature, cbSignature, dwFlags); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngCommon.SignVerify.cs b/src/libraries/Common/src/System/Security/Cryptography/CngHelpers.SignVerify.cs similarity index 98% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngCommon.SignVerify.cs rename to src/libraries/Common/src/System/Security/Cryptography/CngHelpers.SignVerify.cs index d8968e2e6adff6..12966724e09485 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngCommon.SignVerify.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CngHelpers.SignVerify.cs @@ -1,16 +1,14 @@ // 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.Diagnostics; -using Internal.Cryptography; using Microsoft.Win32.SafeHandles; using AsymmetricPaddingMode = Interop.NCrypt.AsymmetricPaddingMode; using ErrorCode = Interop.NCrypt.ErrorCode; namespace System.Security.Cryptography { - internal static partial class CngCommon + internal static partial class CngHelpers { private const int StatusUnsuccessfulRetryCount = 1; diff --git a/src/libraries/Common/src/System/Security/Cryptography/CngHelpers.cs b/src/libraries/Common/src/System/Security/Cryptography/CngHelpers.cs new file mode 100644 index 00000000000000..e13c839f6ea2bc --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/CngHelpers.cs @@ -0,0 +1,301 @@ +// 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.Runtime.InteropServices; +using System.Runtime.Versioning; +using Internal.Cryptography; +using Microsoft.Win32.SafeHandles; +using ErrorCode = Interop.NCrypt.ErrorCode; + +namespace System.Security.Cryptography +{ + internal static partial class CngHelpers + { + internal static CryptographicException ToCryptographicException(this Interop.NCrypt.ErrorCode errorCode) + { + return ((int)errorCode).ToCryptographicException(); + } + + internal static void SetExportPolicy(this SafeNCryptKeyHandle keyHandle, CngExportPolicies exportPolicy) + { + unsafe + { + ErrorCode errorCode = Interop.NCrypt.NCryptSetProperty( + keyHandle, + KeyPropertyName.ExportPolicy, + &exportPolicy, + sizeof(CngExportPolicies), + CngPropertyOptions.Persist); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + } + } + + /// + /// Returns a CNG key property. + /// + /// + /// null - if property not defined on key. + /// throws - for any other type of error. + /// + internal static byte[]? GetProperty(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) + { + Debug.Assert(!ncryptHandle.IsInvalid); + unsafe + { + ErrorCode errorCode = Interop.NCrypt.NCryptGetProperty( + ncryptHandle, + propertyName, + null, + 0, + out int numBytesNeeded, + options); + + if (errorCode == ErrorCode.NTE_NOT_FOUND) + { + return null; + } + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + byte[] propertyValue = new byte[numBytesNeeded]; + + fixed (byte* pPropertyValue = propertyValue) + { + errorCode = Interop.NCrypt.NCryptGetProperty( + ncryptHandle, + propertyName, + pPropertyValue, + propertyValue.Length, + out numBytesNeeded, + options); + } + + if (errorCode == ErrorCode.NTE_NOT_FOUND) + { + return null; + } + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + Array.Resize(ref propertyValue, numBytesNeeded); + return propertyValue; + } + } + + /// + /// Retrieve a well-known CNG string property. (Note: .NET Framework compat: this helper likes to return special + /// values rather than throw exceptions for missing or ill-formatted property values. Only use it for well-known + /// properties that are unlikely to be ill-formatted.) + /// + internal static string? GetPropertyAsString(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) + { + Debug.Assert(!ncryptHandle.IsInvalid); + byte[]? value = GetProperty(ncryptHandle, propertyName, options); + + if (value == null) + { + // .NET Framework compat: return null if key not present. + return null; + } + + if (value.Length == 0) + { + // .NET Framework compat: return empty if property value is 0-length. + return string.Empty; + } + + unsafe + { + fixed (byte* pValue = &value[0]) + { + string valueAsString = Marshal.PtrToStringUni((IntPtr)pValue)!; + return valueAsString; + } + } + } + + internal static bool TryExportKeyBlob( + this SafeNCryptKeyHandle handle, + string blobType, + Span destination, + out int bytesWritten) + { + // Sanity check the current bounds + Span empty = default; + + ErrorCode errorCode = Interop.NCrypt.NCryptExportKey( + handle, + IntPtr.Zero, + blobType, + IntPtr.Zero, + ref MemoryMarshal.GetReference(empty), + empty.Length, + out int written, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + if (written > destination.Length) + { + bytesWritten = 0; + return false; + } + + errorCode = Interop.NCrypt.NCryptExportKey( + handle, + IntPtr.Zero, + blobType, + IntPtr.Zero, + ref MemoryMarshal.GetReference(destination), + destination.Length, + out written, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + bytesWritten = written; + return true; + } + + internal static unsafe bool ExportPkcs8KeyBlob( + bool allocate, + SafeNCryptKeyHandle keyHandle, + ReadOnlySpan password, + int kdfCount, + Span destination, + out int bytesWritten, + out byte[]? allocated) + { + using (SafeUnicodeStringHandle stringHandle = new SafeUnicodeStringHandle(password)) + { + ReadOnlySpan pkcs12TripleDesOidBytes = "1.2.840.113549.1.12.1.3\0"u8; // the Windows APIs for OID strings are ASCII-only + fixed (byte* oidPtr = &MemoryMarshal.GetReference(pkcs12TripleDesOidBytes)) + { + Interop.NCrypt.NCryptBuffer* buffers = stackalloc Interop.NCrypt.NCryptBuffer[3]; + + Interop.NCrypt.PBE_PARAMS pbeParams = default; + Span salt = new Span(pbeParams.rgbSalt, Interop.NCrypt.PBE_PARAMS.RgbSaltSize); +#if NET + RandomNumberGenerator.Fill(salt); +#else + CngHelpers.GetRandomBytes(salt); +#endif + pbeParams.Params.cbSalt = salt.Length; + pbeParams.Params.iIterations = kdfCount; + + buffers[0] = new Interop.NCrypt.NCryptBuffer + { + BufferType = Interop.NCrypt.BufferType.PkcsSecret, + cbBuffer = checked(2 * (password.Length + 1)), + pvBuffer = stringHandle.DangerousGetHandle(), + }; + + if (buffers[0].pvBuffer == IntPtr.Zero) + { + buffers[0].cbBuffer = 0; + } + + buffers[1] = new Interop.NCrypt.NCryptBuffer + { + BufferType = Interop.NCrypt.BufferType.PkcsAlgOid, + cbBuffer = pkcs12TripleDesOidBytes.Length, + pvBuffer = (IntPtr)oidPtr, + }; + + buffers[2] = new Interop.NCrypt.NCryptBuffer + { + BufferType = Interop.NCrypt.BufferType.PkcsAlgParam, + cbBuffer = sizeof(Interop.NCrypt.PBE_PARAMS), + pvBuffer = (IntPtr)(&pbeParams), + }; + + Interop.NCrypt.NCryptBufferDesc desc = new Interop.NCrypt.NCryptBufferDesc + { + cBuffers = 3, + pBuffers = (IntPtr)buffers, + ulVersion = 0, + }; + + Span empty = default; + + ErrorCode errorCode = Interop.NCrypt.NCryptExportKey( + keyHandle, + IntPtr.Zero, + Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, + ref desc, + ref MemoryMarshal.GetReference(empty), + 0, + out int numBytesNeeded, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + allocated = null; + + if (allocate) + { + allocated = new byte[numBytesNeeded]; + destination = allocated; + } + else if (numBytesNeeded > destination.Length) + { + bytesWritten = 0; + return false; + } + + errorCode = Interop.NCrypt.NCryptExportKey( + keyHandle, + IntPtr.Zero, + Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, + ref desc, + ref MemoryMarshal.GetReference(destination), + destination.Length, + out numBytesNeeded, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + if (allocate && numBytesNeeded != destination.Length) + { + byte[] trimmed = new byte[numBytesNeeded]; + destination.Slice(0, numBytesNeeded).CopyTo(trimmed); + Array.Clear(allocated!, 0, numBytesNeeded); + allocated = trimmed; + } + + bytesWritten = numBytesNeeded; + return true; + } + } + } + + [SupportedOSPlatform("windows")] + internal static CngKey Duplicate(this SafeNCryptKeyHandle keyHandle, bool isEphemeral) + { + return CngKey.Open(keyHandle, isEphemeral ? CngKeyHandleOpenOptions.EphemeralKey : CngKeyHandleOpenOptions.None); + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/CngPkcs8.Shared.cs b/src/libraries/Common/src/System/Security/Cryptography/CngPkcs8.Shared.cs new file mode 100644 index 00000000000000..b23e787d6f50cc --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/CngPkcs8.Shared.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal static partial class CngPkcs8 + { + internal static bool AllowsOnlyEncryptedExport(CngKey key) + { + const CngExportPolicies Exportable = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport; + return (key.ExportPolicy & Exportable) == CngExportPolicies.AllowExport; + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/DSACng.SignVerify.cs b/src/libraries/Common/src/System/Security/Cryptography/DSACng.SignVerify.cs index 6b3c8eabfd4cb2..b2ec1a976fa6d5 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/DSACng.SignVerify.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/DSACng.SignVerify.cs @@ -28,7 +28,7 @@ public override byte[] CreateSignature(byte[] rgbHash) { unsafe { - return CngCommon.SignHash(keyHandle, source, AsymmetricPaddingMode.None, null, source.Length * 2); + return CngHelpers.SignHash(keyHandle, source, AsymmetricPaddingMode.None, null, source.Length * 2); } } } @@ -44,7 +44,7 @@ protected override unsafe bool TryCreateSignatureCore( using (SafeNCryptKeyHandle keyHandle = GetDuplicatedKeyHandle()) { - if (!CngCommon.TrySignHash(keyHandle, source, destination, AsymmetricPaddingMode.None, null, out bytesWritten)) + if (!CngHelpers.TrySignHash(keyHandle, source, destination, AsymmetricPaddingMode.None, null, out bytesWritten)) { bytesWritten = 0; return false; @@ -104,7 +104,7 @@ protected override bool VerifySignatureCore( { unsafe { - return CngCommon.VerifyHash(keyHandle, source, signature, AsymmetricPaddingMode.None, null); + return CngHelpers.VerifyHash(keyHandle, source, signature, AsymmetricPaddingMode.None, null); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/KeyPropertyName.cs b/src/libraries/Common/src/System/Security/Cryptography/KeyPropertyName.cs similarity index 100% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/KeyPropertyName.cs rename to src/libraries/Common/src/System/Security/Cryptography/KeyPropertyName.cs diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.Windows.cs similarity index 79% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.Windows.cs rename to src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.Windows.cs index 31b586abd5fb27..da4fb219956403 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.Windows.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.Formats.Asn1; using System.Security.Cryptography.Asn1; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; using BCRYPT_PQDSA_PADDING_INFO = Interop.BCrypt.BCRYPT_PQDSA_PADDING_INFO; @@ -35,9 +37,11 @@ internal MLDsaCng(CngKey key, bool transferOwnership) _key = key; } + [SupportedOSPlatform("windows")] private static partial MLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey duplicateKey) { ArgumentNullException.ThrowIfNull(key); + ThrowIfNotSupported(); if (key.AlgorithmGroup != CngAlgorithmGroup.MLDsa) { @@ -45,7 +49,13 @@ private static partial MLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey } MLDsaAlgorithm algorithm = AlgorithmFromHandleImpl(key); - duplicateKey = CngAlgorithmCore.Duplicate(key); + +#if SYSTEM_SECURITY_CRYPTOGRAPHY + duplicateKey = CngHelpers.Duplicate(key.HandleNoDuplicate, key.IsEphemeral); +#else + duplicateKey = key.Duplicate(); +#endif + return algorithm; } @@ -64,7 +74,11 @@ private static MLDsaAlgorithm AlgorithmFromHandleNoDuplicate(CngKey key) private static MLDsaAlgorithm AlgorithmFromHandleImpl(CngKey key) { string? parameterSet = - key.Handle.GetPropertyAsString(KeyPropertyName.ParameterSetName, CngPropertyOptions.None); +#if SYSTEM_SECURITY_CRYPTOGRAPHY + key.HandleNoDuplicate.GetPropertyAsString(KeyPropertyName.ParameterSetName, CngPropertyOptions.None); +#else + key.GetPropertyAsString(KeyPropertyName.ParameterSetName, CngPropertyOptions.None); +#endif return parameterSet switch { @@ -91,9 +105,11 @@ public partial CngKey Key } } + /// protected override void ExportMLDsaPublicKeyCore(Span destination) => ExportKey(CngKeyBlobFormat.PQDsaPublicBlob, Algorithm.PublicKeySizeInBytes, destination); + /// protected override void ExportMLDsaPrivateSeedCore(Span destination) { bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(_key); @@ -127,6 +143,7 @@ protected override void ExportMLDsaPrivateSeedCore(Span destination) } } + /// protected override void ExportMLDsaSecretKeyCore(Span destination) { bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(_key); @@ -176,6 +193,7 @@ protected override void ExportMLDsaSecretKeyCore(Span destination) } } + /// protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) { bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(_key); @@ -208,6 +226,7 @@ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out out bytesWritten); } + /// protected override unsafe void SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) { using (SafeNCryptKeyHandle duplicatedHandle = _key.Handle) @@ -227,6 +246,7 @@ protected override unsafe void SignDataCore(ReadOnlySpan data, ReadOnlySpa } } + /// protected override unsafe bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) { using (SafeNCryptKeyHandle duplicatedHandle = _key.Handle) @@ -246,7 +266,8 @@ protected override unsafe bool VerifyDataCore(ReadOnlySpan data, ReadOnlyS } } - internal static MLDsaCng ImportPkcs8PrivateKey(ReadOnlySpan source, out int bytesRead) + [SupportedOSPlatform("windows")] + internal static MLDsaCng ImportPkcs8PrivateKey(byte[] source, out int bytesRead) { int len; @@ -265,32 +286,77 @@ internal static MLDsaCng ImportPkcs8PrivateKey(ReadOnlySpan source, out in } bytesRead = len; - ReadOnlySpan pkcs8Source = source.Slice(0, len); CngKey key; - try - { - key = CngKey.Import(pkcs8Source, CngKeyBlobFormat.Pkcs8PrivateBlob); - } - catch (CryptographicException) +#if SYSTEM_SECURITY_CRYPTOGRAPHY + ReadOnlySpan pkcs8Source = source.AsSpan(0, len); +#else + using (TrimAndTrack(source, bytesRead, out byte[] pkcs8Source)) +#endif { - // TODO: Once Windows moves to new PKCS#8 format, we can remove this conversion. - byte[] newPkcs8Source = MLDsaPkcs8.ConvertToOldChoicelessFormat(pkcs8Source); + try + { + key = CngKey.Import(pkcs8Source, CngKeyBlobFormat.Pkcs8PrivateBlob); + } + catch (CryptographicException) + { + // TODO: Once Windows moves to new PKCS#8 format, we can remove this conversion. + byte[] newPkcs8Source = MLDsaPkcs8.ConvertToOldChoicelessFormat(pkcs8Source); - using (PinAndClear.Track(newPkcs8Source)) + using (PinAndClear.Track(newPkcs8Source)) + { + key = CngKey.Import(newPkcs8Source, CngKeyBlobFormat.Pkcs8PrivateBlob); + } + } + catch (AsnContentException e) { - key = CngKey.Import(newPkcs8Source, CngKeyBlobFormat.Pkcs8PrivateBlob); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } +#if SYSTEM_SECURITY_CRYPTOGRAPHY key.ExportPolicy = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport; +#else + CngKeyExtensions.SetExportPolicy(key, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); +#endif return new MLDsaCng(key, transferOwnership: true); + +#if !SYSTEM_SECURITY_CRYPTOGRAPHY + // Pinning and clearing keyMaterial must be done by the caller. + // The returned PinAndClear only applies to arrays that this method creates. + static PinAndClear? TrimAndTrack(byte[] keyMaterial, int length, out byte[] trimmed) + { + int keyMaterialLength = keyMaterial.Length; + + if (keyMaterialLength == length) + { + trimmed = keyMaterial; + return null; // Tracking original array is up to the caller + } + + // AsSpan will validate length so we won't need to + ReadOnlySpan bytesToCopy = keyMaterial.AsSpan(0, length); + byte[] trimmedKeyMaterial = new byte[length]; + PinAndClear ret = PinAndClear.Track(trimmedKeyMaterial); + + try + { + bytesToCopy.CopyTo(trimmedKeyMaterial); + trimmed = trimmedKeyMaterial; + return ret; + } + catch + { + // This should never happen, but let's be safe and clean up the GC Handle if it does + ret.Dispose(); + Debug.Fail("Copy failed."); + throw; + } + } +#endif } + /// protected override void Dispose(bool disposing) { _key.Dispose(); @@ -316,7 +382,7 @@ private void ExportKey( { Debug.Fail( $"{nameof(blobType)}: {blobType}, " + - $"{nameof(parameterSet)}: {parameterSet}, " + + $"{nameof(parameterSet)}: {parameterSet.ToString()}, " + $"{nameof(keyBytes)}.Length: {keyBytes.Length} / {expectedKeySize}"); throw new CryptographicException(); @@ -361,11 +427,7 @@ private void ExportKeyWithEncryptedOnlyExport(KeySelectorFunc keySelector, MLDsa } finally { - if (newPkcs8 is not null) - { - Array.Clear(newPkcs8); - } - + CryptographicOperations.ZeroMemory(newPkcs8); CryptoPool.Return(pkcs8); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.cs similarity index 67% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.cs rename to src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.cs index 8016decb41048b..f4d7ab54006a25 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaCng.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaCng.cs @@ -2,10 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Runtime.Versioning; namespace System.Security.Cryptography { + /// + /// Provides a Cryptography Next Generation (CNG) implementation of the Module-Lattice-Based Digital Signature Algorithm (ML-DSA). + /// + /// + /// + /// This algorithm is specified by FIPS-204. + /// + /// + /// Developers are encouraged to program against the base class, + /// rather than any specific derived class. + /// The derived classes are intended for interop with the underlying system + /// cryptographic libraries. + /// + /// [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public sealed partial class MLDsaCng : MLDsa { @@ -28,11 +43,23 @@ public sealed partial class MLDsaCng : MLDsa /// [SupportedOSPlatform("windows")] public MLDsaCng(CngKey key) - : base(AlgorithmFromHandle(key, out CngKey duplicateKey)) + : base(AlgorithmFromHandleWithPlatformCheck(key, out CngKey duplicateKey)) { _key = duplicateKey; } + private static MLDsaAlgorithm AlgorithmFromHandleWithPlatformCheck(CngKey key, out CngKey duplicateKey) + { +#if !NETFRAMEWORK + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException(); + } +#endif + + return AlgorithmFromHandle(key, out duplicateKey); + } + private static partial MLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey duplicateKey); /// diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs similarity index 62% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs rename to src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs index 134408d540fa91..55b81de4939e83 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.CreateCng.cs @@ -1,10 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.Versioning; + namespace System.Security.Cryptography { internal sealed partial class MLDsaImplementation { + [SupportedOSPlatform("windows")] internal CngKey CreateEphemeralCng() { string bcryptBlobType = @@ -17,19 +20,20 @@ internal CngKey CreateEphemeralCng() _hasSeed ? CngKeyBlobFormat.PQDsaPrivateSeedBlob : CngKeyBlobFormat.PQDsaPublicBlob; - ArraySegment keyBlob = Interop.BCrypt.BCryptExportKey(_key, bcryptBlobType); - CngKey key; - - try - { - key = CngKey.Import(keyBlob, cngBlobFormat); - } - finally - { - CryptoPool.Return(keyBlob); - } + CngKey key = Interop.BCrypt.BCryptExportKey( + _key, + bcryptBlobType, +#if SYSTEM_SECURITY_CRYPTOGRAPHY + (ReadOnlySpan keyMaterial) => CngKey.Import(keyMaterial, cngBlobFormat)); +#else + (byte[] keyMaterial) => CngKey.Import(keyMaterial, cngBlobFormat)); +#endif +#if SYSTEM_SECURITY_CRYPTOGRAPHY key.ExportPolicy = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport; +#else + key.SetExportPolicy(CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); +#endif return key; } } diff --git a/src/libraries/System.Security.Cryptography/tests/MLDsaCngTests.Windows.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.Windows.cs similarity index 77% rename from src/libraries/System.Security.Cryptography/tests/MLDsaCngTests.Windows.cs rename to src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.Windows.cs index c10a91cac36d97..86d981dcff79a5 100644 --- a/src/libraries/System.Security.Cryptography/tests/MLDsaCngTests.Windows.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.Windows.cs @@ -8,38 +8,47 @@ namespace System.Security.Cryptography.Tests { [ConditionalClass(typeof(MLDsa), nameof(MLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] public sealed class MLDsaCngTests_AllowPlaintextExport : MLDsaTestsBase { - protected override MLDsaCng GenerateKey(MLDsaAlgorithm algorithm) => + protected override MLDsa GenerateKey(MLDsaAlgorithm algorithm) => MLDsaTestHelpers.GenerateKey(algorithm, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); - protected override MLDsaCng ImportPrivateSeed(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportPrivateSeed(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportPrivateSeed(algorithm, source, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); - protected override MLDsaCng ImportSecretKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportSecretKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportSecretKey(algorithm, source, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); - protected override MLDsaCng ImportPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportPublicKey(algorithm, source); + + protected override void AssertExportPkcs8FromPublicKey(Action export) => + MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(export); } [ConditionalClass(typeof(MLDsa), nameof(MLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] public sealed class MLDsaCngTests_AllowExport : MLDsaTestsBase { - protected override MLDsaCng GenerateKey(MLDsaAlgorithm algorithm) => + protected override MLDsa GenerateKey(MLDsaAlgorithm algorithm) => MLDsaTestHelpers.GenerateKey(algorithm, CngExportPolicies.AllowExport); - protected override MLDsaCng ImportPrivateSeed(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportPrivateSeed(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportPrivateSeed(algorithm, source, CngExportPolicies.AllowExport); - protected override MLDsaCng ImportSecretKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportSecretKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportSecretKey(algorithm, source, CngExportPolicies.AllowExport); - protected override MLDsaCng ImportPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => + protected override MLDsa ImportPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) => MLDsaTestHelpers.ImportPublicKey(algorithm, source); + + protected override void AssertExportPkcs8FromPublicKey(Action export) => + MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(export); } [ConditionalClass(typeof(MLDsa), nameof(MLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] public sealed class MLDsaCngTests { [Theory] @@ -48,14 +57,16 @@ public void ImportPrivateKey_NoExportFlag(MLDsaKeyInfo info) { using MLDsa mldsa = MLDsaTestHelpers.ImportSecretKey(info.Algorithm, info.SecretKey, CngExportPolicies.None); - MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => - AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + MLDsaTestHelpers.AssertExportMLDsaPublicKey( + export => AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaSecretKey(export => - AssertExtensions.ThrowsContains(() => export(mldsa), "The requested operation is not supported")); + MLDsaTestHelpers.AssertExportMLDsaSecretKey( + export => Assert.Throws(() => export(mldsa)), + export => MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(() => export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export => - AssertExtensions.ThrowsContains(() => export(mldsa), "The requested operation is not supported")); + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed( + export => Assert.Throws(() => export(mldsa)), + export => MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(() => export(mldsa))); byte[] signature = new byte[info.Algorithm.SignatureSizeInBytes]; mldsa.SignData("test"u8, signature); @@ -68,14 +79,16 @@ public void ImportPrivateSeed_NoExportFlag(MLDsaKeyInfo info) { using MLDsa mldsa = MLDsaTestHelpers.ImportPrivateSeed(info.Algorithm, info.PrivateSeed, CngExportPolicies.None); - MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => - AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + MLDsaTestHelpers.AssertExportMLDsaPublicKey( + export => AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaSecretKey(export => - AssertExtensions.ThrowsContains(() => export(mldsa), "The requested operation is not supported")); + MLDsaTestHelpers.AssertExportMLDsaSecretKey( + export => Assert.Throws(() => export(mldsa)), + export => MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(() => export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export => - AssertExtensions.ThrowsContains(() => export(mldsa), "The requested operation is not supported")); + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed( + export => Assert.Throws(() => export(mldsa)), + export => MLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(() => export(mldsa))); byte[] signature = new byte[info.Algorithm.SignatureSizeInBytes]; mldsa.SignData("test"u8, signature); @@ -180,12 +193,6 @@ public void ImportSecretKey_Persisted() } } - [Fact] - public void MLDsaCng_Ctor_ArgValidation() - { - AssertExtensions.Throws("key", static () => new MLDsaCng(null)); - } - [Fact] public void MLDsaCng_WrongAlgorithm() { diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.cs new file mode 100644 index 00000000000000..fa243706048d8d --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaCngTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public sealed class MLDsaCngTests_AllPlatforms + { + [Fact] + public void MLDsaCng_Ctor_ArgValidation() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + AssertExtensions.Throws("key", static () => new MLDsaCng(null)); + } + else + { + Assert.Throws(() => new MLDsaCng(null)); + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs index e3c7ffba61a31c..5800ef812b767d 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs @@ -23,6 +23,8 @@ internal static partial class MLDsaTestHelpers // DER encoding of ASN.1 BitString "foo" internal static readonly ReadOnlyMemory s_derBitStringFoo = new byte[] { 0x03, 0x04, 0x00, 0x66, 0x6f, 0x6f }; + private const int NTE_NOT_SUPPORTED = unchecked((int)0x80090029); + internal static void VerifyDisposed(MLDsa mldsa) { PbeParameters pbeParams = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 10); @@ -463,6 +465,15 @@ internal static byte[] DoTryUntilDone(TryExportFunc func) return buffer.AsSpan(0, written).ToArray(); } + // CryptographicException can only have both HRESULT and Message set starting in .NET Core 3.0+. + // To work around this, the product code throws an exception derived from CryptographicException + // that has both set. This assert checks for that instead. + internal static void AssertThrowsCryptographicExceptionWithHResult(Action export) + { + CryptographicException ce = Assert.ThrowsAny(export); + Assert.Equal(NTE_NOT_SUPPORTED, ce.HResult); + } + internal static CngProperty GetCngProperty(MLDsaAlgorithm algorithm) { string parameterSetValue = algorithm.Name switch diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs index 9304d5cb742a8b..2675718198b45c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs @@ -222,20 +222,25 @@ public void NistImportSecretKeyVerifyExportsAndSignature(MLDsaNistTestCase testC Assert.Equal(testCase.ShouldPass, mldsa.VerifyData(testCase.Message, testCase.Signature, testCase.Context)); } + protected virtual void AssertExportPkcs8FromPublicKey(Action export) => + Assert.Throws(export); + [Theory] [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] public void ImportPublicKey_Export(MLDsaKeyInfo info) { using MLDsa mldsa = ImportPublicKey(info.Algorithm, info.PublicKey); - MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => - AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + MLDsaTestHelpers.AssertExportMLDsaPublicKey( + export => AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaSecretKey(export => - Assert.Throws(() => export(mldsa))); + MLDsaTestHelpers.AssertExportMLDsaSecretKey( + export => Assert.Throws(() => export(mldsa)), + export => AssertExportPkcs8FromPublicKey(() => export(mldsa))); - MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export => - Assert.Throws(() => export(mldsa))); + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed( + export => Assert.Throws(() => export(mldsa)), + export => AssertExportPkcs8FromPublicKey(() => export(mldsa))); } [Theory] diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs index 2c6b1abe322f53..26241c1ec9bd0b 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.Forwards.cs @@ -14,6 +14,7 @@ #if NET10_0_OR_GREATER [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.MLDsa))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.MLDsaAlgorithm))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.MLDsaCng))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.MLKem))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.MLKemAlgorithm))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.SlhDsa))] 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 2a334bb514612b..652ea62ccc0114 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -419,6 +419,45 @@ Link="Common\System\Security\Cryptography\MLDsaAlgorithm.cs" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index 3553714adbac1e..09c0349974778f 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -138,6 +138,9 @@ Algorithm '{0}' is not supported on this platform. + + Keys used with the MLDsaCng algorithm must have an algorithm group of MLDsa. + The computed authentication tag did not match the input authentication tag. diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngExtensions.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngExtensions.cs new file mode 100644 index 00000000000000..464ab69639f00b --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngExtensions.cs @@ -0,0 +1,74 @@ +// 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.Runtime.Versioning; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography +{ + internal static class CngKeyExtensions + { + [SupportedOSPlatform("windows")] + internal static CngKey Duplicate(this CngKey key) + { + using (SafeNCryptKeyHandle handle = key.Handle) + { + return CngHelpers.Duplicate(handle, key.IsEphemeral); + } + } + + internal static void SetExportPolicy(this CngKey key, CngExportPolicies exportPolicy) + { + using (SafeNCryptKeyHandle keyHandle = key.Handle) + { + CngHelpers.SetExportPolicy(keyHandle, exportPolicy); + } + } + + internal static string? GetPropertyAsString( + this CngKey key, + string propertyName, + CngPropertyOptions options = CngPropertyOptions.None) + { + using (SafeNCryptKeyHandle keyHandle = key.Handle) + { + return CngHelpers.GetPropertyAsString(keyHandle, propertyName, options); + } + } + + internal static bool TryExportKeyBlob( + this CngKey key, + string blobType, + Span destination, + out int bytesWritten) + { + using (SafeNCryptKeyHandle keyHandle = key.Handle) + { + return keyHandle.TryExportKeyBlob(blobType, destination, out bytesWritten); + } + } + + internal static byte[] ExportPkcs8KeyBlob( + this CngKey key, + ReadOnlySpan password, + int kdfCount) + { + using (SafeNCryptKeyHandle keyHandle = key.Handle) + { + bool ret = CngHelpers.ExportPkcs8KeyBlob( + allocate: true, + keyHandle, + password, + kdfCount, + Span.Empty, + out _, + out byte[]? allocated); + + Debug.Assert(ret); + Debug.Assert(allocated != null); // since `allocate: true` + return allocated; + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngHelpers.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngHelpers.cs new file mode 100644 index 00000000000000..fd13b4cd9e77c7 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngHelpers.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal static partial class CngHelpers + { + internal static unsafe void GetRandomBytes(Span buffer) + { + if (buffer.Length > 0) + { + fixed (byte* pbBuffer = buffer) + { + Interop.BCrypt.NTSTATUS status = Interop.BCrypt.BCryptGenRandom(IntPtr.Zero, pbBuffer, buffer.Length, Interop.BCrypt.BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != Interop.BCrypt.NTSTATUS.STATUS_SUCCESS) + throw Interop.BCrypt.CreateCryptographicException(status); + } + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs new file mode 100644 index 00000000000000..4e3bb62b6fa4db --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal static class CngAlgorithmExtensions + { + private static CngAlgorithm? _mlDsaCngAlgorithm; + + extension (CngAlgorithm) + { + internal static CngAlgorithm MLDsa => + _mlDsaCngAlgorithm ??= new CngAlgorithm("ML-DSA"); // BCRYPT_MLDSA_ALGORITHM + } + } + + internal static class CngAlgorithmGroupExtensions + { + private static CngAlgorithmGroup? _mlDsaCngAlgorithmGroup; + + extension (CngAlgorithmGroup) + { + internal static CngAlgorithmGroup MLDsa => + _mlDsaCngAlgorithmGroup ??= new CngAlgorithmGroup("MLDSA"); // NCRYPT_MLDSA_ALGORITHM_GROUP + } + } + + internal static class CngKeyBlobFormatExtensions + { + private static CngKeyBlobFormat? _pqDsaPublicBlob; + private static CngKeyBlobFormat? _pqDsaPrivateBlob; + private static CngKeyBlobFormat? _pqDsaPrivateSeedBlob; + + extension (CngKeyBlobFormat) + { + internal static CngKeyBlobFormat PQDsaPublicBlob => + _pqDsaPublicBlob ??= new CngKeyBlobFormat("PQDSAPUBLICBLOB"); // BCRYPT_PQDSA_PUBLIC_BLOB + + internal static CngKeyBlobFormat PQDsaPrivateBlob => + _pqDsaPrivateBlob ??= new CngKeyBlobFormat("PQDSAPRIVATEBLOB"); // BCRYPT_PQDSA_PRIVATE_BLOB + + internal static CngKeyBlobFormat PQDsaPrivateSeedBlob => + _pqDsaPrivateSeedBlob ??= new CngKeyBlobFormat("PQDSAPRIVATESEEDBLOB"); // BCRYPT_PQDSA_PRIVATE_SEED_BLOB + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PinAndClear.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PinAndClear.cs new file mode 100644 index 00000000000000..b7e6423623949d --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PinAndClear.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Security.Cryptography +{ + internal struct PinAndClear : IDisposable + { + private byte[] _data; + private GCHandle _gcHandle; + + internal static PinAndClear Track(byte[] data) + { + return new PinAndClear + { + _gcHandle = GCHandle.Alloc( + data, + GCHandleType.Pinned), + _data = data, + }; + } + + public void Dispose() + { + _data.AsSpan().Clear(); + _gcHandle.Free(); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index b4be58c63531ea..77f18bea13709e 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -13,6 +13,8 @@ + @@ -134,6 +136,8 @@ Link="CommonTest\System\Security\Cryptography\MLKemNotSupportedTests.cs" /> + + + + + + diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index ad5e3bf4b92b38..a34f01b4e4bebc 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -2,7 +2,7 @@ true - $(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS + $(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS;SYSTEM_SECURITY_CRYPTOGRAPHY $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-android;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent) $(NoWarn);CA5350;CA5351;CA5379;CA5384;SYSLIB0026 @@ -406,6 +406,8 @@ Link="Common\System\Security\Cryptography\MLDsa.cs" /> + - @@ -1823,8 +1824,14 @@ Link="Common\System\Security\Cryptography\AeadCommon.Windows" /> + + + + + + - @@ -1929,10 +1941,7 @@ - - - diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngHelpers.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngHelpers.cs index 926432b2f01f41..a28da92a91a1ed 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngHelpers.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngHelpers.cs @@ -4,9 +4,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; -using Internal.Cryptography; using Microsoft.Win32.SafeHandles; using BCRYPT_RSAKEY_BLOB = Interop.BCrypt.BCRYPT_RSAKEY_BLOB; @@ -15,15 +13,10 @@ namespace System.Security.Cryptography { - internal static class CngHelpers + internal static partial class CngHelpers { private static readonly CngKeyBlobFormat s_cipherKeyBlobFormat = new CngKeyBlobFormat(Interop.NCrypt.NCRYPT_CIPHER_KEY_BLOB); - internal static CryptographicException ToCryptographicException(this Interop.NCrypt.ErrorCode errorCode) - { - return ((int)errorCode).ToCryptographicException(); - } - internal static SafeNCryptProviderHandle OpenStorageProvider(this CngProvider provider) { string providerName = provider.Provider; @@ -39,114 +32,6 @@ internal static SafeNCryptProviderHandle OpenStorageProvider(this CngProvider pr return providerHandle; } - public static void SetExportPolicy(this SafeNCryptKeyHandle keyHandle, CngExportPolicies exportPolicy) - { - unsafe - { - ErrorCode errorCode = Interop.NCrypt.NCryptSetProperty( - keyHandle, - KeyPropertyName.ExportPolicy, - &exportPolicy, - sizeof(CngExportPolicies), - CngPropertyOptions.Persist); - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - } - } - - /// - /// Returns a CNG key property. - /// - /// - /// null - if property not defined on key. - /// throws - for any other type of error. - /// - internal static byte[]? GetProperty(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) - { - Debug.Assert(!ncryptHandle.IsInvalid); - unsafe - { - ErrorCode errorCode = Interop.NCrypt.NCryptGetProperty( - ncryptHandle, - propertyName, - null, - 0, - out int numBytesNeeded, - options); - - if (errorCode == ErrorCode.NTE_NOT_FOUND) - { - return null; - } - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - byte[] propertyValue = new byte[numBytesNeeded]; - - fixed (byte* pPropertyValue = propertyValue) - { - errorCode = Interop.NCrypt.NCryptGetProperty( - ncryptHandle, - propertyName, - pPropertyValue, - propertyValue.Length, - out numBytesNeeded, - options); - } - - if (errorCode == ErrorCode.NTE_NOT_FOUND) - { - return null; - } - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - Array.Resize(ref propertyValue, numBytesNeeded); - return propertyValue; - } - } - - /// - /// Retrieve a well-known CNG string property. (Note: .NET Framework compat: this helper likes to return special - /// values rather than throw exceptions for missing or ill-formatted property values. Only use it for well-known - /// properties that are unlikely to be ill-formatted.) - /// - internal static string? GetPropertyAsString(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) - { - Debug.Assert(!ncryptHandle.IsInvalid); - byte[]? value = GetProperty(ncryptHandle, propertyName, options); - - if (value == null) - { - // .NET Framework compat: return null if key not present. - return null; - } - - if (value.Length == 0) - { - // .NET Framework compat: return empty if property value is 0-length. - return string.Empty; - } - - unsafe - { - fixed (byte* pValue = &value[0]) - { - string valueAsString = Marshal.PtrToStringUni((IntPtr)pValue)!; - return valueAsString; - } - } - } - /// /// Retrieve a well-known CNG dword property. (Note: .NET Framework compat: this helper likes to return special values /// rather than throw exceptions for missing or ill-formatted property values. Only use it for well-known properties that diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngKey.Export.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngKey.Export.cs index 720eeb660de68a..68af97d6bdb0e4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngKey.Export.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngKey.Export.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.InteropServices; -using Internal.Cryptography; -using Microsoft.Win32.SafeHandles; using ErrorCode = Interop.NCrypt.ErrorCode; @@ -41,54 +38,14 @@ internal bool TryExportKeyBlob( Span destination, out int bytesWritten) { - // Sanity check the current bounds - Span empty = default; - - ErrorCode errorCode = Interop.NCrypt.NCryptExportKey( - _keyHandle, - IntPtr.Zero, - blobType, - IntPtr.Zero, - ref MemoryMarshal.GetReference(empty), - empty.Length, - out int written, - 0); - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - if (written > destination.Length) - { - bytesWritten = 0; - return false; - } - - errorCode = Interop.NCrypt.NCryptExportKey( - _keyHandle, - IntPtr.Zero, - blobType, - IntPtr.Zero, - ref MemoryMarshal.GetReference(destination), - destination.Length, - out written, - 0); - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - bytesWritten = written; - return true; + return _keyHandle.TryExportKeyBlob(blobType, destination, out bytesWritten); } internal byte[] ExportPkcs8KeyBlob( ReadOnlySpan password, int kdfCount) { - bool ret = ExportPkcs8KeyBlob( + bool ret = CngHelpers.ExportPkcs8KeyBlob( allocate: true, _keyHandle, password, @@ -108,7 +65,7 @@ internal bool TryExportPkcs8KeyBlob( Span destination, out int bytesWritten) { - return ExportPkcs8KeyBlob( + return CngHelpers.ExportPkcs8KeyBlob( false, _keyHandle, password, @@ -117,119 +74,5 @@ internal bool TryExportPkcs8KeyBlob( out bytesWritten, out _); } - - internal static unsafe bool ExportPkcs8KeyBlob( - bool allocate, - SafeNCryptKeyHandle keyHandle, - ReadOnlySpan password, - int kdfCount, - Span destination, - out int bytesWritten, - out byte[]? allocated) - { - using (SafeUnicodeStringHandle stringHandle = new SafeUnicodeStringHandle(password)) - { - ReadOnlySpan pkcs12TripleDesOidBytes = "1.2.840.113549.1.12.1.3\0"u8; // the Windows APIs for OID strings are ASCII-only - fixed (byte* oidPtr = &MemoryMarshal.GetReference(pkcs12TripleDesOidBytes)) - { - Interop.NCrypt.NCryptBuffer* buffers = stackalloc Interop.NCrypt.NCryptBuffer[3]; - - Interop.NCrypt.PBE_PARAMS pbeParams = default; - Span salt = new Span(pbeParams.rgbSalt, Interop.NCrypt.PBE_PARAMS.RgbSaltSize); - RandomNumberGenerator.Fill(salt); - pbeParams.Params.cbSalt = salt.Length; - pbeParams.Params.iIterations = kdfCount; - - buffers[0] = new Interop.NCrypt.NCryptBuffer - { - BufferType = Interop.NCrypt.BufferType.PkcsSecret, - cbBuffer = checked(2 * (password.Length + 1)), - pvBuffer = stringHandle.DangerousGetHandle(), - }; - - if (buffers[0].pvBuffer == IntPtr.Zero) - { - buffers[0].cbBuffer = 0; - } - - buffers[1] = new Interop.NCrypt.NCryptBuffer - { - BufferType = Interop.NCrypt.BufferType.PkcsAlgOid, - cbBuffer = pkcs12TripleDesOidBytes.Length, - pvBuffer = (IntPtr)oidPtr, - }; - - buffers[2] = new Interop.NCrypt.NCryptBuffer - { - BufferType = Interop.NCrypt.BufferType.PkcsAlgParam, - cbBuffer = sizeof(Interop.NCrypt.PBE_PARAMS), - pvBuffer = (IntPtr)(&pbeParams), - }; - - Interop.NCrypt.NCryptBufferDesc desc = new Interop.NCrypt.NCryptBufferDesc - { - cBuffers = 3, - pBuffers = (IntPtr)buffers, - ulVersion = 0, - }; - - Span empty = default; - - ErrorCode errorCode = Interop.NCrypt.NCryptExportKey( - keyHandle, - IntPtr.Zero, - Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, - ref desc, - ref MemoryMarshal.GetReference(empty), - 0, - out int numBytesNeeded, - 0); - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - allocated = null; - - if (allocate) - { - allocated = new byte[numBytesNeeded]; - destination = allocated; - } - else if (numBytesNeeded > destination.Length) - { - bytesWritten = 0; - return false; - } - - errorCode = Interop.NCrypt.NCryptExportKey( - keyHandle, - IntPtr.Zero, - Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, - ref desc, - ref MemoryMarshal.GetReference(destination), - destination.Length, - out numBytesNeeded, - 0); - - if (errorCode != ErrorCode.ERROR_SUCCESS) - { - throw errorCode.ToCryptographicException(); - } - - if (allocate && numBytesNeeded != destination.Length) - { - byte[] trimmed = new byte[numBytesNeeded]; - destination.Slice(0, numBytesNeeded).CopyTo(trimmed); - Array.Clear(allocated!, 0, numBytesNeeded); - allocated = trimmed; - } - - bytesWritten = numBytesNeeded; - return true; - } - } - } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs index 27d059dfa11e21..7324a310746ba7 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs @@ -43,11 +43,5 @@ private static Pkcs8Response ImportPkcs8( Key = key, }; } - - internal static bool AllowsOnlyEncryptedExport(CngKey key) - { - const CngExportPolicies Exportable = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport; - return (key.ExportPolicy & Exportable) == CngExportPolicies.AllowExport; - } } } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 858b94a47985bb..a8c950e998f3ac 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -355,6 +355,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\ECDsa\ECDsaTestsBase.cs" /> + + -