diff --git a/src/libraries/Common/src/Internal/Cryptography/PemKeyImportHelpers.cs b/src/libraries/Common/src/Internal/Cryptography/PemKeyHelpers.cs similarity index 65% rename from src/libraries/Common/src/Internal/Cryptography/PemKeyImportHelpers.cs rename to src/libraries/Common/src/Internal/Cryptography/PemKeyHelpers.cs index 27e4db1f817a4f..21432c2ae2e620 100644 --- a/src/libraries/Common/src/Internal/Cryptography/PemKeyImportHelpers.cs +++ b/src/libraries/Common/src/Internal/Cryptography/PemKeyHelpers.cs @@ -7,8 +7,106 @@ namespace Internal.Cryptography { - internal static class PemKeyImportHelpers + internal static class PemKeyHelpers { + public delegate bool TryExportKeyAction(T arg, Span destination, out int bytesWritten); + public delegate bool TryExportEncryptedKeyAction( + T arg, + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten); + + public static unsafe bool TryExportToEncryptedPem( + T arg, + ReadOnlySpan password, + PbeParameters pbeParameters, + TryExportEncryptedKeyAction exporter, + Span destination, + out int charsWritten) + { + int bufferSize = 4096; + + while (true) + { + byte[] buffer = CryptoPool.Rent(bufferSize); + int bytesWritten = 0; + bufferSize = buffer.Length; + + // Fixed to prevent GC moves. + fixed (byte* bufferPtr = buffer) + { + try + { + if (exporter(arg, password, pbeParameters, buffer, out bytesWritten)) + { + Span writtenSpan = new Span(buffer, 0, bytesWritten); + return PemEncoding.TryWrite(PemLabels.EncryptedPkcs8PrivateKey, writtenSpan, destination, out charsWritten); + } + } + finally + { + CryptoPool.Return(buffer, bytesWritten); + } + + bufferSize = checked(bufferSize * 2); + } + } + } + + public static unsafe bool TryExportToPem( + T arg, + string label, + TryExportKeyAction exporter, + Span destination, + out int charsWritten) + { + int bufferSize = 4096; + + while (true) + { + byte[] buffer = CryptoPool.Rent(bufferSize); + int bytesWritten = 0; + bufferSize = buffer.Length; + + // Fixed to prevent GC moves. + fixed (byte* bufferPtr = buffer) + { + try + { + if (exporter(arg, buffer, out bytesWritten)) + { + Span writtenSpan = new Span(buffer, 0, bytesWritten); + return PemEncoding.TryWrite(label, writtenSpan, destination, out charsWritten); + } + } + finally + { + CryptoPool.Return(buffer, bytesWritten); + } + + bufferSize = checked(bufferSize * 2); + } + } + } + + internal static string CreatePemFromData(string label, ReadOnlyMemory data) + { + int pemSize = PemEncoding.GetEncodedSize(label.Length, data.Length); + + return string.Create(pemSize, (label, data), static (destination, args) => + { + (string label, ReadOnlyMemory data) = args; + + if (!PemEncoding.TryWrite(label, data.Span, destination, out int charsWritten) || + charsWritten != destination.Length) + { + Debug.Fail("Pre-allocated buffer was not the correct size."); + throw new CryptographicException(); + } + }); + } + public delegate void ImportKeyAction(ReadOnlySpan source, out int bytesRead); public delegate ImportKeyAction? FindImportActionFunc(ReadOnlySpan label); public delegate void ImportEncryptedKeyAction( diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj index 2c4c3e75f4644a..6873ecafbc81a5 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj @@ -108,8 +108,8 @@ Link="Internal\Cryptography\BasicSymmetricCipher.cs" /> - + destination, out int /// public override void ImportFromPem(ReadOnlySpan input) { - PemKeyImportHelpers.ImportPem(input, label => { + PemKeyHelpers.ImportPem(input, label => { if (label.SequenceEqual(PemLabels.Pkcs8PrivateKey)) { return ImportPkcs8PrivateKey; diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ECDsa.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ECDsa.cs index 4c6d1cc38c2197..77d87b102f2547 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ECDsa.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ECDsa.cs @@ -1333,7 +1333,7 @@ public int GetMaxSignatureSize(DSASignatureFormat signatureFormat) /// public override void ImportFromPem(ReadOnlySpan input) { - PemKeyImportHelpers.ImportPem(input, label => { + PemKeyHelpers.ImportPem(input, label => { if (label.SequenceEqual(PemLabels.Pkcs8PrivateKey)) { return ImportPkcs8PrivateKey; diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RSA.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RSA.cs index d98396703ee624..9aa15207646682 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RSA.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/RSA.cs @@ -679,7 +679,7 @@ public override unsafe void ImportEncryptedPkcs8PrivateKey( /// public override void ImportFromPem(ReadOnlySpan input) { - PemKeyImportHelpers.ImportPem(input, label => { + PemKeyHelpers.ImportPem(input, label => { if (label.SequenceEqual(PemLabels.RsaPrivateKey)) { return ImportRSAPrivateKey; 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 28f4fe3fd09f48..6a76359cc41fd2 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -63,8 +63,11 @@ public void Dispose() { } protected virtual void Dispose(bool disposing) { } public virtual byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public virtual byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public string ExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public virtual byte[] ExportPkcs8PrivateKey() { throw null; } + public string ExportPkcs8PrivateKeyPem() { throw null; } public virtual byte[] ExportSubjectPublicKeyInfo() { throw null; } + public string ExportSubjectPublicKeyInfoPem() { throw null; } public virtual void FromXmlString(string xmlString) { } public virtual void ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.ReadOnlySpan source, out int bytesRead) { throw null; } public virtual void ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.ReadOnlySpan source, out int bytesRead) { throw null; } @@ -76,8 +79,11 @@ public virtual void ImportFromPem(System.ReadOnlySpan input) { } public virtual string ToXmlString(bool includePrivateParameters) { throw null; } public virtual bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public virtual bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int charsWritten) { throw null; } public virtual bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportPkcs8PrivateKeyPem(System.Span destination, out int charsWritten) { throw null; } public virtual bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportSubjectPublicKeyInfoPem(System.Span destination, out int charsWritten) { throw null; } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public abstract partial class AsymmetricKeyExchangeFormatter diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 091ebeec2de2ba..bc8d17c1f702aa 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -21,8 +21,8 @@ Link="Common\System\Obsoletions.cs" /> - + ExportArray( (Span destination, out int i) => TryExportSubjectPublicKeyInfo(destination, out i)); - public virtual bool TryExportEncryptedPkcs8PrivateKey( ReadOnlySpan passwordBytes, PbeParameters pbeParameters, @@ -238,7 +237,7 @@ public virtual bool TryExportSubjectPublicKeyInfo(Span destination, out in /// public virtual void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySpan password) { - PemKeyImportHelpers.ImportEncryptedPem(input, password, ImportEncryptedPkcs8PrivateKey); + PemKeyHelpers.ImportEncryptedPem(input, password, ImportEncryptedPkcs8PrivateKey); } /// @@ -315,7 +314,7 @@ public virtual void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySpa /// public virtual void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySpan passwordBytes) { - PemKeyImportHelpers.ImportEncryptedPem(input, passwordBytes, ImportEncryptedPkcs8PrivateKey); + PemKeyHelpers.ImportEncryptedPem(input, passwordBytes, ImportEncryptedPkcs8PrivateKey); } /// @@ -362,7 +361,7 @@ public virtual void ImportFromEncryptedPem(ReadOnlySpan input, ReadOnlySpa /// public virtual void ImportFromPem(ReadOnlySpan input) { - PemKeyImportHelpers.ImportPem(input, label => + PemKeyHelpers.ImportPem(input, label => { if (label.SequenceEqual(PemLabels.Pkcs8PrivateKey)) { @@ -379,6 +378,295 @@ public virtual void ImportFromPem(ReadOnlySpan input) }); } + /// + /// Exports the current key in the PKCS#8 PrivateKeyInfo format, PEM encoded. + /// + /// A string containing the PEM-encoded PKCS#8 PrivateKeyInfo. + /// + /// An implementation for or + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#8 PrivateKeyInfo will begin with -----BEGIN PRIVATE KEY----- + /// and end with -----END 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 ExportPkcs8PrivateKeyPem() + { + byte[] exported = ExportPkcs8PrivateKey(); + + // Fixed to prevent GC moves. + fixed (byte* pExported = exported) + { + try + { + return PemKeyHelpers.CreatePemFromData(PemLabels.Pkcs8PrivateKey, exported); + } + finally + { + CryptographicOperations.ZeroMemory(exported); + } + } + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format + /// with a char-based password, PEM encoded. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo. + /// + /// An implementation for or + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// When indicates an algorithm that + /// uses PBKDF2 (Password-Based Key Derivation Function 2), the password + /// is converted to bytes via the UTF-8 encoding. + ///

+ ///

+ /// A PEM-encoded PKCS#8 EncryptedPrivateKeyInfo will begin with + /// -----BEGIN ENCRYPTED PRIVATE KEY----- and end with + /// -----END ENCRYPTED 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 ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan password, PbeParameters pbeParameters) + { + byte[] exported = ExportEncryptedPkcs8PrivateKey(password, pbeParameters); + + // Fixed to prevent GC moves. + fixed (byte* pExported = exported) + { + try + { + return PemKeyHelpers.CreatePemFromData(PemLabels.EncryptedPkcs8PrivateKey, exported); + } + finally + { + CryptographicOperations.ZeroMemory(exported); + } + } + } + + /// + /// Exports the public-key portion of the current key in the X.509 + /// SubjectPublicKeyInfo format, PEM encoded. + /// + /// A string containing the PEM-encoded X.509 SubjectPublicKeyInfo. + /// + /// An implementation for or + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded X.509 SubjectPublicKeyInfo will begin with + /// -----BEGIN PUBLIC KEY----- and end with + /// -----END 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 ExportSubjectPublicKeyInfoPem() + { + byte[] exported = ExportSubjectPublicKeyInfo(); + return PemKeyHelpers.CreatePemFromData(PemLabels.SpkiPublicKey, exported); + } + + /// + /// Attempts to export the current key in the PEM-encoded X.509 + /// SubjectPublicKeyInfo format into a provided buffer. + /// + /// + /// The character span to receive the PEM-encoded X.509 SubjectPublicKeyInfo 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, . + /// + /// + /// An implementation for + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded X.509 SubjectPublicKeyInfo will begin with + /// -----BEGIN PUBLIC KEY----- and end with + /// -----END 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 TryExportSubjectPublicKeyInfoPem(Span destination, out int charsWritten) + { + static bool Export(AsymmetricAlgorithm alg, Span destination, out int bytesWritten) + { + return alg.TryExportSubjectPublicKeyInfo(destination, out bytesWritten); + } + + return PemKeyHelpers.TryExportToPem( + this, + PemLabels.SpkiPublicKey, + Export, + destination, + out charsWritten); + } + + /// + /// Attempts to export the current key in the PEM-encoded PKCS#8 + /// PrivateKeyInfo format into a provided buffer. + /// + /// + /// The character span to receive the PEM-encoded PKCS#8 PrivateKeyInfo 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, . + /// + /// + /// An implementation for + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// A PEM-encoded PKCS#8 PrivateKeyInfo will begin with -----BEGIN PRIVATE KEY----- + /// and end with -----END 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 TryExportPkcs8PrivateKeyPem(Span destination, out int charsWritten) + { + static bool Export(AsymmetricAlgorithm alg, Span destination, out int bytesWritten) + { + return alg.TryExportPkcs8PrivateKey(destination, out bytesWritten); + } + + return PemKeyHelpers.TryExportToPem( + this, + PemLabels.Pkcs8PrivateKey, + Export, + destination, + out charsWritten); + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format + /// with a char-based password, PEM encoded. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The character span to receive the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo 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, . + /// + /// + /// An implementation for + /// has not been provided. + /// + /// + /// The key could not be exported. + /// + /// + ///

+ /// When indicates an algorithm that + /// uses PBKDF2 (Password-Based Key Derivation Function 2), the password + /// is converted to bytes via the UTF-8 encoding. + ///

+ ///

+ /// A PEM-encoded PKCS#8 EncryptedPrivateKeyInfo will begin with + /// -----BEGIN ENCRYPTED PRIVATE KEY----- and end with + /// -----END ENCRYPTED 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 TryExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan password, PbeParameters pbeParameters, Span destination, out int charsWritten) + { + static bool Export( + AsymmetricAlgorithm alg, + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + return alg.TryExportEncryptedPkcs8PrivateKey(password, pbeParameters, destination, out bytesWritten); + } + + return PemKeyHelpers.TryExportToEncryptedPem( + this, + password, + pbeParameters, + Export, + destination, + out charsWritten); + } + private delegate bool TryExportPbe( ReadOnlySpan password, PbeParameters pbeParameters, diff --git a/src/libraries/System.Security.Cryptography/tests/AsymmetricAlgorithmTests.cs b/src/libraries/System.Security.Cryptography/tests/AsymmetricAlgorithmTests.cs index 4e2165d2534bac..ca40479e6c2583 100644 --- a/src/libraries/System.Security.Cryptography/tests/AsymmetricAlgorithmTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/AsymmetricAlgorithmTests.cs @@ -29,7 +29,7 @@ static void ImportSubjectPublicKeyInfo(ReadOnlySpan source, out int bytesR bytesRead = expected.Length; } - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { alg.ImportSubjectPublicKeyInfoImpl = ImportSubjectPublicKeyInfo; alg.ImportFromPem(pemText); @@ -55,7 +55,7 @@ static void ImportPkcs8PrivateKey(ReadOnlySpan source, out int bytesRead) bytesRead = expected.Length; } - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { alg.ImportPkcs8PrivateKeyImpl = ImportPkcs8PrivateKey; alg.ImportFromPem(pemText); @@ -86,7 +86,7 @@ void ImportEncryptedPkcs8PrivateKey( bytesRead = expected.Length; } - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { alg.ImportEncryptedPkcs8PrivateKeyByteFunc = ImportEncryptedPkcs8PrivateKey; alg.ImportFromEncryptedPem(pemText, pemPassword); @@ -117,7 +117,7 @@ void ImportEncryptedPkcs8PrivateKey( bytesRead = expected.Length; } - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { alg.ImportEncryptedPkcs8PrivateKeyCharFunc = ImportEncryptedPkcs8PrivateKey; alg.ImportFromEncryptedPem(pemText, pemPassword); @@ -135,7 +135,7 @@ public static void ImportFromPem_AmbiguousKey() Y29mZmVl -----END PUBLIC KEY-----"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromPem(pemText)); } @@ -153,7 +153,7 @@ public static void ImportFromPem_Encrypted_AmbiguousKey() -----END ENCRYPTED PRIVATE KEY-----"; string pemPassword = "PLACEHOLDER"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromEncryptedPem(pemText, pemPassword)); } @@ -167,7 +167,7 @@ public static void ImportFromPem_NoUnderstoodPemLabel() zzzz -----END SLEEPING-----"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromPem(pemText)); } @@ -182,7 +182,7 @@ public static void ImportFromPem_Encrypted_NoUnderstoodPemLabel() -----END SLEEPING-----"; string pemPassword = "PLACEHOLDER"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromEncryptedPem(pemText, pemPassword)); } @@ -196,7 +196,7 @@ public static void ImportFromPem_EncryptedPemWithoutPassword() c2xlZXA= -----END ENCRYPTED PRIVATE KEY-----"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromPem(pemText)); } @@ -211,25 +211,284 @@ public static void ImportFromPem_NotEncryptedWithPassword() -----END PRIVATE KEY-----"; string pemPassword = "PLACEHOLDER"; - using (ImportAsymmetricAlgorithm alg = new ImportAsymmetricAlgorithm()) + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) { AssertExtensions.Throws("input", () => alg.ImportFromEncryptedPem(pemText, pemPassword)); } } - private class ImportAsymmetricAlgorithm : AsymmetricAlgorithm + [Fact] + public static void ExportPem_ExportSubjectPublicKeyInfoPem() + { + string expectedPem = + "-----BEGIN PUBLIC KEY-----\n" + + "cGVubnk=\n" + + "-----END PUBLIC KEY-----"; + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.ExportSubjectPublicKeyInfoImpl = static () => new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + string pem = alg.ExportSubjectPublicKeyInfoPem(); + Assert.Equal(expectedPem, pem); + } + } + + [Fact] + public static void ExportPem_TryExportSubjectPublicKeyInfoPem() + { + string expectedPem = + "-----BEGIN PUBLIC KEY-----\n" + + "cGVubnk=\n" + + "-----END PUBLIC KEY-----"; + + static bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesWritten) + { + ReadOnlySpan result = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + bytesWritten = result.Length; + result.CopyTo(destination); + return true; + } + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.TryExportSubjectPublicKeyInfoImpl = TryExportSubjectPublicKeyInfo; + int written; + bool result; + char[] buffer; + + // buffer not enough + buffer = new char[expectedPem.Length - 1]; + result = alg.TryExportSubjectPublicKeyInfoPem(buffer, out written); + Assert.False(result, nameof(alg.TryExportSubjectPublicKeyInfoPem)); + Assert.Equal(0, written); + + // buffer just enough + buffer = new char[expectedPem.Length]; + result = alg.TryExportSubjectPublicKeyInfoPem(buffer, out written); + Assert.True(result, nameof(alg.TryExportSubjectPublicKeyInfoPem)); + 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 = alg.TryExportSubjectPublicKeyInfoPem(bufferSpan, out written); + Assert.True(result, nameof(alg.TryExportSubjectPublicKeyInfoPem)); + 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_TryExportPkcs8PrivateKeyPem() + { + string expectedPem = + "-----BEGIN PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END PRIVATE KEY-----"; + + static bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) + { + ReadOnlySpan result = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + bytesWritten = result.Length; + result.CopyTo(destination); + return true; + } + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.TryExportPkcs8PrivateKeyImpl = TryExportPkcs8PrivateKey; + int written; + bool result; + char[] buffer; + + // buffer not enough + buffer = new char[expectedPem.Length - 1]; + result = alg.TryExportPkcs8PrivateKeyPem(buffer, out written); + Assert.False(result, nameof(alg.TryExportPkcs8PrivateKeyPem)); + Assert.Equal(0, written); + + // buffer just enough + buffer = new char[expectedPem.Length]; + result = alg.TryExportPkcs8PrivateKeyPem(buffer, out written); + Assert.True(result, nameof(alg.TryExportPkcs8PrivateKeyPem)); + 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 = alg.TryExportPkcs8PrivateKeyPem(bufferSpan, out written); + Assert.True(result, nameof(alg.TryExportPkcs8PrivateKeyPem)); + 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_ExportPkcs8PrivateKeyPem() + { + string expectedPem = + "-----BEGIN PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END PRIVATE KEY-----"; + + byte[] exportedBytes = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.ExportPkcs8PrivateKeyPemImpl = () => exportedBytes; + string pem = alg.ExportPkcs8PrivateKeyPem(); + Assert.Equal(expectedPem, pem); + + // Test that the PEM export cleared the PKCS8 bytes from memory + // that were returned from ExportPkcs8PrivateKey. + AssertExtensions.FilledWith((byte)0, exportedBytes); + } + } + + [Fact] + public static void ExportPem_ExportEncryptedPkcs8PrivateKeyPem() { + string expectedPem = + "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END ENCRYPTED PRIVATE KEY-----"; + + byte[] exportedBytes = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + string expectedPassword = "PLACEHOLDER"; + PbeParameters expectedPbeParameters = new PbeParameters( + PbeEncryptionAlgorithm.Aes256Cbc, + HashAlgorithmName.SHA384, + RandomNumberGenerator.GetInt32(0, 100_000)); + + byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbeParameters pbeParameters) + { + Assert.Equal(expectedPbeParameters.EncryptionAlgorithm, pbeParameters.EncryptionAlgorithm); + Assert.Equal(expectedPbeParameters.HashAlgorithm, pbeParameters.HashAlgorithm); + Assert.Equal(expectedPbeParameters.IterationCount, pbeParameters.IterationCount); + Assert.Equal(expectedPassword, new string(password)); + + return exportedBytes; + } + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.ExportEncryptedPkcs8PrivateKeyImpl = ExportEncryptedPkcs8PrivateKey; + string pem = alg.ExportEncryptedPkcs8PrivateKeyPem(expectedPassword, expectedPbeParameters); + Assert.Equal(expectedPem, pem); + + // Test that the PEM export cleared the PKCS8 bytes from memory + // that were returned from ExportEncryptedPkcs8PrivateKey. + AssertExtensions.FilledWith((byte)0, exportedBytes); + } + } + + [Fact] + public static void ExportPem_TryExportEncryptedPkcs8PrivateKeyPem() + { + string expectedPem = + "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" + + "cGVubnk=\n" + + "-----END ENCRYPTED PRIVATE KEY-----"; + + byte[] exportedBytes = new byte[] { 0x70, 0x65, 0x6e, 0x6e, 0x79 }; + string expectedPassword = "PLACEHOLDER"; + PbeParameters expectedPbeParameters = new PbeParameters( + PbeEncryptionAlgorithm.Aes256Cbc, + HashAlgorithmName.SHA384, + RandomNumberGenerator.GetInt32(0, 100_000)); + + bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + Assert.Equal(expectedPbeParameters.EncryptionAlgorithm, pbeParameters.EncryptionAlgorithm); + Assert.Equal(expectedPbeParameters.HashAlgorithm, pbeParameters.HashAlgorithm); + Assert.Equal(expectedPbeParameters.IterationCount, pbeParameters.IterationCount); + Assert.Equal(expectedPassword, new string(password)); + + exportedBytes.AsSpan().CopyTo(destination); + bytesWritten = exportedBytes.Length; + return true; + } + + using (StubAsymmetricAlgorithm alg = new StubAsymmetricAlgorithm()) + { + alg.TryExportEncryptedPkcs8PrivateKeyImpl = TryExportEncryptedPkcs8PrivateKey; + int written; + bool result; + char[] buffer; + + // buffer not enough + buffer = new char[expectedPem.Length - 1]; + result = alg.TryExportEncryptedPkcs8PrivateKeyPem(expectedPassword, expectedPbeParameters, buffer, out written); + Assert.False(result, nameof(alg.TryExportEncryptedPkcs8PrivateKeyPem)); + Assert.Equal(0, written); + + // buffer just enough + buffer = new char[expectedPem.Length]; + result = alg.TryExportEncryptedPkcs8PrivateKeyPem(expectedPassword, expectedPbeParameters, buffer, out written); + Assert.True(result, nameof(alg.TryExportEncryptedPkcs8PrivateKeyPem)); + 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 = alg.TryExportEncryptedPkcs8PrivateKeyPem(expectedPassword, expectedPbeParameters, bufferSpan, out written); + Assert.True(result, nameof(alg.TryExportEncryptedPkcs8PrivateKeyPem)); + 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 StubAsymmetricAlgorithm : AsymmetricAlgorithm + { + public delegate byte[] ExportSubjectPublicKeyInfoFunc(); + public delegate byte[] ExportPkcs8PrivateKeyPemFunc(); + public delegate byte[] ExportEncryptedPkcs8PrivateKeyFunc(ReadOnlySpan password, PbeParameters pbeParameters); + public delegate bool TryExportSubjectPublicKeyInfoFunc(Span destination, out int bytesWritten); + public delegate bool TryExportPkcs8PrivateKeyFunc(Span destination, out int bytesWritten); public delegate void ImportSubjectPublicKeyInfoFunc(ReadOnlySpan source, out int bytesRead); public delegate void ImportPkcs8PrivateKeyFunc(ReadOnlySpan source, out int bytesRead); public delegate void ImportEncryptedPkcs8PrivateKeyFunc( ReadOnlySpan password, ReadOnlySpan source, out int bytesRead); + public delegate bool TryExportEncryptedPkcs8PrivateKeyFunc( + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten); public ImportSubjectPublicKeyInfoFunc ImportSubjectPublicKeyInfoImpl { get; set; } public ImportPkcs8PrivateKeyFunc ImportPkcs8PrivateKeyImpl { get; set; } public ImportEncryptedPkcs8PrivateKeyFunc ImportEncryptedPkcs8PrivateKeyByteFunc { get; set; } public ImportEncryptedPkcs8PrivateKeyFunc ImportEncryptedPkcs8PrivateKeyCharFunc { get; set; } + public ExportSubjectPublicKeyInfoFunc ExportSubjectPublicKeyInfoImpl {get; set; } + public TryExportSubjectPublicKeyInfoFunc TryExportSubjectPublicKeyInfoImpl { get; set; } + public ExportPkcs8PrivateKeyPemFunc ExportPkcs8PrivateKeyPemImpl { get; set; } + public TryExportPkcs8PrivateKeyFunc TryExportPkcs8PrivateKeyImpl { get; set; } + public ExportEncryptedPkcs8PrivateKeyFunc ExportEncryptedPkcs8PrivateKeyImpl { get; set; } + public TryExportEncryptedPkcs8PrivateKeyFunc TryExportEncryptedPkcs8PrivateKeyImpl { get; set; } public override void ImportSubjectPublicKeyInfo(ReadOnlySpan source, out int bytesRead) => ImportSubjectPublicKeyInfoImpl(source, out bytesRead); @@ -237,6 +496,14 @@ public override void ImportSubjectPublicKeyInfo(ReadOnlySpan source, out i public override void ImportPkcs8PrivateKey(ReadOnlySpan source, out int bytesRead) => ImportPkcs8PrivateKeyImpl(source, out bytesRead); + public override byte[] ExportSubjectPublicKeyInfo() => ExportSubjectPublicKeyInfoImpl(); + public override byte[] ExportPkcs8PrivateKey() => ExportPkcs8PrivateKeyPemImpl(); + + public override byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbeParameters pbeParameters) + { + return ExportEncryptedPkcs8PrivateKeyImpl(password, pbeParameters); + } + public override void ImportEncryptedPkcs8PrivateKey( ReadOnlySpan passwordBytes, ReadOnlySpan source, @@ -252,6 +519,25 @@ public override void ImportEncryptedPkcs8PrivateKey( { ImportEncryptedPkcs8PrivateKeyCharFunc(password, source, out bytesRead); } + + public override bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesWritten) + { + return TryExportSubjectPublicKeyInfoImpl(destination, out bytesWritten); + } + + public override bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritten) + { + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + public override bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + return TryExportEncryptedPkcs8PrivateKeyImpl(password, pbeParameters, destination, out bytesWritten); + } } } }