From 2b46f3a7eb3d0df99c523e5648f00cc8b53caa05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:52:24 -0700 Subject: [PATCH] [release/9.0] Permit unencrypted key exports from CNG (#109134) * Permit unencrypted key exports from CNG. CNG, by default, loads PKCS#12 certificate private keys as "AllowExport", not "AllowsPlaintextExport". When users attempt to export the private key from a loaded PKCS#12, they will receive an error that the operation is not permitted because they are expected to perform an encrypted export. This is counter-intuitive to some people, as the general expectation is that they can export private keys they just loaded. Starting in .NET 9, we are loading more PKCS#12 private keys in CNG instead of the legacy CSP, meaning users will hit this problem more. This is also a regression from .NET 8. The default provider changed, meaning keys that were once exportable no longer are. This pull request makes a change similar to what we do for macOS. If a user asks for an unencrypted export of the private key, and the key does not permit that, we will ask CNG for an encrypted export of the private key and decrypt it for them. This makes the unencrypted exports "just work", as they do on other platforms. * Skip explicit curve tests on platforms that do not support explicit curves * Fix property name to make sense to people other than me * Apply fix to ECDH and DSA * Fix tests * Code review feedback --------- Co-authored-by: Kevin Jones --- .../Cryptography/DSACng.ImportExport.cs | 15 +- .../ECDiffieHellmanCng.ImportExport.cs | 42 +-- .../Cryptography/ECDsaCng.ImportExport.cs | 24 +- .../Cryptography/RSACng.ImportExport.cs | 14 + .../tests/CngPkcs8Tests.cs | 29 +- .../System/Security/Cryptography/CngPkcs8.cs | 6 + .../Cryptography/DSACng.ImportExport.cs | 14 + .../Cryptography/ECCng.ImportExport.cs | 95 +++++++ .../Cryptography/ECDiffieHellmanCng.cs | 14 + .../System/Security/Cryptography/ECDsaCng.cs | 14 + .../Cryptography/RSACng.ImportExport.cs | 14 + .../tests/X509Certificates/ExportTests.cs | 256 ++++++++++++++++++ 12 files changed, 471 insertions(+), 66 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs b/src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs index 31a2418f5e5ce4..ca2acd8a08792c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs @@ -310,6 +310,20 @@ private static unsafe void GenerateV2DsaBlob(out byte[] blob, DSAParameters para public override DSAParameters ExportParameters(bool includePrivateParameters) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (includePrivateParameters && encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + DSAKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out DSAParameters dsaParameters); + return dsaParameters; + } + byte[] dsaBlob = ExportKeyBlob(includePrivateParameters); KeyBlobMagicNumber magic = (KeyBlobMagicNumber)BitConverter.ToInt32(dsaBlob, 0); @@ -423,6 +437,5 @@ private static void CheckMagicValueOfKey(KeyBlobMagicNumber magic, bool includeP throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } } - } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECDiffieHellmanCng.ImportExport.cs b/src/libraries/Common/src/System/Security/Cryptography/ECDiffieHellmanCng.ImportExport.cs index 2dc7e613b72c8b..60717ea02bdf5c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECDiffieHellmanCng.ImportExport.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECDiffieHellmanCng.ImportExport.cs @@ -66,50 +66,12 @@ public override void ImportParameters(ECParameters parameters) public override ECParameters ExportExplicitParameters(bool includePrivateParameters) { - byte[] blob = ExportFullKeyBlob(includePrivateParameters); - - try - { - ECParameters ecparams = default; - ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters); - return ecparams; - } - finally - { - Array.Clear(blob); - } + return ECCng.ExportExplicitParameters(Key, includePrivateParameters); } public override ECParameters ExportParameters(bool includePrivateParameters) { - ECParameters ecparams = default; - - string? curveName = GetCurveName(out string? oidValue); - byte[]? blob = null; - - try - { - if (string.IsNullOrEmpty(curveName)) - { - blob = ExportFullKeyBlob(includePrivateParameters); - ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters); - } - else - { - blob = ExportKeyBlob(includePrivateParameters); - ECCng.ExportNamedCurveParameters(ref ecparams, blob, includePrivateParameters); - ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName)); - } - - return ecparams; - } - finally - { - if (blob != null) - { - Array.Clear(blob); - } - } + return ECCng.ExportParameters(Key, includePrivateParameters); } public override void ImportPkcs8PrivateKey(ReadOnlySpan source, out int bytesRead) diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECDsaCng.ImportExport.cs b/src/libraries/Common/src/System/Security/Cryptography/ECDsaCng.ImportExport.cs index 0eb393a4f2516c..cf3bd9108dbac9 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECDsaCng.ImportExport.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECDsaCng.ImportExport.cs @@ -1,6 +1,7 @@ // 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 Internal.NativeCrypto; namespace System.Security.Cryptography @@ -87,10 +88,7 @@ public override void ImportParameters(ECParameters parameters) /// The key and explicit curve parameters used by the ECC object. public override ECParameters ExportExplicitParameters(bool includePrivateParameters) { - byte[] blob = ExportFullKeyBlob(includePrivateParameters); - ECParameters ecparams = default; - ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters); - return ecparams; + return ECCng.ExportExplicitParameters(Key, includePrivateParameters); } /// @@ -103,23 +101,7 @@ public override ECParameters ExportExplicitParameters(bool includePrivateParamet /// The key and named curve parameters used by the ECC object. public override ECParameters ExportParameters(bool includePrivateParameters) { - ECParameters ecparams = default; - - string? curveName = GetCurveName(out string? oidValue); - - if (string.IsNullOrEmpty(curveName)) - { - byte[] fullKeyBlob = ExportFullKeyBlob(includePrivateParameters); - ECCng.ExportPrimeCurveParameters(ref ecparams, fullKeyBlob, includePrivateParameters); - } - else - { - byte[] keyBlob = ExportKeyBlob(includePrivateParameters); - ECCng.ExportNamedCurveParameters(ref ecparams, keyBlob, includePrivateParameters); - ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName)); - } - - return ecparams; + return ECCng.ExportParameters(Key, includePrivateParameters); } public override void ImportPkcs8PrivateKey(ReadOnlySpan source, out int bytesRead) diff --git a/src/libraries/Common/src/System/Security/Cryptography/RSACng.ImportExport.cs b/src/libraries/Common/src/System/Security/Cryptography/RSACng.ImportExport.cs index 2d0129412d9fb2..45f9cdda7cc751 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/RSACng.ImportExport.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/RSACng.ImportExport.cs @@ -180,6 +180,20 @@ public override bool TryExportEncryptedPkcs8PrivateKey( /// public override RSAParameters ExportParameters(bool includePrivateParameters) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (includePrivateParameters && encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + RSAKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out RSAParameters rsaParameters); + return rsaParameters; + } + byte[] rsaBlob = ExportKeyBlob(includePrivateParameters); RSAParameters rsaParams = default; rsaParams.FromBCryptBlob(rsaBlob, includePrivateParameters); diff --git a/src/libraries/System.Security.Cryptography.Cng/tests/CngPkcs8Tests.cs b/src/libraries/System.Security.Cryptography.Cng/tests/CngPkcs8Tests.cs index fd264c44db8734..93854e191e66e8 100644 --- a/src/libraries/System.Security.Cryptography.Cng/tests/CngPkcs8Tests.cs +++ b/src/libraries/System.Security.Cryptography.Cng/tests/CngPkcs8Tests.cs @@ -17,11 +17,32 @@ public void NoPlaintextExportFailsPkcs8() { SetExportPolicy(cngKey, CngExportPolicies.AllowExport); - Assert.ThrowsAny( - () => key.ExportPkcs8PrivateKey()); + byte[] exported = key.ExportPkcs8PrivateKey(); + + using (T imported = CreateKey(out _)) + { + imported.ImportPkcs8PrivateKey(exported, out int importRead); + Assert.Equal(exported.Length, importRead); + VerifyMatch(key, imported); + } + + byte[] tryExported = new byte[exported.Length]; + + int written; + + while (!key.TryExportPkcs8PrivateKey(tryExported, out written)) + { + Array.Resize(ref tryExported, checked(tryExported.Length * 2)); + } + + using (T imported = CreateKey(out _)) + { + imported.ImportPkcs8PrivateKey(tryExported.AsSpan(0, written), out int tryImportRead); + Assert.Equal(written, tryImportRead); + VerifyMatch(key, imported); + } + - Assert.ThrowsAny( - () => key.TryExportPkcs8PrivateKey(Span.Empty, out _)); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs index 7324a310746ba7..27d059dfa11e21 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs @@ -43,5 +43,11 @@ private static Pkcs8Response ImportPkcs8( Key = key, }; } + + internal static bool AllowsOnlyEncryptedExport(CngKey key) + { + const CngExportPolicies Exportable = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport; + return (key.ExportPolicy & Exportable) == CngExportPolicies.AllowExport; + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/DSACng.ImportExport.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/DSACng.ImportExport.cs index 7ac9a0f118dfa3..9ccc7e69fb9cc1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/DSACng.ImportExport.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/DSACng.ImportExport.cs @@ -50,6 +50,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response) public override bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + DSAKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out DSAParameters dsaParameters); + return DSAKeyFormatHelper.WritePkcs8(dsaParameters).TryEncode(destination, out bytesWritten); + } + return Key.TryExportKeyBlob( Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, destination, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCng.ImportExport.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCng.ImportExport.cs index 14d7b17565e95d..96aa067ac3897a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCng.ImportExport.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCng.ImportExport.cs @@ -1,6 +1,7 @@ // 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 static Interop.BCrypt; namespace System.Security.Cryptography @@ -77,6 +78,100 @@ internal static byte[] ExportKeyBlob( return blob; } + internal static ECParameters ExportExplicitParameters(CngKey key, bool includePrivateParameters) + { + if (includePrivateParameters) + { + return ExportPrivateExplicitParameters(key); + } + else + { + byte[] blob = ExportFullKeyBlob(key, includePrivateParameters: false); + ECParameters ecparams = default; + ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters: false); + return ecparams; + } + } + + internal static ECParameters ExportParameters(CngKey key, bool includePrivateParameters) + { + ECParameters ecparams = default; + + const string TemporaryExportPassword = "DotnetExportPhrase"; + string? curveName = key.GetCurveName(out string? oidValue); + + if (string.IsNullOrEmpty(curveName)) + { + if (includePrivateParameters) + { + ecparams = ExportPrivateExplicitParameters(key); + } + else + { + byte[] fullKeyBlob = ExportFullKeyBlob(key, includePrivateParameters: false); + ECCng.ExportPrimeCurveParameters(ref ecparams, fullKeyBlob, includePrivateParameters: false); + } + } + else + { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(key); + + if (includePrivateParameters && encryptedOnlyExport) + { + byte[] exported = key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1); + EccKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out ecparams); + } + else + { + byte[] keyBlob = ExportKeyBlob(key, includePrivateParameters); + ECCng.ExportNamedCurveParameters(ref ecparams, keyBlob, includePrivateParameters); + ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName)); + } + } + + return ecparams; + } + + private static ECParameters ExportPrivateExplicitParameters(CngKey key) + { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(key); + + ECParameters ecparams = default; + + if (encryptedOnlyExport) + { + // We can't ask CNG for the explicit parameters when performing a PKCS#8 export. Instead, + // we ask CNG for the explicit parameters for the public part only, since the parameters are public. + // Then we ask CNG by encrypted PKCS#8 for the private parameters (D) and combine the explicit public + // key along with the private key. + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] publicKeyBlob = ExportFullKeyBlob(key, includePrivateParameters: false); + ExportPrimeCurveParameters(ref ecparams, publicKeyBlob, includePrivateParameters: false); + + byte[] exported = key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1); + EccKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out ECParameters localParameters); + + Debug.Assert(ecparams.Q.X.AsSpan().SequenceEqual(localParameters.Q.X)); + Debug.Assert(ecparams.Q.Y.AsSpan().SequenceEqual(localParameters.Q.Y)); + ecparams.D = localParameters.D; + } + else + { + byte[] blob = ExportFullKeyBlob(key, includePrivateParameters: true); + ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters: true); + } + + return ecparams; + } + private static unsafe void FixupGenericBlob(byte[] blob) { if (blob.Length > sizeof(BCRYPT_ECCKEY_BLOB)) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDiffieHellmanCng.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDiffieHellmanCng.cs index ec993ba9f7bec1..4a0d436cb45a21 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDiffieHellmanCng.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDiffieHellmanCng.cs @@ -206,6 +206,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response) public override bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + EccKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out ECParameters ecParameters); + return EccKeyFormatHelper.WritePkcs8PrivateKey(ecParameters).TryEncode(destination, out bytesWritten); + } + return Key.TryExportKeyBlob( Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, destination, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsaCng.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsaCng.cs index a60e319c08b87e..a91e4208b933a1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsaCng.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsaCng.cs @@ -151,6 +151,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response) public override bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + EccKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out ECParameters ecParameters); + return EccKeyFormatHelper.WritePkcs8PrivateKey(ecParameters).TryEncode(destination, out bytesWritten); + } + return Key.TryExportKeyBlob( Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, destination, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.ImportExport.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.ImportExport.cs index dcc2f9742990c8..f06cac0c8abd65 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.ImportExport.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.ImportExport.cs @@ -52,6 +52,20 @@ private byte[] ExportKeyBlob(bool includePrivateParameters) public override bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key); + + if (encryptedOnlyExport) + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1); + RSAKeyFormatHelper.ReadEncryptedPkcs8( + exported, + TemporaryExportPassword, + out _, + out RSAParameters rsaParameters); + return RSAKeyFormatHelper.WritePkcs8PrivateKey(rsaParameters).TryEncode(destination, out bytesWritten); + } + return Key.TryExportKeyBlob( Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB, destination, diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExportTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExportTests.cs index 1ddfb8418774b7..24969126cf7c9a 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExportTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ExportTests.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using System.Security.Cryptography.Dsa.Tests; +using System.Security.Cryptography.EcDsa.Tests; +using System.Security.Cryptography.Pkcs; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests @@ -348,5 +351,258 @@ public static void ExportCertificatePem() Assert.Equal(TestData.CertRfc7468Wrapped, pem); } } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void RSA_Export_DefaultKeyStorePermitsUnencryptedExports_ExportParameters() + { + (byte[] pkcs12, RSA rsa) = CreateSimplePkcs12(); + + using (rsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using RSA key = cert.GetRSAPrivateKey(); + RSAParameters expected = rsa.ExportParameters(true); + RSAParameters actual = key.ExportParameters(true); + + Assert.Equal(expected.Modulus, actual.Modulus); + Assert.Equal(expected.D, actual.D); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void RSA_Export_DefaultKeyStorePermitsUnencryptedExports_Pkcs8PrivateKey() + { + (byte[] pkcs12, RSA rsa) = CreateSimplePkcs12(); + + using (rsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using RSA key = cert.GetRSAPrivateKey(); + byte[] exported = key.ExportPkcs8PrivateKey(); + + using RSA imported = RSA.Create(); + imported.ImportPkcs8PrivateKey(exported, out _); + RSAParameters actual = imported.ExportParameters(true); + RSAParameters expected = rsa.ExportParameters(true); + + Assert.Equal(expected.Modulus, actual.Modulus); + Assert.Equal(expected.D, actual.D); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void ECDsa_Export_DefaultKeyStorePermitsUnencryptedExports_Pkcs8PrivateKey() + { + (byte[] pkcs12, ECDsa ecdsa) = CreateSimplePkcs12(); + + using (ecdsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using ECDsa key = cert.GetECDsaPrivateKey(); + byte[] exported = key.ExportPkcs8PrivateKey(); + + using ECDsa imported = ECDsa.Create(); + imported.ImportPkcs8PrivateKey(exported, out _); + ECParameters actual = imported.ExportParameters(true); + ECParameters expected = ecdsa.ExportParameters(true); + + Assert.Equal(expected.D, actual.D); + Assert.Equal(expected.Q.X, actual.Q.X); + Assert.Equal(expected.Q.Y, actual.Q.Y); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void ECDsa_Export_DefaultKeyStorePermitsUnencryptedExports_ExportParameters(bool explicitParameters) + { + if (explicitParameters && !ECDsaFactory.ExplicitCurvesSupported) + { + return; + } + + (byte[] pkcs12, ECDsa ecdsa) = CreateSimplePkcs12(); + + using (ecdsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using ECDsa key = cert.GetECDsaPrivateKey(); + + ECParameters actual = explicitParameters ? key.ExportExplicitParameters(true) : key.ExportParameters(true); + ECParameters expected = explicitParameters ? ecdsa.ExportExplicitParameters(true) : ecdsa.ExportParameters(true); + + Assert.Equal(expected.D, actual.D); + Assert.Equal(expected.Q.X, actual.Q.X); + Assert.Equal(expected.Q.Y, actual.Q.Y); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void ECDH_Export_DefaultKeyStorePermitsUnencryptedExports_ExportParameters(bool explicitParameters) + { + if (explicitParameters && !ECDsaFactory.ExplicitCurvesSupported) + { + return; + } + + (byte[] pkcs12, ECDiffieHellman ecdh) = CreateSimplePkcs12(); + + using (ecdh) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using ECDiffieHellman key = cert.GetECDiffieHellmanPrivateKey(); + + ECParameters actual = explicitParameters ? key.ExportExplicitParameters(true) : key.ExportParameters(true); + ECParameters expected = explicitParameters ? ecdh.ExportExplicitParameters(true) : ecdh.ExportParameters(true); + + Assert.Equal(expected.D, actual.D); + Assert.Equal(expected.Q.X, actual.Q.X); + Assert.Equal(expected.Q.Y, actual.Q.Y); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void ECDH_Export_DefaultKeyStorePermitsUnencryptedExports_Pkcs8PrivateKey() + { + (byte[] pkcs12, ECDiffieHellman ecdh) = CreateSimplePkcs12(); + + using (ecdh) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using ECDiffieHellman key = cert.GetECDiffieHellmanPrivateKey(); + byte[] exported = key.ExportPkcs8PrivateKey(); + + using ECDiffieHellman imported = ECDiffieHellman.Create(); + imported.ImportPkcs8PrivateKey(exported, out _); + ECParameters actual = imported.ExportParameters(true); + ECParameters expected = ecdh.ExportParameters(true); + + Assert.Equal(expected.D, actual.D); + Assert.Equal(expected.Q.X, actual.Q.X); + Assert.Equal(expected.Q.Y, actual.Q.Y); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void DSA_Export_DefaultKeyStorePermitsUnencryptedExports_ExportParameters() + { + (byte[] pkcs12, DSA dsa) = CreateSimplePkcs12(); + + using (dsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using DSA key = cert.GetDSAPrivateKey(); + DSAParameters expected = dsa.ExportParameters(true); + DSAParameters actual = key.ExportParameters(true); + + Assert.Equal(expected.X, actual.X); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public static void DSA_Export_DefaultKeyStorePermitsUnencryptedExports_Pkcs8PrivateKey() + { + (byte[] pkcs12, DSA dsa) = CreateSimplePkcs12(); + + using (dsa) + { + using X509Certificate2 cert = new X509Certificate2(pkcs12, "", X509KeyStorageFlags.Exportable); + using DSA key = cert.GetDSAPrivateKey(); + byte[] exported = key.ExportPkcs8PrivateKey(); + + using DSA imported = DSA.Create(); + imported.ImportPkcs8PrivateKey(exported, out _); + DSAParameters actual = imported.ExportParameters(true); + DSAParameters expected = dsa.ExportParameters(true); + + Assert.Equal(expected.X, actual.X); + } + } + + private static (byte[] Pkcs12, TKey key) CreateSimplePkcs12() where TKey : AsymmetricAlgorithm + { + using (ECDsa ca = ECDsa.Create(ECCurve.NamedCurves.nistP256)) + { + CertificateRequest issuerRequest = new CertificateRequest( + new X500DistinguishedName("CN=root"), + ca, + HashAlgorithmName.SHA256); + + issuerRequest.CertificateExtensions.Add(X509BasicConstraintsExtension.CreateForCertificateAuthority()); + + DateTimeOffset notBefore = DateTimeOffset.UtcNow; + DateTimeOffset notAfter = notBefore.AddDays(30); + byte[] serial = [1, 2, 3, 4, 5, 6, 7, 8]; + X509SignatureGenerator generator = X509SignatureGenerator.CreateForECDsa(ca); + + using (X509Certificate2 issuer = issuerRequest.CreateSelfSigned(notBefore, notAfter)) + { + CertificateRequest req; + TKey key; + + if (typeof(TKey) == typeof(ECDsa)) + { + ECDsa ecKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + req = new("CN=simple", ecKey, HashAlgorithmName.SHA256); + key = (TKey)(object)ecKey; + } + else if (typeof(TKey) == typeof(RSA)) + { + RSA rsaKey = RSA.Create(2048); + req = new("CN=simple", rsaKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + key = (TKey)(object)rsaKey; + } + else if (typeof(TKey) == typeof(ECDiffieHellman)) + { + ECDiffieHellman ecKey = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256); + req = new CertificateRequest(new X500DistinguishedName("CN=simple"), new PublicKey(ecKey), HashAlgorithmName.SHA256); + req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyAgreement, true)); + key = (TKey)(object)ecKey; + } + else if (typeof(TKey) == typeof(DSA)) + { + DSA dsaKey = DSA.Create(); + dsaKey.ImportParameters(DSATestData.GetDSA1024Params()); + req = new CertificateRequest(new X500DistinguishedName("CN=simple"), new PublicKey(dsaKey), HashAlgorithmName.SHA256); + req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); + key = (TKey)(object)dsaKey; + } + else + { + throw new InvalidOperationException(); + } + + req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); + + using X509Certificate2 cert = req.Create(issuer.SubjectName, generator, notBefore, notAfter, serial); + Pkcs9LocalKeyId keyId = new([1]); + PbeParameters pbe = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 1); + + Pkcs12Builder builder = new(); + Pkcs12SafeContents certContainer = new(); + Pkcs12SafeContents keyContainer = new(); + Pkcs12SafeBag certBag = certContainer.AddCertificate(cert); + Pkcs12SafeBag keyBag = keyContainer.AddShroudedKey(key, "", pbe); + certBag.Attributes.Add(keyId); + keyBag.Attributes.Add(keyId); + builder.AddSafeContentsEncrypted(certContainer, "", pbe); + builder.AddSafeContentsUnencrypted(keyContainer); + + builder.SealWithMac("", pbe.HashAlgorithm, pbe.IterationCount); + return (builder.Encode(), key); + } + } + } } }