diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index db5f1702f1d0a6..649f897e50af5b 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -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) { } @@ -410,6 +411,7 @@ public virtual void ImportParameters(System.Security.Cryptography.ECParameters p public override void ImportPkcs8PrivateKey(System.ReadOnlySpan source, out int bytesRead) { throw null; } public override void ImportSubjectPublicKeyInfo(System.ReadOnlySpan source, out int bytesRead) { throw null; } public virtual bool TryExportECPrivateKey(System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportECPrivateKeyPem(System.Span destination, out int charsWritten) { throw null; } public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public override bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } @@ -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; } @@ -1050,7 +1054,9 @@ public override void ImportFromPem(System.ReadOnlySpan input) { } public override bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public override bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } public virtual bool TryExportRSAPrivateKey(System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportRSAPrivateKeyPem(System.Span destination, out int charsWritten) { throw null; } public virtual bool TryExportRSAPublicKey(System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportRSAPublicKeyPem(System.Span destination, out int charsWritten) { throw null; } public override bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } protected virtual bool TryHashData(System.ReadOnlySpan data, System.Span destination, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, out int bytesWritten) { throw null; } public virtual bool TrySignData(System.ReadOnlySpan data, System.Span destination, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding, out int bytesWritten) { throw null; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECAlgorithm.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECAlgorithm.cs index 3117b81d04f5fd..d5c34e18d9fb01 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECAlgorithm.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECAlgorithm.cs @@ -862,5 +862,87 @@ public override void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySp // override remains for compatibility. base.ImportFromEncryptedPem(input, passwordBytes); } + + /// + /// Exports the current key in the ECPrivateKey format, PEM encoded. + /// + /// A string containing the PEM-encoded ECPrivateKey. + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded ECPrivateKey will begin with -----BEGIN EC PRIVATE KEY----- + /// and end with -----END EC PRIVATE KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ 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); + } + } + } + + /// + /// Attempts to export the current key in the PEM-encoded + /// ECPrivateKey format into a provided buffer. + /// + /// + /// The character span to receive the PEM-encoded ECPrivateKey data. + /// + /// + /// When this method returns, contains a value that indicates the number + /// of characters written to . This + /// parameter is treated as uninitialized. + /// + /// + /// if is big enough + /// to receive the output; otherwise, . + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded ECPrivateKey will begin with + /// -----BEGIN EC PRIVATE KEY----- and end with + /// -----END EC PRIVATE KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ public bool TryExportECPrivateKeyPem(Span destination, out int charsWritten) + { + static bool Export(ECAlgorithm alg, Span destination, out int bytesWritten) + { + return alg.TryExportECPrivateKey(destination, out bytesWritten); + } + + return PemKeyHelpers.TryExportToPem( + this, + PemLabels.EcPrivateKey, + Export, + destination, + out charsWritten); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSA.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSA.cs index 780c6e759f9032..e5fa800517f381 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSA.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/RSA.cs @@ -850,6 +850,159 @@ public override void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySp base.ImportFromEncryptedPem(input, passwordBytes); } + /// + /// Exports the current key in the PKCS#1 RSAPrivateKey format, PEM encoded. + /// + /// A string containing the PEM-encoded PKCS#1 RSAPrivateKey. + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#1 RSAPrivateKey will begin with -----BEGIN RSA PRIVATE KEY----- + /// and end with -----END RSA PRIVATE KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ public unsafe string ExportRSAPrivateKeyPem() + { + byte[] exported = ExportRSAPrivateKey(); + + // Fixed to prevent GC moves. + fixed (byte* pExported = exported) + { + try + { + return PemKeyHelpers.CreatePemFromData(PemLabels.RsaPrivateKey, exported); + } + finally + { + CryptographicOperations.ZeroMemory(exported); + } + } + } + + /// + /// Exports the public-key portion of the current key in the PKCS#1 + /// RSAPublicKey format, PEM encoded. + /// + /// A string containing the PEM-encoded PKCS#1 RSAPublicKey. + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#1 RSAPublicKey will begin with -----BEGIN RSA PUBLIC KEY----- + /// and end with -----END RSA PUBLIC KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ public string ExportRSAPublicKeyPem() + { + byte[] exported = ExportRSAPublicKey(); + return PemKeyHelpers.CreatePemFromData(PemLabels.RsaPublicKey, exported); + } + + /// + /// Attempts to export the current key in the PEM-encoded PKCS#1 + /// RSAPrivateKey format into a provided buffer. + /// + /// + /// The character span to receive the PEM-encoded PKCS#1 RSAPrivateKey data. + /// + /// + /// When this method returns, contains a value that indicates the number + /// of characters written to . This + /// parameter is treated as uninitialized. + /// + /// + /// if is big enough + /// to receive the output; otherwise, . + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#1 RSAPrivateKey will begin with + /// -----BEGIN RSA PRIVATE KEY----- and end with + /// -----END RSA PRIVATE KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ public bool TryExportRSAPrivateKeyPem(Span destination, out int charsWritten) + { + static bool Export(RSA alg, Span destination, out int bytesWritten) + { + return alg.TryExportRSAPrivateKey(destination, out bytesWritten); + } + + return PemKeyHelpers.TryExportToPem( + this, + PemLabels.RsaPrivateKey, + Export, + destination, + out charsWritten); + } + + /// + /// Attempts to export the current key in the PEM-encoded PKCS#1 + /// RSAPublicKey format into a provided buffer. + /// + /// + /// The character span to receive the PEM-encoded PKCS#1 RSAPublicKey data. + /// + /// + /// When this method returns, contains a value that indicates the number + /// of characters written to . This + /// parameter is treated as uninitialized. + /// + /// + /// if is big enough + /// to receive the output; otherwise, . + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#1 RSAPublicKey will begin with + /// -----BEGIN RSA PUBLIC KEY----- and end with + /// -----END RSA PUBLIC KEY-----, with the base64 encoded DER + /// contents of the key between the PEM boundaries. + ///

+ ///

+ /// The PEM is encoded according to the IETF RFC 7468 "strict" + /// encoding rules. + ///

+ ///
+ public bool TryExportRSAPublicKeyPem(Span destination, out int charsWritten) + { + static bool Export(RSA alg, Span 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); diff --git a/src/libraries/System.Security.Cryptography/tests/ECPemExportTests.cs b/src/libraries/System.Security.Cryptography/tests/ECPemExportTests.cs new file mode 100644 index 00000000000000..6f3456678799a1 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/ECPemExportTests.cs @@ -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 destination, out int bytesWritten) + { + ReadOnlySpan 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 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 destination, out int bytesWritten); + + public Func ExportECPrivateKeyDelegate = null; + public TryExportFunc TryExportECPrivateKeyDelegate = null; + + + public DelegateECAlgorithm() + { + } + + public override byte[] ExportECPrivateKey() => ExportECPrivateKeyDelegate(); + + public override bool TryExportECPrivateKey(Span destination, out int bytesWritten) => + TryExportECPrivateKeyDelegate(destination, out bytesWritten); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/RSATests.cs b/src/libraries/System.Security.Cryptography/tests/RSATests.cs index 27b3831c53b4c9..9decadbc179f7e 100644 --- a/src/libraries/System.Security.Cryptography/tests/RSATests.cs +++ b/src/libraries/System.Security.Cryptography/tests/RSATests.cs @@ -230,6 +230,150 @@ public void RSASignaturePadding_Equality() Assert.True(RSASignaturePadding.Pkcs1 != null); } + [Fact] + public static void ExportPem_ExportRSAPublicKey() + { + string expectedPem = + "-----BEGIN RSA PUBLIC KEY-----\n" + + "cGVubnk=\n" + + "-----END RSA PUBLIC KEY-----"; + + static byte[] ExportRSAPublicKey() + { + return new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + } + + using (DelegateRSA rsa = new DelegateRSA()) + { + rsa.ExportRSAPublicKeyDelegate = ExportRSAPublicKey; + Assert.Equal(expectedPem, rsa.ExportRSAPublicKeyPem()); + } + } + + [Fact] + public static void ExportPem_TryExportRSAPublicKey() + { + string expectedPem = + "-----BEGIN RSA PUBLIC KEY-----\n" + + "cGVubnk=\n" + + "-----END RSA PUBLIC KEY-----"; + + static bool TryExportRSAPublicKey(Span destination, out int bytesWritten) + { + ReadOnlySpan result = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + bytesWritten = result.Length; + result.CopyTo(destination); + return true; + } + + using (DelegateRSA rsa = new DelegateRSA()) + { + rsa.TryExportRSAPublicKeyDelegate = TryExportRSAPublicKey; + + int written; + bool result; + char[] buffer; + + // buffer not enough + buffer = new char[expectedPem.Length - 1]; + result = rsa.TryExportRSAPublicKeyPem(buffer, out written); + Assert.False(result, nameof(rsa.TryExportRSAPublicKeyPem)); + Assert.Equal(0, written); + + // buffer just enough + buffer = new char[expectedPem.Length]; + result = rsa.TryExportRSAPublicKeyPem(buffer, out written); + Assert.True(result, nameof(rsa.TryExportRSAPublicKeyPem)); + 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 bufferSpan = buffer.AsSpan(10); + result = rsa.TryExportRSAPublicKeyPem(bufferSpan, out written); + Assert.True(result, nameof(rsa.TryExportRSAPublicKeyPem)); + 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..]); + } + } + + [Fact] + public static void ExportPem_ExportRSAPrivateKey() + { + string expectedPem = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END RSA PRIVATE KEY-----"; + + static byte[] ExportRSAPrivateKey() + { + return new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + } + + using (DelegateRSA rsa = new DelegateRSA()) + { + rsa.ExportRSAPrivateKeyDelegate = ExportRSAPrivateKey; + Assert.Equal(expectedPem, rsa.ExportRSAPrivateKeyPem()); + } + } + + [Fact] + public static void ExportPem_TryExportRSAPrivateKey() + { + string expectedPem = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END RSA PRIVATE KEY-----"; + + static bool TryExportRSAPrivateKey(Span destination, out int bytesWritten) + { + ReadOnlySpan result = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + bytesWritten = result.Length; + result.CopyTo(destination); + return true; + } + + using (DelegateRSA rsa = new DelegateRSA()) + { + rsa.TryExportRSAPrivateKeyDelegate = TryExportRSAPrivateKey; + + int written; + bool result; + char[] buffer; + + // buffer not enough + buffer = new char[expectedPem.Length - 1]; + result = rsa.TryExportRSAPrivateKeyPem(buffer, out written); + Assert.False(result, nameof(rsa.TryExportRSAPrivateKeyPem)); + Assert.Equal(0, written); + + // buffer just enough + buffer = new char[expectedPem.Length]; + result = rsa.TryExportRSAPrivateKeyPem(buffer, out written); + Assert.True(result, nameof(rsa.TryExportRSAPrivateKeyPem)); + 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 bufferSpan = buffer.AsSpan(10); + result = rsa.TryExportRSAPrivateKeyPem(bufferSpan, out written); + Assert.True(result, nameof(rsa.TryExportRSAPrivateKeyPem)); + 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 sealed class EmptyRSA : RSA { public override RSAParameters ExportParameters(bool includePrivateParameters) => throw new NotImplementedException(); @@ -240,12 +384,17 @@ private sealed class EmptyRSA : RSA private sealed class DelegateRSA : RSA { + public delegate bool TryExportFunc(Span destination, out int bytesWritten); public Func DecryptDelegate; public Func EncryptDelegate; public Func SignHashDelegate = null; public Func VerifyHashDelegate = null; public Func HashDataArrayDelegate = null; public Func HashDataStreamDelegate = null; + public Func ExportRSAPublicKeyDelegate = null; + public TryExportFunc TryExportRSAPublicKeyDelegate = null; + public Func ExportRSAPrivateKeyDelegate = null; + public TryExportFunc TryExportRSAPrivateKeyDelegate = null; public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) => EncryptDelegate(data, padding); @@ -265,6 +414,15 @@ protected override byte[] HashData(byte[] data, int offset, int count, HashAlgor protected override byte[] HashData(Stream data, HashAlgorithmName hashAlgorithm) => HashDataStreamDelegate(data, hashAlgorithm); + public override byte[] ExportRSAPublicKey() => ExportRSAPublicKeyDelegate(); + public override byte[] ExportRSAPrivateKey() => ExportRSAPrivateKeyDelegate(); + + public override bool TryExportRSAPublicKey(Span destination, out int bytesWritten) => + TryExportRSAPublicKeyDelegate(destination, out bytesWritten); + + public override bool TryExportRSAPrivateKey(Span destination, out int bytesWritten) => + TryExportRSAPrivateKeyDelegate(destination, out bytesWritten); + public new bool TryHashData(ReadOnlySpan source, Span destination, HashAlgorithmName hashAlgorithm, out int bytesWritten) => base.TryHashData(source, destination, hashAlgorithm, out bytesWritten); diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 7b7211285d8478..8590d2df5538f2 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -213,6 +213,7 @@ +