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