From bf688124197dcad5885b07e83901d9ba3846f113 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 7 May 2026 12:27:13 -0400 Subject: [PATCH 1/3] Implement X25519DiffieHellmanCng Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interop/Windows/NCrypt/Interop.Keys.cs | 3 + .../NCrypt/Interop.NCryptDeriveKeyMaterial.cs | 40 +++ .../ECCng.ImportExport.NamedCurve.cs | 5 +- .../Cryptography/KeyFormatHelper.Encrypted.cs | 71 +++++ .../Security/Cryptography/KeyFormatHelper.cs | 29 ++- .../Cryptography/X25519WindowsHelpers.cs | 213 +++++++++++++++ .../ref/System.Security.Cryptography.cs | 11 + .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 4 + .../Security/Cryptography/Cng.NotSupported.cs | 38 +++ .../X25519DiffieHellmanCng.Windows.cs | 242 ++++++++++++++++++ .../Cryptography/X25519DiffieHellmanCng.cs | 70 +++++ ...5519DiffieHellmanImplementation.Windows.cs | 226 ++-------------- .../tests/CngHelpers.cs | 15 ++ .../System.Security.Cryptography.Tests.csproj | 20 ++ .../tests/X25519DiffieHellmanBaseTests.cs | 37 ++- .../tests/X25519DiffieHellmanCngTests.cs | 178 +++++++++++++ 17 files changed, 991 insertions(+), 214 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/CngHelpers.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs diff --git a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.Keys.cs b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.Keys.cs index 1b24ea9b4e471c..4cd779d627aeba 100644 --- a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.Keys.cs +++ b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.Keys.cs @@ -31,6 +31,9 @@ internal static partial class NCrypt [LibraryImport(Interop.Libraries.NCrypt, StringMarshalling = StringMarshalling.Utf16)] internal static partial ErrorCode NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, IntPtr pParameterList, ref byte pbOutput, int cbOutput, out int pcbResult, int dwFlags); + [LibraryImport(Interop.Libraries.NCrypt, StringMarshalling = StringMarshalling.Utf16)] + internal static partial ErrorCode NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, IntPtr pParameterList, Span pbOutput, int cbOutput, out int pcbResult, int dwFlags); + [LibraryImport(Interop.Libraries.NCrypt, StringMarshalling = StringMarshalling.Utf16)] internal static partial ErrorCode NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, ref NCryptBufferDesc pParameterList, ref byte pbOutput, int cbOutput, out int pcbResult, int dwFlags); diff --git a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.NCryptDeriveKeyMaterial.cs b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.NCryptDeriveKeyMaterial.cs index 61635c50767301..f3fd2c21c102f6 100644 --- a/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.NCryptDeriveKeyMaterial.cs +++ b/src/libraries/Common/src/Interop/Windows/NCrypt/Interop.NCryptDeriveKeyMaterial.cs @@ -25,6 +25,16 @@ private static partial ErrorCode NCryptDeriveKey( out int pcbResult, SecretAgreementFlags dwFlags); + [LibraryImport(Interop.Libraries.NCrypt, StringMarshalling = StringMarshalling.Utf16)] + private static partial ErrorCode NCryptDeriveKey( + SafeNCryptSecretHandle hSharedSecret, + string pwszKDF, + IntPtr pParameterList, + Span pbDerivedKey, + int cbDerivedKey, + out int pcbResult, + SecretAgreementFlags dwFlags); + /// /// Derive key material from a hash or HMAC KDF /// @@ -257,5 +267,35 @@ internal static byte[] DeriveKeyMaterialTruncate( Array.Reverse(result); return result; } + + internal static bool TryDeriveKeyMaterialTruncate( + SafeNCryptSecretHandle secretAgreement, + SecretAgreementFlags flags, + Span destination, + out int bytesWritten) + { + ErrorCode error = NCryptDeriveKey( + secretAgreement, + BCryptNative.KeyDerivationFunction.Raw, + IntPtr.Zero, + destination, + destination.Length, + out int localWritten, + flags); + + switch (error) + { + case ErrorCode.ERROR_SUCCESS: + destination.Slice(0, localWritten).Reverse(); + bytesWritten = localWritten; + return true; + case ErrorCode c when c.IsBufferTooSmall(): + destination.Clear(); + bytesWritten = 0; + return false; + default: + throw error.ToCryptographicException(); + } + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.NamedCurve.cs b/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.NamedCurve.cs index 52802f1ff30265..cc7f4655f9bf23 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.NamedCurve.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.NamedCurve.cs @@ -145,7 +145,8 @@ internal static SafeNCryptKeyHandle ImportKeyBlob( string blobType, ReadOnlySpan keyBlob, string curveName, - SafeNCryptProviderHandle provider) + SafeNCryptProviderHandle provider, + int flags = 0) { ErrorCode errorCode; SafeNCryptKeyHandle keyHandle; @@ -173,7 +174,7 @@ internal static SafeNCryptKeyHandle ImportKeyBlob( out keyHandle, ref MemoryMarshal.GetReference(keyBlob), keyBlob.Length, - 0); + flags); } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs index 01ae208960261d..0094f67258df07 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs @@ -32,6 +32,29 @@ internal static void ReadEncryptedPkcs8( out ret); } + internal static void ReadEncryptedPkcs8( + string[] validOids, + ReadOnlySpan source, + ReadOnlySpan password, + TState state, + KeyReader keyReader, + out int bytesRead, + out TRet ret) +#if NET + where TState : allows ref struct +#endif + { + ReadEncryptedPkcs8( + validOids, + source, + password, + ReadOnlySpan.Empty, + state, + keyReader, + out bytesRead, + out ret); + } + internal static void ReadEncryptedPkcs8( string[] validOids, ReadOnlySpan source, @@ -50,6 +73,29 @@ internal static void ReadEncryptedPkcs8( out ret); } + internal static void ReadEncryptedPkcs8( + string[] validOids, + ReadOnlySpan source, + ReadOnlySpan passwordBytes, + TState state, + KeyReader keyReader, + out int bytesRead, + out TRet ret) +#if NET + where TState : allows ref struct +#endif + { + ReadEncryptedPkcs8( + validOids, + source, + ReadOnlySpan.Empty, + passwordBytes, + state, + keyReader, + out bytesRead, + out ret); + } + private static void ReadEncryptedPkcs8( string[] validOids, ReadOnlySpan source, @@ -58,6 +104,30 @@ private static void ReadEncryptedPkcs8( KeyReader keyReader, out int bytesRead, out TRet ret) + { + ReadEncryptedPkcs8>( + validOids, + source, + password, + passwordBytes, + keyReader, + static (key, kr, in algId, out ret) => kr(key, algId, out ret), + out bytesRead, + out ret); + } + + private static void ReadEncryptedPkcs8( + string[] validOids, + ReadOnlySpan source, + ReadOnlySpan password, + ReadOnlySpan passwordBytes, + TState state, + KeyReader keyReader, + out int bytesRead, + out TRet ret) +#if NET + where TState : allows ref struct +#endif { int read; ValueEncryptedPrivateKeyInfoAsn epki; @@ -92,6 +162,7 @@ private static void ReadEncryptedPkcs8( ReadPkcs8( validOids, decryptedMemory.Span, + state, keyReader, out int innerRead, out ret); diff --git a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs index 66dc46b83c9ef1..109f25afc5096c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs @@ -13,6 +13,13 @@ internal static partial class KeyFormatHelper { internal delegate void KeyReader(ReadOnlySpan key, in ValueAlgorithmIdentifierAsn algId, out TRet ret); + internal delegate void KeyReader(ReadOnlySpan key, TState state, in ValueAlgorithmIdentifierAsn algId, out TRet ret) +#if NET + where TState : allows ref struct; +#else + ; +#endif + internal static void ReadSubjectPublicKeyInfo( string[] validOids, ReadOnlySpan source, @@ -78,6 +85,26 @@ internal static void ReadPkcs8( KeyReader keyReader, out int bytesRead, out TRet ret) + { + ReadPkcs8>( + validOids, + source, + keyReader, + static (key, kr, in algId, out ret) => kr(key, algId, out ret), + out bytesRead, + out ret); + } + + internal static void ReadPkcs8( + string[] validOids, + ReadOnlySpan source, + TState state, + KeyReader keyReader, + out int bytesRead, + out TRet ret) +#if NET + where TState : allows ref struct +#endif { try { @@ -91,7 +118,7 @@ internal static void ReadPkcs8( } // Fails if there are unconsumed bytes. - keyReader(privateKeyInfo.PrivateKey, privateKeyInfo.PrivateKeyAlgorithm, out ret); + keyReader(privateKeyInfo.PrivateKey, state, privateKeyInfo.PrivateKeyAlgorithm, out ret); bytesRead = read; } catch (AsnContentException e) diff --git a/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs new file mode 100644 index 00000000000000..775df71729ea7d --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs @@ -0,0 +1,213 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal static class X25519WindowsHelpers + { + // https://learn.microsoft.com/en-us/windows/win32/seccng/cng-named-elliptic-curves + internal const string BCRYPT_ECC_CURVE_25519 = "curve25519"; + private const int PublicKeySizeInBytes = X25519DiffieHellman.PublicKeySizeInBytes; + private const int ElementSize = 32; + + // p = 2^255 - 19 in little-endian + private static ReadOnlySpan FieldPrime => + [ + 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, + ]; + + internal static void ExportKey(ReadOnlySpan exported, bool privateKey, Span destination) + { + Interop.BCrypt.KeyBlobMagicNumber expectedMagicNumber = privateKey ? + Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC : + Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC; + + unsafe + { + int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB); + + // For private key we expect three parameters (x, y, d) and for public keys we expect two (x, y). + if (exported.Length < blobHeaderSize + ElementSize * (privateKey ? 3 : 2)) + { + throw new CryptographicException(); + } + + fixed (byte* pExportedSpan = exported) + { + Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pExportedSpan; + + if (blob->cbKey != ElementSize || blob->Magic != expectedMagicNumber) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + // x + ReadOnlySpan y = new(pExportedSpan + blobHeaderSize + ElementSize, ElementSize); + // d + + // y shouldn't have a value. + if (y.IndexOfAnyExcept((byte)0) >= 0) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + if (privateKey) + { + ReadOnlySpan d = new(pExportedSpan + blobHeaderSize + ElementSize * 2, ElementSize); + d.CopyTo(destination); + } + else + { + ReadOnlySpan x = new(pExportedSpan + blobHeaderSize, ElementSize); + x.CopyTo(destination); + } + } + } + } + + internal static CryptoPoolLease CreateCngBlob(ReadOnlySpan key, bool privateKey, out byte preservation) + { + Interop.BCrypt.KeyBlobMagicNumber magicNumber = privateKey ? + Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC : + Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC; + + unsafe + { + int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB); + int requiredBufferSize = blobHeaderSize + ElementSize * 2; // blob + X, Y + + if (privateKey) + { + requiredBufferSize += ElementSize; // d + } + + CryptoPoolLease lease = CryptoPoolLease.Rent(requiredBufferSize, skipClear: !privateKey); + lease.Span.Clear(); + + fixed (byte* pBlobHeader = lease.Span) + { + Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pBlobHeader; + blob->Magic = magicNumber; + blob->cbKey = ElementSize; + } + + if (privateKey) + { + Span destination = lease.Span.Slice(blobHeaderSize + ElementSize * 2, ElementSize); + key.CopyTo(destination); + preservation = FixupPrivateScalar(destination); + } + else + { + Span destination = lease.Span.Slice(blobHeaderSize, ElementSize); + key.CopyTo(destination); + preservation = 0; + } + + return lease; + } + } + + internal static byte FixupPrivateScalar(Span bytes) + { + byte preservation = (byte)(bytes[0] & 0b111 | bytes[^1] & 0b11000000); + + // From RFC 7748: + // For X25519, in + // order to decode 32 random bytes as an integer scalar, set the three + // least significant bits of the first byte and the most significant bit + // of the last to zero, set the second most significant bit of the last + // byte to 1 and, finally, decode as little-endian. + // + // Most other X25519 implementations do this for you when importing a private key. CNG does not, so we + // apply the scalar fixup here. + // + // If we import a key that requires us to modify it, we store the modified bits in a byte. This byte does + // not effectively contain any private key material since these bits are always coerced. However we want + // keys to roundtrip correctly. + bytes[0] &= 0b11111000; + bytes[^1] &= 0b01111111; + bytes[^1] |= 0b01000000; + return preservation; + } + + internal static void RefixPrivateScalar(Span bytes, byte preservation) + { + bytes[0] = (byte)((preservation & 0b111) | (bytes[0] & 0b11111000)); + bytes[^1] = (byte)((preservation & 0b11000000) | (bytes[^1] & 0b00111111)); + } + + internal static bool ReducePublicKey(ReadOnlySpan publicKey, Span reduced) + { + Debug.Assert(publicKey.Length == PublicKeySizeInBytes); + Debug.Assert(reduced.Length == PublicKeySizeInBytes); + + // RFC 7748 Section 5: "implementations of X25519 MUST mask the most significant + // bit in the final byte" and "Implementations MUST accept non-canonical values and + // process them as if they had been reduced modulo the field prime." + // + // CNG rejects non-canonical u-coordinates (values >= p = 2^255 - 19) and does not + // mask the high bit. We handle both by masking the high bit then if the value is + // non-canonical, subtract p to reduce it. Since all values are < 2^255 after + // masking and p = 2^255 - 19, a single subtraction suffices. + publicKey.CopyTo(reduced); + reduced[^1] &= 0x7F; + + bool requiredReduction = false; + + if (IsNonCanonicalPublicKey(reduced)) + { + requiredReduction = true; + ReducePublicKey(reduced); + } + else if ((publicKey[^1] & 0x80) != 0) + { + requiredReduction = true; + } + + return requiredReduction; + } + + private static bool IsNonCanonicalPublicKey(ReadOnlySpan key) + { + Debug.Assert(key.Length == PublicKeySizeInBytes); + Debug.Assert((key[^1] & 0x80) == 0); + + // Compare key >= p (little-endian). Since key < 2^255 (high bit masked) + // and p = 2^255 - 19, a non-canonical value is in [p, 2^255 - 1]. + // Compare from most significant byte to least significant. + for (int i = PublicKeySizeInBytes - 1; i >= 0; i--) + { + if (key[i] > FieldPrime[i]) + return true; + if (key[i] < FieldPrime[i]) + return false; + } + + // key == p, which is also non-canonical (reduces to 0) + return true; + } + + private static void ReducePublicKey(Span key) + { + Debug.Assert(key.Length == PublicKeySizeInBytes); + + // Subtract p from key. Since we only call this when key >= p and key < 2^255, + // a single subtraction is sufficient: key = key - p. + int borrow = 0; + + for (int i = 0; i < PublicKeySizeInBytes; i++) + { + int diff = key[i] - FieldPrime[i] - borrow; + key[i] = (byte)diff; + borrow = (diff < 0) ? 1 : 0; + } + + Debug.Assert(borrow == 0); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 48693c2beaa2ae..ebd3d646bdbc63 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3494,6 +3494,17 @@ public void ExportPublicKey(System.Span destination) { } protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten); public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } } + public sealed partial class X25519DiffieHellmanCng : System.Security.Cryptography.X25519DiffieHellman + { + [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] + public X25519DiffieHellmanCng(System.Security.Cryptography.CngKey key) { } + protected override void DeriveRawSecretAgreementCore(System.Security.Cryptography.X25519DiffieHellman otherParty, System.Span destination) { } + protected override void Dispose(bool disposing) { } + protected override void ExportPrivateKeyCore(System.Span destination) { } + protected override void ExportPublicKeyCore(System.Span destination) { } + public System.Security.Cryptography.CngKey GetKey() { throw null; } + protected override bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten) { throw null; } + } public sealed partial class X25519DiffieHellmanOpenSsl : System.Security.Cryptography.X25519DiffieHellman { [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 4086d30920f071..9987d8ea2ea082 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -255,6 +255,9 @@ Keys used with the MLKemCng algorithm must have an algorithm group of MLKem. + + Keys used with the X25519DiffieHellmanCng algorithm must have an algorithm group of ECDiffieHellman using curve25519. + Keys used with the RSACng algorithm must have an algorithm group of RSA. 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 e4bdb62e1c1dd0..344b9f60f008ed 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -688,6 +688,7 @@ + @@ -1948,6 +1949,8 @@ Link="Common\System\Security\Cryptography\RSACng.SignVerify.cs" /> + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs index 7148d243f1c220..5c5bd54dccdb08 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs @@ -487,4 +487,42 @@ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); } + + public sealed partial class X25519DiffieHellmanCng : X25519DiffieHellman + { + public partial X25519DiffieHellmanCng(CngKey key) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + public partial CngKey GetKey() + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + protected override partial void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + protected override partial void ExportPrivateKeyCore(Span destination) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + protected override partial void ExportPublicKeyCore(Span destination) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + protected override partial bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + + protected override partial void Dispose(bool disposing) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); + } + } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs new file mode 100644 index 00000000000000..d26dc8bfcdd8b4 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs @@ -0,0 +1,242 @@ +// 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.Formats.Asn1; +using System.Runtime.Versioning; +using System.Security.Cryptography.Asn1; +using Microsoft.Win32.SafeHandles; +using Internal.Cryptography; + +using ErrorCode = Interop.NCrypt.ErrorCode; + +namespace System.Security.Cryptography +{ + public sealed partial class X25519DiffieHellmanCng : X25519DiffieHellman + { + private CngKey _key; + private static readonly string[] s_eccKeyOid = [Oids.EcPublicKey]; + + public partial X25519DiffieHellmanCng(CngKey key) + { + Debug.Assert(Helpers.IsOSPlatformWindows); + ArgumentNullException.ThrowIfNull(key); + ThrowIfNotSupported(); + + if (key.AlgorithmGroup != CngAlgorithmGroup.ECDiffieHellman || + key.GetCurveName(out _) != X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519) + { + throw new ArgumentException(SR.Cryptography_ArgX25519RequiresX25519Key, nameof(key)); + } + + _key = CngHelpers.Duplicate(key.HandleNoDuplicate, key.IsEphemeral); + } + + public partial CngKey GetKey() + { + ThrowIfDisposed(); + return CngHelpers.Duplicate(_key.HandleNoDuplicate, _key.IsEphemeral); + } + + protected override partial void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + bool success; + int bytesWritten; + + // Assume that if key agreement is being done between two different X25519DiffieHellmanCng instances then + // the Provider is the same. It's the caller's responsibility to ensure the providers match. + if (otherParty is X25519DiffieHellmanCng x25519Cng) + { + using (SafeNCryptSecretHandle secretAgreement = Interop.NCrypt.DeriveSecretAgreement( + _key.HandleNoDuplicate, + x25519Cng._key.HandleNoDuplicate)) + { + success = Interop.NCrypt.TryDeriveKeyMaterialTruncate( + secretAgreement, + Interop.NCrypt.SecretAgreementFlags.None, + destination, + out bytesWritten); + } + } + else + { + Span publicKeyBytes = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(publicKeyBytes); + Span reducedPublicKey = stackalloc byte[PublicKeySizeInBytes]; + X25519WindowsHelpers.ReducePublicKey(publicKeyBytes, reducedPublicKey); + + using (CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(reducedPublicKey, privateKey: false, out _)) + { + // CNG does not permit cross-provider key agreements. Import the public key in to the same provider + // as the current key. + CngProvider provider = _key.Provider ?? CngProvider.MicrosoftSoftwareKeyStorageProvider; + + using (SafeNCryptProviderHandle providerHandle = provider.OpenStorageProvider()) + { + int flags = 0; + + if (provider == CngProvider.MicrosoftSoftwareKeyStorageProvider) + { + const int NCRYPT_NO_KEY_VALIDATION = (int)Interop.BCrypt.BCryptImportKeyPairFlags.BCRYPT_NO_KEY_VALIDATION; + flags |= NCRYPT_NO_KEY_VALIDATION; + } + + SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob( + CngKeyBlobFormat.EccPublicBlob.Format, + lease.Span, + X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519, + providerHandle, + flags); + + using (keyHandle) + using (SafeNCryptSecretHandle secretAgreement = Interop.NCrypt.DeriveSecretAgreement( + _key.HandleNoDuplicate, + keyHandle)) + { + success = Interop.NCrypt.TryDeriveKeyMaterialTruncate( + secretAgreement, + Interop.NCrypt.SecretAgreementFlags.None, + destination, + out bytesWritten); + } + } + } + } + + if (!success || bytesWritten != SecretAgreementSizeInBytes) + { + // The destination should have already been pre-sized to a well-behaving X25519 implementation + // but a provider could be implemented incorrectly. Zero whatever was written since it is incorrect. + CryptographicOperations.ZeroMemory(destination); + throw new CryptographicException(); + } + + // If the CngKey was created with NCRYPT_NO_KEY_VALIDATION then low-order public keys can be imported. + // Block low-order key agreements that result in an all-zero secret. + if (CryptographicOperations.FixedTimeEquals(destination, 0)) + { + throw new CryptographicException(); + } + } + + protected override partial void ExportPublicKeyCore(Span destination) + { + Debug.Assert(destination.Length == PublicKeySizeInBytes); + ExportKeyFromBlob(_key, privateKey: false, destination); + } + + protected override partial void ExportPrivateKeyCore(Span destination) + { + Debug.Assert(destination.Length == PrivateKeySizeInBytes); + + if (CngPkcs8.AllowsOnlyEncryptedExport(_key)) + { + ExportPrivateKeyFromEncryptedPkcs8(destination); + } + else + { + ExportKeyFromBlob(_key, privateKey: true, destination); + } + } + + protected override partial bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + // This will use ExportPrivateKeyCore which, in turn, will handle encrypted-only exports + // so we don't handle it here. We cannot use the PKCS#8 that CNG gives us - it does not understand + // RFC 8410 OIDs so X25519 keys are exported with explicit parameters. Since the PKCS#8 would need to be + // re-assembled anyway, let it use the existing exporter instead. + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override partial void Dispose(bool disposing) + { + if (disposing) + { + _key?.Dispose(); + _key = null!; + } + + base.Dispose(disposing); + } + + private static void ExportKeyFromBlob(CngKey key, bool privateKey, Span destination) + { + int numBytesNeeded; + string format = privateKey ? + CngKeyBlobFormat.EccPrivateBlob.Format : + CngKeyBlobFormat.EccPublicBlob.Format; + + ErrorCode errorCode = Interop.NCrypt.NCryptExportKey( + key.HandleNoDuplicate, + IntPtr.Zero, + format, + IntPtr.Zero, + null, + 0, + out numBytesNeeded, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + using (CryptoPoolLease lease = CryptoPoolLease.Rent(numBytesNeeded, skipClear: !privateKey)) + { + errorCode = Interop.NCrypt.NCryptExportKey( + key.HandleNoDuplicate, + IntPtr.Zero, + format, + IntPtr.Zero, + lease.Span, + lease.Span.Length, + out numBytesNeeded, + 0); + + if (errorCode != ErrorCode.ERROR_SUCCESS) + { + throw errorCode.ToCryptographicException(); + } + + X25519WindowsHelpers.ExportKey(lease.Span.Slice(0, numBytesNeeded), privateKey, destination); + } + } + + private void ExportPrivateKeyFromEncryptedPkcs8(Span destination) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = _key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1); + + using (PinAndClear.Track(exported)) + { + KeyFormatHelper.ReadEncryptedPkcs8( + s_eccKeyOid, + exported, + TemporaryExportPassword, + destination, + static (ReadOnlySpan key, Span destination, in ValueAlgorithmIdentifierAsn algId, out object? ret) => + { + if (algId.Algorithm != Oids.EcPublicKey) + { + throw new CryptographicException(SR.Cryptography_NotValidPrivateKey); + } + + // X25519 on Windows exports currently exports X25519 keys as an explicit curve. However + // since the constructor validates that the CngKey curve is curve25519 we can be reasonably sure + // that the key is for X25519, so we don't validate the parameters. + ValueECPrivateKey.Decode(key, AsnEncodingRules.BER, out ValueECPrivateKey ecKey); + + if (ecKey.PrivateKey.Length != PrivateKeySizeInBytes) + { + throw new CryptographicException(SR.Cryptography_NotValidPrivateKey); + } + + ecKey.PrivateKey.CopyTo(destination); + ret = (object?)null; + }, + out _, + out _); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs new file mode 100644 index 00000000000000..af041f7442d31a --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs @@ -0,0 +1,70 @@ +// 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 +{ + /// + /// Provides a Cryptography Next Generation (CNG) implementation of X25519 Diffie-Hellman. + /// + /// + /// + /// This algorithm is specified by RFC 7748. + /// + /// + /// Developers are encouraged to program against the X25519DiffieHellman base class, + /// rather than any specific derived class. + /// The derived classes are intended for interop with the underlying system + /// cryptographic libraries. + /// + /// + public sealed partial class X25519DiffieHellmanCng : X25519DiffieHellman + { + /// + /// Initializes a new instance of the class + /// by using the specified . + /// + /// + /// The key that will be used as input to the cryptographic operations performed by the current object. + /// + /// + /// is . + /// + /// + /// does not specify a X25519 Diffie-Hellman key. + /// + /// + /// Cryptography Next Generation (CNG) classes are not supported on this system. + /// + [SupportedOSPlatform("windows")] + public partial X25519DiffieHellmanCng(CngKey key); + + /// + /// Gets a new representing the key used by the current instance. + /// + /// + /// This instance has been disposed. + /// + /// + /// This object is not the same as the one passed to , + /// if that constructor was used. However, it will point to the same CNG key. + /// + public partial CngKey GetKey(); + + /// + protected override partial void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination); + + /// + protected override partial void ExportPublicKeyCore(Span destination); + + /// + protected override partial void ExportPrivateKeyCore(Span destination); + + /// + protected override partial bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten); + + /// + protected override partial void Dispose(bool disposing); + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs index 1660da4d802d2c..f378fe7f56bd1a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs @@ -13,17 +13,8 @@ namespace System.Security.Cryptography { internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman { - // https://learn.microsoft.com/en-us/windows/win32/seccng/cng-named-elliptic-curves - private const string BCRYPT_ECC_CURVE_25519 = "curve25519"; private static readonly SafeBCryptAlgorithmHandle? s_algHandle = OpenAlgorithmHandle(); - // p = 2^255 - 19 in little-endian - private static ReadOnlySpan FieldPrime => - [ - 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, - ]; - private readonly SafeBCryptKeyHandle _key; private readonly bool _hasPrivate; private readonly byte _privatePreservation; @@ -65,7 +56,7 @@ protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherPa Span publicKeyBytes = stackalloc byte[PublicKeySizeInBytes]; otherParty.ExportPublicKey(publicKeyBytes); - using (X25519DiffieHellmanImplementation otherPartyImplementation = X25519DiffieHellmanImplementation.ImportPublicKeyImpl(publicKeyBytes)) + using (X25519DiffieHellmanImplementation otherPartyImplementation = ImportPublicKeyImpl(publicKeyBytes)) using (SafeBCryptSecretHandle secret = Interop.BCrypt.BCryptSecretAgreement(_key, otherPartyImplementation._key)) { Interop.BCrypt.BCryptDeriveKey( @@ -103,7 +94,7 @@ protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherPa protected override void ExportPrivateKeyCore(Span destination) { ExportKey(true, destination); - RefixPrivateScalar(destination, _privatePreservation); + X25519WindowsHelpers.RefixPrivateScalar(destination, _privatePreservation); } protected override void ExportPublicKeyCore(Span destination) @@ -161,88 +152,32 @@ internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlyS internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) { - // RFC 7748 Section 5: "implementations of X25519 MUST mask the most significant - // bit in the final byte" and "Implementations MUST accept non-canonical values and - // process them as if they had been reduced modulo the field prime." - // - // CNG rejects non-canonical u-coordinates (values >= p = 2^255 - 19) and does not - // mask the high bit. We handle both by masking the high bit then if the value is - // non-canonical, subtract p to reduce it. Since all values are < 2^255 after - // masking and p = 2^255 - 19, a single subtraction suffices. - Span reduced = stackalloc byte[PublicKeySizeInBytes]; - source.CopyTo(reduced); - reduced[^1] &= 0x7F; - - byte[]? originalPublicKey = null; - - if (IsNonCanonicalPublicKey(reduced)) - { - originalPublicKey = source.ToArray(); - ReducePublicKey(reduced); - } - else if ((source[^1] & 0x80) != 0) - { - // The value is canonical but has the high bit set. CNG doesn't mask it, - // so we need to clear it before import and preserve the original for export. - originalPublicKey = source.ToArray(); - } + Span reducedPublicKey = stackalloc byte[PublicKeySizeInBytes]; + bool requiredReduction = X25519WindowsHelpers.ReducePublicKey(source, reducedPublicKey); + + SafeBCryptKeyHandle key = ImportKey(false, reducedPublicKey, out _); - SafeBCryptKeyHandle key = ImportKey(false, reduced, out _); Debug.Assert(!key.IsInvalid); - return new X25519DiffieHellmanImplementation(key, hasPrivate: false, privatePreservation: 0, originalPublicKey); + return new X25519DiffieHellmanImplementation( + key, + hasPrivate: false, + privatePreservation: 0, + requiredReduction ? source.ToArray() : null); } + + private void ExportKey(bool privateKey, Span destination) { string blobType = privateKey ? Interop.BCrypt.KeyBlobType.BCRYPT_ECCPRIVATE_BLOB : Interop.BCrypt.KeyBlobType.BCRYPT_ECCPUBLIC_BLOB; - Interop.BCrypt.KeyBlobMagicNumber expectedMagicNumber = privateKey ? - Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC : - Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC; - ArraySegment key = Interop.BCrypt.BCryptExportKey(_key, blobType); try { - unsafe - { - int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB); - ReadOnlySpan exported = key; - - fixed (byte* pExportedSpan = exported) - { - const int ElementSize = 32; - Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pExportedSpan; - - if (blob->cbKey != ElementSize || blob->Magic != expectedMagicNumber) - { - throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); - } - - // x - ReadOnlySpan y = new(pExportedSpan + blobHeaderSize + ElementSize, ElementSize); - // d - - // y shouldn't have a value. - if (y.IndexOfAnyExcept((byte)0) >= 0) - { - throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); - } - - if (privateKey) - { - ReadOnlySpan d = new(pExportedSpan + blobHeaderSize + ElementSize * 2, ElementSize); - d.CopyTo(destination); - } - else - { - ReadOnlySpan x = new(pExportedSpan + blobHeaderSize, ElementSize); - x.CopyTo(destination); - } - } - } + X25519WindowsHelpers.ExportKey(key, privateKey, destination); } finally { @@ -264,134 +199,17 @@ private static SafeBCryptKeyHandle ImportKey(bool privateKey, ReadOnlySpan Interop.BCrypt.KeyBlobType.BCRYPT_ECCPRIVATE_BLOB : Interop.BCrypt.KeyBlobType.BCRYPT_ECCPUBLIC_BLOB; - Interop.BCrypt.KeyBlobMagicNumber magicNumber = privateKey ? - Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC : - Interop.BCrypt.KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC; - - unsafe + using (CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(key, privateKey, out preservation)) { - int blobHeaderSize = sizeof(Interop.BCrypt.BCRYPT_ECCKEY_BLOB); - const int ElementSize = 32; - int requiredBufferSize = blobHeaderSize + ElementSize * 2; // blob + X, Y - - if (privateKey) - { - requiredBufferSize += ElementSize; // d - } - byte[] rented = CryptoPool.Rent(requiredBufferSize); - Span buffer = rented.AsSpan(0, requiredBufferSize); - buffer.Clear(); - - try - { - fixed (byte* pBlobHeader = buffer) - { - Interop.BCrypt.BCRYPT_ECCKEY_BLOB* blob = (Interop.BCrypt.BCRYPT_ECCKEY_BLOB*)pBlobHeader; - blob->Magic = magicNumber; - blob->cbKey = ElementSize; - } - - if (privateKey) - { - Span destination = buffer.Slice(blobHeaderSize + ElementSize * 2, ElementSize); - key.CopyTo(destination); - preservation = FixupPrivateScalar(destination); - } - else - { - Span destination = buffer.Slice(blobHeaderSize, ElementSize); - key.CopyTo(destination); - preservation = 0; - } - - return Interop.BCrypt.BCryptImportKeyPair( - s_algHandle, - blobType, - buffer, - Interop.BCrypt.BCryptImportKeyPairFlags.BCRYPT_NO_KEY_VALIDATION); - } - finally - { - if (privateKey) - { - CryptoPool.Return(rented); - } - else - { - CryptoPool.Return(rented, clearSize: 0); - } - } + return Interop.BCrypt.BCryptImportKeyPair( + s_algHandle, + blobType, + lease.Span, + Interop.BCrypt.BCryptImportKeyPairFlags.BCRYPT_NO_KEY_VALIDATION); } } - private static byte FixupPrivateScalar(Span bytes) - { - byte preservation = (byte)(bytes[0] & 0b111 | bytes[^1] & 0b11000000); - - // From RFC 7748: - // For X25519, in - // order to decode 32 random bytes as an integer scalar, set the three - // least significant bits of the first byte and the most significant bit - // of the last to zero, set the second most significant bit of the last - // byte to 1 and, finally, decode as little-endian. - // - // Most other X25519 implementations do this for you when importing a private key. CNG does not, so we - // apply the scalar fixup here. - // - // If we import a key that requires us to modify it, we store the modified bits in a byte. This byte does - // not effectively contain any private key material since these bits are always coerced. However we want - // keys to roundtrip correctly. - bytes[0] &= 0b11111000; - bytes[^1] &= 0b01111111; - bytes[^1] |= 0b01000000; - return preservation; - } - - private static void RefixPrivateScalar(Span bytes, byte preservation) - { - bytes[0] = (byte)((preservation & 0b111) | (bytes[0] & 0b11111000)); - bytes[^1] = (byte)((preservation & 0b11000000) | (bytes[^1] & 0b00111111)); - } - - private static bool IsNonCanonicalPublicKey(ReadOnlySpan key) - { - Debug.Assert(key.Length == PublicKeySizeInBytes); - Debug.Assert((key[^1] & 0x80) == 0); - - // Compare key >= p (little-endian). Since key < 2^255 (high bit masked) - // and p = 2^255 - 19, a non-canonical value is in [p, 2^255 - 1]. - // Compare from most significant byte to least significant. - for (int i = PublicKeySizeInBytes - 1; i >= 0; i--) - { - if (key[i] > FieldPrime[i]) - return true; - if (key[i] < FieldPrime[i]) - return false; - } - - // key == p, which is also non-canonical (reduces to 0) - return true; - } - - private static void ReducePublicKey(Span key) - { - Debug.Assert(key.Length == PublicKeySizeInBytes); - - // Subtract p from key. Since we only call this when key >= p and key < 2^255, - // a single subtraction is sufficient: key = key - p. - int borrow = 0; - - for (int i = 0; i < PublicKeySizeInBytes; i++) - { - int diff = key[i] - FieldPrime[i] - borrow; - key[i] = (byte)diff; - borrow = (diff < 0) ? 1 : 0; - } - - Debug.Assert(borrow == 0); - } - private static SafeBCryptAlgorithmHandle? OpenAlgorithmHandle() { NTSTATUS status = Interop.BCrypt.BCryptOpenAlgorithmProvider( @@ -408,13 +226,13 @@ private static void ReducePublicKey(Span key) unsafe { - fixed (char* pbInput = BCRYPT_ECC_CURVE_25519) + fixed (char* pbInput = X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519) { status = Interop.BCrypt.BCryptSetProperty( hAlgorithm, KeyPropertyName.ECCCurveName, pbInput, - ((uint)BCRYPT_ECC_CURVE_25519.Length + 1) * 2, + ((uint)X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519.Length + 1) * 2, 0); } } diff --git a/src/libraries/System.Security.Cryptography/tests/CngHelpers.cs b/src/libraries/System.Security.Cryptography/tests/CngHelpers.cs new file mode 100644 index 00000000000000..056c59e1fde897 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/CngHelpers.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + internal static class CngHelpers + { + public static CryptographicException ToCryptographicException(this Interop.NCrypt.ErrorCode errorCode) + { + return ((int)errorCode).ToCryptographicException(); + } + } +} 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 396242cc2d09ad..f490702df59f7b 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 @@ -693,12 +693,32 @@ + + + + + + + + + + + diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs index 4dc90d5b15952d..d91bb42514237c 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Text; using Test.Cryptography; using Xunit; @@ -17,6 +18,7 @@ public abstract class X25519DiffieHellmanBaseTests public abstract X25519DiffieHellman GenerateKey(); public abstract X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source); public abstract X25519DiffieHellman ImportPublicKey(ReadOnlySpan source); + public virtual bool CanRoundTripKeys => true; // SymCrypt, thus SCOSSL, is stricter about keys it is willing to import. These keys fall in to // two buckets. @@ -219,7 +221,7 @@ public void ExportPkcs8PrivateKey_Roundtrip() AssertExportPkcs8PrivateKey(xdh, pkcs8 => { using X25519DiffieHellman imported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); - AssertExtensions.SequenceEqual( + AssertPrivateKey( X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); }); @@ -247,7 +249,7 @@ public void ExportEncryptedPkcs8PrivateKey_Roundtrip() X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, pkcs8); - AssertExtensions.SequenceEqual( + AssertPrivateKey( X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); }); @@ -321,23 +323,22 @@ public static IEnumerable ExportPkcs8Parameters public void PrivateKey_Roundtrip_UnclampedScalar() { byte[] privateKey = X25519DiffieHellmanTestData.BobPrivateKey; + using X25519DiffieHellman xdh = ImportPrivateKey(privateKey); + AssertPrivateKey(privateKey, xdh.ExportPrivateKey()); - AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.BobPublicKey, xdh.ExportPublicKey()); byte[] pkcs8 = xdh.ExportPkcs8PrivateKey(); using X25519DiffieHellman reimported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); - AssertExtensions.SequenceEqual(privateKey, reimported.ExportPrivateKey()); + AssertPrivateKey(privateKey, reimported.ExportPrivateKey()); } [Fact] public void PrivateKey_Roundtrip_ClampedScalar() { byte[] privateKey = (byte[])X25519DiffieHellmanTestData.AlicePrivateKey.Clone(); - privateKey[0] &= 0b11111000; - privateKey[^1] &= 0b01111111; - privateKey[^1] |= 0b01000000; + ClampPrivateKey(privateKey); using X25519DiffieHellman xdh = ImportPrivateKey(privateKey); AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); @@ -482,6 +483,28 @@ private static byte[] DoTryUntilDone(TryExportFunc func) return buffer.AsSpan(0, written).ToArray(); } + private static void ClampPrivateKey(Span privateKey) + { + Debug.Assert(privateKey.Length == X25519DiffieHellman.PrivateKeySizeInBytes); + privateKey[0] &= 0b11111000; + privateKey[^1] &= 0b01111111; + privateKey[^1] |= 0b01000000; + } + + private void AssertPrivateKey(ReadOnlySpan expectedPrivateKey, ReadOnlySpan actualPrivateKey) + { + if (CanRoundTripKeys) + { + AssertExtensions.SequenceEqual(expectedPrivateKey, actualPrivateKey); + } + else + { + byte[] clampedKey = expectedPrivateKey.ToArray(); + ClampPrivateKey(clampedKey); + AssertExtensions.SequenceEqual(clampedKey, actualPrivateKey); + } + } + /// /// A wrapper around an X25519DiffieHellman instance that is not the platform's /// internal implementation type. This forces the DeriveRawSecretAgreementCore fallback diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs new file mode 100644 index 00000000000000..917b42d52afb54 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Asn1; +using System.Text; +using Microsoft.Win32.SafeHandles; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public sealed class X25519DiffieHellmanExportableCngTests : X25519DiffieHellmanCngTests + { + protected override CngExportPolicies ExportPolicy => CngExportPolicies.AllowExport; + } + + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public sealed class X25519DiffieHellmanPlaintextExportableCngTests : X25519DiffieHellmanCngTests + { + protected override CngExportPolicies ExportPolicy => + CngExportPolicies.AllowExport | + CngExportPolicies.AllowPlaintextExport; + } + + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public static class X25519DiffieHellmanCngContractTests + { + [Fact] + public static void ArgValidation_Ctor_NullKey_Throws() + { + Assert.Throws("key", static () => new X25519DiffieHellmanCng(null)); + } + + [Fact] + public static void ArgValidation_Ctor_WrongAlgorithmGroup() + { + using CngKey key = CngKey.Create(CngAlgorithm.Rsa); + Assert.Throws("key", () => new X25519DiffieHellmanCng(key)); + } + + [Fact] + public static void ArgValidation_Ctor_WrongCurve() + { + using CngKey key = X25519DiffieHellmanCngTests.GenerateCngKey(nameof(ECCurve.NamedCurves.nistP256)); + Assert.Throws("key", () => new X25519DiffieHellmanCng(key)); + } + + [Fact] + public static void GetKey() + { + using CngKey key = X25519DiffieHellmanCngTests.GenerateCngKey(); + + using (X25519DiffieHellmanCng xdhKey = new(key)) + using (CngKey getKey1 = xdhKey.GetKey()) + { + using (CngKey getKey2 = xdhKey.GetKey()) + { + Assert.NotSame(key, getKey1); + Assert.NotSame(getKey1, getKey2); + } + + Assert.Equal(key.Algorithm, getKey1.Algorithm); // Assert.NoThrow on getKey1.Algorithm + } + } + + [Fact] + public static void GetKey_Disposed() + { + using CngKey key = X25519DiffieHellmanCngTests.GenerateCngKey(); + X25519DiffieHellmanCng xdhKey = new(key); + xdhKey.Dispose(); + xdhKey.Dispose(); // No-op + Assert.Throws(() => xdhKey.GetKey()); + } + } + + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public static class X25519DiffieHellmanCngNonExportableTests + { + [Fact] + public static void X25519DiffieHellmanCng_NonExportable_ExportPrivateKeyThrows() + { + using CngKey key = X25519DiffieHellmanCngTests.GenerateCngKey(exportPolicy: CngExportPolicies.None); + using X25519DiffieHellmanCng xdh = new(key); + Assert.Throws(() => xdh.ExportPrivateKey()); + } + + [Fact] + public static void X25519DiffieHellmanCng_NonExportable_ExportPublicKeyAlwaysWorks() + { + using CngKey key = X25519DiffieHellmanCngTests.GenerateCngKey(exportPolicy: CngExportPolicies.None); + using X25519DiffieHellmanCng xdh = new(key); + ReadOnlySpan publicKey = xdh.ExportPublicKey(); + AssertExtensions.TrueExpression(publicKey.IndexOfAnyExcept((byte)0) >= 0); + } + } + + public abstract class X25519DiffieHellmanCngTests : X25519DiffieHellmanBaseTests + { + private const int NCRYPT_NO_KEY_VALIDATION = 0x00000008; + + private static readonly Lazy s_lazyDefaultProviderHandle = new(() => + { + using CngKey key = GenerateCngKey(); + return key.ProviderHandle; + }); + + private static SafeNCryptProviderHandle DefaultProviderHandle => s_lazyDefaultProviderHandle.Value; + + protected abstract CngExportPolicies ExportPolicy { get; } + + public override X25519DiffieHellmanCng GenerateKey() + { + using CngKey key = GenerateCngKey(); + return new X25519DiffieHellmanCng(key); + } + + public override X25519DiffieHellmanCng ImportPrivateKey(ReadOnlySpan source) + { + using CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(source, true, out _); + using SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob( + CngKeyBlobFormat.EccPrivateBlob.Format, + lease.Span, + "curve25519", + DefaultProviderHandle, + NCRYPT_NO_KEY_VALIDATION); + + using CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.EphemeralKey); + byte[] exportPolicyBytes = BitConverter.GetBytes((int)ExportPolicy); + + cngKey.SetProperty(new CngProperty( + "Export Policy", + exportPolicyBytes, + CngPropertyOptions.None)); + + return new X25519DiffieHellmanCng(cngKey); + } + + public override X25519DiffieHellmanCng ImportPublicKey(ReadOnlySpan source) + { + Span reducedPublicKey = stackalloc byte[X25519DiffieHellman.PublicKeySizeInBytes]; + X25519WindowsHelpers.ReducePublicKey(source, reducedPublicKey); + using CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(reducedPublicKey, false, out _); + using SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob( + CngKeyBlobFormat.EccPublicBlob.Format, + lease.Span, + "curve25519", + DefaultProviderHandle, + NCRYPT_NO_KEY_VALIDATION); + + using CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.EphemeralKey); + return new X25519DiffieHellmanCng(cngKey); + } + + // X25519DiffieHellmanCng CNG can't unfix adjusted keys because it can't keep track of adjustments made + // since the key came from somewhere else. + public override bool CanRoundTripKeys => false; + + internal static CngKey GenerateCngKey( + string curve = "curve25519", + CngExportPolicies exportPolicy = CngExportPolicies.AllowPlaintextExport) + { + CngKeyCreationParameters creationParameters = new() { ExportPolicy = exportPolicy }; + + creationParameters.Parameters.Add( + new CngProperty( + "ECCCurveName", + Encoding.Unicode.GetBytes(curve + "\0"), + CngPropertyOptions.None)); + + return CngKey.Create( + CngAlgorithm.ECDiffieHellman, + null, + creationParameters); + } + } +} From defee899789461575e74ecbad78e1790e7becf38 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 7 May 2026 13:25:57 -0400 Subject: [PATCH 2/3] Copilot feedback --- .../src/System/Security/Cryptography/X25519WindowsHelpers.cs | 1 - .../Security/Cryptography/X25519DiffieHellmanCng.Windows.cs | 5 +++-- .../System/Security/Cryptography/X25519DiffieHellmanCng.cs | 2 +- .../tests/X25519DiffieHellmanCngTests.cs | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs index 775df71729ea7d..ef405c0f73a55c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.Versioning; namespace System.Security.Cryptography { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs index d26dc8bfcdd8b4..c751ee0cd3e369 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Formats.Asn1; -using System.Runtime.Versioning; using System.Security.Cryptography.Asn1; using Microsoft.Win32.SafeHandles; using Internal.Cryptography; @@ -47,6 +46,8 @@ protected override partial void DeriveRawSecretAgreementCore(X25519DiffieHellman // the Provider is the same. It's the caller's responsibility to ensure the providers match. if (otherParty is X25519DiffieHellmanCng x25519Cng) { + x25519Cng.ThrowIfDisposed(); + using (SafeNCryptSecretHandle secretAgreement = Interop.NCrypt.DeriveSecretAgreement( _key.HandleNoDuplicate, x25519Cng._key.HandleNoDuplicate)) @@ -221,7 +222,7 @@ private void ExportPrivateKeyFromEncryptedPkcs8(Span destination) throw new CryptographicException(SR.Cryptography_NotValidPrivateKey); } - // X25519 on Windows exports currently exports X25519 keys as an explicit curve. However + // Windows currently exports X25519 keys as an explicit curve. However // since the constructor validates that the CngKey curve is curve25519 we can be reasonably sure // that the key is for X25519, so we don't validate the parameters. ValueECPrivateKey.Decode(key, AsnEncodingRules.BER, out ValueECPrivateKey ecKey); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs index af041f7442d31a..27e77afa6049fc 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.cs @@ -32,7 +32,7 @@ public sealed partial class X25519DiffieHellmanCng : X25519DiffieHellman /// is . /// /// - /// does not specify a X25519 Diffie-Hellman key. + /// does not specify an X25519 Diffie-Hellman key. /// /// /// Cryptography Next Generation (CNG) classes are not supported on this system. diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs index 917b42d52afb54..b50adbbffac1b0 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanCngTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Formats.Asn1; using System.Text; using Microsoft.Win32.SafeHandles; using Test.Cryptography; @@ -112,7 +111,7 @@ public abstract class X25519DiffieHellmanCngTests : X25519DiffieHellmanBaseTests public override X25519DiffieHellmanCng GenerateKey() { - using CngKey key = GenerateCngKey(); + using CngKey key = GenerateCngKey(exportPolicy: ExportPolicy); return new X25519DiffieHellmanCng(key); } From c06c51cd6b980ec9e9eeb5017932299280a40478 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 8 May 2026 16:01:00 -0400 Subject: [PATCH 3/3] Code review feedback --- .../Cryptography/X25519WindowsHelpers.cs | 13 ++- .../X25519DiffieHellmanCng.Windows.cs | 109 +++++++----------- ...5519DiffieHellmanImplementation.Windows.cs | 2 - 3 files changed, 55 insertions(+), 69 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs index ef405c0f73a55c..dd5f891f08b585 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/X25519WindowsHelpers.cs @@ -44,9 +44,11 @@ internal static void ExportKey(ReadOnlySpan exported, bool privateKey, Spa throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } - // x + // The key material after the blob is { x || y }, and optionally d if there is a private key. + // y should always be zero as it is not used for Curve25519 keys. + + // Check y is zero, skip over the blob header and x. ReadOnlySpan y = new(pExportedSpan + blobHeaderSize + ElementSize, ElementSize); - // d // y shouldn't have a value. if (y.IndexOfAnyExcept((byte)0) >= 0) @@ -96,12 +98,19 @@ internal static CryptoPoolLease CreateCngBlob(ReadOnlySpan key, bool priva if (privateKey) { + // This builds a blob of { x || y || d }. x is the public key, and we leave it as all zeros + // and CNG will reconstruct the public key from the private key. + // y is not used for Curve25519 so we leave it as zeros. + // d follows y. Since we zeroed the whole blob, skip over the header, x, and y and write d. Span destination = lease.Span.Slice(blobHeaderSize + ElementSize * 2, ElementSize); key.CopyTo(destination); + + // Any fixup of the key is done in-place in the rented blob, which gets zeroed later. preservation = FixupPrivateScalar(destination); } else { + // Otherwise if we are importing the public key, write x after the header. Span destination = lease.Span.Slice(blobHeaderSize, ElementSize); key.CopyTo(destination); preservation = 0; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs index c751ee0cd3e369..4c36b457306c85 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanCng.Windows.cs @@ -39,85 +39,64 @@ public partial CngKey GetKey() protected override partial void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) { - bool success; - int bytesWritten; - - // Assume that if key agreement is being done between two different X25519DiffieHellmanCng instances then - // the Provider is the same. It's the caller's responsibility to ensure the providers match. - if (otherParty is X25519DiffieHellmanCng x25519Cng) + // We intentionally don't special case otherParty being an instance of X25519DiffieHellmanCng and always + // export the public key into the current instance's provider. + Span publicKeyBuffer = stackalloc byte[PublicKeySizeInBytes * 2]; + Span publicKeyBytes = publicKeyBuffer.Slice(0, PublicKeySizeInBytes); + Span reducedPublicKey = publicKeyBuffer.Slice(PublicKeySizeInBytes, PublicKeySizeInBytes); + otherParty.ExportPublicKey(publicKeyBytes); + X25519WindowsHelpers.ReducePublicKey(publicKeyBytes, reducedPublicKey); + + // CNG does not permit cross-provider key agreements. Import the public key in to the same provider + // as the current key. + CngProvider provider = _key.Provider ?? CngProvider.MicrosoftSoftwareKeyStorageProvider; + + using (CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(reducedPublicKey, privateKey: false, out _)) + using (SafeNCryptProviderHandle providerHandle = provider.OpenStorageProvider()) { - x25519Cng.ThrowIfDisposed(); + int flags = 0; + + if (provider == CngProvider.MicrosoftSoftwareKeyStorageProvider) + { + const int NCRYPT_NO_KEY_VALIDATION = (int)Interop.BCrypt.BCryptImportKeyPairFlags.BCRYPT_NO_KEY_VALIDATION; + flags |= NCRYPT_NO_KEY_VALIDATION; + } + SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob( + CngKeyBlobFormat.EccPublicBlob.Format, + lease.Span, + X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519, + providerHandle, + flags); + + using (keyHandle) using (SafeNCryptSecretHandle secretAgreement = Interop.NCrypt.DeriveSecretAgreement( _key.HandleNoDuplicate, - x25519Cng._key.HandleNoDuplicate)) + keyHandle)) { - success = Interop.NCrypt.TryDeriveKeyMaterialTruncate( + bool success = Interop.NCrypt.TryDeriveKeyMaterialTruncate( secretAgreement, Interop.NCrypt.SecretAgreementFlags.None, destination, - out bytesWritten); - } - } - else - { - Span publicKeyBytes = stackalloc byte[PublicKeySizeInBytes]; - otherParty.ExportPublicKey(publicKeyBytes); - Span reducedPublicKey = stackalloc byte[PublicKeySizeInBytes]; - X25519WindowsHelpers.ReducePublicKey(publicKeyBytes, reducedPublicKey); + out int bytesWritten); - using (CryptoPoolLease lease = X25519WindowsHelpers.CreateCngBlob(reducedPublicKey, privateKey: false, out _)) - { - // CNG does not permit cross-provider key agreements. Import the public key in to the same provider - // as the current key. - CngProvider provider = _key.Provider ?? CngProvider.MicrosoftSoftwareKeyStorageProvider; - - using (SafeNCryptProviderHandle providerHandle = provider.OpenStorageProvider()) + if (!success || bytesWritten != SecretAgreementSizeInBytes) { - int flags = 0; - - if (provider == CngProvider.MicrosoftSoftwareKeyStorageProvider) - { - const int NCRYPT_NO_KEY_VALIDATION = (int)Interop.BCrypt.BCryptImportKeyPairFlags.BCRYPT_NO_KEY_VALIDATION; - flags |= NCRYPT_NO_KEY_VALIDATION; - } + // The destination should have already been pre-sized to a well-behaving X25519 implementation + // but a provider could be implemented incorrectly. Zero whatever was written since it is + // incorrect. + CryptographicOperations.ZeroMemory(destination); + throw new CryptographicException(); + } - SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob( - CngKeyBlobFormat.EccPublicBlob.Format, - lease.Span, - X25519WindowsHelpers.BCRYPT_ECC_CURVE_25519, - providerHandle, - flags); - - using (keyHandle) - using (SafeNCryptSecretHandle secretAgreement = Interop.NCrypt.DeriveSecretAgreement( - _key.HandleNoDuplicate, - keyHandle)) - { - success = Interop.NCrypt.TryDeriveKeyMaterialTruncate( - secretAgreement, - Interop.NCrypt.SecretAgreementFlags.None, - destination, - out bytesWritten); - } + // If the CngKey was created with NCRYPT_NO_KEY_VALIDATION then low-order public keys can be imported. + // Block low-order key agreements that result in an all-zero secret. + if (CryptographicOperations.FixedTimeEquals(destination, 0)) + { + throw new CryptographicException(); } } } - - if (!success || bytesWritten != SecretAgreementSizeInBytes) - { - // The destination should have already been pre-sized to a well-behaving X25519 implementation - // but a provider could be implemented incorrectly. Zero whatever was written since it is incorrect. - CryptographicOperations.ZeroMemory(destination); - throw new CryptographicException(); - } - - // If the CngKey was created with NCRYPT_NO_KEY_VALIDATION then low-order public keys can be imported. - // Block low-order key agreements that result in an all-zero secret. - if (CryptographicOperations.FixedTimeEquals(destination, 0)) - { - throw new CryptographicException(); - } } protected override partial void ExportPublicKeyCore(Span destination) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs index f378fe7f56bd1a..96eb9d370cb7c6 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs @@ -165,8 +165,6 @@ internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySp requiredReduction ? source.ToArray() : null); } - - private void ExportKey(bool privateKey, Span destination) { string blobType = privateKey ?