Skip to content

Commit

Permalink
Permit unencrypted key exports from CNG (#109119)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vcsjones authored Oct 22, 2024
1 parent 601fad8 commit 9b10a46
Show file tree
Hide file tree
Showing 12 changed files with 471 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -423,6 +437,5 @@ private static void CheckMagicValueOfKey(KeyBlobMagicNumber magic, bool includeP
throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey);
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> source, out int bytesRead)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -87,10 +88,7 @@ public override void ImportParameters(ECParameters parameters)
/// <returns>The key and explicit curve parameters used by the ECC object.</returns>
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);
}

/// <summary>
Expand All @@ -103,23 +101,7 @@ public override ECParameters ExportExplicitParameters(bool includePrivateParamet
/// <returns>The key and named curve parameters used by the ECC object.</returns>
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<byte> source, out int bytesRead)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ public override bool TryExportEncryptedPkcs8PrivateKey(
/// </summary>
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,32 @@ public void NoPlaintextExportFailsPkcs8()
{
SetExportPolicy(cngKey, CngExportPolicies.AllowExport);

Assert.ThrowsAny<CryptographicException>(
() => 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<CryptographicException>(
() => key.TryExportPkcs8PrivateKey(Span<byte>.Empty, out _));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)

public override bool TryExportPkcs8PrivateKey(Span<byte> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)

public override bool TryExportPkcs8PrivateKey(Span<byte> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ private void AcceptImport(CngPkcs8.Pkcs8Response response)

public override bool TryExportPkcs8PrivateKey(Span<byte> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ private byte[] ExportKeyBlob(bool includePrivateParameters)

public override bool TryExportPkcs8PrivateKey(Span<byte> 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,
Expand Down
Loading

0 comments on commit 9b10a46

Please sign in to comment.