|
// 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;
using Microsoft.Win32.SafeHandles;
#if NETFRAMEWORK
using KeyBlobMagicNumber = Interop.BCrypt.KeyBlobMagicNumber;
#endif
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<ECDsaComponent, ECDsaAlgorithm>
#pragma warning restore SA1001 // Commas should be spaced correctly
#endif
{
private readonly ECDsaAlgorithm _algorithm;
#if NET || NETSTANDARD
private ECDsa _ecdsa;
#else
private ECDsaCng _ecdsa;
#endif
private ECDsaComponent(
#if NET || NETSTANDARD
ECDsa ecdsa,
#else
ECDsaCng ecdsa,
#endif
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) =>
algorithm.CurveOidValue is Oids.secp256r1 or Oids.secp384r1 or Oids.secp521r1;
public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm)
{
#if NET || NETSTANDARD
return new ECDsaComponent(ECDsa.Create(algorithm.Curve), algorithm);
#else
return new ECDsaComponent(CreateKey(algorithm.CurveOid.FriendlyName), algorithm);
#endif
}
public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan<byte> source)
{
Helpers.ThrowIfAsnInvalidLength(source);
fixed (byte* ptr = &MemoryMarshal.GetReference(source))
{
using (MemoryManager<byte> manager = new PointerMemoryManager<byte>(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.CurveOidValue)
{
// 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<byte> 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 || NETSTANDARD
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 // NETFRAMEWORK
#if NET472_OR_GREATER
#error ECDsa.Create(ECParameters) is avaliable in .NET Framework 4.7.2 and later, so this workaround is not needed anymore.
#endif
Debug.Assert(!string.IsNullOrEmpty(algorithm.CurveOid.FriendlyName));
Debug.Assert(x is null == y is null);
if (x is null)
{
byte[] zero = new byte[d.Length];
x = zero;
y = zero;
}
if (!TryValidateNamedCurve(x, y, d))
{
throw new CryptographicException(SR.Cryptography_InvalidECPrivateKeyParameters);
}
return new ECDsaComponent(
ECCng.EncodeEccKeyBlob(
algorithm.PrivateKeyBlobMagicNumber,
x,
y,
d,
blob => ImportKeyBlob(blob, algorithm.CurveOid.FriendlyName, includePrivateParameters: true)),
algorithm);
#endif
}
}
}
}
public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan<byte> 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);
}
byte[] x = source.Slice(1, fieldWidth).ToArray();
byte[] y = source.Slice(1 + fieldWidth).ToArray();
#if NET || NETSTANDARD
ECParameters parameters = new ECParameters()
{
Curve = algorithm.Curve,
Q = new ECPoint()
{
X = x,
Y = y,
}
};
return new ECDsaComponent(ECDsa.Create(parameters), algorithm);
#else // NETFRAMEWORK
#if NET472_OR_GREATER
#error ECDsa.Create(ECParameters) is available in .NET Framework 4.7.2 and later, so this workaround is not needed anymore.
#endif
Debug.Assert(!string.IsNullOrEmpty(algorithm.CurveOid.FriendlyName));
return new ECDsaComponent(
ECCng.EncodeEccKeyBlob(
algorithm.PublicKeyBlobMagicNumber,
x,
y,
d: null,
blob => ImportKeyBlob(blob, algorithm.CurveOid.FriendlyName, includePrivateParameters: false)),
algorithm);
#endif
}
internal override bool TryExportPrivateKey(Span<byte> destination, out int bytesWritten)
{
#if NET || NETSTANDARD
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.CurveOidValue && ecParameters.Curve.Oid.FriendlyName != _algorithm.CurveOid.FriendlyName))
{
Debug.Fail("Unexpected curve OID.");
throw new CryptographicException();
}
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
try
{
WriteKey(ecParameters.D, ecParameters.Q.X, ecParameters.Q.Y, _algorithm.CurveOidValue, writer);
return writer.TryEncode(destination, out bytesWritten);
}
finally
{
writer.Reset();
}
}
#else
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
try
{
_ecdsa.Key.ExportKeyBlob(
CngKeyBlobFormat.EccPrivateBlob.Format,
blob =>
{
ECCng.DecodeEccKeyBlob(
blob,
(magic, x, y, d) =>
{
if (magic != _algorithm.PrivateKeyBlobMagicNumber)
{
Debug.Fail("Unexpected magic number.");
throw new CryptographicException();
}
if (!TryValidateNamedCurve(x, y, d))
{
Debug.Fail("Invalid EC parameters.");
throw new CryptographicException();
}
WriteKey(d, x, y, _algorithm.CurveOidValue, writer);
return true;
});
});
return writer.TryEncode(destination, out bytesWritten);
}
finally
{
writer.Reset();
}
#endif
static void WriteKey(byte[] d, byte[]? x, byte[]? y, string curveOid, AsnWriter writer)
{
// 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);
}
}
}
}
}
internal override bool TryExportPublicKey(Span<byte> destination, out int bytesWritten)
{
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;
}
byte[]? x = null;
byte[]? y = null;
#if NET || NETSTANDARD
ECParameters ecParameters = _ecdsa.ExportParameters(includePrivateParameters: false);
ecParameters.Validate();
x = ecParameters.Q.X;
y = ecParameters.Q.Y;
#else
// Public parameters, x and y, don't need pinning so they can be pulled out of the inner lambdas.
_ecdsa.Key.ExportKeyBlob(
CngKeyBlobFormat.EccPublicBlob.Format,
blob =>
{
ECCng.DecodeEccKeyBlob(
blob,
(magic, localX, localY, _) =>
{
if (magic != _algorithm.PublicKeyBlobMagicNumber)
{
Debug.Fail("Unexpected magic number.");
throw new CryptographicException();
}
if (!TryValidateNamedCurve(localX, localY, null))
{
Debug.Fail("Invalid EC parameters.");
throw new CryptographicException();
}
x = localX;
y = localY;
return true;
});
});
#endif
Debug.Assert(x is not null);
Debug.Assert(y is not null);
if (x.Length != fieldWidth || y.Length != fieldWidth)
{
Debug.Fail("Unexpected key size.");
throw new CryptographicException();
}
// Uncompressed ECPoint format
destination[0] = 0x04;
x.CopyTo(destination.Slice(1, fieldWidth));
y.CopyTo(destination.Slice(1 + fieldWidth));
bytesWritten = 1 + 2 * fieldWidth;
return true;
}
internal override bool VerifyData(
#if NET
ReadOnlySpan<byte> data,
#else
byte[] data,
#endif
ReadOnlySpan<byte> signature)
{
#if NET
return _ecdsa.VerifyData(data, signature, _algorithm.HashAlgorithmName, DSASignatureFormat.Rfc3279DerSequence);
#else
byte[] ieeeSignature = null;
try
{
ieeeSignature = AsymmetricAlgorithmHelpers.ConvertDerToIeee1363(signature, _algorithm.KeySizeInBits);
}
catch (CryptographicException)
{
return false;
}
return _ecdsa.VerifyData(data, ieeeSignature, _algorithm.HashAlgorithmName);
#endif
}
internal override int SignData(
#if NET
ReadOnlySpan<byte> data,
#else
byte[] data,
#endif
Span<byte> 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
byte[] ieeeSignature = _ecdsa.SignData(data, _algorithm.HashAlgorithmName);
if (!AsymmetricAlgorithmHelpers.TryConvertIeee1363ToDer(ieeeSignature, destination, out int bytesWritten))
{
Debug.Fail("Buffer size should have been validated by caller.");
throw new CryptographicException();
}
return bytesWritten;
#endif
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_ecdsa?.Dispose();
_ecdsa = null!;
}
base.Dispose(disposing);
}
#if NETFRAMEWORK
#if NET472_OR_GREATER
#error ECDsa.Create(ECParameters) is available in .NET Framework 4.7.2 and later, so this workaround is not needed anymore.
#endif
private static ECDsaCng ImportKeyBlob(byte[] ecKeyBlob, string curveName, bool includePrivateParameters)
{
CngKeyBlobFormat blobFormat = includePrivateParameters ? CngKeyBlobFormat.EccPrivateBlob : CngKeyBlobFormat.EccPublicBlob;
CngProvider provider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
using (SafeNCryptProviderHandle providerHandle = provider.OpenStorageProvider())
using (SafeNCryptKeyHandle keyHandle = ECCng.ImportKeyBlob(blobFormat.Format, ecKeyBlob, curveName, providerHandle))
using (CngKey key = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.EphemeralKey))
{
key.SetExportPolicy(CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport);
return new ECDsaCng(key);
}
}
private static ECDsaCng CreateKey(string curveName)
{
CngKeyCreationParameters creationParameters = new CngKeyCreationParameters()
{
ExportPolicy = CngExportPolicies.AllowPlaintextExport,
};
byte[] curveNameBytes = new byte[(curveName.Length + 1) * sizeof(char)]; // +1 to add trailing null
System.Text.Encoding.Unicode.GetBytes(curveName, 0, curveName.Length, curveNameBytes, 0);
creationParameters.Parameters.Add(new CngProperty(KeyPropertyName.ECCCurveName, curveNameBytes, CngPropertyOptions.None));
using (CngKey key = CngKey.Create(new CngAlgorithm("ECDSA"), null, creationParameters))
{
return new ECDsaCng(key);
}
}
private static bool TryValidateNamedCurve(byte[]? x, byte[]? y, byte[]? d)
{
bool hasErrors = true;
if (d is not null && y is null && x is null)
{
hasErrors = false;
}
else if (y is not null && x is not null && y.Length == x.Length)
{
hasErrors = (d is not null && (d.Length != x.Length));
}
return !hasErrors;
}
#endif
}
}
}
|