2

I am implementing Apple's App Attestation service.

As part of the process, i receive a EC key and a signature.

Sample key:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEd34IR9wYL76jLyZ148O/hjXo9iaF
z/q/xEMXCwYPy6yxbxYzWDZPegG4FH+snXaXQPYD6QIzZNY/kcMjIGtUTg==
-----END PUBLIC KEY-----

Sample signature:

MEUCIQDXR/22YAi90PUdKrtTHwigrDxWFoiCqPLB/Of1bZPCKQIgNLxFAeUU2x+FSWfhRGX0SOKUIDxPRoigsCHpJxgGXXU=

Sample sha256 hash:

S3i6LAEzew5SDjQbq59/FraEAvGDg9y7fRIfbnhHPf4=

If i put this into a couple of txt files like so:

System.IO.File.WriteAllBytes("/wherever/sig", Convert.FromBase64String(sampleSignature));

System.IO.File.WriteAllBytes("/wherever/hash", Convert.FromBase64String(sampleSha256Hash));

Then i can validate the signature with Openssl like so

openssl dgst -sha256 -verify sampleKey.pem -signature /wherever/sig /wherever/hash

(the above outputs)

Verified OK

I can verify the signature using Bouncy Castle like so:

var bouncyCert = DotNetUtilities.FromX509Certificate(certificate);
var bouncyPk = (ECPublicKeyParameters)bouncyCert.GetPublicKey();
var verifier = SignerUtilities.GetSigner("SHA-256withECDSA");
verifier.Init(false, bouncyPk);
verifier.BlockUpdate(sha256HashByteArray, 0, sha256HashByteArray.Length);
var valid = verifier.VerifySignature(signature); // Happy days, this is true

Since i don't want to share my whole certificate here, the same sample may be achieved as follows:

// these are the values from the sample key shared at the start of the post
// as returned by BC. Note that .Net's Y byte array is completely different.

 Org.BouncyCastle.Math.BigInteger x = new Org.BouncyCastle.Math.BigInteger(Convert.FromBase64String("d34IR9wYL76jLyZ148O/hjXo9iaFz/q/xEMXCwYPy6w="));
Org.BouncyCastle.Math.BigInteger y = new Org.BouncyCastle.Math.BigInteger(Convert.FromBase64String("ALFvFjNYNk96AbgUf6yddpdA9gPpAjNk1j+RwyMga1RO"));

X9ECParameters nistParams = NistNamedCurves.GetByName("P-256");
ECDomainParameters domainParameters = new ECDomainParameters(nistParams.Curve, nistParams.G, nistParams.N, nistParams.H, nistParams.GetSeed());
var G = nistParams.G;
Org.BouncyCastle.Math.EC.ECCurve curve = nistParams.Curve;
Org.BouncyCastle.Math.EC.ECPoint q = curve.CreatePoint(x, y);

ECPublicKeyParameters pubkeyParam = new ECPublicKeyParameters(q, domainParameters);

var verifier = SignerUtilities.GetSigner("SHA-256withECDSA");
verifier.Init(false, pubkeyParam);
verifier.BlockUpdate(sha256HashByteArray, 0, sha256HashByteArray.Length);
var valid = verifier.VerifySignature(signature); // again, happy days.

However, i really want to avoid using bouncy castle.

So i am trying to use ECDsa available in .net core:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

var certificate = new X509Certificate2(cert);
var publicKey = certificate.GetECDsaPublicKey();
var valid = publicKey.VerifyHash(sha256HashByteArray, signature); // FALSE :(

if you want to try to run the above here's the sample that creates the keys without the whole certificate:

using System.Security.Cryptography;

var ecParams = new ECParameters();
ecParams.Curve = ECCurve.CreateFromValue("1.2.840.10045.3.1.7");
ecParams.Q.X = Convert.FromBase64String("d34IR9wYL76jLyZ148O/hjXo9iaFz/q/xEMXCwYPy6w=");
// I KNOW that this is different from BC sample - i got each respective values from
// certificates in respective libraries, and it seems the way they format the coordinates
// are different.
ecParams.Q.Y = Convert.FromBase64String("sW8WM1g2T3oBuBR/rJ12l0D2A+kCM2TWP5HDIyBrVE4=");

var ecDsa = ECDsa.Create(ecParams);

var isValid = ecDsa.VerifyHash(nonce, signature); // FALSE :(

I tried using VerifyData() instead and feeding raw data and HashAlgorithmName.SHA256 with no luck.

