From c947d93905d771d40f15b8975e6d8c242f1f8662 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 4 Apr 2025 16:06:36 -0400 Subject: [PATCH 1/6] Add PKCS#8 EncryptedPrivateKeyInfo exports --- .../src/System/Security/Cryptography/MLKem.cs | 291 ++++++++++++++++++ .../Security/Cryptography/MLKemBaseTests.cs | 35 ++- .../ref/System.Security.Cryptography.cs | 5 + 3 files changed, 329 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs index 9d8e35812bfefe..d62d4bfa2af67c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs @@ -887,6 +887,261 @@ static void ClearAndReturnToPool(byte[] buffer, int clearSize) /// protected abstract bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten); + /// + /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer, + /// using a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The buffer to receive the PKCS#8 EncryptedPrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// is . + /// + /// + /// This instance has been disposed. + /// + /// + /// This instance only represents a public key. + /// -or- + /// The private key is not exportable. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan password, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); + ThrowIfDisposed(); + + AsnWriter? writer = null; + + try + { + writer = TryExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); + } + finally + { + writer?.Reset(); + } + } + + /// + /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer, + /// using a byte-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// The buffer to receive the PKCS#8 EncryptedPrivateKeyInfo value. + /// + /// + /// When this method returns, contains the number of bytes written to the buffer. + /// This parameter is treated as uninitialized. + /// + /// + /// if was large enough to hold the result; + /// otherwise, . + /// + /// + /// is . + /// + /// + /// This instance has been disposed. + /// + /// + /// This instance only represents a public key. + /// -or- + /// The private key is not exportable. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public bool TryExportEncryptedPkcs8PrivateKey( + ReadOnlySpan passwordBytes, + PbeParameters pbeParameters, + Span destination, + out int bytesWritten) + { + ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); + ThrowIfDisposed(); + + AsnWriter? writer = null; + + try + { + writer = TryExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); + } + finally + { + writer?.Reset(); + } + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a byte-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of the this key. + /// + /// + /// is . + /// + /// + /// This instance has been disposed. + /// + /// + /// This instance only represents a public key. + /// -or- + /// The private key is not exportable. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, PbeParameters pbeParameters) + { + ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); + ThrowIfDisposed(); + + AsnWriter? writer = null; + + try + { + writer = TryExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); + } + finally + { + writer?.Reset(); + } + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of the this key. + /// + /// + /// is . + /// + /// + /// This instance has been disposed. + /// + /// + /// This instance only represents a public key. + /// -or- + /// The private key is not exportable. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbeParameters pbeParameters) + { + ThrowIfNull(pbeParameters); + PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); + ThrowIfDisposed(); + + AsnWriter? writer = null; + + try + { + writer = TryExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); + } + finally + { + writer?.Reset(); + } + } + + /// + /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a char-based password. + /// + /// + /// The password to use when encrypting the key material. + /// + /// + /// The password-based encryption (PBE) parameters to use when encrypting the key material. + /// + /// + /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of the this key. + /// + /// + /// or is . + /// + /// + /// This instance has been disposed. + /// + /// + /// This instance only represents a public key. + /// -or- + /// The private key is not exportable. + /// -or- + /// An error occurred while exporting the key. + /// -or- + /// does not represent a valid password-based encryption algorithm. + /// + public byte[] ExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters) + { + ThrowIfNull(password); + return ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); + } + /// /// Imports an ML-KEM encapsulation key from an X.509 SubjectPublicKeyInfo structure. /// @@ -1452,5 +1707,41 @@ private static void ThrowIfNull( } #endif } + + private AsnWriter TryExportEncryptedPkcs8PrivateKeyCore( + ReadOnlySpan password, + PbeParameters pbeParameters, + WriteEncryptedPkcs8Func encryptor) + { + // There are 28 bytes of overhead on a plain PKCS#8 export for an expanded key. Add a little extra for + // some extra space. + int initialSize = Algorithm.DecapsulationKeySizeInBytes + 32; + byte[] rented = CryptoPool.Rent(initialSize); + int written; + + while (!TryExportPkcs8PrivateKey(rented, out written)) + { + CryptoPool.Return(rented, 0); + rented = CryptoPool.Rent(rented.Length * 2); + } + + AsnWriter tmp = new(AsnEncodingRules.BER, initialCapacity: written); + + try + { + tmp.WriteEncodedValueForCrypto(rented.AsSpan(0, written)); + return encryptor(password, tmp, pbeParameters); + } + finally + { + tmp.Reset(); + CryptoPool.Return(rented, written); + } + } + + private delegate AsnWriter WriteEncryptedPkcs8Func( + ReadOnlySpan password, + AsnWriter writer, + PbeParameters pbeParameters); } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs index 6f4007bf85b8fc..45c774f08be098 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Text; using Microsoft.DotNet.XUnitExtensions; using Test.Cryptography; using Xunit; @@ -497,11 +498,41 @@ public void TryExportPkcs8PrivateKey_EncapsulationKey_Fails() private static void AssertExportPkcs8PrivateKey(MLKem kem, Action callback) { - byte[] pkcs8 = DoTryUntilDone(kem.TryExportPkcs8PrivateKey); - callback(pkcs8); + callback(DoTryUntilDone(kem.TryExportPkcs8PrivateKey)); callback(kem.ExportPkcs8PrivateKey()); } + private static void AssertEncryptedExportPkcs8PrivateKey( + MLKem kem, + string password, + PbeParameters pbeParameters, + Action callback) + { + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + + callback(DoTryUntilDone((Span destination, out int bytesWritten) => + { + return kem.TryExportEncryptedPkcs8PrivateKey( + password.AsSpan(), + pbeParameters, + destination, + out bytesWritten); + })); + + callback(DoTryUntilDone((Span destination, out int bytesWritten) => + { + return kem.TryExportEncryptedPkcs8PrivateKey( + new ReadOnlySpan(passwordBytes), + pbeParameters, + destination, + out bytesWritten); + })); + + callback(kem.ExportEncryptedPkcs8PrivateKey(password, pbeParameters)); + callback(kem.ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters)); + callback(kem.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(passwordBytes), pbeParameters)); + } + private delegate bool TryExportFunc(Span destination, out int bytesWritten); private static byte[] DoTryUntilDone(TryExportFunc func) 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 0930925517942f..7de66924526cd3 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1874,6 +1874,9 @@ public void ExportDecapsulationKey(System.Span destination) { } public byte[] ExportEncapsulationKey() { throw null; } public void ExportEncapsulationKey(System.Span destination) { } protected abstract void ExportEncapsulationKeyCore(System.Span destination); + public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportEncryptedPkcs8PrivateKey(string password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public byte[] ExportPkcs8PrivateKey() { throw null; } public byte[] ExportPrivateSeed() { throw null; } public void ExportPrivateSeed(System.Span destination) { } @@ -1899,6 +1902,8 @@ public void ExportPrivateSeed(System.Span destination) { } public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(byte[] source) { throw null; } public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(System.ReadOnlySpan source) { throw null; } protected void ThrowIfDisposed() { } + public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten); public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } From de9551c99e7cde753e79c7eb40cbd6b321a1a3fe Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 4 Apr 2025 18:09:41 -0400 Subject: [PATCH 2/6] Two simple round-trip tests --- .../Security/Cryptography/MLKemBaseTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs index 45c774f08be098..0613e5cb878054 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs @@ -11,6 +11,8 @@ namespace System.Security.Cryptography.Tests { public abstract class MLKemBaseTests { + private static readonly PbeParameters s_aes128Pbe = new(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 2); + public abstract MLKem GenerateKey(MLKemAlgorithm algorithm); public abstract MLKem ImportPrivateSeed(MLKemAlgorithm algorithm, ReadOnlySpan seed); public abstract MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, ReadOnlySpan source); @@ -496,6 +498,40 @@ public void TryExportPkcs8PrivateKey_EncapsulationKey_Fails() Assert.Throws(() => kem.ExportPkcs8PrivateKey()); } + [Fact] + public void ExportEncryptedPkcs8PrivateKey_DecapsulationKey_Roundtrip() + { + using MLKem kem = ImportDecapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512DecapsulationKey); + AssertEncryptedExportPkcs8PrivateKey(kem, MLKemTestData.EncryptedPrivateKeyPassword, s_aes128Pbe, pkcs8 => + { + using MLKem imported = MLKem.ImportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + pkcs8); + + AssertExtensions.SequenceEqual( + MLKemTestData.MLKem512DecapsulationKey, + imported.ExportDecapsulationKey()); + Assert.Throws(() => imported.ExportPrivateSeed()); + }); + } + + [Fact] + public void ExportEncryptedPkcs8PrivateKey_Seed_Roundtrip() + { + using MLKem kem = ImportPrivateSeed(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512PrivateSeed); + AssertEncryptedExportPkcs8PrivateKey(kem, MLKemTestData.EncryptedPrivateKeyPassword, s_aes128Pbe, pkcs8 => + { + using MLKem imported = MLKem.ImportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + pkcs8); + + AssertExtensions.SequenceEqual(MLKemTestData.MLKem512PrivateSeed, imported.ExportPrivateSeed()); + AssertExtensions.SequenceEqual( + MLKemTestData.MLKem512DecapsulationKey, + imported.ExportDecapsulationKey()); + }); + } + private static void AssertExportPkcs8PrivateKey(MLKem kem, Action callback) { callback(DoTryUntilDone(kem.TryExportPkcs8PrivateKey)); From 742f47c49ec73c71e8782bc9438f3aa259cdc008 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 4 Apr 2025 21:51:27 -0400 Subject: [PATCH 3/6] Add some contract tests --- .../Security/Cryptography/MLKemBaseTests.cs | 32 +++++ .../Cryptography/MLKemContractTests.cs | 129 ++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs index 0613e5cb878054..6fb8630bc1bed9 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs @@ -532,6 +532,38 @@ public void ExportEncryptedPkcs8PrivateKey_Seed_Roundtrip() }); } + [Fact] + public void ExportEncryptedPkcs8PrivateKey_EncapsulationKey_Fails() + { + using MLKem kem = ImportEncapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512EncapsulationKey); + + Assert.Throws(() => DoTryUntilDone((Span destination, out int bytesWritten) => + kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), + s_aes128Pbe, + destination, + out bytesWritten))); + + Assert.Throws(() => DoTryUntilDone((Span destination, out int bytesWritten) => + kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + destination, + out bytesWritten))); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe)); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), + s_aes128Pbe)); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe)); + } + private static void AssertExportPkcs8PrivateKey(MLKem kem, Action callback) { callback(DoTryUntilDone(kem.TryExportPkcs8PrivateKey)); diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs index a057b40d6e37e9..51cad0e341899f 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs @@ -10,6 +10,8 @@ namespace System.Security.Cryptography.Tests { public static class MLKemContractTests { + private static readonly PbeParameters s_aes128Pbe = new(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 2); + [Fact] public static void Constructor_ThrowsForNullAlgorithm() { @@ -904,6 +906,133 @@ public static void ExportPkcs8PrivateKey_Disposed() Assert.Throws(() => kem.TryExportPkcs8PrivateKey(new byte[512], out _)); } + [Fact] + public static void ExportEncryptedPkcs8PrivateKey_Disposed() + { + MLKemContract kem = new(MLKemAlgorithm.MLKem512); + kem.Dispose(); + Assert.Throws(() => kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), + s_aes128Pbe, + new byte[2048], + out _)); + + Assert.Throws(() => kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + new byte[2048], + out _)); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe)); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), + s_aes128Pbe)); + + Assert.Throws(() => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void TryExportEncryptedPkcs8PrivateKey_ExportsPkcs8(bool useCharPassword) + { + using MLKemContract kem = new(MLKemAlgorithm.MLKem512) + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.AsSpan().TryCopyTo(destination)) + { + bytesWritten = MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.Length; + return true; + } + + Assert.Fail("Initial buffer was not correctly sized."); + bytesWritten = 0; + return false; + } + }; + + byte[] buffer = new byte[2048]; + bool success; + int written; + + if (useCharPassword) + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe, + buffer, + out written); + } + else + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + buffer, + out written); + } + + AssertExtensions.TrueExpression(success); + AssertExtensions.GreaterThan(written, 0); + Assert.Equal(1, kem.TryExportPkcs8PrivateKeyCoreCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void TryExportEncryptedPkcs8PrivateKey_InnerBuffer_LargePkcs8(bool useCharPassword) + { + using MLKemContract kem = new(MLKemAlgorithm.MLKem512); + kem.OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (kem.TryExportPkcs8PrivateKeyCoreCount < 2) + { + bytesWritten = 0; + return false; + } + + if (MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.AsSpan().TryCopyTo(destination)) + { + bytesWritten = MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.Length; + return true; + } + + bytesWritten = 0; + return false; + }; + + byte[] buffer = new byte[2048]; + bool success; + int written; + + if (useCharPassword) + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe, + buffer, + out written); + } + else + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + buffer, + out written); + } + + AssertExtensions.TrueExpression(success); + AssertExtensions.GreaterThan(written, 0); + Assert.Equal(2, kem.TryExportPkcs8PrivateKeyCoreCount); + } + private static string MapAlgorithmOid(MLKemAlgorithm algorithm) { if (algorithm == MLKemAlgorithm.MLKem512) From 5300e77b301528ac1a5b57cfa66f27c0cb2033a2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 5 Apr 2025 00:26:53 -0400 Subject: [PATCH 4/6] Round out test coverage --- .../Security/Cryptography/MLKemBaseTests.cs | 97 ++++++++++++++-- .../Cryptography/MLKemContractTests.cs | 108 ++++++++++++++++++ .../Microsoft.Bcl.Cryptography.Tests.csproj | 69 +++++++++++ 3 files changed, 264 insertions(+), 10 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs index 6fb8630bc1bed9..bc37227f5d5f05 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs @@ -2,10 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Formats.Asn1; +using System.Security.Cryptography.Asn1; using System.Text; using Microsoft.DotNet.XUnitExtensions; using Test.Cryptography; using Xunit; +using Xunit.Sdk; namespace System.Security.Cryptography.Tests { @@ -564,6 +567,28 @@ public void ExportEncryptedPkcs8PrivateKey_EncapsulationKey_Fails() s_aes128Pbe)); } + [Theory] + [MemberData(nameof(ExportPkcs8Parameters))] + public void ExportEncryptedPkcs8PrivateKey_PbeParameters(PbeParameters pbeParameters) + { + using MLKem kem = ImportPrivateSeed(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512PrivateSeed); + AssertEncryptedExportPkcs8PrivateKey(kem, MLKemTestData.EncryptedPrivateKeyPassword, pbeParameters, pkcs8 => + { + AssertEncryptedPkcs8PrivateKeyContents(pbeParameters, pkcs8); + }); + } + + public static IEnumerable ExportPkcs8Parameters + { + get + { + yield return new[] { new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 42) }; + yield return new[] { new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA512, 43) }; + yield return new[] { new PbeParameters(PbeEncryptionAlgorithm.Aes192Cbc, HashAlgorithmName.SHA384, 44) }; + yield return new[] { new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 24) }; + } + } + private static void AssertExportPkcs8PrivateKey(MLKem kem, Action callback) { callback(DoTryUntilDone(kem.TryExportPkcs8PrivateKey)); @@ -587,18 +612,23 @@ private static void AssertEncryptedExportPkcs8PrivateKey( out bytesWritten); })); - callback(DoTryUntilDone((Span destination, out int bytesWritten) => - { - return kem.TryExportEncryptedPkcs8PrivateKey( - new ReadOnlySpan(passwordBytes), - pbeParameters, - destination, - out bytesWritten); - })); - callback(kem.ExportEncryptedPkcs8PrivateKey(password, pbeParameters)); callback(kem.ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters)); - callback(kem.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(passwordBytes), pbeParameters)); + + // PKCS12 PBE requires char-passwords, so don't run byte-password callbacks. + if (pbeParameters.EncryptionAlgorithm != PbeEncryptionAlgorithm.TripleDes3KeyPkcs12) + { + callback(DoTryUntilDone((Span destination, out int bytesWritten) => + { + return kem.TryExportEncryptedPkcs8PrivateKey( + new ReadOnlySpan(passwordBytes), + pbeParameters, + destination, + out bytesWritten); + })); + + callback(kem.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(passwordBytes), pbeParameters)); + } } private delegate bool TryExportFunc(Span destination, out int bytesWritten); @@ -647,6 +677,53 @@ private static void AssertSubjectPublicKeyInfo(MLKem kem, bool useTryExport, Rea AssertExtensions.SequenceEqual(encapsulatorSharedSecret, decapsulatedSharedSecret); } + private static void AssertEncryptedPkcs8PrivateKeyContents(PbeParameters pbeParameters, ReadOnlyMemory contents) + { + EncryptedPrivateKeyInfoAsn epki = EncryptedPrivateKeyInfoAsn.Decode(contents, AsnEncodingRules.BER); + AlgorithmIdentifierAsn algorithmIdentifier = epki.EncryptionAlgorithm; + + if (pbeParameters.EncryptionAlgorithm == PbeEncryptionAlgorithm.TripleDes3KeyPkcs12) + { + // pbeWithSHA1And3-KeyTripleDES-CBC + Assert.Equal("1.2.840.113549.1.12.1.3", algorithmIdentifier.Algorithm); + PBEParameter pbeParameterAsn = PBEParameter.Decode(algorithmIdentifier.Parameters.Value, AsnEncodingRules.BER); + + Assert.Equal(pbeParameters.IterationCount, pbeParameterAsn.IterationCount); + } + else + { + Assert.Equal("1.2.840.113549.1.5.13", algorithmIdentifier.Algorithm); // PBES2 + PBES2Params pbes2Params = PBES2Params.Decode(algorithmIdentifier.Parameters.Value, AsnEncodingRules.BER); + Assert.Equal("1.2.840.113549.1.5.12", pbes2Params.KeyDerivationFunc.Algorithm); // PBKDF2 + Pbkdf2Params pbkdf2Params = Pbkdf2Params.Decode( + pbes2Params.KeyDerivationFunc.Parameters.Value, + AsnEncodingRules.BER); + string expectedEncryptionOid = pbeParameters.EncryptionAlgorithm switch + { + PbeEncryptionAlgorithm.Aes128Cbc => "2.16.840.1.101.3.4.1.2", + PbeEncryptionAlgorithm.Aes192Cbc => "2.16.840.1.101.3.4.1.22", + PbeEncryptionAlgorithm.Aes256Cbc => "2.16.840.1.101.3.4.1.42", + _ => throw new CryptographicException(), + }; + + Assert.Equal(pbeParameters.IterationCount, pbkdf2Params.IterationCount); + Assert.Equal(pbeParameters.HashAlgorithm, GetHashAlgorithmFromPbkdf2Params(pbkdf2Params)); + Assert.Equal(expectedEncryptionOid, pbes2Params.EncryptionScheme.Algorithm); + } + } + + private static HashAlgorithmName GetHashAlgorithmFromPbkdf2Params(Pbkdf2Params pbkdf2Params) + { + return pbkdf2Params.Prf.Algorithm switch + { + "1.2.840.113549.2.7" => HashAlgorithmName.SHA1, + "1.2.840.113549.2.9" => HashAlgorithmName.SHA256, + "1.2.840.113549.2.10" => HashAlgorithmName.SHA384, + "1.2.840.113549.2.11" => HashAlgorithmName.SHA512, + string other => throw new XunitException($"Unknown hash algorithm OID '{other}'."), + }; + } + public record MLKemTestDecapsulationVector(MLKemAlgorithm Algorithm, string EncapsulationKey, string DecapsulationKey, string Ciphertext, string SharedSecret); public static IEnumerable MLKemDecapsulationTestVectors diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs index 51cad0e341899f..d4b24f7cdb42c4 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs @@ -1033,6 +1033,114 @@ public static void TryExportEncryptedPkcs8PrivateKey_InnerBuffer_LargePkcs8(bool Assert.Equal(2, kem.TryExportPkcs8PrivateKeyCoreCount); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void TryExportEncryptedPkcs8PrivateKey_DestinationTooSmall(bool useCharPassword) + { + byte[] buffer = new byte[3]; + using MLKemContract kem = new(MLKemAlgorithm.MLKem512) + { + OnTryExportPkcs8PrivateKeyCore = (Span destination, out int bytesWritten) => + { + if (MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.AsSpan().TryCopyTo(destination)) + { + bytesWritten = MLKemTestData.IetfMlKem512PrivateKeyExpandedKey.Length; + return true; + } + + bytesWritten = 0; + return false; + } + }; + + bool success; + int written; + + if (useCharPassword) + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + s_aes128Pbe, + buffer, + out written); + } + else + { + success = kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + s_aes128Pbe, + buffer, + out written); + } + + AssertExtensions.FalseExpression(success); + Assert.Equal(0, written); + } + + [Fact] + public static void ExportPkcs8PrivateKey_ValidatesPbeParameters_Bad3DESHash() + { + byte[] buffer = new byte[2048]; + PbeParameters pbeParameters = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA256, 3); + using MLKemContract kem = new(MLKemAlgorithm.MLKem512); + Assert.Throws(() => + kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, + pbeParameters, + buffer, + out _)); + Assert.Throws(() => + kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + pbeParameters, + buffer, + out _)); + Assert.Throws(() => + kem.ExportEncryptedPkcs8PrivateKey(MLKemTestData.EncryptedPrivateKeyPassword, pbeParameters)); + Assert.Throws(() => + kem.ExportEncryptedPkcs8PrivateKey(MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), pbeParameters)); + Assert.Throws(() => + kem.ExportEncryptedPkcs8PrivateKey(MLKemTestData.EncryptedPrivateKeyPasswordBytes, pbeParameters)); + } + + [Fact] + public static void ExportPkcs8PrivateKey_ValidatesPbeParameters_3DESRequiresChar() + { + byte[] buffer = new byte[2048]; + PbeParameters pbeParameters = new(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 3); + using MLKemContract kem = new(MLKemAlgorithm.MLKem512); + Assert.Throws(() => + kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, + pbeParameters, + buffer, + out _)); + Assert.Throws(() => + kem.ExportEncryptedPkcs8PrivateKey(MLKemTestData.EncryptedPrivateKeyPasswordBytes, pbeParameters)); + } + + [Fact] + public static void ExportPkcs8PrivateKey_NullArgs() + { + byte[] buffer = new byte[2048]; + using MLKemContract kem = new(MLKemAlgorithm.MLKem512); + AssertExtensions.Throws("pbeParameters", () => kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, pbeParameters: null, buffer, out _)); + AssertExtensions.Throws("pbeParameters", () => kem.TryExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, pbeParameters: null, buffer, out _)); + + AssertExtensions.Throws("pbeParameters", () => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword, pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPassword.AsSpan(), pbeParameters: null)); + AssertExtensions.Throws("pbeParameters", () => kem.ExportEncryptedPkcs8PrivateKey( + MLKemTestData.EncryptedPrivateKeyPasswordBytes, pbeParameters: null)); + + AssertExtensions.Throws("password", () => kem.ExportEncryptedPkcs8PrivateKey( + (string)null, s_aes128Pbe)); + } + private static string MapAlgorithmOid(MLKemAlgorithm algorithm) { if (algorithm == MLKemAlgorithm.MLKem512) diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 545da1c9adae58..4d02d279cc6787 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -9,7 +9,76 @@ true + + + + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml.cs + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.manual.cs + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml.cs + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.manual.cs + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\PBEParameter.xml + + + Common\System\Security\Cryptography\Asn1\PBEParameter.xml.cs + Common\System\Security\Cryptography\Asn1\PBEParameter.xml + + + Common\System\Security\Cryptography\Asn1\PBES2Params.xml + + + Common\System\Security\Cryptography\Asn1\PBES2Params.xml.cs + Common\System\Security\Cryptography\Asn1\PBES2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml.cs + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml.cs + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml + + + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml + Date: Sat, 5 Apr 2025 00:53:17 -0400 Subject: [PATCH 5/6] Disable tests on Browser where appropriate --- .../tests/System/Security/Cryptography/MLKemContractTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs index d4b24f7cdb42c4..b3bf461d74b74b 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs @@ -939,6 +939,7 @@ public static void ExportEncryptedPkcs8PrivateKey_Disposed() [Theory] [InlineData(true)] [InlineData(false)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] public static void TryExportEncryptedPkcs8PrivateKey_ExportsPkcs8(bool useCharPassword) { using MLKemContract kem = new(MLKemAlgorithm.MLKem512) @@ -986,6 +987,7 @@ public static void TryExportEncryptedPkcs8PrivateKey_ExportsPkcs8(bool useCharPa [Theory] [InlineData(true)] [InlineData(false)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] public static void TryExportEncryptedPkcs8PrivateKey_InnerBuffer_LargePkcs8(bool useCharPassword) { using MLKemContract kem = new(MLKemAlgorithm.MLKem512); @@ -1036,6 +1038,7 @@ public static void TryExportEncryptedPkcs8PrivateKey_InnerBuffer_LargePkcs8(bool [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser does not support symmetric encryption")] public static void TryExportEncryptedPkcs8PrivateKey_DestinationTooSmall(bool useCharPassword) { byte[] buffer = new byte[3]; From f192001292d2e5c9ae7c0cbaf168788fb7e65367 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 5 Apr 2025 04:44:01 -0400 Subject: [PATCH 6/6] Code review feedback --- .../src/System/Security/Cryptography/MLKem.cs | 78 +++++-------------- 1 file changed, 21 insertions(+), 57 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs index d62d4bfa2af67c..35b3e19ac29d33 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs @@ -933,20 +933,11 @@ public bool TryExportEncryptedPkcs8PrivateKey( PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); ThrowIfDisposed(); - AsnWriter? writer = null; - - try - { - writer = TryExportEncryptedPkcs8PrivateKeyCore( - password, - pbeParameters, - KeyFormatHelper.WriteEncryptedPkcs8); - return writer.TryEncode(destination, out bytesWritten); - } - finally - { - writer?.Reset(); - } + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); } /// @@ -995,20 +986,11 @@ public bool TryExportEncryptedPkcs8PrivateKey( PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); ThrowIfDisposed(); - AsnWriter? writer = null; - - try - { - writer = TryExportEncryptedPkcs8PrivateKeyCore( - passwordBytes, - pbeParameters, - KeyFormatHelper.WriteEncryptedPkcs8); - return writer.TryEncode(destination, out bytesWritten); - } - finally - { - writer?.Reset(); - } + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.TryEncode(destination, out bytesWritten); } /// @@ -1044,20 +1026,11 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, P PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes); ThrowIfDisposed(); - AsnWriter? writer = null; - - try - { - writer = TryExportEncryptedPkcs8PrivateKeyCore( - passwordBytes, - pbeParameters, - KeyFormatHelper.WriteEncryptedPkcs8); - return writer.Encode(); - } - finally - { - writer?.Reset(); - } + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + passwordBytes, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); } /// @@ -1093,20 +1066,11 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbePar PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty); ThrowIfDisposed(); - AsnWriter? writer = null; - - try - { - writer = TryExportEncryptedPkcs8PrivateKeyCore( - password, - pbeParameters, - KeyFormatHelper.WriteEncryptedPkcs8); - return writer.Encode(); - } - finally - { - writer?.Reset(); - } + AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore( + password, + pbeParameters, + KeyFormatHelper.WriteEncryptedPkcs8); + return writer.Encode(); } /// @@ -1708,7 +1672,7 @@ private static void ThrowIfNull( #endif } - private AsnWriter TryExportEncryptedPkcs8PrivateKeyCore( + private AsnWriter ExportEncryptedPkcs8PrivateKeyCore( ReadOnlySpan password, PbeParameters pbeParameters, WriteEncryptedPkcs8Func encryptor)