From bd94cef474878df221f8cf9a78c6efd944bde17b Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 21 Apr 2026 17:12:37 -0400 Subject: [PATCH 1/9] Implement X25519 Diffie-Hellman --- THIRD-PARTY-NOTICES.TXT | 207 +++ .../Interop.X25519.cs | 145 ++ .../Interop.EvpPkey.X25519.cs | 115 ++ .../Windows/BCrypt/Interop.BCryptDeriveKey.cs | 47 + .../BCrypt/Interop.BCryptDestroySecret.cs | 14 + .../BCrypt/Interop.BCryptSecretAgreement.cs | 38 + .../SafeHandles/SafeBCryptSecretHandle.cs | 16 + .../src/System/Security/Cryptography/Oids.cs | 4 + .../Security/Cryptography/MLKemBaseTests.cs | 39 +- .../Security/Cryptography/Pkcs8TestHelpers.cs | 67 + .../ref/System.Security.Cryptography.cs | 53 + .../src/System.Security.Cryptography.csproj | 18 + .../Cryptography/X25519DiffieHellman.cs | 1470 +++++++++++++++++ ...X25519DiffieHellmanImplementation.Apple.cs | 96 ++ ...iffieHellmanImplementation.NotSupported.cs | 56 + ...5519DiffieHellmanImplementation.OpenSsl.cs | 109 ++ ...5519DiffieHellmanImplementation.Windows.cs | 345 ++++ .../System.Security.Cryptography.Tests.csproj | 9 + .../tests/X25519DiffieHellmanBaseTests.cs | 607 +++++++ .../tests/X25519DiffieHellmanContractTests.cs | 873 ++++++++++ .../X25519DiffieHellmanImplementationTests.cs | 19 + .../tests/X25519DiffieHellmanKeyTests.cs | 391 +++++ .../X25519DiffieHellmanNotSupportedTests.cs | 100 ++ .../tests/X25519DiffieHellmanTestData.cs | 95 ++ .../tests/X25519DiffieHellmanTests.cs | 685 ++++++++ .../entrypoints.c | 7 + .../pal_swiftbindings.h | 8 + .../pal_swiftbindings.swift | 151 ++ .../CMakeLists.txt | 1 + .../entrypoints.c | 6 + .../opensslshim.h | 8 + .../pal_evp_pkey_x25519.c | 98 ++ .../pal_evp_pkey_x25519.h | 42 + 33 files changed, 5901 insertions(+), 38 deletions(-) create mode 100644 src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs create mode 100644 src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKey.cs create mode 100644 src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDestroySecret.cs create mode 100644 src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSecretAgreement.cs create mode 100644 src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBCryptSecretHandle.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/Pkcs8TestHelpers.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Apple.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.NotSupported.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanKeyTests.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTestData.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTests.cs create mode 100644 src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c create mode 100644 src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index 7f020cf8e67da5..2419a1a34585a9 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -1457,3 +1457,210 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License for Project Wycheproof +-------------------------------------------------------------------- +Available at https://github.com/C2SP/wycheproof/blob/2fb7240f471b70bb3aa5cc9b0718cdff01632508/LICENSE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs new file mode 100644 index 00000000000000..7e6951539232ae --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs @@ -0,0 +1,145 @@ +// 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 System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Swift; +using System.Security.Cryptography; +using System.Security.Cryptography.Apple; + +#pragma warning disable CS3016 // Arrays as attribute arguments are not CLS Compliant + +internal static partial class Interop +{ + internal static partial class AppleCrypto + { + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519DeriveRawSecretAgreement")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static partial int AppleCryptoNative_X25519DeriveRawSecretAgreement( + SafeX25519KeyHandle key, + SafeX25519KeyHandle peerKey, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519ExportPrivateKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static partial int AppleCryptoNative_X25519ExportPrivateKey( + SafeX25519KeyHandle key, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519ExportPublicKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static partial int AppleCryptoNative_X25519ExportPublicKey( + SafeX25519KeyHandle key, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519ImportPrivateKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static partial SafeX25519KeyHandle AppleCryptoNative_X25519ImportPrivateKey( + ReadOnlySpan source, + int sourceLength); + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519ImportPublicKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static partial SafeX25519KeyHandle AppleCryptoNative_X25519ImportPublicKey( + ReadOnlySpan source, + int sourceLength); + + internal static void X25519DeriveRawSecretAgreement(SafeX25519KeyHandle key, SafeX25519KeyHandle peerKey, Span destination) + { + const int Success = 1; + int ret = AppleCryptoNative_X25519DeriveRawSecretAgreement( + key, + peerKey, + destination, + destination.Length); + + if (ret != Success) + { + Debug.Fail($"Unexpected result from {nameof(AppleCryptoNative_X25519ExportPrivateKey)}: {ret}."); + throw new CryptographicException(); + } + } + + internal static void X25519ExportPrivateKey(SafeX25519KeyHandle key, Span destination) + { + const int Success = 1; + int ret = AppleCryptoNative_X25519ExportPrivateKey(key, destination, destination.Length); + + if (ret != Success) + { + Debug.Fail($"Unexpected result from {nameof(AppleCryptoNative_X25519ExportPrivateKey)}: {ret}."); + throw new CryptographicException(); + } + } + + internal static void X25519ExportPublicKey(SafeX25519KeyHandle key, Span destination) + { + const int Success = 1; + int ret = AppleCryptoNative_X25519ExportPublicKey(key, destination, destination.Length); + + if (ret != Success) + { + Debug.Fail($"Unexpected result from {nameof(AppleCryptoNative_X25519ExportPublicKey)}: {ret}."); + throw new CryptographicException(); + } + } + + internal static SafeX25519KeyHandle X25519ImportPrivateKey(ReadOnlySpan source) + { + SafeX25519KeyHandle ret = AppleCryptoNative_X25519ImportPrivateKey(source, source.Length); + + if (ret.IsInvalid) + { + ret.Dispose(); + throw new CryptographicException(); + } + + return ret; + } + + internal static SafeX25519KeyHandle X25519ImportPublicKey(ReadOnlySpan source) + { + SafeX25519KeyHandle ret = AppleCryptoNative_X25519ImportPublicKey(source, source.Length); + + if (ret.IsInvalid) + { + ret.Dispose(); + throw new CryptographicException(); + } + + return ret; + } + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519FreeKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + internal static partial void X25519FreeKey(IntPtr ptr); + + [LibraryImport(Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_X25519GenerateKey")] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + internal static partial SafeX25519KeyHandle X25519GenerateKey(); + } +} + +namespace System.Security.Cryptography.Apple +{ + internal sealed class SafeX25519KeyHandle : SafeHandle + { + public SafeX25519KeyHandle() : base(IntPtr.Zero, ownsHandle: true) + { + } + + protected override bool ReleaseHandle() + { + Interop.AppleCrypto.X25519FreeKey(handle); + SetHandle(IntPtr.Zero); + return true; + } + + public override bool IsInvalid => handle == IntPtr.Zero; + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs new file mode 100644 index 00000000000000..98d772b2720f6d --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs @@ -0,0 +1,115 @@ +// 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 System.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Crypto + { + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ExportPrivateKey")] + private static partial int X25519ExportPrivateKey( + SafeEvpPKeyHandle key, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ExportPublicKey")] + private static partial int X25519ExportPublicKey( + SafeEvpPKeyHandle key, + Span destination, + int destinationLength); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519GenerateKey")] + private static partial SafeEvpPKeyHandle CryptoNative_X25519GenerateKey(); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ImportPrivateKey")] + private static partial SafeEvpPKeyHandle X25519ImportPrivateKey(ReadOnlySpan source, int sourceLength); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ImportPublicKey")] + private static partial SafeEvpPKeyHandle X25519ImportPublicKey(ReadOnlySpan source, int sourceLength); + + internal static void X25519ExportPrivateKey(SafeEvpPKeyHandle key, Span destination) + { + const int Success = 1; + const int Fail = 0; + + int ret = X25519ExportPrivateKey(key, destination, destination.Length); + + switch (ret) + { + case Success: + return; + case Fail: + throw CreateOpenSslCryptographicException(); + default: + Debug.Fail($"{nameof(X25519ExportPrivateKey)} returned '{ret}' unexpectedly."); + throw new CryptographicException(); + } + } + + internal static void X25519ExportPublicKey(SafeEvpPKeyHandle key, Span destination) + { + const int Success = 1; + const int Fail = 0; + + int ret = X25519ExportPublicKey(key, destination, destination.Length); + + switch (ret) + { + case Success: + return; + case Fail: + throw CreateOpenSslCryptographicException(); + default: + Debug.Fail($"{nameof(X25519ExportPublicKey)} returned '{ret}' unexpectedly."); + throw new CryptographicException(); + } + } + + internal static SafeEvpPKeyHandle X25519GenerateKey() + { + SafeEvpPKeyHandle key = CryptoNative_X25519GenerateKey(); + + if (key.IsInvalid) + { + Exception ex = CreateOpenSslCryptographicException(); + key.Dispose(); + throw ex; + } + + return key; + } + + internal static SafeEvpPKeyHandle X25519ImportPrivateKey(ReadOnlySpan source) + { + SafeEvpPKeyHandle key = X25519ImportPrivateKey(source, source.Length); + + if (key.IsInvalid) + { + Exception ex = CreateOpenSslCryptographicException(); + key.Dispose(); + throw ex; + } + + return key; + } + + internal static SafeEvpPKeyHandle X25519ImportPublicKey(ReadOnlySpan source) + { + SafeEvpPKeyHandle key = X25519ImportPublicKey(source, source.Length); + + if (key.IsInvalid) + { + Exception ex = CreateOpenSslCryptographicException(); + key.Dispose(); + throw ex; + } + + return key; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKey.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKey.cs new file mode 100644 index 00000000000000..0ac167f36debaf --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKey.cs @@ -0,0 +1,47 @@ +// 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.Runtime.InteropServices; + +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class BCrypt + { + [LibraryImport(Libraries.BCrypt, StringMarshalling = StringMarshalling.Utf16)] + private static unsafe partial NTSTATUS BCryptDeriveKey( + SafeBCryptSecretHandle hSharedSecret, + string pwszKDF, + ref readonly BCryptBufferDesc pParameterList, + Span pbDerivedKey, + uint cbDerivedKey, + out uint pcbResult, + uint dwFlags); + + internal static unsafe void BCryptDeriveKey( + SafeBCryptSecretHandle hSharedSecret, + string pwszKDF, + ref readonly BCryptBufferDesc parameterList, + Span destination, + out int written) + { + NTSTATUS status = BCryptDeriveKey( + hSharedSecret, + pwszKDF, + in parameterList, + destination, + (uint)destination.Length, + out uint pcbResult, + 0); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + throw CreateCryptographicException(status); + } + + written = checked((int)pcbResult); + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDestroySecret.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDestroySecret.cs new file mode 100644 index 00000000000000..52ae3566ef3c12 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDestroySecret.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. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class BCrypt + { + [LibraryImport(Libraries.BCrypt)] + internal static partial NTSTATUS BCryptDestroySecret(IntPtr hSecret); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSecretAgreement.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSecretAgreement.cs new file mode 100644 index 00000000000000..0e7b3f66862732 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptSecretAgreement.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class BCrypt + { + [LibraryImport(Libraries.BCrypt)] + private static unsafe partial NTSTATUS BCryptSecretAgreement( + SafeBCryptKeyHandle hPrivKey, + SafeBCryptKeyHandle hPubKey, + out SafeBCryptSecretHandle phAgreedSecret, + uint dwFlags); + + internal static SafeBCryptSecretHandle BCryptSecretAgreement( + SafeBCryptKeyHandle hPrivKey, + SafeBCryptKeyHandle hPubKey) + { + NTSTATUS status = BCryptSecretAgreement( + hPrivKey, + hPubKey, + out SafeBCryptSecretHandle agreedSecret, + 0); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + agreedSecret.Dispose(); + throw CreateCryptographicException(status); + } + + return agreedSecret; + } + } +} diff --git a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBCryptSecretHandle.cs b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBCryptSecretHandle.cs new file mode 100644 index 00000000000000..e446eae886ae33 --- /dev/null +++ b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBCryptSecretHandle.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NTSTATUS = Interop.BCrypt.NTSTATUS; + +namespace Microsoft.Win32.SafeHandles +{ + internal sealed class SafeBCryptSecretHandle : SafeBCryptHandle + { + protected sealed override bool ReleaseHandle() + { + NTSTATUS ntStatus = Interop.BCrypt.BCryptDestroySecret(handle); + return ntStatus == NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs index b6a439ef42f5f1..5abab4ef7eeea7 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs @@ -269,6 +269,10 @@ internal static partial class Oids // LDAP internal const string DomainComponent = "0.9.2342.19200300.100.1.25"; + // X25519 + // id-X25519 + internal const string X25519 = "1.3.101.110"; + // ML-KEM // id-alg-ml-kem-512 internal const string MlKem512 = "2.16.840.1.101.3.4.4.1"; diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs index fbc4c3b87870d6..e9214813f8d411 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs @@ -559,44 +559,7 @@ private static void AssertSubjectPublicKeyInfo(MLKem kem, bool useTryExport, Rea private static void AssertEncryptedPkcs8PrivateKeyContents(PbeParameters pbeParameters, ReadOnlyMemory contents) { - EncryptedPrivateKeyInfoAsn epki = EncryptedPrivateKeyInfoAsn.Decode(contents, AsnEncodingRules.BER); - AlgorithmIdentifierAsn algorithmIdentifier = epki.EncryptionAlgorithm; - - if (pbeParameters.EncryptionAlgorithm == PbeEncryptionAlgorithm.TripleDes3KeyPkcs12) - { - // pbeWithSHA1And3-KeyTripleDES-CBC - Assert.Equal("1.2.840.113549.1.12.1.3", algorithmIdentifier.Algorithm); - ValuePBEParameter.Decode( - algorithmIdentifier.Parameters.Value.Span, - AsnEncodingRules.BER, - out ValuePBEParameter pbeParameterAsn); - - Assert.Equal(pbeParameters.IterationCount, pbeParameterAsn.IterationCount); - } - else - { - Assert.Equal("1.2.840.113549.1.5.13", algorithmIdentifier.Algorithm); // PBES2 - ValuePBES2Params.Decode( - algorithmIdentifier.Parameters.Value.Span, - AsnEncodingRules.BER, - out ValuePBES2Params pbes2Params); - Assert.Equal("1.2.840.113549.1.5.12", pbes2Params.KeyDerivationFunc.Algorithm); // PBKDF2 - ValuePbkdf2Params.Decode( - pbes2Params.KeyDerivationFunc.Parameters, - AsnEncodingRules.BER, - out ValuePbkdf2Params pbkdf2Params); - string expectedEncryptionOid = pbeParameters.EncryptionAlgorithm switch - { - PbeEncryptionAlgorithm.Aes128Cbc => "2.16.840.1.101.3.4.1.2", - PbeEncryptionAlgorithm.Aes192Cbc => "2.16.840.1.101.3.4.1.22", - PbeEncryptionAlgorithm.Aes256Cbc => "2.16.840.1.101.3.4.1.42", - _ => throw new CryptographicException(), - }; - - Assert.Equal(pbeParameters.IterationCount, pbkdf2Params.IterationCount); - Assert.Equal(pbeParameters.HashAlgorithm, GetHashAlgorithmFromPbkdf2Params(pbkdf2Params)); - Assert.Equal(expectedEncryptionOid, pbes2Params.EncryptionScheme.Algorithm); - } + Pkcs8TestHelpers.AssertEncryptedPkcs8PrivateKeyContents(pbeParameters, contents); } private static HashAlgorithmName GetHashAlgorithmFromPbkdf2Params(in ValuePbkdf2Params pbkdf2Params) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/Pkcs8TestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/Pkcs8TestHelpers.cs new file mode 100644 index 00000000000000..694298ee70ba5e --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/Pkcs8TestHelpers.cs @@ -0,0 +1,67 @@ +// 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.Security.Cryptography.Asn1; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + internal static class Pkcs8TestHelpers + { + internal static void AssertEncryptedPkcs8PrivateKeyContents(PbeParameters pbeParameters, ReadOnlyMemory contents) + { + EncryptedPrivateKeyInfoAsn epki = EncryptedPrivateKeyInfoAsn.Decode(contents, AsnEncodingRules.BER); + AlgorithmIdentifierAsn algorithmIdentifier = epki.EncryptionAlgorithm; + + if (pbeParameters.EncryptionAlgorithm == PbeEncryptionAlgorithm.TripleDes3KeyPkcs12) + { + // pbeWithSHA1And3-KeyTripleDES-CBC + Assert.Equal("1.2.840.113549.1.12.1.3", algorithmIdentifier.Algorithm); + ValuePBEParameter.Decode( + algorithmIdentifier.Parameters.Value.Span, + AsnEncodingRules.BER, + out ValuePBEParameter pbeParameterAsn); + + Assert.Equal(pbeParameters.IterationCount, pbeParameterAsn.IterationCount); + } + else + { + Assert.Equal("1.2.840.113549.1.5.13", algorithmIdentifier.Algorithm); // PBES2 + ValuePBES2Params.Decode( + algorithmIdentifier.Parameters.Value.Span, + AsnEncodingRules.BER, + out ValuePBES2Params pbes2Params); + Assert.Equal("1.2.840.113549.1.5.12", pbes2Params.KeyDerivationFunc.Algorithm); // PBKDF2 + ValuePbkdf2Params.Decode( + pbes2Params.KeyDerivationFunc.Parameters, + AsnEncodingRules.BER, + out ValuePbkdf2Params pbkdf2Params); + string expectedEncryptionOid = pbeParameters.EncryptionAlgorithm switch + { + PbeEncryptionAlgorithm.Aes128Cbc => "2.16.840.1.101.3.4.1.2", + PbeEncryptionAlgorithm.Aes192Cbc => "2.16.840.1.101.3.4.1.22", + PbeEncryptionAlgorithm.Aes256Cbc => "2.16.840.1.101.3.4.1.42", + _ => throw new CryptographicException(), + }; + + Assert.Equal(pbeParameters.IterationCount, pbkdf2Params.IterationCount); + Assert.Equal(pbeParameters.HashAlgorithm, GetHashAlgorithmFromPbkdf2Params(pbkdf2Params)); + Assert.Equal(expectedEncryptionOid, pbes2Params.EncryptionScheme.Algorithm); + } + } + + private static HashAlgorithmName GetHashAlgorithmFromPbkdf2Params(in ValuePbkdf2Params pbkdf2Params) + { + return pbkdf2Params.Prf.Algorithm switch + { + "1.2.840.113549.2.7" => HashAlgorithmName.SHA1, + "1.2.840.113549.2.9" => HashAlgorithmName.SHA256, + "1.2.840.113549.2.10" => HashAlgorithmName.SHA384, + "1.2.840.113549.2.11" => HashAlgorithmName.SHA512, + string other => throw new XunitException($"Unknown hash algorithm OID '{other}'."), + }; + } + } +} 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 dc2d4f36f5f9cd..7a2269429c83e7 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3440,6 +3440,59 @@ protected override void Dispose(bool disposing) { } public override void GenerateIV() { } public override void GenerateKey() { } } + public abstract partial class X25519DiffieHellman : System.IDisposable + { + public const int PrivateKeySizeInBytes = 32; + public const int PublicKeySizeInBytes = 32; + public const int SecretAgreementSizeInBytes = 32; + protected X25519DiffieHellman() { } + public static bool IsSupported { get { throw null; } } + public byte[] DeriveRawSecretAgreement(System.Security.Cryptography.X25519DiffieHellman otherParty) { throw null; } + public void DeriveRawSecretAgreement(System.Security.Cryptography.X25519DiffieHellman otherParty, System.Span destination) { } + protected abstract void DeriveRawSecretAgreementCore(System.Security.Cryptography.X25519DiffieHellman otherParty, System.Span destination); + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public byte[] ExportPrivateKey() { throw null; } + public void ExportPrivateKey(System.Span destination) { } + protected abstract void ExportPrivateKeyCore(System.Span destination); + public byte[] ExportPublicKey() { throw null; } + public void ExportPublicKey(System.Span destination) { } + protected abstract void ExportPublicKeyCore(System.Span destination); + public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportEncryptedPkcs8PrivateKey(string password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public string ExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public string ExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public string ExportEncryptedPkcs8PrivateKeyPem(string password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportPkcs8PrivateKey() { throw null; } + public string ExportPkcs8PrivateKeyPem() { throw null; } + public byte[] ExportSubjectPublicKeyInfo() { throw null; } + public string ExportSubjectPublicKeyInfoPem() { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman GenerateKey() { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(string password, byte[] source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromEncryptedPem(System.ReadOnlySpan source, System.ReadOnlySpan passwordBytes) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromEncryptedPem(System.ReadOnlySpan source, System.ReadOnlySpan password) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromEncryptedPem(string source, byte[] passwordBytes) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromEncryptedPem(string source, string password) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromPem(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportFromPem(string source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPkcs8PrivateKey(byte[] source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPkcs8PrivateKey(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPrivateKey(byte[] source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPrivateKey(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPublicKey(byte[] source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportPublicKey(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportSubjectPublicKeyInfo(byte[] source) { throw null; } + public static System.Security.Cryptography.X25519DiffieHellman ImportSubjectPublicKeyInfo(System.ReadOnlySpan source) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKey(string password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } + protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten); + public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } + } } namespace System.Security.Cryptography.X509Certificates { 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 c0df094935c129..d99843f361213c 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -687,6 +687,7 @@ + @@ -884,6 +885,7 @@ + @@ -972,6 +974,8 @@ Link="Common\Interop\Unix\System.Security.Cryptography.Native\Interop.EvpPkey.SlhDsaAlgs.cs" /> + + System\Security\Cryptography\X509Certificates\Asn1\DistributionPointAsn.xml @@ -1293,6 +1298,7 @@ + @@ -1359,6 +1365,8 @@ Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Symmetric.cs" /> + + @@ -1566,10 +1575,14 @@ Link="Common\Interop\Windows\BCrypt\Interop.BCryptCreateHash.cs" /> + + + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs new file mode 100644 index 00000000000000..2312f212028244 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs @@ -0,0 +1,1470 @@ +// 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 System.Formats.Asn1; +using System.Security.Cryptography.Asn1; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + /// + /// Represents an X25519 Diffie-Hellman key. + /// + /// + /// + /// 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 abstract class X25519DiffieHellman : IDisposable + { + private static readonly string[] s_knownOids = [Oids.X25519]; + + private bool _disposed; + + /// + /// The size of the secret agreement, in bytes. + /// + public const int SecretAgreementSizeInBytes = 32; + + /// + /// The size of the private key, in bytes. + /// + public const int PrivateKeySizeInBytes = 32; + + /// + /// The size of the public key, in bytes. + /// + public const int PublicKeySizeInBytes = 32; + + /// + /// Gets a value that indicates whether the algorithm is supported on the current platform. + /// + /// + /// if the algorithm is supported; otherwise, . + /// + public static bool IsSupported => X25519DiffieHellmanImplementation.IsSupported; + + /// + /// Derives a raw secret agreement with the other party's key. + /// + /// + /// The other party's key. + /// + /// + /// The secret agreement. + /// + /// + /// The raw secret agreement value is expected to be used as input into a Key Derivation Function, + /// and not used directly as key material. + /// + /// + /// is . + /// + /// + /// An error occurred during the secret agreement derivation. + /// + /// The object has already been disposed. + public byte[] DeriveRawSecretAgreement(X25519DiffieHellman otherParty) + { + ArgumentNullException.ThrowIfNull(otherParty); + ThrowIfDisposed(); + + byte[] buffer = new byte[SecretAgreementSizeInBytes]; + DeriveRawSecretAgreementCore(otherParty, buffer); + return buffer; + } + + /// + /// Derives a raw secret agreement with the other party's key, writing it into the provided buffer. + /// + /// + /// The other party's key. + /// + /// + /// The buffer to receive the secret agreement. + /// + /// + /// The raw secret agreement value is expected to be used as input into a Key Derivation Function, + /// and not used directly as key material. + /// + /// + /// is . + /// + /// + /// is the incorrect length to receive the secret agreement. + /// + /// + /// An error occurred during the secret agreement derivation. + /// + /// The object has already been disposed. + public void DeriveRawSecretAgreement(X25519DiffieHellman otherParty, Span destination) + { + ArgumentNullException.ThrowIfNull(otherParty); + + if (destination.Length != SecretAgreementSizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, SecretAgreementSizeInBytes), + nameof(destination)); + } + + ThrowIfDisposed(); + DeriveRawSecretAgreementCore(otherParty, destination); + } + + /// + /// Generates a new X25519 Diffie-Hellman key. + /// + /// + /// The generated key. + /// + /// + /// An error occurred generating the X25519 Diffie-Hellman key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman GenerateKey() + { + ThrowIfNotSupported(); + return X25519DiffieHellmanImplementation.GenerateKeyImpl(); + } + + /// + /// Exports the private key. + /// + /// + /// The private key. + /// + /// + /// An error occurred while exporting the key. + /// + /// The object has already been disposed. + public byte[] ExportPrivateKey() + { + ThrowIfDisposed(); + + byte[] buffer = new byte[PrivateKeySizeInBytes]; + ExportPrivateKeyCore(buffer); + return buffer; + } + + /// + /// Exports the private key into the provided buffer. + /// + /// + /// The buffer to receive the private key. + /// + /// + /// is the incorrect length to receive the private key. + /// + /// + /// An error occurred while exporting the key. + /// + /// The object has already been disposed. + public void ExportPrivateKey(Span destination) + { + if (destination.Length != PrivateKeySizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, PrivateKeySizeInBytes), + nameof(destination)); + } + + ThrowIfDisposed(); + ExportPrivateKeyCore(destination); + } + + /// + /// Exports the public key. + /// + /// + /// The public key. + /// + /// + /// An error occurred while exporting the key. + /// + /// The object has already been disposed. + public byte[] ExportPublicKey() + { + ThrowIfDisposed(); + + byte[] buffer = new byte[PublicKeySizeInBytes]; + ExportPublicKeyCore(buffer); + return buffer; + } + + /// + /// Exports the public key into the provided buffer. + /// + /// + /// The buffer to receive the public key. + /// + /// + /// is the incorrect length to receive the public key. + /// + /// + /// An error occurred while exporting the key. + /// + /// The object has already been disposed. + public void ExportPublicKey(Span destination) + { + if (destination.Length != PublicKeySizeInBytes) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, PublicKeySizeInBytes), + nameof(destination)); + } + + ThrowIfDisposed(); + ExportPublicKeyCore(destination); + } + + /// + /// Attempts to export the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format + /// into the provided buffer. + /// + /// + /// The buffer to receive the X.509 SubjectPublicKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesWritten) + { + ThrowIfDisposed(); + return ExportSubjectPublicKeyInfoCore().TryEncode(destination, out bytesWritten); + } + + /// + /// Exports the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format. + /// + /// + /// A byte array containing the X.509 SubjectPublicKeyInfo representation of the public-key portion of this key. + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public byte[] ExportSubjectPublicKeyInfo() + { + ThrowIfDisposed(); + return ExportSubjectPublicKeyInfoCore().Encode(); + } + + /// + /// Exports the public-key portion of the current key in a PEM-encoded representation of + /// the X.509 SubjectPublicKeyInfo format. + /// + /// + /// A string containing the PEM-encoded representation of the X.509 SubjectPublicKeyInfo + /// representation of the public-key portion of this key. + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public string ExportSubjectPublicKeyInfoPem() + { + ThrowIfDisposed(); + AsnWriter writer = ExportSubjectPublicKeyInfoCore(); + // SPKI does not contain sensitive data. + return Helpers.EncodeAsnWriterToPem(PemLabels.SpkiPublicKey, writer, clear: false); + } + + /// + /// Attempts to export the current key in the PKCS#8 PrivateKeyInfo format + /// into the provided buffer. + /// + /// + /// The buffer to receive the PKCS#8 PrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) + { + ThrowIfDisposed(); + + // An X25519 PKCS#8 PrivateKeyInfo with no attributes is 48 bytes: + // SEQUENCE (2) + INTEGER version (3) + SEQUENCE AlgorithmIdentifier (7) + + // OCTET STRING outer (2) + OCTET STRING CurvePrivateKey (2) + 32 byte key = 48. + // A buffer smaller than that cannot hold a PKCS#8 encoded key. + const int MinimumPossiblePkcs8X25519Key = 48; + + if (destination.Length < MinimumPossiblePkcs8X25519Key) + { + bytesWritten = 0; + return false; + } + + return TryExportPkcs8PrivateKeyCore(destination, out bytesWritten); + } + + /// + /// Exports the current key in the PKCS#8 PrivateKeyInfo format. + /// + /// + /// A byte array containing the PKCS#8 PrivateKeyInfo representation of this key. + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public byte[] ExportPkcs8PrivateKey() + { + ThrowIfDisposed(); + return ExportPkcs8PrivateKeyCallback(static pkcs8 => pkcs8.ToArray()); + } + + /// + /// Exports the current key in a PEM-encoded representation of the PKCS#8 PrivateKeyInfo format. + /// + /// + /// A string containing the PEM-encoded representation of the PKCS#8 PrivateKeyInfo. + /// + /// The object has already been disposed. + /// + /// An error occurred while exporting the key. + /// + public string ExportPkcs8PrivateKeyPem() + { + ThrowIfDisposed(); + return ExportPkcs8PrivateKeyCallback(static pkcs8 => PemEncoding.WriteString(PemLabels.Pkcs8PrivateKey, pkcs8)); + } + + /// + /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer, + /// using a byte-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The buffer to receive the PKCS#8 EncryptedPrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan passwordBytes, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); + } + + /// + /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer, + /// using a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The buffer to receive the PKCS#8 EncryptedPrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); + } + + /// + /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer, + /// using a string password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The buffer to receive the PKCS#8 EncryptedPrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// or is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public bool TryExportEncryptedPkcs8PrivateKey( + string password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + ArgumentNullException.ThrowIfNull(password); + return TryExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters, destination, out bytesWritten); + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a byte-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of this key. + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of this key. + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a string password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of this key. + /// + /// + /// or is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(password); + return ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); + } + + /// + /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo + /// format, using a byte-based password. + /// + /// + /// The bytes to use as a password when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo. + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// specifies a KDF that requires a char-based password. + /// -or- + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// + public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan passwordBytes, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + + // Skip clear since the data is already encrypted. + return Helpers.EncodeAsnWriterToPem(PemLabels.EncryptedPkcs8PrivateKey, writer, clear: false); + } + + /// + /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo + /// format, using a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo. + /// + /// + /// is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// + public string ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan password, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); + ThrowIfDisposed(); + + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + + // Skip clear since the data is already encrypted. + return Helpers.EncodeAsnWriterToPem(PemLabels.EncryptedPkcs8PrivateKey, writer, clear: false); + } + + /// + /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo + /// format, using a string password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo. + /// + /// + /// or is . + /// + /// The object has already been disposed. + /// + /// This instance only represents a public key. + /// -or- + /// An error occurred while exporting the key. + /// + public string ExportEncryptedPkcs8PrivateKeyPem(string password, PbeParameters pbeParameters) + { + ArgumentNullException.ThrowIfNull(password); + return ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters); + } + + /// + /// When overridden in a derived class, derives a raw secret agreement with the other party's key, + /// writing it into the provided buffer. + /// + /// + /// The other party's key. + /// + /// + /// The buffer to receive the secret agreement. + /// + /// + /// An error occurred during the secret agreement derivation. + /// + protected abstract void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination); + + /// + /// When overridden in a derived class, exports the private key into the provided buffer. + /// + /// + /// The buffer to receive the private key. + /// + protected abstract void ExportPrivateKeyCore(Span destination); + + /// + /// When overridden in a derived class, exports the public key into the provided buffer. + /// + /// + /// The buffer to receive the public key. + /// + protected abstract void ExportPublicKeyCore(Span destination); + + /// + /// When overridden in a derived class, attempts to export the current key in the PKCS#8 PrivateKeyInfo format + /// into the provided buffer. + /// + /// + /// The buffer to receive the PKCS#8 PrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// An error occurred while exporting the key. + /// + protected abstract bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten); + + /// + /// Imports an X25519 Diffie-Hellman key from a private key. + /// + /// + /// The private key. + /// + /// + /// The imported key. + /// + /// + /// is . + /// + /// + /// has a length that is not . + /// + /// + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportPrivateKey(byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportPrivateKey(new ReadOnlySpan(source)); + } + + /// + /// Imports an X25519 Diffie-Hellman key from a private key. + /// + /// + /// The private key. + /// + /// + /// The imported key. + /// + /// + /// has a length that is not . + /// + /// + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source) + { + if (source.Length != PrivateKeySizeInBytes) + throw new ArgumentException(SR.Argument_PrivateKeyWrongSizeForAlgorithm, nameof(source)); + + ThrowIfNotSupported(); + return X25519DiffieHellmanImplementation.ImportPrivateKeyImpl(source); + } + + /// + /// Imports an X25519 Diffie-Hellman key from a public key. + /// + /// + /// The public key. + /// + /// + /// The imported key. + /// + /// + /// is . + /// + /// + /// has a length that is not . + /// + /// + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportPublicKey(byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportPublicKey(new ReadOnlySpan(source)); + } + + /// + /// Imports an X25519 Diffie-Hellman key from a public key. + /// + /// + /// The public key. + /// + /// + /// The imported key. + /// + /// + /// has a length that is not . + /// + /// + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportPublicKey(ReadOnlySpan source) + { + if (source.Length != PublicKeySizeInBytes) + throw new ArgumentException(SR.Argument_PublicKeyWrongSizeForAlgorithm, nameof(source)); + + ThrowIfNotSupported(); + return X25519DiffieHellmanImplementation.ImportPublicKeyImpl(source); + } + + /// + /// Imports an X25519 Diffie-Hellman key from an X.509 SubjectPublicKeyInfo structure. + /// + /// + /// The bytes of an X.509 SubjectPublicKeyInfo structure in the ASN.1-DER encoding. + /// + /// + /// The imported key. + /// + /// + /// + /// The contents of do not represent an ASN.1-DER-encoded X.509 SubjectPublicKeyInfo structure. + /// + /// -or- + /// + /// The SubjectPublicKeyInfo value does not represent an X25519 Diffie-Hellman key. + /// + /// -or- + /// + /// The algorithm-specific import failed. + /// + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportSubjectPublicKeyInfo(ReadOnlySpan source) + { + Helpers.ThrowIfAsnInvalidLength(source); + ThrowIfNotSupported(); + + KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_knownOids, + source, + SubjectPublicKeyReader, + out int read, + out X25519DiffieHellman key); + + Debug.Assert(read == source.Length); + return key; + + static void SubjectPublicKeyReader( + ReadOnlySpan key, + in ValueAlgorithmIdentifierAsn identifier, + out X25519DiffieHellman result) + { + if (identifier.HasParameters) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (key.Length != PublicKeySizeInBytes) + { + throw new CryptographicException(SR.Argument_PublicKeyWrongSizeForAlgorithm); + } + + result = X25519DiffieHellmanImplementation.ImportPublicKeyImpl(key); + } + } + + /// + /// + /// is . + /// + public static X25519DiffieHellman ImportSubjectPublicKeyInfo(byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportSubjectPublicKeyInfo(new ReadOnlySpan(source)); + } + + /// + /// Imports an X25519 Diffie-Hellman private key from a PKCS#8 PrivateKeyInfo structure. + /// + /// + /// The bytes of a PKCS#8 PrivateKeyInfo structure in the ASN.1-BER encoding. + /// + /// + /// The imported key. + /// + /// + /// + /// The contents of do not represent an ASN.1-BER-encoded PKCS#8 PrivateKeyInfo structure. + /// + /// -or- + /// + /// The PrivateKeyInfo value does not represent an X25519 Diffie-Hellman key. + /// + /// -or- + /// + /// contains trailing data after the ASN.1 structure. + /// + /// -or- + /// + /// The algorithm-specific import failed. + /// + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportPkcs8PrivateKey(ReadOnlySpan source) + { + Helpers.ThrowIfAsnInvalidLength(source); + ThrowIfNotSupported(); + + KeyFormatHelper.ReadPkcs8(s_knownOids, source, Pkcs8KeyReader, out int read, out X25519DiffieHellman key); + Debug.Assert(read == source.Length); + return key; + } + + /// + /// + /// is . + /// + public static X25519DiffieHellman ImportPkcs8PrivateKey(byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportPkcs8PrivateKey(new ReadOnlySpan(source)); + } + + /// + /// Imports an X25519 Diffie-Hellman private key from a PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// + /// The bytes to use as a password when decrypting the key material. + /// + /// + /// The bytes of a PKCS#8 EncryptedPrivateKeyInfo structure in the ASN.1-BER encoding. + /// + /// + /// The imported key. + /// + /// + /// + /// The contents of do not represent an ASN.1-BER-encoded PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// -or- + /// + /// The specified password is incorrect. + /// + /// -or- + /// + /// The EncryptedPrivateKeyInfo indicates the Key Derivation Function (KDF) to apply is the legacy PKCS#12 KDF, + /// which requires -based passwords. + /// + /// -or- + /// + /// The value does not represent an X25519 Diffie-Hellman key. + /// + /// -or- + /// + /// The algorithm-specific import failed. + /// + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, ReadOnlySpan source) + { + Helpers.ThrowIfAsnInvalidLength(source); + ThrowIfNotSupported(); + + return KeyFormatHelper.DecryptPkcs8( + passwordBytes, + source, + ImportPkcs8PrivateKey, + out _); + } + + /// + /// Imports an X25519 Diffie-Hellman private key from a PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// + /// The password to use when decrypting the key material. + /// + /// + /// The bytes of a PKCS#8 EncryptedPrivateKeyInfo structure in the ASN.1-BER encoding. + /// + /// + /// The imported key. + /// + /// + /// + /// The contents of do not represent an ASN.1-BER-encoded PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// -or- + /// + /// The specified password is incorrect. + /// + /// -or- + /// + /// The value does not represent an X25519 Diffie-Hellman key. + /// + /// -or- + /// + /// The algorithm-specific import failed. + /// + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(ReadOnlySpan password, ReadOnlySpan source) + { + Helpers.ThrowIfAsnInvalidLength(source); + ThrowIfNotSupported(); + + return KeyFormatHelper.DecryptPkcs8( + password, + source, + ImportPkcs8PrivateKey, + out _); + } + + /// + /// Imports an X25519 Diffie-Hellman private key from a PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// + /// The password to use when decrypting the key material. + /// + /// + /// The bytes of a PKCS#8 EncryptedPrivateKeyInfo structure in the ASN.1-BER encoding. + /// + /// + /// The imported key. + /// + /// + /// or is . + /// + /// + /// + /// The contents of do not represent an ASN.1-BER-encoded PKCS#8 EncryptedPrivateKeyInfo structure. + /// + /// -or- + /// + /// The specified password is incorrect. + /// + /// -or- + /// + /// The value does not represent an X25519 Diffie-Hellman key. + /// + /// -or- + /// + /// The algorithm-specific import failed. + /// + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + public static X25519DiffieHellman ImportEncryptedPkcs8PrivateKey(string password, byte[] source) + { + ArgumentNullException.ThrowIfNull(password); + ArgumentNullException.ThrowIfNull(source); + Helpers.ThrowIfAsnInvalidLength(source); + ThrowIfNotSupported(); + + return KeyFormatHelper.DecryptPkcs8( + password, + source, + ImportPkcs8PrivateKey, + out _); + } + + /// + /// Imports an X25519 Diffie-Hellman key from an RFC 7468 PEM-encoded string. + /// + /// + /// The text of the PEM key to import. + /// + /// + /// The imported key. + /// + /// + /// contains an encrypted PEM-encoded key. + /// -or- + /// contains multiple PEM-encoded X25519 Diffie-Hellman keys. + /// -or- + /// contains no PEM-encoded X25519 Diffie-Hellman keys. + /// + /// + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + /// + /// + /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels + /// are found, an exception is raised to prevent importing a key when the key is ambiguous. + /// + /// + /// This method supports the following PEM labels: + /// + /// PUBLIC KEY + /// PRIVATE KEY + /// + /// + /// + public static X25519DiffieHellman ImportFromPem(ReadOnlySpan source) + { + ThrowIfNotSupported(); + + return PemKeyHelpers.ImportFactoryPem(source, label => + label switch + { + PemLabels.Pkcs8PrivateKey => ImportPkcs8PrivateKey, + PemLabels.SpkiPublicKey => ImportSubjectPublicKeyInfo, + _ => null, + }); + } + + /// + /// + /// is . + /// + public static X25519DiffieHellman ImportFromPem(string source) + { + ArgumentNullException.ThrowIfNull(source); + return ImportFromPem(source.AsSpan()); + } + + /// + /// Imports an X25519 Diffie-Hellman key from an encrypted RFC 7468 PEM-encoded string. + /// + /// + /// The PEM text of the encrypted key to import. + /// + /// + /// The password to use for decrypting the key material. + /// + /// + /// The imported key. + /// + /// + /// does not contain a PEM-encoded key with a recognized label. + /// -or- + /// contains multiple PEM-encoded keys with a recognized label. + /// + /// + /// The password is incorrect. + /// -or- + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + /// + /// + /// When the base-64 decoded contents of indicate an algorithm that uses PBKDF1 + /// (Password-Based Key Derivation Function 1) or PBKDF2 (Password-Based Key Derivation Function 2), + /// the password is converted to bytes via the UTF-8 encoding. + /// + /// + /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels + /// are found, an exception is thrown to prevent importing a key when the key is ambiguous. + /// + /// This method supports the ENCRYPTED PRIVATE KEY PEM label. + /// + public static X25519DiffieHellman ImportFromEncryptedPem(ReadOnlySpan source, ReadOnlySpan password) + { + return PemKeyHelpers.ImportEncryptedFactoryPem( + source, + password, + ImportEncryptedPkcs8PrivateKey); + } + + /// + /// Imports an X25519 Diffie-Hellman key from an encrypted RFC 7468 PEM-encoded string. + /// + /// + /// The PEM text of the encrypted key to import. + /// + /// + /// The password to use for decrypting the key material. + /// + /// + /// The imported key. + /// + /// + /// does not contain a PEM-encoded key with a recognized label. + /// -or- + /// contains multiple PEM-encoded keys with a recognized label. + /// + /// + /// The password is incorrect. + /// -or- + /// An error occurred while importing the key. + /// + /// + /// The platform does not support X25519 Diffie-Hellman. Callers can use the property + /// to determine if the platform supports X25519 Diffie-Hellman. + /// + /// + /// + /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels + /// are found, an exception is thrown to prevent importing a key when the key is ambiguous. + /// + /// This method supports the ENCRYPTED PRIVATE KEY PEM label. + /// + public static X25519DiffieHellman ImportFromEncryptedPem(ReadOnlySpan source, ReadOnlySpan passwordBytes) + { + return PemKeyHelpers.ImportEncryptedFactoryPem( + source, + passwordBytes, + ImportEncryptedPkcs8PrivateKey); + } + + /// + /// + /// or is . + /// + public static X25519DiffieHellman ImportFromEncryptedPem(string source, string password) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(password); + return ImportFromEncryptedPem(source.AsSpan(), password.AsSpan()); + } + + /// + /// + /// or is . + /// + public static X25519DiffieHellman ImportFromEncryptedPem(string source, byte[] passwordBytes) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(passwordBytes); + return ImportFromEncryptedPem(source.AsSpan(), new ReadOnlySpan(passwordBytes)); + } + + /// + /// Releases all resources used by the class. + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Called by the Dispose() and Finalize() methods to release the managed and unmanaged + /// resources used by the current instance of the class. + /// + /// + /// to release managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + + private AsnWriter ExportSubjectPublicKeyInfoCore() + { + Span publicKey = stackalloc byte[PublicKeySizeInBytes]; + ExportPublicKeyCore(publicKey); + + ValueSubjectPublicKeyInfoAsn spki = new ValueSubjectPublicKeyInfoAsn + { + Algorithm = new ValueAlgorithmIdentifierAsn + { + Algorithm = Oids.X25519, + }, + SubjectPublicKey = publicKey, + }; + + // The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is 12 bytes. + // Round it off to 16. + const int Capacity = 16 + PublicKeySizeInBytes; + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER, Capacity); + spki.Encode(writer); + return writer; + } + + private TResult ExportPkcs8PrivateKeyCallback(Func, TResult> func) + { + // A PKCS#8 X25519 PrivateKeyInfo has an ASN.1 overhead of 16 bytes, assuming no attributes. + // Make it an even 32 and that should give a good starting point for a buffer size. + int size = PrivateKeySizeInBytes + 32; + byte[] buffer = CryptoPool.Rent(size); + int written; + + while (!TryExportPkcs8PrivateKeyCore(buffer, out written)) + { + CryptoPool.Return(buffer); + size = checked(size * 2); + buffer = CryptoPool.Rent(size); + } + + if (written < 0 || written > buffer.Length) + { + CryptographicOperations.ZeroMemory(buffer); + throw new CryptographicException(); + } + + TResult result = func(buffer.AsSpan(0, written)); + CryptoPool.Return(buffer, written); + return result; + } + + private protected bool TryExportPkcs8PrivateKeyImpl(Span destination, out int bytesWritten) + { + ValueAlgorithmIdentifierAsn algorithmIdentifier = new() + { + Algorithm = Oids.X25519, + }; + + Span privateKey = stackalloc byte[PrivateKeySizeInBytes]; + ExportPrivateKey(privateKey); + + try + { + AsnWriter algorithmWriter = new(AsnEncodingRules.DER); + algorithmIdentifier.Encode(algorithmWriter); + AsnWriter privateKeyWriter = new(AsnEncodingRules.DER); + privateKeyWriter.WriteOctetString(privateKey); + AsnWriter pkcs8Writer = KeyFormatHelper.WritePkcs8(algorithmWriter, privateKeyWriter); + + bool result = pkcs8Writer.TryEncode(destination, out bytesWritten); + privateKeyWriter.Reset(); + pkcs8Writer.Reset(); + return result; + } + finally + { + CryptographicOperations.ZeroMemory(privateKey); + } + } + + private AsnWriter ExportEncryptedPkcs8PrivateKeyCore( + ReadOnlySpan password, + PbeParameters pbeParameters, + Func, AsnWriter, PbeParameters, AsnWriter> encryptor) + { + // A PKCS#8 X25519 PrivateKeyInfo has an ASN.1 overhead of 16 bytes, assuming no attributes. + // Make it an even 32 and that should give a good starting point for a buffer size. + int initialSize = PrivateKeySizeInBytes + 32; + byte[] rented = CryptoPool.Rent(initialSize); + int written; + + while (!TryExportPkcs8PrivateKey(rented, out written)) + { + CryptoPool.Return(rented, 0); + rented = CryptoPool.Rent(rented.Length * 2); + } + + AsnWriter tmp = new(AsnEncodingRules.BER, initialCapacity: written); + + try + { + tmp.WriteEncodedValueForCrypto(rented.AsSpan(0, written)); + return encryptor(password, tmp, pbeParameters); + } + finally + { + tmp.Reset(); + CryptoPool.Return(rented, written); + } + } + + private static void Pkcs8KeyReader( + ReadOnlySpan privateKeyContents, + in ValueAlgorithmIdentifierAsn algorithmIdentifier, + out X25519DiffieHellman key) + { + if (algorithmIdentifier.HasParameters) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ValueAsnReader reader = new(privateKeyContents, AsnEncodingRules.BER); + ReadOnlySpan privateKey = reader.ReadOctetString(); + reader.ThrowIfNotEmpty(); + + if (privateKey.Length != PrivateKeySizeInBytes) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } + + key = X25519DiffieHellmanImplementation.ImportPrivateKeyImpl(privateKey); + } + + private protected void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, typeof(X25519DiffieHellman)); + } + + private protected static void ThrowIfNotSupported() + { + if (!IsSupported) + { + throw new PlatformNotSupportedException( + SR.Format(SR.Cryptography_AlgorithmNotSupported, + nameof(X25519DiffieHellman))); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Apple.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Apple.cs new file mode 100644 index 00000000000000..fe20e8da5b5fc3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Apple.cs @@ -0,0 +1,96 @@ +// 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.Security.Cryptography.Apple; + +namespace System.Security.Cryptography +{ + internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman + { + private readonly SafeX25519KeyHandle _key; + private readonly bool _hasPrivate; + + internal static new bool IsSupported => true; + + private X25519DiffieHellmanImplementation(SafeX25519KeyHandle key, bool hasPrivate) + { + _key = key; + _hasPrivate = hasPrivate; + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + ThrowIfPrivateNeeded(); + + if (otherParty is X25519DiffieHellmanImplementation x25519impl) + { + Interop.AppleCrypto.X25519DeriveRawSecretAgreement(_key, x25519impl._key, destination); + } + else + { + Span publicKeyBuffer = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(publicKeyBuffer); + + using (SafeX25519KeyHandle publicKey = Interop.AppleCrypto.X25519ImportPublicKey(publicKeyBuffer)) + { + Interop.AppleCrypto.X25519DeriveRawSecretAgreement(_key, publicKey, destination); + } + } + } + + protected override void ExportPrivateKeyCore(Span destination) + { + ThrowIfPrivateNeeded(); + Debug.Assert(destination.Length == PrivateKeySizeInBytes); + Interop.AppleCrypto.X25519ExportPrivateKey(_key, destination); + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Assert(destination.Length == PublicKeySizeInBytes); + Interop.AppleCrypto.X25519ExportPublicKey(_key, destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + ThrowIfPrivateNeeded(); + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key.Dispose(); + } + + base.Dispose(disposing); + } + + internal static X25519DiffieHellmanImplementation GenerateKeyImpl() + { + return new X25519DiffieHellmanImplementation(Interop.AppleCrypto.X25519GenerateKey(), hasPrivate: true); + } + + internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlySpan source) + { + return new X25519DiffieHellmanImplementation( + Interop.AppleCrypto.X25519ImportPrivateKey(source), + hasPrivate: true); + } + + internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) + { + return new X25519DiffieHellmanImplementation( + Interop.AppleCrypto.X25519ImportPublicKey(source), + hasPrivate: false); + } + + private void ThrowIfPrivateNeeded() + { + if (!_hasPrivate) + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.NotSupported.cs new file mode 100644 index 00000000000000..f42527a2d5a67d --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.NotSupported.cs @@ -0,0 +1,56 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman + { + internal static new bool IsSupported => false; + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override void ExportPrivateKeyCore(Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + internal static X25519DiffieHellmanImplementation GenerateKeyImpl() + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlySpan source) + { + _ = source; + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) + { + _ = source; + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs new file mode 100644 index 00000000000000..434cfbf5cbe672 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs @@ -0,0 +1,109 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman + { + private readonly SafeEvpPKeyHandle _key; + private readonly bool _hasPrivate; + + internal static new bool IsSupported => true; + + private X25519DiffieHellmanImplementation(SafeEvpPKeyHandle key, bool hasPrivate) + { + _key = key; + _hasPrivate = hasPrivate; + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + ThrowIfPrivateNeeded(); + + int written; + + if (otherParty is X25519DiffieHellmanImplementation x25519Impl) + { + written = Interop.Crypto.EvpPKeyDeriveSecretAgreement(_key, x25519Impl._key, destination); + } + else + { + Span publicKey = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(publicKey); + + using (SafeEvpPKeyHandle peerKeyHandle = Interop.Crypto.X25519ImportPublicKey(publicKey)) + { + written = Interop.Crypto.EvpPKeyDeriveSecretAgreement(_key, peerKeyHandle, destination); + } + } + + if (written != SecretAgreementSizeInBytes) + { + Debug.Fail($"{nameof(Interop.Crypto.EvpPKeyDeriveSecretAgreement)} wrote an unexpected number of bytes: {written}."); + throw new CryptographicException(); + } + } + + protected override void ExportPrivateKeyCore(Span destination) + { + Debug.Assert(destination.Length == PrivateKeySizeInBytes); + ThrowIfPrivateNeeded(); + Interop.Crypto.X25519ExportPrivateKey(_key, destination); + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Assert(destination.Length == PublicKeySizeInBytes); + Interop.Crypto.X25519ExportPublicKey(_key, destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + ThrowIfPrivateNeeded(); + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key.Dispose(); + } + + base.Dispose(disposing); + } + + internal static X25519DiffieHellmanImplementation GenerateKeyImpl() + { + Debug.Assert(IsSupported); + SafeEvpPKeyHandle key = Interop.Crypto.X25519GenerateKey(); + Debug.Assert(!key.IsInvalid); + return new X25519DiffieHellmanImplementation(key, hasPrivate: true); + } + + internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlySpan source) + { + Debug.Assert(IsSupported); + SafeEvpPKeyHandle key = Interop.Crypto.X25519ImportPrivateKey(source); + Debug.Assert(!key.IsInvalid); + return new X25519DiffieHellmanImplementation(key, hasPrivate: true); + } + + internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) + { + Debug.Assert(IsSupported); + SafeEvpPKeyHandle key = Interop.Crypto.X25519ImportPublicKey(source); + Debug.Assert(!key.IsInvalid); + return new X25519DiffieHellmanImplementation(key, hasPrivate: false); + } + + private void ThrowIfPrivateNeeded() + { + if (!_hasPrivate) + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + } +} 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 new file mode 100644 index 00000000000000..c49550fe4fd69d --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Windows.cs @@ -0,0 +1,345 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Internal.NativeCrypto; +using Microsoft.Win32.SafeHandles; + +using NTSTATUS = Interop.BCrypt.NTSTATUS; + +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(); + + private readonly SafeBCryptKeyHandle _key; + private readonly bool _hasPrivate; + private readonly byte _privatePreservation; + + private X25519DiffieHellmanImplementation(SafeBCryptKeyHandle key, bool hasPrivate, byte privatePreservation) + { + _key = key; + _hasPrivate = hasPrivate; + _privatePreservation = privatePreservation; + Debug.Assert(_hasPrivate || _privatePreservation == 0); + } + + [MemberNotNullWhen(true, nameof(s_algHandle))] + internal static new bool IsSupported => s_algHandle is not null; + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + ThrowIfPrivateNeeded(); + int written; + + if (otherParty is X25519DiffieHellmanImplementation x25519impl) + { + using (SafeBCryptSecretHandle secret = Interop.BCrypt.BCryptSecretAgreement(_key, x25519impl._key)) + { + Interop.BCrypt.BCryptDeriveKey( + secret, + BCryptNative.KeyDerivationFunction.Raw, + in Unsafe.NullRef(), + destination, + out written); + } + } + else + { + Span publicKeyBytes = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(publicKeyBytes); + + using (SafeBCryptKeyHandle otherPartyHandle = ImportKey(false, publicKeyBytes, out _)) + using (SafeBCryptSecretHandle secret = Interop.BCrypt.BCryptSecretAgreement(_key, otherPartyHandle)) + { + Interop.BCrypt.BCryptDeriveKey( + secret, + BCryptNative.KeyDerivationFunction.Raw, + in Unsafe.NullRef(), + destination, + out written); + } + } + + if (written != SecretAgreementSizeInBytes) + { + destination.Clear(); + Debug.Fail($"Unexpected number of bytes written: {written}."); + throw new CryptographicException(); + } + else + { + // BCryptDeriveKey exports with the wrong endianness. + destination.Reverse(); + } + } + + protected override void ExportPrivateKeyCore(Span destination) + { + ExportKey(true, destination); + RefixPrivateScalar(destination, _privatePreservation); + } + + protected override void ExportPublicKeyCore(Span destination) + { + ExportKey(false, destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + ThrowIfPrivateNeeded(); + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key.Dispose(); + } + + base.Dispose(disposing); + } + + internal static X25519DiffieHellmanImplementation GenerateKeyImpl() + { + Debug.Assert(IsSupported); + SafeBCryptKeyHandle key = Interop.BCrypt.BCryptGenerateKeyPair(s_algHandle, 0); + Debug.Assert(!key.IsInvalid); + + try + { + Interop.BCrypt.BCryptFinalizeKeyPair(key); + return new X25519DiffieHellmanImplementation(key, hasPrivate: true, privatePreservation: 0); + } + catch + { + key.Dispose(); + throw; + } + } + + internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlySpan source) + { + SafeBCryptKeyHandle key = ImportKey(true, source, out byte preservation); + Debug.Assert(!key.IsInvalid); + return new X25519DiffieHellmanImplementation(key, hasPrivate: true, privatePreservation: preservation); + } + + internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) + { + SafeBCryptKeyHandle key = ImportKey(false, source, out _); + Debug.Assert(!key.IsInvalid); + return new X25519DiffieHellmanImplementation(key, hasPrivate: false, privatePreservation: 0); + } + + 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); + } + } + } + } + finally + { + if (privateKey) + { + CryptoPool.Return(key); + } + else + { + CryptoPool.Return(key, clearSize: 0); + } + } + } + + private static SafeBCryptKeyHandle ImportKey(bool privateKey, ReadOnlySpan key, out byte preservation) + { + Debug.Assert(IsSupported); + string blobType = privateKey ? + 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 + { + 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); + } + finally + { + if (privateKey) + { + CryptoPool.Return(rented); + } + else + { + CryptoPool.Return(rented, clearSize: 0); + } + } + } + } + + 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) + { + if (preservation != 0) + { + bytes[0] = (byte)((preservation & 0b111) | (bytes[0] & 0b11111000)); + bytes[^1] = (byte)((preservation & 0b11000000) | (bytes[^1] & 0b00111111)); + } + } + + private static SafeBCryptAlgorithmHandle? OpenAlgorithmHandle() + { + NTSTATUS status = Interop.BCrypt.BCryptOpenAlgorithmProvider( + out SafeBCryptAlgorithmHandle hAlgorithm, + BCryptNative.AlgorithmName.ECDH, + pszImplementation: null, + Interop.BCrypt.BCryptOpenAlgorithmProviderFlags.None); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + hAlgorithm.Dispose(); + return null; + } + + unsafe + { + fixed (char* pbInput = BCRYPT_ECC_CURVE_25519) + { + status = Interop.BCrypt.BCryptSetProperty( + hAlgorithm, + KeyPropertyName.ECCCurveName, + pbInput, + ((uint)BCRYPT_ECC_CURVE_25519.Length + 1) * 2, + 0); + } + } + + if (status != NTSTATUS.STATUS_SUCCESS) + { + hAlgorithm.Dispose(); + return null; + } + + return hAlgorithm; + } + + private void ThrowIfPrivateNeeded() + { + if (!_hasPrivate) + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + } +} 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 13591541685b9f..cac44ca8e544a2 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 @@ -234,6 +234,8 @@ Link="CommonTest\System\Security\Cryptography\MLKemAlgorithmTests.cs" /> + + + + + + + + diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs new file mode 100644 index 00000000000000..c749d97dd2455a --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs @@ -0,0 +1,607 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public abstract class X25519DiffieHellmanBaseTests + { + private static readonly PbeParameters s_aes128Pbe = new(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 2); + + public abstract X25519DiffieHellman GenerateKey(); + public abstract X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source); + public abstract X25519DiffieHellman ImportPublicKey(ReadOnlySpan source); + + // SymCrypt, thus SCOSSL and and CNG, are stricter about keys they are willing to import. These keys fall in to + // two buckets. + // 1. Public keys that are non-canonical. RFC 7748 says: + // Implementations MUST accept non-canonical values and process them as + // if they had been reduced modulo the field prime. The non-canonical + // values are 2^255 - 19 through 2^255 - 1 for X25519 + // Regardless, SymCrypt rejects these non-canonical keys anyway. There are only 20 possible keys that fall in to this category. + // 2. Public keys that are not in the prime-order subgroup. [GOrd] * P ≠ O. + // X25519DH doesn't strictly need this, but Windows enforces this property anyway. + public static bool IsStrictKeyValidatingPlatform => OperatingSystem.IsWindows() || PlatformDetection.IsSymCryptOpenSsl; + + [Fact] + public void ExportPrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = GenerateKey(); + + byte[] privateKey = xdh.ExportPrivateKey(); + Assert.True(privateKey.AsSpan().ContainsAnyExcept((byte)0)); + Assert.Equal(X25519DiffieHellman.PrivateKeySizeInBytes, privateKey.Length); + + Span privateKeySpan = new byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + xdh.ExportPrivateKey(privateKeySpan); + AssertExtensions.SequenceEqual(privateKey.AsSpan(), privateKeySpan); + + using X25519DiffieHellman imported = ImportPrivateKey(privateKey); + AssertExtensions.SequenceEqual(privateKey, imported.ExportPrivateKey()); + } + + [Fact] + public void ExportPublicKey_Roundtrip() + { + using X25519DiffieHellman xdh = GenerateKey(); + + byte[] publicKey = xdh.ExportPublicKey(); + Assert.True(publicKey.AsSpan().ContainsAnyExcept((byte)0)); + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, publicKey.Length); + + Span publicKeySpan = new byte[X25519DiffieHellman.PublicKeySizeInBytes]; + xdh.ExportPublicKey(publicKeySpan); + AssertExtensions.SequenceEqual(publicKey.AsSpan(), publicKeySpan); + + using X25519DiffieHellman imported = ImportPublicKey(publicKey); + AssertExtensions.SequenceEqual(publicKey, imported.ExportPublicKey()); + } + + [Fact] + public void ExportPublicKey_PublicKeyOnly() + { + using X25519DiffieHellman xdh = GenerateKey(); + byte[] publicKey = xdh.ExportPublicKey(); + + using X25519DiffieHellman publicOnly = ImportPublicKey(publicKey); + AssertExtensions.SequenceEqual(publicKey, publicOnly.ExportPublicKey()); + } + + [Fact] + public void ExportPrivateKey_PublicKeyOnly_Throws() + { + using X25519DiffieHellman xdh = GenerateKey(); + using X25519DiffieHellman publicOnly = ImportPublicKey(xdh.ExportPublicKey()); + + Assert.Throws(() => publicOnly.ExportPrivateKey()); + Assert.Throws(() => publicOnly.ExportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes])); + } + + [Fact] + public void DeriveRawSecretAgreement_Symmetric() + { + using X25519DiffieHellman key1 = GenerateKey(); + using X25519DiffieHellman key2 = GenerateKey(); + + byte[] secret1 = key1.DeriveRawSecretAgreement(key2); + byte[] secret2 = key2.DeriveRawSecretAgreement(key1); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_ExactBuffers() + { + using X25519DiffieHellman key1 = GenerateKey(); + using X25519DiffieHellman key2 = GenerateKey(); + + byte[] secret1 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + byte[] secret2 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + key1.DeriveRawSecretAgreement(key2, secret1); + key2.DeriveRawSecretAgreement(key1, secret2); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_PublicKeyOnly_Throws() + { + using X25519DiffieHellman xdh = GenerateKey(); + using X25519DiffieHellman publicOnly = ImportPublicKey(xdh.ExportPublicKey()); + using X25519DiffieHellman other = GenerateKey(); + + Assert.Throws(() => publicOnly.DeriveRawSecretAgreement(other)); + Assert.Throws(() => publicOnly.DeriveRawSecretAgreement(other, new byte[X25519DiffieHellman.SecretAgreementSizeInBytes])); + } + + [Fact] + public void DeriveRawSecretAgreement_Vectors() + { + foreach (DeriveSecretAgreementVector vector in DeriveSecretAgreementVectors) + { + using X25519DiffieHellman key = ImportPrivateKey(vector.PrivateKey); + using X25519DiffieHellman peer = ImportPublicKey(vector.PeerPublicKey); + + // Allocating + byte[] secret = key.DeriveRawSecretAgreement(peer); + AssertExtensions.SequenceEqual(vector.SharedSecret, secret); + + // Exact buffers + byte[] secretBuffer = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + key.DeriveRawSecretAgreement(peer, secretBuffer); + AssertExtensions.SequenceEqual(vector.SharedSecret, secretBuffer); + } + } + + [Fact] + public void DeriveRawSecretAgreement_CrossImplementation() + { + foreach (DeriveSecretAgreementVector vector in DeriveSecretAgreementVectors) + { + using X25519DiffieHellman key = ImportPrivateKey(vector.PrivateKey); + using X25519DiffieHellmanWrapper wrapper = new(ImportPublicKey(vector.PeerPublicKey)); + + byte[] secret = key.DeriveRawSecretAgreement(wrapper); + AssertExtensions.SequenceEqual(vector.SharedSecret, secret); + + byte[] secretBuffer = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + key.DeriveRawSecretAgreement(wrapper, secretBuffer); + AssertExtensions.SequenceEqual(vector.SharedSecret, secretBuffer); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SubjectPublicKeyInfo_Roundtrip(bool useTryExport) + { + using X25519DiffieHellman xdh = ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + AssertSubjectPublicKeyInfo(xdh, useTryExport, X25519DiffieHellmanTestData.AliceSpki); + } + + [Fact] + public void ExportSubjectPublicKeyInfo_Allocated_Independent() + { + using X25519DiffieHellman xdh = GenerateKey(); + xdh.ExportSubjectPublicKeyInfo().AsSpan().Clear(); + byte[] spki1 = xdh.ExportSubjectPublicKeyInfo(); + byte[] spki2 = xdh.ExportSubjectPublicKeyInfo(); + Assert.NotSame(spki1, spki2); + AssertExtensions.SequenceEqual(spki1, spki2); + } + + [Fact] + public void TryExportSubjectPublicKeyInfo_Buffers() + { + using X25519DiffieHellman xdh = GenerateKey(); + byte[] expectedSpki = xdh.ExportSubjectPublicKeyInfo(); + byte[] buffer; + int written; + + buffer = new byte[expectedSpki.Length - 1]; + Assert.False(xdh.TryExportSubjectPublicKeyInfo(buffer, out written)); + Assert.Equal(0, written); + + buffer = new byte[expectedSpki.Length]; + Assert.True(xdh.TryExportSubjectPublicKeyInfo(buffer, out written)); + Assert.Equal(expectedSpki.Length, written); + AssertExtensions.SequenceEqual(expectedSpki, buffer); + + buffer = new byte[expectedSpki.Length + 42]; + Assert.True(xdh.TryExportSubjectPublicKeyInfo(buffer, out written)); + Assert.Equal(expectedSpki.Length, written); + AssertExtensions.SequenceEqual(expectedSpki.AsSpan(), buffer.AsSpan(0, written)); + } + + [Fact] + public void ExportPkcs8PrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + + AssertExportPkcs8PrivateKey(xdh, pkcs8 => + { + using X25519DiffieHellman imported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); + AssertExtensions.SequenceEqual( + X25519DiffieHellmanTestData.AlicePrivateKey, + imported.ExportPrivateKey()); + }); + } + + [Fact] + public void ExportPkcs8PrivateKey_PublicKeyOnly_Fails() + { + using X25519DiffieHellman xdh = ImportPublicKey(X25519DiffieHellmanTestData.AlicePublicKey); + Assert.Throws(() => DoTryUntilDone(xdh.TryExportPkcs8PrivateKey)); + Assert.Throws(() => xdh.ExportPkcs8PrivateKey()); + } + + [Fact] + public void ExportEncryptedPkcs8PrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + AssertEncryptedExportPkcs8PrivateKey( + xdh, + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe, + pkcs8 => + { + using X25519DiffieHellman imported = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + pkcs8); + + AssertExtensions.SequenceEqual( + X25519DiffieHellmanTestData.AlicePrivateKey, + imported.ExportPrivateKey()); + }); + } + + [Fact] + public void ExportEncryptedPkcs8PrivateKey_PublicKeyOnly_Fails() + { + using X25519DiffieHellman xdh = ImportPublicKey(X25519DiffieHellmanTestData.AlicePublicKey); + + Assert.Throws(() => DoTryUntilDone((Span destination, out int bytesWritten) => + xdh.TryExportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), + s_aes128Pbe, + destination, + out bytesWritten))); + + Assert.Throws(() => DoTryUntilDone((Span destination, out int bytesWritten) => + xdh.TryExportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + destination, + out bytesWritten))); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, s_aes128Pbe)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, s_aes128Pbe)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKeyPem( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, s_aes128Pbe)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKeyPem( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, s_aes128Pbe)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKeyPem( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe)); + } + + [Theory] + [MemberData(nameof(ExportPkcs8Parameters))] + public void ExportEncryptedPkcs8PrivateKey_PbeParameters(PbeParameters pbeParameters) + { + using X25519DiffieHellman xdh = ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + AssertEncryptedExportPkcs8PrivateKey( + xdh, + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + pbeParameters, + pkcs8 => + { + Pkcs8TestHelpers.AssertEncryptedPkcs8PrivateKeyContents(pbeParameters, pkcs8); + }); + } + + public static IEnumerable ExportPkcs8Parameters + { + get + { + yield return [new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 42)]; + yield return [new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA512, 43)]; + yield return [new PbeParameters(PbeEncryptionAlgorithm.Aes192Cbc, HashAlgorithmName.SHA384, 44)]; + yield return [new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 24)]; + } + } + + [Fact] + public void PrivateKey_Roundtrip_UnclampedScalar() + { + byte[] privateKey = X25519DiffieHellmanTestData.BobPrivateKey; + using X25519DiffieHellman xdh = ImportPrivateKey(privateKey); + + 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()); + } + + [Fact] + public void PrivateKey_Roundtrip_ClampedScalar() + { + byte[] privateKey = (byte[])X25519DiffieHellmanTestData.AlicePrivateKey.Clone(); + privateKey[0] &= 0b11111000; + privateKey[^1] &= 0b01111111; + privateKey[^1] |= 0b01000000; + + using X25519DiffieHellman xdh = ImportPrivateKey(privateKey); + AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public void PrivateKey_ClampedAndUnclamped_SamePublicKey() + { + byte[] unclamped = (byte[])X25519DiffieHellmanTestData.AlicePrivateKey.Clone(); + byte[] clamped = (byte[])unclamped.Clone(); + clamped[0] &= 0b11111000; + clamped[^1] &= 0b01111111; + clamped[^1] |= 0b01000000; + + using X25519DiffieHellman xdhUnclamped = ImportPrivateKey(unclamped); + using X25519DiffieHellman xdhClamped = ImportPrivateKey(clamped); + + AssertExtensions.SequenceEqual(xdhUnclamped.ExportPublicKey(), xdhClamped.ExportPublicKey()); + } + + private static void AssertSubjectPublicKeyInfo(X25519DiffieHellman xdh, bool useTryExport, ReadOnlySpan expectedSpki) + { + byte[] spki; + int written; + + if (useTryExport) + { + spki = new byte[X25519DiffieHellman.PublicKeySizeInBytes + 16]; + Assert.True(xdh.TryExportSubjectPublicKeyInfo(spki, out written)); + } + else + { + spki = xdh.ExportSubjectPublicKeyInfo(); + written = spki.Length; + } + + ReadOnlySpan encodedSpki = spki.AsSpan(0, written); + AssertExtensions.SequenceEqual(expectedSpki, encodedSpki); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportSubjectPublicKeyInfo(encodedSpki); + AssertExtensions.SequenceEqual(xdh.ExportPublicKey(), imported.ExportPublicKey()); + } + + private static void AssertExportPkcs8PrivateKey(X25519DiffieHellman xdh, Action callback) + { + callback(DoTryUntilDone(xdh.TryExportPkcs8PrivateKey)); + callback(xdh.ExportPkcs8PrivateKey()); + } + + private static void AssertEncryptedExportPkcs8PrivateKey( + X25519DiffieHellman xdh, + string password, + PbeParameters pbeParameters, + Action callback) + { + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + + callback(DoTryUntilDone((Span destination, out int bytesWritten) => + { + return xdh.TryExportEncryptedPkcs8PrivateKey( + password.AsSpan(), + pbeParameters, + destination, + out bytesWritten); + })); + + callback(xdh.ExportEncryptedPkcs8PrivateKey(password, pbeParameters)); + callback(xdh.ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters)); + callback(DecodePem(xdh.ExportEncryptedPkcs8PrivateKeyPem(password, pbeParameters))); + callback(DecodePem(xdh.ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters))); + + if (pbeParameters.EncryptionAlgorithm != PbeEncryptionAlgorithm.TripleDes3KeyPkcs12) + { + callback(DoTryUntilDone((Span destination, out int bytesWritten) => + { + return xdh.TryExportEncryptedPkcs8PrivateKey( + new ReadOnlySpan(passwordBytes), + pbeParameters, + destination, + out bytesWritten); + })); + + callback(xdh.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(passwordBytes), pbeParameters)); + callback(DecodePem(xdh.ExportEncryptedPkcs8PrivateKeyPem(new ReadOnlySpan(passwordBytes), pbeParameters))); + } + + static byte[] DecodePem(string pem) + { + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("ENCRYPTED PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString()); + } + } + + [Fact] + public void DeriveRawSecretAgreement_NonPlatformOtherParty() + { + using X25519DiffieHellman key1 = GenerateKey(); + using X25519DiffieHellman key2 = GenerateKey(); + + // Wrap key2 in a non-platform wrapper. This forces the implementation to go through + // the ExportPublicKey fallback path rather than using the native key handle directly. + using X25519DiffieHellmanWrapper wrapper = new(key2); + + byte[] secret1 = key1.DeriveRawSecretAgreement(wrapper); + byte[] secret2 = key2.DeriveRawSecretAgreement(key1); + + AssertExtensions.SequenceEqual(secret1, secret2); + Assert.True(wrapper.ExportPublicKeyCoreWasCalled); + } + + [Fact] + public void DeriveRawSecretAgreement_NonPlatformOtherParty_ExactBuffers() + { + using X25519DiffieHellman key1 = GenerateKey(); + using X25519DiffieHellman key2 = GenerateKey(); + using X25519DiffieHellmanWrapper wrapper = new(key2); + + byte[] secret1 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + byte[] secret2 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + key1.DeriveRawSecretAgreement(wrapper, secret1); + key2.DeriveRawSecretAgreement(key1, secret2); + + AssertExtensions.SequenceEqual(secret1, secret2); + Assert.True(wrapper.ExportPublicKeyCoreWasCalled); + } + + private delegate bool TryExportFunc(Span destination, out int bytesWritten); + + private static byte[] DoTryUntilDone(TryExportFunc func) + { + byte[] buffer = new byte[512]; + int written; + + while (!func(buffer, out written)) + { + Array.Resize(ref buffer, buffer.Length * 2); + } + + return buffer.AsSpan(0, written).ToArray(); + } + + /// + /// A wrapper around an X25519DiffieHellman instance that is not the platform's + /// internal implementation type. This forces the DeriveRawSecretAgreementCore fallback + /// path that exports the public key and re-imports it. + /// + private sealed class X25519DiffieHellmanWrapper : X25519DiffieHellman + { + private readonly X25519DiffieHellman _inner; + + public bool ExportPublicKeyCoreWasCalled { get; private set; } + + public X25519DiffieHellmanWrapper(X25519DiffieHellman inner) + { + _inner = inner; + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + _inner.DeriveRawSecretAgreement(otherParty, destination); + } + + protected override void ExportPrivateKeyCore(Span destination) + { + _inner.ExportPrivateKey(destination); + } + + protected override void ExportPublicKeyCore(Span destination) + { + ExportPublicKeyCoreWasCalled = true; + _inner.ExportPublicKey(destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + return _inner.TryExportPkcs8PrivateKey(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + // Don't dispose _inner; the test owns it. + base.Dispose(disposing); + } + } + + public record DeriveSecretAgreementVector(string Name, byte[] PrivateKey, byte[] PeerPublicKey, byte[] SharedSecret); + + public static IEnumerable DeriveSecretAgreementVectors + { + get + { + // Wycheproof Cases 2-4: low-order shared secret results (same private key) + byte[] wycheproofPrivate2 = + [ + 0xa0, 0xa4, 0xf1, 0x30, 0xb9, 0x8a, 0x5b, 0xe4, 0xb1, 0xce, 0xdb, 0x7c, 0xb8, 0x55, 0x84, 0xa3, + 0x52, 0x0e, 0x14, 0x2d, 0x47, 0x4d, 0xc9, 0xcc, 0xb9, 0x09, 0xa0, 0x73, 0xa9, 0x76, 0xbf, 0x63, + ]; + + // RFC 7748 Section 6.1 + yield return new("RFC7748_Alice", + X25519DiffieHellmanTestData.AlicePrivateKey, + X25519DiffieHellmanTestData.BobPublicKey, + X25519DiffieHellmanTestData.SharedSecret); + + yield return new("RFC7748_Bob", + X25519DiffieHellmanTestData.BobPrivateKey, + X25519DiffieHellmanTestData.AlicePublicKey, + X25519DiffieHellmanTestData.SharedSecret); + + if (!IsStrictKeyValidatingPlatform) + { + // Wycheproof Case 0: near-max public key (0xF0FF...FF7F) + yield return new("Wycheproof_0", + [ + 0x28, 0x87, 0x96, 0xbc, 0x5a, 0xff, 0x4b, 0x81, 0xa3, 0x75, 0x01, 0x75, 0x7b, 0xc0, 0x75, 0x3a, + 0x3c, 0x21, 0x96, 0x47, 0x90, 0xd3, 0x86, 0x99, 0x30, 0x8d, 0xeb, 0xc1, 0x7a, 0x6e, 0xaf, 0x8d, + ], + [ + 0xf0, 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, + ], + [ + 0xb4, 0xe0, 0xdd, 0x76, 0xda, 0x7b, 0x07, 0x17, 0x28, 0xb6, 0x1f, 0x85, 0x67, 0x71, 0xaa, 0x35, + 0x6e, 0x57, 0xed, 0xa7, 0x8a, 0x5b, 0x16, 0x55, 0xcc, 0x38, 0x20, 0xfb, 0x5f, 0x85, 0x4c, 0x5c, + ]); + + // Wycheproof Case 1: all-bits-set public key (0xF0FF...FFFF) + yield return new("Wycheproof_1", + [ + 0x60, 0x88, 0x7b, 0x3d, 0xc7, 0x24, 0x43, 0x02, 0x6e, 0xbe, 0xdb, 0xbb, 0xb7, 0x06, 0x65, 0xf4, + 0x2b, 0x87, 0xad, 0xd1, 0x44, 0x0e, 0x77, 0x68, 0xfb, 0xd7, 0xe8, 0xe2, 0xce, 0x5f, 0x63, 0x9d, + ], + [ + 0xf0, 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, 0xff, + ], + [ + 0x38, 0xd6, 0x30, 0x4c, 0x4a, 0x7e, 0x6d, 0x9f, 0x79, 0x59, 0x33, 0x4f, 0xb5, 0x24, 0x5b, 0xd2, + 0xc7, 0x54, 0x52, 0x5d, 0x4c, 0x91, 0xdb, 0x95, 0x02, 0x06, 0x92, 0x62, 0x34, 0xc1, 0xf6, 0x33, + ]); + + yield return new("Wycheproof_2_LowOrder", + wycheproofPrivate2, + [ + 0x0a, 0xb4, 0xe7, 0x63, 0x80, 0xd8, 0x4d, 0xde, 0x4f, 0x68, 0x33, 0xc5, 0x8f, 0x2a, 0x9f, 0xb8, + 0xf8, 0x3b, 0xb0, 0x16, 0x9b, 0x17, 0x2b, 0xe4, 0xb6, 0xe0, 0x59, 0x28, 0x87, 0x74, 0x1a, 0x36, + ], + [ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + } + + yield return new("Wycheproof_3_LowOrder", + wycheproofPrivate2, + [ + 0x89, 0xe1, 0x0d, 0x57, 0x01, 0xb4, 0x33, 0x7d, 0x2d, 0x03, 0x21, 0x81, 0x53, 0x8b, 0x10, 0x64, + 0xbd, 0x40, 0x84, 0x40, 0x1c, 0xec, 0xa1, 0xfd, 0x12, 0x66, 0x3a, 0x19, 0x59, 0x38, 0x80, 0x00, + ], + [ + 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + + yield return new("Wycheproof_4_LowOrder", + wycheproofPrivate2, + [ + 0x2b, 0x55, 0xd3, 0xaa, 0x4a, 0x8f, 0x80, 0xc8, 0xc0, 0xb2, 0xae, 0x5f, 0x93, 0x3e, 0x85, 0xaf, + 0x49, 0xbe, 0xac, 0x36, 0xc2, 0xfa, 0x73, 0x94, 0xba, 0xb7, 0x6c, 0x89, 0x33, 0xf8, 0xf8, 0x1d, + ], + [ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs new file mode 100644 index 00000000000000..72f8c947d81883 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs @@ -0,0 +1,873 @@ +// 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.Runtime.CompilerServices; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + public static class X25519DiffieHellmanContractTests + { + private static readonly PbeParameters s_aes128Pbe = new(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 2); + + private const string EncryptedPrivateKeyPassword = "PLACEHOLDER"; + private static ReadOnlySpan EncryptedPrivateKeyPasswordBytes => "PLACEHOLDER"u8; + + // A valid PKCS#8 PrivateKeyInfo for X25519 with an all-0x42 private key. + private static ReadOnlySpan TestPkcs8PrivateKey => + [ + 0x30, 0x2E, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x6E, + 0x04, 0x22, 0x04, 0x20, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + ]; + + [Theory] + [InlineData(1)] + [InlineData(7)] + public static void Dispose_OnDisposing(int disposeCalls) + { + int count = 0; + X25519DiffieHellmanContract xdh = new() + { + OnDispose = (bool disposing) => + { + count++; + AssertExtensions.TrueExpression(disposing); + } + }; + + for (int i = 0; i < disposeCalls; i++) + { + xdh.Dispose(); + } + + Assert.Equal(1, count); + } + + [Fact] + public static void DeriveRawSecretAgreement_Allocated_NullOtherParty() + { + using X25519DiffieHellmanContract xdh = new(); + AssertExtensions.Throws("otherParty", () => + xdh.DeriveRawSecretAgreement((X25519DiffieHellman)null)); + } + + [Fact] + public static void DeriveRawSecretAgreement_Exact_NullOtherParty() + { + using X25519DiffieHellmanContract xdh = new(); + AssertExtensions.Throws("otherParty", () => + xdh.DeriveRawSecretAgreement(null, new byte[X25519DiffieHellman.SecretAgreementSizeInBytes])); + } + + [Fact] + public static void DeriveRawSecretAgreement_Exact_WrongDestinationLength() + { + using X25519DiffieHellmanContract xdh = new(); + using X25519DiffieHellmanContract other = new(); + + AssertExtensions.Throws("destination", () => + xdh.DeriveRawSecretAgreement(other, new byte[X25519DiffieHellman.SecretAgreementSizeInBytes - 1])); + + AssertExtensions.Throws("destination", () => + xdh.DeriveRawSecretAgreement(other, new byte[X25519DiffieHellman.SecretAgreementSizeInBytes + 1])); + } + + [Fact] + public static void DeriveRawSecretAgreement_Allocated_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + using X25519DiffieHellmanContract other = new(); + Assert.Throws(() => xdh.DeriveRawSecretAgreement(other)); + } + + [Fact] + public static void DeriveRawSecretAgreement_Exact_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + using X25519DiffieHellmanContract other = new(); + Assert.Throws(() => + xdh.DeriveRawSecretAgreement(other, new byte[X25519DiffieHellman.SecretAgreementSizeInBytes])); + } + + [Fact] + public static void DeriveRawSecretAgreement_Allocated_Works() + { + using X25519DiffieHellmanContract other = new(); + using X25519DiffieHellmanContract xdh = new() + { + OnDeriveRawSecretAgreementCore = (X25519DiffieHellman otherParty, Span destination) => + { + Assert.Same(other, otherParty); + destination.Fill(0xAA); + } + }; + + byte[] agreement = xdh.DeriveRawSecretAgreement(other); + Assert.Equal(X25519DiffieHellman.SecretAgreementSizeInBytes, agreement.Length); + AssertExtensions.FilledWith(0xAA, agreement); + } + + [Fact] + public static void DeriveRawSecretAgreement_Exact_Works() + { + byte[] buffer = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + using X25519DiffieHellmanContract other = new(); + using X25519DiffieHellmanContract xdh = new() + { + OnDeriveRawSecretAgreementCore = (X25519DiffieHellman otherParty, Span destination) => + { + Assert.Same(other, otherParty); + AssertExtensions.Same(buffer, destination); + } + }; + + xdh.DeriveRawSecretAgreement(other, buffer); + } + + [Fact] + public static void ExportPrivateKey_Exact_WrongSize() + { + using X25519DiffieHellmanContract xdh = new(); + AssertExtensions.Throws("destination", () => + xdh.ExportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes - 1])); + AssertExtensions.Throws("destination", () => + xdh.ExportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes + 1])); + } + + [Fact] + public static void ExportPrivateKey_Exact_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => + xdh.ExportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes])); + } + + [Fact] + public static void ExportPrivateKey_Exact_Works() + { + byte[] buffer = new byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + using X25519DiffieHellmanContract xdh = new() + { + OnExportPrivateKeyCore = (Span destination) => + { + AssertExtensions.Same(buffer, destination); + } + }; + + xdh.ExportPrivateKey(buffer); + } + + [Fact] + public static void ExportPrivateKey_Allocated_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportPrivateKey()); + } + + [Fact] + public static void ExportPrivateKey_Allocated_Works() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPrivateKeyCore = (Span destination) => + { + destination.Fill(0x42); + } + }; + + byte[] exported = xdh.ExportPrivateKey(); + Assert.Equal(X25519DiffieHellman.PrivateKeySizeInBytes, exported.Length); + AssertExtensions.FilledWith(0x42, exported); + } + + [Fact] + public static void ExportPublicKey_Exact_WrongSize() + { + using X25519DiffieHellmanContract xdh = new(); + AssertExtensions.Throws("destination", () => + xdh.ExportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes - 1])); + AssertExtensions.Throws("destination", () => + xdh.ExportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes + 1])); + } + + [Fact] + public static void ExportPublicKey_Exact_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => + xdh.ExportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes])); + } + + [Fact] + public static void ExportPublicKey_Exact_Works() + { + byte[] buffer = new byte[X25519DiffieHellman.PublicKeySizeInBytes]; + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + AssertExtensions.Same(buffer, destination); + } + }; + + xdh.ExportPublicKey(buffer); + } + + [Fact] + public static void ExportPublicKey_Allocated_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportPublicKey()); + } + + [Fact] + public static void ExportPublicKey_Allocated_Works() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + destination.Fill(0x42); + } + }; + + byte[] exported = xdh.ExportPublicKey(); + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, exported.Length); + AssertExtensions.FilledWith(0x42, exported); + } + + [Fact] + public static void TryExportSubjectPublicKeyInfo_Buffers() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + destination.Fill(0x42); + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, destination.Length); + } + }; + + byte[] destination = new byte[256]; + destination.AsSpan().Fill(0xFF); + AssertExtensions.TrueExpression(xdh.TryExportSubjectPublicKeyInfo(destination, out int written)); + ReadSubjectPublicKeyInfo( + destination.AsMemory(0, written), + out string oid, + out ReadOnlyMemory? parameters, + out ReadOnlyMemory publicKey); + + Assert.Equal("1.3.101.110", oid); + AssertExtensions.FalseExpression(parameters.HasValue); + AssertExtensions.FilledWith(0x42, publicKey.Span); + AssertExtensions.FilledWith(0xFF, destination.AsSpan(written)); + } + + [Fact] + public static void TryExportSubjectPublicKeyInfo_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.TryExportSubjectPublicKeyInfo([], out _)); + } + + [Fact] + public static void TryExportSubjectPublicKeyInfo_DestinationTooSmall() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + destination.Fill(0x42); + } + }; + + byte[] destination = new byte[1]; + AssertExtensions.FalseExpression(xdh.TryExportSubjectPublicKeyInfo(destination, out int written)); + Assert.Equal(0, written); + } + + [Fact] + public static void ExportSubjectPublicKeyInfo_Allocated() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + destination.Fill(0x42); + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, destination.Length); + } + }; + + byte[] spki = xdh.ExportSubjectPublicKeyInfo(); + ReadSubjectPublicKeyInfo( + spki, + out string oid, + out ReadOnlyMemory? parameters, + out ReadOnlyMemory publicKey); + + Assert.Equal("1.3.101.110", oid); + AssertExtensions.FalseExpression(parameters.HasValue); + AssertExtensions.FilledWith(0x42, publicKey.Span); + } + + [Fact] + public static void ExportSubjectPublicKeyInfoPem() + { + using X25519DiffieHellmanContract xdh = new() + { + OnExportPublicKeyCore = (Span destination) => + { + destination.Fill(0x42); + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, destination.Length); + } + }; + + string spkiPem = xdh.ExportSubjectPublicKeyInfoPem(); + const string ExpectedPem = + "-----BEGIN PUBLIC KEY-----\n" + + "MCowBQYDK2VuAyEAQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI=\n" + + "-----END PUBLIC KEY-----"; + Assert.Equal(ExpectedPem, spkiPem); + } + + [Fact] + public static void ExportSubjectPublicKeyInfo_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportSubjectPublicKeyInfo()); + } + + [Fact] + public static void TryExportPkcs8PrivateKey_EarlyExitForSmallBuffer() + { + X25519DiffieHellmanContract xdh = new(); + byte[] destination = new byte[47]; + AssertExtensions.FalseExpression(xdh.TryExportPkcs8PrivateKey(destination, out int written)); + Assert.Equal(0, written); + } + + [Fact] + public static void TryExportPkcs8PrivateKey() + { + int bufferSize = Random.Shared.Next(50, 1024); + int writtenSize = Random.Shared.Next(48, bufferSize); + bool success = (writtenSize & 1) == 1; + byte[] buffer = new byte[bufferSize]; + X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + AssertExtensions.Same(buffer, destination); + bytesWritten = writtenSize; + return success; + } + }; + + AssertExtensions.TrueExpression(success == xdh.TryExportPkcs8PrivateKey(buffer, out int written)); + Assert.Equal(writtenSize, written); + } + + [Fact] + public static void ExportPkcs8PrivateKey_OneExportCall() + { + int size = -1; + X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + destination.Fill(0x88); + bytesWritten = destination.Length; + size = destination.Length; + return true; + } + }; + + byte[] exported = xdh.ExportPkcs8PrivateKey(); + AssertExtensions.FilledWith(0x88, exported); + Assert.Equal(size, exported.Length); + } + + [Fact] + public static void ExportPkcs8PrivateKey_ExpandAndRetry() + { + const int TargetSize = 4567; + X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (destination.Length < TargetSize) + { + bytesWritten = 0; + return false; + } + + destination.Fill(0x88); + bytesWritten = TargetSize; + return true; + } + }; + + byte[] exported = xdh.ExportPkcs8PrivateKey(); + AssertExtensions.FilledWith(0x88, exported); + Assert.Equal(TargetSize, exported.Length); + AssertExtensions.GreaterThan(xdh.TryExportPkcs8PrivateKeyCoreCount, 1); + } + + [Fact] + public static void ExportPkcs8PrivateKey_MisbehavingBytesWritten_Oversized() + { + X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + bytesWritten = destination.Length + 1; + return true; + } + }; + + Assert.Throws(() => xdh.ExportPkcs8PrivateKey()); + } + + [Fact] + public static void ExportPkcs8PrivateKey_MisbehavingBytesWritten_Negative() + { + X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + bytesWritten = -1; + return true; + } + }; + + Assert.Throws(() => xdh.ExportPkcs8PrivateKey()); + } + + [Fact] + public static void ExportPkcs8PrivateKey_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportPkcs8PrivateKey()); + Assert.Throws(() => xdh.TryExportPkcs8PrivateKey(new byte[512], out _)); + } + + [Fact] + public static void ExportPkcs8PrivateKeyPem() + { + using X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (TestPkcs8PrivateKey.TryCopyTo(destination)) + { + bytesWritten = TestPkcs8PrivateKey.Length; + return true; + } + + bytesWritten = 0; + return false; + } + }; + + string pem = xdh.ExportPkcs8PrivateKeyPem(); + byte[] pkcs8 = xdh.ExportPkcs8PrivateKey(); + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + AssertExtensions.SequenceEqual(pkcs8, Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString())); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + + Assert.Throws(() => xdh.TryExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe, new byte[2048], out _)); + Assert.Throws(() => xdh.TryExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPassword, s_aes128Pbe, new byte[2048], out _)); + Assert.Throws(() => xdh.TryExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPasswordBytes, s_aes128Pbe, new byte[2048], out _)); + + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPassword, s_aes128Pbe)); + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe)); + Assert.Throws(() => xdh.ExportEncryptedPkcs8PrivateKey( + EncryptedPrivateKeyPasswordBytes, s_aes128Pbe)); + } + + [Theory] + [InlineData(TryExportPkcs8PasswordKind.StringPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfBytesPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfCharsPassword)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] + public static void TryExportEncryptedPkcs8PrivateKey_ExportsPkcs8(TryExportPkcs8PasswordKind kind) + { + using X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (TestPkcs8PrivateKey.TryCopyTo(destination)) + { + bytesWritten = TestPkcs8PrivateKey.Length; + return true; + } + + Assert.Fail("Initial buffer was not correctly sized."); + bytesWritten = 0; + return false; + } + }; + + byte[] buffer = new byte[2048]; + bool success = TryExportEncryptedPkcs8PrivateKeyByKind(xdh, kind, buffer, out int written); + AssertExtensions.TrueExpression(success); + AssertExtensions.GreaterThan(written, 0); + Assert.Equal(1, xdh.TryExportPkcs8PrivateKeyCoreCount); + } + + [Theory] + [InlineData(TryExportPkcs8PasswordKind.StringPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfBytesPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfCharsPassword)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] + public static void TryExportEncryptedPkcs8PrivateKey_InnerBuffer_LargePkcs8(TryExportPkcs8PasswordKind kind) + { + using X25519DiffieHellmanContract xdh = new(); + xdh.OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (xdh.TryExportPkcs8PrivateKeyCoreCount < 2) + { + bytesWritten = 0; + return false; + } + + if (TestPkcs8PrivateKey.TryCopyTo(destination)) + { + bytesWritten = TestPkcs8PrivateKey.Length; + return true; + } + + bytesWritten = 0; + return false; + }; + + byte[] buffer = new byte[2048]; + bool success = TryExportEncryptedPkcs8PrivateKeyByKind(xdh, kind, buffer, out int written); + AssertExtensions.TrueExpression(success); + AssertExtensions.GreaterThan(written, 0); + Assert.Equal(2, xdh.TryExportPkcs8PrivateKeyCoreCount); + } + + [Theory] + [InlineData(TryExportPkcs8PasswordKind.StringPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfBytesPassword)] + [InlineData(TryExportPkcs8PasswordKind.SpanOfCharsPassword)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] + public static void TryExportEncryptedPkcs8PrivateKey_DestinationTooSmall(TryExportPkcs8PasswordKind kind) + { + using X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (TestPkcs8PrivateKey.TryCopyTo(destination)) + { + bytesWritten = TestPkcs8PrivateKey.Length; + return true; + } + + bytesWritten = 0; + return false; + } + }; + + byte[] buffer = new byte[3]; + bool success = TryExportEncryptedPkcs8PrivateKeyByKind(xdh, kind, buffer, out int written); + AssertExtensions.FalseExpression(success); + Assert.Equal(0, written); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_ValidatesPbeParameters_Bad3DESHash() + { + byte[] buffer = new byte[2048]; + PbeParameters pbeParameters = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA256, 3); + using X25519DiffieHellmanContract xdh = new(); + Assert.Throws(() => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword, pbeParameters, buffer, out _)); + Assert.Throws(() => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword.AsSpan(), pbeParameters, buffer, out _)); + Assert.Throws(() => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters, buffer, out _)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword, pbeParameters)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword.AsSpan(), pbeParameters)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters)); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_ValidatesPbeParameters_3DESRequiresChar() + { + byte[] buffer = new byte[2048]; + PbeParameters pbeParameters = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 3); + using X25519DiffieHellmanContract xdh = new(); + Assert.Throws(() => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters, buffer, out _)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPasswordBytes, pbeParameters)); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_NullArgs() + { + byte[] buffer = new byte[2048]; + using X25519DiffieHellmanContract xdh = new(); + AssertExtensions.Throws("pbeParameters", () => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword, pbeParameters: null, buffer, out _)); + AssertExtensions.Throws("pbeParameters", () => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword.AsSpan(), pbeParameters: null, buffer, out _)); + AssertExtensions.Throws("pbeParameters", () => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters: null, buffer, out _)); + AssertExtensions.Throws("password", () => + xdh.TryExportEncryptedPkcs8PrivateKey((string)null, s_aes128Pbe, buffer, out _)); + + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword, pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword.AsSpan(), pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, pbeParameters: null)); + + AssertExtensions.Throws("password", () => + xdh.ExportEncryptedPkcs8PrivateKey((string)null, s_aes128Pbe)); + + AssertExtensions.Throws("password", () => + xdh.ExportEncryptedPkcs8PrivateKeyPem((string)null, s_aes128Pbe)); + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword, pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword.AsSpan(), pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPasswordBytes, pbeParameters: null)); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKeyPem_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword, s_aes128Pbe)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe)); + Assert.Throws(() => + xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPasswordBytes, s_aes128Pbe)); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] + public static void ExportEncryptedPkcs8PrivateKeyPem_Works() + { + using X25519DiffieHellmanContract xdh = new() + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (TestPkcs8PrivateKey.TryCopyTo(destination)) + { + bytesWritten = TestPkcs8PrivateKey.Length; + return true; + } + + bytesWritten = 0; + return false; + } + }; + + string pem = xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword, s_aes128Pbe); + AssertPem(pem); + pem = xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPasswordBytes, s_aes128Pbe); + AssertPem(pem); + pem = xdh.ExportEncryptedPkcs8PrivateKeyPem(EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe); + AssertPem(pem); + + static void AssertPem(string pem) + { + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("ENCRYPTED PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + } + } + + [Fact] + public static void ExportSubjectPublicKeyInfoPem_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportSubjectPublicKeyInfoPem()); + } + + [Fact] + public static void ExportPkcs8PrivateKeyPem_Disposed() + { + X25519DiffieHellmanContract xdh = new(); + xdh.Dispose(); + Assert.Throws(() => xdh.ExportPkcs8PrivateKeyPem()); + } + + private static void ReadSubjectPublicKeyInfo( + ReadOnlyMemory source, + out string oid, + out ReadOnlyMemory? algorithmParameters, + out ReadOnlyMemory subjectPublicKey) + { + AsnReader outer = new(source, AsnEncodingRules.DER); + AsnReader reader = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + AsnReader spkiAlgorithm = reader.ReadSequence(); + oid = spkiAlgorithm.ReadObjectIdentifier(); + + if (spkiAlgorithm.HasData) + { + algorithmParameters = spkiAlgorithm.ReadEncodedValue(); + } + else + { + algorithmParameters = null; + } + + spkiAlgorithm.ThrowIfNotEmpty(); + + AssertExtensions.TrueExpression(reader.TryReadPrimitiveBitString(out int unusedBits, out subjectPublicKey)); + reader.ThrowIfNotEmpty(); + Assert.Equal(0, unusedBits); + } + + private static bool TryExportEncryptedPkcs8PrivateKeyByKind( + X25519DiffieHellman xdh, + TryExportPkcs8PasswordKind kind, + Span destination, + out int bytesWritten) + { + return kind switch + { + TryExportPkcs8PasswordKind.StringPassword => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword, s_aes128Pbe, destination, out bytesWritten), + TryExportPkcs8PasswordKind.SpanOfCharsPassword => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPassword.AsSpan(), s_aes128Pbe, destination, out bytesWritten), + TryExportPkcs8PasswordKind.SpanOfBytesPassword => + xdh.TryExportEncryptedPkcs8PrivateKey(EncryptedPrivateKeyPasswordBytes, s_aes128Pbe, destination, out bytesWritten), + _ => throw new XunitException($"Unknown password kind '{kind}'."), + }; + } + + public enum TryExportPkcs8PasswordKind + { + StringPassword, + SpanOfCharsPassword, + SpanOfBytesPassword, + } + } + + internal sealed class X25519DiffieHellmanContract : X25519DiffieHellman + { + internal DeriveRawSecretAgreementCoreCallback OnDeriveRawSecretAgreementCore { get; set; } + internal ExportKeyCoreCallback OnExportPrivateKeyCore { get; set; } + internal ExportKeyCoreCallback OnExportPublicKeyCore { get; set; } + internal TryExportPkcs8PrivateKeyCoreCallback OnTryExportPkcs8PrivateKeyCore { get; set; } + internal Action OnDispose { get; set; } = (bool disposing) => { }; + + internal int DeriveRawSecretAgreementCoreCount { get; set; } + internal int ExportPrivateKeyCoreCount { get; set; } + internal int ExportPublicKeyCoreCount { get; set; } + internal int TryExportPkcs8PrivateKeyCoreCount { get; set; } + + private bool _disposed; + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + DeriveRawSecretAgreementCoreCount++; + GetCallback(OnDeriveRawSecretAgreementCore)(otherParty, destination); + } + + protected override void ExportPrivateKeyCore(Span destination) + { + ExportPrivateKeyCoreCount++; + GetCallback(OnExportPrivateKeyCore)(destination); + } + + protected override void ExportPublicKeyCore(Span destination) + { + ExportPublicKeyCoreCount++; + GetCallback(OnExportPublicKeyCore)(destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + TryExportPkcs8PrivateKeyCoreCount++; + return GetCallback(OnTryExportPkcs8PrivateKeyCore)(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + GetCallback(OnDispose)(disposing); + VerifyCalledOnDispose(); + _disposed = true; + } + + private void VerifyCalledOnDispose() + { + if (OnDeriveRawSecretAgreementCore is not null && DeriveRawSecretAgreementCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(DeriveRawSecretAgreementCore)}."); + } + if (OnExportPrivateKeyCore is not null && ExportPrivateKeyCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(ExportPrivateKeyCore)}."); + } + if (OnExportPublicKeyCore is not null && ExportPublicKeyCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(ExportPublicKeyCore)}."); + } + if (OnTryExportPkcs8PrivateKeyCore is not null && TryExportPkcs8PrivateKeyCoreCount == 0) + { + Assert.Fail($"Expected call to {nameof(TryExportPkcs8PrivateKeyCore)}."); + } + } + + internal delegate void DeriveRawSecretAgreementCoreCallback(X25519DiffieHellman otherParty, Span destination); + internal delegate void ExportKeyCoreCallback(Span destination); + internal delegate bool TryExportPkcs8PrivateKeyCoreCallback(Span destination, out int bytesWritten); + + private T GetCallback(T callback, [CallerMemberName] string caller = null) where T : Delegate + { + if (_disposed) + { + Assert.Fail($"Unexpected call to {caller} after Dispose."); + } + + return callback ?? throw new XunitException($"Unexpected call to {caller}."); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs new file mode 100644 index 00000000000000..9c09e73c7ce278 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public sealed class X25519DiffieHellmanImplementationTests : X25519DiffieHellmanBaseTests + { + public override X25519DiffieHellman GenerateKey() => X25519DiffieHellman.GenerateKey(); + + public override X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source) => + X25519DiffieHellman.ImportPrivateKey(source); + + public override X25519DiffieHellman ImportPublicKey(ReadOnlySpan source) => + X25519DiffieHellman.ImportPublicKey(source); + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanKeyTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanKeyTests.cs new file mode 100644 index 00000000000000..35f69bd5d1b788 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanKeyTests.cs @@ -0,0 +1,391 @@ +// 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 Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public static class X25519DiffieHellmanKeyTests + { + public static bool IsNotStrictKeyValidatingPlatform => !X25519DiffieHellmanBaseTests.IsStrictKeyValidatingPlatform; + private static readonly PbeParameters s_aes128Pbe = new(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 2); + + [Fact] + public static void Generate_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.GenerateKey(); + + byte[] publicKey = xdh.ExportPublicKey(); + AssertExtensions.GreaterThanOrEqualTo(publicKey.IndexOfAnyExcept((byte)0), 0); + + byte[] privateKey = xdh.ExportPrivateKey(); + AssertExtensions.GreaterThanOrEqualTo(privateKey.IndexOfAnyExcept((byte)0), 0); + + Assert.Equal(X25519DiffieHellman.PublicKeySizeInBytes, publicKey.Length); + Assert.Equal(X25519DiffieHellman.PrivateKeySizeInBytes, privateKey.Length); + AssertExtensions.SequenceNotEqual(publicKey, privateKey); + + using X25519DiffieHellman xdh2 = X25519DiffieHellman.ImportPublicKey(publicKey); + byte[] publicKey2 = xdh2.ExportPublicKey(); + AssertExtensions.SequenceEqual(publicKey, publicKey2); + + using X25519DiffieHellman xdh3 = X25519DiffieHellman.ImportPrivateKey(privateKey); + byte[] privateKey2 = xdh3.ExportPrivateKey(); + AssertExtensions.SequenceEqual(privateKey, privateKey2); + } + + [Fact] + public static void Rfc7748_TestVector_Alice() + { + using X25519DiffieHellman alice = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + using X25519DiffieHellman bob = X25519DiffieHellman.ImportPublicKey(X25519DiffieHellmanTestData.BobPublicKey); + + byte[] sharedSecret = alice.DeriveRawSecretAgreement(bob); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.SharedSecret, sharedSecret); + + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, alice.ExportPublicKey()); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, alice.ExportPrivateKey()); + } + + [Fact] + public static void Rfc7748_TestVector_Bob() + { + using X25519DiffieHellman bob = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.BobPrivateKey); + using X25519DiffieHellman alice = X25519DiffieHellman.ImportPublicKey(X25519DiffieHellmanTestData.AlicePublicKey); + + byte[] sharedSecret = bob.DeriveRawSecretAgreement(alice); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.SharedSecret, sharedSecret); + + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.BobPublicKey, bob.ExportPublicKey()); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.BobPrivateKey, bob.ExportPrivateKey()); + } + + [Fact] + public static void DeriveSecretAgreement_Symmetric() + { + using X25519DiffieHellman key1 = X25519DiffieHellman.GenerateKey(); + using X25519DiffieHellman key2 = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = key1.DeriveRawSecretAgreement(key2); + byte[] secret2 = key2.DeriveRawSecretAgreement(key1); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public static void ImportPrivateKey_Roundtrip_Array() + { + byte[] privateKeyBytes = X25519DiffieHellmanTestData.AlicePrivateKey; + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(privateKeyBytes); + + byte[] exported = xdh.ExportPrivateKey(); + AssertExtensions.SequenceEqual(privateKeyBytes, exported); + } + + [Fact] + public static void ImportPrivateKey_Roundtrip_Span() + { + ReadOnlySpan privateKeyBytes = X25519DiffieHellmanTestData.AlicePrivateKey; + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(privateKeyBytes); + + Span exported = new byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + xdh.ExportPrivateKey(exported); + AssertExtensions.SequenceEqual(privateKeyBytes, exported); + } + + [Fact] + public static void ImportPublicKey_Roundtrip_Array() + { + byte[] publicKeyBytes = X25519DiffieHellmanTestData.AlicePublicKey; + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPublicKey(publicKeyBytes); + + byte[] exported = xdh.ExportPublicKey(); + AssertExtensions.SequenceEqual(publicKeyBytes, exported); + } + + [Fact] + public static void ImportPublicKey_Roundtrip_Span() + { + ReadOnlySpan publicKeyBytes = X25519DiffieHellmanTestData.AlicePublicKey; + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPublicKey(publicKeyBytes); + + Span exported = new byte[X25519DiffieHellman.PublicKeySizeInBytes]; + xdh.ExportPublicKey(exported); + AssertExtensions.SequenceEqual(publicKeyBytes, exported); + } + + [Fact] + public static void ExportSubjectPublicKeyInfo_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] spki = xdh.ExportSubjectPublicKeyInfo(); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportSubjectPublicKeyInfo(spki); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, imported.ExportPublicKey()); + } + + [Fact] + public static void TryExportSubjectPublicKeyInfo_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] buffer = new byte[256]; + AssertExtensions.TrueExpression(xdh.TryExportSubjectPublicKeyInfo(buffer, out int written)); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportSubjectPublicKeyInfo(buffer.AsSpan(0, written)); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, imported.ExportPublicKey()); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_KnownValue() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportSubjectPublicKeyInfo(X25519DiffieHellmanTestData.AliceSpki); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, xdh.ExportPublicKey()); + } + + [Fact] + public static void ExportPkcs8PrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] pkcs8 = xdh.ExportPkcs8PrivateKey(); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, imported.ExportPublicKey()); + } + + [Fact] + public static void TryExportPkcs8PrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] buffer = new byte[256]; + AssertExtensions.TrueExpression(xdh.TryExportPkcs8PrivateKey(buffer, out int written)); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportPkcs8PrivateKey(buffer.AsSpan(0, written)); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void ImportPkcs8PrivateKey_KnownValue() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPkcs8PrivateKey(X25519DiffieHellmanTestData.AlicePkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, xdh.ExportPublicKey()); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] encrypted = xdh.ExportEncryptedPkcs8PrivateKey("test", s_aes128Pbe); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey("test", encrypted); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_Roundtrip_BytePassword() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + byte[] encrypted = xdh.ExportEncryptedPkcs8PrivateKey("test"u8, s_aes128Pbe); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey("test"u8, encrypted); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void ImportFromPem_PublicKey() + { + string pem = + "-----BEGIN PUBLIC KEY-----\n" + + "MCowBQYDK2VuAyEAhSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=\n" + + "-----END PUBLIC KEY-----"; + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportFromPem(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, xdh.ExportPublicKey()); + } + + [Fact] + public static void ImportFromPem_PrivateKey() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + string pem = xdh.ExportPkcs8PrivateKeyPem(); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportFromPem(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void ImportFromEncryptedPem_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + string pem = xdh.ExportEncryptedPkcs8PrivateKeyPem("test", s_aes128Pbe); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportFromEncryptedPem(pem, "test"); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void ExportSubjectPublicKeyInfoPem_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + string pem = xdh.ExportSubjectPublicKeyInfoPem(); + + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal("PUBLIC KEY", pem.AsSpan()[fields.Label].ToString()); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportFromPem(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePublicKey, imported.ExportPublicKey()); + } + + [Fact] + public static void ExportPkcs8PrivateKeyPem_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(X25519DiffieHellmanTestData.AlicePrivateKey); + string pem = xdh.ExportPkcs8PrivateKeyPem(); + + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal("PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + + using X25519DiffieHellman imported = X25519DiffieHellman.ImportFromPem(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, imported.ExportPrivateKey()); + } + + [Fact] + public static void DeriveSecretAgreement_PublicKeyOnly_Throws() + { + using X25519DiffieHellman publicOnly = X25519DiffieHellman.ImportPublicKey(X25519DiffieHellmanTestData.AlicePublicKey); + using X25519DiffieHellman other = X25519DiffieHellman.ImportPublicKey(X25519DiffieHellmanTestData.BobPublicKey); + + Assert.Throws(() => publicOnly.DeriveRawSecretAgreement(other)); + } + + [Fact] + public static void ExportPrivateKey_PublicKeyOnly_Throws() + { + using X25519DiffieHellman publicOnly = X25519DiffieHellman.ImportPublicKey(X25519DiffieHellmanTestData.AlicePublicKey); + + Assert.Throws(() => publicOnly.ExportPrivateKey()); + } + + [Fact] + public static void PrivateKey_Roundtrip_UnclampedScalar_AllPreservationBits() + { + // A private key where bytes[0] low 3 bits = 0b111 AND bytes[31] high 2 bits = 0b11. + // This exercises the maximum scalar fixup on Windows CNG (all preservation bits set). + // Bob's RFC 7748 key has this property: bytes[0]=0x5d (low 3=0b101), bytes[31]=0xeb (high 2=0b11). + byte[] privateKey = X25519DiffieHellmanTestData.BobPrivateKey; + Assert.Equal(0b101, privateKey[0] & 0b111); + Assert.Equal(0b11000000, privateKey[^1] & 0b11000000); + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(privateKey); + + // Private key must roundtrip with original unclamped bits preserved + AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); + + // Public key must still be correct (computed from the clamped scalar) + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.BobPublicKey, xdh.ExportPublicKey()); + + // PKCS#8 roundtrip must also preserve the original private key + byte[] pkcs8 = xdh.ExportPkcs8PrivateKey(); + using X25519DiffieHellman reimported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); + AssertExtensions.SequenceEqual(privateKey, reimported.ExportPrivateKey()); + } + + [Fact] + public static void PrivateKey_Roundtrip_ClampedScalar() + { + // Construct a private key that is ALREADY properly clamped per RFC 7748: + // bytes[0] low 3 bits = 0, bytes[31] bit 7 = 0 and bit 6 = 1. + // Importing this key should be a no-op fixup; the key roundtrips unchanged. + byte[] privateKey = (byte[])X25519DiffieHellmanTestData.AlicePrivateKey.Clone(); + privateKey[0] &= 0b11111000; + privateKey[^1] &= 0b01111111; + privateKey[^1] |= 0b01000000; + + Assert.Equal(0, privateKey[0] & 0b111); + Assert.Equal(0b01000000, privateKey[^1] & 0b11000000); + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(privateKey); + AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); + + // PKCS#8 roundtrip + byte[] pkcs8 = xdh.ExportPkcs8PrivateKey(); + using X25519DiffieHellman reimported = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); + AssertExtensions.SequenceEqual(privateKey, reimported.ExportPrivateKey()); + } + + [Fact] + public static void PrivateKey_ClampedAndUnclamped_SamePublicKey() + { + // The unclamped and clamped forms of the same key should produce the same public key, + // because the DH computation always operates on the clamped scalar. + byte[] unclamped = (byte[])X25519DiffieHellmanTestData.AlicePrivateKey.Clone(); + byte[] clamped = (byte[])unclamped.Clone(); + clamped[0] &= 0b11111000; + clamped[^1] &= 0b01111111; + clamped[^1] |= 0b01000000; + + using X25519DiffieHellman xdhUnclamped = X25519DiffieHellman.ImportPrivateKey(unclamped); + using X25519DiffieHellman xdhClamped = X25519DiffieHellman.ImportPrivateKey(clamped); + + AssertExtensions.SequenceEqual(xdhUnclamped.ExportPublicKey(), xdhClamped.ExportPublicKey()); + } + + [Fact] + public static void PrivateKey_Roundtrip_MaxPreservation() + { + // A key with bytes[0]=0xFF and bytes[31]=0xFF — maximum preservation needed. + // The scalar fixup would clamp bytes[0] to 0xF8 and bytes[31] to 0x7F, + // but the original 0xFF values must be restored on export. + byte[] privateKey = new byte[X25519DiffieHellman.PrivateKeySizeInBytes]; + privateKey.AsSpan().Fill(0xFF); + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPrivateKey(privateKey); + AssertExtensions.SequenceEqual(privateKey, xdh.ExportPrivateKey()); + } + + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(9)] + [InlineData(18)] + [ConditionalTheory(typeof(X25519DiffieHellmanKeyTests), nameof(IsNotStrictKeyValidatingPlatform))] + public static void PublicKey_NonCanonical_Roundtrip(int offset) + { + // RFC 7748 Section 5: Non-canonical u-coordinates are p through 2^255 - 1. + // Construct p + offset in little-endian. + // p = 2^255 - 19 = 0xED_FFFF...FFFF_7F (little-endian) + byte[] nonCanonical = new byte[X25519DiffieHellman.PublicKeySizeInBytes]; + nonCanonical.AsSpan().Fill(0xFF); + nonCanonical[0] = (byte)(0xED + offset); + nonCanonical[^1] = 0x7F; + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPublicKey(nonCanonical); + byte[] exported = xdh.ExportPublicKey(); + AssertExtensions.SequenceEqual(nonCanonical, exported); + } + + [InlineData(0)] + [InlineData(3)] + [InlineData(18)] + [ConditionalTheory(typeof(X25519DiffieHellmanKeyTests), nameof(IsNotStrictKeyValidatingPlatform))] + public static void PublicKey_NonCanonical_HighBitSet_Roundtrip(int offset) + { + // Same as above but with the high bit set in byte[31]. + // RFC 7748 says the high bit MUST be masked, but the original + // byte should be preserved on export for roundtripping. + byte[] nonCanonical = new byte[X25519DiffieHellman.PublicKeySizeInBytes]; + nonCanonical.AsSpan().Fill(0xFF); + nonCanonical[0] = (byte)(0xED + offset); + // byte[31] = 0xFF (high bit set) + + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPublicKey(nonCanonical); + byte[] exported = xdh.ExportPublicKey(); + AssertExtensions.SequenceEqual(nonCanonical, exported); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs new file mode 100644 index 00000000000000..7b77455eca29be --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellmanNotSupportedTests), nameof(X25519DiffieHellmanNotSupportedTests.IsNotSupported))] + public static class X25519DiffieHellmanNotSupportedTests + { + public static bool IsNotSupported => !X25519DiffieHellman.IsSupported; + + [Fact] + public static void Generate_NotSupported() + { + Assert.Throws(() => X25519DiffieHellman.GenerateKey()); + } + + [Fact] + public static void ImportPrivateKey_NotSupported() + { + Assert.Throws(() => + X25519DiffieHellman.ImportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes])); + + Assert.Throws(() => + X25519DiffieHellman.ImportPrivateKey(new ReadOnlySpan(new byte[X25519DiffieHellman.PrivateKeySizeInBytes]))); + } + + [Fact] + public static void ImportPublicKey_NotSupported() + { + Assert.Throws(() => + X25519DiffieHellman.ImportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes])); + + Assert.Throws(() => + X25519DiffieHellman.ImportPublicKey(new ReadOnlySpan(new byte[X25519DiffieHellman.PublicKeySizeInBytes]))); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_NotSupported() + { + // A minimal valid SPKI for X25519 + byte[] spki = Convert.FromHexString( + "302a300506032b656e032100" + "0000000000000000000000000000000000000000000000000000000000000000"); + + Assert.Throws(() => + X25519DiffieHellman.ImportSubjectPublicKeyInfo(spki)); + + Assert.Throws(() => + X25519DiffieHellman.ImportSubjectPublicKeyInfo(new ReadOnlySpan(spki))); + } + + [Fact] + public static void ImportPkcs8PrivateKey_NotSupported() + { + // A minimal valid PKCS#8 for X25519 + byte[] pkcs8 = Convert.FromHexString( + "302e020100300506032b656e04220420" + + "0000000000000000000000000000000000000000000000000000000000000000"); + + Assert.Throws(() => + X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportPkcs8PrivateKey(new ReadOnlySpan(pkcs8))); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_NotSupported() + { + // Use an encrypted PKCS#8 blob. The implementation should throw PlatformNotSupportedException + // before attempting decryption. + byte[] pkcs8 = Convert.FromHexString( + "302e020100300506032b656e04220420" + + "0000000000000000000000000000000000000000000000000000000000000000"); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey("password", pkcs8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey("password".AsSpan(), pkcs8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey("password"u8, pkcs8)); + } + + [Fact] + public static void ImportFromPem_NotSupported() + { + string pem = """ + -----BEGIN THING----- + Should throw before even attempting to read the PEM + -----END THING----- + """; + Assert.Throws(() => X25519DiffieHellman.ImportFromPem(pem)); + Assert.Throws(() => X25519DiffieHellman.ImportFromPem(pem.AsSpan())); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTestData.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTestData.cs new file mode 100644 index 00000000000000..089768b70f779e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTestData.cs @@ -0,0 +1,95 @@ +// 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.Tests +{ + public static class X25519DiffieHellmanTestData + { + public const string X25519Oid = "1.3.101.110"; + + public const string EncryptedPrivateKeyPassword = "PLACEHOLDER"; + public static ReadOnlySpan EncryptedPrivateKeyPasswordBytes => "PLACEHOLDER"u8; + + // RFC 7748 Section 6.1 test vectors + public static readonly byte[] AlicePrivateKey = + [ + 0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2, 0x66, 0x45, + 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5, 0x1d, 0xb9, 0x2c, 0x2a, + ]; + + public static readonly byte[] AlicePublicKey = + [ + 0x85, 0x20, 0xf0, 0x09, 0x89, 0x30, 0xa7, 0x54, 0x74, 0x8b, 0x7d, 0xdc, 0xb4, 0x3e, 0xf7, 0x5a, + 0x0d, 0xbf, 0x3a, 0x0d, 0x26, 0x38, 0x1a, 0xf4, 0xeb, 0xa4, 0xa9, 0x8e, 0xaa, 0x9b, 0x4e, 0x6a, + ]; + + public static readonly byte[] BobPrivateKey = + [ + 0x5d, 0xab, 0x08, 0x7e, 0x62, 0x4a, 0x8a, 0x4b, 0x79, 0xe1, 0x7f, 0x8b, 0x83, 0x80, 0x0e, 0xe6, + 0x6f, 0x3b, 0xb1, 0x29, 0x26, 0x18, 0xb6, 0xfd, 0x1c, 0x2f, 0x8b, 0x27, 0xff, 0x88, 0xe0, 0xeb, + ]; + + public static readonly byte[] BobPublicKey = + [ + 0xde, 0x9e, 0xdb, 0x7d, 0x7b, 0x7d, 0xc1, 0xb4, 0xd3, 0x5b, 0x61, 0xc2, 0xec, 0xe4, 0x35, 0x37, + 0x3f, 0x83, 0x43, 0xc8, 0x5b, 0x78, 0x67, 0x4d, 0xad, 0xfc, 0x7e, 0x14, 0x6f, 0x88, 0x2b, 0x4f, + ]; + + public static readonly byte[] SharedSecret = + [ + 0x4a, 0x5d, 0x9d, 0x5b, 0xa4, 0xce, 0x2d, 0xe1, 0x72, 0x8e, 0x3b, 0xf4, 0x80, 0x35, 0x0f, 0x25, + 0xe0, 0x7e, 0x21, 0xc9, 0x47, 0xd1, 0x9e, 0x33, 0x76, 0xf0, 0x9b, 0x3c, 0x1e, 0x16, 0x17, 0x42, + ]; + + // SPKI: SEQUENCE { SEQUENCE { OID 1.3.101.110 } BIT STRING } + public static readonly byte[] AliceSpki = + [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00, + 0x85, 0x20, 0xf0, 0x09, 0x89, 0x30, 0xa7, 0x54, 0x74, 0x8b, 0x7d, 0xdc, 0xb4, 0x3e, 0xf7, 0x5a, + 0x0d, 0xbf, 0x3a, 0x0d, 0x26, 0x38, 0x1a, 0xf4, 0xeb, 0xa4, 0xa9, 0x8e, 0xaa, 0x9b, 0x4e, 0x6a, + ]; + + // PKCS#8: SEQUENCE { INTEGER 0, SEQUENCE { OID 1.3.101.110 }, OCTET STRING { OCTET STRING } } + public static readonly byte[] AlicePkcs8 = + [ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20, + 0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2, 0x66, 0x45, + 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5, 0x1d, 0xb9, 0x2c, 0x2a, + ]; + + public static readonly byte[] BobSpki = + [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00, + 0xde, 0x9e, 0xdb, 0x7d, 0x7b, 0x7d, 0xc1, 0xb4, 0xd3, 0x5b, 0x61, 0xc2, 0xec, 0xe4, 0x35, 0x37, + 0x3f, 0x83, 0x43, 0xc8, 0x5b, 0x78, 0x67, 0x4d, 0xad, 0xfc, 0x7e, 0x14, 0x6f, 0x88, 0x2b, 0x4f, + ]; + + public static readonly byte[] BobPkcs8 = + [ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20, + 0x5d, 0xab, 0x08, 0x7e, 0x62, 0x4a, 0x8a, 0x4b, 0x79, 0xe1, 0x7f, 0x8b, 0x83, 0x80, 0x0e, 0xe6, + 0x6f, 0x3b, 0xb1, 0x29, 0x26, 0x18, 0xb6, 0xfd, 0x1c, 0x2f, 0x8b, 0x27, 0xff, 0x88, 0xe0, 0xeb, + ]; + + // Encrypted PKCS#8 for Alice's private key, password = "PLACEHOLDER", AES-128-CBC + public static readonly byte[] AliceEncryptedPkcs8 = + [ + 0x30, 0x81, 0xa3, 0x30, 0x5f, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0d, + 0x30, 0x52, 0x30, 0x31, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0c, 0x30, + 0x24, 0x04, 0x10, 0x6a, 0x2d, 0x90, 0xcf, 0xd9, 0xa8, 0xf9, 0x64, 0x4d, 0x4e, 0xa2, 0x89, 0xa1, + 0x97, 0x58, 0xa6, 0x02, 0x02, 0x08, 0x00, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, + 0x0d, 0x02, 0x09, 0x05, 0x00, 0x30, 0x1d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, + 0x01, 0x02, 0x04, 0x10, 0x99, 0xbb, 0x64, 0x9f, 0x99, 0xa8, 0xdc, 0x08, 0x6f, 0x3e, 0x3d, 0x41, + 0x10, 0xfa, 0x0d, 0x4a, 0x04, 0x40, 0x9b, 0xee, 0x71, 0x66, 0xf2, 0xdc, 0xe5, 0x37, 0x83, 0x31, + 0x5d, 0xde, 0xde, 0x6e, 0x0b, 0xd1, 0x94, 0xf2, 0xde, 0xda, 0x3e, 0x22, 0x94, 0xe0, 0x92, 0xb1, + 0x01, 0x8c, 0xad, 0x6d, 0x2d, 0xc0, 0x08, 0xed, 0xe6, 0x19, 0xad, 0x51, 0x59, 0xda, 0xed, 0xe3, + 0xb8, 0xe5, 0x02, 0x71, 0x9a, 0xc6, 0x97, 0x43, 0xf5, 0xe5, 0x82, 0xb0, 0x67, 0x1e, 0x23, 0x27, + 0xd7, 0x34, 0x12, 0x17, 0xd4, 0x0c, + ]; + + public const string AliceSpkiPem = + "-----BEGIN PUBLIC KEY-----\n" + + "MCowBQYDK2VuAyEAhSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=\n" + + "-----END PUBLIC KEY-----"; + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTests.cs new file mode 100644 index 00000000000000..2c62e79d305bd1 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanTests.cs @@ -0,0 +1,685 @@ +// 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 Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public static class X25519DiffieHellmanTests + { + private static readonly byte[] s_asnNull = [0x05, 0x00]; + + private static readonly byte[] AliceSpki = X25519DiffieHellmanTestData.AliceSpki; + private static readonly byte[] AlicePkcs8 = X25519DiffieHellmanTestData.AlicePkcs8; + private static readonly byte[] AliceEncryptedPkcs8 = X25519DiffieHellmanTestData.AliceEncryptedPkcs8; + + [Fact] + public static void ImportPrivateKey_NullSource() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportPrivateKey((byte[])null)); + } + + [Fact] + public static void ImportPrivateKey_WrongSize_Array() + { + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes + 1])); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(new byte[X25519DiffieHellman.PrivateKeySizeInBytes - 1])); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(Array.Empty())); + } + + [Fact] + public static void ImportPrivateKey_WrongSize_Span() + { + byte[] key = new byte[X25519DiffieHellman.PrivateKeySizeInBytes + 1]; + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(key.AsSpan())); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(key.AsSpan(0, key.Length - 2))); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPrivateKey(ReadOnlySpan.Empty)); + } + + [Fact] + public static void ImportPublicKey_NullSource() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportPublicKey((byte[])null)); + } + + [Fact] + public static void ImportPublicKey_WrongSize_Array() + { + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes + 1])); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(new byte[X25519DiffieHellman.PublicKeySizeInBytes - 1])); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(Array.Empty())); + } + + [Fact] + public static void ImportPublicKey_WrongSize_Span() + { + byte[] key = new byte[X25519DiffieHellman.PublicKeySizeInBytes + 1]; + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(key.AsSpan())); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(key.AsSpan(0, key.Length - 2))); + + AssertExtensions.Throws("source", () => + X25519DiffieHellman.ImportPublicKey(ReadOnlySpan.Empty)); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_NullSource() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportSubjectPublicKeyInfo((byte[])null)); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_WrongAlgorithm() + { + byte[] ecP256Spki = Convert.FromBase64String( + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuiPJ2IV089LVrXZGDo9Mc542UZZE" + + "UtPQVd60Ckb/u5OXHAlmITVzFPThKI+N/bUMEnnHEmF8ZDUtLiQPBaKiMQ=="); + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo(ecP256Spki)); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_NotAsn() + { + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo("potatoes"u8)); + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo("potatoes"u8.ToArray())); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_WrongParameters() + { + // RFC 8410: AlgorithmIdentifier parameters MUST be absent + byte[] spki = SpkiEncode( + X25519DiffieHellmanTestData.X25519Oid, + new byte[X25519DiffieHellman.PublicKeySizeInBytes], + algorithmParameters: s_asnNull); + + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo(spki)); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_WrongSize() + { + byte[] spki = SpkiEncode( + X25519DiffieHellmanTestData.X25519Oid, + new byte[X25519DiffieHellman.PublicKeySizeInBytes - 1]); + + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo(spki)); + + spki = SpkiEncode( + X25519DiffieHellmanTestData.X25519Oid, + new byte[X25519DiffieHellman.PublicKeySizeInBytes + 1]); + + Assert.Throws(() => X25519DiffieHellman.ImportSubjectPublicKeyInfo(spki)); + } + + [Fact] + public static void ImportSubjectPublicKeyInfo_TrailingData() + { + byte[] oversized = new byte[AliceSpki.Length + 1]; + AliceSpki.AsSpan().CopyTo(oversized); + Assert.Throws(() => + X25519DiffieHellman.ImportSubjectPublicKeyInfo(oversized)); + Assert.Throws(() => + X25519DiffieHellman.ImportSubjectPublicKeyInfo(new ReadOnlySpan(oversized))); + } + + [Fact] + public static void ImportPkcs8PrivateKey_NullSource() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportPkcs8PrivateKey((byte[])null)); + } + + [Fact] + public static void ImportPkcs8PrivateKey_WrongAlgorithm() + { + byte[] ecP256Key = Convert.FromBase64String( + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZg/vYKeaTgco6dGx" + + "6KCMw5/L7/Xu7j7idYWNSCBcod6hRANCAASc/jV6ZojlesoM+qNnSYZdc7Fkd4+E" + + "2raDwlFPZGucEHDUmdCwaDx/hglDZaLimpD/67F5k5jUe+I3CkijLST7"); + + Assert.Throws(() => + X25519DiffieHellman.ImportPkcs8PrivateKey(new ReadOnlySpan(ecP256Key))); + Assert.Throws(() => + X25519DiffieHellman.ImportPkcs8PrivateKey(ecP256Key)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void ImportPkcs8PrivateKey_BogusAsnChoice(bool useSpanImport) + { + // SEQUENCE { + // INTEGER 0 + // SEQUENCE { + // OBJECT IDENTIFIER 1.3.101.110 (id-X25519) + // } + // PRINTABLE STRING "Potato" + // } + byte[] pkcs8 = "3014020100300506032b656e1306506F7461746F".HexToByteArray(); + + if (useSpanImport) + { + Assert.Throws(() => + X25519DiffieHellman.ImportPkcs8PrivateKey(new ReadOnlySpan(pkcs8))); + } + else + { + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8)); + } + } + + [Fact] + public static void ImportPkcs8PrivateKey_WrongKeySize() + { + byte[] pkcs8 = Pkcs8Encode( + X25519DiffieHellmanTestData.X25519Oid, + new byte[X25519DiffieHellman.PrivateKeySizeInBytes - 1]); + + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8)); + + pkcs8 = Pkcs8Encode( + X25519DiffieHellmanTestData.X25519Oid, + new byte[X25519DiffieHellman.PrivateKeySizeInBytes + 1]); + + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8)); + } + + [Fact] + public static void ImportPkcs8PrivateKey_BadAlgorithmIdentifier() + { + // RFC 8410: AlgorithmIdentifier parameters MUST be absent + byte[] pkcs8 = Pkcs8Encode( + X25519DiffieHellmanTestData.X25519Oid, + X25519DiffieHellmanTestData.AlicePrivateKey, + algorithmParameters: s_asnNull); + + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8.AsSpan())); + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8)); + } + + [Fact] + public static void ImportPkcs8PrivateKey_TrailingData() + { + byte[] oversized = new byte[AlicePkcs8.Length + 1]; + AlicePkcs8.AsSpan().CopyTo(oversized); + + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(oversized.AsSpan())); + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey(oversized)); + } + + [Fact] + public static void ImportPkcs8PrivateKey_NotAsn() + { + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey("potatoes"u8)); + Assert.Throws(() => X25519DiffieHellman.ImportPkcs8PrivateKey("potatoes"u8.ToArray())); + } + + [Fact] + public static void ImportPkcs8PrivateKey_Array_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPkcs8PrivateKey(AlicePkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public static void ImportPkcs8PrivateKey_Span_Roundtrip() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportPkcs8PrivateKey(new ReadOnlySpan(AlicePkcs8)); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_WrongAlgorithm() + { + byte[] ecP256Key = Convert.FromBase64String( + "MIHrMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBCr0ipJGBOnThng8uXT" + + "iyZWAgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAgNPETMQWxeYgSBkN4J" + + "tW/1aNLGpRCBPvz2aHMulF/bBRRy3G8hwidysLR/mc0CaFWeltzZUpSGJgMSDJE4" + + "/zQJXhyXcEApuChzg0H0o8cPK1SCyi4wScMokiUHskOhcxhyr1VQ7cFAT+qS+66C" + + "gJoH9z0+/Z9WzLU8ix8F7B+HWwRhib5Cd6si+AX6DsNelMq2zP1NO7Un416dkg=="); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + ecP256Key)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), + new ReadOnlySpan(ecP256Key))); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, + new ReadOnlySpan(ecP256Key))); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_TrailingData() + { + byte[] oversized = new byte[AliceEncryptedPkcs8.Length + 1]; + AliceEncryptedPkcs8.AsSpan().CopyTo(oversized); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + oversized)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), + oversized)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, + oversized)); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_NotAsn() + { + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + "potatoes"u8.ToArray())); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), + "potatoes"u8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, + "potatoes"u8)); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_DoesNotProcessUnencryptedData() + { + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, AlicePkcs8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, AlicePkcs8)); + + Assert.Throws(() => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey(string.Empty, AlicePkcs8)); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_CharPassword() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword.AsSpan(), AliceEncryptedPkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_StringPassword() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, AliceEncryptedPkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_BytePassword() + { + using X25519DiffieHellman xdh = X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes, AliceEncryptedPkcs8); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + } + + [Fact] + public static void ImportEncryptedPkcs8PrivateKey_NullArgs() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey( + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword, + (byte[])null)); + + AssertExtensions.Throws("password", static () => + X25519DiffieHellman.ImportEncryptedPkcs8PrivateKey((string)null, AliceEncryptedPkcs8)); + } + + [Fact] + public static void ImportFromPem_NullSource() + { + AssertExtensions.Throws("source", static () => + X25519DiffieHellman.ImportFromPem((string)null)); + } + + [Fact] + public static void ImportFromPem_PublicKey_Roundtrip() + { + string pem = WritePem("PUBLIC KEY", AliceSpki); + AssertImportFromPem(importer => + { + using X25519DiffieHellman xdh = importer(pem); + byte[] exportedSpki = xdh.ExportSubjectPublicKeyInfo(); + AssertExtensions.SequenceEqual(AliceSpki, exportedSpki); + }); + } + + [Fact] + public static void ImportFromPem_PublicKey_IgnoresNotUnderstoodPems() + { + string pem = $""" + -----BEGIN POTATO----- + dmluY2U= + -----END POTATO----- + {WritePem("PUBLIC KEY", AliceSpki)} + """; + + AssertImportFromPem(importer => + { + using X25519DiffieHellman xdh = importer(pem); + byte[] exportedSpki = xdh.ExportSubjectPublicKeyInfo(); + AssertExtensions.SequenceEqual(AliceSpki, exportedSpki); + }); + } + + [Fact] + public static void ImportFromPem_PrivateKey_Roundtrip() + { + string pem = WritePem("PRIVATE KEY", AlicePkcs8); + AssertImportFromPem(importer => + { + using X25519DiffieHellman xdh = importer(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + }); + } + + [Fact] + public static void ImportFromPem_PrivateKey_IgnoresNotUnderstoodPems() + { + string pem = $""" + -----BEGIN UNKNOWN----- + cGNq + -----END UNKNOWN----- + {WritePem("PRIVATE KEY", AlicePkcs8)} + """; + + AssertImportFromPem(importer => + { + using X25519DiffieHellman xdh = importer(pem); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + }); + } + + [Fact] + public static void ImportFromPem_AmbiguousImportWithPublicKey_Throws() + { + string pem = $""" + {WritePem("PUBLIC KEY", AliceSpki)} + {WritePem("PUBLIC KEY", AliceSpki)} + """; + + AssertImportFromPem(importer => + { + AssertExtensions.Throws("source", () => importer(pem)); + }); + } + + [Fact] + public static void ImportFromPem_AmbiguousImportWithPrivateKey_Throws() + { + string pem = $""" + {WritePem("PUBLIC KEY", AliceSpki)} + {WritePem("PRIVATE KEY", AlicePkcs8)} + """; + + AssertImportFromPem(importer => + { + AssertExtensions.Throws("source", () => importer(pem)); + }); + } + + [Fact] + public static void ImportFromPem_AmbiguousImportWithEncryptedPrivateKey_Throws() + { + string pem = $""" + {WritePem("PUBLIC KEY", AliceSpki)} + {WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8)} + """; + + AssertImportFromPem(importer => + { + AssertExtensions.Throws("source", () => importer(pem)); + }); + } + + [Fact] + public static void ImportFromPem_EncryptedPrivateKey_Throws() + { + string pem = WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8); + AssertImportFromPem(importer => + { + AssertExtensions.Throws("source", () => importer(pem)); + }); + } + + [Fact] + public static void ImportFromPem_NoUnderstoodPem_Throws() + { + string pem = """ + -----BEGIN UNKNOWN----- + cGNq + -----END UNKNOWN----- + """; + + AssertImportFromPem(importer => + { + AssertExtensions.Throws("source", () => importer(pem)); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_NullSource() + { + AssertExtensions.Throws("source", + static () => X25519DiffieHellman.ImportFromEncryptedPem( + (string)null, + X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword)); + + AssertExtensions.Throws("source", + static () => X25519DiffieHellman.ImportFromEncryptedPem( + (string)null, + X25519DiffieHellmanTestData.EncryptedPrivateKeyPasswordBytes.ToArray())); + } + + [Fact] + public static void ImportFromEncryptedPem_NullPassword() + { + AssertExtensions.Throws("password", + static () => X25519DiffieHellman.ImportFromEncryptedPem("the pem", (string)null)); + + AssertExtensions.Throws("passwordBytes", + static () => X25519DiffieHellman.ImportFromEncryptedPem("the pem", (byte[])null)); + } + + [Fact] + public static void ImportFromEncryptedPem_PrivateKey_Roundtrip() + { + string pem = WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8); + AssertImportFromEncryptedPem(importer => + { + using X25519DiffieHellman xdh = importer(pem, X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_PrivateKey_Ambiguous_Throws() + { + string pem = $""" + {WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8)} + {WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8)} + """; + AssertImportFromEncryptedPem(importer => + { + AssertExtensions.Throws("source", + () => importer(pem, X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword)); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_PrivateKey_DoesNotImportNonEncrypted() + { + string pem = WritePem("PRIVATE KEY", AlicePkcs8); + AssertImportFromEncryptedPem(importer => + { + AssertExtensions.Throws("source", + () => importer(pem, "")); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_NoUnderstoodPem_Throws() + { + string pem = """ + -----BEGIN UNKNOWN----- + cGNq + -----END UNKNOWN----- + """; + AssertImportFromEncryptedPem(importer => + { + AssertExtensions.Throws("source", + () => importer(pem, "")); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_PrivateKey_IgnoresNotUnderstoodPems() + { + string pem = $""" + -----BEGIN UNKNOWN----- + cGNq + -----END UNKNOWN----- + {WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8)} + """; + AssertImportFromEncryptedPem(importer => + { + using X25519DiffieHellman xdh = importer(pem, X25519DiffieHellmanTestData.EncryptedPrivateKeyPassword); + AssertExtensions.SequenceEqual(X25519DiffieHellmanTestData.AlicePrivateKey, xdh.ExportPrivateKey()); + }); + } + + [Fact] + public static void ImportFromEncryptedPem_PrivateKey_WrongPassword() + { + string pem = WritePem("ENCRYPTED PRIVATE KEY", AliceEncryptedPkcs8); + AssertImportFromEncryptedPem(importer => + { + Assert.Throws( + () => importer(pem, "WRONG")); + }); + } + + private static void AssertImportFromPem(Action> callback) + { + callback(static (string pem) => X25519DiffieHellman.ImportFromPem(pem)); + callback(static (string pem) => X25519DiffieHellman.ImportFromPem(pem.AsSpan())); + } + + private static void AssertImportFromEncryptedPem(Action> callback) + { + callback(static (string pem, string password) => X25519DiffieHellman.ImportFromEncryptedPem(pem, password)); + + callback(static (string pem, string password) => X25519DiffieHellman.ImportFromEncryptedPem( + pem.AsSpan(), + password.AsSpan())); + + callback(static (string pem, string password) => X25519DiffieHellman.ImportFromEncryptedPem( + pem, + Encoding.UTF8.GetBytes(password))); + + callback(static (string pem, string password) => X25519DiffieHellman.ImportFromEncryptedPem( + pem.AsSpan(), + new ReadOnlySpan(Encoding.UTF8.GetBytes(password)))); + } + + private static string WritePem(string label, byte[] contents) + { + string base64 = Convert.ToBase64String(contents, Base64FormattingOptions.InsertLineBreaks); + return $"-----BEGIN {label}-----\n{base64}\n-----END {label}-----"; + } + + private static byte[] SpkiEncode(string oid, byte[] publicKey, byte[] algorithmParameters = null) + { + AsnWriter writer = new(AsnEncodingRules.DER); + using (writer.PushSequence()) + { + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(oid); + + if (algorithmParameters is not null) + { + writer.WriteEncodedValue(algorithmParameters); + } + } + + writer.WriteBitString(publicKey, 0); + } + + return writer.Encode(); + } + + private static byte[] Pkcs8Encode(string oid, byte[] privateKey, byte[] algorithmParameters = null) + { + AsnWriter privateKeyWriter = new(AsnEncodingRules.DER); + privateKeyWriter.WriteOctetString(privateKey); + byte[] encodedPrivateKey = privateKeyWriter.Encode(); + + AsnWriter writer = new(AsnEncodingRules.DER); + using (writer.PushSequence()) + { + writer.WriteInteger(0); + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(oid); + + if (algorithmParameters is not null) + { + writer.WriteEncodedValue(algorithmParameters); + } + } + + writer.WriteOctetString(encodedPrivateKey); + } + + return writer.Encode(); + } + } +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c index 1958ac1ca51f56..56e8762cdc4fc3 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -115,6 +115,13 @@ static const Entry s_cryptoAppleNative[] = DllImportEntry(AppleCryptoNative_StoreEnumerateMachineRoot) DllImportEntry(AppleCryptoNative_StoreEnumerateUserDisallowed) DllImportEntry(AppleCryptoNative_StoreEnumerateMachineDisallowed) + DllImportEntry(AppleCryptoNative_X25519DeriveRawSecretAgreement) + DllImportEntry(AppleCryptoNative_X25519ExportPrivateKey) + DllImportEntry(AppleCryptoNative_X25519ExportPublicKey) + DllImportEntry(AppleCryptoNative_X25519ImportPrivateKey) + DllImportEntry(AppleCryptoNative_X25519ImportPublicKey) + DllImportEntry(AppleCryptoNative_X25519FreeKey) + DllImportEntry(AppleCryptoNative_X25519GenerateKey) DllImportEntry(AppleCryptoNative_X509ChainCreate) DllImportEntry(AppleCryptoNative_X509DemuxAndRetainHandle) DllImportEntry(AppleCryptoNative_X509GetContentType) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h index bb81f0bf8b617b..93641c5f9f0baf 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h @@ -24,3 +24,11 @@ EXTERN_C void* AppleCryptoNative_DigestCurrent; EXTERN_C void* AppleCryptoNative_DigestOneShot; EXTERN_C void* AppleCryptoNative_DigestReset; EXTERN_C void* AppleCryptoNative_DigestClone; + +EXTERN_C void* AppleCryptoNative_X25519DeriveRawSecretAgreement; +EXTERN_C void* AppleCryptoNative_X25519ExportPrivateKey; +EXTERN_C void* AppleCryptoNative_X25519ExportPublicKey; +EXTERN_C void* AppleCryptoNative_X25519FreeKey; +EXTERN_C void* AppleCryptoNative_X25519GenerateKey; +EXTERN_C void* AppleCryptoNative_X25519ImportPublicKey; +EXTERN_C void* AppleCryptoNative_X25519ImportPrivateKey; diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift index 02b21a2160b54c..c94cff0ef005cb 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift @@ -11,6 +11,25 @@ final class HashBox { } } +enum X25519Key { + case PrivateKey(Curve25519.KeyAgreement.PrivateKey) + case PublicKey(Curve25519.KeyAgreement.PublicKey) + + func getPublic() -> Curve25519.KeyAgreement.PublicKey { + switch self { + case .PrivateKey(let key): return key.publicKey + case .PublicKey(let key): return key + } + } +} + +final class X25519KeyBox { + var value: X25519Key + init(_ value: X25519Key) { + self.value = value + } +} + protocol NonceProtocol { init(data: D) throws where D : DataProtocol } @@ -536,3 +555,135 @@ public func AppleCryptoNative_DigestCurrent(ctx: UnsafeMutableRawPointer?, pOutp return 1 } + +@_silgen_name("AppleCryptoNative_X25519DeriveRawSecretAgreement") +public func AppleCryptoNative_X25519DeriveRawSecretAgreement( + keyPtr: UnsafeMutableRawPointer?, + peerKeyPtr: UnsafeMutableRawPointer?, + pOutput: UnsafeMutablePointer?, + cbOutput: Int32) -> Int32 { + guard let keyPtr, let peerKeyPtr, let pOutput else { + return -1 + } + + let keyBox = Unmanaged.fromOpaque(keyPtr).takeUnretainedValue() + let peerBox = Unmanaged.fromOpaque(peerKeyPtr).takeUnretainedValue() + + guard case .PrivateKey(let key) = keyBox.value else { + return -1 + } + + let peerKey = peerBox.value.getPublic() + let destination = UnsafeMutableRawBufferPointer(start: pOutput, count: Int(cbOutput)) + + guard let sharedSecret = try? key.sharedSecretFromKeyAgreement(with: peerKey) else { + return 0 + } + + let copied = sharedSecret.withUnsafeBytes { rawSecret in + return rawSecret.copyBytes(to: destination) == rawSecret.count + } + + if (!copied) { + return -1 + } + + return 1 +} + +@_silgen_name("AppleCryptoNative_X25519FreeKey") +public func AppleCryptoNative_X25519FreeKey(ptr: UnsafeMutableRawPointer?) { + if let ptr { + Unmanaged.fromOpaque(ptr).release() + } +} + +@_silgen_name("AppleCryptoNative_X25519ExportPrivateKey") +public func AppleCryptoNative_X25519ExportPrivateKey( + keyPtr: UnsafeMutableRawPointer?, + pOutput: UnsafeMutablePointer?, + cbOutput: Int32) -> Int32 { + guard let keyPtr, let pOutput else { + return -1 + } + + let box = Unmanaged.fromOpaque(keyPtr).takeUnretainedValue() + + guard case .PrivateKey(let key) = box.value else { + return -1 + } + + let destination = UnsafeMutableRawBufferPointer(start: pOutput, count: Int(cbOutput)) + let copied = key.rawRepresentation.withUnsafeBytes { privateKey in + return privateKey.copyBytes(to: destination) == privateKey.count + } + + if (!copied) { + return -1 + } + + return 1 +} + +@_silgen_name("AppleCryptoNative_X25519ExportPublicKey") +public func AppleCryptoNative_X25519ExportPublicKey( + keyPtr: UnsafeMutableRawPointer?, + pOutput: UnsafeMutablePointer?, + cbOutput: Int32) -> Int32 { + guard let keyPtr, let pOutput else { + return -1 + } + + let box = Unmanaged.fromOpaque(keyPtr).takeUnretainedValue() + let key = box.value.getPublic() + let destination = UnsafeMutableRawBufferPointer(start: pOutput, count: Int(cbOutput)) + + let copied = key.rawRepresentation.withUnsafeBytes { pubKey in + return pubKey.copyBytes(to: destination) == pubKey.count + } + + if (!copied) { + return -1 + } + + return 1 +} + +@_silgen_name("AppleCryptoNative_X25519GenerateKey") +public func AppleCryptoNative_X25519GenerateKey() -> UnsafeMutableRawPointer? { + let key = Curve25519.KeyAgreement.PrivateKey.init() + let box = X25519KeyBox(X25519Key.PrivateKey(key)) + return Unmanaged.passRetained(box).toOpaque() +} + +@_silgen_name("AppleCryptoNative_X25519ImportPrivateKey") +public func AppleCryptoNative_X25519ImportPrivateKey(pKey: UnsafeMutableRawPointer?, cbKey: Int32) -> UnsafeMutableRawPointer? { + guard let pKey else { + return nil + } + + let source = Data(bytesNoCopy: pKey, count: Int(cbKey), deallocator: Data.Deallocator.none) + + guard let key = try? Curve25519.KeyAgreement.PrivateKey.init(rawRepresentation: source) else { + return nil + } + + let box = X25519KeyBox(X25519Key.PrivateKey(key)) + return Unmanaged.passRetained(box).toOpaque() +} + +@_silgen_name("AppleCryptoNative_X25519ImportPublicKey") +public func AppleCryptoNative_X25519ImportPublicKey(pKey: UnsafeMutableRawPointer?, cbKey: Int32) -> UnsafeMutableRawPointer? { + guard let pKey else { + return nil + } + + let source = Data(bytesNoCopy: pKey, count: Int(cbKey), deallocator: Data.Deallocator.none) + + guard let key = try? Curve25519.KeyAgreement.PublicKey.init(rawRepresentation: source) else { + return nil + } + + let box = X25519KeyBox(X25519Key.PublicKey(key)) + return Unmanaged.passRetained(box).toOpaque() +} diff --git a/src/native/libs/System.Security.Cryptography.Native/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native/CMakeLists.txt index 0a71e9888a0ba8..720d6237e9dac7 100644 --- a/src/native/libs/System.Security.Cryptography.Native/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native/CMakeLists.txt @@ -72,6 +72,7 @@ set(NATIVECRYPTO_SOURCES pal_evp_pkey_raw_signverify.c pal_evp_pkey_ml_dsa.c pal_evp_pkey_slh_dsa.c + pal_evp_pkey_x25519.c pal_hmac.c pal_ocsp.c pal_pkcs7.c diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index 389504b3265e3f..20fd329b974314 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -26,6 +26,7 @@ #include "pal_evp_pkey_rsa.h" #include "pal_evp_pkey_ml_dsa.h" #include "pal_evp_pkey_slh_dsa.h" +#include "pal_evp_pkey_x25519.h" #include "pal_hmac.h" #include "pal_ocsp.h" #include "pal_pkcs7.h" @@ -420,6 +421,11 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_SslV2_3Method) DllImportEntry(CryptoNative_SslWrite) DllImportEntry(CryptoNative_Tls13Supported) + DllImportEntry(CryptoNative_X25519ExportPrivateKey) + DllImportEntry(CryptoNative_X25519ExportPublicKey) + DllImportEntry(CryptoNative_X25519GenerateKey) + DllImportEntry(CryptoNative_X25519ImportPrivateKey) + DllImportEntry(CryptoNative_X25519ImportPublicKey) DllImportEntry(CryptoNative_X509DecodeOcspToExpiration) DllImportEntry(CryptoNative_X509Duplicate) DllImportEntry(CryptoNative_SslGet0AlpnSelected) diff --git a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h index 22425beb8a4126..f4cd4846ca5d53 100644 --- a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h +++ b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h @@ -535,6 +535,8 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(EVP_PKEY_CTX_new_id) \ LIGHTUP_FUNCTION(EVP_PKEY_CTX_new_from_name) \ LIGHTUP_FUNCTION(EVP_PKEY_CTX_new_from_pkey) \ + REQUIRED_FUNCTION(EVP_PKEY_new_raw_private_key) \ + REQUIRED_FUNCTION(EVP_PKEY_new_raw_public_key) \ LIGHTUP_FUNCTION(EVP_PKEY_CTX_set_params) \ FALLBACK_FUNCTION(EVP_PKEY_CTX_set_rsa_keygen_bits) \ FALLBACK_FUNCTION(EVP_PKEY_CTX_set_rsa_oaep_md) \ @@ -558,6 +560,8 @@ extern bool g_libSslUses32BitTime; LIGHTUP_FUNCTION(EVP_PKEY_fromdata_init) \ RENAMED_FUNCTION(EVP_PKEY_get_base_id, EVP_PKEY_base_id) \ RENAMED_FUNCTION(EVP_PKEY_get_bits, EVP_PKEY_bits) \ + REQUIRED_FUNCTION(EVP_PKEY_get_raw_private_key) \ + REQUIRED_FUNCTION(EVP_PKEY_get_raw_public_key) \ LIGHTUP_FUNCTION(EVP_PKEY_get0_RSA) \ LIGHTUP_FUNCTION(EVP_PKEY_get0_type_name) \ REQUIRED_FUNCTION(EVP_PKEY_get1_DSA) \ @@ -1115,6 +1119,8 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define EVP_PKEY_fromdata_init EVP_PKEY_fromdata_init_ptr #define EVP_PKEY_get_base_id EVP_PKEY_get_base_id_ptr #define EVP_PKEY_get_bits EVP_PKEY_get_bits_ptr +#define EVP_PKEY_get_raw_private_key EVP_PKEY_get_raw_private_key_ptr +#define EVP_PKEY_get_raw_public_key EVP_PKEY_get_raw_public_key_ptr #define EVP_PKEY_get0_RSA EVP_PKEY_get0_RSA_ptr #define EVP_PKEY_get0_type_name EVP_PKEY_get0_type_name_ptr #define EVP_PKEY_get1_DSA EVP_PKEY_get1_DSA_ptr @@ -1126,6 +1132,8 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define EVP_PKEY_CTX_new_from_name EVP_PKEY_CTX_new_from_name_ptr #define EVP_PKEY_CTX_new_from_pkey EVP_PKEY_CTX_new_from_pkey_ptr #define EVP_PKEY_CTX_new_from_name EVP_PKEY_CTX_new_from_name_ptr +#define EVP_PKEY_new_raw_private_key EVP_PKEY_new_raw_private_key_ptr +#define EVP_PKEY_new_raw_public_key EVP_PKEY_new_raw_public_key_ptr #define EVP_PKEY_CTX_set_params EVP_PKEY_CTX_set_params_ptr #define EVP_PKEY_public_check EVP_PKEY_public_check_ptr #define EVP_PKEY_set1_DSA EVP_PKEY_set1_DSA_ptr diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c new file mode 100644 index 00000000000000..0ed9e111c24a08 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_evp_pkey.h" +#include "pal_evp_pkey_x25519.h" +#include "pal_utilities.h" +#include "openssl.h" +#include + +static int32_t ExportRawKeyMaterial( + const EVP_PKEY* key, + uint8_t* destination, + int32_t destinationLength, + int (*exporter)(const EVP_PKEY*, unsigned char*, size_t*)) +{ + assert(key != NULL && destination != NULL && exporter != NULL); + + ERR_clear_error(); + + size_t len = Int32ToSizeT(destinationLength); + int result = exporter(key, destination, &len); + + if (result != 1) + { + return 0; + } + + if (len != Int32ToSizeT(destinationLength)) + { + assert("Exported raw key was not the correct length." && 0); + return -1; + } + + return 1; +} + +int32_t CryptoNative_X25519ExportPrivateKey(const EVP_PKEY* key, uint8_t* destination, int32_t destinationLength) +{ + return ExportRawKeyMaterial(key, destination, destinationLength, EVP_PKEY_get_raw_private_key); +} + +int32_t CryptoNative_X25519ExportPublicKey(const EVP_PKEY* key, uint8_t* destination, int32_t destinationLength) +{ + return ExportRawKeyMaterial(key, destination, destinationLength, EVP_PKEY_get_raw_public_key); +} + +EVP_PKEY* CryptoNative_X25519GenerateKey(void) +{ + ERR_clear_error(); + + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + + if (ctx == NULL) + { + return NULL; + } + + EVP_PKEY* pkey = NULL; + EVP_PKEY* ret = NULL; + + if (EVP_PKEY_keygen_init(ctx) == 1 && EVP_PKEY_keygen(ctx, &pkey) == 1) + { + ret = pkey; + pkey = NULL; + } + + if (pkey != NULL) + { + EVP_PKEY_free(pkey); + } + + EVP_PKEY_CTX_free(ctx); + return ret; +} + +EVP_PKEY* CryptoNative_X25519ImportPrivateKey(const uint8_t* source, int32_t sourceLength) +{ + assert(source && sourceLength > 0); + ERR_clear_error(); + + return EVP_PKEY_new_raw_private_key( + EVP_PKEY_X25519, + NULL, + source, + Int32ToSizeT(sourceLength)); +} + +EVP_PKEY* CryptoNative_X25519ImportPublicKey(const uint8_t* source, int32_t sourceLength) +{ + assert(source && sourceLength > 0); + ERR_clear_error(); + + return EVP_PKEY_new_raw_public_key( + EVP_PKEY_X25519, + NULL, + source, + Int32ToSizeT(sourceLength)); +} diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h new file mode 100644 index 00000000000000..a76ee2f894711d --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "opensslshim.h" +#include "pal_compiler.h" +#include "pal_types.h" + +/* +Exports the raw private key material from an X25519 EVP_PKEY. + +Returns 1 on success, 0 on failure, -1 if the exported key length does not match destinationLength. +*/ +PALEXPORT int32_t CryptoNative_X25519ExportPrivateKey(const EVP_PKEY* key, uint8_t* destination, int32_t destinationLength); + +/* +Exports the raw public key material from an X25519 EVP_PKEY. + +Returns 1 on success, 0 on failure, -1 if the exported key length does not match destinationLength. +*/ +PALEXPORT int32_t CryptoNative_X25519ExportPublicKey(const EVP_PKEY* key, uint8_t* destination, int32_t destinationLength); + +/* +Imports a raw private key and returns a new X25519 EVP_PKEY. + +Returns the new EVP_PKEY on success, NULL on failure. +*/ +PALEXPORT EVP_PKEY* CryptoNative_X25519ImportPrivateKey(const uint8_t* source, int32_t sourceLength); + +/* +Imports a raw public key and returns a new X25519 EVP_PKEY. + +Returns the new EVP_PKEY on success, NULL on failure. +*/ +PALEXPORT EVP_PKEY* CryptoNative_X25519ImportPublicKey(const uint8_t* source, int32_t sourceLength); + +/* +Generates a new X25519 key pair and returns it as an EVP_PKEY. + +Returns the new EVP_PKEY on success, NULL on failure. +*/ +PALEXPORT EVP_PKEY* CryptoNative_X25519GenerateKey(void); + From 2cfa928b5092b2787d3838885e333aac052b52cc Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 21 Apr 2026 17:38:34 -0400 Subject: [PATCH 2/9] Fix typo. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../System.Security.Cryptography.Native.Apple/Interop.X25519.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs index 7e6951539232ae..1c4181115a7d2f 100644 --- a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X25519.cs @@ -60,7 +60,7 @@ internal static void X25519DeriveRawSecretAgreement(SafeX25519KeyHandle key, Saf if (ret != Success) { - Debug.Fail($"Unexpected result from {nameof(AppleCryptoNative_X25519ExportPrivateKey)}: {ret}."); + Debug.Fail($"Unexpected result from {nameof(AppleCryptoNative_X25519DeriveRawSecretAgreement)}: {ret}."); throw new CryptographicException(); } } From 012265d5248311c6aca7dd04ae5339456f496b57 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 21 Apr 2026 17:45:02 -0400 Subject: [PATCH 3/9] Fix accidentally removed character --- .../tests/X25519DiffieHellmanNotSupportedTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs index 7b77455eca29be..03e2711dbb0a8a 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanNotSupportedTests.cs @@ -41,7 +41,7 @@ public static void ImportSubjectPublicKeyInfo_NotSupported() { // A minimal valid SPKI for X25519 byte[] spki = Convert.FromHexString( - "302a300506032b656e032100" + "302a300506032b656e032100" + "0000000000000000000000000000000000000000000000000000000000000000"); Assert.Throws(() => From 1040a970b931300f1105e8ba9cb50e39db5b3461 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 21 Apr 2026 22:05:35 -0400 Subject: [PATCH 4/9] Code review feedback --- .../Microsoft.Bcl.Cryptography.Tests.csproj | 2 + .../Cryptography/X25519DiffieHellman.cs | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) 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 ebb1d7bcceeb63..616f3521063d97 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 @@ -149,6 +149,8 @@ Link="CommonTest\System\Security\Cryptography\MLKemCngTests.NotSupported.cs" /> + (Func, private protected bool TryExportPkcs8PrivateKeyImpl(Span destination, out int bytesWritten) { - ValueAlgorithmIdentifierAsn algorithmIdentifier = new() + // Pre-encoded PKCS#8 PrivateKeyInfo for X25519 (RFC 8410): + ReadOnlySpan pkcs8Preamble = + [ + 0x30, 0x2e, // SEQUENCE (46 bytes) + 0x02, 0x01, 0x00, // INTEGER 0 + 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, // SEQUENCE { OID 1.3.101.110 } + 0x04, 0x22, // OCTET STRING (34 bytes) + 0x04, 0x20, // OCTET STRING (32 bytes) + ]; + + int pkcs8SizeInBytes = pkcs8Preamble.Length + PrivateKeySizeInBytes; + + if (destination.Length < pkcs8SizeInBytes) { - Algorithm = Oids.X25519, - }; + bytesWritten = 0; + return false; + } - Span privateKey = stackalloc byte[PrivateKeySizeInBytes]; - ExportPrivateKey(privateKey); + pkcs8Preamble.CopyTo(destination); + Span privateKeyBuffer = destination.Slice(pkcs8Preamble.Length, PrivateKeySizeInBytes); try { - AsnWriter algorithmWriter = new(AsnEncodingRules.DER); - algorithmIdentifier.Encode(algorithmWriter); - AsnWriter privateKeyWriter = new(AsnEncodingRules.DER); - privateKeyWriter.WriteOctetString(privateKey); - AsnWriter pkcs8Writer = KeyFormatHelper.WritePkcs8(algorithmWriter, privateKeyWriter); - - bool result = pkcs8Writer.TryEncode(destination, out bytesWritten); - privateKeyWriter.Reset(); - pkcs8Writer.Reset(); - return result; + ExportPrivateKey(privateKeyBuffer); + bytesWritten = pkcs8SizeInBytes; + return true; } - finally + catch { - CryptographicOperations.ZeroMemory(privateKey); + CryptographicOperations.ZeroMemory(privateKeyBuffer); + throw; } } From d3dc8bee3941a55ea53fc0f214d5fdf05cf3acb0 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 21 Apr 2026 22:30:48 -0400 Subject: [PATCH 5/9] Pre-encode the PKCS#8 and SPKI preambles --- .../Cryptography/X25519DiffieHellman.cs | 65 ++++++++++++------- .../tests/X25519DiffieHellmanContractTests.cs | 8 +-- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs index 26e36cb52ef032..3036e6f14ed021 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs @@ -41,6 +41,9 @@ public abstract class X25519DiffieHellman : IDisposable /// public const int PublicKeySizeInBytes = 32; + // Pre-encoded SPKI for X25519 is 44 bytes: 12 byte preamble + 32 byte public key. + private const int SpkiSizeInBytes = 12 + PublicKeySizeInBytes; + /// /// Gets a value that indicates whether the algorithm is supported on the current platform. /// @@ -248,7 +251,7 @@ public void ExportPublicKey(Span destination) public bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesWritten) { ThrowIfDisposed(); - return ExportSubjectPublicKeyInfoCore().TryEncode(destination, out bytesWritten); + return TryExportSubjectPublicKeyInfoCore(destination, out bytesWritten); } /// @@ -264,7 +267,16 @@ public bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesW public byte[] ExportSubjectPublicKeyInfo() { ThrowIfDisposed(); - return ExportSubjectPublicKeyInfoCore().Encode(); + byte[] result = new byte[SpkiSizeInBytes]; + bool exported = TryExportSubjectPublicKeyInfoCore(result, out int bytesWritten); + + if (!exported || bytesWritten != SpkiSizeInBytes) + { + Debug.Fail("Export unexpectedly failed to pre-sized buffer or wrote an unexpected number of bytes."); + throw new CryptographicException(); + } + + return result; } /// @@ -282,9 +294,16 @@ public byte[] ExportSubjectPublicKeyInfo() public string ExportSubjectPublicKeyInfoPem() { ThrowIfDisposed(); - AsnWriter writer = ExportSubjectPublicKeyInfoCore(); - // SPKI does not contain sensitive data. - return Helpers.EncodeAsnWriterToPem(PemLabels.SpkiPublicKey, writer, clear: false); + Span spki = stackalloc byte[SpkiSizeInBytes]; + bool exported = TryExportSubjectPublicKeyInfoCore(spki, out int bytesWritten); + + if (!exported || bytesWritten != SpkiSizeInBytes) + { + Debug.Fail("Export unexpectedly failed to pre-sized buffer or wrote an unexpected number of bytes."); + throw new CryptographicException(); + } + + return PemEncoding.WriteString(PemLabels.SpkiPublicKey, spki); } /// @@ -1322,26 +1341,28 @@ protected virtual void Dispose(bool disposing) { } - private AsnWriter ExportSubjectPublicKeyInfoCore() + private bool TryExportSubjectPublicKeyInfoCore(Span destination, out int bytesWritten) { - Span publicKey = stackalloc byte[PublicKeySizeInBytes]; - ExportPublicKeyCore(publicKey); + // Pre-encoded SubjectPublicKeyInfo for X25519 (RFC 8410): + ReadOnlySpan spkiPreamble = + [ + 0x30, 0x2a, // SEQUENCE (42 bytes) + 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, // SEQUENCE { OID 1.3.101.110 } + 0x03, 0x21, 0x00, // BIT STRING (33 bytes, 0 unused bits) + ]; + + Debug.Assert(spkiPreamble.Length + PublicKeySizeInBytes == SpkiSizeInBytes); - ValueSubjectPublicKeyInfoAsn spki = new ValueSubjectPublicKeyInfoAsn + if (destination.Length < SpkiSizeInBytes) { - Algorithm = new ValueAlgorithmIdentifierAsn - { - Algorithm = Oids.X25519, - }, - SubjectPublicKey = publicKey, - }; - - // The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is 12 bytes. - // Round it off to 16. - const int Capacity = 16 + PublicKeySizeInBytes; - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER, Capacity); - spki.Encode(writer); - return writer; + bytesWritten = 0; + return false; + } + + spkiPreamble.CopyTo(destination); + ExportPublicKeyCore(destination.Slice(spkiPreamble.Length, PublicKeySizeInBytes)); + bytesWritten = SpkiSizeInBytes; + return true; } private TResult ExportPkcs8PrivateKeyCallback(Func, TResult> func) diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs index 72f8c947d81883..294692e94fd9c2 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanContractTests.cs @@ -284,13 +284,7 @@ public static void TryExportSubjectPublicKeyInfo_Disposed() [Fact] public static void TryExportSubjectPublicKeyInfo_DestinationTooSmall() { - using X25519DiffieHellmanContract xdh = new() - { - OnExportPublicKeyCore = (Span destination) => - { - destination.Fill(0x42); - } - }; + using X25519DiffieHellmanContract xdh = new(); byte[] destination = new byte[1]; AssertExtensions.FalseExpression(xdh.TryExportSubjectPublicKeyInfo(destination, out int written)); From 2f9436927e7332a3bf451cc32bda6be4b8dfaf72 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 22 Apr 2026 14:23:50 -0400 Subject: [PATCH 6/9] Check for availability of X25519 --- .../Interop.EvpPkey.X25519.cs | 4 ++++ ...5519DiffieHellmanImplementation.OpenSsl.cs | 2 +- .../entrypoints.c | 1 + .../pal_evp_pkey_x25519.c | 23 +++++++++++++++++++ .../pal_evp_pkey_x25519.h | 8 ++++++- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs index 98d772b2720f6d..cd5640e407b267 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs @@ -11,6 +11,10 @@ internal static partial class Interop { internal static partial class Crypto { + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519Available")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool X25519Available(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ExportPrivateKey")] private static partial int X25519ExportPrivateKey( SafeEvpPKeyHandle key, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs index 434cfbf5cbe672..7d144ec72411c4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs @@ -10,7 +10,7 @@ internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman private readonly SafeEvpPKeyHandle _key; private readonly bool _hasPrivate; - internal static new bool IsSupported => true; + internal static new bool IsSupported { get; } = Interop.Crypto.X25519Available(); private X25519DiffieHellmanImplementation(SafeEvpPKeyHandle key, bool hasPrivate) { diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index 20fd329b974314..87a567832bfad0 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -421,6 +421,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_SslV2_3Method) DllImportEntry(CryptoNative_SslWrite) DllImportEntry(CryptoNative_Tls13Supported) + DllImportEntry(CryptoNative_X25519Available) DllImportEntry(CryptoNative_X25519ExportPrivateKey) DllImportEntry(CryptoNative_X25519ExportPublicKey) DllImportEntry(CryptoNative_X25519GenerateKey) diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c index 0ed9e111c24a08..43c4a5ae0c8a4e 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c @@ -34,6 +34,29 @@ static int32_t ExportRawKeyMaterial( return 1; } +int32_t CryptoNative_X25519Available(void) +{ + ERR_clear_error(); + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); + + if (ctx) + { + EVP_PKEY_CTX_free(ctx); + return 1; + } + + // X25519 might not be available for two reasons. + // 1. It was built with `no-ecx` which is available starting in OpenSSL 3.2. + // 2. The default Provider is the FIPS provider, and X25519 is not available in the FIPS provider. + // In both cases, ERR_R_UNSUPPORTED is put in the error queue. + // If we errored for a different reason, we _still_ want to return "yes" for is supported. This will allow for + // actual use of X25519 to throw the appropriate error. + unsigned long error = ERR_peek_error(); + int32_t result = ERR_GET_REASON(error) == ERR_R_UNSUPPORTED ? 0 : 1; + ERR_clear_error(); + return result; +} + int32_t CryptoNative_X25519ExportPrivateKey(const EVP_PKEY* key, uint8_t* destination, int32_t destinationLength) { return ExportRawKeyMaterial(key, destination, destinationLength, EVP_PKEY_get_raw_private_key); diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h index a76ee2f894711d..10ce3831db2282 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h @@ -5,6 +5,13 @@ #include "pal_compiler.h" #include "pal_types.h" +/* +Determines if the X25519 algorithm is supported. + +Returns 1 if the algorithm is available, 0 otherwise. +*/ +PALEXPORT int32_t CryptoNative_X25519Available(void); + /* Exports the raw private key material from an X25519 EVP_PKEY. @@ -39,4 +46,3 @@ Generates a new X25519 key pair and returns it as an EVP_PKEY. Returns the new EVP_PKEY on success, NULL on failure. */ PALEXPORT EVP_PKEY* CryptoNative_X25519GenerateKey(void); - From dbd13fdc712b39914d942178a2707a86bda1c881 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 22 Apr 2026 17:48:52 -0400 Subject: [PATCH 7/9] Add test for IsSupported --- .../X25519DiffieHellmanImplementationTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs index 9c09e73c7ce278..fa00b438fbc569 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs @@ -16,4 +16,18 @@ public override X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source) public override X25519DiffieHellman ImportPublicKey(ReadOnlySpan source) => X25519DiffieHellman.ImportPublicKey(source); } + + public static class X25519DiffieHellmanImplementationSupportedTests + { + [Fact] + public static void IsSupported_AgreesWithPlatform() + { + bool expectedSupported = + PlatformDetection.IsWindows10OrLater || + PlatformDetection.IsApplePlatform || + PlatformDetection.IsOpenSslSupported; // X25519 is in OpenSSL 1.1.0 and .NET's floor is 1.1.1. + + Assert.Equal(expectedSupported, X25519DiffieHellman.IsSupported); + } + } } From 46696458b0c7f3128338557b0f8a76ae4436a018 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 22 Apr 2026 18:09:23 -0400 Subject: [PATCH 8/9] Fix dup word. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/X25519DiffieHellmanBaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs index c749d97dd2455a..1161bda89702a0 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanBaseTests.cs @@ -16,7 +16,7 @@ public abstract class X25519DiffieHellmanBaseTests public abstract X25519DiffieHellman ImportPrivateKey(ReadOnlySpan source); public abstract X25519DiffieHellman ImportPublicKey(ReadOnlySpan source); - // SymCrypt, thus SCOSSL and and CNG, are stricter about keys they are willing to import. These keys fall in to + // SymCrypt, thus SCOSSL and CNG, are stricter about keys they are willing to import. These keys fall in to // two buckets. // 1. Public keys that are non-canonical. RFC 7748 says: // Implementations MUST accept non-canonical values and process them as From 4902ae4f91dc5578622f15ab0fa2f9e41c43dbbb Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 22 Apr 2026 18:14:02 -0400 Subject: [PATCH 9/9] Use idiomatic casing for swift enums --- .../pal_swiftbindings.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift index c94cff0ef005cb..2505f546ef9fde 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift @@ -12,13 +12,13 @@ final class HashBox { } enum X25519Key { - case PrivateKey(Curve25519.KeyAgreement.PrivateKey) - case PublicKey(Curve25519.KeyAgreement.PublicKey) + case privateKey(Curve25519.KeyAgreement.PrivateKey) + case publicKey(Curve25519.KeyAgreement.PublicKey) func getPublic() -> Curve25519.KeyAgreement.PublicKey { switch self { - case .PrivateKey(let key): return key.publicKey - case .PublicKey(let key): return key + case .privateKey(let key): return key.publicKey + case .publicKey(let key): return key } } } @@ -569,7 +569,7 @@ public func AppleCryptoNative_X25519DeriveRawSecretAgreement( let keyBox = Unmanaged.fromOpaque(keyPtr).takeUnretainedValue() let peerBox = Unmanaged.fromOpaque(peerKeyPtr).takeUnretainedValue() - guard case .PrivateKey(let key) = keyBox.value else { + guard case .privateKey(let key) = keyBox.value else { return -1 } @@ -609,7 +609,7 @@ public func AppleCryptoNative_X25519ExportPrivateKey( let box = Unmanaged.fromOpaque(keyPtr).takeUnretainedValue() - guard case .PrivateKey(let key) = box.value else { + guard case .privateKey(let key) = box.value else { return -1 } @@ -652,7 +652,7 @@ public func AppleCryptoNative_X25519ExportPublicKey( @_silgen_name("AppleCryptoNative_X25519GenerateKey") public func AppleCryptoNative_X25519GenerateKey() -> UnsafeMutableRawPointer? { let key = Curve25519.KeyAgreement.PrivateKey.init() - let box = X25519KeyBox(X25519Key.PrivateKey(key)) + let box = X25519KeyBox(X25519Key.privateKey(key)) return Unmanaged.passRetained(box).toOpaque() } @@ -668,7 +668,7 @@ public func AppleCryptoNative_X25519ImportPrivateKey(pKey: UnsafeMutableRawPoint return nil } - let box = X25519KeyBox(X25519Key.PrivateKey(key)) + let box = X25519KeyBox(X25519Key.privateKey(key)) return Unmanaged.passRetained(box).toOpaque() } @@ -684,6 +684,6 @@ public func AppleCryptoNative_X25519ImportPublicKey(pKey: UnsafeMutableRawPointe return nil } - let box = X25519KeyBox(X25519Key.PublicKey(key)) + let box = X25519KeyBox(X25519Key.publicKey(key)) return Unmanaged.passRetained(box).toOpaque() }