Skip to content

Commit 2b46f3a

Browse files
[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 <kevin@vcsjones.com>
1 parent f9b6a51 commit 2b46f3a

File tree

12 files changed

+471
-66
lines changed

12 files changed

+471
-66
lines changed

src/libraries/Common/src/System/Security/Cryptography/DSACng.ImportExport.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,20 @@ private static unsafe void GenerateV2DsaBlob(out byte[] blob, DSAParameters para
310310

311311
public override DSAParameters ExportParameters(bool includePrivateParameters)
312312
{
313+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
314+
315+
if (includePrivateParameters && encryptedOnlyExport)
316+
{
317+
const string TemporaryExportPassword = "DotnetExportPhrase";
318+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
319+
DSAKeyFormatHelper.ReadEncryptedPkcs8(
320+
exported,
321+
TemporaryExportPassword,
322+
out _,
323+
out DSAParameters dsaParameters);
324+
return dsaParameters;
325+
}
326+
313327
byte[] dsaBlob = ExportKeyBlob(includePrivateParameters);
314328

315329
KeyBlobMagicNumber magic = (KeyBlobMagicNumber)BitConverter.ToInt32(dsaBlob, 0);
@@ -423,6 +437,5 @@ private static void CheckMagicValueOfKey(KeyBlobMagicNumber magic, bool includeP
423437
throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey);
424438
}
425439
}
426-
427440
}
428441
}

src/libraries/Common/src/System/Security/Cryptography/ECDiffieHellmanCng.ImportExport.cs

+2-40
Original file line numberDiff line numberDiff line change
@@ -66,50 +66,12 @@ public override void ImportParameters(ECParameters parameters)
6666

6767
public override ECParameters ExportExplicitParameters(bool includePrivateParameters)
6868
{
69-
byte[] blob = ExportFullKeyBlob(includePrivateParameters);
70-
71-
try
72-
{
73-
ECParameters ecparams = default;
74-
ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters);
75-
return ecparams;
76-
}
77-
finally
78-
{
79-
Array.Clear(blob);
80-
}
69+
return ECCng.ExportExplicitParameters(Key, includePrivateParameters);
8170
}
8271

8372
public override ECParameters ExportParameters(bool includePrivateParameters)
8473
{
85-
ECParameters ecparams = default;
86-
87-
string? curveName = GetCurveName(out string? oidValue);
88-
byte[]? blob = null;
89-
90-
try
91-
{
92-
if (string.IsNullOrEmpty(curveName))
93-
{
94-
blob = ExportFullKeyBlob(includePrivateParameters);
95-
ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters);
96-
}
97-
else
98-
{
99-
blob = ExportKeyBlob(includePrivateParameters);
100-
ECCng.ExportNamedCurveParameters(ref ecparams, blob, includePrivateParameters);
101-
ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName));
102-
}
103-
104-
return ecparams;
105-
}
106-
finally
107-
{
108-
if (blob != null)
109-
{
110-
Array.Clear(blob);
111-
}
112-
}
74+
return ECCng.ExportParameters(Key, includePrivateParameters);
11375
}
11476

11577
public override void ImportPkcs8PrivateKey(ReadOnlySpan<byte> source, out int bytesRead)

src/libraries/Common/src/System/Security/Cryptography/ECDsaCng.ImportExport.cs

+3-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using Internal.NativeCrypto;
56

67
namespace System.Security.Cryptography
@@ -87,10 +88,7 @@ public override void ImportParameters(ECParameters parameters)
8788
/// <returns>The key and explicit curve parameters used by the ECC object.</returns>
8889
public override ECParameters ExportExplicitParameters(bool includePrivateParameters)
8990
{
90-
byte[] blob = ExportFullKeyBlob(includePrivateParameters);
91-
ECParameters ecparams = default;
92-
ECCng.ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters);
93-
return ecparams;
91+
return ECCng.ExportExplicitParameters(Key, includePrivateParameters);
9492
}
9593

9694
/// <summary>
@@ -103,23 +101,7 @@ public override ECParameters ExportExplicitParameters(bool includePrivateParamet
103101
/// <returns>The key and named curve parameters used by the ECC object.</returns>
104102
public override ECParameters ExportParameters(bool includePrivateParameters)
105103
{
106-
ECParameters ecparams = default;
107-
108-
string? curveName = GetCurveName(out string? oidValue);
109-
110-
if (string.IsNullOrEmpty(curveName))
111-
{
112-
byte[] fullKeyBlob = ExportFullKeyBlob(includePrivateParameters);
113-
ECCng.ExportPrimeCurveParameters(ref ecparams, fullKeyBlob, includePrivateParameters);
114-
}
115-
else
116-
{
117-
byte[] keyBlob = ExportKeyBlob(includePrivateParameters);
118-
ECCng.ExportNamedCurveParameters(ref ecparams, keyBlob, includePrivateParameters);
119-
ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName));
120-
}
121-
122-
return ecparams;
104+
return ECCng.ExportParameters(Key, includePrivateParameters);
123105
}
124106

