diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.MLDsa.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.MLDsa.cs
index c38270bad490d1..2f41b60b6d1628 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.MLDsa.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.MLDsa.cs
@@ -24,25 +24,35 @@ internal enum PalMLDsaAlgorithmId
[LibraryImport(Libraries.CryptoNative)]
private static partial int CryptoNative_MLDsaGetPalId(
SafeEvpPKeyHandle mldsa,
- out PalMLDsaAlgorithmId mldsaId);
-
- internal static PalMLDsaAlgorithmId MLDsaGetPalId(SafeEvpPKeyHandle key)
+ out PalMLDsaAlgorithmId mldsaId,
+ out int hasSeed,
+ out int hasSecretKey);
+
+ internal static PalMLDsaAlgorithmId MLDsaGetPalId(
+ SafeEvpPKeyHandle key,
+ out bool hasSeed,
+ out bool hasSecretKey)
{
const int Success = 1;
+ const int Yes = 1;
const int Fail = 0;
- int result = CryptoNative_MLDsaGetPalId(key, out PalMLDsaAlgorithmId mldsaId);
-
- return result switch
- {
- Success => mldsaId,
- Fail => throw CreateOpenSslCryptographicException(),
- int other => throw FailThrow(other),
- };
+ int result = CryptoNative_MLDsaGetPalId(
+ key,
+ out PalMLDsaAlgorithmId mldsaId,
+ out int pKeyHasSeed,
+ out int pKeyHasSecretKey);
- static Exception FailThrow(int result)
+ switch (result)
{
- Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_MLDsaGetPalId)}.");
- return new CryptographicException();
+ case Success:
+ hasSeed = pKeyHasSeed == Yes;
+ hasSecretKey = pKeyHasSecretKey == Yes;
+ return mldsaId;
+ case Fail:
+ throw CreateOpenSslCryptographicException();
+ default:
+ Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_MLDsaGetPalId)}.");
+ throw new CryptographicException();
}
}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml
new file mode 100644
index 00000000000000..439609c36fc65f
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml.cs
new file mode 100644
index 00000000000000..a1c2ec4fd75661
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyAsn.xml.cs
@@ -0,0 +1,150 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma warning disable SA1028 // ignore whitespace warnings for generated code
+using System;
+using System.Formats.Asn1;
+using System.Runtime.InteropServices;
+
+namespace System.Security.Cryptography.Asn1
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal partial struct MLDsaPrivateKeyAsn
+ {
+ internal ReadOnlyMemory? Seed;
+ internal ReadOnlyMemory? ExpandedKey;
+ internal System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn? Both;
+
+#if DEBUG
+ static MLDsaPrivateKeyAsn()
+ {
+ var usedTags = new System.Collections.Generic.Dictionary();
+ Action ensureUniqueTag = (tag, fieldName) =>
+ {
+ if (usedTags.TryGetValue(tag, out string? existing))
+ {
+ throw new InvalidOperationException($"Tag '{tag}' is in use by both '{existing}' and '{fieldName}'");
+ }
+
+ usedTags.Add(tag, fieldName);
+ };
+
+ ensureUniqueTag(new Asn1Tag(TagClass.ContextSpecific, 0), "Seed");
+ ensureUniqueTag(Asn1Tag.PrimitiveOctetString, "ExpandedKey");
+ ensureUniqueTag(Asn1Tag.Sequence, "Both");
+ }
+#endif
+
+ internal readonly void Encode(AsnWriter writer)
+ {
+ bool wroteValue = false;
+
+ if (Seed.HasValue)
+ {
+ if (wroteValue)
+ throw new CryptographicException();
+
+ writer.WriteOctetString(Seed.Value.Span, new Asn1Tag(TagClass.ContextSpecific, 0));
+ wroteValue = true;
+ }
+
+ if (ExpandedKey.HasValue)
+ {
+ if (wroteValue)
+ throw new CryptographicException();
+
+ writer.WriteOctetString(ExpandedKey.Value.Span);
+ wroteValue = true;
+ }
+
+ if (Both.HasValue)
+ {
+ if (wroteValue)
+ throw new CryptographicException();
+
+ Both.Value.Encode(writer);
+ wroteValue = true;
+ }
+
+ if (!wroteValue)
+ {
+ throw new CryptographicException();
+ }
+ }
+
+ internal static MLDsaPrivateKeyAsn Decode(ReadOnlyMemory encoded, AsnEncodingRules ruleSet)
+ {
+ try
+ {
+ AsnValueReader reader = new AsnValueReader(encoded.Span, ruleSet);
+
+ DecodeCore(ref reader, encoded, out MLDsaPrivateKeyAsn decoded);
+ reader.ThrowIfNotEmpty();
+ return decoded;
+ }
+ catch (AsnContentException e)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
+ }
+ }
+
+ internal static void Decode(ref AsnValueReader reader, ReadOnlyMemory rebind, out MLDsaPrivateKeyAsn decoded)
+ {
+ try
+ {
+ DecodeCore(ref reader, rebind, out decoded);
+ }
+ catch (AsnContentException e)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
+ }
+ }
+
+ private static void DecodeCore(ref AsnValueReader reader, ReadOnlyMemory rebind, out MLDsaPrivateKeyAsn decoded)
+ {
+ decoded = default;
+ Asn1Tag tag = reader.PeekTag();
+ ReadOnlySpan rebindSpan = rebind.Span;
+ int offset;
+ ReadOnlySpan tmpSpan;
+
+ if (tag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 0)))
+ {
+
+ if (reader.TryReadPrimitiveOctetString(out tmpSpan, new Asn1Tag(TagClass.ContextSpecific, 0)))
+ {
+ decoded.Seed = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
+ }
+ else
+ {
+ decoded.Seed = reader.ReadOctetString(new Asn1Tag(TagClass.ContextSpecific, 0));
+ }
+
+ }
+ else if (tag.HasSameClassAndValue(Asn1Tag.PrimitiveOctetString))
+ {
+
+ if (reader.TryReadPrimitiveOctetString(out tmpSpan))
+ {
+ decoded.ExpandedKey = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
+ }
+ else
+ {
+ decoded.ExpandedKey = reader.ReadOctetString();
+ }
+
+ }
+ else if (tag.HasSameClassAndValue(Asn1Tag.Sequence))
+ {
+ System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn tmpBoth;
+ System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn.Decode(ref reader, rebind, out tmpBoth);
+ decoded.Both = tmpBoth;
+
+ }
+ else
+ {
+ throw new CryptographicException();
+ }
+ }
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml
new file mode 100644
index 00000000000000..8f19fe890289f7
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml.cs
new file mode 100644
index 00000000000000..bd3535e6cd57a5
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/MLDsaPrivateKeyBothAsn.xml.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma warning disable SA1028 // ignore whitespace warnings for generated code
+using System;
+using System.Formats.Asn1;
+using System.Runtime.InteropServices;
+
+namespace System.Security.Cryptography.Asn1
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal partial struct MLDsaPrivateKeyBothAsn
+ {
+ internal ReadOnlyMemory Seed;
+ internal ReadOnlyMemory ExpandedKey;
+
+ internal readonly void Encode(AsnWriter writer)
+ {
+ Encode(writer, Asn1Tag.Sequence);
+ }
+
+ internal readonly void Encode(AsnWriter writer, Asn1Tag tag)
+ {
+ writer.PushSequence(tag);
+
+ writer.WriteOctetString(Seed.Span);
+ writer.WriteOctetString(ExpandedKey.Span);
+ writer.PopSequence(tag);
+ }
+
+ internal static MLDsaPrivateKeyBothAsn Decode(ReadOnlyMemory encoded, AsnEncodingRules ruleSet)
+ {
+ return Decode(Asn1Tag.Sequence, encoded, ruleSet);
+ }
+
+ internal static MLDsaPrivateKeyBothAsn Decode(Asn1Tag expectedTag, ReadOnlyMemory encoded, AsnEncodingRules ruleSet)
+ {
+ try
+ {
+ AsnValueReader reader = new AsnValueReader(encoded.Span, ruleSet);
+
+ DecodeCore(ref reader, expectedTag, encoded, out MLDsaPrivateKeyBothAsn decoded);
+ reader.ThrowIfNotEmpty();
+ return decoded;
+ }
+ catch (AsnContentException e)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
+ }
+ }
+
+ internal static void Decode(ref AsnValueReader reader, ReadOnlyMemory rebind, out MLDsaPrivateKeyBothAsn decoded)
+ {
+ Decode(ref reader, Asn1Tag.Sequence, rebind, out decoded);
+ }
+
+ internal static void Decode(ref AsnValueReader reader, Asn1Tag expectedTag, ReadOnlyMemory rebind, out MLDsaPrivateKeyBothAsn decoded)
+ {
+ try
+ {
+ DecodeCore(ref reader, expectedTag, rebind, out decoded);
+ }
+ catch (AsnContentException e)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
+ }
+ }
+
+ private static void DecodeCore(ref AsnValueReader reader, Asn1Tag expectedTag, ReadOnlyMemory rebind, out MLDsaPrivateKeyBothAsn decoded)
+ {
+ decoded = default;
+ AsnValueReader sequenceReader = reader.ReadSequence(expectedTag);
+ ReadOnlySpan rebindSpan = rebind.Span;
+ int offset;
+ ReadOnlySpan tmpSpan;
+
+
+ if (sequenceReader.TryReadPrimitiveOctetString(out tmpSpan))
+ {
+ decoded.Seed = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
+ }
+ else
+ {
+ decoded.Seed = sequenceReader.ReadOctetString();
+ }
+
+
+ if (sequenceReader.TryReadPrimitiveOctetString(out tmpSpan))
+ {
+ decoded.ExpandedKey = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
+ }
+ else
+ {
+ decoded.ExpandedKey = sequenceReader.ReadOctetString();
+ }
+
+
+ sequenceReader.ThrowIfNotEmpty();
+ }
+ }
+}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs
index 24e369c75e94c8..a4492f5c473cd9 100644
--- a/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs
@@ -8,19 +8,13 @@
using System.Security.Cryptography.Asn1;
using Internal.Cryptography;
-#pragma warning disable CA1510, CA1513
-
-// The type being internal is making unused parameter warnings fire for
-// not-implemented methods. Suppress those warnings.
-#pragma warning disable IDE0060
-
namespace System.Security.Cryptography
{
///
/// Represents an ML-DSA key.
///
///
- /// Developers are encouraged to program against the MLDsa base class,
+ /// Developers are encouraged to program against the base class,
/// rather than any specific derived class.
/// The derived classes are intended for interop with the underlying system
/// cryptographic libraries.
@@ -33,10 +27,17 @@ public abstract partial class MLDsa : IDisposable
#pragma warning restore SA1001
#endif
{
+ private static readonly string[] s_knownOids =
+ [
+ Oids.MLDsa44,
+ Oids.MLDsa65,
+ Oids.MLDsa87,
+ ];
+
private const int MaxContextLength = 255;
///
- /// Gets the specific ML-DSA algorithm for this key.
+ /// Gets the specific ML-DSA algorithm for this key.
///
public MLDsaAlgorithm Algorithm { get; }
private bool _disposed;
@@ -54,13 +55,7 @@ protected MLDsa(MLDsaAlgorithm algorithm)
Algorithm = algorithm;
}
- protected void ThrowIfDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(typeof(MLDsa).FullName);
- }
- }
+ private protected void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, typeof(MLDsa));
///
/// Gets a value indicating whether the current platform supports ML-DSA.
@@ -71,7 +66,7 @@ protected void ThrowIfDisposed()
public static bool IsSupported { get; } = MLDsaImplementation.SupportsAny();
///
- /// Releases all resources used by the class.
+ /// Releases all resources used by the class.
///
public void Dispose()
{
@@ -187,7 +182,7 @@ public bool VerifyData(ReadOnlySpan data, ReadOnlySpan signature, Re
// TODO: VerifyPreHash
///
- /// Exports the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format.
+ /// Exports the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format.
///
///
/// A byte array containing the X.509 SubjectPublicKeyInfo representation of the public-key portion of this key.
@@ -207,8 +202,8 @@ public byte[] ExportSubjectPublicKeyInfo()
}
///
- /// Attempts to export the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format
- /// into the provided buffer.
+ /// Attempts to export the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format
+ /// into the provided buffer.
///
///
/// The buffer to receive the X.509 SubjectPublicKeyInfo value.
@@ -236,8 +231,8 @@ public bool TryExportSubjectPublicKeyInfo(Span destination, out int bytesW
}
///
- /// Exports the public-key portion of the current key in a PEM-encoded representation of
- /// the X.509 SubjectPublicKeyInfo format.
+ /// Exports the public-key portion of the current key in a PEM-encoded representation of
+ /// the X.509 SubjectPublicKeyInfo format.
///
///
/// A string containing the PEM-encoded representation of the X.509 SubjectPublicKeyInfo
@@ -254,11 +249,13 @@ public string ExportSubjectPublicKeyInfoPem()
ThrowIfDisposed();
AsnWriter writer = ExportSubjectPublicKeyInfoCore();
- return writer.Encode(static span => PemEncoding.WriteString(PemLabels.SpkiPublicKey, span));
+
+ // SPKI does not contain sensitive data.
+ return EncodeAsnWriterToPem(PemLabels.SpkiPublicKey, writer, clear: false);
}
///
- /// Exports the current key in the PKCS#8 PrivateKeyInfo format.
+ /// Exports the current key in the PKCS#8 PrivateKeyInfo format.
///
///
/// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
@@ -277,17 +274,12 @@ public byte[] ExportPkcs8PrivateKey()
{
ThrowIfDisposed();
- // TODO: When defining this, provide a virtual method whose base implementation is to
- // call ExportPrivateSeed and/or ExportSecretKey, and then assemble the result,
- // but allow the derived class to override it in case they need to implement those
- // others in terms of the PKCS8 export from the underlying provider.
-
- throw new NotImplementedException("The PKCS#8 format is still under debate");
+ return ExportPkcs8PrivateKeyCallback(static pkcs8 => pkcs8.ToArray());
}
///
- /// Attempts to export the current key in the PKCS#8 PrivateKeyInfo format
- /// into the provided buffer.
+ /// Attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+ /// into the provided buffer.
///
///
/// The buffer to receive the PKCS#8 PrivateKeyInfo value.
@@ -310,13 +302,49 @@ public bool TryExportPkcs8PrivateKey(Span destination, out int bytesWritte
{
ThrowIfDisposed();
- // TODO: Once the minimum size of a PKCS#8 export is known, add an early return false.
+ // A private key export with no attributes has at least 12 bytes overhead so a buffer smaller than that cannot hold a
+ // PKCS#8 encoded key. If we happen to get a buffer smaller than that, it won't export.
+ int minimumPossiblePkcs8MLDsaKey =
+ 2 + // PrivateKeyInfo Sequence
+ 3 + // Version Integer
+ 2 + // AlgorithmIdentifier Sequence
+ 3 + // AlgorithmIdentifier OID value, undervalued to be safe
+ 2 + // Secret key Octet String prefix, undervalued to be safe
+ Algorithm.PrivateSeedSizeInBytes;
+
+ if (destination.Length < minimumPossiblePkcs8MLDsaKey)
+ {
+ bytesWritten = 0;
+ return false;
+ }
- throw new NotImplementedException("The PKCS#8 format is still under debate");
+ return TryExportPkcs8PrivateKeyCore(destination, out bytesWritten);
}
///
- /// Exports the current key in a PEM-encoded representation of the PKCS#8 PrivateKeyInfo format.
+ /// When overridden in a derived class, attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+ /// into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the PKCS#8 PrivateKeyInfo value.
+ ///
+ ///
+ /// When this method returns, contains the number of bytes written to the buffer.
+ ///
+ ///
+ /// if was large enough to hold the result;
+ /// otherwise, .
+ ///
+ ///
+ /// This instance has been disposed.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ protected abstract bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten);
+
+ ///
+ /// Exports the current key in a PEM-encoded representation of the PKCS#8 PrivateKeyInfo format.
///
///
/// A string containing the PEM-encoded representation of the PKCS#8 PrivateKeyInfo
@@ -332,11 +360,11 @@ public string ExportPkcs8PrivateKeyPem()
{
ThrowIfDisposed();
- throw new NotImplementedException("The PKCS#8 format is still under debate");
+ return ExportPkcs8PrivateKeyCallback(static pkcs8 => PemEncoding.WriteString(PemLabels.Pkcs8PrivateKey, pkcs8));
}
///
- /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a char-based password.
+ /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a char-based password.
///
///
/// The password to use when encrypting the key material.
@@ -345,12 +373,17 @@ public string ExportPkcs8PrivateKeyPem()
/// The password-based encryption (PBE) parameters to use when encrypting the key material.
///
///
- /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of the this key.
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -359,10 +392,10 @@ public string ExportPkcs8PrivateKeyPem()
///
public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbeParameters pbeParameters)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty);
ThrowIfDisposed();
- // TODO: Validation on pbeParameters.
-
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(password, pbeParameters);
try
@@ -376,7 +409,7 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbePar
}
///
- /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a byte-based password.
+ /// Exports the current key in the PKCS#8 EncryptedPrivateKeyInfo format with a byte-based password.
///
///
/// The bytes to use as a password when encrypting the key material.
@@ -385,14 +418,19 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbePar
/// The password-based encryption (PBE) parameters to use when encrypting the key material.
///
///
- /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ /// A byte array containing the PKCS#8 EncryptedPrivateKeyInfo representation of the this key.
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
/// specifies a KDF that requires a char-based password.
/// -or-
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -401,10 +439,10 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan password, PbePar
///
public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, PbeParameters pbeParameters)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes);
ThrowIfDisposed();
- // TODO: Validation on pbeParameters.
-
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(passwordBytes, pbeParameters);
try
@@ -417,9 +455,20 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, P
}
}
+ ///
+ ///
+ /// or is .
+ ///
+ public byte[] ExportEncryptedPkcs8PrivateKey(string password, PbeParameters pbeParameters)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+
+ return ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
+ }
+
///
- /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer,
- /// using a char-based password.
+ /// 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.
@@ -435,12 +484,18 @@ public byte[] ExportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, P
/// This parameter is treated as uninitialized.
///
///
- /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ /// if was large enough to hold the result;
+ /// otherwise, .
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -453,6 +508,8 @@ public bool TryExportEncryptedPkcs8PrivateKey(
Span destination,
out int bytesWritten)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty);
ThrowIfDisposed();
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(password, pbeParameters);
@@ -468,8 +525,8 @@ public bool TryExportEncryptedPkcs8PrivateKey(
}
///
- /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer,
- /// using a byte-based password.
+ /// Attempts to export the current key in the PKCS#8 EncryptedPrivateKeyInfo format into a provided buffer,
+ /// using a byte-based password.
///
///
/// The bytes to use as a password when encrypting the key material.
@@ -485,14 +542,20 @@ public bool TryExportEncryptedPkcs8PrivateKey(
/// This parameter is treated as uninitialized.
///
///
- /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ /// if was large enough to hold the result;
+ /// otherwise, .
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
/// specifies a KDF that requires a char-based password.
/// -or-
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -505,6 +568,8 @@ public bool TryExportEncryptedPkcs8PrivateKey(
Span destination,
out int bytesWritten)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes);
ThrowIfDisposed();
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(passwordBytes, pbeParameters);
@@ -519,23 +584,43 @@ public bool TryExportEncryptedPkcs8PrivateKey(
}
}
+ ///
+ ///
+ /// or is .
+ ///
+ public bool TryExportEncryptedPkcs8PrivateKey(
+ string password,
+ PbeParameters pbeParameters,
+ Span destination,
+ out int bytesWritten)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+
+ return TryExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters, destination, out bytesWritten);
+ }
+
///
- /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo representation of this key,
- /// using a char-based password.
+ /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo
+ /// representation of this key, using a char-based password.
///
///
- /// The bytes to use as a password when encrypting the key material.
+ /// 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 PrivateKeyInfo representation of the this key.
+ /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo.
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -546,25 +631,19 @@ public string ExportEncryptedPkcs8PrivateKeyPem(
ReadOnlySpan password,
PbeParameters pbeParameters)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, password, ReadOnlySpan.Empty);
ThrowIfDisposed();
- // TODO: Validation on pbeParameters.
-
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(password, pbeParameters);
- try
- {
- return writer.Encode(static span => PemEncoding.WriteString(PemLabels.EncryptedPkcs8PrivateKey, span));
- }
- finally
- {
- writer.Reset();
- }
+ // Skip clear since the data is already encrypted.
+ return EncodeAsnWriterToPem(PemLabels.EncryptedPkcs8PrivateKey, writer, clear: false);
}
///
- /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo representation of this key,
- /// using a byte-based password.
+ /// Exports the current key in a PEM-encoded representation of the PKCS#8 EncryptedPrivateKeyInfo
+ /// representation of this key, using a byte-based password.
///
///
/// The bytes to use as a password when encrypting the key material.
@@ -573,14 +652,19 @@ public string ExportEncryptedPkcs8PrivateKeyPem(
/// The password-based encryption (PBE) parameters to use when encrypting the key material.
///
///
- /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ /// A string containing the PEM-encoded PKCS#8 EncryptedPrivateKeyInfo.
///
+ ///
+ /// is .
+ ///
///
/// This instance has been disposed.
///
///
/// specifies a KDF that requires a char-based password.
/// -or-
+ /// does not represent a valid password-based encryption algorithm.
+ /// -or-
/// This instance only represents a public key.
/// -or-
/// The private key is not exportable.
@@ -591,24 +675,31 @@ public string ExportEncryptedPkcs8PrivateKeyPem(
ReadOnlySpan passwordBytes,
PbeParameters pbeParameters)
{
+ ArgumentNullException.ThrowIfNull(pbeParameters);
+ PasswordBasedEncryption.ValidatePbeParameters(pbeParameters, ReadOnlySpan.Empty, passwordBytes);
ThrowIfDisposed();
- // TODO: Validation on pbeParameters.
-
AsnWriter writer = ExportEncryptedPkcs8PrivateKeyCore(passwordBytes, pbeParameters);
- try
- {
- return writer.Encode(static span => PemEncoding.WriteString(PemLabels.EncryptedPkcs8PrivateKey, span));
- }
- finally
- {
- writer.Reset();
- }
+ // Skip clear since the data is already encrypted.
+ return EncodeAsnWriterToPem(PemLabels.EncryptedPkcs8PrivateKey, writer, clear: false);
+ }
+
+ ///
+ ///
+ /// or is .
+ ///
+ public string ExportEncryptedPkcs8PrivateKeyPem(
+ string password,
+ PbeParameters pbeParameters)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+
+ return ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters);
}
///
- /// Exports the public-key portion of the current key in the FIPS 204 public key format.
+ /// Exports the public-key portion of the current key in the FIPS 204 public key format.
///
///
/// The buffer to receive the public key.
@@ -632,7 +723,7 @@ public int ExportMLDsaPublicKey(Span destination)
}
///
- /// Exports the current key in the FIPS 204 secret key format.
+ /// Exports the current key in the FIPS 204 secret key format.
///
///
/// The buffer to receive the secret key.
@@ -659,7 +750,7 @@ public int ExportMLDsaSecretKey(Span destination)
}
///
- /// Exports the private seed of the current key.
+ /// Exports the private seed of the current key.
///
///
/// The buffer to receive the private seed.
@@ -713,10 +804,10 @@ public static MLDsa GenerateKey(MLDsaAlgorithm algorithm)
}
///
- /// Imports an ML-DSA public key from an X.509 SubjectPublicKeyInfo structure.
+ /// Imports an ML-DSA public key from an X.509 SubjectPublicKeyInfo structure.
///
///
- /// The bytes of an X.509 SubjectPublicKeyInfo structure in the ASN.1-DER encoding.
+ /// The bytes of an X.509 SubjectPublicKeyInfo structure in the ASN.1-DER encoding.
///
///
/// The imported key.
@@ -731,11 +822,20 @@ public static MLDsa GenerateKey(MLDsaAlgorithm algorithm)
///
/// -or-
///
+ /// contains trailing data after the ASN.1 structure.
+ ///
+ /// -or-
+ ///
/// The algorithm-specific import failed.
///
///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
+ ///
public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan source)
{
+ ThrowIfInvalidLength(source);
ThrowIfNotSupported();
unsafe
@@ -747,18 +847,12 @@ public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan source)
AsnValueReader reader = new AsnValueReader(source, AsnEncodingRules.DER);
SubjectPublicKeyInfoAsn.Decode(ref reader, manager.Memory, out SubjectPublicKeyInfoAsn spki);
- MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(spki.Algorithm.Algorithm);
-
- if (algorithm is null)
- {
- throw Helpers.CreateAlgorithmUnknownException(spki.Algorithm.Algorithm);
- }
+ MLDsaAlgorithm algorithm = GetAlgorithmIdentifier(ref spki.Algorithm);
+ ReadOnlySpan publicKey = spki.SubjectPublicKey.Span;
- if (spki.Algorithm.Parameters.HasValue)
+ if (publicKey.Length != algorithm.PublicKeySizeInBytes)
{
- AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
- spki.Algorithm.Encode(writer);
- throw Helpers.CreateAlgorithmUnknownException(writer);
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
}
return MLDsaImplementation.ImportPublicKey(algorithm, spki.SubjectPublicKey.Span);
@@ -767,11 +861,22 @@ public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan source)
}
}
+ ///
+ ///
+ /// is .
+ ///
+ public static MLDsa ImportSubjectPublicKeyInfo(byte[] source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ return ImportSubjectPublicKeyInfo(new ReadOnlySpan(source));
+ }
+
///
- /// Imports an ML-DSA private key from a PKCS#8 PrivateKeyInfo structure.
+ /// Imports an ML-DSA private key from a PKCS#8 PrivateKeyInfo structure.
///
///
- /// The bytes of a PKCS#8 PrivateKeyInfo structure in the ASN.1-DER encoding.
+ /// The bytes of a PKCS#8 PrivateKeyInfo structure in the ASN.1-BER encoding.
///
///
/// The imported key.
@@ -786,40 +891,36 @@ public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan source)
///
/// -or-
///
+ /// contains trailing data after the ASN.1 structure.
+ ///
+ /// -or-
+ ///
/// The algorithm-specific import failed.
///
///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
+ ///
public static MLDsa ImportPkcs8PrivateKey(ReadOnlySpan source)
{
+ ThrowIfInvalidLength(source);
ThrowIfNotSupported();
- unsafe
- {
- fixed (byte* pointer = source)
- {
- using (PointerMemoryManager manager = new(pointer, source.Length))
- {
- AsnValueReader reader = new AsnValueReader(source, AsnEncodingRules.DER);
- PrivateKeyInfoAsn.Decode(ref reader, manager.Memory, out PrivateKeyInfoAsn pki);
-
- MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(pki.PrivateKeyAlgorithm.Algorithm);
-
- if (algorithm is null)
- {
- throw Helpers.CreateAlgorithmUnknownException(pki.PrivateKeyAlgorithm.Algorithm);
- }
+ KeyFormatHelper.ReadPkcs8(s_knownOids, source, MLDsaKeyReader, out int read, out MLDsa dsa);
+ Debug.Assert(read == source.Length);
+ return dsa;
+ }
- if (pki.PrivateKeyAlgorithm.Parameters.HasValue)
- {
- AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
- pki.PrivateKeyAlgorithm.Encode(writer);
- throw Helpers.CreateAlgorithmUnknownException(writer);
- }
+ /// >
+ ///
+ /// is .
+ ///
+ public static MLDsa ImportPkcs8PrivateKey(byte[] source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
- return MLDsaImplementation.ImportPkcs8PrivateKeyValue(algorithm, pki.PrivateKey.Span);
- }
- }
- }
+ return ImportPkcs8PrivateKey(new ReadOnlySpan(source));
}
///
@@ -853,11 +954,20 @@ public static MLDsa ImportPkcs8PrivateKey(ReadOnlySpan source)
///
/// -or-
///
+ /// contains trailing data after the ASN.1 structure.
+ ///
+ /// -or-
+ ///
/// The algorithm-specific import failed.
///
///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
+ ///
public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBytes, ReadOnlySpan source)
{
+ ThrowIfInvalidLength(source);
ThrowIfNotSupported();
return KeyFormatHelper.DecryptPkcs8(
@@ -893,11 +1003,20 @@ public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan passwordBy
///
/// -or-
///
+ /// contains trailing data after the ASN.1 structure.
+ ///
+ /// -or-
+ ///
/// The algorithm-specific import failed.
///
///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
+ ///
public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan password, ReadOnlySpan source)
{
+ ThrowIfInvalidLength(source);
ThrowIfNotSupported();
return KeyFormatHelper.DecryptPkcs8(
@@ -907,8 +1026,20 @@ public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan password,
out _);
}
+ ///
+ ///
+ /// or is .
+ ///
+ public static MLDsa ImportEncryptedPkcs8PrivateKey(string password, byte[] source)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+ ArgumentNullException.ThrowIfNull(source);
+
+ return ImportEncryptedPkcs8PrivateKey(password.AsSpan(), new ReadOnlySpan(source));
+ }
+
///
- /// Imports an ML-DSA key from an RFC 7468 PEM-encoded string.
+ /// Imports an ML-DSA key from an RFC 7468 PEM-encoded string.
///
///
/// The text of the PEM key to import.
@@ -926,77 +1057,205 @@ public static MLDsa ImportEncryptedPkcs8PrivateKey(ReadOnlySpan password,
///
/// An error occurred while importing the key.
///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
+ ///
+ ///
+ ///
+ /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels
+ /// are found, an exception is raised to prevent importing a key when the key is ambiguous.
+ ///
+ ///
+ /// This method supports the following PEM labels:
+ ///
+ /// - PUBLIC KEY
+ /// - PRIVATE KEY
+ ///
+ ///
+ ///
public static MLDsa ImportFromPem(ReadOnlySpan source)
{
ThrowIfNotSupported();
- // TODO: Match the behavior of ECDsa.ImportFromPem.
- // Double-check that the base64-decoded data has no trailing contents.
- throw new NotImplementedException();
+ return PemKeyHelpers.ImportFactoryPem(source, label =>
+ label switch
+ {
+ PemLabels.Pkcs8PrivateKey => ImportPkcs8PrivateKey,
+ PemLabels.SpkiPublicKey => ImportSubjectPublicKeyInfo,
+ _ => null,
+ });
+ }
+
+ ///
+ ///
+ /// is .
+ ///
+ public static MLDsa ImportFromPem(string source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ThrowIfNotSupported();
+
+ return ImportFromPem(source.AsSpan());
}
///
- /// Imports an ML-DSA key from an RFC 7468 PEM-encoded string.
+ /// Imports an ML-DSA key from an encrypted RFC 7468 PEM-encoded string.
///
///
- /// The text of the PEM key to import.
- ///
+ /// The PEM text of the encrypted key to import.
///
- /// The password to use when decrypting the key material.
+ /// The password to use for decrypting the key material.
///
- ///
- /// if the source did not contain a PEM-encoded ML-DSA key;
- /// if the source contains an ML-DSA key and it was successfully imported;
- /// otherwise, an exception is thrown.
- ///
///
- /// contains an encrypted PEM-encoded key.
- /// -or-
- /// contains multiple PEM-encoded ML-DSA keys.
+ ///
+ /// does not contain a PEM-encoded key with a recognized label.
+ ///
/// -or-
- /// contains no PEM-encoded ML-DSA keys.
+ ///
+ /// contains multiple PEM-encoded keys with a recognized label.
+ ///
///
///
- /// An error occurred while importing the key.
+ ///
+ /// The password is incorrect.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// do not represent an ASN.1-BER-encoded PKCS#8 EncryptedPrivateKeyInfo structure.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// indicate the key is for an algorithm other than the algorithm
+ /// represented by this instance.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// represent the key in a format that is not supported.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while importing the key.
+ ///
+ ///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
///
+ ///
+ ///
+ /// When the base-64 decoded contents of indicate an algorithm that uses PBKDF1
+ /// (Password-Based Key Derivation Function 1) or PBKDF2 (Password-Based Key Derivation Function 2),
+ /// the password is converted to bytes via the UTF-8 encoding.
+ ///
+ ///
+ /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels
+ /// are found, an exception is thrown to prevent importing a key when
+ /// the key is ambiguous.
+ ///
+ /// This method supports the ENCRYPTED PRIVATE KEY PEM label.
+ ///
public static MLDsa ImportFromEncryptedPem(ReadOnlySpan source, ReadOnlySpan password)
{
ThrowIfNotSupported();
- // TODO: Match the behavior of ECDsa.ImportFromEncryptedPem.
- throw new NotImplementedException();
+ return PemKeyHelpers.ImportEncryptedFactoryPem(
+ source,
+ password,
+ ImportEncryptedPkcs8PrivateKey);
}
///
- /// Imports an ML-DSA key from an RFC 7468 PEM-encoded string.
+ /// Imports an ML-DSA key from an encrypted RFC 7468 PEM-encoded string.
///
///
- /// The text of the PEM key to import.
- ///
+ /// The PEM text of the encrypted key to import.
///
- /// The password to use when decrypting the key material.
+ /// The bytes to use as a password when decrypting the key material.
///
- ///
- /// if the source did not contain a PEM-encoded ML-DSA key;
- /// if the source contains an ML-DSA key and it was successfully imported;
- /// otherwise, an exception is thrown.
- ///
///
- /// contains an encrypted PEM-encoded key.
- /// -or-
- /// contains multiple PEM-encoded ML-DSA keys.
+ ///
+ /// does not contain a PEM-encoded key with a recognized label.
+ ///
/// -or-
- /// contains no PEM-encoded ML-DSA keys.
+ ///
+ /// contains multiple PEM-encoded keys with a recognized label.
+ ///
///
///
- /// An error occurred while importing the key.
+ ///
+ /// The password is incorrect.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// do not represent an ASN.1-BER-encoded PKCS#8 EncryptedPrivateKeyInfo structure.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// indicate the key is for an algorithm other than the algorithm
+ /// represented by this instance.
+ ///
+ /// -or-
+ ///
+ /// The base-64 decoded contents of the PEM text from
+ /// represent the key in a format that is not supported.
+ ///
+ /// -or-
+ ///
+ /// An error occurred while importing the key.
+ ///
+ ///
+ ///
+ /// The platform does not support ML-DSA. Callers can use the property
+ /// to determine if the platform supports ML-DSA.
///
+ ///
+ ///
+ /// Unsupported or malformed PEM-encoded objects will be ignored. If multiple supported PEM labels
+ /// are found, an exception is thrown to prevent importing a key when
+ /// the key is ambiguous.
+ ///
+ /// This method supports the ENCRYPTED PRIVATE KEY PEM label.
+ ///
public static MLDsa ImportFromEncryptedPem(ReadOnlySpan source, ReadOnlySpan passwordBytes)
{
ThrowIfNotSupported();
- // TODO: Match the behavior of ECDsa.ImportFromEncryptedPem.
- throw new NotImplementedException();
+ return PemKeyHelpers.ImportEncryptedFactoryPem(
+ source,
+ passwordBytes,
+ ImportEncryptedPkcs8PrivateKey);
+ }
+
+ ///
+ ///
+ /// or is .
+ ///
+ public static MLDsa ImportFromEncryptedPem(string source, string password)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(password);
+ ThrowIfNotSupported();
+
+ return ImportFromEncryptedPem(source.AsSpan(), password.AsSpan());
+ }
+
+ ///
+ ///
+ /// or is .
+ ///
+ public static MLDsa ImportFromEncryptedPem(string source, byte[] passwordBytes)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(passwordBytes);
+ ThrowIfNotSupported();
+
+ return ImportFromEncryptedPem(source.AsSpan(), new ReadOnlySpan(passwordBytes));
}
///
@@ -1189,16 +1448,19 @@ protected virtual void Dispose(bool disposing)
private AsnWriter ExportSubjectPublicKeyInfoCore()
{
- ThrowIfDisposed();
-
- byte[] rented = CryptoPool.Rent(Algorithm.PublicKeySizeInBytes);
+ int publicKeySizeInBytes = Algorithm.PublicKeySizeInBytes;
+ byte[] rented = CryptoPool.Rent(publicKeySizeInBytes);
try
{
- Span keySpan = rented.AsSpan(0, Algorithm.PublicKeySizeInBytes);
- ExportMLDsaPublicKey(keySpan);
+ Span publicKey = rented.AsSpan(0, publicKeySizeInBytes);
+ ExportMLDsaPublicKeyCore(publicKey);
- AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ // The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is 22 bytes.
+ // Round it off to 32. This checked operation should never throw because the inputs are not
+ // user provided.
+ int capacity = checked(32 + publicKeySizeInBytes);
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER, capacity);
using (writer.PushSequence())
{
@@ -1207,81 +1469,197 @@ private AsnWriter ExportSubjectPublicKeyInfoCore()
writer.WriteObjectIdentifier(Algorithm.Oid);
}
- writer.WriteBitString(keySpan);
+ writer.WriteBitString(publicKey);
}
+ Debug.Assert(writer.GetEncodedLength() <= capacity);
return writer;
}
finally
{
- // Public key doesn't need to be cleared
- CryptoPool.Return(rented, 0);
+ // Public key does not need to be cleared.
+ CryptoPool.Return(rented, clearSize: 0);
}
}
private AsnWriter ExportEncryptedPkcs8PrivateKeyCore(ReadOnlySpan passwordBytes, PbeParameters pbeParameters)
{
- ThrowIfDisposed();
-
- // TODO: Determine a more appropriate maximum size once the format is actually known.
- int size = Algorithm.SecretKeySizeInBytes * 2;
- // The buffer is only being passed out as a span, so the derived type can't meaningfully
- // hold on to it without being malicious.
- byte[] rented = CryptoPool.Rent(size);
- int written;
-
- while (!TryExportPkcs8PrivateKey(rented, out written))
+ AsnWriter tmp = ExportPkcs8PrivateKeyCallback(static pkcs8 =>
{
- size = rented.Length;
- CryptoPool.Return(rented, 0);
- rented = CryptoPool.Rent(size * 2);
- }
+ AsnWriter writer = new(AsnEncodingRules.BER, initialCapacity: pkcs8.Length);
+
+ try
+ {
+ writer.WriteEncodedValueForCrypto(pkcs8);
+ }
+ catch
+ {
+ writer.Reset();
+ throw;
+ }
- AsnWriter tmp = new AsnWriter(AsnEncodingRules.BER);
+ return writer;
+ });
try
{
- tmp.WriteEncodedValueForCrypto(rented.AsSpan(0, written));
return KeyFormatHelper.WriteEncryptedPkcs8(passwordBytes, tmp, pbeParameters);
}
finally
{
tmp.Reset();
- CryptoPool.Return(rented, written);
}
}
private AsnWriter ExportEncryptedPkcs8PrivateKeyCore(ReadOnlySpan password, PbeParameters pbeParameters)
{
- ThrowIfDisposed();
+ AsnWriter tmp = ExportPkcs8PrivateKeyCallback(static pkcs8 =>
+ {
+ AsnWriter writer = new(AsnEncodingRules.BER, initialCapacity: pkcs8.Length);
+
+ try
+ {
+ writer.WriteEncodedValueForCrypto(pkcs8);
+ }
+ catch
+ {
+ writer.Reset();
+ throw;
+ }
+
+ return writer;
+ });
+
+ try
+ {
+ return KeyFormatHelper.WriteEncryptedPkcs8(password, tmp, pbeParameters);
+ }
+ finally
+ {
+ tmp.Reset();
+ }
+ }
- // TODO: Determine a more appropriate maximum size once the format is actually known.
- int initialSize = Algorithm.SecretKeySizeInBytes * 2;
+ private TResult ExportPkcs8PrivateKeyCallback(ExportPkcs8PrivateKeyFunc func)
+ {
+ // A PKCS#8 ML-DSA secret key has an ASN.1 overhead of 28 bytes, assuming no attributes.
+ // Make it an even 32 and that should give a good starting point for a buffer size.
+ // The secret key is always larger than the seed so this buffer size can accommodate both.
+ int size = Algorithm.SecretKeySizeInBytes + 32;
// The buffer is only being passed out as a span, so the derived type can't meaningfully
// hold on to it without being malicious.
- byte[] rented = CryptoPool.Rent(initialSize);
+ byte[] buffer = CryptoPool.Rent(size);
int written;
- while (!TryExportPkcs8PrivateKey(rented, out written))
+ while (!TryExportPkcs8PrivateKeyCore(buffer, out written))
{
- CryptoPool.Return(rented, 0);
- rented = CryptoPool.Rent(rented.Length * 2);
+ CryptoPool.Return(buffer);
+ size = checked(size * 2);
+ buffer = CryptoPool.Rent(size);
}
- AsnWriter tmp = new AsnWriter(AsnEncodingRules.BER);
+ if ((uint)written > buffer.Length)
+ {
+ // We got a nonsense value written back. Clear the buffer, but don't put it back in the pool.
+ CryptographicOperations.ZeroMemory(buffer);
+ throw new CryptographicException();
+ }
try
{
- tmp.WriteEncodedValueForCrypto(rented.AsSpan(0, written));
- return KeyFormatHelper.WriteEncryptedPkcs8(password, tmp, pbeParameters);
+ return func(buffer.AsSpan(0, written));
}
finally
{
- tmp.Reset();
- CryptoPool.Return(rented, written);
+ CryptoPool.Return(buffer, written);
}
}
+ private static void MLDsaKeyReader(
+ ReadOnlyMemory privateKeyContents,
+ in AlgorithmIdentifierAsn algorithmIdentifier,
+ out MLDsa dsa)
+ {
+ MLDsaAlgorithm algorithm = GetAlgorithmIdentifier(in algorithmIdentifier);
+ MLDsaPrivateKeyAsn dsaKey = MLDsaPrivateKeyAsn.Decode(privateKeyContents, AsnEncodingRules.BER);
+
+ if (dsaKey.Seed is ReadOnlyMemory seed)
+ {
+ if (seed.Length != algorithm.PrivateSeedSizeInBytes)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+ }
+
+ dsa = MLDsaImplementation.ImportMLDsaPrivateSeed(algorithm, seed.Span);
+ }
+ else if (dsaKey.ExpandedKey is ReadOnlyMemory expandedKey)
+ {
+ if (expandedKey.Length != algorithm.SecretKeySizeInBytes)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+ }
+
+ dsa = MLDsaImplementation.ImportSecretKey(algorithm, expandedKey.Span);
+ }
+ else if (dsaKey.Both is MLDsaPrivateKeyBothAsn both)
+ {
+ int secretKeySize = algorithm.SecretKeySizeInBytes;
+
+ if (both.Seed.Length != algorithm.PrivateSeedSizeInBytes ||
+ both.ExpandedKey.Length != secretKeySize)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+ }
+
+ MLDsa key = MLDsaImplementation.ImportMLDsaPrivateSeed(algorithm, both.Seed.Span);
+ byte[] rent = CryptoPool.Rent(secretKeySize);
+ Span buffer = rent.AsSpan(0, secretKeySize);
+
+ try
+ {
+ key.ExportMLDsaSecretKey(buffer);
+
+ if (CryptographicOperations.FixedTimeEquals(buffer, both.ExpandedKey.Span))
+ {
+ dsa = key;
+ }
+ else
+ {
+ throw new CryptographicException(SR.Cryptography_MLDsaPkcs8KeyMismatch);
+ }
+ }
+ catch
+ {
+ key.Dispose();
+ throw;
+ }
+ finally
+ {
+ CryptoPool.Return(rent, secretKeySize);
+ }
+ }
+ else
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+ }
+ }
+
+ private static MLDsaAlgorithm GetAlgorithmIdentifier(ref readonly AlgorithmIdentifierAsn identifier)
+ {
+ MLDsaAlgorithm algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(identifier.Algorithm) ??
+ throw new CryptographicException(
+ SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, identifier.Algorithm));
+
+ if (identifier.Parameters.HasValue)
+ {
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ identifier.Encode(writer);
+ throw Helpers.CreateAlgorithmUnknownException(writer);
+ }
+
+ return algorithm;
+ }
+
internal static void ThrowIfNotSupported()
{
if (!IsSupported)
@@ -1289,5 +1667,47 @@ internal static void ThrowIfNotSupported()
throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa)));
}
}
+
+ private static void ThrowIfInvalidLength(ReadOnlySpan data)
+ {
+ int bytesRead;
+
+ try
+ {
+ AsnDecoder.ReadEncodedValue(data, AsnEncodingRules.BER, out _, out _, out bytesRead);
+ }
+ catch (AsnContentException ace)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, ace);
+ }
+
+ if (bytesRead != data.Length)
+ {
+ throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
+ }
+ }
+
+ private static string EncodeAsnWriterToPem(string label, AsnWriter writer, bool clear = true)
+ {
+#if NET10_0_OR_GREATER
+ return writer.Encode(label, static (label, span) => PemEncoding.WriteString(label, span));
+#else
+ int length = writer.GetEncodedLength();
+ byte[] rent = CryptoPool.Rent(length);
+
+ try
+ {
+ int written = writer.Encode(rent);
+ Debug.Assert(written == length);
+ return PemEncoding.WriteString(label, rent.AsSpan(0, written));
+ }
+ finally
+ {
+ CryptoPool.Return(rent, clear ? length : 0);
+ }
+#endif
+ }
+
+ private delegate TResult ExportPkcs8PrivateKeyFunc(ReadOnlySpan pkcs8);
}
}
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.NotSupported.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.NotSupported.cs
index 4efac5913dece8..216aac9abb9573 100644
--- a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.NotSupported.cs
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.NotSupported.cs
@@ -26,6 +26,9 @@ protected override void ExportMLDsaSecretKeyCore(Span destination) =>
protected override void ExportMLDsaPrivateSeedCore(Span destination) =>
throw new PlatformNotSupportedException();
+ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) =>
+ throw new PlatformNotSupportedException();
+
internal static partial MLDsaImplementation GenerateKeyImpl(MLDsaAlgorithm algorithm) =>
throw new PlatformNotSupportedException();
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs
index b8ec622c7c741d..b2bda14c4faf84 100644
--- a/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaImplementation.Windows.cs
@@ -27,6 +27,9 @@ protected override void ExportMLDsaSecretKeyCore(Span destination) =>
protected override void ExportMLDsaPrivateSeedCore(Span destination) =>
throw new PlatformNotSupportedException();
+ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) =>
+ throw new PlatformNotSupportedException();
+
internal static partial MLDsaImplementation GenerateKeyImpl(MLDsaAlgorithm algorithm) =>
throw new PlatformNotSupportedException();
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsaPkcs8.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaPkcs8.cs
new file mode 100644
index 00000000000000..74434d935ca519
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaPkcs8.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+
+namespace System.Security.Cryptography
+{
+ internal static class MLDsaPkcs8
+ {
+ internal static bool TryExportPkcs8PrivateKey(
+ MLDsa dsa,
+ bool hasSeed,
+ bool hasSecretKey,
+ Span destination,
+ out int bytesWritten)
+ {
+ AlgorithmIdentifierAsn algorithmIdentifier = new()
+ {
+ Algorithm = dsa.Algorithm.Oid,
+ Parameters = default(ReadOnlyMemory?),
+ };
+
+ MLDsaPrivateKeyAsn privateKeyAsn = default;
+ byte[]? rented = null;
+ int written = 0;
+
+ try
+ {
+ if (hasSeed)
+ {
+ int seedSize = dsa.Algorithm.PrivateSeedSizeInBytes;
+ rented = CryptoPool.Rent(seedSize);
+ Memory buffer = rented.AsMemory(0, seedSize);
+ dsa.ExportMLDsaPrivateSeed(buffer.Span);
+ written = buffer.Length;
+ privateKeyAsn.Seed = buffer;
+ }
+ else if (hasSecretKey)
+ {
+ int secretKeySize = dsa.Algorithm.SecretKeySizeInBytes;
+ rented = CryptoPool.Rent(secretKeySize);
+ Memory buffer = rented.AsMemory(0, secretKeySize);
+ dsa.ExportMLDsaSecretKey(buffer.Span);
+ written = buffer.Length;
+ privateKeyAsn.ExpandedKey = buffer;
+ }
+ else
+ {
+ throw new CryptographicException(SR.Cryptography_NotValidPrivateKey);
+ }
+
+ AsnWriter algorithmWriter = new(AsnEncodingRules.DER);
+ algorithmIdentifier.Encode(algorithmWriter);
+ AsnWriter privateKeyWriter = new(AsnEncodingRules.DER);
+ privateKeyAsn.Encode(privateKeyWriter);
+ AsnWriter pkcs8Writer = KeyFormatHelper.WritePkcs8(algorithmWriter, privateKeyWriter);
+
+ bool result = pkcs8Writer.TryEncode(destination, out bytesWritten);
+ privateKeyWriter.Reset();
+ pkcs8Writer.Reset();
+ return result;
+ }
+ finally
+ {
+ if (rented is not null)
+ {
+ CryptoPool.Return(rented, written);
+ }
+ }
+ }
+ }
+}
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaImplementationTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaImplementationTests.cs
index 73352a9cd2b00b..48c5a73d5b42e3 100644
--- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaImplementationTests.cs
+++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaImplementationTests.cs
@@ -1,16 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Generic;
-using System.Security.Cryptography.Dsa.Tests;
-using Microsoft.DotNet.RemoteExecutor;
-using Microsoft.DotNet.XUnitExtensions;
+using System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
using Test.Cryptography;
using Xunit;
+using Xunit.Sdk;
namespace System.Security.Cryptography.Tests
{
- [ConditionalClass(typeof(MLDsa), nameof(MLDsa.IsSupported))]
public class MLDsaImplementationTests : MLDsaTestsBase
{
protected override MLDsa GenerateKey(MLDsaAlgorithm algorithm) => MLDsa.GenerateKey(algorithm);
@@ -31,27 +29,89 @@ public static void GenerateImport_NullAlgorithm()
[MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
public static void ImportMLDsaSecretKey_WrongSize(MLDsaAlgorithm algorithm)
{
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaSecretKey(algorithm, new byte[algorithm.SecretKeySizeInBytes - 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaSecretKey(algorithm, new byte[algorithm.SecretKeySizeInBytes + 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaSecretKey(algorithm, default));
+ int secretKeySize = algorithm.SecretKeySizeInBytes;
+
+ // ML-DSA key size is wrong when importing algorithm key. Throw an argument exception.
+ Action> assertDirectImport = import => AssertExtensions.Throws("source", import);
+
+ // ML-DSA key size is wrong when importing SPKI/PKCS8/PEM. Throw a cryptographic exception unless platform is not supported.
+ // Note: this is the algorithm key size, not the PKCS#8 key size.
+ Action> assertEmbeddedImport = import => AssertThrowIfNotSupported(() => Assert.Throws(() => import()));
+
+ MLDsaTestHelpers.AssertImportSecretKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[secretKeySize + 1]);
+ MLDsaTestHelpers.AssertImportSecretKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[secretKeySize - 1]);
+ MLDsaTestHelpers.AssertImportSecretKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[0]);
}
[Theory]
[MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
public static void ImportMLDsaPrivateSeed_WrongSize(MLDsaAlgorithm algorithm)
{
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPrivateSeed(algorithm, new byte[algorithm.PrivateSeedSizeInBytes - 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPrivateSeed(algorithm, new byte[algorithm.PrivateSeedSizeInBytes + 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPrivateSeed(algorithm, default));
+ int privateSeedSize = algorithm.PrivateSeedSizeInBytes;
+
+ // ML-DSA key size is wrong when importing algorithm key. Throw an argument exception.
+ Action> assertDirectImport = import => AssertExtensions.Throws("source", import);
+
+ // ML-DSA key size is wrong when importing SPKI/PKCS8/PEM. Throw a cryptographic exception unless platform is not supported.
+ // Note: this is the algorithm key size, not the PKCS#8 key size.
+ Action> assertEmbeddedImport = import => AssertThrowIfNotSupported(() => Assert.Throws(() => import()));
+
+ MLDsaTestHelpers.AssertImportPrivateSeed(assertDirectImport, assertEmbeddedImport, algorithm, new byte[privateSeedSize + 1]);
+ MLDsaTestHelpers.AssertImportPrivateSeed(assertDirectImport, assertEmbeddedImport, algorithm, new byte[privateSeedSize - 1]);
+ MLDsaTestHelpers.AssertImportPrivateSeed(assertDirectImport, assertEmbeddedImport, algorithm, new byte[0]);
}
[Theory]
[MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
public static void ImportMLDsaPublicKey_WrongSize(MLDsaAlgorithm algorithm)
{
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPublicKey(algorithm, new byte[algorithm.PublicKeySizeInBytes - 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPublicKey(algorithm, new byte[algorithm.PublicKeySizeInBytes + 1]));
- AssertExtensions.Throws("source", () => MLDsa.ImportMLDsaPublicKey(algorithm, default));
+ int publicKeySize = algorithm.PublicKeySizeInBytes;
+
+ // ML-DSA key size is wrong when importing algorithm key. Throw an argument exception.
+ Action> assertDirectImport = import => AssertExtensions.Throws("source", import);
+
+ // ML-DSA key size is wrong when importing SPKI/PKCS8/PEM. Throw a cryptographic exception unless platform is not supported.
+ // Note: this is the algorithm key size, not the PKCS#8 key size.
+ Action> assertEmbeddedImport = import => AssertThrowIfNotSupported(() => Assert.Throws(() => import()));
+
+ MLDsaTestHelpers.AssertImportPublicKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[publicKeySize + 1]);
+ MLDsaTestHelpers.AssertImportPublicKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[publicKeySize - 1]);
+ MLDsaTestHelpers.AssertImportPublicKey(assertDirectImport, assertEmbeddedImport, algorithm, new byte[0]);
+ }
+
+ [Fact]
+ public static void ImportSubjectKeyPublicInfo_NullSource()
+ {
+ AssertExtensions.Throws("source", () => MLDsa.ImportSubjectPublicKeyInfo(null));
+ }
+
+ [Fact]
+ public static void ImportPkcs8PrivateKey_NullSource()
+ {
+ AssertExtensions.Throws("source", () => MLDsa.ImportPkcs8PrivateKey(null));
+ }
+
+ [Fact]
+ public static void ImportFromPem_NullSource()
+ {
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(null));
+ }
+
+ [Fact]
+ public static void ImportEncrypted_NullSource()
+ {
+ AssertExtensions.Throws("source", () => MLDsa.ImportEncryptedPkcs8PrivateKey("", null));
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(null, (byte[])null));
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(null, (string)null));
+ }
+
+ [Fact]
+ public static void ImportEncrypted_NullPassword()
+ {
+ AssertExtensions.Throws("password", () => MLDsa.ImportEncryptedPkcs8PrivateKey(null, null));
+ AssertExtensions.Throws("password", () => MLDsa.ImportFromEncryptedPem("", (string)null));
+
+ AssertExtensions.Throws("passwordBytes", () => MLDsa.ImportFromEncryptedPem("", (byte[])null));
}
[Fact]
@@ -61,7 +121,518 @@ public static void UseAfterDispose()
mldsa.Dispose();
mldsa.Dispose(); // no throw
- VerifyDisposed(mldsa);
+ MLDsaTestHelpers.VerifyDisposed(mldsa);
+ }
+
+ [Fact]
+ public static void ArgumentValidation_MalformedAsnEncoding()
+ {
+ // Generate a valid ASN.1 encoding
+ byte[] encodedBytes = CreateAsn1EncodedBytes();
+ int actualEncodedLength = encodedBytes.Length;
+
+ // Add a trailing byte so the length indicated in the encoding will be smaller than the actual data.
+ Array.Resize(ref encodedBytes, actualEncodedLength + 1);
+ AssertThrows(encodedBytes);
+
+ // Remove the last byte so the length indicated in the encoding will be larger than the actual data.
+ Array.Resize(ref encodedBytes, actualEncodedLength - 1);
+ AssertThrows(encodedBytes);
+
+ static void AssertThrows(byte[] encodedBytes)
+ {
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(
+ import => Assert.Throws(() => import(encodedBytes)),
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(encodedBytes))));
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => Assert.Throws(() => import(encodedBytes)),
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(encodedBytes))));
+
+ MLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey(
+ import => Assert.Throws(() => import("PLACEHOLDER", encodedBytes)),
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import("PLACEHOLDER", encodedBytes))));
+ }
+ }
+
+ [Fact]
+ public static void ImportSpki_BerEncoding()
+ {
+ // Valid BER but invalid DER - uses indefinite length encoding
+ byte[] indefiniteLengthOctet = [0x04, 0x80, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00];
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import =>
+ AssertThrowIfNotSupported(() =>
+ Assert.Throws(() => import(indefiniteLengthOctet))));
+ }
+
+ [Fact]
+ public static void ImportPkcs8_BerEncoding()
+ {
+ // Seed is DER encoded, so create a BER encoding from it by making it use indefinite length encoding.
+ byte[] seedPkcs8 = MLDsaTestsData.IetfMLDsa44.Pkcs8PrivateKey_Seed;
+
+ // Two 0x00 bytes at the end signal the end of the indefinite length encoding
+ byte[] indefiniteLengthOctet = new byte[seedPkcs8.Length + 2];
+ seedPkcs8.CopyTo(indefiniteLengthOctet);
+ indefiniteLengthOctet[1] = 0b1000_0000; // change length to indefinite
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(import =>
+ MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export =>
+ WithDispose(import(indefiniteLengthOctet), mldsa =>
+ AssertExtensions.SequenceEqual(MLDsaTestsData.IetfMLDsa44.PrivateSeed, export(mldsa)))));
+ }
+
+ [Fact]
+ public static void ImportPkcs8_WrongTypeInAsn()
+ {
+ // Create an incorrect ASN.1 structure to pass into the import methods.
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ AlgorithmIdentifierAsn algorithmIdentifier = new AlgorithmIdentifierAsn
+ {
+ Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44),
+ };
+ algorithmIdentifier.Encode(writer);
+ byte[] wrongAsnType = writer.Encode();
+
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(wrongAsnType))));
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(wrongAsnType))));
+
+ MLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import("PLACEHOLDER", wrongAsnType))));
+ }
+
+ [Fact]
+ public static void ImportSubjectKeyPublicInfo_AlgorithmErrorsInAsn()
+ {
+#if !NETFRAMEWORK // Does not support exporting RSA SPKI
+ if (!OperatingSystem.IsBrowser())
+ {
+ // RSA key
+ using RSA rsa = RSA.Create();
+ byte[] rsaSpkiBytes = rsa.ExportSubjectPublicKeyInfo();
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(rsaSpkiBytes))));
+ }
+#endif
+
+ // Create an invalid ML-DSA SPKI with parameters
+ SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn
+ {
+ Algorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44),
+ Parameters = MLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid
+ },
+ SubjectPublicKey = new byte[MLDsaAlgorithm.MLDsa44.PublicKeySizeInBytes]
+ };
+
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(spki.Encode()))));
+
+ spki.Algorithm.Parameters = AsnUtils.DerNull;
+
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(spki.Encode()))));
+
+ // Sanity check
+ spki.Algorithm.Parameters = null;
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import => AssertThrowIfNotSupported(() => import(spki.Encode())));
+ }
+
+ [Fact]
+ public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn()
+ {
+#if !NETFRAMEWORK // Does not support exporting RSA PKCS#8 private key
+ if (!OperatingSystem.IsBrowser())
+ {
+ // RSA key isn't valid for ML-DSA
+ using RSA rsa = RSA.Create();
+ byte[] rsaPkcs8Bytes = rsa.ExportPkcs8PrivateKey();
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(rsaPkcs8Bytes))));
+ }
+#endif
+
+ // Create an invalid ML-DSA PKCS8 with parameters
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ MLDsaPrivateKeyAsn seed = new MLDsaPrivateKeyAsn
+ {
+ Seed = new byte[MLDsaAlgorithm.MLDsa44.PrivateSeedSizeInBytes],
+ };
+ seed.Encode(writer);
+
+ PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn
+ {
+ PrivateKeyAlgorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44),
+ Parameters = MLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid
+ },
+ PrivateKey = writer.Encode(),
+ };
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(pkcs8.Encode()))));
+
+ pkcs8.PrivateKeyAlgorithm.Parameters = AsnUtils.DerNull;
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(pkcs8.Encode()))));
+
+ // Sanity check
+ pkcs8.PrivateKeyAlgorithm.Parameters = null;
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => AssertThrowIfNotSupported(() => import(pkcs8.Encode())));
+ }
+
+ [Fact]
+ public static void ImportPkcs8PrivateKey_KeyErrorsInAsn()
+ {
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn()
+ });
+
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn
+ {
+ Seed = new byte[MLDsaAlgorithm.MLDsa44.PrivateSeedSizeInBytes],
+ }
+ });
+
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn
+ {
+ ExpandedKey = new byte[MLDsaAlgorithm.MLDsa44.SecretKeySizeInBytes],
+ }
+ });
+
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn
+ {
+ Seed = new byte[MLDsaAlgorithm.MLDsa44.PrivateSeedSizeInBytes - 1],
+ ExpandedKey = new byte[MLDsaAlgorithm.MLDsa44.SecretKeySizeInBytes],
+ }
+ });
+
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn
+ {
+ Seed = new byte[MLDsaAlgorithm.MLDsa44.PrivateSeedSizeInBytes],
+ ExpandedKey = new byte[MLDsaAlgorithm.MLDsa44.SecretKeySizeInBytes - 1],
+ }
+ });
+
+ AssertInvalidAsn(new MLDsaPrivateKeyAsn
+ {
+ Both = new MLDsaPrivateKeyBothAsn
+ {
+ // This will also fail because the seed and expanded key mismatch
+ Seed = new byte[MLDsaAlgorithm.MLDsa44.PrivateSeedSizeInBytes],
+ ExpandedKey = new byte[MLDsaAlgorithm.MLDsa44.SecretKeySizeInBytes],
+ }
+ });
+
+ static void AssertInvalidAsn(MLDsaPrivateKeyAsn privateKeyAsn)
+ {
+ PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn
+ {
+ PrivateKeyAlgorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44),
+ Parameters = null,
+ },
+ };
+
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ privateKeyAsn.Encode(writer);
+ pkcs8.PrivateKey = writer.Encode();
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(
+ import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(pkcs8.Encode()))));
+ }
+ }
+
+ [Fact]
+ public static void ImportFromPem_MalformedPem()
+ {
+ AssertThrows(WritePemRaw("UNKNOWN LABEL", []));
+ AssertThrows(string.Empty);
+ AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", []));
+ AssertThrows(WritePemRaw("PUBLIC KEY", []) + '\n' + WritePemRaw("PUBLIC KEY", []));
+ AssertThrows(WritePemRaw("PRIVATE KEY", []) + '\n' + WritePemRaw("PUBLIC KEY", []));
+ AssertThrows(WritePemRaw("PUBLIC KEY", []) + '\n' + WritePemRaw("PRIVATE KEY", []));
+ AssertThrows(WritePemRaw("PRIVATE KEY", []) + '\n' + WritePemRaw("PRIVATE KEY", []));
+ AssertThrows(WritePemRaw("PRIVATE KEY", "%"));
+ AssertThrows(WritePemRaw("PUBLIC KEY", "%"));
+
+ static void AssertThrows(string pem)
+ {
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem)));
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem.AsSpan())));
+ }
+ }
+
+ [Fact]
+ public static void ImportFromEncryptedPem_MalformedPem()
+ {
+ AssertThrows(WritePemRaw("UNKNOWN LABEL", []));
+ AssertThrows(WritePemRaw("CERTIFICATE", []));
+ AssertThrows(string.Empty);
+ AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", []) + '\n' + WritePemRaw("ENCRYPTED PRIVATE KEY", []));
+ AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", "%"));
+
+ static void AssertThrows(string encryptedPem)
+ {
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER")));
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8)));
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem.AsSpan(), "PLACEHOLDER")));
+ AssertThrowIfNotSupported(() =>
+ AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8.ToArray())));
+ }
+ }
+
+ [ConditionalTheory(typeof(MLDsa), nameof(MLDsa.IsSupported))]
+ [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public static void AlgorithmMatches_GenerateKey(MLDsaAlgorithm algorithm)
+ {
+ byte[] publicKey = new byte[algorithm.PublicKeySizeInBytes];
+ byte[] secretKey = new byte[algorithm.SecretKeySizeInBytes];
+ byte[] privateSeed = new byte[algorithm.PrivateSeedSizeInBytes];
+ AssertThrowIfNotSupported(() =>
+ {
+ using MLDsa mldsa = MLDsa.GenerateKey(algorithm);
+ mldsa.ExportMLDsaPublicKey(publicKey);
+ mldsa.ExportMLDsaSecretKey(secretKey);
+ mldsa.ExportMLDsaPrivateSeed(privateSeed);
+ Assert.Equal(algorithm, mldsa.Algorithm);
+ });
+
+ MLDsaTestHelpers.AssertImportPublicKey(import =>
+ AssertThrowIfNotSupported(() =>
+ WithDispose(import(), mldsa =>
+ Assert.Equal(algorithm, mldsa.Algorithm))), algorithm, publicKey);
+
+ MLDsaTestHelpers.AssertImportSecretKey(import =>
+ AssertThrowIfNotSupported(() =>
+ WithDispose(import(), mldsa =>
+ Assert.Equal(algorithm, mldsa.Algorithm))), algorithm, secretKey);
+
+ MLDsaTestHelpers.AssertImportPrivateSeed(import =>
+ AssertThrowIfNotSupported(() =>
+ WithDispose(import(), mldsa => Assert.Equal(algorithm, mldsa.Algorithm))), algorithm, privateSeed);
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_PublicKey(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportPublicKey(import =>
+ MLDsaTestHelpers.AssertExportMLDsaPublicKey(export =>
+ WithDispose(import(), mldsa =>
+ AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa)))),
+ info.Algorithm,
+ info.PublicKey);
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_PrivateKey(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportSecretKey(import =>
+ MLDsaTestHelpers.AssertExportMLDsaSecretKey(export =>
+ WithDispose(import(), mldsa =>
+ AssertExtensions.SequenceEqual(info.SecretKey, export(mldsa)))),
+ info.Algorithm,
+ info.SecretKey);
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_PrivateSeed(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportPrivateSeed(import =>
+ MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export =>
+ WithDispose(import(), mldsa =>
+ AssertExtensions.SequenceEqual(info.PrivateSeed, export(mldsa)))),
+ info.Algorithm,
+ info.PrivateSeed);
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_SPKI(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import =>
+ MLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export =>
+ WithDispose(import(info.Pkcs8PublicKey), mldsa =>
+ AssertExtensions.SequenceEqual(info.Pkcs8PublicKey, export(mldsa)))));
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_PublicPem(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportFromPem(import =>
+ MLDsaTestHelpers.AssertExportToPublicKeyPem(export =>
+ WithDispose(import(info.PublicKeyPem), mldsa =>
+ Assert.Equal(info.PublicKeyPem, export(mldsa)))));
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_Pkcs8PrivateKey(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(import =>
+ MLDsaTestHelpers.AssertExportPkcs8PrivateKey(export =>
+ WithDispose(import(info.Pkcs8PrivateKey_Seed), mldsa =>
+ AssertExtensions.SequenceEqual(info.Pkcs8PrivateKey_Seed, export(mldsa)))));
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(import =>
+ MLDsaTestHelpers.AssertExportPkcs8PrivateKey(export =>
+ WithDispose(import(info.Pkcs8PrivateKey_Expanded), mldsa =>
+ AssertExtensions.SequenceEqual(info.Pkcs8PrivateKey_Expanded, export(mldsa)))));
+
+ MLDsaTestHelpers.AssertImportPkcs8PrivateKey(import =>
+ MLDsaTestHelpers.AssertExportPkcs8PrivateKey(export =>
+ WithDispose(import(info.Pkcs8PrivateKey_Both), mldsa =>
+ // We will only export seed instead of both since either is valid.
+ AssertExtensions.SequenceEqual(info.Pkcs8PrivateKey_Seed, export(mldsa)))));
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_Import_Export_Pkcs8PrivateKeyPem(MLDsaKeyInfo info)
+ {
+ MLDsaTestHelpers.AssertImportFromPem(import =>
+ MLDsaTestHelpers.AssertExportToPrivateKeyPem(export =>
+ WithDispose(import(info.PrivateKeyPem), mldsa =>
+ Assert.Equal(info.PrivateKeyPem, export(mldsa)))));
+ }
+
+ [Theory]
+ [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))]
+ public void RoundTrip_EncryptedPrivateKey(MLDsaKeyInfo info)
+ {
+ // Load key
+ using MLDsa mldsa = MLDsa.ImportEncryptedPkcs8PrivateKey(info.EncryptionPassword, info.Pkcs8EncryptedPrivateKey);
+
+ byte[] secretKey = new byte[mldsa.Algorithm.SecretKeySizeInBytes];
+ mldsa.ExportMLDsaSecretKey(secretKey);
+ AssertExtensions.SequenceEqual(info.SecretKey, secretKey);
+
+ byte[] privateSeed = new byte[mldsa.Algorithm.PrivateSeedSizeInBytes];
+ mldsa.ExportMLDsaPrivateSeed(privateSeed);
+ AssertExtensions.SequenceEqual(info.PrivateSeed, privateSeed);
+
+ byte[] publicKey = new byte[mldsa.Algorithm.PublicKeySizeInBytes];
+ mldsa.ExportMLDsaPublicKey(publicKey);
+ AssertExtensions.SequenceEqual(info.PublicKey, publicKey);
+
+ MLDsaTestHelpers.EncryptionPasswordType validPasswordTypes = MLDsaTestHelpers.GetValidPasswordTypes(info.EncryptionParameters);
+
+ MLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export =>
+ MLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey(import =>
+ {
+ // Roundtrip it using encrypted PKCS#8
+ using MLDsa roundTrippedMLDsa = import(info.EncryptionPassword, export(mldsa, info.EncryptionPassword, info.EncryptionParameters));
+
+ // The keys should be the same
+ Assert.Equal(info.Algorithm, roundTrippedMLDsa.Algorithm);
+
+ byte[] roundTrippedSecretKey = new byte[roundTrippedMLDsa.Algorithm.SecretKeySizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaSecretKey(roundTrippedSecretKey);
+ AssertExtensions.SequenceEqual(secretKey, roundTrippedSecretKey);
+
+ byte[] roundTrippedPrivateSeed = new byte[roundTrippedMLDsa.Algorithm.PrivateSeedSizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaPrivateSeed(roundTrippedPrivateSeed);
+ AssertExtensions.SequenceEqual(privateSeed, roundTrippedPrivateSeed);
+
+ byte[] roundTrippedPublicKey = new byte[roundTrippedMLDsa.Algorithm.PublicKeySizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaPublicKey(roundTrippedPublicKey);
+ AssertExtensions.SequenceEqual(publicKey, roundTrippedPublicKey);
+ }, validPasswordTypes), validPasswordTypes);
+
+ MLDsaTestHelpers.AssertExportToEncryptedPem(export =>
+ MLDsaTestHelpers.AssertImportFromEncryptedPem(import =>
+ {
+ // Roundtrip it using encrypted PEM
+ using MLDsa roundTrippedMLDsa = import(export(mldsa, info.EncryptionPassword, info.EncryptionParameters), info.EncryptionPassword);
+
+ // The keys should be the same
+ Assert.Equal(info.Algorithm, roundTrippedMLDsa.Algorithm);
+
+ byte[] roundTrippedSecretKey = new byte[roundTrippedMLDsa.Algorithm.SecretKeySizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaSecretKey(roundTrippedSecretKey);
+ AssertExtensions.SequenceEqual(secretKey, roundTrippedSecretKey);
+
+ byte[] roundTrippedPrivateSeed = new byte[roundTrippedMLDsa.Algorithm.PrivateSeedSizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaPrivateSeed(roundTrippedPrivateSeed);
+ AssertExtensions.SequenceEqual(privateSeed, roundTrippedPrivateSeed);
+
+ byte[] roundTrippedPublicKey = new byte[roundTrippedMLDsa.Algorithm.PublicKeySizeInBytes];
+ roundTrippedMLDsa.ExportMLDsaPublicKey(roundTrippedPublicKey);
+ AssertExtensions.SequenceEqual(publicKey, roundTrippedPublicKey);
+ }, validPasswordTypes), validPasswordTypes);
+ }
+
+ ///
+ /// Asserts that on platforms that do not support ML-DSA, the input test throws PlatformNotSupportedException.
+ /// If the test does pass, it implies that the test is validating code after the platform check.
+ ///
+ /// The test to run.
+ private static void AssertThrowIfNotSupported(Action test)
+ {
+ if (MLDsa.IsSupported)
+ {
+ test();
+ }
+ else
+ {
+ try
+ {
+ test();
+ }
+ catch (PlatformNotSupportedException pnse)
+ {
+ Assert.Contains("MLDsa", pnse.Message);
+ }
+ catch (ThrowsException te) when (te.InnerException is PlatformNotSupportedException pnse)
+ {
+ Assert.Contains("MLDsa", pnse.Message);
+ }
+ }
+ }
+
+ private static void WithDispose(T disposable, Action callback)
+ where T : IDisposable
+ {
+ using (disposable)
+ {
+ callback(disposable);
+ }
+ }
+
+ private static string WritePemRaw(string label, ReadOnlySpan data) =>
+ $"-----BEGIN {label}-----\n{data.ToString()}\n-----END {label}-----";
+
+ private static byte[] CreateAsn1EncodedBytes()
+ {
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.BER);
+ writer.WriteOctetString("some data"u8);
+ byte[] encodedBytes = writer.Encode();
+ return encodedBytes;
}
}
}
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs
new file mode 100644
index 00000000000000..4fd2909d0bbec0
--- /dev/null
+++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestHelpers.cs
@@ -0,0 +1,448 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+using System.Text;
+using Test.Cryptography;
+using Xunit;
+using Xunit.Sdk;
+
+namespace System.Security.Cryptography.Tests
+{
+ internal static class MLDsaTestHelpers
+ {
+ // DER encoding of ASN.1 BitString "foo"
+ internal static readonly ReadOnlyMemory s_derBitStringFoo = new byte[] { 0x03, 0x04, 0x00, 0x66, 0x6f, 0x6f };
+
+ internal static void VerifyDisposed(MLDsa mldsa)
+ {
+ PbeParameters pbeParams = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 10);
+
+ Assert.Throws(() => mldsa.SignData(ReadOnlySpan.Empty, new byte[mldsa.Algorithm.SignatureSizeInBytes]));
+ Assert.Throws(() => mldsa.VerifyData(ReadOnlySpan.Empty, new byte[mldsa.Algorithm.SignatureSizeInBytes]));
+
+ Assert.Throws(() => mldsa.ExportMLDsaPrivateSeed(new byte[mldsa.Algorithm.PrivateSeedSizeInBytes]));
+ Assert.Throws(() => mldsa.ExportMLDsaPublicKey(new byte[mldsa.Algorithm.PublicKeySizeInBytes]));
+ Assert.Throws(() => mldsa.ExportMLDsaSecretKey(new byte[mldsa.Algorithm.SecretKeySizeInBytes]));
+
+ Assert.Throws(() => mldsa.ExportPkcs8PrivateKey());
+ Assert.Throws(() => mldsa.TryExportPkcs8PrivateKey(new byte[10000], out _));
+ Assert.Throws(() => mldsa.ExportPkcs8PrivateKeyPem());
+
+ Assert.Throws(() => mldsa.ExportEncryptedPkcs8PrivateKey([1, 2, 3], pbeParams));
+ Assert.Throws(() => mldsa.ExportEncryptedPkcs8PrivateKey("123", pbeParams));
+ Assert.Throws(() => mldsa.TryExportEncryptedPkcs8PrivateKey([1, 2, 3], pbeParams, new byte[10000], out _));
+ Assert.Throws(() => mldsa.TryExportEncryptedPkcs8PrivateKey("123", pbeParams, new byte[10000], out _));
+
+ Assert.Throws(() => mldsa.ExportEncryptedPkcs8PrivateKeyPem([1, 2, 3], pbeParams));
+ Assert.Throws(() => mldsa.ExportEncryptedPkcs8PrivateKeyPem("123", pbeParams));
+
+ Assert.Throws(() => mldsa.ExportSubjectPublicKeyInfo());
+ Assert.Throws(() => mldsa.TryExportSubjectPublicKeyInfo(new byte[10000], out _));
+ Assert.Throws(() => mldsa.ExportSubjectPublicKeyInfoPem());
+ }
+
+ internal static void AssertImportPublicKey(Action> test, MLDsaAlgorithm algorithm, byte[] publicKey) =>
+ AssertImportPublicKey(test, test, algorithm, publicKey);
+
+ internal static void AssertImportPublicKey(Action> testDirectCall, Action> testEmbeddedCall, MLDsaAlgorithm algorithm, byte[] publicKey)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPublicKey(algorithm, publicKey));
+
+ if (publicKey?.Length == 0)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPublicKey(algorithm, Array.Empty().AsSpan()));
+ testDirectCall(() => MLDsa.ImportMLDsaPublicKey(algorithm, ReadOnlySpan.Empty));
+ }
+ else
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPublicKey(algorithm, publicKey.AsSpan()));
+ }
+
+ SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn
+ {
+ Algorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = AlgorithmToOid(algorithm),
+ Parameters = default(ReadOnlyMemory?),
+ },
+ SubjectPublicKey = publicKey,
+ };
+
+ AssertImportSubjectKeyPublicInfo(import => testEmbeddedCall(() => import(spki.Encode())));
+ }
+
+ internal delegate MLDsa ImportSubjectKeyPublicInfoCallback(byte[] spki);
+ internal static void AssertImportSubjectKeyPublicInfo(Action test) =>
+ AssertImportSubjectKeyPublicInfo(test, test);
+
+ internal static void AssertImportSubjectKeyPublicInfo(
+ Action testDirectCall,
+ Action testEmbeddedCall)
+ {
+ testDirectCall(spki => MLDsa.ImportSubjectPublicKeyInfo(spki));
+ testDirectCall(spki => MLDsa.ImportSubjectPublicKeyInfo(spki.AsSpan()));
+
+ testEmbeddedCall(spki => MLDsa.ImportFromPem(PemEncoding.WriteString("PUBLIC KEY", spki)));
+ testEmbeddedCall(spki => MLDsa.ImportFromPem(PemEncoding.WriteString("PUBLIC KEY", spki).AsSpan()));
+ }
+
+ internal static void AssertImportSecretKey(Action> test, MLDsaAlgorithm algorithm, byte[] secretKey) =>
+ AssertImportSecretKey(test, test, algorithm, secretKey);
+
+ internal static void AssertImportSecretKey(Action> testDirectCall, Action> testEmbeddedCall, MLDsaAlgorithm algorithm, byte[] secretKey)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaSecretKey(algorithm, secretKey));
+
+ if (secretKey?.Length == 0)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaSecretKey(algorithm, Array.Empty().AsSpan()));
+ testDirectCall(() => MLDsa.ImportMLDsaSecretKey(algorithm, ReadOnlySpan.Empty));
+ }
+ else
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaSecretKey(algorithm, secretKey.AsSpan()));
+ }
+
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ MLDsaPrivateKeyAsn privateKey = new MLDsaPrivateKeyAsn
+ {
+ ExpandedKey = secretKey
+ };
+ privateKey.Encode(writer);
+
+ PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn
+ {
+ PrivateKeyAlgorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = AlgorithmToOid(algorithm),
+ Parameters = default(ReadOnlyMemory?),
+ },
+ PrivateKey = writer.Encode(),
+ };
+
+ AssertImportPkcs8PrivateKey(import =>
+ testEmbeddedCall(() => import(pkcs8.Encode())));
+ }
+
+ internal static void AssertImportPrivateSeed(Action> test, MLDsaAlgorithm algorithm, byte[] secretKey) =>
+ AssertImportPrivateSeed(test, test, algorithm, secretKey);
+
+ internal static void AssertImportPrivateSeed(Action> testDirectCall, Action> testEmbeddedCall, MLDsaAlgorithm algorithm, byte[] privateSeed)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPrivateSeed(algorithm, privateSeed));
+
+ if (privateSeed?.Length == 0)
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPrivateSeed(algorithm, Array.Empty().AsSpan()));
+ testDirectCall(() => MLDsa.ImportMLDsaPrivateSeed(algorithm, ReadOnlySpan.Empty));
+ }
+ else
+ {
+ testDirectCall(() => MLDsa.ImportMLDsaPrivateSeed(algorithm, privateSeed.AsSpan()));
+ }
+
+ AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
+ MLDsaPrivateKeyAsn privateKey = new MLDsaPrivateKeyAsn
+ {
+ Seed = privateSeed,
+ };
+ privateKey.Encode(writer);
+
+ PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn
+ {
+ PrivateKeyAlgorithm = new AlgorithmIdentifierAsn
+ {
+ Algorithm = AlgorithmToOid(algorithm),
+ Parameters = default(ReadOnlyMemory?),
+ },
+ PrivateKey = writer.Encode(),
+ };
+
+ AssertImportPkcs8PrivateKey(import =>
+ testEmbeddedCall(() => import(pkcs8.Encode())));
+ }
+
+ internal delegate MLDsa ImportPkcs8PrivateKeyCallback(ReadOnlySpan pkcs8);
+ internal static void AssertImportPkcs8PrivateKey(Action callback) =>
+ AssertImportPkcs8PrivateKey(callback, callback);
+
+ internal static void AssertImportPkcs8PrivateKey(
+ Action testDirectCall,
+ Action testEmbeddedCall)
+ {
+ testDirectCall(pkcs8 => MLDsa.ImportPkcs8PrivateKey(pkcs8));
+ testDirectCall(pkcs8 => MLDsa.ImportPkcs8PrivateKey(pkcs8.ToArray()));
+
+ AssertImportFromPem(importPem =>
+ {
+ testEmbeddedCall(pkcs8 => importPem(PemEncoding.WriteString("PRIVATE KEY", pkcs8)));
+ });
+ }
+
+ internal static void AssertImportFromPem(Action> callback)
+ {
+ callback(static (string pem) => MLDsa.ImportFromPem(pem));
+ callback(static (string pem) => MLDsa.ImportFromPem(pem.AsSpan()));
+ }
+
+ internal static void AssertImportEncryptedPkcs8PrivateKey(
+ Action test,
+ EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All) =>
+ AssertImportEncryptedPkcs8PrivateKey(test, test, passwordTypeToTest);
+
+ internal delegate MLDsa ImportEncryptedPkcs8PrivateKeyCallback(string password, ReadOnlySpan pkcs8);
+ internal static void AssertImportEncryptedPkcs8PrivateKey(
+ Action testDirectCall,
+ Action testEmbeddedCall,
+ EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All)
+ {
+ if ((passwordTypeToTest & EncryptionPasswordType.Char) != 0)
+ {
+ testDirectCall((password, pkcs8) => MLDsa.ImportEncryptedPkcs8PrivateKey(password, pkcs8.ToArray()));
+ testDirectCall((password, pkcs8) => MLDsa.ImportEncryptedPkcs8PrivateKey(password.AsSpan(), pkcs8));
+ }
+
+ if ((passwordTypeToTest & EncryptionPasswordType.Byte) != 0)
+ {
+ testDirectCall((password, pkcs8) =>
+ MLDsa.ImportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), pkcs8.ToArray()));
+ }
+
+ AssertImportFromEncryptedPem(importPem =>
+ {
+ testEmbeddedCall((string password, ReadOnlySpan pkcs8) =>
+ {
+ string pem = PemEncoding.WriteString("ENCRYPTED PRIVATE KEY", pkcs8);
+ return importPem(pem, password);
+ });
+ }, passwordTypeToTest);
+ }
+
+ internal delegate MLDsa ImportFromEncryptedPemCallback(string source, string password);
+ internal static void AssertImportFromEncryptedPem(
+ Action callback,
+ EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All)
+ {
+ if ((passwordTypeToTest & EncryptionPasswordType.Char) != 0)
+ {
+ callback(static (string pem, string password) => MLDsa.ImportFromEncryptedPem(pem, password));
+ callback(static (string pem, string password) => MLDsa.ImportFromEncryptedPem(pem.AsSpan(), password));
+ }
+
+ if ((passwordTypeToTest & EncryptionPasswordType.Byte) != 0)
+ {
+ callback(static (string pem, string password) =>
+ MLDsa.ImportFromEncryptedPem(pem, Encoding.UTF8.GetBytes(password)));
+ callback(static (string pem, string password) =>
+ MLDsa.ImportFromEncryptedPem(pem.AsSpan(), Encoding.UTF8.GetBytes(password)));
+ }
+ }
+
+ internal static void AssertExportMLDsaPublicKey(Action> callback)
+ {
+ callback(mldsa =>
+ {
+ byte[] buffer = new byte[mldsa.Algorithm.PublicKeySizeInBytes];
+ mldsa.ExportMLDsaPublicKey(buffer.AsSpan());
+ return buffer;
+ });
+
+ AssertExportSubjectPublicKeyInfo(exportSpki =>
+ callback(mldsa =>
+ SubjectPublicKeyInfoAsn.Decode(exportSpki(mldsa), AsnEncodingRules.DER).SubjectPublicKey.Span.ToArray()));
+ }
+
+ internal static void AssertExportMLDsaSecretKey(Action> callback) =>
+ AssertExportMLDsaSecretKey(callback, callback);
+
+ internal static void AssertExportMLDsaSecretKey(Action> directCallback, Action> indirectCallback)
+ {
+ directCallback(mldsa =>
+ {
+ byte[] buffer = new byte[mldsa.Algorithm.SecretKeySizeInBytes];
+ mldsa.ExportMLDsaSecretKey(buffer.AsSpan());
+ return buffer;
+ });
+
+ AssertExportPkcs8PrivateKey(exportPkcs8 =>
+ indirectCallback(mldsa =>
+ MLDsaPrivateKeyAsn.Decode(
+ PrivateKeyInfoAsn.Decode(
+ exportPkcs8(mldsa), AsnEncodingRules.DER).PrivateKey, AsnEncodingRules.DER).ExpandedKey?.ToArray()));
+ }
+
+ internal static void AssertExportMLDsaPrivateSeed(Action> callback) =>
+ AssertExportMLDsaPrivateSeed(callback, callback);
+
+ internal static void AssertExportMLDsaPrivateSeed(Action> directCallback, Action> indirectCallback)
+ {
+ directCallback(mldsa =>
+ {
+ byte[] buffer = new byte[mldsa.Algorithm.PrivateSeedSizeInBytes];
+ mldsa.ExportMLDsaPrivateSeed(buffer.AsSpan());
+ return buffer;
+ });
+
+ AssertExportPkcs8PrivateKey(exportPkcs8 =>
+ indirectCallback(mldsa =>
+ MLDsaPrivateKeyAsn.Decode(
+ PrivateKeyInfoAsn.Decode(
+ exportPkcs8(mldsa), AsnEncodingRules.DER).PrivateKey, AsnEncodingRules.DER).Seed?.ToArray()));
+ }
+
+ internal static void AssertExportPkcs8PrivateKey(MLDsa mldsa, Action callback) =>
+ AssertExportPkcs8PrivateKey(export => callback(export(mldsa)));
+
+ internal static void AssertExportPkcs8PrivateKey(Action> callback)
+ {
+ callback(mldsa => DoTryUntilDone(mldsa.TryExportPkcs8PrivateKey));
+ callback(mldsa => mldsa.ExportPkcs8PrivateKey());
+ callback(mldsa => DecodePem(mldsa.ExportPkcs8PrivateKeyPem()));
+
+ static byte[] DecodePem(string pem)
+ {
+ PemFields fields = PemEncoding.Find(pem.AsSpan());
+ Assert.Equal(Index.FromStart(0), fields.Location.Start);
+ Assert.Equal(Index.FromStart(pem.Length), fields.Location.End);
+ Assert.Equal("PRIVATE KEY", pem.AsSpan()[fields.Label].ToString());
+ return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString());
+ }
+ }
+
+ internal static void AssertExportSubjectPublicKeyInfo(MLDsa mldsa, Action callback) =>
+ AssertExportSubjectPublicKeyInfo(export => callback(export(mldsa)));
+
+ internal static void AssertExportSubjectPublicKeyInfo(Action> callback)
+ {
+ callback(mldsa => DoTryUntilDone(mldsa.TryExportSubjectPublicKeyInfo));
+ callback(mldsa => mldsa.ExportSubjectPublicKeyInfo());
+ callback(mldsa => DecodePem(mldsa.ExportSubjectPublicKeyInfoPem()));
+
+ static byte[] DecodePem(string pem)
+ {
+ PemFields fields = PemEncoding.Find(pem.AsSpan());
+ Assert.Equal(Index.FromStart(0), fields.Location.Start);
+ Assert.Equal(Index.FromStart(pem.Length), fields.Location.End);
+ Assert.Equal("PUBLIC KEY", pem.AsSpan()[fields.Label].ToString());
+ return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString());
+ }
+ }
+
+ internal static void AssertEncryptedExportPkcs8PrivateKey(
+ MLDsa mldsa,
+ string password,
+ PbeParameters pbeParameters,
+ Action callback) =>
+ AssertEncryptedExportPkcs8PrivateKey(export => callback(export(mldsa, password, pbeParameters)));
+
+ internal delegate byte[] ExportEncryptedPkcs8PrivateKeyCallback(MLDsa mldsa, string password, PbeParameters pbeParameters);
+ internal static void AssertEncryptedExportPkcs8PrivateKey(
+ Action callback,
+ EncryptionPasswordType passwordTypesToTest = EncryptionPasswordType.All)
+ {
+ if ((passwordTypesToTest & EncryptionPasswordType.Char) != 0)
+ {
+ callback((mldsa, password, pbeParameters) =>
+ DoTryUntilDone((Span destination, out int bytesWritten) =>
+ mldsa.TryExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters, destination, out bytesWritten)));
+ callback((mldsa, password, pbeParameters) =>
+ DoTryUntilDone((Span destination, out int bytesWritten) =>
+ mldsa.TryExportEncryptedPkcs8PrivateKey(password, pbeParameters, destination, out bytesWritten)));
+
+ callback((mldsa, password, pbeParameters) => mldsa.ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters));
+ callback((mldsa, password, pbeParameters) => mldsa.ExportEncryptedPkcs8PrivateKey(password, pbeParameters));
+
+ callback((mldsa, password, pbeParameters) => DecodePem(mldsa.ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters)));
+ callback((mldsa, password, pbeParameters) => DecodePem(mldsa.ExportEncryptedPkcs8PrivateKeyPem(password, pbeParameters)));
+ }
+
+ if ((passwordTypesToTest & EncryptionPasswordType.Byte) != 0)
+ {
+ callback((mldsa, password, pbeParameters) =>
+ DoTryUntilDone((Span destination, out int bytesWritten) =>
+ mldsa.TryExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters, destination, out bytesWritten)));
+
+ callback((mldsa, password, pbeParameters) =>
+ mldsa.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters));
+
+ callback((mldsa, password, pbeParameters) =>
+ DecodePem(mldsa.ExportEncryptedPkcs8PrivateKeyPem(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters)));
+ }
+
+ static byte[] DecodePem(string pem)
+ {
+ PemFields fields = PemEncoding.Find(pem.AsSpan());
+ Assert.Equal(Index.FromStart(0), fields.Location.Start);
+ Assert.Equal(Index.FromStart(pem.Length), fields.Location.End);
+ Assert.Equal("ENCRYPTED PRIVATE KEY", pem.AsSpan()[fields.Label].ToString());
+ return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString());
+ }
+ }
+
+ internal delegate string ExportToPemCallback(MLDsa mldsa, string password, PbeParameters pbeParameters);
+ internal static void AssertExportToEncryptedPem(
+ Action callback,
+ EncryptionPasswordType passwordTypesToTest = EncryptionPasswordType.All)
+ {
+ if ((passwordTypesToTest & EncryptionPasswordType.Char) != 0)
+ {
+ callback((mldsa, password, pbeParameters) =>
+ mldsa.ExportEncryptedPkcs8PrivateKeyPem(password, pbeParameters));
+ callback((mldsa, password, pbeParameters) =>
+ mldsa.ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters));
+ }
+
+ if ((passwordTypesToTest & EncryptionPasswordType.Byte) != 0)
+ {
+ callback((mldsa, password, pbeParameters) =>
+ mldsa.ExportEncryptedPkcs8PrivateKeyPem(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters));
+ }
+ }
+
+ internal static void AssertExportToPrivateKeyPem(Action> callback) =>
+ callback(mldsa => mldsa.ExportPkcs8PrivateKeyPem());
+
+ internal static void AssertExportToPublicKeyPem(Action> callback) =>
+ callback(mldsa => mldsa.ExportSubjectPublicKeyInfoPem());
+
+ internal delegate bool TryExportFunc(Span destination, out int bytesWritten);
+ internal static byte[] DoTryUntilDone(TryExportFunc func)
+ {
+ byte[] buffer = new byte[512];
+ int written;
+
+ while (!func(buffer, out written))
+ {
+ Array.Resize(ref buffer, buffer.Length * 2);
+ }
+
+ return buffer.AsSpan(0, written).ToArray();
+ }
+
+ internal static string? AlgorithmToOid(MLDsaAlgorithm algorithm)
+ {
+ return algorithm?.Name switch
+ {
+ "ML-DSA-44" => "2.16.840.1.101.3.4.3.17",
+ "ML-DSA-65" => "2.16.840.1.101.3.4.3.18",
+ "ML-DSA-87" => "2.16.840.1.101.3.4.3.19",
+ _ => throw new XunitException("Unknown algorithm."),
+ };
+ }
+
+ internal static EncryptionPasswordType GetValidPasswordTypes(PbeParameters pbeParameters)
+ => pbeParameters.EncryptionAlgorithm == PbeEncryptionAlgorithm.TripleDes3KeyPkcs12
+ ? EncryptionPasswordType.Char
+ : EncryptionPasswordType.All;
+
+ [Flags]
+ internal enum EncryptionPasswordType
+ {
+ Byte = 1,
+ Char = 2,
+ All = Char | Byte,
+ }
+ }
+}
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs
index b6427cc4ef0b2b..ac2c839e2d8068 100644
--- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs
+++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs
@@ -8,31 +8,71 @@ namespace System.Security.Cryptography.Tests
internal sealed class MLDsaTestImplementation : MLDsa
{
internal delegate void ExportAction(Span destination);
+ internal delegate bool TryExportFunc(Span destination, out int bytesWritten);
internal delegate void SignAction(ReadOnlySpan data, ReadOnlySpan context, Span destination);
internal delegate bool VerifyFunc(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature);
+ internal int VerifyDataCoreCallCount = 0;
+ internal int SignDataCoreCallCount = 0;
+ internal int ExportMLDsaPrivateSeedCoreCallCount = 0;
+ internal int ExportMLDsaPublicKeyCoreCallCount = 0;
+ internal int ExportMLDsaSecretKeyCoreCallCount = 0;
+ internal int TryExportPkcs8PrivateKeyCoreCallCount = 0;
+ internal int DisposeCallCount = 0;
+
internal ExportAction ExportMLDsaPrivateSeedHook { get; set; }
internal ExportAction ExportMLDsaPublicKeyHook { get; set; }
internal ExportAction ExportMLDsaSecretKeyHook { get; set; }
+ internal TryExportFunc TryExportPkcs8PrivateKeyHook { get; set; }
internal SignAction SignDataHook { get; set; }
internal VerifyFunc VerifyDataHook { get; set; }
- internal Action DisposeHook { get; set; } = _ => { };
+ internal Action DisposeHook { get; set; }
private MLDsaTestImplementation(MLDsaAlgorithm algorithm) : base(algorithm)
{
}
- protected override void Dispose(bool disposing) => DisposeHook(disposing);
+ protected override void Dispose(bool disposing)
+ {
+ DisposeCallCount++;
+ DisposeHook(disposing);
+ }
+
+ protected override void ExportMLDsaPrivateSeedCore(Span destination)
+ {
+ ExportMLDsaPrivateSeedCoreCallCount++;
+ ExportMLDsaPrivateSeedHook(destination);
+ }
+
+ protected override void ExportMLDsaPublicKeyCore(Span destination)
+ {
+ ExportMLDsaPublicKeyCoreCallCount++;
+ ExportMLDsaPublicKeyHook(destination);
+ }
+
+ protected override void ExportMLDsaSecretKeyCore(Span destination)
+ {
+ ExportMLDsaSecretKeyCoreCallCount++;
+ ExportMLDsaSecretKeyHook(destination);
+ }
- protected override void ExportMLDsaPrivateSeedCore(Span destination) => ExportMLDsaPrivateSeedHook(destination);
- protected override void ExportMLDsaPublicKeyCore(Span destination) => ExportMLDsaPublicKeyHook(destination);
- protected override void ExportMLDsaSecretKeyCore(Span destination) => ExportMLDsaSecretKeyHook(destination);
+ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten)
+ {
+ TryExportPkcs8PrivateKeyCoreCallCount++;
+ return TryExportPkcs8PrivateKeyHook(destination, out bytesWritten);
+ }
- protected override void SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) =>
+ protected override void SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination)
+ {
+ SignDataCoreCallCount++;
SignDataHook(data, context, destination);
+ }
- protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) =>
- VerifyDataHook(data, context, signature);
+ protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature)
+ {
+ VerifyDataCoreCallCount++;
+ return VerifyDataHook(data, context, signature);
+ }
internal static MLDsaTestImplementation CreateOverriddenCoreMethodsFail(MLDsaAlgorithm algorithm)
{
@@ -43,6 +83,14 @@ internal static MLDsaTestImplementation CreateOverriddenCoreMethodsFail(MLDsaAlg
ExportMLDsaSecretKeyHook = _ => Assert.Fail(),
SignDataHook = (_, _, _) => Assert.Fail(),
VerifyDataHook = (_, _, _) => { Assert.Fail(); return false; },
+ DisposeHook = _ => { },
+
+ TryExportPkcs8PrivateKeyHook = (_, out bytesWritten) =>
+ {
+ Assert.Fail();
+ bytesWritten = 0;
+ return false;
+ },
};
}
@@ -55,6 +103,14 @@ internal static MLDsaTestImplementation CreateNoOp(MLDsaAlgorithm algorithm)
ExportMLDsaSecretKeyHook = d => d.Clear(),
SignDataHook = (data, context, destination) => destination.Clear(),
VerifyDataHook = (data, context, signature) => signature.IndexOfAnyExcept((byte)0) == -1,
+ DisposeHook = _ => { },
+
+ TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) =>
+ {
+ destination.Clear();
+ bytesWritten = destination.Length;
+ return true;
+ },
};
}
@@ -67,6 +123,176 @@ internal static MLDsaTestImplementation Wrap(MLDsa other)
ExportMLDsaSecretKeyHook = d => other.ExportMLDsaSecretKey(d),
SignDataHook = (data, context, destination) => other.SignData(data, destination, context),
VerifyDataHook = (data, context, signature) => other.VerifyData(data, signature, context),
+ DisposeHook = _ => other.Dispose(),
+
+ TryExportPkcs8PrivateKeyHook =
+ (Span destination, out int bytesWritten) =>
+ other.TryExportPkcs8PrivateKey(destination, out bytesWritten),
+ };
+ }
+
+ public void AddLengthAssertion()
+ {
+ ExportAction oldExportMLDsaPrivateSeedHook = ExportMLDsaPrivateSeedHook;
+ ExportMLDsaPrivateSeedHook = (Span destination) =>
+ {
+ oldExportMLDsaPrivateSeedHook(destination);
+ Assert.Equal(Algorithm.PrivateSeedSizeInBytes, destination.Length);
+ };
+
+ ExportAction oldExportMLDsaPublicKeyHook = ExportMLDsaPublicKeyHook;
+ ExportMLDsaPublicKeyHook = (Span destination) =>
+ {
+ oldExportMLDsaPublicKeyHook(destination);
+ Assert.Equal(Algorithm.PublicKeySizeInBytes, destination.Length);
+ };
+
+ ExportAction oldExportMLDsaSecretKeyHook = ExportMLDsaSecretKeyHook;
+ ExportMLDsaSecretKeyHook = (Span destination) =>
+ {
+ oldExportMLDsaSecretKeyHook(destination);
+ Assert.Equal(Algorithm.SecretKeySizeInBytes, destination.Length);
+ };
+
+ SignAction oldSignDataHook = SignDataHook;
+ SignDataHook = (ReadOnlySpan data, ReadOnlySpan context, Span destination) =>
+ {
+ oldSignDataHook(data, context, destination);
+ Assert.Equal(Algorithm.SignatureSizeInBytes, destination.Length);
+ };
+
+ VerifyFunc oldVerifyDataHook = VerifyDataHook;
+ VerifyDataHook = (ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) =>
+ {
+ bool ret = oldVerifyDataHook(data, context, signature);
+ Assert.Equal(Algorithm.SignatureSizeInBytes, signature.Length);
+ return ret;
+ };
+ }
+
+ public void AddDestinationBufferIsSameAssertion(ReadOnlyMemory buffer)
+ {
+ ExportAction oldExportMLDsaPrivateSeedHook = ExportMLDsaPrivateSeedHook;
+ ExportMLDsaPrivateSeedHook = (Span destination) =>
+ {
+ oldExportMLDsaPrivateSeedHook(destination);
+ AssertExtensions.Same(buffer.Span, destination);
+ };
+
+ ExportAction oldExportMLDsaPublicKeyHook = ExportMLDsaPublicKeyHook;
+ ExportMLDsaPublicKeyHook = (Span destination) =>
+ {
+ oldExportMLDsaPublicKeyHook(destination);
+ AssertExtensions.Same(buffer.Span, destination);
+ };
+
+ ExportAction oldExportMLDsaSecretKeyHook = ExportMLDsaSecretKeyHook;
+ ExportMLDsaSecretKeyHook = (Span destination) =>
+ {
+ oldExportMLDsaSecretKeyHook(destination);
+ AssertExtensions.Same(buffer.Span, destination);
+ };
+
+ SignAction oldSignDataHook = SignDataHook;
+ SignDataHook = (ReadOnlySpan data, ReadOnlySpan context, Span destination) =>
+ {
+ oldSignDataHook(data, context, destination);
+ AssertExtensions.Same(buffer.Span, destination);
+ };
+
+ TryExportFunc oldTryExportPkcs8PrivateKeyHook = TryExportPkcs8PrivateKeyHook;
+ TryExportPkcs8PrivateKeyHook = (Span