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 destination, out int bytesWritten) => + { + bool ret = oldTryExportPkcs8PrivateKeyHook(destination, out bytesWritten); + AssertExtensions.Same(buffer.Span, destination); + return ret; + }; + } + + public void AddContextBufferIsSameAssertion(ReadOnlyMemory buffer) + { + SignAction oldSignDataHook = SignDataHook; + SignDataHook = (ReadOnlySpan data, ReadOnlySpan context, Span destination) => + { + oldSignDataHook(data, context, destination); + AssertExtensions.Same(buffer.Span, context); + }; + + VerifyFunc oldVerifyDataHook = VerifyDataHook; + VerifyDataHook = (ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => + { + bool ret = oldVerifyDataHook(data, context, signature); + AssertExtensions.Same(buffer.Span, context); + return ret; + }; + } + + public void AddSignatureBufferIsSameAssertion(ReadOnlyMemory buffer) + { + VerifyFunc oldVerifyDataHook = VerifyDataHook; + VerifyDataHook = (ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => + { + bool ret = oldVerifyDataHook(data, context, signature); + AssertExtensions.Same(buffer.Span, signature); + return ret; + }; + } + + public void AddDataBufferIsSameAssertion(ReadOnlyMemory buffer) + { + SignAction oldSignDataHook = SignDataHook; + SignDataHook = (ReadOnlySpan data, ReadOnlySpan context, Span destination) => + { + oldSignDataHook(data, context, destination); + AssertExtensions.Same(buffer.Span, data); + }; + + VerifyFunc oldVerifyDataHook = VerifyDataHook; + VerifyDataHook = (ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => + { + bool ret = oldVerifyDataHook(data, context, signature); + AssertExtensions.Same(buffer.Span, data); + return ret; + }; + } + + public void AddFillDestination(byte b) + { + ExportAction oldExportMLDsaPrivateSeedHook = ExportMLDsaPrivateSeedHook; + ExportMLDsaPrivateSeedHook = (Span destination) => + { + oldExportMLDsaPrivateSeedHook(destination); + destination.Fill(b); + }; + + ExportAction oldExportMLDsaPublicKeyHook = ExportMLDsaPublicKeyHook; + ExportMLDsaPublicKeyHook = (Span destination) => + { + oldExportMLDsaPublicKeyHook(destination); + destination.Fill(b); + }; + + ExportAction oldExportMLDsaSecretKeyHook = ExportMLDsaSecretKeyHook; + ExportMLDsaSecretKeyHook = (Span destination) => + { + oldExportMLDsaSecretKeyHook(destination); + destination.Fill(b); + }; + + SignAction oldSignDataHook = SignDataHook; + SignDataHook = (ReadOnlySpan data, ReadOnlySpan context, Span destination) => + { + oldSignDataHook(data, context, destination); + destination.Fill(b); + }; + + TryExportFunc oldTryExportPkcs8PrivateKeyHook = TryExportPkcs8PrivateKeyHook; + TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + _ = oldTryExportPkcs8PrivateKeyHook(destination, out bytesWritten); + destination.Fill(b); + bytesWritten = destination.Length; + return true; }; } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTests.cs index 5cb78e93dcb98e..6c4d010bb00c51 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTests.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Formats.Asn1; +using System.Linq; +using System.Security.Cryptography.Asn1; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using Test.Cryptography; @@ -50,5 +53,641 @@ public static void DisposeIsCalledOnImplementation() mldsa.Dispose(); Assert.Equal(1, numberOfTimesDisposeCalled); } + + public static IEnumerable ArgumentValidationData => + from algorithm in MLDsaTestsData.AllMLDsaAlgorithms().Select(args => args.Single()) + from shouldDispose in new[] { true, false } + select new object[] { algorithm, shouldDispose }; + + [Theory] + [MemberData(nameof(ArgumentValidationData))] + public static void NullArgumentValidation(MLDsaAlgorithm algorithm, bool shouldDispose) + { + using MLDsa mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + if (shouldDispose) + { + // Test that argument validation exceptions take precedence over ObjectDisposedException + mldsa.Dispose(); + } + + PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42); + + AssertExtensions.Throws("password", () => mldsa.ExportEncryptedPkcs8PrivateKey((string)null, pbeParameters)); + AssertExtensions.Throws("password", () => mldsa.ExportEncryptedPkcs8PrivateKeyPem((string)null, pbeParameters)); + AssertExtensions.Throws("password", () => mldsa.TryExportEncryptedPkcs8PrivateKey((string)null, pbeParameters, Span.Empty, out _)); + + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKey(string.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.ExportEncryptedPkcs8PrivateKeyPem(string.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => mldsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null, Span.Empty, out _)); + AssertExtensions.Throws("pbeParameters", () => mldsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null, Span.Empty, out _)); + AssertExtensions.Throws("pbeParameters", () => mldsa.TryExportEncryptedPkcs8PrivateKey(string.Empty, null, Span.Empty, out _)); + } + + [Theory] + [MemberData(nameof(ArgumentValidationData))] + public static void ArgumentValidation(MLDsaAlgorithm algorithm, bool shouldDispose) + { + using MLDsa mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + int publicKeySize = algorithm.PublicKeySizeInBytes; + int secretKeySize = algorithm.SecretKeySizeInBytes; + int privateSeedSize = algorithm.PrivateSeedSizeInBytes; + int signatureSize = algorithm.SignatureSizeInBytes; + + if (shouldDispose) + { + // Test that argument validation exceptions take precedence over ObjectDisposedException + mldsa.Dispose(); + } + + AssertExtensions.Throws("destination", () => mldsa.ExportMLDsaPublicKey(new byte[publicKeySize - 1])); + AssertExtensions.Throws("destination", () => mldsa.ExportMLDsaSecretKey(new byte[secretKeySize - 1])); + AssertExtensions.Throws("destination", () => mldsa.ExportMLDsaPrivateSeed(new byte[privateSeedSize - 1])); + AssertExtensions.Throws("destination", () => mldsa.SignData(ReadOnlySpan.Empty, new byte[signatureSize - 1], ReadOnlySpan.Empty)); + + // Context length must be less than 256 + AssertExtensions.Throws("context", () => mldsa.SignData(ReadOnlySpan.Empty, new byte[signatureSize], new byte[256])); + AssertExtensions.Throws("context", () => mldsa.SignData(Array.Empty(), new byte[signatureSize], new byte[256])); + AssertExtensions.Throws("context", () => mldsa.VerifyData(ReadOnlySpan.Empty, new byte[signatureSize], new byte[256])); + AssertExtensions.Throws("context", () => mldsa.VerifyData(Array.Empty(), new byte[signatureSize], new byte[256])); + } + + [Theory] + [MemberData(nameof(ArgumentValidationData))] + public static void ArgumentValidation_PbeParameters(MLDsaAlgorithm algorithm, bool shouldDispose) + { + using MLDsa mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + if (shouldDispose) + { + // Test that argument validation exceptions take precedence over ObjectDisposedException + mldsa.Dispose(); + } + + MLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export => + { + // Unknown algorithm + AssertExtensions.Throws(() => + export(mldsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Unknown, HashAlgorithmName.SHA1, 42))); + + // TripleDes3KeyPkcs12 only works with SHA1 + AssertExtensions.Throws(() => + export(mldsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA512, 42))); + }); + + MLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export => + { + // Bytes not allowed in TripleDes3KeyPkcs12 + AssertExtensions.Throws(() => + export(mldsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42))); + }, MLDsaTestHelpers.EncryptionPasswordType.Byte); + } + + public static IEnumerable ApiWithDestinationSpanTestData => + from algorithm in MLDsaTestsData.AllMLDsaAlgorithms().Select(args => args.Single()) + from destinationLargerThanRequired in new[] { true, false } + select new object[] { algorithm, destinationLargerThanRequired }; + + private const int PaddingSize = 10; + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportMLDsaPublicKey_CallsCore(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + mldsa.ExportMLDsaPublicKeyHook = _ => { }; + mldsa.AddFillDestination(1); + + int publicKeySize = algorithm.PublicKeySizeInBytes; + byte[] publicKey = CreatePaddedFilledArray(publicKeySize, 42); + + // Extra bytes in destination buffer should not be touched + Memory destination = publicKey.AsMemory(PaddingSize, publicKeySize); + mldsa.AddDestinationBufferIsSameAssertion(destination); + + mldsa.ExportMLDsaPublicKey(destination.Span); + Assert.Equal(1, mldsa.ExportMLDsaPublicKeyCoreCallCount); + AssertExpectedFill(publicKey, fillElement: 1, paddingElement: 42, PaddingSize, publicKeySize); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportMLDsaSecretKey_CallsCore(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + mldsa.ExportMLDsaSecretKeyHook = _ => { }; + mldsa.AddFillDestination(1); + + int secretKeySize = algorithm.SecretKeySizeInBytes; + byte[] secretKey = CreatePaddedFilledArray(secretKeySize, 42); + + // Extra bytes in destination buffer should not be touched + Memory destination = secretKey.AsMemory(PaddingSize, secretKeySize); + mldsa.AddDestinationBufferIsSameAssertion(destination); + + mldsa.ExportMLDsaSecretKey(destination.Span); + Assert.Equal(1, mldsa.ExportMLDsaSecretKeyCoreCallCount); + AssertExpectedFill(secretKey, fillElement: 1, paddingElement: 42, PaddingSize, secretKeySize); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void SignData_CallsCore(MLDsaAlgorithm algorithm) + { + byte[] testData = [2]; + byte[] testContext = [3]; + + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + mldsa.SignDataHook = (_, _, _) => { }; + mldsa.AddDataBufferIsSameAssertion(testData); + mldsa.AddContextBufferIsSameAssertion(testContext); + mldsa.AddFillDestination(1); + + int signatureSize = algorithm.SignatureSizeInBytes; + byte[] signature = CreatePaddedFilledArray(signatureSize, 42); + + // Extra bytes in destination buffer should not be touched + Memory destination = signature.AsMemory(PaddingSize, signatureSize); + mldsa.AddDestinationBufferIsSameAssertion(destination); + + mldsa.SignData(testData, destination.Span, testContext); + Assert.Equal(1, mldsa.SignDataCoreCallCount); + AssertExpectedFill(signature, fillElement: 1, paddingElement: 42, PaddingSize, signatureSize); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void VerifyData_CallsCore(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + int signatureSize = algorithm.SignatureSizeInBytes; + byte[] testSignature = CreatePaddedFilledArray(signatureSize, 42); + byte[] testData = [2]; + byte[] testContext = [3]; + bool returnValue = false; + + mldsa.VerifyDataHook = (_, _, _) => returnValue; + mldsa.AddDataBufferIsSameAssertion(testData); + mldsa.AddContextBufferIsSameAssertion(testContext); + mldsa.AddSignatureBufferIsSameAssertion(testSignature.AsMemory(PaddingSize, signatureSize)); + + // Since `returnValue` is true, this shows the Core method doesn't get called for the wrong sized signature. + returnValue = true; + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, testSignature.AsSpan(PaddingSize, signatureSize - 1), testContext)); + Assert.Equal(0, mldsa.VerifyDataCoreCallCount); + + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, testSignature.AsSpan(PaddingSize, signatureSize + 1), testContext)); + Assert.Equal(0, mldsa.VerifyDataCoreCallCount); + + // But does for the right one. + AssertExtensions.TrueExpression(mldsa.VerifyData(testData, testSignature.AsSpan(PaddingSize, signatureSize), testContext)); + Assert.Equal(1, mldsa.VerifyDataCoreCallCount); + + // And just to prove that the Core method controls the answer... + returnValue = false; + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, testSignature.AsSpan(PaddingSize, signatureSize), testContext)); + Assert.Equal(2, mldsa.VerifyDataCoreCallCount); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void VerifyData_ByteArray_CallsCore(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + int signatureSize = algorithm.SignatureSizeInBytes; + byte[] testSignature = CreateFilledArray(signatureSize, 42); + byte[] testData = [2]; + byte[] testContext = [3]; + bool returnValue = false; + + mldsa.VerifyDataHook = (_, _, _) => returnValue; + mldsa.AddDataBufferIsSameAssertion(testData); + mldsa.AddContextBufferIsSameAssertion(testContext); + mldsa.AddSignatureBufferIsSameAssertion(testSignature); + + // Since `returnValue` is true, this shows the Core method doesn't get called for the wrong sized signature. + returnValue = true; + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, new byte[signatureSize - 1], testContext)); + Assert.Equal(0, mldsa.VerifyDataCoreCallCount); + + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, new byte[signatureSize - 1], testContext)); + Assert.Equal(0, mldsa.VerifyDataCoreCallCount); + + // But does for the right one. + AssertExtensions.TrueExpression(mldsa.VerifyData(testData, testSignature, testContext)); + Assert.Equal(1, mldsa.VerifyDataCoreCallCount); + + // And just to prove that the Core method controls the answer... + returnValue = false; + AssertExtensions.FalseExpression(mldsa.VerifyData(testData, testSignature, testContext)); + Assert.Equal(2, mldsa.VerifyDataCoreCallCount); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void Dispose_CallsVirtual(MLDsaAlgorithm algorithm) + { + MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + bool disposeCalled = false; + + // First Dispose call should invoke overridden Dispose should be called + mldsa.DisposeHook = (bool disposing) => + { + AssertExtensions.TrueExpression(disposing); + disposeCalled = true; + }; + + mldsa.Dispose(); + AssertExtensions.TrueExpression(disposeCalled); + + // Subsequent Dispose calls should be a no-op + mldsa.DisposeHook = _ => Assert.Fail(); + + mldsa.Dispose(); + mldsa.Dispose(); // no throw + + MLDsaTestHelpers.VerifyDisposed(mldsa); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_DoesNotCallExportMLDsaSecretKey(MLDsaAlgorithm algorithm) + { + byte[] secretKeyBytes = CreateFilledArray(algorithm.SecretKeySizeInBytes, 42); + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = MLDsaTestHelpers.AlgorithmToOid(algorithm), + Parameters = null, + }, + PrivateKey = secretKeyBytes, + }; + byte[] minimalEncoding = pkcs8.Encode(); + + MLDsaTestHelpers.AssertExportPkcs8PrivateKey(export => + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + // Override the TryExport method to return our test data + mldsa.TryExportPkcs8PrivateKeyHook = (dest, out bytesWritten) => + { + if (dest.Length >= minimalEncoding.Length) + { + minimalEncoding.CopyTo(dest); + bytesWritten = minimalEncoding.Length; + return true; + } + + bytesWritten = 0; + return false; + }; + + mldsa.AddLengthAssertion(); + + byte[] exported = export(mldsa); + + // Assert that the PKCS#8 private key is NOT generated with the secret key but from our test callback + Assert.Equal(0, mldsa.ExportMLDsaSecretKeyCoreCallCount); + AssertExtensions.GreaterThan(mldsa.TryExportPkcs8PrivateKeyCoreCallCount, 0); + + PrivateKeyInfoAsn exportedPkcs8 = PrivateKeyInfoAsn.Decode(exported, AsnEncodingRules.DER); + AssertExtensions.SequenceEqual(pkcs8.PrivateKey.Span, exportedPkcs8.PrivateKey.Span); + Assert.Equal(pkcs8.Version, exportedPkcs8.Version); + Assert.Equal(pkcs8.PrivateKeyAlgorithm.Algorithm, exportedPkcs8.PrivateKeyAlgorithm.Algorithm); + Assert.Equal(pkcs8.PrivateKeyAlgorithm.Parameters, exportedPkcs8.PrivateKeyAlgorithm.Parameters); + Assert.Equal(pkcs8.Attributes, exportedPkcs8.Attributes); // Null + }); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportSubjectPublicKeyInfo_CallsExportMLDsaPublicKey(MLDsaAlgorithm algorithm) + { + MLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export => + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + mldsa.ExportMLDsaPublicKeyHook = _ => { }; + mldsa.AddLengthAssertion(); + mldsa.AddFillDestination(1); + + byte[] exported = export(mldsa); + AssertExtensions.GreaterThan(mldsa.ExportMLDsaPublicKeyCoreCallCount, 0); + + SubjectPublicKeyInfoAsn exportedPkcs8 = SubjectPublicKeyInfoAsn.Decode(exported, AsnEncodingRules.DER); + AssertExtensions.SequenceEqual(CreateFilledArray(algorithm.PublicKeySizeInBytes, 1), exportedPkcs8.SubjectPublicKey.Span); + Assert.Equal(MLDsaTestHelpers.AlgorithmToOid(algorithm), exportedPkcs8.Algorithm.Algorithm); + AssertExtensions.FalseExpression(exportedPkcs8.Algorithm.Parameters.HasValue); + }); + } + + public static IEnumerable AlgorithmWithPbeParametersData => + from algorithm in MLDsaTestsData.AllMLDsaAlgorithms().Select(args => args.Single()) + from pbeParameters in new[] + { + new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42), + new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 1), + new PbeParameters(PbeEncryptionAlgorithm.Aes192Cbc, HashAlgorithmName.SHA384, 5), + new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA512, 10), + } + select new object[] { algorithm, pbeParameters }; + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void TryExportPkcs8PrivateKey_DestinationTooSmall(MLDsaAlgorithm algorithm) + { + const int MinimumOverhead = 12; + int lengthCutoff = algorithm.PrivateSeedSizeInBytes + MinimumOverhead; + + // First check that the length cutoff is enforced + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + byte[] privateKey = new byte[lengthCutoff]; + + // Early heuristic based bailout so no core methods are called + AssertExtensions.FalseExpression( + mldsa.TryExportPkcs8PrivateKey(privateKey.AsSpan(0, lengthCutoff - 1), out int bytesWritten)); + Assert.Equal(0, bytesWritten); + + // No bailout case: set up the core method + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + bytesWritten = destination.Length; + return true; + }; + + AssertExtensions.TrueExpression(mldsa.TryExportPkcs8PrivateKey(privateKey, out bytesWritten)); + Assert.Equal(privateKey.Length, bytesWritten); + + // Now check that the length cutoff permits a minimal encoding + // Build the minimal encoding: + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + using (writer.PushSequence()) + { + writer.WriteInteger(0); // Version + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(MLDsaTestHelpers.AlgorithmToOid(algorithm)); + } + + using (writer.PushOctetString()) + { + AsnWriter privateKeyWriter = new AsnWriter(AsnEncodingRules.DER); + privateKeyWriter.WriteOctetString(new byte[algorithm.PrivateSeedSizeInBytes]); + privateKeyWriter.CopyTo(writer); + } + } + + byte[] encodedMetadata = writer.Encode(); + + // Verify that a buffer of this size meets the length cutoff + AssertExtensions.LessThanOrEqualTo(lengthCutoff, encodedMetadata.Length); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_DestinationInitialSize(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + byte[] secretKeyBytes = CreateFilledArray(algorithm.SecretKeySizeInBytes, 42); + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44), + Parameters = null, + }, + PrivateKey = secretKeyBytes, + }; + + byte[] minimalEncoding = pkcs8.Encode(); + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + // The first call should at least be the size of the minimal encoding + bool ret = true; + AssertExtensions.TrueExpression(destination.Length >= minimalEncoding.Length); + minimalEncoding.CopyTo(destination); + bytesWritten = minimalEncoding.Length; + + // Before we return, update the next callback so subsequent calls fail the test + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return ret; + }; + + byte[] exported = mldsa.ExportPkcs8PrivateKey(); + PrivateKeyInfoAsn exportedPkcs8 = PrivateKeyInfoAsn.Decode(exported, AsnEncodingRules.DER); + + Assert.Equal(1, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + AssertExtensions.SequenceEqual(pkcs8.PrivateKey.Span, exportedPkcs8.PrivateKey.Span); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_Resizes(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + byte[] secretKeyBytes = CreateFilledArray(algorithm.SecretKeySizeInBytes, 42); + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = MLDsaTestHelpers.AlgorithmToOid(MLDsaAlgorithm.MLDsa44), + Parameters = null, + }, + PrivateKey = secretKeyBytes, + }; + + byte[] minimalEncoding = pkcs8.Encode(); + int originalSize = -1; + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + // Return false to force a resize + bool ret = false; + originalSize = destination.Length; + bytesWritten = 0; + + // Before we return false, update the callback so the next call will succeed + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + // New buffer must be larger than the original + bool ret = true; + AssertExtensions.GreaterThan(destination.Length, originalSize); + minimalEncoding.CopyTo(destination); + bytesWritten = minimalEncoding.Length; + + // Before we return, update the next callback so subsequent calls fail the test + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return ret; + }; + + return ret; + }; + + byte[] exported = mldsa.ExportPkcs8PrivateKey(); + PrivateKeyInfoAsn exportedPkcs8 = PrivateKeyInfoAsn.Decode(exported, AsnEncodingRules.DER); + + Assert.Equal(2, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + AssertExtensions.SequenceEqual(pkcs8.PrivateKey.Span, exportedPkcs8.PrivateKey.Span); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_IgnoreReturnValue(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + int[] valuesToWrite = [-1, 0, int.MaxValue]; + int index = 0; + + int finalDestinationSize = -1; + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + // Go through all the values we want to test, and once we reach the last one, + // return true with a valid value + if (index >= valuesToWrite.Length) + { + finalDestinationSize = bytesWritten = 1; + return true; + } + + // This returned value should should be ignored. There's no way to check + // what happens with it, but at the very least we should expect no exceptions + // and the correct number of calls. + bytesWritten = valuesToWrite[index]; + index++; + return false; + }; + + int actualSize = mldsa.ExportPkcs8PrivateKey().Length; + Assert.Equal(finalDestinationSize, actualSize); + Assert.Equal(valuesToWrite.Length + 1, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_HandleBadReturnValue(MLDsaAlgorithm algorithm) + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + Func getBadReturnValue = (int destinationLength) => destinationLength + 1; + MLDsaTestImplementation.TryExportFunc hook = (Span destination, out int bytesWritten) => + { + bool ret = true; + + bytesWritten = getBadReturnValue(destination.Length); + + // Before we return, update the next callback so subsequent calls fail the test + mldsa.TryExportPkcs8PrivateKeyHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return ret; + }; + + mldsa.TryExportPkcs8PrivateKeyHook = hook; + Assert.Throws(mldsa.ExportPkcs8PrivateKey); + Assert.Equal(1, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + + mldsa.TryExportPkcs8PrivateKeyHook = hook; + getBadReturnValue = (int destinationLength) => int.MaxValue; + Assert.Throws(mldsa.ExportPkcs8PrivateKey); + Assert.Equal(2, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + + mldsa.TryExportPkcs8PrivateKeyHook = hook; + getBadReturnValue = (int destinationLength) => -1; + Assert.Throws(mldsa.ExportPkcs8PrivateKey); + Assert.Equal(3, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.AllMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public static void ExportPkcs8PrivateKey_HandleBadReturnBuffer(MLDsaAlgorithm algorithm) + { + MLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(exportEncrypted => + { + using MLDsaTestImplementation mldsa = MLDsaTestImplementation.CreateOverriddenCoreMethodsFail(algorithm); + + // Create a bad encoding + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteBitString("some string"u8); + byte[] validEncoding = writer.Encode(); + Memory badEncoding = validEncoding.AsMemory(0, validEncoding.Length - 1); // Chop off the last byte + + MLDsaTestImplementation.TryExportFunc hook = (Span destination, out int bytesWritten) => + { + bool ret = badEncoding.Span.TryCopyTo(destination); + bytesWritten = ret ? badEncoding.Length : 0; + return ret; + }; + + mldsa.TryExportPkcs8PrivateKeyHook = hook; + + // Exporting the key should work without any issues because there's no validation + AssertExtensions.SequenceEqual(badEncoding.Span, mldsa.ExportPkcs8PrivateKey().AsSpan()); + + int numberOfCalls = mldsa.TryExportPkcs8PrivateKeyCoreCallCount; + mldsa.TryExportPkcs8PrivateKeyCoreCallCount = 0; + + // However, exporting the encrypted key should fail because it validates the PKCS#8 private key encoding first + AssertExtensions.Throws(() => + exportEncrypted(mldsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA1, 1))); + + // Sanity check that the code to export the private key was called + Assert.Equal(numberOfCalls, mldsa.TryExportPkcs8PrivateKeyCoreCallCount); + }); + } + + private static void AssertExpectedFill(ReadOnlySpan source, byte fillElement) => + AssertExpectedFill(source, fillElement, 255, 0, source.Length); + + private static void AssertExpectedFill(ReadOnlySpan source, byte fillElement, byte paddingElement, int startIndex, int length) + { + // Ensure that the data was filled correctly + AssertExtensions.FilledWith(fillElement, source.Slice(startIndex, length)); + + // And that the padding was not touched + AssertExtensions.FilledWith(paddingElement, source.Slice(0, startIndex)); + AssertExtensions.FilledWith(paddingElement, source.Slice(startIndex + length)); + } + + private static byte[] CreatePaddedFilledArray(int size, byte filling) + { + byte[] publicKey = new byte[size + 2 * PaddingSize]; + publicKey.AsSpan().Fill(filling); + return publicKey; + } + + private static byte[] CreateFilledArray(int size, byte filling) + { + byte[] publicKey = new byte[size]; + publicKey.AsSpan().Fill(filling); + return publicKey; + } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs index 39bdb8d91c33a3..eff4320b92fd1e 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsBase.cs @@ -220,13 +220,67 @@ public void NistImportSecretKeyVerifyExportsAndSignature(MLDsaNistTestCase testC Assert.Equal(testCase.ShouldPass, mldsa.VerifyData(testCase.Message, testCase.Signature, testCase.Context)); } + [Theory] + [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public void ImportPublicKey_Export(MLDsaKeyInfo info) + { + using MLDsa mldsa = ImportPublicKey(info.Algorithm, info.PublicKey); + + MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => + AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaSecretKey(export => + Assert.Throws(() => export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export => + Assert.Throws(() => export(mldsa))); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public void ImportPrivateKey_Export(MLDsaKeyInfo info) + { + using MLDsa mldsa = ImportSecretKey(info.Algorithm, info.SecretKey); + + MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => + AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaSecretKey(export => + AssertExtensions.SequenceEqual(info.SecretKey, export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed( + export => Assert.Throws(() => export(mldsa)), + // Seed is is not available in PKCS#8 + export => Assert.Null(export(mldsa))); + } + + [Theory] + [MemberData(nameof(MLDsaTestsData.IetfMLDsaAlgorithms), MemberType = typeof(MLDsaTestsData))] + public void ImportPrivateSeed_Export(MLDsaKeyInfo info) + { + using MLDsa mldsa = ImportPrivateSeed(info.Algorithm, info.PrivateSeed); + + MLDsaTestHelpers.AssertExportMLDsaPublicKey(export => + AssertExtensions.SequenceEqual(info.PublicKey, export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaSecretKey( + export => AssertExtensions.SequenceEqual(info.SecretKey, export(mldsa)), + // Seed is preferred in PKCS#8, so secret key won't be available + export => Assert.Null(export(mldsa))); + + MLDsaTestHelpers.AssertExportMLDsaPrivateSeed(export => + AssertExtensions.SequenceEqual(info.PrivateSeed, export(mldsa))); + } + protected static void ExerciseSuccessfulVerify(MLDsa mldsa, byte[] data, byte[] signature, byte[] context) { + ReadOnlySpan buffer = [0, 1, 2, 3]; + AssertExtensions.TrueExpression(mldsa.VerifyData(data, signature, context)); if (data.Length > 0) { - AssertExtensions.FalseExpression(mldsa.VerifyData([], signature, context)); + AssertExtensions.FalseExpression(mldsa.VerifyData(Array.Empty(), signature, context)); AssertExtensions.FalseExpression(mldsa.VerifyData(ReadOnlySpan.Empty, signature, context)); data[0] ^= 1; @@ -235,11 +289,11 @@ protected static void ExerciseSuccessfulVerify(MLDsa mldsa, byte[] data, byte[] } else { - AssertExtensions.TrueExpression(mldsa.VerifyData([], signature, context)); + AssertExtensions.TrueExpression(mldsa.VerifyData(Array.Empty(), signature, context)); AssertExtensions.TrueExpression(mldsa.VerifyData(ReadOnlySpan.Empty, signature, context)); - AssertExtensions.FalseExpression(mldsa.VerifyData([0], signature, context)); - AssertExtensions.FalseExpression(mldsa.VerifyData([1, 2, 3], signature, context)); + AssertExtensions.FalseExpression(mldsa.VerifyData(buffer.Slice(0, 1), signature, context)); + AssertExtensions.FalseExpression(mldsa.VerifyData(buffer.Slice(1, 3), signature, context)); } signature[0] ^= 1; @@ -248,7 +302,7 @@ protected static void ExerciseSuccessfulVerify(MLDsa mldsa, byte[] data, byte[] if (context.Length > 0) { - AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, [])); + AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, Array.Empty())); AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, ReadOnlySpan.Empty)); context[0] ^= 1; @@ -257,42 +311,14 @@ protected static void ExerciseSuccessfulVerify(MLDsa mldsa, byte[] data, byte[] } else { - AssertExtensions.TrueExpression(mldsa.VerifyData(data, signature, [])); + AssertExtensions.TrueExpression(mldsa.VerifyData(data, signature, Array.Empty())); AssertExtensions.TrueExpression(mldsa.VerifyData(data, signature, ReadOnlySpan.Empty)); - AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, [0])); - AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, [1, 2, 3])); + AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, buffer.Slice(0, 1))); + AssertExtensions.FalseExpression(mldsa.VerifyData(data, signature, buffer.Slice(1, 3))); } AssertExtensions.TrueExpression(mldsa.VerifyData(data, signature, context)); } - - protected static void VerifyDisposed(MLDsa mldsa) - { - PbeParameters pbeParams = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA256, 10); - - Assert.Throws(() => mldsa.SignData([], new byte[mldsa.Algorithm.SignatureSizeInBytes])); - Assert.Throws(() => mldsa.VerifyData([], 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()); - } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.Ietf.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.Ietf.cs new file mode 100644 index 00000000000000..65c1d38e2e1cce --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.Ietf.cs @@ -0,0 +1,1593 @@ +// 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.Text; +using Microsoft.DotNet.XUnitExtensions; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public static partial class MLDsaTestsData + { + // Data is from https://datatracker.ietf.org/doc/draft-ietf-lamps-dilithium-certificates/09/ + internal static partial MLDsaKeyInfo IetfMLDsa44 => field ??= new MLDsaKeyInfo( + MLDsaAlgorithm.MLDsa44, + "d7b2b47254aae0db45e7930d4a98d2c97d8f1397d17" + + "89dafa17024b316e9bec94fc9946d42f19b79a7413bbaa33e7149cb42ed51156" + + "93ac041facb988adeb5fe0e1d8631184995b592c397d2294e2e14f90aa414ba3" + + "826899ac43f4cccacbc26e9a832b95118d5cb433cbef9660b00138e0817f61e7" + + "62ca274c36ad554eb22aac1162e4ab01acba1e38c4efd8f80b65b333d0f72e55" + + "dfe71ce9c1ebb9889e7c56106c0fd73803a2aecfeafded7aa3cb2ceda54d12bd" + + "8cd36a78cf975943b47abd25e880ac452e5742ed1e8d1a82afa86e590c758c15" + + "ae4d2840d92bca1a5090f40496597fca7d8b9513f1a1bda6e950aaa98de46750" + + "7d4a4f5a4f0599216582c3572f62eda8905ab3581670c4a02777a33e0ca7295f" + + "d8f4ff6d1a0a3a7683d65f5f5f7fc60da023e826c5f92144c02f7d1ba1075987" + + "553ea9367fcd76d990b7fa99cd45afdb8836d43e459f5187df058479709a01ea" + + "6835935fa70460990cd3dc1ba401ba94bab1dde41ac67ab3319dcaca06048d4c" + + "4eef27ee13a9c17d0538f430f2d642dc2415660de78877d8d8abc72523978c04" + + "2e4285f4319846c44126242976844c10e556ba215b5a719e59d0c6b2a96d3985" + + "9071fdcc2cde7524a7bedae54e85b318e854e8fe2b2f3edfac9719128270aafd" + + "1e5044c3a4fdafd9ff31f90784b8e8e4596144a0daf586511d3d9962b9ea95af" + + "197b4e5fc60f2b1ed15de3a5bef5f89bdc79d91051d9b2816e74fa54531efdc1" + + "cbe74d448857f476bcd58f21c0b653b3b76a4e076a6559a302718555cc63f748" + + "59aabab925f023861ca8cd0f7badb2871f67d55326d7451135ad45f4a1ba6911" + + "8fbb2c8a30eec9392ef3f977066c9add5c710cc647b1514d217d958c7017c3e9" + + "0fd20c04e674b90486e9370a31a001d32f473979e4906749e7e477fa0b74508f" + + "8a5f2378312b83c25bd388ca0b0fff7478baf42b71667edaac97c46b129643e5" + + "86e5b055a0c211946d4f36e675bed5860fa042a315d9826164d6a9237c35a5fb" + + "f495490a5bd4df248b95c4aae7784b605673166ac4245b5b4b082a09e9323e62" + + "f2078c5b76783446defd736ad3a3702d49b089844900a61833397bc4419b30d7" + + "a97a0b387c1911474c4d41b53e32a977acb6f0ea75db65bb39e59e701e76957d" + + "ef6f2d44559c31a77122b5204e3b5c219f1688b14ed0bc0b801b3e6e82dcd43e" + + "9c0e9f41744cd9815bd1bc8820d8bb123f04facd1b1b685dd5a2b1b8dbbf3ed9" + + "33670f095a180b4f192d08b10b8fabbdfcc2b24518e32eea0a5e0c904ca84478" + + "0083f3b0cd2d0b8b6af67bc355b9494025dc7b0a78fa80e3a2dbfeb51328851d" + + "6078198e9493651ae787ec0251f922ba30e9f51df62a6d72784cf3dd20539317" + + "6dfa324a512bd94970a36dd34a514a86791f0eb36f0145b09ab64651b4a0313b" + + "299611a2a1c48891627598768a3114060ba4443486df51522a1ce88b30985c21" + + "6f8e6ed178dd567b304a0d4cafba882a28342f17a9aa26ae58db630083d2c358" + + "fdf566c3f5d62a428567bc9ea8ce95caa0f35474b0bfa8f339a250ab4dfcf208" + + "3be8eefbc1055e18fe15370eecb260566d83ff06b211aaec43ca29b54ccd00f8" + + "815a2465ef0b46515cc7e41f3124f09efff739309ab58b29a1459a00bce5038e" + + "938c9678f72eb0e4ee5fdaae66d9f8573fc97fc42b4959f4bf8b61d78433e86b" + + "0335d6e9191c4d8bf487b3905c108cfd6ac24b0ceb7dcb7cf51f84d0ed687b95" + + "eaeb1c533c06f0d97023d92a70825837b59ba6cb7d4e56b0a87c203862ae8f31" + + "5ba5925e8edefa679369a2202766151f16a965f9f81ece76cc070b55869e4db9" + + "784cf05c830b3242c8312", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "d7b2b47254aae0db45e7930d4a98d2c97d8f1397d178" + + "9dafa17024b316e9bec939ce0f7f77f8db5644dcda366bfe4734bd95f435ff9a" + + "613aa54aa41c2c694c04329a07b1fabb48f52a309f11a1898f848e2322ffe623" + + "ec810db3bee33685854a88269da320d5120bfcfe89a18e30f7114d83aa404a64" + + "6b6c997389860d12522ee0006e2384819186619b260d118664d4a62822184482" + + "402898146148a6614c4248a19208c2382951244808a125c2083108c471201409" + + "14836c18a78084106ec9c07022b56408b0610c07049812445188695900462293" + + "2041062e42b64c01164914284c41a85180460a5116515a0820022244dc9849d1" + + "3251e13065d3c08592a85112a1640039220946621cc70cd9086dd00626524085" + + "80443091062c50c80924c5841a966d4a982c99066da4443220a7645a326e11b5" + + "7020926124138e04852c0a4872c8a051d3082a99208058242024074e59148810" + + "a46460c06de0b28d1b1909203422c024410943710a212061a2015222521b8080" + + "9a340013934dd3322922170a9892691a14512027219cc02062a2814818691a85" + + "4d8344695b2041031242cb184601a90d0c023183b0215a224ac89205d9906904" + + "306a4b064ad2b2011c404081423252327254a6405a18100c321292c280521262" + + "5c82280bb46c03428d53100c14010ee1365288842491020a63462620062911c2" + + "28d0204802b36ca236095a8648cbb4618b4662c440821a890910024d24b24520" + + "122524c90588288cc9c04d5948220a276ec134644c90605b4450828649438804" + + "43b28c603080a2882d84a46d8ca629d0c68442064689885100a98d01498de438" + + "0da4068dd3947142b26c1a84611ba32842b42808a0711ac531e0a04c01376524" + + "2862142890091061d940221b3360090292d02481200408491844a3222d5c8844" + + "149808a446610195640b390a0c9450ca406ad2b220c0380182308e13b9089180" + + "84148829c0189112350da02422e20406d9c2850428121cc989180272d24029c2" + + "0812d8062a9994719bb8682384291a2289144511dc82445096450c4484c0b204" + + "9aa60543862c44326e88442120a84c9a3070e3b82d63268803254903438c48a8" + + "09ca147253344e1243081ba704593022d99480e234228142129c302a94342661" + + "04452426281346094a326d11280918b82562281113410d41b21190844c8b1212" + + "a2c688c9c030220606d2188e848630904452128831d9207113c52843060e0330" + + "60cca6845826524c88011ef72562c85ffa43acfa49217f2b172d7bbc14620e6d" + + "980a71aabbdf0c45e9a206ecb1423fee15decc17601300149d9223cd6e6c6e1f" + + "a8e41fc7c64938ab68905fd3dcda50d87082e7d0d71d1bc9b2b84c85523ca8fe" + + "6cad294adf83be15b108ff721d0cc87bc3dd3a7590184b0e845663a91fc9e1c3" + + "c53a61d867420b04f092355753bc65a06368fd41295fd09924132c6f91f67964" + + "c142674a725c343914c4cecf58c074bcaf4558c97bf7911e07aa6d0938f2ee2b" + + "b3c1a8c595d635e84342fdea01dc24b211ad2fc281cf77e59110c7abc54bf0c8" + + "6d480b9be276471dc9d603cee98cfdab3e9fcfb703793560549ea4450fa7b33f" + + "b9169c44b4d25fb9c457f49791cd3da03eac96095813c105132ccda4e63e4922" + + "8cd23d8a1f37856f142d93b90db09f82af89258c63aab8047a80c036c9357ea2" + + "046f8dc6354f0c5295f342bb417d3cfeb0b1fd33622c29e14cbbd92e1363c65e" + + "bd4504b7512329b9670e32e1b2c67a54e7f1a55f8b9f9ea04e8ca3a705e62a3c" + + "5e637374afb7aeb6ddea612cde28f01a202d7aa4e34722d27dd3f9b89894d019" + + "fd5d4d7119efe3723bba104cb8bb0981e074de3afe200daaaead826cc45f244d" + + "bf431afab34efbdf782474d2fd57118f646214934ed99cba3b003e8d67a3836f" + + "6f19fc41910ce5163ee3ae99eb84d514eb761e63684ea56f9791d2dd4aac6e61" + + "68b948c817f75a222acb0e8cdc03cc4afe8f67157e1a363b7faeff9f172b9891" + + "3677c5a1dd085e9ee4c22052c1af58193116673dcd3bfc5f34b855dcc6c77885" + + "649e9e71f43d4aea0f4b72ca7eda0578ba13d31a658d2d060a9a66ff69ed1be7" + + "997a2fb1d2723d38f9bfabe18f8e7b3cda906e4e9b5e942c8eaeb296070ebfd3" + + "64947a940cc978bed66b37749e6d5dcd7be8c494440e2b84cecfefb98c0bedfb" + + "3c41e3359d2cd7197fbe720c48aa6c6b6465c1ee63e3569c2adc744491370b7f" + + "7826fe0b77a1d19d64101d032b918106b42d2ef73747e5601fe4ba50f23ede52" + + "1f031a817d15294a43722e8378784b6db0cf1ba9e8ae911d9201b9ce9cc3019c" + + "6f5c27cb98da26144b64225a7c932b30f761e78a2d59a1d8b83ec6344a2f6dd4" + + "7e765706d00bf4a79a6a926c3ba91d812c8f2c797ab1796709e5d16856778293" + + "529f0286d015c3b5399619642a333e9e593d6e3f5353994208e9e6a332851d7f" + + "652522a928b917e27e2d6d42137dfe2ebfa6fb1c67b26c0254528685f7ebdbe3" + + "15a68eaa2da769e8a9f42d3e60007c71330926b2c0012d83ead4e4fd1ed872cc" + + "d1972201d2b027f3545ac2d30cd78bc1d740feccbc6fc2a0446c6e30eac51f5a" + + "69098aa2d447f2085b4e4e4b92ccc26921d2de478518cd090ce267aea2d27ada" + + "57fd88b4976d89fb843cdccf49a76ca2679e6801bfa7fb031896fb50629704b9" + + "923936bb5dd385311121cadfb11995e59b73034cf67ed03ab813867648d02582" + + "8087e949a9afd16b95d72d99b1edca257aac132ffb7a0709aed5a9c0ff05fb0f" + + "2bbf28409eed7b5f5801be964ced019e1cb7851d3851f10290674e19ffb008b3" + + "01c4acf641a2bb14216e1d69cabf52b5ef227496b0f30799a855d117fad3744a" + + "6fa33503ea798b52ddd7ee5426609dbfcd3f0c13b164d6c051f7ed4a119719a7" + + "12e388d328402081ff1354b554d2c237afed3b151c4ba8e9f4bdeb8499a3066e" + + "26bbc69e8af089dec71731d1dc529eab17ef7374734c0fe475494c83836bdd34" + + "a03b9bc89914716061bfb98ec6e61c3ed4438edcaf25243c647086b9ea7018b0" + + "d9a8a0b00cecb00abde2498d69c2336101a772cbe4f571523f51bd05882cdf35" + + "8b849cc140aa1faf22423a12851ce0e33fd48975a4959fa5c5fe418c93908191" + + "ab6e741b77bfe02cbd698ee795c466d615619e6441382c6eac01834ee9ab73ce" + + "a80bbe235c78da91bd79b6f82f899785d68700d393e675c2224d6b7a1ad21320" + + "495679adaed70167b50866713a53109db7b6f7d81304ecdfd83b319b1ef24830" + + "6b45ad29e7ddcc863dac56048b5d69ea175011f7614c00a86a863cde1872a893" + + "2878b9ac7e1ac5bda4997b72064f0cd75f4c814e034de11acb9013cf7ea926b4" + + "e7eaace070c7ba2188efad2e431e1223d45dd05c4d8403c2e45cee6413ecbe75" + + "27e873e455c4e610a61839aacc0bd56d2483e78f298b66a478eb2f558cbafca8" + + "6be847baeb02c5b216c8cd88fea4df249b09e670a20703abac24b0a91abc4a56" + + "46601442ba10becfd30993880051d07f56a05a9379e7a8e6befee3f22faa1063" + + "98f7706006e42e9be1ef89d25c272f11a95095c587d713732284de9dbd3c7217" + + "b0689e21d8eb0ff69668", + "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 + 1MTYHSKY1nFQFmVTM85HDnBXIsDg + """, + """ + MIIPlDCCBgqgAwIBAgIUFZ/+byL9XMQsUk32/V4o0N44804wCwYJYIZIAWUDBAMR + MCIxDTALBgNVBAoTBElFVEYxETAPBgNVBAMTCExBTVBTIFdHMB4XDTIwMDIwMzA0 + MzIxMFoXDTQwMDEyOTA0MzIxMFowIjENMAsGA1UEChMESUVURjERMA8GA1UEAxMI + TEFNUFMgV0cwggUyMAsGCWCGSAFlAwQDEQOCBSEA17K0clSq4NtF55MNSpjSyX2P + E5fReJ2voXAksxbpvslPyZRtQvGbeadBO7qjPnFJy0LtURVpOsBB+suYit61/g4d + hjEYSZW1ksOX0ilOLhT5CqQUujgmiZrEP0zMrLwm6agyuVEY1ctDPL75ZgsAE44I + F/YediyidMNq1VTrIqrBFi5KsBrLoeOMTv2PgLZbMz0PcuVd/nHOnB67mInnxWEG + wP1zgDoq7P6v3teqPLLO2lTRK9jNNqeM+XWUO0er0l6ICsRS5XQu0ejRqCr6huWQ + x1jBWuTShA2SvKGlCQ9ASWWX/KfYuVE/GhvabpUKqpjeRnUH1KT1pPBZkhZYLDVy + 9i7aiQWrNYFnDEoCd3oz4Mpylf2PT/bRoKOnaD1l9fX3/GDaAj6CbF+SFEwC99G6 + EHWYdVPqk2f8122ZC3+pnNRa/biDbUPkWfUYffBYR5cJoB6mg1k1+nBGCZDNPcG6 + QBupS6sd3kGsZ6szGdysoGBI1MTu8n7hOpwX0FOPQw8tZC3CQVZg3niHfY2KvHJS + OXjAQuQoX0MZhGxEEmJCl2hEwQ5Va6IVtacZ5Z0MayqW05hZBx/cws3nUkp77a5U + 6FsxjoVOj+Ky8+36yXGRKCcKr9HlBEw6T9r9n/MfkHhLjo5FlhRKDa9YZRHT2ZYr + nqla8Ze05fxg8rHtFd46W+9fib3HnZEFHZsoFudPpUUx79wcvnTUSIV/R2vNWPIc + C2U7O3ak4HamVZowJxhVXMY/dIWaq6uSXwI4YcqM0Pe62yhx9n1VMm10URNa1F9K + G6aRGPuyyKMO7JOS7z+XcGbJrdXHEMxkexUU0hfZWMcBfD6Q/SDATmdLkEhuk3Cj + GgAdMvRzl55JBnSefkd/oLdFCPil8jeDErg8Jb04jKCw//dHi69CtxZn7arJfEax + KWQ+WG5bBVoMIRlG1PNuZ1vtWGD6BCoxXZgmFk1qkjfDWl+/SVSQpb1N8ki5XEqu + d4S2BWcxZqxCRbW0sIKgnpMj5i8geMW3Z4NEbe/XNq06NwLUmwiYRJAKYYMzl7xE + GbMNepegs4fBkRR0xNQbU+Mql3rLbw6nXbZbs55Z5wHnaVfe9vLURVnDGncSK1IE + 47XCGfFoixTtC8C4AbPm6C3NQ+nA6fQXRM2YFb0byIINi7Ej8E+s0bG2hd1aKxuN + u/PtkzZw8JWhgLTxktCLELj6u9/MKyRRjjLuoKXgyQTKhEeACD87DNLQuLavZ7w1 + W5SUAl3HsKePqA46Lb/rUTKIUdYHgZjpSTZRrnh+wCUfkiujDp9R32Km1yeEzz3S + BTkxdt+jJKUSvZSXCjbdNKUUqGeR8Os28BRbCatkZRtKAxOymWEaKhxIiRYnWYdo + oxFAYLpEQ0ht9RUioc6IswmFwhb45u0XjdVnswSg1Mr7qIKig0LxepqiauWNtjAI + PSw1j99WbD9dYqQoVnvJ6ozpXKoPNUdLC/qPM5olCrTfzyCDvo7vvBBV4Y/hU3Du + yyYFZtg/8GshGq7EPKKbVMzQD4gVokZe8LRlFcx+QfMSTwnv/3OTCatYspoUWaAL + zlA46TjJZ49y6w5O5f2q5m2fhXP8l/xCtJWfS/i2HXhDPoawM11ukZHE2L9IezkF + wQjP1qwksM633LfPUfhNDtaHuV6uscUzwG8NlwI9kqcIJYN7Wbpst9TlawqHwgOG + KujzFbpZJejt76Z5NpoiAnZhUfFqll+fgeznbMBwtVhp5NuXhM8FyDCzJCyDEqNC + MEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDKa + B7H6u0j1KjCfEaGJj4SOIyL/MAsGCWCGSAFlAwQDEQOCCXUAZ6iVH8MI4S9oZ2Ef + 3CVL9Ly1FPf18v3rcvqOGgMAYWd7hM0nVZfYMVQZWWaxQWcMsOiBE0YNl4oaejiV + wRykGZV3XAnWTd60e8h8TovxyTJ/xK/Vw3hlU+F9YpsPJxQnZUgUMrXnzNC6YeUc + rT3Y+Vk4wjXr7O6vixauM2bzAMU1jse+nrI6HqGj2lhoZwTwSD+Wim5LH4lnCgE0 + s2oY1scn3JsCexJ5R5OkjHq2bt9XrBgRORTADQoRtlplL0d3Eze/dDZm/Klby9OR + Ia4HUL7FWtWoy86Y5TiuUjlH1pKZdjMPyj/JXAHRQDtJ5cuoGBL0NlDdATEJNCee + zQfMqzTCyjCn091QkuFjDhQjzJ+sQ6G02w49lw8Kpm1ASuh7BLTPcuz7Z+rLpNjN + jmW67rR6+hHMK474mSKIZnuO3vVKnidntjLhSYc1soxvYPCLWWnl4m3XyjlrnlzD + 4Soec2I2AjKNZKCO9KKa81cRzIcNJjc7sbnrLv/hKXNUTESn4s3yAyRPU7N6bVIy + N9ifBvb1U07WMRPI8A7/f9zVCaLYx87ym9P7GGpMjDYrPUQpOaKQdu4ycWuPrlEA + 2BoHIVzbHHm9373BT1LjcxjR5SbbhNFg+42hwG284VlVzcLW/XiipaWN8jnONmxt + kLMui9R/wf0TCehilMDDtRznfm37b2ci5o9MP/LrTDRpMVBudDuwIZmLgPQ/bj08 + n+VHd8D2WADpR/kEMpDhSwG2P44mwwE4CUKGbHS0qQLOSRwMlQVEzwxpOOrLMusw + JmzoLE0KNsUR6o/3xAlUmjqCZMqYPYxtXgNfJEJDp3V1iqyZK1iES3EQ0/h8m7oZ + 3YqNKrEpTgVV7EmVpUjcVszjWgXcSKynVVsWQd3j0Zf83zXRLwmq8+anJ3XNGCSa + IecO2sZxDbaiHhwFYRkt0BGRM2QM//IPMYeXhRa/1svmbOEHGxJG9LqTffkBs+01 + Bp7r3/9lRZ+5t3eukpinpJrCT0AgeV3l3ujbzyCiQbboFDaPS4+kKvi+iS2eHjiu + S/WkfP1Go5jksxhkceJFNPsTmGCyXGPy2/haU9hkiMg9/wmuIKm/gxRfIBh/DoIr + 1HWZjTuWcBGWTu2NuXeAVO/MbMtpB0u6mWYktHQcVxA2LenU+N5LEPbbHp+AmPQC + RZPqBziTyx/nuVnFD+/EAbPKzeqMKhcTW6nfkKt/Md4zmi1vhWxx7c+wDlo9cyAf + vsS0p5uXKK1wzaC4mBIVdPYNlZtAjBCK8asKpH3/NyYJ8xhsBjxXLLiQifKiGOpA + LLBy/LyJWmo4R4zkAtUILD4FcsIyLMIJlsqWjaNdey7bwGI75hZQkBIF8QJxFVtT + n4HQBtuNe2ek7e72d+bayceJvlUAFXTu6oeX9/UuS7AhuY4giNzI1pNOgNwWXRxx + REmwvPrzJatZZ7cwfsKTezSSQlv2O4q70+2X2h0VtUg/pkz3GknE07S3ggDR9Qkg + bywQS/42luPIADbbAKXhHaBaX/TaD/uZVn+BOZ5sqWmxEbbHtvzlSea02J1Fk4Hq + kWbpuzByCJ25SuDRr+Xyn84ZDnetumQ0lBkc2ro+rZKXw8YGMyt0aX8ZwJxL4qNB + /WFFEproVsOru8G7iwXgt4QP8WRBSp2kTlQUbNTF3gxOTsslkUErTnvcRQ0GpK06 + DRQG8wbjgewpHyw7O8Sfi34EjAzic0gwtIp501/MWmKpRUgAow9LPreiaLq2TBIQ + DXEhUb9fEhY77QKeir8cpue3sShqcz9TLa5REJGqsP/8/URk7lZjiI+YWbRLp2U2 + D//0NPEq8fxrzNtacZRxSdx2id/yTWumtj5swjFA4yk0tunadltDMgEYuKgR+Jw9 + G3/yFTDnepHK41V6x8eE/4JjUAvIJWADDWxudO7oF/wsY0AnUuWe9DkW09g8IWhk + NukDTdpsl08hCLF06qH3MSHJrdUAzs2GGLMCvtrXK2L3k70PcLqMXhbPSr7d1RGW + gW0BlRfR4l+2LJ952SMv3xzuxgT43aX3FFVBxXk7nFrhWJWIpJpuYXRhTqASkzoZ + KzsIRyW0ZbsaIsy0tgzzyhQvdoOoJn+2sKjcCzpfY6tgRD9sfucOm1sGet/cM5YP + iJYei2qKMeYcvACWiI8GNGY37OzhlikbleO4xXnfJwEOYx66NjTHZqkz1/TiCBGU + a7h+l/fnut6VfkxS1yZ2r5Gsdx7DUfNkEeKyzIMnYRA3zw3047lHqH714rV5VbE3 + yYEQWvdtYlHMFM2z9DDta59RRATOemm7AA1fYsfodrV/QPJi5qPmvpHtCvfItbdL + Fg88Zh1zV5nV+0doUTXFVR9poJRE9fASlfU5qCJ9Jx5ISfvIkGz1fmfqXhUN9fE7 + C0Evl7IYQLguTXFznRvsXvnliwR9Ut/g85JtXUiku4F2ThCBMHBDbov6p128kP+2 + 7LBgShM4IG80clxon8sWh6y0RLUz1MTamEYZKCXAPZzJoWhbzdNns/QTsjNP8wlu + vBRtdkb6w4Vrm6GO2BXY6pQUBPcoDuymAhfAF9TxRn860OQeMcT/NRsU9Z/8nRnz + 3KbAuMTYsQ6qbjuLTDwfF9B4b4YUDQR22z8wlzCNLzgwFlGSI12xhf3ejRlwjGZJ + J/11Up4pEegRS/c+Li2OUvQr9Jxi8XGIdEJZY1T8oVpzDJf3C29gpARWSDAXrFn0 + lgZHnqFyebeC1uDW8r/wGtYmI2EC53+FlOF5AFcH+3LzObZzerqwror4UMOA+B5c + QMU5vDv1LFcWLzvJHMXJfCHL5nVSukXCMawr+DbeKjrkseG0UX0gpUbQy0vHIH1K + 2geD2xyl3TJ8jCaKOxb/Hu+KfkvtOCsh07TA+cnTV1WHR77svUcMErzHXWOFm8+U + omIXALO1EiDbpu38gERRLkC84eMhRBQjKcdmlcBFsmilt3cfIofypuhMRiIFjIke + 00y2GEdQVsZGA/LX1HILqD4dEFDDQI2LPvCG5qe28HTfWspzsqK94IRESzm+Vmdp + IjNzkTyrPI06yMvxaHGajwUtLWCReJOG/uXhswbX7EviVYyqCR4vzDLDVXAulxo/ + OsHaQhMX8xYOLXontx7SNCBlu/EEBww5QklKUldgd5igr7bDxsvZ6vHy/wcNIzY3 + RUdidnuDkpSm1hIoLz4/SW2Tm6C2u9La5evu7xAfIy1ul8LE3/P0AAAAAAAAAAAA + AAAAABcmOEM= + """, + "PLACEHOLDER", + new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, 2)); + + internal static partial MLDsaKeyInfo IetfMLDsa65 => field ??= new MLDsaKeyInfo( + MLDsaAlgorithm.MLDsa65, + "48683d91978e31eb3dddb8b0473482d2b88a5f62594" + + "9fd8f58a561e696bd4c27d05b38dbb2edf01e664efd81be1ea893688ce68aa2d" + + "51c5958f8bbc6eb4e89ee67d2c0320954d57212cac7229ff1d6eaf03928bd515" + + "11f8d88d847736c7de2730d5978e5410713160978867711bf5539a0bfc4c350c" + + "2be572baf0ee2e2fb16ccfea08028d99ac49aebb75937ddce111cdab62fff3ce" + + "a8ba2233d1e56fbc5c5a1e726de63fadd2af016b119177fa3d971a2d9277173f" + + "ce55b67745af0b7c21d597dbeb93e6a32f341c49a5a8be9e825088d1f2aa4515" + + "5d6c8ae15367e4eb003b8fdf7851071949739f9fff09023eaf45104d2a84a459" + + "06eed4671a44dc28d27987bb55df69e9e8561f61a80a72699503865fed9b7ee7" + + "2a8e17a19c408144f4b29afef7031c3a6d8571610b42c9f421245a88f197e168" + + "12b031159b65b9687e5b3e934c5225ae98a79ba73d2b399d73510effad19e53b" + + "8450f0ba8fce1012fd98d260a74aaaa13fae249a006b1c34f5ba0b882f263782" + + "22fb36f2283c243f0ffeb5f1bb414a0a70d55e3d40a56b6cbc88ae1f03b7b288" + + "2d98deea28e145c9dedfd8eaf1cef2ed94a8b050f8964f46d1ea0d0c2a43e0dd" + + "a6182adbf4f6ed175b6742257859bf22f3a417ecf1f9d89317b5e539d587af16" + + "b9e1313e04514ffa64ba8b3ff2b8321f8811cb3fb022c8f644e70a4b80a2fbfe" + + "e604abb7379091ea8e6c5c74dfc0283666b40c0793870028204a136bf5da9568" + + "eb798d349038bdb0c11e03445e7847cb5069c75cf28ac601c7799d958210ddbc" + + "b226e51afef9f1de47b073873d6d3f97456bede085082e74a298b2cd48f4b309" + + "3155f366c8fa601c6af858dfa32c08491b2a29887f90335949a5d6edaa679882" + + "a3a95d6bf6d970a221f4b9d3d8cbf384af81aac95e2b3294e04789ac83727a5d" + + "c04559f96af41d8a053516feeeebc52746eb6ab2819e09108710d835f011fa63" + + "065872ad334d5cdffb2b2310507e92fc993ae317da97f4f309cdaf0f67ed99d9" + + "0215576083849f953b246d7fedb3fdb67679850a5ad404e64147fb7cf4f6aedd" + + "d05afb4b834968d1fe88014960dce5d942236526e12a478d69e5fbe6970310b3" + + "08c06845018cfc7b2ab430a13a6b1ac7bb02cccbb3d911ac2f11068613fbe029" + + "bfdce02cf5cd38950ed72c83944edfbc75615af87f864c051f3c55456c541286" + + "3a40c06d1dab562bdff0571b8d3c3917bbd300880bba5e998239b95fa91b7d64" + + "16d4f398b3adbcd30983ed3592b4d9ef7d4236fd00f50d98aa53a235ac417272" + + "0f77d96172672980cfe8ff7a5a702783edc2ba31b2259015a112fc7f468a9c2f" + + "9464039002d30ef678b4cb798bc116216bf7a9a7c18ba03b7b58fd07515d3115" + + "049d3614be7a07e744300750df1d2c58753389059eafc3d785ccdd31c07648be" + + "dc03a5c3b8ad46d064d59c13d57374729fc4e295362e2a5191204530428bc152" + + "2afa28ff5fe1655e304ca5bc8c27ad0e0c6a39dd4df28956c14b38cc93682cef" + + "e402bbd5e82d29c464e44eb5d37b48fc568dfe0cc6e8e16baea05e5135590f19" + + "294e73e8367b0216dbb815030b9de55913f08039c42351c59e5515dd5af8e089" + + "a15e625e8f6dee639386c46497d7a263288774de581a7de9629b41b4424141f9" + + "78fb8331208efdec3c6e0de39bc57063f3dcd6c470373c08891ea29cbc7cc6d6" + + "483b8889083ace86aa7b51b1c2cfe6e2ad18d97ce36fbc56ea42fae97e6a7ac1" + + "14864478c366df1ebb1e7b11a9098504fd5975bdf1f49dc70002b63c1739a9d2" + + "63fbad4073f6a9f6c2b8af4b4c332a103a0cffa5deeb2d062ca3c215fd360026" + + "be7c5164f4a4424ef74948804d66f46487732c8202c795478647b4ea71d627c0" + + "86024cca354a41f0877b38f19b3774ad2095c8da53b069e21c76ae2d2007e167" + + "19ed40080d334f7da52e9f5a5990439caf083a95b833f02ad10a08c1a6d0f260" + + "c007285bd4a2f47703a5aef465287d253b18ac22514316210ff566814b10f87a" + + "293d6f199d3c3959990d0c1268b4f50d5f9fcefbbf237bd0c28b80182d665974" + + "1f14f10bfbb21bba12ab620aa2396f56c0686b4ea9017990224216b2fe8ad76c" + + "4a9148eef9a86a3635a6aa77bc1dcfb6fba59a77dfda9b7530dc0ca8648c8d97" + + "3738e01bab8f08b4905e84aa4641bd602410cd97520265f2f231f2b35e15eb2f" + + "a04d2bd94d5a77abaf1e0e161010a990087f5b46ea988b2bc0512fda0fa923da" + + "dd6c45c5301d09483673265b5ab2e10f4ba520f6bbad564a5c3d5e27bdb080f7" + + "d20e13296a3181954c39c649c943ebe17df5c1f7aae0a8fe126c477585a5d4d6" + + "48a0d008b6af5e8cd31be69a9296d4f3fd25ed86f221e4b93f65f59299675336" + + "24b9235750c30707550b58536d109a7131c5a5bbe4a5715567c12534aec76607" + + "61eebb9fae2891c774589b80e566ad557ddef7367196b7227ea9870ef09ddfec" + + "79d6b9319a6879b5205d76bf7aba5acf33afb59d17fc54e68383d6be5a08e9b6" + + "6da53dcde008bb294b8582bd132cdcc49959fdbc21e52721880c8ad0352c79f0" + + "3a43bbd84c4cdfdc6c529005e1e7cd9a349a7168a35569ba5dea818968d5a914" + + "66bd6e64e20bf62417198afc4e81c28dd77ed4028232398b52fbde86bc84f475" + + "b9016710ce2aabc11a06b4dbac901ec16cf365ca3f2d53813948a693a0f93e79" + + "c46ca5d5a6dca3d28ca50ad18bd13fca55059dd9b185f79f9c47196a4e81b210" + + "4bc460a051e02f2e8444f", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "48683d91978e31eb3dddb8b0473482d2b88a5f625949" + + "fd8f58a561e696bd4c27d853fa69b8199023e8cd678dd9fabf9047646ffd0cb3" + + "cc7f795805a71e70d2371b0563e3cd3346149c8c9ebcf23b0a4e5a900eea9c65" + + "62790a7c63e38663daa2dddb6e480dc405a1e701948b74841ef5cc1c3f2bf327" + + "972e9510510cd5375ecc08557177118722218623810004247780614750075017" + + "1703550451512547183804617572224410886860864601274756718087066686" + + "4332444122043638667502823634244322057364106455547722755681433614" + + "6255082064376854687543537510687183338054750525807528188438110872" + + "6020200858830183611382821206171157876878887864375460165715508471" + + "8866072732880664741856762180318276641578245025646643113504364780" + + "1266731430116606558647183688635038478611012023561161378607853212" + + "4007547882304366611660425541828560536778563843443063261077073178" + + "4272141116530385276867460150823735320766107504681248066603032652" + + "3124454088003180887672173071824721512780116544748661722333808660" + + "6446835215842036801180211818331773545348810044865367437057725883" + + "3460384232856810060426042584560235682051838638432421224245645858" + + "6771457285047887171806188360868641565081165026467006082662273831" + + "7240725730072728862066758868260706402033034366315546424534566718" + + "7345658370225084685628807036708462371710065717584778708655537822" + + "3514467728567303228700143320617158455266325026513347773803551643" + + "1347351066275175740246888170674346818601765245333087210434340103" + + "2287635155265081307745444168154183636411204026873043677712808846" + + "3554530062458104583651248427803451666358437856014651157423214366" + + "8522477731345017836242055000648447123440880060473540578333630821" + + "0615225207248851348637067622588571265673476816464684258708122705" + + "5008383200232080663453360033468572470635540035771227523071425368" + + "7437457005664322448285207218333020533733407727805525306352504067" + + "3346131807280717248377634573185851602333443625164338160858773462" + + "4288300703658537550075523150370213246304370868063615030300435863" + + "5708021106647346352262033043802108528757832107886748085634743673" + + "4284058466841437005510873426447721127384736526472577144704178644" + + "2602471187408122166058471781370676808170581855854713634210755801" + + "6358358518440384711033874262824774136554427073463577750066256268" + + "4202124683864616646031225388845400845734464754472560546166846630" + + "8806382715632871838406522476811606621303301868028013846305056572" + + "3875836572323068804612260665167557053241322767351708015300162846" + + "0134887701118815571315464311704732882856368234555041862765631111" + + "6875051042544144278522111717881536851574471662553655836302502855" + + "7687532713710372370571476171365184124236644466414352052108515703" + + "3363860258426628148110546268173038756433216588568663632813406254" + + "0120408865478861716576237262348670301151156320507535021221084265" + + "3143556711152572010685363015055758605878431431327878808738478863" + + "7881813873426178388524667733506021151464238232680135440783475385" + + "5357528323351876011521343257733336551886158161682418422122308414" + + "4815120110302477724254436606771770760301452540350018387323773526" + + "5086357113734481605277456553730085837785035121115480628850180268" + + "1386520534680132072418032130057238640764271141018385255106326071" + + "0486517683382857276235451873508313288637666142631167503311255376" + + "4176031433177212234418a82e4f5c9ea0faf99eb04d78a7332711117c33f18e" + + "ca21f8743376ada5219804a7ed9a5557fcd67a3550b3a4b8c588629c021475fa" + + "3d56d5d6cfbb1a09bda8d14de622ddff16d8bc99b14278a8af1d76bed157672d" + + "d9c32316f97e8daadef8d9da69586725567fb96b59990d4bf0bc9c195b90b742" + + "95f5675b24257c2710c175b0153f2911328c2eb7abb9ad46e70a8b53c39ea642" + + "cee4b3cb42620e863ce8b650ce8adcd923721a1687023c673a8cbb6b03d51cd1" + + "97e8c346ebadce93950f88cee201db9e320843e29f300d9a19500d70a4caf272" + + "c69e4eef69fbb8a55efd7ca2bed990d2d3b582848f9c45c2abc54cfc47d34f06" + + "c0ffa56fcd762ab9cba9146d7725218963b240d72b6d22c93171fbd47788b76e" + + "72042def0878d23df631a1a1e5a6027686de5b4a10e91069c8f2ba0259b04d64" + + "09da96567ca52da497026e583a0ecefc1f01e6b988e21f9767a2b7e1672deb9a" + + "1e2a3fcc863aa91517c334620601b4fe79730e934935f4b6fbc4e32695145c2b" + + "5f6a127fecc0a277451ebc3fd523444f9ee7c9c34534f356db544fc31c1bfde5" + + "f65c77ea2f7c2eae4c55ebaf104271c566fd4ebac71c7a62c74952817ae67550" + + "4d9599b1b762b6aca168a83248c9d9adb0ceb1556e5759490bbc0c7900795ad7" + + "2123038b662f64f106a9993681a25d59af7bc97a235be9284c5bc45a6c90cb1c" + + "2999c663d96b478e2307f85548957d65740e2673e9ebd1352829038f462b8fd3" + + "b5681da55c0252523853525ea0ad647e71ac2c5a8893e603ac97e56c04ceb2f2" + + "6f5c5b4b6d94ab811380fd00f2208fe86535086aebfd35c29120624c04fbb611" + + "3929d9c556350253766c209fdba83c95fccd342a28099355d00bc863f4eef596" + + "eb0b42ebcc7c79491cceae205ea0b8059fbb8a5726c5949d2b15e7e29c51fc9b" + + "02ee1a4fc357b5f1bef9c4add46a2a920c2fbf08a37eb1514bfa15110a4392a7" + + "4c6f13c50c5cffd97531098d7cd23b60eb35c4a428b46c55386e1010c4ba7f70" + + "e4c7ecb7575f3063a71e84dfdcf09a58b2cdb0f99f27ed378610d25cbad7bfa6" + + "ba0d59189cfe88eab9b46d7e6db0307eabe4198e99bd71f779ab66581e0912fc" + + "7b1d2585245e9a12687a975cd5e8e1dcc045d5f891c4c685db07cf81e77389b3" + + "63eb6bdfe39b27ff84c97eefee162e3b451fe6914719cb6436d855960ff915d7" + + "cea6adeafdfc1c05786c49f923a474ffdfc3153a06e6ed0b0ad220d72524434d" + + "5273c0aab6dde4e91476d581a2695a60de6d9f44d77aa08266e938eeb4a9597c" + + "9b64986059e49262a4eab2454e14015ad0536c42733a5d77d7995c2a20446009" + + "ebfe5632c80c08ed2b97af35066489f597eb1b1f11f04f60e0c9040159c44ab3" + + "e60e0a15229d191228bed17bbc3ac939b3c67cee135f352c27216c9c31f72a3e" + + "87040c5f619306eb0b6cca2a9ce7b22a1694d00ca9c05e315126457f26ce84f9" + + "617241860782f864b473d84017491902b1bdc8cdc5800dd46127fb80a71c095b" + + "473a562529b3b1e7e437e158a5f6666e9974d005b062c2309e6dce98f9b658c6" + + "e3f9a216d58c8c9142bd1c8c85a9da872ebbfad3fea9d9aba2b68c0e8f19c6ff" + + "5f00584d45daf9d6c9d69ed04b8da8d687258b77807927612c530446fea7697a" + + "e3f926698929bc6a5a8cf3e2024c0f0c5ee57b5869bf981881caf9e3665fc7f7" + + "efc678929f87a56eaa42ea4d1ff6691822dd79a47096b776d1d8f01456e5873b" + + "0738406c382c573ae9cde2d9e7f231b6cc5c676e7cf43963373013a58075381f" + + "f0949be084546d72e4f8a3e5fe4aa5091add234e2afe0030b1b663ae9d2d3241" + + "0986b9402aaaf2465b74a5e2d0bc38e3a92bbddd8a1fed7b948c23cce6f8c08f" + + "e356835ba65b0f984068616ef48138efd89bf357a54d2ebbf376cbdcc69c5f1f" + + "61c64d2794bc06ccb9abdf66e25085d8c830e2ae3b0fe0f07a7af8b9320bf342" + + "970997d67d7c12593a8fbfade635aac53083a7022c47d5f77a52b57b598da939" + + "2ae6d86afc46fc06455181b9c75a646dc21f81e4bf213753de737fd2a1400279" + + "20add35a223f9f5f4465ceb60c03ed0455a333a5cc83adbf43f1f42c2ccb8328" + + "c21c7ab7faed2b21cfade2da55223aaab2af9b41c7332341746341b39aa2f438" + + "15650f5480511424cfa6901779c4d18b638cc0287aaaf31680338d20b17c7449" + + "fdc6a278a8d96a82ee4c4eca40125e2d65290071c7aef1be6a991598fb9d5951" + + "2523bcd4b38c566b8e80a73ae333e134414327ef1d83c47c49dfe7936df1338a" + + "5e247787868fc84fdcb95ac89c185c4bb5fd57b2338ac42b41c10a823df39624" + + "f36b15a2f067584e06ca2e08ccaff1618fe01dd06df3512e0b724dec8506da24" + + "215acacc2c51b82ad8d302002fb41068b1da4f8bb147987b3516bad5dbddf013" + + "18fd3fa9bc43702ac498c719d95f2e841b622a5e4848a3c5c262959992ea7a7d" + + "72ca8a368028f497dfad93355cbb1bb9786d14ff2cf590317848f95856427110" + + "dda36f5192a816ce9c8816cc7bbfc804efc40085a3850b89f1e7fe5656dba410" + + "f906a97c32336c1ae7e81737a83e087354e428da8538d948dbf5dfacb59dd2b5" + + "fd3bc803f4ba432c9a739df2cfa9ed9484320f97edff1a48c6b86b3002cfb772" + + "dd5e562bc4c3d683ed964b6199fa0514b0790d958095b7b85c6be875fbb559e1" + + "930146ccea63a388a194fe09c3dea03be52de27e901017afe809af630a7382bf" + + "5c4cd4d1b8f41579fb4348ede4ca05f4cd3f139a31b2544e516dbe4086b9bb4b" + + "2bed47e2d230982dd5192429d377b7c0745cc068e2f5a4aa04c7ff87209ed125" + + "9976a0fc9b25e9e851d4e3502c02c85d6dff029e211d01ebf0e9e7188d568f84" + + "37d813b0f122f2fb17603b693ed9c38f17cfd50b815e6d9dfc0ed2ccf19f6399" + + "274a1420f235a59d8bf724345e14e45d9e4be8934dfc3fa92678db61d7118bf5" + + "3cb8a2225b335f7eae50e3f941237628db76d8ea38f77a72af3a26c81fe43523" + + "b335535a5d1db7c38f341082bb5734d089e8ae309cfda3a0bcb5cd5b097113c8" + + "edf9616aa4f6e6631b9125276fb3f680a34341c3db668dc6cad45fc93b2708ca" + + "2af75ccce734fd191c50089dad53982fddae02531ff93e1f21ff395fc0a12874" + + "edf06b6f9647e95a7324586c71dfd91d901d621858190fecd00ccd110bbac59f" + + "96cb884c3c93994748a56f41283bfc41fb89052153a894588c3cb9017f3d6632" + + "6c985637e575acb812346342654025d602de3ba940c19ac1a633dffda977b529" + + "b8013e19c1d6d0680f4dae62c924450ae66aab82f21473061dab3d62b247f907" + + "e3551939ad3f5465e9d08a82bfea17eea1b6b2b923757477f993000b2f43b70f" + + "28aaab1fe9a26ad1fd3361616c0b0e242fe76604b7033a1f30e97e28f526ca3c" + + "880fe2b8d9d1b0c9ff188b31cb9d97425acab9b216d98a6ae355e583da71e886" + + "4ee3d16b0759796190ef545c1e62bfef92af6ca147b13244d6c892fc8ef223ab" + + "3f43f924c2f466097ee8", + "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 + v++Sr2yhR7EyRNbIkvyO8iOrP0P5JML0Zgl+6A== + """, + """ + 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 + 1siS/I7yI6s/Q/kkwvRmCX7o + """, + """ + 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 + HgLy6ERP + """, + """ + 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 + IA== + """, + "PLACEHOLDER", + new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2)); + + internal static partial MLDsaKeyInfo IetfMLDsa87 => field ??= new MLDsaKeyInfo( + MLDsaAlgorithm.MLDsa87, + "9792bcec2f2430686a82fccf3c2f5ff665e771d7ab4" + + "1b90258cfa7e90ec97124a73b323b9ba21ab64d767c433f5a521effe18f86e46" + + "a188952c4467e048b729e7fc4d115e7e48da1896d5fe119b10dcddef62cb3079" + + "54074b42336e52836de61da941f8d37ea68ac8106fabe19070679af600853712" + + "0f70793b8ea9cc0e6e7b7b4c9a5c7421c60f24451ba1e933db1a2ee16c79559f" + + "21b3d1b8305850aa42afbb13f1f4d5b9f4835f9d87dfceb162d0ef4a7fdc4cba" + + "1743cd1c87bb4967da16cc8764b6569df8ee5bdcbffe9a4e05748e6fdf225af9" + + "e4eeb7773b62e8f85f9b56b548945551844fbd89806a4ac369bed2d256100f68" + + "8a6ad5e0a709826dc4449e91e23c5506e642361ef5a313712f79bc4b3186861c" + + "a85a4bab17e7f943d1b8a333aa3ae7ce16b440d6018f9e04daf5725c7f1a93fa" + + "d1a5a27b67895bd249aa91685de20af32c8b7e268c7f96877d0c85001135a4f0" + + "a8f1b8264fa6ebe5a349d8aecad1a16299ccf2fd9c7b85bace2ced3aa1276ba6" + + "1ee78ed7e5ca5b67cdd458a9354030e6abbbabf56a0a2316fec9dba83b51d42f" + + "d3167f1e0f90855d5c66509b210265dc1e54ec44b43ba7cf9aef118b44d80912" + + "ce75166a6651e116cebe49229a7062c09931f71abd2293f76f7efc3215ba9780" + + "0037e58e470bdbbb43c1b0439eaf79c54d93b44aac9efe9fbe151874cfb2a64c" + + "bee28cc4c0fe7775e5d870f1c02e5b2e3c5004c995f24c9b779cb753a277d0e7" + + "1fd425eb6bc2ca56ce129db51f70740f31e63976b50c7312e9797d78c5b1ac24" + + "a5fa347cc916e0a83f5c3b675cd30b81e3fa10b93444e07397571cce98b28da5" + + "1db9056bc728c5b0b1181e2fbd387b4c79ab1a5fefece37167af772ddad14eb4" + + "c3982da5a59d0e9eb173ec6315091170027a3ab5ef6aa129cb8585727b9358a2" + + "8501d713a72f3f1db31714286f9b6408013af06045d75592fc0b7dd47c73ed9c" + + "75b11e9d7c69f7cadfc3280a9062c5273c43be1c34f87448864cea7b5c97d6d3" + + "2f59bd5f25384653bb5c4faa45bea8b89402843e645b6b9269e2bd988ddacb03" + + "3328ffb060450f7df080053e6969b251e875ecec32cfc592840d69ab69a75e06" + + "b379c535d95266b082f4f09c93162b33b0d9f7307a4eaaa52104437fed66f8ee" + + "3eabbd45d67b25a8133f496468b52baffdbfad93eef1a9818b5e42ec722788a3" + + "d8d3529fc777d2ba570801dfae01ec88302837c1fb9e0355727645ee1046c3f9" + + "15f6ae82dad4fb6b0356a46518ffc834155c3b4fe6dafa6cc8a5ccf53c73a084" + + "9d8d44f7dcf72754e70e1b7dfb447bb4ef49d1a718f6171bbce200950e0ce926" + + "106b151a3e871d5ce49731bd6650a9b0ca972da1c5f136d44820ea6383c08f3b" + + "384cf2338e789c513f618cc5694a6f0cee104511e1ed7c5f23a1ebfd8a0db842" + + "4553240156dbf622831b0c643d1c551b6f3f7a98d29b85c2de05a65fa615eee1" + + "6495bd90737672115b53e91c5d90028cf3f1a93953a153de53b44084e9ccff6b" + + "736693926daefebb2d77aa5ad689b92f31686669df16d1715cc58f7a2cfb72dd" + + "1a51e92f825993a74022be7e9eb6054654457094d14928f20215e7b222ac56b5" + + "1adbec8d8bdb6983979a7e3a21b44b5d1518ca97d0b5195f51ed6a24350c8974" + + "7e1edea51b448e3e9147054ce927873c90db394d86888e07dff177593d6f79e1" + + "52302204aeb03be2386af3e24078bd028b1689f5e147c9f452c8ceb02ec59cc9" + + "db63a03576ceeafe98239023897da0236630a53c0de7f435a19869792fab36e7" + + "b9e635760f09069e6432e700035ac2a02879fff0a1e1bec522047193d94eb5df" + + "1efd53eea1144ca78940852f5ec9727904b366ede4f5e2d331fad5fc282ea2c4" + + "7e923142771c3dd75a87357487def99e5f18e9d9ed623c175d02888c51f82c07" + + "a80d54716b3c3c2bdbe2e9f0a9bbaaebeb4d52936876406f5c00e8e4bbd0a5ec" + + "05797e6207c5ab6c88f1a688421bd05a114f4d7de2ac241fa0e8bedff47f762d" + + "dcbeaa91004f8d31e85095c81054994ad3826e344ba96040810fc0b2ad1de48c" + + "fade002c62e5a49a0731ab38344bc1636df16bf607d56855e56d684003c718e4" + + "bad9e5a099979fcddeeb1c4a7776cd37a3417cb0e184e29ef9bc0e87475ba663" + + "be09e00ab562eb7c0f7165f969a9b42414198ccf1bff2a2c8d689a414ece7662" + + "927665689e94db961ebaec5615cbc1a7895c6851ac961432ff1118d4607d32ef" + + "9dc732d51333be4b4d0e30ddea784eca8be47e741be9c19631dc470a52ef4dc1" + + "3a4f3633fd434d787c170977b417df598e1d0dde506bb71d6f0bc17ec70e3b03" + + "cdc1965cb36993f633b0472e50d0923ac6c66fdf1d3e6459cc121f0f5f94d09e" + + "9dbcf5d690e23233838a0bacb7c638d1b2650a4308cd171b6855126d1da672a6" + + "ed85a8d78c286fb56f4ab3d21497528045c63262c8a42af2f9802c53b7bb8be2" + + "8e78fe0b5ce45fbb7a1af1a3b28a8d94b7890e3c882e39bc98e9f0ad76025bf0" + + "dd2f00298e7141a226b3d7cee414f604d1e0ba54d11d5fe58bccea6ad77ad2e8" + + "c1caacf32459014b7b91001b1efa8ad172a523fb8e365b577121bf9fd88a2c60" + + "c21e821d7b6acb47a5a995e40caced5c223b8fe6de5e18e9d2e5893aefebb7aa" + + "e7ff1a146260e2f110e939528213a0025a38ec79aabc861b25ebc509a4674c13" + + "2aaacb7e0146f14efd11cfcaf4caa4f775a716ce325e0a435a4d349d720bcf13" + + "7450afc45046fc1a1f83a9d329777a7084e4aadae7122ce97005930528eb3c7f" + + "7f1129b372887a371155a3ba201a25cbf1dcb64e7cdee092c3141fb5550fe3d0" + + "dd82e870e578b2b46500818113b8f6569773c677385b69a42b77dcba7acffd95" + + "fd4452e23aaa1d37e1da2151ea658d40a3596b27ac9f8129dc6cf0643772624b" + + "59f4f461230df471ca26087c3942d5c6687df6082835935a3f87cb762b0c3b1d" + + "0dda4a6533965bef1b7b8292e254c014d090fed857c44c1839c694c0a64e3fad" + + "90a11f534722b6ee1574f2e149d55d744de4887024e08511431c062750e16c74" + + "ab9f3242f2db3ffb12a8d6107faa229d6f6373b07f36d3932b3bdb04c19dd64e" + + "add7f93c3c564c358a1c81dcf1c9c31e5b06568f97544c17dc15698c5cb38983" + + "a9afc42783faa773a52c9d8260690be9e3156aa5bc1509dea3f69587695cd6ff" + + "172ba83e6a6d8a7d6bbebbbcda3672731983f89bc5831dc37c3f3c5c56facc69" + + "7f3cb20bd5dbadbd702e54844ac2f626901fe159db93dfd4773d8fe73562b846" + + "c1fc856d1802762840ebc72d7988bde75cbca70d319d32ce0cc0253bb2ad4557" + + "23ee0c7f4736ce6e6665c5aca32a481c53839bc259167b013d0423395eeb9aaa" + + "ee3206149a7d550d67fc5fdfe4a8a5c35d2510b664379ab8f72855a2af47abce" + + "2a632048eaf89e5cb4a88debc53a595103acce4f1cff18acff07afe1eb5716aa" + + "1e40b63134c3a3ae9579fa87f515be093c2d29db6d6b65c93661e00636b59270" + + "4d093cc6716c2342eb1853d48c85c63ac8a2854462c7b77e7e3bd1eac5bca28f" + + "faa00b5d349f8a547ad875b96a8c2b2910c9301309a3f9138a5693111f55b3c0" + + "09ca947c39dfc82d98eb1caa4a9cbe885f786fa86e55be062222f8ba90a97407" + + "3326b31212aece0a34a60", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "9792bcec2f2430686a82fccf3c2f5ff665e771d7ab41" + + "b90258cfa7e90ec97124d8e9ee4e90a16c602f5ec9bc38517dc30e329d5ab276" + + "73bd85f4c9b0300f776389886750b57c24db3fc012e61ede59753337374fa712" + + "4991549af243496d0637cb3be05a5948235bf79875f896d8fe0cab30c84948db" + + "4d6315aaaf160ac6243664220148161109112c94028922452c62b84500452a08" + + "967090126e149370d446108444515896910ca92982b241c90871c42868049689" + + "4840859b226d1c28645912419cb891840489449005cb3462a086904026922099" + + "291305695c3468a4328e19269259461009a44923424d1236615810650128901a" + + "334c998631d3a249098225431428c0388103154d5b2886088748233152942225" + + "c3c04da49821984020d14286cb40705bb0719c962cc112065346090c45021446" + + "6e91b42154b08ce446429a208c0121251341055a402213c90ca0184052c230cb" + + "342c4bc8681ba4604984846330294aa0695b8004d2380a14264ce2b2448ba211" + + "244649c414520b427103b210922880012488e308110a052819c481002022dc44" + + "6842122244002ac9266a0c8731e0c04499148418360d11374222188c63b2910c" + + "9808a1a0010892440413245c987182847184325251b0319c422e1aa828020891" + + "01c0890bc7058a246522c2644c88915c826813a5645020862104029194444822" + + "3022394a02988409a28819192944488c22950ca104720487701221104206841b" + + "498589064ad33608dbc04058b6510ca7098c24619990648cc2905b249010a349" + + "03256143328a119844c8b22004384110472c19c6441c252c048830d946699b20" + + "001b46825aa4805ba0491890250026800bc2315a407254c620c1b03124b14d10" + + "952814000aa0c84d54a28823988160900402162c13214091868d08c291911426" + + "d0b40c09c66051442e04112600291193c20863a431220028c114080c402c4114" + + "069c20725422068b084da148691030411c284944188ecc9648d942501b06490c" + + "458823040d20302e23852c14073040b6854cc02044862019006c540248cc886c" + + "59063249148404c750134928e40609d3c610c8284c23394452a464cca8494438" + + "320a898400342c22858d1031090932651c898c40402921850009a16d84c064e2" + + "022d48044012098ee0422e93440812106a01840592308acb348ea2262e5c8611" + + "0b3508181000023426242389d1840024466013b249242846180271a038908914" + + "44d3962da31840232721c0185043c80441428d5c264144a26d48120e4032250b" + + "14820a482ecb828803a3601b25268cb82024b08598042108a72c833864543289" + + "010401234984029569d1a44d13a40c91460d6194809038455cc65001172053c6" + + "289b1810411268901221c01084421692538229812649d8a4059a2624240329e0" + + "4026d20248112468119989981485c9200d50128c1c0810021000009528c12890" + + "59b485d314650a406e11296518c24c213465d830691030521931668c188ac8c0" + + "084c9830a3a62041162218052e22252a64b8250ab30163208409800919280e02" + + "1101a39411e3986c58202109411060362208067112c2855aa085c0c6845c3806" + + "cbb669148484532282a1a640cc8600c42622a0a808983472d4204143c4904816" + + "681b16521a370250204248488a201141e2006cc0c20c14064911314d19060a89" + + "46091b816544c800820670001672cc24508a42899c969064287092b268982662" + + "619440c11689d842641a214e62906421c8248b286d5c4292a0c64d0c8580cc88" + + "4dd4428d42348a0b0451c32686242581123506a04404c894815bb4311c08065c" + + "240803276a20c225e1809019b46da3460c4b186050c62c1b922d111504a20004" + + "21482ed81606d2108a83a22508310d093851d948490b164c2332251919024a44" + + "09d1b2210b832c23258593168544a0441b83500222724b04809b146521936018" + + "130ad9460d224561c8b440a1422d02b8090014449bb6110b978c40104a82146a" + + "da90051c028e0c1972a3b48d24305011870964c628e4189298b46c6116514046" + + "0e1c3248da205188368a23b1218290281a1532e2186192048e13b690131368c9" + + "84684c406d0b330081464dd2380c049681a4885002908522b004d3a471d28010" + + "ca964051a641a48428e008520b308cd2380a0c2951c38209ca2091d83692a3a6" + + "28924222a216011a348637d9a659169881ec21cf4811869d1d7f139f0537e96f" + + "1184585405fd17808af1e06239d3b34e5aca8bf1369677b447ac718ac47d850c" + + "4d77b0be31dc9f508e3978f24274ab0185f727abdff59f4490371bf04610e364" + + "e64ec875ef9d20dc94077e1e166327a879b8ab516160b2a3f77437b9b3cc7d17" + + "aeaddc84db62746a35ac096f782f62a7f01aa6d6693deec90b23c66985a02307" + + "e0a1cae598a67324dba0f52f22432275e93257065c3b7e5e1cfe1dfd4d0df086" + + "df21243414a2d27e20230a829be4eb4c82c16d35f78b0e5e198332e00074bb64" + + "612fab17d4c8971cb68e5edab0369f1157b3469abd8384e2d9553f1b78e786e1" + + "ee9d0b98d39f83ccecf37d1ebd3a9d63aec766164a10171a4fd8c63daf182c42" + + "1258c5f529aa55cb7ebae2e1652315e1f71e8a74131410d03247ede11d34db91" + + "f6f08aa2478fd789679c04949f71bc0171e07e3a8bb5753dbbdaa411a6350ab4" + + "6eefbf86fc551c29efe4cdd7661d5cf6c3db22d0cedde599854459d97f20df74" + + "55bdf356a198d0f7eb6d34111fc940b25c0543b788edda9d26810eac3d6cc9c5" + + "1327c2cf83e887d4089e19695e11add837f6f440cc360f93f32fee8a9663712c" + + "6bbd38c84ab7b54823ec363eb7e42eb59fc1fce60fbd55307b3ec85fd9daf320" + + "6d7b4b3917f1c8b7a92e3c67d89880fdf2e47f5a0c994595db170af41babf5a2" + + "5b4dc1c42dd6a9db271e764de2fb015a49a850c7919be47006a336e2e325fde5" + + "3ac599554d0a7de4ef45ec40c39d6baff311beee75d89e02ad31f4be4bd20ae9" + + "194f5edddaa6650776116e9f270f77714ad7a8e89acef74b7ff7d8dbec27f802" + + "0a985247e2cdacef4894a4d68ba37ca912d6be73501c995181e5b77723350b36" + + "31da3700e13fd366e131bf06b36eb6b0345093209f0a7beffae1fdd875b00687" + + "c1163c353d7d2ac90937b34e978e92f821adc9662202ece89a17e7bb65ae17d8" + + "3b90dbbe6a501a4e1345bee4e5a5b53af2e5ba3d1ef3f4e05adf0b3a4cf2e530" + + "360fee64929902b571f6fd2e305652a4cb010f79f815e18f2bbb8cc89fa6fc76" + + "f77c89e293cf175a0b195800fe72d2ccdd7d75e5bd90bc6ac435d6a440ef852e" + + "9a1c8c53de03bf193365d735aaf29c5162a617e364e7f944168d0fb48fef4055" + + "8f454297cc3dd508662cf23fb88e1954aa45d1c5e115bcc36f05b3e098d55522" + + "0f40be2629b34507b8464c54c27b5dec78da8f22650514797af86a2512bcb7e2" + + "923379ef6d73c137006c1b38f51e37f93585e29041a3e4e3af46007ce13b8b5f" + + "7b17d5d65d7d5668e427bcbe7ec1d7c408c054a48c1ae797bf99acbc8d260752" + + "2935fd665ea7822d930f23eabff783bb23697569e204b943141e00c08810956b" + + "e0525365dbab54ed48cb76964ccdf5cbd3aee7282d4a0000d2784d7b8fab16b2" + + "f7f0d5225732b1efbc4eb1cfedeb43fde79b69ecc0fbeaa1e6b40728673bd4b2" + + "e98a0d4a8f02f853950730f28d35eb12fcc79768b8e18e4bda0e58a331a2f71d" + + "7ccc2d451b32b1c65c312acf47ee513b21954c41c00c873872ee94cf14f46037" + + "425361f4bdb54821f711460cebae8c07508a9219f88fa6bedaa678eed501944a" + + "16ae6f7b5bb7a2e1e357e70d7b98461a2c71cb0fa762d6ad9824081d37f292fd" + + "4be8b84c36110dc744360201beebe0bd6c9d05e869256d2ff3f99517b7efd2a3" + + "3774056cb5671675a8b492e9f5f2620eb8ef9381d3d1df19938b7b5ffaac59bc" + + "8110fa87ba8d7a3d0165f8e41dd0f804f11b9ded0f352a597835d06307a8e0c6" + + "ef4d21904339e1cf458923a3e89e025d945347366c02f3dd6368d4e47e85d3d2" + + "a9705bd57961852e5a579f93b1c514c539f49ea1163a2a493b0efcb47f4748f6" + + "a99e10bf7078282e4ace18136e2a8b3ee0a380dcd3b3ef3e65e1b8157289d624" + + "67ad488ba0392b2e90a1ededcbdc931dc17298ccef76645c7d330a05c2ce40f8" + + "9b85468f357a217751e154631304ec4e04bb45b3678909c74af51ce370364d8f" + + "4f7eb1e61e00287429c9961de8322ca9a2629b1309d800e92bc1dc5055dcc797" + + "f33866eb0cfd8d490250d48ffca8022f49290e2d5376162fbaa982d16453c825" + + "b35f6515635ea92bea72367baa54de3f9eaea69542a81a4127f71cbaa257f324" + + "fefef14f08fbd65a049cd2fb362594a8e23ff1a2617db5b158f6f01cf50ab0ed" + + "95c6e709841164108b06e1b40ab0ab11c408301d3d9d8ea69e968a9600b3d17f" + + "38011ce28074e2c2e10bf6197c602d8d0ce7d3a3ef2d89623bc9f12ea338791e" + + "9266bb8ce02b124c6c7929baea693244098454a080eb7523e13bb1b7c5b6775f" + + "abababbe9075fe5687aa451397bb9cfccd051243e9bf5aef24062d335de5fce2" + + "4e9ddbde1191052d80c36df9f8434872f277ed4f5a1ce8ebd3b960824a4e4f10" + + "01b04cb685f9bee4d0ddb0c571598ac2021a6606fd23345c6fbb84f0ce05fe52" + + "734521b7b07c6388d3a3b99318bf0131504aa9dfbaf548f9d32a9cd4c6893524" + + "b11330a2d3aad3ed2a58966ebb0134465d543fd7797af549f568eaebe957f64f" + + "ec854674902b97558756986946ea3ab7a251cbbea11a687bd43f5d0bd89cd2ca" + + "ba61d5218374990ee8b92219ed25dca011c68a9757c013bd837b2dd734e3751f" + + "64fcb4b23dcd6bc57ea567f5716e17367244751e2303b22a953e772756956cdc" + + "c013ffd2c32490754422a572529d4c92f1ebb19f1dad4d036f2fdf31ca9101bd" + + "f81aea948aedcf217aa8fccd7a0771aa2753e1a823bf41c95377a2ffa61b2265" + + "138153ce86d2c87dd07a4b32d27f5f2872641431ce9a18a502aaefd9afc5b0d1" + + "3cd46c357e38e69e1ee945add1992932a5b1e5c5629c9f48f7661853da00787c" + + "9d78fb925553bf07a50dd5b9d935853420e4d1a71ae62ff90ca193cdd6c2f4be" + + "d263415aaf9a35094bc2a22e2a663c7645001cd190b7bc17c75feadf8e87ce5c" + + "24b763b6584ed32e71b0268142ea3ed6898157bf923bebf0192d1bf5ee30a7d3" + + "51634a60b504dde38a2e114f7ae9bf176d4a18ba2895a7bb4b47444a9ba8dbb4" + + "c124cd41bbb32f4bcb1de48c4abb510607a001b5a000bba43618b6c19e43517b" + + "45b42405928b67c713881858bad3a42511c2716ff9cd332034b672b52ff16610" + + "805cdbe7544a8a84b66e1c745a73c1b6bcda5b77b951f36c0f7a5372de9e5d1f" + + "9bbcde8843c6909002dda4875e67571af0bec581856c32c09c240e664e761e57" + + "cd0d8dc8a71cb918a5762d111285cd8b5613ddbd0ca08ac0342b2bdee38f96fa" + + "754bb2b087179c113c93986a810356eb94540b93cb9dec4aa9290ff12ec1aa2e" + + "656c9be3d590753c366c601406c061bc22033a1fd1f4e1111d039b8813b983cb" + + "506c3ea7ff3057983e8bf01682fbb00f43005313c82c1392918a6165a13338ff" + + "e11a992c1fb3d1032aa679a418c8ba4f8a0bc199e10cf6bd77a14fdd6a060935" + + "14348e3a8974434ae8a3676369c6be2cf90e672b343fce04ac6b22e0cf47568b" + + "c45d70a68e68c649a4830ae218590c1a437e7a23a54efe44f67086eb697b9fa5" + + "7835f0b8f70f0a929226efb336c0e21833a028218cd63732c80aa477e62d141d" + + "ba81854f70da68daff4a84cb6de779254e8a97e73565374af4092af05cbd6654" + + "afc3fd72f0ae232695cb6668eafecc4069bd90bb528b83efa2fbcdbd93b28992" + + "9621ed74d808738fc103eeb105510851fc9319f171ea0ced0b97b5b9fb5ef985" + + "186bc52098f9eb476f67b7cc7665d47587975cb45a50fc64100719bf76345f0f" + + "df1e09efe9fb800dc114e46be0879a195cc06870e23d2631dae71c3994481c87" + + "61c40d07c5bfca95e718b7b22585af03ed34175a46d57af3518e32a7fc1aa448" + + "2732a81a87f724f8d2e780b3a39d451a380f75c2d680cc7213eab1d4a59d394a" + + "e3810a1c90818d52f93fb203e2d8b1b5fa8f60b2d585d9135d648846f138b869" + + "53242d2bb1f2ecdf389b4de7651817b8e4e64b333f1aac523a93f2748a9c38ff" + + "bc29ced457b6f9781b08a67a1975d031ccd71545c0037434056c2434d13e6c4b" + + "eebf46fc12222c0b2eccd6159d5aea8e554d7a09652b06bf7ca699a7199e716d" + + "05dd553041a8f2b303d236a9babaafb9fa528f28a2ca2aa780b940383c099aa6" + + "5a0074b83fd1f0bc5b7b5e46c25e54838b3cbcfc95f87f1d471b3ba894434fa5" + + "8952fdcb77f161372693306dba4e8f216d1c8e5caff0fe8360a51c6076364416" + + "9fdc6a8267f2e3f909a61b2a678bce6ae90403a836b1a7b7e8cd8b54c37087a9" + + "e14446d95e6908d2eedbfcc653e02fdf771f701a79b9e5a26ed0a947842070f3" + + "b5701742211219e761762c37f0d0a1d1b9750fee577e1208115c66ac07ec091e" + + "6a3fc4aa6a253bcba868edd3154dcaf5162f615e85490a6ca342f34c43ac61a3" + + "ea6bfeefd850e190eb1d8da4d28b5eceeb1678c02433ecd5d48b2536404257e8" + + "ca7bef5855f2b813ed2f4c409445a3317c9be1a35ae2fb4d2b87921b904bf2c1" + + "4db514cee045251cfc276374db15c99dea15acde197c6eb524988e39b63287be" + + "b8676865aaa3bad1b43b8cab15cbf27a498759e3203abf369e97242f0b015414" + + "9f14ac233cdb73a22b7fb8f09325bf2ace83bb6b5db8a121a2b682149a69131c" + + "cce52229840b113fc7b0bcc58405bfe87f1f95ffc2e96fc5596567e94364dfaa" + + "6d9d5a6eb99ae4ddf424", + "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 + v+h/H5X/wulvxVllZ+lDZN+qbZ1abrma5N30JA== + """, + """ + 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 + WWVn6UNk36ptnVpuuZrk3fQk + """, + """ + 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 + 5VvgYiIvi6kKl0BzMmsxISrs4KNKYA== + """, + """ + MIGiMF4GCSqGSIb3DQEFDTBRMDAGCSqGSIb3DQEFDDAjBBCH/Z2roXs942EqMECS + srp0AgECMAwGCCqGSIb3DQILBQAwHQYJYIZIAWUDBAECBBBEYXRFP923P5cAqYH9 + un9LBECILQwjCZgwxVYkLdLusTJSdWMxhP9iQxF8cmgFCHho8brdjQyO23cxaOz4 + OuKxjNfB95U2PgecJIP2LbZQDVI6 + """, + """ + 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 + """, + "PLACEHOLDER", + new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA512, 1)); + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.cs index aded286c0e1ee0..9f793801681b8f 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestsData.cs @@ -2,12 +2,45 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Text; using Microsoft.DotNet.XUnitExtensions; using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.Tests { + public record MLDsaKeyInfo( + MLDsaAlgorithm Algorithm, + string PublicKeyHex, + string PrivateSeedHex, + string SecretKeyHex, + string Pkcs8PrivateKey_Seed_Base64, + string Pkcs8PrivateKey_Expanded_Base64, + string Pkcs8PrivateKey_Both_Base64, + string Pkcs8PublicKeyBase64, + string Pkcs8EncryptedPrivateKeyBase64, + string CertificateBase64, + string EncryptionPassword, + PbeParameters EncryptionParameters) + { + public byte[] PublicKey => PublicKeyHex.HexToByteArray(); + public byte[] PrivateSeed => PrivateSeedHex.HexToByteArray(); + public byte[] SecretKey => SecretKeyHex.HexToByteArray(); + public byte[] Pkcs8PrivateKey_Seed => Convert.FromBase64String(Pkcs8PrivateKey_Seed_Base64); + public byte[] Pkcs8PrivateKey_Expanded => Convert.FromBase64String(Pkcs8PrivateKey_Expanded_Base64); + public byte[] Pkcs8PrivateKey_Both => Convert.FromBase64String(Pkcs8PrivateKey_Both_Base64); + public byte[] Pkcs8PublicKey => Convert.FromBase64String(Pkcs8PublicKeyBase64); + public byte[] Pkcs8EncryptedPrivateKey => Convert.FromBase64String(Pkcs8EncryptedPrivateKeyBase64); + public byte[] EncryptionPasswordBytes => Encoding.UTF8.GetBytes(EncryptionPassword); // Assuming UTF-8 encoding + public byte[] Certificate => Convert.FromBase64String(CertificateBase64); + public string EncryptedPem => PemEncoding.WriteString("ENCRYPTED PRIVATE KEY", Pkcs8EncryptedPrivateKey); + public string PrivateKeyPem => PemEncoding.WriteString("PRIVATE KEY", Pkcs8PrivateKey_Seed); + public string PublicKeyPem => PemEncoding.WriteString("PUBLIC KEY", Pkcs8PublicKey); + + public override string ToString() => + $"{nameof(MLDsaKeyInfo)} {{ {nameof(Algorithm)} = \"{Algorithm.Name}\" }}"; + } + public class MLDsaNistTestCase { public int NistTestCaseId { get; init; } @@ -33,6 +66,17 @@ public class MLDsaNistTestCase public static partial class MLDsaTestsData { + internal static partial MLDsaKeyInfo IetfMLDsa44 { get; } + internal static partial MLDsaKeyInfo IetfMLDsa65 { get; } + internal static partial MLDsaKeyInfo IetfMLDsa87 { get; } + + public static IEnumerable IetfMLDsaAlgorithms => field ??= + [ + [IetfMLDsa44], + [IetfMLDsa65], + [IetfMLDsa87], + ]; + public static IEnumerable AllMLDsaAlgorithms() => [ [MLDsaAlgorithm.MLDsa44], diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 9dbb8e16c89287..721ac842511eed 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -1805,8 +1805,10 @@ public void Dispose() { } protected virtual void Dispose(bool disposing) { } public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public byte[] ExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public byte[] ExportEncryptedPkcs8PrivateKey(string password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public string ExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public string ExportEncryptedPkcs8PrivateKeyPem(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } + public string ExportEncryptedPkcs8PrivateKeyPem(string password, System.Security.Cryptography.PbeParameters pbeParameters) { throw null; } public int ExportMLDsaPrivateSeed(System.Span destination) { throw null; } protected abstract void ExportMLDsaPrivateSeedCore(System.Span destination); public int ExportMLDsaPublicKey(System.Span destination) { throw null; } @@ -1820,20 +1822,27 @@ protected virtual void Dispose(bool disposing) { } public static System.Security.Cryptography.MLDsa GenerateKey(System.Security.Cryptography.MLDsaAlgorithm algorithm) { throw null; } public static System.Security.Cryptography.MLDsa ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.ReadOnlySpan source) { throw null; } public static System.Security.Cryptography.MLDsa ImportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.MLDsa ImportEncryptedPkcs8PrivateKey(string password, byte[] source) { throw null; } public static System.Security.Cryptography.MLDsa ImportFromEncryptedPem(System.ReadOnlySpan source, System.ReadOnlySpan passwordBytes) { throw null; } public static System.Security.Cryptography.MLDsa ImportFromEncryptedPem(System.ReadOnlySpan source, System.ReadOnlySpan password) { throw null; } + public static System.Security.Cryptography.MLDsa ImportFromEncryptedPem(string source, byte[] passwordBytes) { throw null; } + public static System.Security.Cryptography.MLDsa ImportFromEncryptedPem(string source, string password) { throw null; } public static System.Security.Cryptography.MLDsa ImportFromPem(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.MLDsa ImportFromPem(string source) { throw null; } public static System.Security.Cryptography.MLDsa ImportMLDsaPrivateSeed(System.Security.Cryptography.MLDsaAlgorithm algorithm, System.ReadOnlySpan source) { throw null; } public static System.Security.Cryptography.MLDsa ImportMLDsaPublicKey(System.Security.Cryptography.MLDsaAlgorithm algorithm, System.ReadOnlySpan source) { throw null; } public static System.Security.Cryptography.MLDsa ImportMLDsaSecretKey(System.Security.Cryptography.MLDsaAlgorithm algorithm, System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.MLDsa ImportPkcs8PrivateKey(byte[] source) { throw null; } public static System.Security.Cryptography.MLDsa ImportPkcs8PrivateKey(System.ReadOnlySpan source) { throw null; } + public static System.Security.Cryptography.MLDsa ImportSubjectPublicKeyInfo(byte[] source) { throw null; } public static System.Security.Cryptography.MLDsa ImportSubjectPublicKeyInfo(System.ReadOnlySpan source) { throw null; } public int SignData(System.ReadOnlySpan data, System.Span destination, System.ReadOnlySpan context = default(System.ReadOnlySpan)) { throw null; } protected abstract void SignDataCore(System.ReadOnlySpan data, System.ReadOnlySpan context, System.Span destination); - protected void ThrowIfDisposed() { } public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan passwordBytes, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public bool TryExportEncryptedPkcs8PrivateKey(System.ReadOnlySpan password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } + public bool TryExportEncryptedPkcs8PrivateKey(string password, System.Security.Cryptography.PbeParameters pbeParameters, System.Span destination, out int bytesWritten) { throw null; } public bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; } + protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten); public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } public bool VerifyData(System.ReadOnlySpan data, System.ReadOnlySpan signature, System.ReadOnlySpan context = default(System.ReadOnlySpan)) { throw null; } protected abstract bool VerifyDataCore(System.ReadOnlySpan data, System.ReadOnlySpan context, System.ReadOnlySpan signature); @@ -1867,6 +1876,7 @@ protected override void ExportMLDsaPrivateSeedCore(System.Span destination protected override void ExportMLDsaPublicKeyCore(System.Span destination) { } protected override void ExportMLDsaSecretKeyCore(System.Span destination) { } protected override void SignDataCore(System.ReadOnlySpan data, System.ReadOnlySpan context, System.Span destination) { } + protected override bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten) { throw null; } protected override bool VerifyDataCore(System.ReadOnlySpan data, System.ReadOnlySpan context, System.ReadOnlySpan signature) { throw null; } } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 8ab1c492ef25cf..eaa9787bccd8bb 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -534,6 +534,9 @@ The specified EVP_PKEY handle is not a known ML-DSA algorithm. + + The specified PKCS#8 key contains a seed that does not match the expanded key. + The message exceeds the maximum allowable length for the chosen options ({0}). diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 51a71b4218ab8e..236cd618d1d3c3 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -139,6 +139,20 @@ Common\System\Security\Cryptography\Asn1\GeneralNameAsn.xml.cs Common\System\Security\Cryptography\Asn1\GeneralNameAsn.xml + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml.cs + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml.cs + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml + System\Security\Cryptography\Asn1\MLKemPrivateKeyAsn.xml @@ -394,6 +408,8 @@ Link="Common\System\Security\Cryptography\MLDsa.cs" /> + destination) => protected override void ExportMLDsaPrivateSeedCore(Span destination) => Interop.Crypto.MLDsaExportSeed(_key, destination); + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + return MLDsaPkcs8.TryExportPkcs8PrivateKey( + this, + _hasSeed, + _hasSecretKey, + destination, + out bytesWritten); + } + internal static partial MLDsaImplementation GenerateKeyImpl(MLDsaAlgorithm algorithm) { SafeEvpPKeyHandle key = Interop.Crypto.MLDsaGenerateKey(algorithm.Name, ReadOnlySpan.Empty); - return new MLDsaImplementation(algorithm, key); + return new MLDsaImplementation(algorithm, key, hasSeed: true, hasSecretKey: true); } internal static partial MLDsaImplementation ImportPublicKey(MLDsaAlgorithm algorithm, ReadOnlySpan source) { Debug.Assert(source.Length == algorithm.PublicKeySizeInBytes, $"Public key was expected to be {algorithm.PublicKeySizeInBytes} bytes, but was {source.Length} bytes."); SafeEvpPKeyHandle key = Interop.Crypto.EvpPKeyFromData(algorithm.Name, source, privateKey: false); - return new MLDsaImplementation(algorithm, key); + return new MLDsaImplementation(algorithm, key, hasSeed: false, hasSecretKey: false); } internal static partial MLDsaImplementation ImportPkcs8PrivateKeyValue(MLDsaAlgorithm algorithm, ReadOnlySpan source) => @@ -76,14 +95,14 @@ internal static partial MLDsaImplementation ImportSecretKey(MLDsaAlgorithm algor { Debug.Assert(source.Length == algorithm.SecretKeySizeInBytes, $"Secret key was expected to be {algorithm.SecretKeySizeInBytes} bytes, but was {source.Length} bytes."); SafeEvpPKeyHandle key = Interop.Crypto.EvpPKeyFromData(algorithm.Name, source, privateKey: true); - return new MLDsaImplementation(algorithm, key); + return new MLDsaImplementation(algorithm, key, hasSeed: false, hasSecretKey: true); } internal static partial MLDsaImplementation ImportSeed(MLDsaAlgorithm algorithm, ReadOnlySpan source) { Debug.Assert(source.Length == algorithm.PrivateSeedSizeInBytes, $"Seed was expected to be {algorithm.PrivateSeedSizeInBytes} bytes, but was {source.Length} bytes."); SafeEvpPKeyHandle key = Interop.Crypto.MLDsaGenerateKey(algorithm.Name, source); - return new MLDsaImplementation(algorithm, key); + return new MLDsaImplementation(algorithm, key, hasSeed: true, hasSecretKey: true); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.NotSupported.cs index fe058553921ade..e28ff314915001 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.NotSupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.NotSupported.cs @@ -7,7 +7,11 @@ namespace System.Security.Cryptography { public sealed partial class MLDsaOpenSsl : MLDsa { - private static partial MLDsaAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle) + private static partial MLDsaAlgorithm AlgorithmFromHandle( + SafeEvpPKeyHandle pkeyHandle, + out SafeEvpPKeyHandle upRefHandle, + out bool hasSeed, + out bool hasSecretKey) { throw new PlatformNotSupportedException(); } @@ -53,5 +57,11 @@ protected override void ExportMLDsaPrivateSeedCore(Span destination) Debug.Fail("Caller should have checked platform availability."); throw new PlatformNotSupportedException(); } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.OpenSsl.cs index 03663d52fe46b5..19a82f351dc235 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.OpenSsl.cs @@ -16,7 +16,11 @@ public partial SafeEvpPKeyHandle DuplicateKeyHandle() return _key.DuplicateHandle(); } - private static partial MLDsaAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle) + private static partial MLDsaAlgorithm AlgorithmFromHandle( + SafeEvpPKeyHandle pkeyHandle, + out SafeEvpPKeyHandle upRefHandle, + out bool hasSeed, + out bool hasSecretKey) { ArgumentNullException.ThrowIfNull(pkeyHandle); @@ -29,7 +33,10 @@ private static partial MLDsaAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkey try { - Interop.Crypto.PalMLDsaAlgorithmId mldsaId = Interop.Crypto.MLDsaGetPalId(upRefHandle); + Interop.Crypto.PalMLDsaAlgorithmId mldsaId = Interop.Crypto.MLDsaGetPalId( + upRefHandle, + out hasSeed, + out hasSecretKey); switch (mldsaId) { @@ -80,5 +87,16 @@ protected override void ExportMLDsaSecretKeyCore(Span destination) => /// protected override void ExportMLDsaPrivateSeedCore(Span destination) => Interop.Crypto.MLDsaExportSeed(_key, destination); + + /// + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + return MLDsaPkcs8.TryExportPkcs8PrivateKey( + this, + _hasSeed, + _hasSecretKey, + destination, + out bytesWritten); + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.cs index 2253fad6888193..add13a5621e672 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLDsaOpenSsl.cs @@ -24,6 +24,8 @@ namespace System.Security.Cryptography public sealed partial class MLDsaOpenSsl : MLDsa { private SafeEvpPKeyHandle _key; + private bool _hasSeed; + private bool _hasSecretKey; /// /// Initializes a new instance of the class from an existing OpenSSL key @@ -49,12 +51,19 @@ public sealed partial class MLDsaOpenSsl : MLDsa [UnsupportedOSPlatform("osx")] [UnsupportedOSPlatform("tvos")] [UnsupportedOSPlatform("windows")] - public MLDsaOpenSsl(SafeEvpPKeyHandle pkeyHandle) : base(AlgorithmFromHandle(pkeyHandle, out SafeEvpPKeyHandle upRefHandle)) + public MLDsaOpenSsl(SafeEvpPKeyHandle pkeyHandle) + : base(AlgorithmFromHandle(pkeyHandle, out SafeEvpPKeyHandle upRefHandle, out bool hasSeed, out bool hasSecretKey)) { _key = upRefHandle; + _hasSeed = hasSeed; + _hasSecretKey = hasSecretKey; } - private static partial MLDsaAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle); + private static partial MLDsaAlgorithm AlgorithmFromHandle( + SafeEvpPKeyHandle pkeyHandle, + out SafeEvpPKeyHandle upRefHandle, + out bool hasSeed, + out bool hasSecretKey); /// /// Gets a representation of the cryptographic key. diff --git a/src/libraries/System.Security.Cryptography/tests/MLDsaOpenSslTests.Unix.cs b/src/libraries/System.Security.Cryptography/tests/MLDsaOpenSslTests.Unix.cs index be22817f44bb5e..d46cc47c17fa6a 100644 --- a/src/libraries/System.Security.Cryptography/tests/MLDsaOpenSslTests.Unix.cs +++ b/src/libraries/System.Security.Cryptography/tests/MLDsaOpenSslTests.Unix.cs @@ -103,14 +103,14 @@ public void MLDsaOpenSsl_DuplicateKeyHandleLifetime() VerifyInstanceIsUsable(two); } - VerifyDisposed(one); + MLDsaTestHelpers.VerifyDisposed(one); Assert.Throws(() => one.DuplicateKeyHandle()); VerifyInstanceIsUsable(two); ExerciseSuccessfulVerify(two, data, oneSignature, context); } - VerifyDisposed(two); + MLDsaTestHelpers.VerifyDisposed(two); Assert.Throws(() => two.DuplicateKeyHandle()); } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index b3861c02822122..e027a894aba4f4 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -135,6 +135,20 @@ Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml.cs Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml.cs + System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml + + + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml.cs + System\Security\Cryptography\Asn1\MLDsaPrivateKeyBothAsn.xml + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml @@ -343,6 +357,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\ECDsa\ECDsaXml.cs" /> + + -int32_t CryptoNative_MLDsaGetPalId(const EVP_PKEY* pKey, int32_t* mldsaId) +int32_t CryptoNative_MLDsaGetPalId(const EVP_PKEY* pKey, int32_t* mldsaId, int32_t* hasSeed, int32_t* hasSecretKey) { #ifdef NEED_OPENSSL_3_0 - assert(pKey && mldsaId); + assert(pKey && mldsaId && hasSeed && hasSecretKey); if (API_EXISTS(EVP_PKEY_is_a)) { @@ -31,8 +31,13 @@ int32_t CryptoNative_MLDsaGetPalId(const EVP_PKEY* pKey, int32_t* mldsaId) else { *mldsaId = PalMLDsaId_Unknown; + *hasSeed = 0; + *hasSecretKey = 0; + return 1; } + *hasSeed = EvpPKeyHasKeyOctetStringParam(pKey, OSSL_PKEY_PARAM_ML_DSA_SEED); + *hasSecretKey = EvpPKeyHasKeyOctetStringParam(pKey, OSSL_PKEY_PARAM_PRIV_KEY); return 1; } #endif diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_ml_dsa.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_ml_dsa.h index 74a10ea15720f3..dd70967193818b 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_ml_dsa.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_ml_dsa.h @@ -13,7 +13,7 @@ typedef enum PalMLDsaId_MLDsa87 = 3, } PalMLDsaId; -PALEXPORT int32_t CryptoNative_MLDsaGetPalId(const EVP_PKEY* pKey, int32_t* mldsaId); +PALEXPORT int32_t CryptoNative_MLDsaGetPalId(const EVP_PKEY* pKey, int32_t* mldsaId, int32_t* hasSeed, int32_t* hasSecretKey); /* Generates a new EVP_PKEY with random parameters or if seed is not NULL, uses the seed to generate the key.