125107
public override void ImportPkcs8PrivateKey(ReadOnlySpan<byte> source, out int bytesRead)

src/libraries/Common/src/System/Security/Cryptography/RSACng.ImportExport.cs

+14
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ public override bool TryExportEncryptedPkcs8PrivateKey(
180180
/// </summary>
181181
public override RSAParameters ExportParameters(bool includePrivateParameters)
182182
{
183+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
184+
185+
if (includePrivateParameters && encryptedOnlyExport)
186+
{
187+
const string TemporaryExportPassword = "DotnetExportPhrase";
188+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
189+
RSAKeyFormatHelper.ReadEncryptedPkcs8(
190+
exported,
191+
TemporaryExportPassword,
192+
out _,
193+
out RSAParameters rsaParameters);
194+
return rsaParameters;
195+
}
196+
183197
byte[] rsaBlob = ExportKeyBlob(includePrivateParameters);
184198
RSAParameters rsaParams = default;
185199
rsaParams.FromBCryptBlob(rsaBlob, includePrivateParameters);

src/libraries/System.Security.Cryptography.Cng/tests/CngPkcs8Tests.cs

+25-4
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,32 @@ public void NoPlaintextExportFailsPkcs8()
1717
{
1818
SetExportPolicy(cngKey, CngExportPolicies.AllowExport);
1919

20-
Assert.ThrowsAny<CryptographicException>(
21-
() => key.ExportPkcs8PrivateKey());
20+
byte[] exported = key.ExportPkcs8PrivateKey();
21+
22+
using (T imported = CreateKey(out _))
23+
{
24+
imported.ImportPkcs8PrivateKey(exported, out int importRead);
25+
Assert.Equal(exported.Length, importRead);
26+
VerifyMatch(key, imported);
27+
}
28+
29+
byte[] tryExported = new byte[exported.Length];
30+
31+
int written;
32+
33+
while (!key.TryExportPkcs8PrivateKey(tryExported, out written))
34+
{
35+
Array.Resize(ref tryExported, checked(tryExported.Length * 2));
36+
}
37+
38+
using (T imported = CreateKey(out _))
39+
{
40+
imported.ImportPkcs8PrivateKey(tryExported.AsSpan(0, written), out int tryImportRead);
41+
Assert.Equal(written, tryImportRead);
42+
VerifyMatch(key, imported);
43+
}
44+
2245

23-
Assert.ThrowsAny<CryptographicException>(
24-
() => key.TryExportPkcs8PrivateKey(Span<byte>.Empty, out _));
2546
}
2647
}
2748

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngPkcs8.cs

+6
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,11 @@ private static Pkcs8Response ImportPkcs8(
4343
Key = key,
4444
};
4545
}
46+
47+
internal static bool AllowsOnlyEncryptedExport(CngKey key)
48+
{
49+
const CngExportPolicies Exportable = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport;
50+
return (key.ExportPolicy & Exportable) == CngExportPolicies.AllowExport;
51+
}
4652
}
4753
}

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/DSACng.ImportExport.cs

+14
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)
5050

