简体   繁体   中英

Validating Elyptical Public Key signatures for WebAuth Assertions

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: "ME UCICAiMapES55djGcYoBWjLTIC74+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: "ME QCIHpDRriOIExTuSu/Pps+wz53QNBIVvkZkpKqKDvPL18fAiA3gbgWgHeXLbS/VH55yQsISkJF0enJpDmpVL4k+I5Sng==" userHandle: undefined

END EDIT 3

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM