Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ public abstract partial class ECAlgorithm : System.Security.Cryptography.Asymmet
{
protected ECAlgorithm() { }
public virtual byte[] ExportECPrivateKey() { throw null; }
public string ExportECPrivateKeyPem() { throw null; }
public virtual System.Security.Cryptography.ECParameters ExportExplicitParameters(bool includePrivateParameters) { throw null; }
public virtual System.Security.Cryptography.ECParameters ExportParameters(bool includePrivateParameters) { throw null; }
public virtual void GenerateKey(System.Security.Cryptography.ECCurve curve) { }
Expand All @@ -410,6 +411,7 @@ public virtual void ImportParameters(System.Security.Cryptography.ECParameters p
public override void ImportPkcs8PrivateKey(System.ReadOnlySpan<byte> source, out int bytesRead) { throw null; }
public override void ImportSubjectPublicKeyInfo(System.ReadOnlySpan<byte> source, out int bytesRead) { throw null; }
public virtual bool TryExportECPrivateKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
public bool TryExportECPrivateKeyPem(System.Span<char> destination, out int charsWritten) { throw null; }
public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan<byte> passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span<byte> destination, out int bytesWritten) { throw null; }
public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan<char> password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span<byte> destination, out int bytesWritten) { throw null; }
public override bool TryExportPkcs8PrivateKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
Expand Down Expand Up @@ -1025,7 +1027,9 @@ protected RSA() { }
public virtual byte[] EncryptValue(byte[] rgb) { throw null; }
public abstract System.Security.Cryptography.RSAParameters ExportParameters(bool includePrivateParameters);
public virtual byte[] ExportRSAPrivateKey() { throw null; }
public string ExportRSAPrivateKeyPem() { throw null; }
public virtual byte[] ExportRSAPublicKey() { throw null; }
public string ExportRSAPublicKeyPem() { throw null; }
public override void FromXmlString(string xmlString) { }
protected virtual byte[] HashData(byte[] data, int offset, int count, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { throw null; }
protected virtual byte[] HashData(System.IO.Stream data, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { throw null; }
Expand All @@ -1050,7 +1054,9 @@ public override void ImportFromPem(System.ReadOnlySpan<char> input) { }
public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan<char> password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span<byte> destination, out int bytesWritten) { throw null; }
public override bool TryExportPkcs8PrivateKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
public virtual bool TryExportRSAPrivateKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
public bool TryExportRSAPrivateKeyPem(System.Span<char> destination, out int charsWritten) { throw null; }
public virtual bool TryExportRSAPublicKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
public bool TryExportRSAPublicKeyPem(System.Span<char> destination, out int charsWritten) { throw null; }
public override bool TryExportSubjectPublicKeyInfo(System.Span<byte> destination, out int bytesWritten) { throw null; }
protected virtual bool TryHashData(System.ReadOnlySpan<byte> data, System.Span<byte> destination, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, out int bytesWritten) { throw null; }
public virtual bool TrySignData(System.ReadOnlySpan<byte> data, System.Span<byte> destination, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding, out int bytesWritten) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -862,5 +862,87 @@ public override void ImportFromEncryptedPem(ReadOnlySpan<char> input, ReadOnlySp
// override remains for compatibility.
base.ImportFromEncryptedPem(input, passwordBytes);
}

/// <summary>
/// Exports the current key in the ECPrivateKey format, PEM encoded.
/// </summary>
/// <returns>A string containing the PEM-encoded ECPrivateKey.</returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded ECPrivateKey will begin with <c>-----BEGIN EC PRIVATE KEY-----</c>
/// and end with <c>-----END EC PRIVATE KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public unsafe string ExportECPrivateKeyPem()
{
byte[] exported = ExportECPrivateKey();

// Fixed to prevent GC moves.
fixed (byte* pExported = exported)
Copy link
Member

@stephentoub stephentoub Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the key was already exported to the array above, this is too late to guarantee no copies have been made.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true but it's a pattern that occurs quite frequently in other places as a "best effort". Two examples:

But there are many more.

{
try
{
return PemKeyHelpers.CreatePemFromData(PemLabels.EcPrivateKey, exported);
}
finally
{
CryptographicOperations.ZeroMemory(exported);
}
}
}

/// <summary>
/// Attempts to export the current key in the PEM-encoded
/// ECPrivateKey format into a provided buffer.
/// </summary>
/// <param name="destination">
/// The character span to receive the PEM-encoded ECPrivateKey data.
/// </param>
/// <param name="charsWritten">
/// When this method returns, contains a value that indicates the number
/// of characters written to <paramref name="destination" />. This
/// parameter is treated as uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if <paramref name="destination" /> is big enough
/// to receive the output; otherwise, <see langword="false" />.
/// </returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded ECPrivateKey will begin with
/// <c>-----BEGIN EC PRIVATE KEY-----</c> and end with
/// <c>-----END EC PRIVATE KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public bool TryExportECPrivateKeyPem(Span<char> destination, out int charsWritten)
{
static bool Export(ECAlgorithm alg, Span<byte> destination, out int bytesWritten)
{
return alg.TryExportECPrivateKey(destination, out bytesWritten);
}

return PemKeyHelpers.TryExportToPem(
this,
PemLabels.EcPrivateKey,
Export,
destination,
out charsWritten);
Comment on lines +935 to +945
Copy link
Member

@stephentoub stephentoub Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Export need to be a local function? Why not just do it inline as a static lambda below and enable the compiler to cache the delegate?

Copy link
Member Author

@vcsjones vcsjones Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looked cleaner this way (to me) and it's not a particularly performance sensitive function. As a lambda it would look like:

            return PemKeyHelpers.TryExportToPem(
                this,
                PemLabels.EcPrivateKey,
                static (ECAlgorithm alg, Span<byte> destination, out int bytesWritten) =>
                    alg.TryExportECPrivateKey(destination, out bytesWritten),
                destination,
                out charsWritten);

If we want to avoid the delegate allocation, I can fix this up and in a few other places that have already been done this way.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,159 @@ public override void ImportFromEncryptedPem(ReadOnlySpan<char> input, ReadOnlySp
base.ImportFromEncryptedPem(input, passwordBytes);
}

/// <summary>
/// Exports the current key in the PKCS#1 RSAPrivateKey format, PEM encoded.
/// </summary>
/// <returns>A string containing the PEM-encoded PKCS#1 RSAPrivateKey.</returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded PKCS#1 RSAPrivateKey will begin with <c>-----BEGIN RSA PRIVATE KEY-----</c>
/// and end with <c>-----END RSA PRIVATE KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public unsafe string ExportRSAPrivateKeyPem()
{
byte[] exported = ExportRSAPrivateKey();

// Fixed to prevent GC moves.
fixed (byte* pExported = exported)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as earlier. This will prevent exported from being moved from this point on, but it could have already been moved any number of times before this point.

{
try
{
return PemKeyHelpers.CreatePemFromData(PemLabels.RsaPrivateKey, exported);
}
finally
{
CryptographicOperations.ZeroMemory(exported);
}
}
}

/// <summary>
/// Exports the public-key portion of the current key in the PKCS#1
/// RSAPublicKey format, PEM encoded.
/// </summary>
/// <returns>A string containing the PEM-encoded PKCS#1 RSAPublicKey.</returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded PKCS#1 RSAPublicKey will begin with <c>-----BEGIN RSA PUBLIC KEY-----</c>
/// and end with <c>-----END RSA PUBLIC KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public string ExportRSAPublicKeyPem()
{
byte[] exported = ExportRSAPublicKey();
return PemKeyHelpers.CreatePemFromData(PemLabels.RsaPublicKey, exported);
}

/// <summary>
/// Attempts to export the current key in the PEM-encoded PKCS#1
/// RSAPrivateKey format into a provided buffer.
/// </summary>
/// <param name="destination">
/// The character span to receive the PEM-encoded PKCS#1 RSAPrivateKey data.
/// </param>
/// <param name="charsWritten">
/// When this method returns, contains a value that indicates the number
/// of characters written to <paramref name="destination" />. This
/// parameter is treated as uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if <paramref name="destination" /> is big enough
/// to receive the output; otherwise, <see langword="false" />.
/// </returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded PKCS#1 RSAPrivateKey will begin with
/// <c>-----BEGIN RSA PRIVATE KEY-----</c> and end with
/// <c>-----END RSA PRIVATE KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public bool TryExportRSAPrivateKeyPem(Span<char> destination, out int charsWritten)
{
static bool Export(RSA alg, Span<byte> destination, out int bytesWritten)
{
return alg.TryExportRSAPrivateKey(destination, out bytesWritten);
}

return PemKeyHelpers.TryExportToPem(
this,
PemLabels.RsaPrivateKey,
Export,
destination,
out charsWritten);
}

/// <summary>
/// Attempts to export the current key in the PEM-encoded PKCS#1
/// RSAPublicKey format into a provided buffer.
/// </summary>
/// <param name="destination">
/// The character span to receive the PEM-encoded PKCS#1 RSAPublicKey data.
/// </param>
/// <param name="charsWritten">
/// When this method returns, contains a value that indicates the number
/// of characters written to <paramref name="destination" />. This
/// parameter is treated as uninitialized.
/// </param>
/// <returns>
/// <see langword="true" /> if <paramref name="destination" /> is big enough
/// to receive the output; otherwise, <see langword="false" />.
/// </returns>
/// <exception cref="CryptographicException">
/// The key could not be exported.
/// </exception>
/// <remarks>
/// <p>
/// A PEM-encoded PKCS#1 RSAPublicKey will begin with
/// <c>-----BEGIN RSA PUBLIC KEY-----</c> and end with
/// <c>-----END RSA PUBLIC KEY-----</c>, with the base64 encoded DER
/// contents of the key between the PEM boundaries.
/// </p>
/// <p>
/// The PEM is encoded according to the IETF RFC 7468 &quot;strict&quot;
/// encoding rules.
/// </p>
/// </remarks>
public bool TryExportRSAPublicKeyPem(Span<char> destination, out int charsWritten)
{
static bool Export(RSA alg, Span<byte> destination, out int bytesWritten)
{
return alg.TryExportRSAPublicKey(destination, out bytesWritten);
}

return PemKeyHelpers.TryExportToPem(
this,
PemLabels.RsaPublicKey,
Export,
destination,
out charsWritten);
}

private static void ClearPrivateParameters(in RSAParameters rsaParameters)
{
CryptographicOperations.ZeroMemory(rsaParameters.D);
Expand Down
102 changes: 102 additions & 0 deletions src/libraries/System.Security.Cryptography/tests/ECPemExportTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using Xunit;

namespace System.Security.Cryptography.Tests
{
[SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")]
public static class ECPemExportTests
{
[Fact]
public static void ExportPem_ExportECPrivateKey()
{
string expectedPem =
"-----BEGIN EC PRIVATE KEY-----\n" +
"cGVubnk=\n" +
"-----END EC PRIVATE KEY-----";

static byte[] ExportECPrivateKey()
{
return new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 };
}

using (DelegateECAlgorithm ec = new DelegateECAlgorithm())
{
ec.ExportECPrivateKeyDelegate = ExportECPrivateKey;
Assert.Equal(expectedPem, ec.ExportECPrivateKeyPem());
}
}

[Fact]
public static void ExportPem_TryExportECPrivateKey()
{
string expectedPem =
"-----BEGIN EC PRIVATE KEY-----\n" +
"cGVubnk=\n" +
"-----END EC PRIVATE KEY-----";

static bool TryExportECPrivateKey(Span<byte> destination, out int bytesWritten)
{
ReadOnlySpan<byte> result = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 };
bytesWritten = result.Length;
result.CopyTo(destination);
return true;
}

using (DelegateECAlgorithm ec = new DelegateECAlgorithm())
{
ec.TryExportECPrivateKeyDelegate = TryExportECPrivateKey;

int written;
bool result;
char[] buffer;

// buffer not enough
buffer = new char[expectedPem.Length - 1];
result = ec.TryExportECPrivateKeyPem(buffer, out written);
Assert.False(result, nameof(ec.TryExportECPrivateKeyPem));
Assert.Equal(0, written);

// buffer just enough
buffer = new char[expectedPem.Length];
result = ec.TryExportECPrivateKeyPem(buffer, out written);
Assert.True(result, nameof(ec.TryExportECPrivateKeyPem));
Assert.Equal(expectedPem.Length, written);
Assert.Equal(expectedPem, new string(buffer));

// buffer more than enough
buffer = new char[expectedPem.Length + 20];
buffer.AsSpan().Fill('!');
Span<char> bufferSpan = buffer.AsSpan(10);
result = ec.TryExportECPrivateKeyPem(bufferSpan, out written);
Assert.True(result, nameof(ec.TryExportECPrivateKeyPem));
Assert.Equal(expectedPem.Length, written);
Assert.Equal(expectedPem, new string(bufferSpan.Slice(0, written)));

// Ensure padding has not been touched.
AssertExtensions.FilledWith('!', buffer[0..10]);
AssertExtensions.FilledWith('!', buffer[^10..]);
}
}

private class DelegateECAlgorithm : ECAlgorithm
{
public delegate bool TryExportFunc(Span<byte> destination, out int bytesWritten);

public Func<byte[]> ExportECPrivateKeyDelegate = null;
public TryExportFunc TryExportECPrivateKeyDelegate = null;


public DelegateECAlgorithm()
{
}

public override byte[] ExportECPrivateKey() => ExportECPrivateKeyDelegate();

public override bool TryExportECPrivateKey(Span<byte> destination, out int bytesWritten) =>
TryExportECPrivateKeyDelegate(destination, out bytesWritten);
}
}
}
Loading