5151
public override bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
5252
{
53+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
54+
55+
if (encryptedOnlyExport)
56+
{
57+
const string TemporaryExportPassword = "DotnetExportPhrase";
58+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
59+
DSAKeyFormatHelper.ReadEncryptedPkcs8(
60+
exported,
61+
TemporaryExportPassword,
62+
out _,
63+
out DSAParameters dsaParameters);
64+
return DSAKeyFormatHelper.WritePkcs8(dsaParameters).TryEncode(destination, out bytesWritten);
65+
}
66+
5367
return Key.TryExportKeyBlob(
5468
Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
5569
destination,

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCng.ImportExport.cs

+95
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using static Interop.BCrypt;
56

67
namespace System.Security.Cryptography
@@ -77,6 +78,100 @@ internal static byte[] ExportKeyBlob(
7778
return blob;
7879
}
7980

81+
internal static ECParameters ExportExplicitParameters(CngKey key, bool includePrivateParameters)
82+
{
83+
if (includePrivateParameters)
84+
{
85+
return ExportPrivateExplicitParameters(key);
86+
}
87+
else
88+
{
89+
byte[] blob = ExportFullKeyBlob(key, includePrivateParameters: false);
90+
ECParameters ecparams = default;
91+
ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters: false);
92+
return ecparams;
93+
}
94+
}
95+
96+
internal static ECParameters ExportParameters(CngKey key, bool includePrivateParameters)
97+
{
98+
ECParameters ecparams = default;
99+
100+
const string TemporaryExportPassword = "DotnetExportPhrase";
101+
string? curveName = key.GetCurveName(out string? oidValue);
102+
103+
if (string.IsNullOrEmpty(curveName))
104+
{
105+
if (includePrivateParameters)
106+
{
107+
ecparams = ExportPrivateExplicitParameters(key);
108+
}
109+
else
110+
{
111+
byte[] fullKeyBlob = ExportFullKeyBlob(key, includePrivateParameters: false);
112+
ECCng.ExportPrimeCurveParameters(ref ecparams, fullKeyBlob, includePrivateParameters: false);
113+
}
114+
}
115+
else
116+
{
117+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(key);
118+
119+
if (includePrivateParameters && encryptedOnlyExport)
120+
{
121+
byte[] exported = key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1);
122+
EccKeyFormatHelper.ReadEncryptedPkcs8(
123+
exported,
124+
TemporaryExportPassword,
125+
out _,
126+
out ecparams);
127+
}
128+
else
129+
{
130+
byte[] keyBlob = ExportKeyBlob(key, includePrivateParameters);
131+
ECCng.ExportNamedCurveParameters(ref ecparams, keyBlob, includePrivateParameters);
132+
ecparams.Curve = ECCurve.CreateFromOid(new Oid(oidValue, curveName));
133+
}
134+
}
135+
136+
return ecparams;
137+
}
138+
139+
private static ECParameters ExportPrivateExplicitParameters(CngKey key)
140+
{
141+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(key);
142+
143+
ECParameters ecparams = default;
144+
145+
if (encryptedOnlyExport)
146+
{
147+
// We can't ask CNG for the explicit parameters when performing a PKCS#8 export. Instead,
148+
// we ask CNG for the explicit parameters for the public part only, since the parameters are public.
149+
// Then we ask CNG by encrypted PKCS#8 for the private parameters (D) and combine the explicit public
150+
// key along with the private key.
151+
const string TemporaryExportPassword = "DotnetExportPhrase";
152+
byte[] publicKeyBlob = ExportFullKeyBlob(key, includePrivateParameters: false);
153+
ExportPrimeCurveParameters(ref ecparams, publicKeyBlob, includePrivateParameters: false);
154+
155+
byte[] exported = key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1);
156+
EccKeyFormatHelper.ReadEncryptedPkcs8(
157+
exported,
158+
TemporaryExportPassword,
159+
out _,
160+
out ECParameters localParameters);
161+
162+
Debug.Assert(ecparams.Q.X.AsSpan().SequenceEqual(localParameters.Q.X));
163+
Debug.Assert(ecparams.Q.Y.AsSpan().SequenceEqual(localParameters.Q.Y));
164+
ecparams.D = localParameters.D;
165+
}
166+
else
167+
{
168+
byte[] blob = ExportFullKeyBlob(key, includePrivateParameters: true);
169+
ExportPrimeCurveParameters(ref ecparams, blob, includePrivateParameters: true);
170+
}
171+
172+
return ecparams;
173+
}
174+
80175
private static unsafe void FixupGenericBlob(byte[] blob)
81176
{
82177
if (blob.Length > sizeof(BCRYPT_ECCKEY_BLOB))

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDiffieHellmanCng.cs

+14
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)
206206

207207
public override bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
208208
{
209+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
210+
211+
if (encryptedOnlyExport)
212+
{
213+
const string TemporaryExportPassword = "DotnetExportPhrase";
214+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
215+
EccKeyFormatHelper.ReadEncryptedPkcs8(
216+
exported,
217+
TemporaryExportPassword,
218+
out _,
219+
out ECParameters ecParameters);
220+
return EccKeyFormatHelper.WritePkcs8PrivateKey(ecParameters).TryEncode(destination, out bytesWritten);
221+
}
222+
209223
return Key.TryExportKeyBlob(
210224
Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
211225
destination,

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsaCng.cs

+14
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)
151151

152152
public override bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
153153
{
154+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
155+
156+
if (encryptedOnlyExport)
157+
{
158+
const string TemporaryExportPassword = "DotnetExportPhrase";
159+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
160+
EccKeyFormatHelper.ReadEncryptedPkcs8(
161+
exported,
162+
TemporaryExportPassword,
163+
out _,
164+
out ECParameters ecParameters);
165+
return EccKeyFormatHelper.WritePkcs8PrivateKey(ecParameters).TryEncode(destination, out bytesWritten);
166+
}
167+
154168
return Key.TryExportKeyBlob(
155169
Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
156170
destination,

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSACng.ImportExport.cs

+14
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ private byte[] ExportKeyBlob(bool includePrivateParameters)
5252

5353
public override bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
5454
{
55+
bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(Key);
56+
57+
if (encryptedOnlyExport)
58+
{
59+
const string TemporaryExportPassword = "DotnetExportPhrase";
60+
byte[] exported = ExportEncryptedPkcs8(TemporaryExportPassword, 1);
61+
RSAKeyFormatHelper.ReadEncryptedPkcs8(
62+
exported,
63+
TemporaryExportPassword,
64+
out _,
65+
out RSAParameters rsaParameters);
66+
return RSAKeyFormatHelper.WritePkcs8PrivateKey(rsaParameters).TryEncode(destination, out bytesWritten);
67+
}
68+
5569
return Key.TryExportKeyBlob(
5670
Interop.NCrypt.NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
5771
destination,

0 commit comments

Comments
 (0)