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 31a2418f5e5ce..ca2acd8a08792 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 2dc7e613b72c8..60717ea02bdf5 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 0eb393a4f2516..cf3bd9108dbac 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 2d0129412d9fb..45f9cdda7cc75 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 fd264c44db873..93854e191e66e 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 7324a310746ba..27d059dfa11e2 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 7ac9a0f118dfa..9ccc7e69fb9cc 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 14d7b17565e95..96aa067ac3897 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 ec993ba9f7bec..4a0d436cb45a2 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 a60e319c08b87..a91e4208b933a 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 dcc2f9742990c..f06cac0c8abd6 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 1ddfb8418774b..24969126cf7c9 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); + } + } + } } }