1

Sorry to ask for debugging assistance but, as they say in the classics, "This used to work" :-) The "this" being the C# ECDsa.VerifyData() call below over a WebAuthn Assertion.Signature: -

    public string VerifyAssertion([FromBody] Assertion assertion)
    {
        if (assertion == null || assertion.Id == null || assertion.AuthenticatorData == null || assertion.ClientDataJSON == null || assertion.Signature == null)
        {
            // assertion.UserHandle is null for Samsung phone

            return FAIL_STATUS;
        }

        if (assertion.Id != TempDB.Id)
        {
            return FAIL_STATUS;
        }

        if (!ValidateClient(assertion.ClientDataJSON, "webauthn.get"))
        {
            return FAIL_STATUS;
        }

        byte[] authData = Convert.FromBase64String(assertion.AuthenticatorData);
        var creds = ValidateAuthData(authData);
        if (creds == null)
        {
            return FAIL_STATUS;
        }

        creds.Id = TempDB.Id;
        creds.PublicKeyJwk = TempDB.PublicKeyJwk;

        byte[] hashValClientData;
        try
        {
            hashValClientData = _hash.ComputeHash(Encoding.Latin1.GetBytes(assertion.ClientDataJSON));
        }
        catch (Exception e)
        {
            return FAIL_STATUS;
        }

        PublicKey pubKey;
        try
        {
            pubKey = JsonConvert.DeserializeObject<PublicKey>(creds.PublicKeyJwk);
        }
        catch (Exception ex)
        {
            return FAIL_STATUS;
        }

        byte[] data = new byte[authData.Length + hashValClientData.Length];
        Buffer.BlockCopy(authData, 0, data, 0, authData.Length);
        Buffer.BlockCopy(hashValClientData, 0, data, authData.Length, hashValClientData.Length);

        byte[] sig = Convert.FromBase64String(assertion.Signature);

        if (pubKey.kty == "EC")
        {
            byte[] ECDsaSig = convertFromASN1(sig);

            var point = new ECPoint
            {
                X = Convert.FromBase64String(pubKey.x),
                Y = Convert.FromBase64String(pubKey.y),
            };

            var ecparams = new ECParameters
            {
                Q = point,
                Curve = ECCurve.NamedCurves.nistP256
            };
            try
            {
                using (ECDsa dsa = ECDsa.Create(ecparams))
                {
                    ***if (dsa.VerifyData(data, ECDsaSig, HashAlgorithmName.SHA256))
                    {
                        Console.WriteLine("The signature is valid.");
                    }***
                    else
                    {
                        Console.WriteLine("The signature is not valid.");
                        return FAIL_STATUS;
                    }
                }
            }
            catch (Exception e)
            {
                return FAIL_STATUS;
            }
        } 

I assure you I'm not being lazy and am "happily" debugging (and have looked at similar questions here) but a fresh set of eyes may see it immediately? The convert from ASN1 method is as follows: -

    internal byte[] convertFromASN1(byte[] sig)
    {
        const int DER = 48;
        const int LENGTH_MARKER = 2;

        if (sig.Length < 6 || sig[0] != DER || sig[1] != sig.Length - 2 || sig[2] != LENGTH_MARKER || sig[sig[3] + 4] != LENGTH_MARKER)
            throw new ArgumentException("Invalid signature format.", "sig");

        int rLen = sig[3];
        int sLen = sig[rLen + 5];

        byte[] newSig = new byte[rLen + sLen];
        Buffer.BlockCopy(sig, 4, newSig, 0, rLen);
        Buffer.BlockCopy(sig, 6 + rLen, newSig, rLen, sLen);

        return newSig;
    }

Does it ring-a-bell / look-obvious to someone? SHA256 not used anymore?

"The signature is not valid"

EDIT 1

Here are the variables from the above code: -

assertion.Signature (base64)

MEQCIG8K9wWhL9PO16ito5LnsiLhJTFi9yH7DttKibsk6Os6AiBD0tEVSlb43LIaJKMhq1mLK1VV6RwfauJiuhgAhdWdAg==

Public Key { "kty":"EC", "crv":"P-256", "x":"/DNsJqnMWbSqSg5Sxvs26KheFQwMzci5DvjS6fnZGxw=", "y":"ywm5d125rYj6bOi9GZO7PB/04Qc0iPkDYmmHqSOd6Sk=" }

?sig = Convert.FromBase64String(assertion.Signature) {byte[70]} [0]: 48 [1]: 68 [2]: 2 [3]: 32 [4]: 111 [5]: 10 [6]: 247 [7]: 5 [8]: 161 [9]: 47 [10]: 211 [11]: 206 [12]: 215 [13]: 168 [14]: 173 [15]: 163 [16]: 146 [17]: 231 [18]: 178 [19]: 34 [20]: 225 [21]: 37 [22]: 49 [23]: 98 [24]: 247 [25]: 33 [26]: 251 [27]: 14 [28]: 219 [29]: 74 [30]: 137 [31]: 187 [32]: 36 [33]: 232 [34]: 235 [35]: 58 [36]: 2 [37]: 32 [38]: 67 [39]: 210 [40]: 209 [41]: 21 [42]: 74 [43]: 86 [44]: 248 [45]: 220 [46]: 178 [47]: 26 [48]: 36 [49]: 163 [50]: 33 [51]: 171 [52]: 89 [53]: 139 [54]: 43 [55]: 85 [56]: 85 [57]: 233 [58]: 28 [59]: 31 [60]: 106 [61]: 226 [62]: 98 [63]: 186 [64]: 24 [65]: 0 [66]: 133 [67]: 213 [68]: 157 [69]: 2

ECDsaSig = convertFromASN1(sig); {byte[64]} [0]: 111 [1]: 10 [2]: 247 [3]: 5 [4]: 161 [5]: 47 [6]: 211 [7]: 206 [8]: 215 [9]: 168 [10]: 173 [11]: 163 [12]: 146 [13]: 231 [14]: 178 [15]: 34 [16]: 225 [17]: 37 [18]: 49 [19]: 98 [20]: 247 [21]: 33 [22]: 251 [23]: 14 [24]: 219 [25]: 74 [26]: 137 [27]: 187 [28]: 36 [29]: 232 [30]: 235 [31]: 58 [32]: 67 [33]: 210 [34]: 209 [35]: 21 [36]: 74 [37]: 86 [38]: 248 [39]: 220 [40]: 178 [41]: 26 [42]: 36 [43]: 163 [44]: 33 [45]: 171 [46]: 89 [47]: 139 [48]: 43 [49]: 85 [50]: 85 [51]: 233 [52]: 28 [53]: 31 [54]: 106 [55]: 226 [56]: 98 [57]: 186 [58]: 24 [59]: 0 [60]: 133 [61]: 213 [62]: 157 [63]: 2

https://lapo.it/asn1js/#MEQCIG8K9wWhL9PO16ito5LnsiLhJTFi9yH7DttKibsk6Os6AiBD0tEVSlb43LIaJKMhq1mLK1VV6RwfauJiuhgAhdWdAg

The results from that ASN.1 Javascript Decoder page don't look promising :-(

I'm trying to explain the different bytes now

END EDIT 1

EDIT 2

Looks like I'm not outputting type and length bytes from my ASN.1 conversion.

Nah looks good to me???

rLen = 32 sLen = 32

    byte[] newSig = new byte[rLen + sLen];
    Buffer.BlockCopy(sig, 4, newSig, 0, rLen);
    Buffer.BlockCopy(sig, 6 + rLen, newSig, rLen, sLen);

END EDIT 2

EDIT 3

It appears to be timing/data related :-(

Here are examples of two credential verification assertions. The first is not signed correctly but the second is. (Same validation code)

This fails

server key = {"Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIwMzM4MjYsImlzcyI6IlRlc3QuY29tIiwiYXVkIjoiVGVzdC5jb20ifQ.dwOSgad7uoKZFBmg6n6SVLccDNkiQZRDtjAgP-2G2fY"}

=== Assertion response ===

{id: 'Ad1BvBnDxMs7EzShvRJdVS/KC20flHMn5X3KygTYMH0yKnT/HGFxnKROAJg4KRWu3qZEuJfKLRL5oG+4+ufpI4U=', clientDataJSON: '{"type":"webauthn.get","challenge":"ZXlKaGJHY2lPaU…12472","androidPackageName":"com.android.chrome"}', userHandle: undefined, signature: 'MEUCICAiMapES55djGcYoBWjLTIC74+7uWR+ceRHAZyQmJaYAiEA8vfd+Uhg9h3bKIMWA7l9t3Kq8nVk4oa45/Gbs+pQTcM=', authenticatorData: 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ=='} authenticatorData: "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==" clientDataJSON: "{"type":"webauthn.get","challenge":"ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SmxlSEFpT2pFMk5qSXdNek00TWpZc0ltbHpjeUk2SWxSbGMzUXVZMjl0SWl3aVlYVmtJam9pVkdWemRDNWpiMjBpZlEuZHdPU2dhZDd1b0taRkJtZzZuNlNWTGNjRE5raVFaUkR0akFnUC0yRzJmWQ","origin":"http:\/\/localhost:12472","androidPackageName":"com.android.chrome"}" id: "Ad1BvBnDxMs7EzShvRJdVS/KC20flHMn5X3KygTYMH0yKnT/HGFxnKROAJg4KRWu3qZEuJfKLRL5oG+4+ufpI4U=" signature: "MEUCICAiMapES55djGcYoBWjLTIC74+7uWR+ceRHAZyQmJaYAiEA8vfd+Uhg9h3bKIMWA7l9t3Kq8nVk4oa45/Gbs+pQTcM=" userHandle: undefined

This succeeds

server key = {"Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIwMzU0NTMsImlzcyI6IlRlc3QuY29tIiwiYXVkIjoiVGVzdC5jb20ifQ.cLstUjYKzMV8Mip7jhdLucw8qGLcwKTnFu40rR4jy5o"}

=== Assertion response ===

{id: 'Ad1BvBnDxMs7EzShvRJdVS/KC20flHMn5X3KygTYMH0yKnT/HGFxnKROAJg4KRWu3qZEuJfKLRL5oG+4+ufpI4U=', clientDataJSON: '{"type":"webauthn.get","challenge":"ZXlKaGJHY2lPaU…12472","androidPackageName":"com.android.chrome"}', userHandle: undefined, signature: 'MEQCIHpDRriOIExTuSu/Pps+wz53QNBIVvkZkpKqKDvPL18fAiA3gbgWgHeXLbS/VH55yQsISkJF0enJpDmpVL4k+I5Sng==', authenticatorData: 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAg=='} authenticatorData: "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAg==" clientDataJSON: "{"type":"webauthn.get","challenge":"ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SmxlSEFpT2pFMk5qSXdNelUwTlRNc0ltbHpjeUk2SWxSbGMzUXVZMjl0SWl3aVlYVmtJam9pVkdWemRDNWpiMjBpZlEuY0xzdFVqWUt6TVY4TWlwN2poZEx1Y3c4cUdMY3dLVG5GdTQwclI0ank1bw","origin":"http:\/\/localhost:12472","androidPackageName":"com.android.chrome"}" id: "Ad1BvBnDxMs7EzShvRJdVS/KC20flHMn5X3KygTYMH0yKnT/HGFxnKROAJg4KRWu3qZEuJfKLRL5oG+4+ufpI4U=" signature: "MEQCIHpDRriOIExTuSu/Pps+wz53QNBIVvkZkpKqKDvPL18fAiA3gbgWgHeXLbS/VH55yQsISkJF0enJpDmpVL4k+I5Sng==" userHandle: undefined

END EDIT 3

McMurphy
  • 1,235
  • 1
  • 15
  • 39
  • 2
    ASN1 is a tricky little devil to convert into something that we can use in c# - try this -> https://github.com/passwordless-lib/fido2-net-lib/blob/fb5b95218829f8f1d99b8a7bd5245151eafc97a1/Src/Fido2/CryptoUtils.cs#L65 – daveBM Aug 31 '22 at 10:11
  • Pretty much same response from https://stackoverflow.com/questions/66667211/ecdsacng-verify-data-elliptical-curve-signature-from-android-webauthn-via-c-shar/66676200#66676200. – aseigler Aug 31 '22 at 10:52
  • using Asn1; Is interesting but First Principles / logic / spec would be good? – McMurphy Aug 31 '22 at 11:30
  • 1
    If you really, truly want to do it manually, check the history of that SigFromEcDsaSig() function, we used to do byte for byte manual ASN.1 decoding, but not anymore. We moved to an external ASN.1 lib as soon as I found a reasonably portable one, then switched to System.Formats.Asn1 as soon as it became available. – aseigler Aug 31 '22 at 12:06
  • Thanks @aseigler I'll look up S.F.Asn1 today. If you have an example or pointer that would also help? – McMurphy Sep 01 '22 at 00:53
  • System.Formats.Asn1 looks about as friendly as CBOR :-( Is there really anything wrong with my convertFromASN1 method??? – McMurphy Sep 01 '22 at 05:15
  • Please see EDIT 3 for examples of an Assertion that passes the Signature Verification ECDsa.VerifyData and another that does not. – McMurphy Sep 01 '22 at 13:01

1 Answers1

1

You do not want to do this by hand. You definitely want to use System.Formats.Asn1. Your trouble is how integers are encoded in ASN.1, https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf, section 8.3.

Here's an example using your supplied signature values.

https://dotnetfiddle.net/UFuJ49

Basically, if the first byte is 00000000 and the high order bit is set on the second byte, for this use case you will want to remove the first byte. Further explaination https://stackoverflow.com/a/55360715/15356060.

aseigler
  • 504
  • 3
  • 7
  • once again you are a very generous technologist. Much appreciated. – McMurphy Sep 03 '22 at 06:11
  • Sorry for asking to be spoon fed but do I have to use an algorithm such as https://github.com/passwordless-lib/fido2-net-lib/blob/fb5b95218829f8f1d99b8a7bd5245151eafc97a1/Src/Fido2/CryptoUtils.cs#L65 to strip the null byte or does the ASN1.reader offer an automagical 1s/2s compliment conversion? and what is the coefficient for DER? Look I read through the CBOR docs but would be grateful for any opacity on the type, length, value (or whatever it's called) for ASN1 – McMurphy Sep 05 '22 at 07:40
  • 1
    You have to figure out whether to remove the first byte or not yourself. Don't just look for a null first byte and remove it indiscriminately, you have to actually follow the rules and only remove the byte if it matches the pattern. The buffer size for R & S you are looking for depends on whether you are looking at P256, P384, or P521 (32, 48, 64 respectively). This has nothing to do with CBOR. – aseigler Sep 05 '22 at 18:45
  • Thanks again! I meant ASN1.Asn1Reader not CBOR sorry. I was wondering if a ReadInt32 or some such would dor the endianess and compliment stuff for us. The code you provided is perfectly workable and tiny anywy. – McMurphy Sep 06 '22 at 01:39