1

I am the author of a .NET library that allows developers to process data provided by a 3rd party. Among the many features my library provides is the ability to validate that received data was indeed signed by the 3rd party in question. The 3rd party provides the following information:

  • a string containing base64 encoded DER signature
  • a string containing base64 encoded secp256r1/NIST P-256 public key
  • a array of bytes containing the data that was encoded by the 3rd party using the private key

The developer expects my library to return a Boolean value indicating whether the data is legitimate or not. I was able to figure out how to convert the signature to Microsoft CNG supported format thanks to this StackOverflow question and, similarly, I figured out how to convert the public key into Microsoft CNG supported format thanks to this other StackOverflow question. I put it all together in the following C# code snippet:

// Convert the signature and public key provided by the 3rd party into formats usable by the .net crypto classes
var signatureBytes = Convert.FromBase64String(signature);
var sig = ConvertECDSASignature.LightweightConvertSignatureFromX9_62ToISO7816_8(256, signatureBytes);
var cngBlob = Utils.ConvertSecp256R1PublicKeyToEccPublicBlob(publicKey);

// Verify the signature
var cngKey = CngKey.Import(cngBlob, CngKeyBlobFormat.EccPublicBlob);
var eCDsaCng = new ECDsaCng(cngKey);
var verified = eCDsaCng.VerifyData(data, sig);

This has been working perfectly until a developer recently complained about System.PlatformNotSupportedException on Linux/Ubuntu machines. After a quick research, I found out that ECDsaCng is Windows-specific and I should be using ECDsa which is cross-platform.

So I came up with the following code which is no only cross-platform but also is much simpler that my original code because I no longer need to convert the signature and public key to different formats:

var signatureBytes = Convert.FromBase64String(signature);
var publicKeyBytes = Convert.FromBase64String(publicKey);

// Verify the signature
var eCDsa = ECDsa.Create();
eCDsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
var verified = eCDsa.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);

The only caveat is that Microsoft introduced the ImportSubjectPublicKeyInfo method in the ECDsa class in more recent versions of the .NET framework (I could be wrong but I believe it was introduced in .NET core 3.1) and my library targets netstandard2.0 so it can be used by developers who are not necessarily using the latest .NET.

So, all this to say: is there a way to validate the data using the provided signature and public key in a way that is cross-platform AND usable in netstandard2.0?

desautelsj
  • 3,587
  • 4
  • 37
  • 55
  • As of .NET Framework 4.7 [`ECDsa.ImportParameters()`](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.ecdsa.importparameters?view=netframework-4.7#applies-to) is available. With this the public key (as an alternative to `ImportSubjectPublicKeyInfo()`) can be imported as well. However, [`DSASignatureFormat`](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dsasignatureformat?view=net-5.0#applies-to) is only supported in .NET 5, so for .NET Framework and Core probably the signature conversion is still necessary. – Topaco Apr 03 '21 at 20:54
  • @Topaco do you have a code sample to show how to specify the public key when invoking ImportParameters? – desautelsj Apr 03 '21 at 22:27

1 Answers1

2

ECDsa.ImportSubjectPublicKey() is supported in .NET Core 3.0 and later, but not in .NET Framework. An alternative would be ECDsa.ImportParameters(), which according to the documentation is supported as of .NET Framework 4.7 and .NET Core 1.0. DSASignatureFormat is supported as of .NET 5.0.

So a possible alternative would be the current code, with the key import modified as follows:

// Test data
byte[] data = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");               
string signature = "MEUCIFBfoMubs82ExlbPQHR2LKKcJpvODxaoo4NO4VoKmRfxAiEAg6tug3ctzSAZrkF175B71D7Uynn9Bc1O40XIpxD93MY=";
string publicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==";

// Convert the signature and public key provided by the 3rd party into formats usable by the .net crypto classes
var signatureBytes = Convert.FromBase64String(signature);
var sig = lightweightConvertSignatureFromX9_62ToISO7816_8(256, signatureBytes);
ECParameters ecParameters = ConvertSecp256r1PublicKeyToECParameters(publicKey);     // Replaced!

// Verify the signature
var eCDsa = ECDsa.Create();
eCDsa.ImportParameters(ecParameters);
var verified = eCDsa.VerifyData(data, sig, HashAlgorithmName.SHA256);

where ConvertSecp256r1PublicKeyToECParameters() is a slightly modified version of ConvertSecp256R1PublicKeyToEccPublicBlob():

private static readonly byte[] s_secp256r1Prefix = Convert.FromBase64String("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");
private static ECParameters ConvertSecp256r1PublicKeyToECParameters(string base64)
{
    byte[] subjectPublicKeyInfo = Convert.FromBase64String(base64);

    if (subjectPublicKeyInfo.Length != 91)
        throw new InvalidOperationException();

    byte[] prefix = s_secp256r1Prefix;

    if (!subjectPublicKeyInfo.Take(prefix.Length).SequenceEqual(prefix))
        throw new InvalidOperationException();

    byte[] x = new byte[32];
    byte[] y = new byte[32];
    Buffer.BlockCopy(subjectPublicKeyInfo, prefix.Length, x, 0, x.Length);
    Buffer.BlockCopy(subjectPublicKeyInfo, prefix.Length + x.Length, y, 0, y.Length);

    ECParameters ecParameters = new ECParameters();
    ecParameters.Curve = ECCurve.NamedCurves.nistP256; // aka secp256r1 aka  prime256v1
    ecParameters.Q.X = x;
    ecParameters.Q.Y = y;

    return ecParameters;
}

The code is supported on my machine (Windows 10) as of .NET Framework 4.7 and .NET Core 2.0 (it doesn't run under 1.0, but that may be a local issue). I see no reason why there should be problems on Linux/Ubuntu. However, I have not tested this.

Topaco
  • 40,594
  • 4
  • 35
  • 62