I found a response here (https://stackoverflow.com/a/49449863/2057955) that seems to suggest that .net expects the signature as r,s concatenation, so i pulled them out of the DER sequence that i get back from my device (see sample signature) however that had no luck at all, i just can't get that 'true' back.

Question: how can i verify this EC signature using .Net Core on LINUX/MacOs (so unable to use ECDsaCng class)?

zaitsman
  • 8,984
  • 6
  • 47
  • 79

1 Answers1

3

SignerUtilities.GetSigner() hashes implicitly, i.e. sha256HashByteArray is hashed again. Therefore instead of ECDsa#VerifyHash() (does not hash implicitly) the method ECDsa#VerifyData() (hashes implicitly) must be used.
Also, SignerUtilities.GetSigner() returns a signature in ASN.1 format, and ECDsa#VerifyData() expects a signature in r|s format (as you already figured out).
If both are taken into account, the verification is successful:

byte[] publicKey = Convert.FromBase64String("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEd34IR9wYL76jLyZ148O/hjXo9iaFz/q/xEMXCwYPy6yxbxYzWDZPegG4FH+snXaXQPYD6QIzZNY/kcMjIGtUTg==");
byte[] sha256HashByteArray = Convert.FromBase64String("S3i6LAEzew5SDjQbq59/FraEAvGDg9y7fRIfbnhHPf4=");
byte[] signatureRS = Convert.FromBase64String("10f9tmAIvdD1HSq7Ux8IoKw8VhaIgqjywfzn9W2Twik0vEUB5RTbH4VJZ+FEZfRI4pQgPE9GiKCwIeknGAZddQ==");

var ecDsa = ECDsa.Create();
ecDsa.ImportSubjectPublicKeyInfo(publicKey, out _);

var isValid = ecDsa.VerifyData(sha256HashByteArray, signatureRS, HashAlgorithmName.SHA256);
Console.WriteLine(isValid); // True

Regarding the signature formats:

The posted signature in ASN.1 format

MEUCIQDXR/22YAi90PUdKrtTHwigrDxWFoiCqPLB/Of1bZPCKQIgNLxFAeUU2x+FSWfhRGX0SOKUIDxPRoigsCHpJxgGXXU=

is hex encoded

3045022100d747fdb66008bdd0f51d2abb531f08a0ac3c56168882a8f2c1fce7f56d93c229022034bc4501e514db1f854967e14465f448e294203c4f4688a0b021e92718065d75

From this, the signature in r|s format can be derived as (s. here)

d747fdb66008bdd0f51d2abb531f08a0ac3c56168882a8f2c1fce7f56d93c22934bc4501e514db1f854967e14465f448e294203c4f4688a0b021e92718065d75

or Base64 encoded:

10f9tmAIvdD1HSq7Ux8IoKw8VhaIgqjywfzn9W2Twik0vEUB5RTbH4VJZ+FEZfRI4pQgPE9GiKCwIeknGAZddQ==
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • 1
    This is awesome. Can you show how you programmatically convert ASN.1 signature to the r|s format? (e.g. show code for `From this, the signature in r|s format can be derived as`) – zaitsman Jun 30 '21 at 08:50
  • (I can do it with BouncyCastle again, but i really want to avoid it and not having much love from `System.Formats.Asn1.AsnDecoder`) – zaitsman Jun 30 '21 at 08:58
  • I also tried `System.Formats.Asn1.AsnReader` like so `var reader = new AsnReader(signature, AsnEncodingRules.DER); var seqZ = reader.ReadSequence(); var rZ = seqZ.ReadIntegerBytes(); var sZ = seqZ.ReadIntegerBytes(); var b64 = Convert.ToBase64String(rZ.ToArray().Concat(sZ.ToArray()).ToArray());` and my b64 contains `"ANdH/bZgCL3Q9R0qu1MfCKCsPFYWiIKo8sH85/Vtk8IpNLxFAeUU2x+FSWfhRGX0SOKUIDxPRoigsCHpJxgGXXU="` instead of what you posted – zaitsman Jun 30 '21 at 09:08
  • @zaitsman - Your approach is almost correct. `r` and `s` are minimal-sized signed big-endian and positive, which may require an extra 0x00 byte in front. This is explained in detail in this [answer](https://crypto.stackexchange.com/a/57734). The associated question itself explains the conversion. In your example, `r` has a leading 0x00. If this is removed, the result is correct. – Topaco Jun 30 '21 at 09:41
  • @zaitsman so do you have a solution in the end? Interested to know the .net core solution – Nathan Do Apr 13 '22 at 15:35
  • @NathanDo This was some time ago, but to be honest I was just missing the bits that `Topaco` showed me in their answer and from there it was as easy as figuring out whether the r and s had a leading 0x00 byte to be stripped – zaitsman Apr 14 '22 at 00:02
  • haha, I've given up last night and given in to bouncy castle... It works. Definitely more verbose, but it works. – Nathan Do Apr 14 '22 at 03:28