-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Implement PEM exports for RSA PKCS#1 and ECPrivateKey #61946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 "strict" | ||
| /// encoding rules. | ||
| /// </p> | ||
| /// </remarks> | ||
| public unsafe string ExportECPrivateKeyPem() | ||
| { | ||
| byte[] exported = ExportECPrivateKey(); | ||
|
|
||
| // Fixed to prevent GC moves. | ||
| fixed (byte* pExported = exported) | ||
| { | ||
| 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 "strict" | ||
| /// 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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 "strict" | ||
| /// encoding rules. | ||
| /// </p> | ||
| /// </remarks> | ||
| public unsafe string ExportRSAPrivateKeyPem() | ||
| { | ||
| byte[] exported = ExportRSAPrivateKey(); | ||
|
|
||
| // Fixed to prevent GC moves. | ||
| fixed (byte* pExported = exported) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 "strict" | ||
| /// 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 "strict" | ||
| /// 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 "strict" | ||
| /// 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); | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
runtime/src/libraries/Common/src/System/Security/Cryptography/CngPkcs8.cs
Line 412 in 6b6310b
runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECDsa.cs
Line 1033 in 899bf97
But there are many more.