diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs new file mode 100644 index 00000000000000..82111e0cf43d7f --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Asn1; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + internal sealed partial class CompositeMLDsaManaged + { + private sealed class ECDsaComponent : ComponentAlgorithm +#if DESIGNTIMEINTERFACES +#pragma warning disable SA1001 // Commas should be spaced correctly + , IComponentAlgorithmFactory +#pragma warning restore SA1001 // Commas should be spaced correctly +#endif + { + private readonly ECDsaAlgorithm _algorithm; + + private ECDsa _ecdsa; + + private ECDsaComponent(ECDsa ecdsa, ECDsaAlgorithm algorithm) + { + Debug.Assert(ecdsa != null); + + _ecdsa = ecdsa; + _algorithm = algorithm; + } + + // While some of our OSes support the brainpool curves, not all do. + // Limit this implementation to the NIST curves until we have a better understanding + // of where native implementations of composite are aligning. + public static bool IsAlgorithmSupported(ECDsaAlgorithm algorithm) => +#if NET + algorithm.CurveOid is Oids.secp256r1 or Oids.secp384r1 or Oids.secp521r1; +#else + false; +#endif + + public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) + { +#if NET + return new ECDsaComponent(ECDsa.Create(algorithm.Curve), algorithm); +#else + throw new PlatformNotSupportedException(); +#endif + } + + public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) + { + Helpers.ThrowIfAsnInvalidLength(source); + + fixed (byte* ptr = &MemoryMarshal.GetReference(source)) + { + using (MemoryManager manager = new PointerMemoryManager(ptr, source.Length)) + { + ECPrivateKey ecPrivateKey = ECPrivateKey.Decode(manager.Memory, AsnEncodingRules.BER); + + if (ecPrivateKey.Version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If domain parameters are present, validate that they match the composite ML-DSA algorithm. + if (ecPrivateKey.Parameters is ECDomainParameters domainParameters) + { + if (domainParameters.Named is not string curveOid || curveOid != algorithm.CurveOid) + { + // The curve specified must be named and match the required curve for the composite ML-DSA algorithm. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + byte[]? x = null; + byte[]? y = null; + + // If public key is present, add it to the parameters. + if (ecPrivateKey.PublicKey is ReadOnlyMemory publicKey) + { + EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes, out x, out y); + } + + byte[] d = new byte[ecPrivateKey.PrivateKey.Length]; + + using (PinAndClear.Track(d)) + { + ecPrivateKey.PrivateKey.CopyTo(d); + +#if NET + ECParameters parameters = new ECParameters + { + Curve = algorithm.Curve, + Q = new ECPoint + { + X = x, + Y = y, + }, + D = d + }; + + parameters.Validate(); + + return new ECDsaComponent(ECDsa.Create(parameters), algorithm); +#else + throw new PlatformNotSupportedException(); +#endif + } + } + } + } + + public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) + { + int fieldWidth = algorithm.KeySizeInBytes; + + if (source.Length != 1 + fieldWidth * 2) + { + Debug.Fail("Public key format is fixed size, so caller needs to provide exactly correct sized buffer."); + throw new CryptographicException(); + } + + // Implementation limitation. + // 04 (Uncompressed ECPoint) is almost always used. + if (source[0] != 0x04) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + +#if NET + ECParameters parameters = new ECParameters() + { + Curve = algorithm.Curve, + Q = new ECPoint() + { + X = source.Slice(1, fieldWidth).ToArray(), + Y = source.Slice(1 + fieldWidth).ToArray(), + } + }; + + return new ECDsaComponent(ECDsa.Create(parameters), algorithm); +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override bool TryExportPrivateKey(Span destination, out int bytesWritten) + { +#if NET + ECParameters ecParameters = _ecdsa.ExportParameters(includePrivateParameters: true); + + Debug.Assert(ecParameters.D != null); + + using (PinAndClear.Track(ecParameters.D)) + { + ecParameters.Validate(); + + if (ecParameters.D.Length != _algorithm.KeySizeInBytes) + { + Debug.Fail("Unexpected key size."); + throw new CryptographicException(); + } + + // The curve OID must match the composite ML-DSA algorithm. + if (!ecParameters.Curve.IsNamed || + (ecParameters.Curve.Oid.Value != _algorithm.Curve.Oid.Value && ecParameters.Curve.Oid.FriendlyName != _algorithm.Curve.Oid.FriendlyName)) + { + Debug.Fail("Unexpected curve OID."); + throw new CryptographicException(); + } + + return TryWriteKey(ecParameters.D, ecParameters.Q.X, ecParameters.Q.Y, _algorithm.CurveOid, destination, out bytesWritten); + } +#else + throw new PlatformNotSupportedException(); +#endif + +#if NET + static bool TryWriteKey(byte[] d, byte[]? x, byte[]? y, string curveOid, Span destination, out int bytesWritten) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + try + { + // ECPrivateKey + using (writer.PushSequence()) + { + // version 1 + writer.WriteInteger(1); + + // privateKey + writer.WriteOctetString(d); + + // domainParameters + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true))) + { + writer.WriteObjectIdentifier(curveOid); + } + + // publicKey + if (x != null) + { + Debug.Assert(y != null); + + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true))) + { + EccKeyFormatHelper.WriteUncompressedPublicKey(x, y, writer); + } + } + } + + return writer.TryEncode(destination, out bytesWritten); + } + finally + { + writer.Reset(); + } + } +#endif + } + + internal override bool TryExportPublicKey(Span destination, out int bytesWritten) + { +#if NET + int fieldWidth = _algorithm.KeySizeInBytes; + + if (destination.Length < 1 + 2 * fieldWidth) + { + Debug.Fail("Public key format is fixed size, so caller needs to provide exactly correct sized buffer."); + + bytesWritten = 0; + return false; + } + + ECParameters ecParameters = _ecdsa.ExportParameters(includePrivateParameters: false); + + ecParameters.Validate(); + + if (ecParameters.Q.X?.Length != fieldWidth) + { + Debug.Fail("Unexpected key size."); + throw new CryptographicException(); + } + + // Uncompressed ECPoint format + destination[0] = 0x04; + + ecParameters.Q.X.CopyTo(destination.Slice(1, fieldWidth)); + ecParameters.Q.Y.CopyTo(destination.Slice(1 + fieldWidth)); + + bytesWritten = 1 + 2 * fieldWidth; + return true; +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override bool VerifyData( +#if NET + ReadOnlySpan data, +#else + byte[] data, +#endif + ReadOnlySpan signature) + { +#if NET + return _ecdsa.VerifyData(data, signature, _algorithm.HashAlgorithmName, DSASignatureFormat.Rfc3279DerSequence); +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override int SignData( +#if NET + ReadOnlySpan data, +#else + byte[] data, +#endif + Span destination) + { +#if NET + if (!_ecdsa.TrySignData(data, destination, _algorithm.HashAlgorithmName, DSASignatureFormat.Rfc3279DerSequence, out int bytesWritten)) + { + Debug.Fail("Buffer size should have been validated by caller."); + throw new CryptographicException(); + } + + return bytesWritten; +#else + throw new PlatformNotSupportedException(); +#endif + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _ecdsa?.Dispose(); + _ecdsa = null!; + } + + base.Dispose(disposing); + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs index 7388ba650e9bbe..1aed30d805a0a6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs @@ -156,6 +156,10 @@ public static RsaComponent ImportPrivateKey(RsaAlgorithm algorithm, ReadOnlySpan throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } #endif + if (rsa.KeySize != algorithm.KeySizeInBits) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } if (bytesRead != source.Length) { @@ -207,6 +211,10 @@ public static RsaComponent ImportPublicKey(RsaAlgorithm algorithm, ReadOnlySpan< throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } #endif + if (rsa.KeySize != algorithm.KeySizeInBits) + { + throw new CryptographicException(SR.Argument_PublicKeyWrongSizeForAlgorithm); + } if (bytesRead != source.Length) { diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index 0468bdee6253ce..485f2acb3d6d83 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using Internal.Cryptography; @@ -547,38 +546,6 @@ protected virtual void Dispose(bool disposing) } } - private sealed class ECDsaComponent : ComponentAlgorithm -#if DESIGNTIMEINTERFACES -#pragma warning disable SA1001 // Commas should be spaced correctly - , IComponentAlgorithmFactory -#pragma warning restore SA1001 // Commas should be spaced correctly -#endif - { - public static bool IsAlgorithmSupported(ECDsaAlgorithm _) => false; - public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) => throw new NotImplementedException(); - public static ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) => throw new NotImplementedException(); - public static ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) => throw new NotImplementedException(); - - internal override bool TryExportPrivateKey(Span destination, out int bytesWritten) => throw new NotImplementedException(); - internal override bool TryExportPublicKey(Span destination, out int bytesWritten) => throw new NotImplementedException(); - - internal override bool VerifyData( -#if NET - ReadOnlySpan data, -#else - byte[] data, -#endif - ReadOnlySpan signature) => throw new NotImplementedException(); - - internal override int SignData( -#if NET - ReadOnlySpan data, -#else - byte[] data, -#endif - Span destination) => throw new NotImplementedException(); - } - private static Dictionary CreateAlgorithmMetadata() { const int count = 18; @@ -613,7 +580,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa44, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, Oids.secp256r1, HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x03], HashAlgorithmName.SHA256) }, @@ -653,7 +620,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, Oids.secp256r1, HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x08], HashAlgorithmName.SHA512) }, @@ -661,7 +628,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, Oids.secp384r1, HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x09], HashAlgorithmName.SHA512) }, @@ -669,7 +636,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, "1.3.36.3.3.2.8.1.1.7", HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0A], HashAlgorithmName.SHA512) }, @@ -685,7 +652,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, Oids.secp384r1, HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0C], HashAlgorithmName.SHA512) }, @@ -693,7 +660,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, "1.3.36.3.3.2.8.1.1.11", HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0D], HashAlgorithmName.SHA512) }, @@ -725,7 +692,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaP521, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(521, Oids.secp521r1, HashAlgorithmName.SHA512), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x11], HashAlgorithmName.SHA512) } @@ -755,8 +722,37 @@ private sealed class RsaAlgorithm(int keySizeInBits, HashAlgorithmName hashAlgor internal RSASignaturePadding Padding { get; } = padding; } - private sealed class ECDsaAlgorithm + private sealed class ECDsaAlgorithm(int keySizeInBits, string curveOid, HashAlgorithmName hashAlgorithmName) { + internal int KeySizeInBits { get; } = keySizeInBits; + internal HashAlgorithmName HashAlgorithmName { get; } = hashAlgorithmName; + internal string CurveOid { get; } = curveOid; + + internal int KeySizeInBytes => (KeySizeInBits + 7) / 8; + +#if NET + internal ECCurve Curve + { + get + { + return CurveOid switch + { + Oids.secp256r1 => ECCurve.NamedCurves.nistP256, + Oids.secp384r1 => ECCurve.NamedCurves.nistP384, + Oids.secp521r1 => ECCurve.NamedCurves.nistP521, + "1.3.36.3.3.2.8.1.1.7" => ECCurve.NamedCurves.brainpoolP256r1, + "1.3.36.3.3.2.8.1.1.11" => ECCurve.NamedCurves.brainpoolP384r1, + string oid => FailAndThrow(oid) + }; + + static ECCurve FailAndThrow(string oid) + { + Debug.Fail($"'{oid}' is not a valid ECDSA curve for Composite ML-DSA."); + throw new CryptographicException(); + } + } + } +#endif } private sealed class EdDsaAlgorithm diff --git a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs index 1b5059af55b09d..72df332a19fade 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -1,798 +1,39 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; -using System.Runtime.InteropServices; -using System.Security.Cryptography.Asn1; namespace System.Security.Cryptography { - internal static class EccKeyFormatHelper + internal static partial class EccKeyFormatHelper { - // This is the same limit that OpenSSL 1.0.2-1.1.1 has, - // there's no real point reading anything bigger than this (for now). - private const int MaxFieldBitSize = 661; - - private static readonly string[] s_validOids = - { - Oids.EcPublicKey, - }; - - internal static void ReadSubjectPublicKeyInfo( - ReadOnlySpan source, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadSubjectPublicKeyInfo( - s_validOids, - source, - FromECPublicKey, - out bytesRead, - out key); - } - - internal static ReadOnlyMemory ReadSubjectPublicKeyInfo( - ReadOnlyMemory source, - out int bytesRead) - { - return KeyFormatHelper.ReadSubjectPublicKeyInfo( - s_validOids, - source, - out bytesRead); - } - - internal static void ReadEncryptedPkcs8( - ReadOnlySpan source, - ReadOnlySpan password, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadEncryptedPkcs8( - s_validOids, - source, - password, - FromECPrivateKey, - out bytesRead, - out key); - } - - internal static void ReadEncryptedPkcs8( - ReadOnlySpan source, - ReadOnlySpan passwordBytes, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadEncryptedPkcs8( - s_validOids, - source, - passwordBytes, - FromECPrivateKey, - out bytesRead, - out key); - } - - internal static unsafe ECParameters FromECPrivateKey(ReadOnlySpan key, out int bytesRead) - { - try - { - AsnDecoder.ReadEncodedValue( - key, - AsnEncodingRules.BER, - out _, - out _, - out int firstValueLength); - - fixed (byte* ptr = &MemoryMarshal.GetReference(key)) - { - using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) - { - AlgorithmIdentifierAsn algId = default; - FromECPrivateKey(manager.Memory, algId, out ECParameters ret); - bytesRead = firstValueLength; - return ret; - } - } - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - internal static void FromECPrivateKey( - ReadOnlyMemory keyData, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) - { - ECPrivateKey key = ECPrivateKey.Decode(keyData, AsnEncodingRules.BER); - FromECPrivateKey(key, algId, out ret); - } - - internal static void FromECPrivateKey( - ECPrivateKey key, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) - { - ValidateParameters(key.Parameters, algId); - - if (key.Version != 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - byte[]? x = null; - byte[]? y = null; - - if (key.PublicKey != null) - { - ReadOnlySpan publicKeyBytes = key.PublicKey.Value.Span; - - if (publicKeyBytes.Length == 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // Implementation limitation - // 04 (Uncompressed ECPoint) is almost always used. - if (publicKeyBytes[0] != 0x04) - { - throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); - } - - // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) - if (publicKeyBytes.Length != 2 * key.PrivateKey.Length + 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - x = publicKeyBytes.Slice(1, key.PrivateKey.Length).ToArray(); - y = publicKeyBytes.Slice(1 + key.PrivateKey.Length).ToArray(); - } - - ECDomainParameters domainParameters; - - if (key.Parameters != null) - { - domainParameters = key.Parameters.Value; - } - else - { - domainParameters = ECDomainParameters.Decode(algId.Parameters!.Value, AsnEncodingRules.DER); - } - - Debug.Assert((x == null) == (y == null)); - - ret = new ECParameters - { - Curve = GetCurve(domainParameters), - Q = - { - X = x, - Y = y, - }, - D = key.PrivateKey.ToArray(), - }; - - ret.Validate(); - } - - internal static void FromECPublicKey( - ReadOnlyMemory key, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) + internal static void GetECPointFromUncompressedPublicKey(ReadOnlySpan publicKey, int fieldWidthInBytes, out byte[] x, out byte[] y) { - if (algId.Parameters == null) + if (publicKey.Length == 0) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - ReadOnlySpan publicKeyBytes = key.Span; - - if (publicKeyBytes.Length == 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // Implementation limitation. + // Implementation limitation // 04 (Uncompressed ECPoint) is almost always used. - if (publicKeyBytes[0] != 0x04) + if (publicKey[0] != 0x04) { throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) - if ((publicKeyBytes.Length & 0x01) != 1) + if (publicKey.Length != 2 * fieldWidthInBytes + 1) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - int fieldWidth = publicKeyBytes.Length / 2; - - ECDomainParameters domainParameters = ECDomainParameters.Decode( - algId.Parameters.Value, - AsnEncodingRules.DER); - - ret = new ECParameters - { - Curve = GetCurve(domainParameters), - Q = - { - X = publicKeyBytes.Slice(1, fieldWidth).ToArray(), - Y = publicKeyBytes.Slice(1 + fieldWidth).ToArray(), - }, - }; - - ret.Validate(); - } - - private static void ValidateParameters(ECDomainParameters? keyParameters, in AlgorithmIdentifierAsn algId) - { - // At least one is required - if (keyParameters == null && algId.Parameters == null) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // If they are both specified they must match. - if (keyParameters != null && algId.Parameters != null) - { - ReadOnlySpan algIdParameters = algId.Parameters.Value.Span; - - // X.509 SubjectPublicKeyInfo specifies DER encoding. - // RFC 5915 specifies DER encoding for EC Private Keys. - // So we can compare as DER. - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - keyParameters.Value.Encode(writer); - - if (!writer.EncodedValueEquals(algIdParameters)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } - } - - private static ECCurve GetCurve(ECDomainParameters domainParameters) - { - if (domainParameters.Specified.HasValue) - { - return GetSpecifiedECCurve(domainParameters.Specified.Value); - } - - if (domainParameters.Named == null) - { - throw new CryptographicException(SR.Cryptography_ECC_NamedCurvesOnly); - } - - Oid curveOid = domainParameters.Named switch { - Oids.secp256r1 => Oids.secp256r1Oid, - Oids.secp384r1 => Oids.secp384r1Oid, - Oids.secp521r1 => Oids.secp521r1Oid, - _ => new Oid(domainParameters.Named, null) - }; - - return ECCurve.CreateFromOid(curveOid); - } - - private static ECCurve GetSpecifiedECCurve(SpecifiedECDomain specifiedParameters) - { - try - { - return GetSpecifiedECCurveCore(specifiedParameters); - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - private static ECCurve GetSpecifiedECCurveCore(SpecifiedECDomain specifiedParameters) - { - // sec1-v2 C.3: - // - // Versions 1, 2, and 3 are defined. - // 1 is just data, 2 and 3 mean that a seed is required (with different reasons for why, - // but they're human-reasons, not technical ones). - if (specifiedParameters.Version < 1 || specifiedParameters.Version > 3) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (specifiedParameters.Version > 1 && !specifiedParameters.Curve.Seed.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - byte[] primeOrPoly; - bool prime; - - switch (specifiedParameters.FieldID.FieldType) - { - case Oids.EcPrimeField: - prime = true; - AsnReader primeReader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); - ReadOnlySpan primeValue = primeReader.ReadIntegerBytes().Span; - primeReader.ThrowIfNotEmpty(); - - if (primeValue[0] == 0) - { - primeValue = primeValue.Slice(1); - } - - if (primeValue.Length > (MaxFieldBitSize / 8)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - primeOrPoly = primeValue.ToArray(); - break; - case Oids.EcChar2Field: - prime = false; - AsnReader char2Reader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); - AsnReader innerReader = char2Reader.ReadSequence(); - char2Reader.ThrowIfNotEmpty(); - - // Characteristic-two ::= SEQUENCE - // { - // m INTEGER, -- Field size - // basis CHARACTERISTIC-TWO.&id({BasisTypes}), - // parameters CHARACTERISTIC-TWO.&Type({BasisTypes}{@basis}) - // } - - if (!innerReader.TryReadInt32(out int m) || m > MaxFieldBitSize || m < 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - int k1; - int k2 = -1; - int k3 = -1; - - switch (innerReader.ReadObjectIdentifier()) - { - case Oids.EcChar2TrinomialBasis: - // Trinomial ::= INTEGER - if (!innerReader.TryReadInt32(out k1) || k1 >= m || k1 < 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - break; - case Oids.EcChar2PentanomialBasis: - // Pentanomial ::= SEQUENCE - // { - // k1 INTEGER, -- k1 > 0 - // k2 INTEGER, -- k2 > k1 - // k3 INTEGER -- k3 > k2 - // } - AsnReader pentanomialReader = innerReader.ReadSequence(); - - if (!pentanomialReader.TryReadInt32(out k1) || - !pentanomialReader.TryReadInt32(out k2) || - !pentanomialReader.TryReadInt32(out k3) || - k1 < 1 || - k2 <= k1 || - k3 <= k2 || - k3 >= m) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - pentanomialReader.ThrowIfNotEmpty(); - - break; - default: - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - innerReader.ThrowIfNotEmpty(); - - BitArray poly = new BitArray(m + 1); - poly.Set(m, true); - poly.Set(k1, true); - poly.Set(0, true); - - if (k2 > 0) - { - poly.Set(k2, true); - poly.Set(k3, true); - } - - primeOrPoly = new byte[(m + 7) / 8]; - poly.CopyTo(primeOrPoly, 0); - Array.Reverse(primeOrPoly); - break; - default: - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - ECCurve curve; - - if (prime) - { - curve = new ECCurve - { - CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, - Prime = primeOrPoly, - }; - } - else - { - curve = new ECCurve - { - CurveType = ECCurve.ECCurveType.Characteristic2, - Polynomial = primeOrPoly, - }; - } - - curve.A = specifiedParameters.Curve.A.ToUnsignedIntegerBytes(primeOrPoly.Length); - curve.B = specifiedParameters.Curve.B.ToUnsignedIntegerBytes(primeOrPoly.Length); - curve.Order = specifiedParameters.Order.ToUnsignedIntegerBytes(primeOrPoly.Length); - - ReadOnlySpan baseSpan = specifiedParameters.Base.Span; - - // We only understand the uncompressed point encoding, but that's almost always what's used. - if (baseSpan[0] != 0x04 || baseSpan.Length != 2 * primeOrPoly.Length + 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - curve.G.X = baseSpan.Slice(1, primeOrPoly.Length).ToArray(); - curve.G.Y = baseSpan.Slice(1 + primeOrPoly.Length).ToArray(); - - if (specifiedParameters.Cofactor.HasValue) - { - curve.Cofactor = specifiedParameters.Cofactor.Value.ToUnsignedIntegerBytes(); - } - - return curve; - } - - internal static AsnWriter WriteSubjectPublicKeyInfo(ECParameters ecParameters) - { - ecParameters.Validate(); - - // Since the public key format for EC keys is not ASN.1, - // write the SPKI structure manually. - - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // SubjectPublicKeyInfo - writer.PushSequence(); - - // algorithm - WriteAlgorithmIdentifier(ecParameters, writer); - - // subjectPublicKey - WriteUncompressedPublicKey(ecParameters, writer); - - writer.PopSequence(); - return writer; - } - - private static AsnWriter WriteAlgorithmIdentifier(in ECParameters ecParameters) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - WriteAlgorithmIdentifier(ecParameters, writer); - return writer; - } - - private static void WriteAlgorithmIdentifier(in ECParameters ecParameters, AsnWriter writer) - { - writer.PushSequence(); - - writer.WriteObjectIdentifier(Oids.EcPublicKey); - WriteEcParameters(ecParameters, writer); - - writer.PopSequence(); - } - - internal static AsnWriter WritePkcs8PrivateKey(ECParameters ecParameters, AttributeAsn[]? attributes = null) - { - ecParameters.Validate(); - - if (ecParameters.D == null) - { - throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); - } - - // Don't need the domain parameters because they're contained in the algId. - AsnWriter ecPrivateKey = WriteEcPrivateKey(ecParameters, includeDomainParameters: false); - AsnWriter algorithmIdentifier = WriteAlgorithmIdentifier(ecParameters); - AsnWriter? attributeWriter = WritePrivateKeyInfoAttributes(attributes); - - return KeyFormatHelper.WritePkcs8(algorithmIdentifier, ecPrivateKey, attributeWriter); - } - - [return: NotNullIfNotNull(nameof(attributes))] - private static AsnWriter? WritePrivateKeyInfoAttributes(AttributeAsn[]? attributes) - { - if (attributes == null) - return null; - - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 0); - writer.PushSetOf(tag); - - for (int i = 0; i < attributes.Length; i++) - { - attributes[i].Encode(writer); - } - - writer.PopSetOf(tag); - return writer; - } - - private static void WriteEcParameters(ECParameters ecParameters, AsnWriter writer) - { - if (ecParameters.Curve.IsNamed) - { - Oid oid = ecParameters.Curve.Oid; - - // On Windows the FriendlyName is populated in places where the Value mightn't be. - if (string.IsNullOrEmpty(oid.Value)) - { - Debug.Assert(oid.FriendlyName != null); - oid = Oid.FromFriendlyName(oid.FriendlyName, OidGroup.All); - } - - writer.WriteObjectIdentifier(oid.Value!); - } - else if (ecParameters.Curve.IsExplicit) - { - Debug.Assert(ecParameters.Curve.IsPrime || ecParameters.Curve.IsCharacteristic2); - WriteSpecifiedECDomain(ecParameters, writer); - } - else - { - throw new CryptographicException( - SR.Format(SR.Cryptography_CurveNotSupported, ecParameters.Curve.CurveType.ToString())); - } - } - - private static void WriteSpecifiedECDomain(ECParameters ecParameters, AsnWriter writer) - { - int m; - int k1; - int k2; - int k3; - m = k1 = k2 = k3 = -1; - - if (ecParameters.Curve.IsCharacteristic2) - { - DetermineChar2Parameters(ecParameters, ref m, ref k1, ref k2, ref k3); - } - - // SpecifiedECDomain - writer.PushSequence(); - { - // version - // We don't know if the seed (if present) is verifiably random (2). - // We also don't know if the base point is verifiably random (3). - // So just be version 1. - writer.WriteInteger(1); - - // fieldId - writer.PushSequence(); - { - if (ecParameters.Curve.IsPrime) - { - writer.WriteObjectIdentifier(Oids.EcPrimeField); - writer.WriteIntegerUnsigned(ecParameters.Curve.Prime); - } - else - { - Debug.Assert(ecParameters.Curve.IsCharacteristic2); - - // id - writer.WriteObjectIdentifier(Oids.EcChar2Field); - - // Parameters (Characteristic-two) - writer.PushSequence(); - { - // m - writer.WriteInteger(m); - - if (k3 > 0) - { - writer.WriteObjectIdentifier(Oids.EcChar2PentanomialBasis); - - writer.PushSequence(); - { - writer.WriteInteger(k1); - writer.WriteInteger(k2); - writer.WriteInteger(k3); - - writer.PopSequence(); - } - } - else - { - Debug.Assert(k2 < 0); - Debug.Assert(k1 > 0); - - writer.WriteObjectIdentifier(Oids.EcChar2TrinomialBasis); - writer.WriteInteger(k1); - } - - writer.PopSequence(); - } - } - - writer.PopSequence(); - } - - // curve - WriteCurve(ecParameters.Curve, writer); - - // base - WriteUncompressedBasePoint(ecParameters, writer); - - // order - writer.WriteIntegerUnsigned(ecParameters.Curve.Order); - - // cofactor - if (ecParameters.Curve.Cofactor != null) - { - writer.WriteIntegerUnsigned(ecParameters.Curve.Cofactor); - } - - // hash is omitted. - - writer.PopSequence(); - } + x = publicKey.Slice(1, fieldWidthInBytes).ToArray(); + y = publicKey.Slice(1 + fieldWidthInBytes).ToArray(); } - private static void DetermineChar2Parameters( - in ECParameters ecParameters, - ref int m, - ref int k1, - ref int k2, - ref int k3) + internal static void WriteUncompressedPublicKey(byte[] x, byte[] y, AsnWriter writer) { - byte[] polynomial = ecParameters.Curve.Polynomial!; - int lastIndex = polynomial.Length - 1; - - // The most significant byte needs a set bit, and the least significant bit must be set. - if (polynomial[0] == 0 || (polynomial[lastIndex] & 1) != 1) - { - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - - for (int localBitIndex = 7; localBitIndex >= 0; localBitIndex--) - { - int test = 1 << localBitIndex; - - if ((polynomial[0] & test) == test) - { - m = checked(8 * lastIndex + localBitIndex); - } - } - - // Find the other set bits. Since we've already found m and 0, there is either - // one remaining (trinomial) or 3 (pentanomial). - for (int inverseIndex = 0; inverseIndex < polynomial.Length; inverseIndex++) - { - int forwardIndex = lastIndex - inverseIndex; - byte val = polynomial[forwardIndex]; - - for (int localBitIndex = 0; localBitIndex < 8; localBitIndex++) - { - int test = 1 << localBitIndex; - - if ((val & test) == test) - { - int bitIndex = 8 * inverseIndex + localBitIndex; - - if (bitIndex == 0) - { - // The bottom bit is always set, it's not considered a parameter. - } - else if (bitIndex == m) - { - break; - } - else if (k1 < 0) - { - k1 = bitIndex; - } - else if (k2 < 0) - { - k2 = bitIndex; - } - else if (k3 < 0) - { - k3 = bitIndex; - } - else - { - // More than pentanomial. - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - } - } - } - - if (k3 > 0) - { - // Pentanomial - } - else if (k2 > 0) - { - // There is no quatranomial - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - else if (k1 > 0) - { - // Trinomial - } - else - { - // No smaller bases exist - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - } - - private static void WriteCurve(in ECCurve curve, AsnWriter writer) - { - writer.PushSequence(); - WriteFieldElement(curve.A!, writer); - WriteFieldElement(curve.B!, writer); - - if (curve.Seed != null) - { - writer.WriteBitString(curve.Seed); - } - - writer.PopSequence(); - } - - private static void WriteFieldElement(byte[] fieldElement, AsnWriter writer) - { - int start = 0; - - while (start < fieldElement.Length - 1 && fieldElement[start] == 0) - { - start++; - } - - writer.WriteOctetString(fieldElement.AsSpan(start)); - } - - private static void WriteUncompressedBasePoint(in ECParameters ecParameters, AsnWriter writer) - { - int basePointLength = ecParameters.Curve.G.X!.Length * 2 + 1; - - // A NIST P-521 G will be at most 133 bytes (NIST 186-4 defines G.) - // 256 should be plenty for all but very atypical uses. - const int MaxStackAllocSize = 256; - Span basePointBytes = stackalloc byte[MaxStackAllocSize]; - byte[]? rented = null; - - if (basePointLength > MaxStackAllocSize) - { - basePointBytes = rented = CryptoPool.Rent(basePointLength); - } - - basePointBytes[0] = 0x04; - ecParameters.Curve.G.X.CopyTo(basePointBytes.Slice(1)); - ecParameters.Curve.G.Y.CopyTo(basePointBytes.Slice(1 + ecParameters.Curve.G.X.Length)); - - writer.WriteOctetString(basePointBytes.Slice(0, basePointLength)); - - if (rented is not null) - { - // G contains public EC parameters that are not sensitive. - CryptoPool.Return(rented, clearSize: 0); - } - } - - private static void WriteUncompressedPublicKey(in ECParameters ecParameters, AsnWriter writer) - { - int publicKeyLength = ecParameters.Q.X!.Length * 2 + 1; + int publicKeyLength = x.Length * 2 + 1; // A NIST P-521 Q will encode to 133 bytes: (521 + 7)/8 * 2 + 1. // 256 should be plenty for all but very atypical uses. @@ -806,8 +47,8 @@ private static void WriteUncompressedPublicKey(in ECParameters ecParameters, Asn } publicKeyBytes[0] = 0x04; - ecParameters.Q.X.CopyTo(publicKeyBytes.Slice(1)); - ecParameters.Q.Y.CopyTo(publicKeyBytes.Slice(1 + ecParameters.Q.X!.Length)); + x.CopyTo(publicKeyBytes.Slice(1)); + y.CopyTo(publicKeyBytes.Slice(1 + x.Length)); writer.WriteBitString(publicKeyBytes.Slice(0, publicKeyLength)); @@ -817,50 +58,5 @@ private static void WriteUncompressedPublicKey(in ECParameters ecParameters, Asn CryptoPool.Return(rented, clearSize: 0); } } - - internal static AsnWriter WriteECPrivateKey(in ECParameters ecParameters) - { - return WriteEcPrivateKey(ecParameters, includeDomainParameters: true); - } - - private static AsnWriter WriteEcPrivateKey(in ECParameters ecParameters, bool includeDomainParameters) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // ECPrivateKey - writer.PushSequence(); - - // version 1 - writer.WriteInteger(1); - - // privateKey - writer.WriteOctetString(ecParameters.D); - - // domainParameters - if (includeDomainParameters) - { - Asn1Tag explicit0 = new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); - writer.PushSequence(explicit0); - - WriteEcParameters(ecParameters, writer); - - writer.PopSequence(explicit0); - } - - // publicKey - if (ecParameters.Q.X != null) - { - Debug.Assert(ecParameters.Q.Y != null); - Asn1Tag explicit1 = new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true); - writer.PushSequence(explicit1); - - WriteUncompressedPublicKey(ecParameters, writer); - - writer.PopSequence(explicit1); - } - - writer.PopSequence(); - return writer; - } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs index d5997d722d8b85..6c6e9c8565a54c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs @@ -1,6 +1,7 @@ // 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.Linq; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -58,6 +59,50 @@ public static void ImportBadPrivateKey_ShortTradKey(CompositeMLDsaAlgorithm algo AssertImportBadPrivateKey(algorithm, shortTradKey); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPrivateKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + byte[] key = vector.SecretKey; + Array.Resize(ref key, key.Length + 1); + + AssertImportBadPrivateKey(vector.Algorithm, key); + } + + [Fact] + public static void ImportBadPrivateKey_Rsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithRSA3072Pss + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + + // But use MLDsa65WithRSA4096Pss + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.SecretKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.SecretKey); + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithECDsaP256 + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + + // But use MLDsa65WithECDsaP384 + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.SecretKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.SecretKey); + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportPrivateKey_LowerBound(CompositeMLDsaAlgorithm algorithm) @@ -87,6 +132,151 @@ public static void ImportPrivateKey_UpperBound(CompositeMLDsaAlgorithm algorithm AssertImportBadPrivateKey(algorithm, new byte[bound.Value + 1]); } + [Fact] + public static void ImportBadPrivateKey_ECDsa_InvalidVersion() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + + // No version + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(null)); + + // Unsupported version + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(0)); + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(2)); + + // Correct version, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + CreateKeyWithVersion(1)); + + static byte[] CreateKeyWithVersion(int? version) + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + return ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version, ecdsaKey.D, ecdsaKey.Curve.Oid.Value, ecdsaKey.Q)); + } + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_NoPrivateKey() + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + // no private key + byte[] compositeKey = ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, d: null, ecdsaKey.Curve.Oid.Value, point: ecdsaKey.Q)); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, compositeKey); + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_WrongCurve() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + + // Wrong curve OID + AssertImportBadPrivateKey( + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.nistP521.Oid.Value)); + + AssertImportBadPrivateKey( + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.brainpoolP256r1.Oid.Value)); + + // Domain parameters are optional, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.nistP256.Oid.Value)); + + static byte[] CreateKeyWithCurveOid(string? oid) + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + return ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, ecdsaKey.D, oid, ecdsaKey.Q)); + } + } + + [Fact] + public static void ImportPrivateKey_ECDsa_NoPublicKey() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + // no public key + byte[] compositeKey = ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, ecdsaKey.D, ecdsaKey.Curve.Oid.Value, point: null)); + + // Public key is optional, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + compositeKey); + } + + static byte[] ComposeKeys(byte[] mldsaKey, AsnWriter tradKey) + { + byte[] compositeKey = new byte[mldsaKey.Length + tradKey.GetEncodedLength()]; + mldsaKey.CopyTo(compositeKey, 0); + tradKey.Encode(compositeKey.AsSpan(mldsaKey.Length)); + return compositeKey; + } + + private static AsnWriter WriteECPrivateKey(int? version, byte[]? d, string? oid, ECPoint? point) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // ECPrivateKey + using (writer.PushSequence()) + { + // version + if (version is int v) + { + writer.WriteInteger(v); + } + + // privateKey + if (d is not null) + { + writer.WriteOctetString(d); + } + + // domainParameters + if (oid is not null) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true))) + { + writer.WriteObjectIdentifier(oid); + } + } + + // publicKey + if (point is ECPoint q) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true))) + { + int publicKeyLength = 1 + q.X.Length + q.Y.Length; + byte[] publicKeyBytes = new byte[publicKeyLength]; + + publicKeyBytes[0] = 0x04; + q.X.CopyTo(publicKeyBytes.AsSpan(1)); + q.Y.CopyTo(publicKeyBytes.AsSpan(1 + q.X.Length)); + + writer.WriteBitString(publicKeyBytes); + } + } + } + + return writer; + } + private static void AssertImportBadPrivateKey(CompositeMLDsaAlgorithm algorithm, byte[] key) { CompositeMLDsaTestHelpers.AssertImportPrivateKey( @@ -131,6 +321,73 @@ public static void ImportBadPublicKey_ShortTradKey(CompositeMLDsaAlgorithm algor AssertImportBadPublicKey(algorithm, shortTradKey); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPublicKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + byte[] key = vector.PublicKey; + Array.Resize(ref key, key.Length + 1); + + AssertImportBadPublicKey(vector.Algorithm, key); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedECDsaAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPublicKey_ECDsa_Uncompressed(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + byte[] key = vector.PublicKey.AsSpan().ToArray(); + int formatIndex = CompositeMLDsaTestHelpers.MLDsaAlgorithms[vector.Algorithm].PublicKeySizeInBytes; + + // Uncompressed + Assert.Equal(4, key[formatIndex]); + + key[formatIndex] = 0; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 1; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 2; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 3; + AssertImportBadPublicKey(vector.Algorithm, key); + } + + [Fact] + public static void ImportBadPublicKey_Rsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithRSA3072Pss + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + + // But use MLDsa65WithRSA4096Pss + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.PublicKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.PublicKey); + } + + [Fact] + public static void ImportBadPublicKey_ECDsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithECDsaP256 + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + + // But use MLDsa65WithECDsaP384 + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.PublicKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.PublicKey); + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportPublicKey_LowerBound(CompositeMLDsaAlgorithm algorithm) @@ -174,24 +431,28 @@ private static void AssertImportBadPublicKey(CompositeMLDsaAlgorithm algorithm, [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void AlgorithmMatches_GenerateKey(CompositeMLDsaAlgorithm algorithm) { - AssertThrowIfNotSupported(() => - { - using CompositeMLDsa dsa = CompositeMLDsa.GenerateKey(algorithm); - Assert.Equal(algorithm, dsa.Algorithm); - }); + AssertThrowIfNotSupported( + () => + { + using CompositeMLDsa dsa = CompositeMLDsa.GenerateKey(algorithm); + Assert.Equal(algorithm, dsa.Algorithm); + }, + algorithm); } [Theory] [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void AlgorithmMatches_Import(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - CompositeMLDsaTestHelpers.AssertImportPublicKey(import => - AssertThrowIfNotSupported(() => - Assert.Equal(vector.Algorithm, import().Algorithm)), vector.Algorithm, vector.PublicKey); + CompositeMLDsaTestHelpers.AssertImportPublicKey( + import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), + vector.Algorithm, + vector.PublicKey); - CompositeMLDsaTestHelpers.AssertImportPrivateKey(import => - AssertThrowIfNotSupported(() => - Assert.Equal(vector.Algorithm, import().Algorithm)), vector.Algorithm, vector.SecretKey); + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), + vector.Algorithm, + vector.SecretKey); } [Fact] @@ -207,9 +468,13 @@ public static void IsAlgorithmSupported_AgreesWithPlatform(CompositeMLDsaAlgorit { bool supported = CompositeMLDsaTestHelpers.ExecuteComponentFunc( algorithm, - _ => MLDsa.IsSupported, - _ => false, - _ => false); + rsa => MLDsa.IsSupported, +#if NET + ecdsa => ecdsa.IsSec && MLDsa.IsSupported, +#else + ecdsa => false, +#endif + eddsa => false); Assert.Equal( supported, @@ -231,14 +496,11 @@ public static void IsSupported_InitializesCrypto() }, arg).Dispose(); } - /// - /// Asserts that on platforms that do not support Composite 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, CompositeMLDsaAlgorithm? algorithm = null) + // Asserts the test throws PlatformNotSupportedException if Composite ML-DSA is supported; + // otherwise runs the test normally. + private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm algorithm) { - if (algorithm == null ? CompositeMLDsa.IsSupported : CompositeMLDsa.IsAlgorithmSupported(algorithm)) + if (CompositeMLDsa.IsAlgorithmSupported(algorithm)) { test(); } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs index e6bf7084e25a8b..65bb8c366609a9 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs @@ -46,6 +46,11 @@ internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, str public static IEnumerableSupportedAlgorithmIetfVectorsTestData => SupportedAlgorithmIetfVectors.Select(v => new object[] { v }); + public static IEnumerable SupportedECDsaAlgorithmIetfVectorsTestData => + SupportedAlgorithmIetfVectors + .Where(vector => CompositeMLDsa.IsAlgorithmSupported(vector.Algorithm) && CompositeMLDsaTestHelpers.IsECDsa(vector.Algorithm)) + .Select(v => new object[] { v }); + internal static CompositeMLDsaAlgorithm[] AllAlgorithms => field ??= [ CompositeMLDsaAlgorithm.MLDsa44WithRSA2048Pss, diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs index e169c8edc66ba9..bc7dda2e2103a4 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs @@ -70,9 +70,10 @@ internal class RsaAlgorithm(int keySizeInBits) internal int KeySizeInBits { get; } = keySizeInBits; } - internal class ECDsaAlgorithm(int keySizeInBits) + internal class ECDsaAlgorithm(int keySizeInBits, bool isSec) { internal int KeySizeInBits { get; } = keySizeInBits; + internal bool IsSec { get; } = isSec; } internal class EdDsaAlgorithm(int keySizeInBits) @@ -117,20 +118,26 @@ internal static T ExecuteComponentFunc( return rsaFunc(new RsaAlgorithm(4096)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256 || - algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1 || algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256) { - return ecdsaFunc(new ECDsaAlgorithm(256)); + return ecdsaFunc(new ECDsaAlgorithm(256, isSec: true)); + } + else if (algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1) + { + return ecdsaFunc(new ECDsaAlgorithm(256, isSec: false)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384 || - algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1 || algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384) { - return ecdsaFunc(new ECDsaAlgorithm(384)); + return ecdsaFunc(new ECDsaAlgorithm(384, isSec: true)); + } + else if (algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1) + { + return ecdsaFunc(new ECDsaAlgorithm(384, isSec: false)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP521) { - return ecdsaFunc(new ECDsaAlgorithm(521)); + return ecdsaFunc(new ECDsaAlgorithm(521, isSec: true)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa44WithEd25519 || algo == CompositeMLDsaAlgorithm.MLDsa65WithEd25519) @@ -147,6 +154,8 @@ internal static T ExecuteComponentFunc( } } + internal static bool IsECDsa(CompositeMLDsaAlgorithm algorithm) => ExecuteComponentFunc(algorithm, rsa => false, ecdsa => true, eddsa => false); + internal static void AssertExportPublicKey(Action> callback) { callback(dsa => dsa.ExportCompositeMLDsaPublicKey()); diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs index a7d9fe129b90e8..17606e753a1e97 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs @@ -55,8 +55,8 @@ public void GenerateSignVerifyWithPrivateKey(CompositeMLDsaAlgorithm algorithm) { ExerciseSuccessfulVerify(privateKey, data, signature, []); - signature.AsSpan().Clear(); - privateKey.SignData(data, signature, []); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, privateKey.SignData(data, signature, [])); ExerciseSuccessfulVerify(privateKey, data, signature, []); } @@ -71,8 +71,8 @@ public void GenerateSignVerifyNoContext(CompositeMLDsaAlgorithm algorithm) byte[] signature = dsa.SignData(data); ExerciseSuccessfulVerify(dsa, data, signature, []); - signature.AsSpan().Clear(); - dsa.SignData(data, signature, Array.Empty()); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(data, signature, Array.Empty())); ExerciseSuccessfulVerify(dsa, data, signature, Array.Empty()); } @@ -86,6 +86,10 @@ public void GenerateSignVerifyWithContext(CompositeMLDsaAlgorithm algorithm) byte[] signature = dsa.SignData(data, context); ExerciseSuccessfulVerify(dsa, data, signature, context); + + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(data, signature, context)); + ExerciseSuccessfulVerify(dsa, data, signature, context); } [Theory] @@ -96,8 +100,8 @@ public void GenerateSignVerifyEmptyMessageNoContext(CompositeMLDsaAlgorithm algo byte[] signature = dsa.SignData([]); ExerciseSuccessfulVerify(dsa, [], signature, []); - signature.AsSpan().Clear(); - dsa.SignData(Array.Empty(), signature, Array.Empty()); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(Array.Empty(), signature, Array.Empty())); ExerciseSuccessfulVerify(dsa, [], signature, []); } @@ -110,8 +114,8 @@ public void GenerateSignVerifyEmptyMessageWithContext(CompositeMLDsaAlgorithm al byte[] signature = dsa.SignData([], context); ExerciseSuccessfulVerify(dsa, [], signature, context); - signature.AsSpan().Clear(); - dsa.SignData(Array.Empty(), signature, context); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(Array.Empty(), signature, context)); ExerciseSuccessfulVerify(dsa, [], signature, context); } @@ -154,14 +158,14 @@ public void ImportSignVerify(CompositeMLDsaTestData.CompositeMLDsaTestVector vec { signature = privateKey.SignData(vector.Message, null); - Assert.Equal(vector.Signature.Length, signature.Length); - ExerciseSuccessfulVerify(privateKey, vector.Message, signature, []); + ExerciseSuccessfulVerify(privateKey, vector.Message, vector.Signature, []); } using (CompositeMLDsa publicKey = ImportPublicKey(vector.Algorithm, vector.PublicKey)) { ExerciseSuccessfulVerify(publicKey, vector.Message, signature, []); + ExerciseSuccessfulVerify(publicKey, vector.Message, vector.Signature, []); } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index d193ac0db3c1e0..9199288186c28d 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -112,6 +112,27 @@ Common\System\Security\Cryptography\Asn1\AttributeAsn.manual.cs Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + Common\System\Security\Cryptography\Asn1\CurveAsn.xml + + + Common\System\Security\Cryptography\Asn1\CurveAsn.xml.cs + Common\System\Security\Cryptography\Asn1\CurveAsn.xml + + + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml + + + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml.cs + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml + + + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml + + + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml.cs + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml @@ -119,6 +140,13 @@ Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml.cs Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + Common\System\Security\Cryptography\Asn1\FieldID.xml + + + Common\System\Security\Cryptography\Asn1\FieldID.xml.cs + Common\System\Security\Cryptography\Asn1\FieldID.xml + Common\System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml.cs Common\System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml @@ -187,6 +215,13 @@ Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml.cs Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml + + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml.cs + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml + Common\System\Security\Cryptography\Asn1\SubjectPublicKeyInfoAsn.xml @@ -198,6 +233,8 @@ Link="Common\System\Memory\PointerMemoryManager.cs" /> + + + + @@ -1034,6 +1035,8 @@ Link="Common\System\IO\PersistedFiles.Unix.cs" /> + + source, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_validOids, + source, + FromECPublicKey, + out bytesRead, + out key); + } + + internal static ReadOnlyMemory ReadSubjectPublicKeyInfo( + ReadOnlyMemory source, + out int bytesRead) + { + return KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_validOids, + source, + out bytesRead); + } + + internal static void ReadEncryptedPkcs8( + ReadOnlySpan source, + ReadOnlySpan password, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadEncryptedPkcs8( + s_validOids, + source, + password, + FromECPrivateKey, + out bytesRead, + out key); + } + + internal static void ReadEncryptedPkcs8( + ReadOnlySpan source, + ReadOnlySpan passwordBytes, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadEncryptedPkcs8( + s_validOids, + source, + passwordBytes, + FromECPrivateKey, + out bytesRead, + out key); + } + + internal static unsafe ECParameters FromECPrivateKey(ReadOnlySpan key, out int bytesRead) + { + try + { + AsnDecoder.ReadEncodedValue( + key, + AsnEncodingRules.BER, + out _, + out _, + out int firstValueLength); + + fixed (byte* ptr = &MemoryMarshal.GetReference(key)) + { + using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) + { + AlgorithmIdentifierAsn algId = default; + FromECPrivateKey(manager.Memory, algId, out ECParameters ret); + bytesRead = firstValueLength; + return ret; + } + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + internal static void FromECPrivateKey( + ReadOnlyMemory keyData, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + ECPrivateKey key = ECPrivateKey.Decode(keyData, AsnEncodingRules.BER); + FromECPrivateKey(key, algId, out ret); + } + + internal static void FromECPrivateKey( + ECPrivateKey key, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + ValidateParameters(key.Parameters, algId); + + if (key.Version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + byte[]? x = null; + byte[]? y = null; + + if (key.PublicKey is not null) + { + GetECPointFromUncompressedPublicKey(key.PublicKey.Value.Span, key.PrivateKey.Length, out x, out y); + } + + ECDomainParameters domainParameters; + + if (key.Parameters != null) + { + domainParameters = key.Parameters.Value; + } + else + { + domainParameters = ECDomainParameters.Decode(algId.Parameters!.Value, AsnEncodingRules.DER); + } + + Debug.Assert((x == null) == (y == null)); + + ret = new ECParameters + { + Curve = GetCurve(domainParameters), + Q = new ECPoint + { + X = x, + Y = y, + }, + D = key.PrivateKey.ToArray(), + }; + + ret.Validate(); + } + + internal static void FromECPublicKey( + ReadOnlyMemory key, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + if (algId.Parameters == null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlySpan publicKeyBytes = key.Span; + + if (publicKeyBytes.Length == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Implementation limitation. + // 04 (Uncompressed ECPoint) is almost always used. + if (publicKeyBytes[0] != 0x04) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) + if ((publicKeyBytes.Length & 0x01) != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int fieldWidth = publicKeyBytes.Length / 2; + + ECDomainParameters domainParameters = ECDomainParameters.Decode( + algId.Parameters.Value, + AsnEncodingRules.DER); + + ret = new ECParameters + { + Curve = GetCurve(domainParameters), + Q = + { + X = publicKeyBytes.Slice(1, fieldWidth).ToArray(), + Y = publicKeyBytes.Slice(1 + fieldWidth).ToArray(), + }, + }; + + ret.Validate(); + } + + private static void ValidateParameters(ECDomainParameters? keyParameters, in AlgorithmIdentifierAsn algId) + { + // At least one is required + if (keyParameters == null && algId.Parameters == null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If they are both specified they must match. + if (keyParameters != null && algId.Parameters != null) + { + ReadOnlySpan algIdParameters = algId.Parameters.Value.Span; + + // X.509 SubjectPublicKeyInfo specifies DER encoding. + // RFC 5915 specifies DER encoding for EC Private Keys. + // So we can compare as DER. + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + keyParameters.Value.Encode(writer); + + if (!writer.EncodedValueEquals(algIdParameters)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + } + + private static ECCurve GetCurve(ECDomainParameters domainParameters) + { + if (domainParameters.Specified.HasValue) + { + return GetSpecifiedECCurve(domainParameters.Specified.Value); + } + + if (domainParameters.Named == null) + { + throw new CryptographicException(SR.Cryptography_ECC_NamedCurvesOnly); + } + + Oid curveOid = domainParameters.Named switch { + Oids.secp256r1 => Oids.secp256r1Oid, + Oids.secp384r1 => Oids.secp384r1Oid, + Oids.secp521r1 => Oids.secp521r1Oid, + _ => new Oid(domainParameters.Named, null) + }; + + return ECCurve.CreateFromOid(curveOid); + } + + private static ECCurve GetSpecifiedECCurve(SpecifiedECDomain specifiedParameters) + { + try + { + return GetSpecifiedECCurveCore(specifiedParameters); + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + private static ECCurve GetSpecifiedECCurveCore(SpecifiedECDomain specifiedParameters) + { + // sec1-v2 C.3: + // + // Versions 1, 2, and 3 are defined. + // 1 is just data, 2 and 3 mean that a seed is required (with different reasons for why, + // but they're human-reasons, not technical ones). + if (specifiedParameters.Version < 1 || specifiedParameters.Version > 3) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (specifiedParameters.Version > 1 && !specifiedParameters.Curve.Seed.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + byte[] primeOrPoly; + bool prime; + + switch (specifiedParameters.FieldID.FieldType) + { + case Oids.EcPrimeField: + prime = true; + AsnReader primeReader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); + ReadOnlySpan primeValue = primeReader.ReadIntegerBytes().Span; + primeReader.ThrowIfNotEmpty(); + + if (primeValue[0] == 0) + { + primeValue = primeValue.Slice(1); + } + + if (primeValue.Length > (MaxFieldBitSize / 8)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + primeOrPoly = primeValue.ToArray(); + break; + case Oids.EcChar2Field: + prime = false; + AsnReader char2Reader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); + AsnReader innerReader = char2Reader.ReadSequence(); + char2Reader.ThrowIfNotEmpty(); + + // Characteristic-two ::= SEQUENCE + // { + // m INTEGER, -- Field size + // basis CHARACTERISTIC-TWO.&id({BasisTypes}), + // parameters CHARACTERISTIC-TWO.&Type({BasisTypes}{@basis}) + // } + + if (!innerReader.TryReadInt32(out int m) || m > MaxFieldBitSize || m < 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int k1; + int k2 = -1; + int k3 = -1; + + switch (innerReader.ReadObjectIdentifier()) + { + case Oids.EcChar2TrinomialBasis: + // Trinomial ::= INTEGER + if (!innerReader.TryReadInt32(out k1) || k1 >= m || k1 < 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + break; + case Oids.EcChar2PentanomialBasis: + // Pentanomial ::= SEQUENCE + // { + // k1 INTEGER, -- k1 > 0 + // k2 INTEGER, -- k2 > k1 + // k3 INTEGER -- k3 > k2 + // } + AsnReader pentanomialReader = innerReader.ReadSequence(); + + if (!pentanomialReader.TryReadInt32(out k1) || + !pentanomialReader.TryReadInt32(out k2) || + !pentanomialReader.TryReadInt32(out k3) || + k1 < 1 || + k2 <= k1 || + k3 <= k2 || + k3 >= m) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + pentanomialReader.ThrowIfNotEmpty(); + + break; + default: + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + innerReader.ThrowIfNotEmpty(); + + BitArray poly = new BitArray(m + 1); + poly.Set(m, true); + poly.Set(k1, true); + poly.Set(0, true); + + if (k2 > 0) + { + poly.Set(k2, true); + poly.Set(k3, true); + } + + primeOrPoly = new byte[(m + 7) / 8]; + poly.CopyTo(primeOrPoly, 0); + Array.Reverse(primeOrPoly); + break; + default: + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ECCurve curve; + + if (prime) + { + curve = new ECCurve + { + CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, + Prime = primeOrPoly, + }; + } + else + { + curve = new ECCurve + { + CurveType = ECCurve.ECCurveType.Characteristic2, + Polynomial = primeOrPoly, + }; + } + + curve.A = specifiedParameters.Curve.A.ToUnsignedIntegerBytes(primeOrPoly.Length); + curve.B = specifiedParameters.Curve.B.ToUnsignedIntegerBytes(primeOrPoly.Length); + curve.Order = specifiedParameters.Order.ToUnsignedIntegerBytes(primeOrPoly.Length); + + ReadOnlySpan baseSpan = specifiedParameters.Base.Span; + + // We only understand the uncompressed point encoding, but that's almost always what's used. + if (baseSpan[0] != 0x04 || baseSpan.Length != 2 * primeOrPoly.Length + 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + curve.G.X = baseSpan.Slice(1, primeOrPoly.Length).ToArray(); + curve.G.Y = baseSpan.Slice(1 + primeOrPoly.Length).ToArray(); + + if (specifiedParameters.Cofactor.HasValue) + { + curve.Cofactor = specifiedParameters.Cofactor.Value.ToUnsignedIntegerBytes(); + } + + return curve; + } + + internal static AsnWriter WriteSubjectPublicKeyInfo(ECParameters ecParameters) + { + ecParameters.Validate(); + + // Since the public key format for EC keys is not ASN.1, + // write the SPKI structure manually. + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // SubjectPublicKeyInfo + writer.PushSequence(); + + // algorithm + WriteAlgorithmIdentifier(ecParameters, writer); + + // subjectPublicKey + WriteUncompressedPublicKey(ecParameters, writer); + + writer.PopSequence(); + return writer; + } + + private static AsnWriter WriteAlgorithmIdentifier(in ECParameters ecParameters) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + WriteAlgorithmIdentifier(ecParameters, writer); + return writer; + } + + private static void WriteAlgorithmIdentifier(in ECParameters ecParameters, AsnWriter writer) + { + writer.PushSequence(); + + writer.WriteObjectIdentifier(Oids.EcPublicKey); + WriteEcParameters(ecParameters, writer); + + writer.PopSequence(); + } + + internal static AsnWriter WritePkcs8PrivateKey(ECParameters ecParameters, AttributeAsn[]? attributes = null) + { + ecParameters.Validate(); + + if (ecParameters.D == null) + { + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + + // Don't need the domain parameters because they're contained in the algId. + AsnWriter ecPrivateKey = WriteEcPrivateKey(ecParameters, includeDomainParameters: false); + AsnWriter algorithmIdentifier = WriteAlgorithmIdentifier(ecParameters); + AsnWriter? attributeWriter = WritePrivateKeyInfoAttributes(attributes); + + return KeyFormatHelper.WritePkcs8(algorithmIdentifier, ecPrivateKey, attributeWriter); + } + + [return: NotNullIfNotNull(nameof(attributes))] + private static AsnWriter? WritePrivateKeyInfoAttributes(AttributeAsn[]? attributes) + { + if (attributes == null) + return null; + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 0); + writer.PushSetOf(tag); + + for (int i = 0; i < attributes.Length; i++) + { + attributes[i].Encode(writer); + } + + writer.PopSetOf(tag); + return writer; + } + + private static void WriteEcParameters(ECParameters ecParameters, AsnWriter writer) + { + if (ecParameters.Curve.IsNamed) + { + Oid oid = ecParameters.Curve.Oid; + + // On Windows the FriendlyName is populated in places where the Value mightn't be. + if (string.IsNullOrEmpty(oid.Value)) + { + Debug.Assert(oid.FriendlyName != null); + oid = Oid.FromFriendlyName(oid.FriendlyName, OidGroup.All); + } + + writer.WriteObjectIdentifier(oid.Value!); + } + else if (ecParameters.Curve.IsExplicit) + { + Debug.Assert(ecParameters.Curve.IsPrime || ecParameters.Curve.IsCharacteristic2); + WriteSpecifiedECDomain(ecParameters, writer); + } + else + { + throw new CryptographicException( + SR.Format(SR.Cryptography_CurveNotSupported, ecParameters.Curve.CurveType.ToString())); + } + } + + private static void WriteSpecifiedECDomain(ECParameters ecParameters, AsnWriter writer) + { + int m; + int k1; + int k2; + int k3; + m = k1 = k2 = k3 = -1; + + if (ecParameters.Curve.IsCharacteristic2) + { + DetermineChar2Parameters(ecParameters, ref m, ref k1, ref k2, ref k3); + } + + // SpecifiedECDomain + writer.PushSequence(); + { + // version + // We don't know if the seed (if present) is verifiably random (2). + // We also don't know if the base point is verifiably random (3). + // So just be version 1. + writer.WriteInteger(1); + + // fieldId + writer.PushSequence(); + { + if (ecParameters.Curve.IsPrime) + { + writer.WriteObjectIdentifier(Oids.EcPrimeField); + writer.WriteIntegerUnsigned(ecParameters.Curve.Prime); + } + else + { + Debug.Assert(ecParameters.Curve.IsCharacteristic2); + + // id + writer.WriteObjectIdentifier(Oids.EcChar2Field); + + // Parameters (Characteristic-two) + writer.PushSequence(); + { + // m + writer.WriteInteger(m); + + if (k3 > 0) + { + writer.WriteObjectIdentifier(Oids.EcChar2PentanomialBasis); + + writer.PushSequence(); + { + writer.WriteInteger(k1); + writer.WriteInteger(k2); + writer.WriteInteger(k3); + + writer.PopSequence(); + } + } + else + { + Debug.Assert(k2 < 0); + Debug.Assert(k1 > 0); + + writer.WriteObjectIdentifier(Oids.EcChar2TrinomialBasis); + writer.WriteInteger(k1); + } + + writer.PopSequence(); + } + } + + writer.PopSequence(); + } + + // curve + WriteCurve(ecParameters.Curve, writer); + + // base + WriteUncompressedBasePoint(ecParameters, writer); + + // order + writer.WriteIntegerUnsigned(ecParameters.Curve.Order); + + // cofactor + if (ecParameters.Curve.Cofactor != null) + { + writer.WriteIntegerUnsigned(ecParameters.Curve.Cofactor); + } + + // hash is omitted. + + writer.PopSequence(); + } + } + + private static void DetermineChar2Parameters( + in ECParameters ecParameters, + ref int m, + ref int k1, + ref int k2, + ref int k3) + { + byte[] polynomial = ecParameters.Curve.Polynomial!; + int lastIndex = polynomial.Length - 1; + + // The most significant byte needs a set bit, and the least significant bit must be set. + if (polynomial[0] == 0 || (polynomial[lastIndex] & 1) != 1) + { + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + + for (int localBitIndex = 7; localBitIndex >= 0; localBitIndex--) + { + int test = 1 << localBitIndex; + + if ((polynomial[0] & test) == test) + { + m = checked(8 * lastIndex + localBitIndex); + } + } + + // Find the other set bits. Since we've already found m and 0, there is either + // one remaining (trinomial) or 3 (pentanomial). + for (int inverseIndex = 0; inverseIndex < polynomial.Length; inverseIndex++) + { + int forwardIndex = lastIndex - inverseIndex; + byte val = polynomial[forwardIndex]; + + for (int localBitIndex = 0; localBitIndex < 8; localBitIndex++) + { + int test = 1 << localBitIndex; + + if ((val & test) == test) + { + int bitIndex = 8 * inverseIndex + localBitIndex; + + if (bitIndex == 0) + { + // The bottom bit is always set, it's not considered a parameter. + } + else if (bitIndex == m) + { + break; + } + else if (k1 < 0) + { + k1 = bitIndex; + } + else if (k2 < 0) + { + k2 = bitIndex; + } + else if (k3 < 0) + { + k3 = bitIndex; + } + else + { + // More than pentanomial. + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + } + } + } + + if (k3 > 0) + { + // Pentanomial + } + else if (k2 > 0) + { + // There is no quatranomial + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + else if (k1 > 0) + { + // Trinomial + } + else + { + // No smaller bases exist + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + } + + private static void WriteCurve(in ECCurve curve, AsnWriter writer) + { + writer.PushSequence(); + WriteFieldElement(curve.A!, writer); + WriteFieldElement(curve.B!, writer); + + if (curve.Seed != null) + { + writer.WriteBitString(curve.Seed); + } + + writer.PopSequence(); + } + + private static void WriteFieldElement(byte[] fieldElement, AsnWriter writer) + { + int start = 0; + + while (start < fieldElement.Length - 1 && fieldElement[start] == 0) + { + start++; + } + + writer.WriteOctetString(fieldElement.AsSpan(start)); + } + + private static void WriteUncompressedBasePoint(in ECParameters ecParameters, AsnWriter writer) + { + int basePointLength = ecParameters.Curve.G.X!.Length * 2 + 1; + + // A NIST P-521 G will be at most 133 bytes (NIST 186-4 defines G.) + // 256 should be plenty for all but very atypical uses. + const int MaxStackAllocSize = 256; + Span basePointBytes = stackalloc byte[MaxStackAllocSize]; + byte[]? rented = null; + + if (basePointLength > MaxStackAllocSize) + { + basePointBytes = rented = CryptoPool.Rent(basePointLength); + } + + basePointBytes[0] = 0x04; + ecParameters.Curve.G.X.CopyTo(basePointBytes.Slice(1)); + ecParameters.Curve.G.Y.CopyTo(basePointBytes.Slice(1 + ecParameters.Curve.G.X.Length)); + + writer.WriteOctetString(basePointBytes.Slice(0, basePointLength)); + + if (rented is not null) + { + // G contains public EC parameters that are not sensitive. + CryptoPool.Return(rented, clearSize: 0); + } + } + + private static void WriteUncompressedPublicKey(in ECParameters ecParameters, AsnWriter writer) + { + WriteUncompressedPublicKey(ecParameters.Q.X!, ecParameters.Q.Y!, writer); + } + + internal static AsnWriter WriteECPrivateKey(in ECParameters ecParameters) + { + return WriteEcPrivateKey(ecParameters, includeDomainParameters: true); + } + + private static AsnWriter WriteEcPrivateKey(in ECParameters ecParameters, bool includeDomainParameters) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // ECPrivateKey + writer.PushSequence(); + + // version 1 + writer.WriteInteger(1); + + // privateKey + writer.WriteOctetString(ecParameters.D); + + // domainParameters + if (includeDomainParameters) + { + Asn1Tag explicit0 = new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); + writer.PushSequence(explicit0); + + WriteEcParameters(ecParameters, writer); + + writer.PopSequence(explicit0); + } + + // publicKey + if (ecParameters.Q.X != null) + { + Debug.Assert(ecParameters.Q.Y != null); + Asn1Tag explicit1 = new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true); + writer.PushSequence(explicit1); + + WriteUncompressedPublicKey(ecParameters, writer); + + writer.PopSequence(explicit1); + } + + writer.PopSequence(); + return writer; + } + } +}