Webauthn authentication with Windows Hello via c#

Viewed 703

UPDATE: ===========

Having a very similar issue with Elliptical Curve as I did with RSA. VarifyData always returns false.

        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")
        {
            var keyType = new byte[] { 0x45, 0x43, 0x53, 0x31 }; // BCRYPT_ECDSA_PUBLIC_P256_MAGIC {E, C, S, 1}
            var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; // Key length 32
            byte[] keyImport = new byte [72];
            Buffer.BlockCopy(keyType, 0, keyImport, 0, 4);
            Buffer.BlockCopy(keyLength, 0, keyImport, 4, 4);
            Buffer.BlockCopy(Convert.FromBase64String(pubKey.x), 0, keyImport, 8, 32);
            Buffer.BlockCopy(Convert.FromBase64String(pubKey.y), 0, keyImport, 40, 32);

            try
            {
                using (ECDsaCng dsa = new ECDsaCng(CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob)))
                {
                    dsa.HashAlgorithm = CngAlgorithm.Sha256;
                    if (dsa.VerifyData(data, sig))
                    {
                        Console.WriteLine("The signature is valid.");
                    }
                    else
                    {
                        Console.WriteLine("The signature is not valid.");
                        return FAIL_STATUS;
                    }
                }
            }
            catch (Exception e)
            {
                return FAIL_STATUS;
            }
        } 

The ECDsaCng object and the CngKey are valid but the VerifyData method alway returns false. Something wrong with my data?

ClientDataJSON has the forward slashes escaped from Android EC1 but not Windows RSA

===================

I'm trying to use C# to validate the FIDO2/WebAuthn credentials returned from the Javascript navigator.credentials.get(). The authenticator here is Windows Hello (the PIN code) The VerifySignature method keeps returning invalid signature no matter what I do. Am I using the wrong RSA algorithm? Base64 when it shouldn't be? Any ideas?

Javascript code: -

return navigator.credentials.get({
        publicKey: getAssertionOptions
    }).then(rawAssertion => {
        var assertion = {
            id: base64encode(rawAssertion.rawId),
            clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON),
            userHandle: base64encode(rawAssertion.response.userHandle),
            signature: base64encode(rawAssertion.response.signature),
            authenticatorData: base64encode(rawAssertion.response.authenticatorData)
        };

C# Code: -

creds.Id = tempDB.Id;
creds.PublicKeyJwk = tempDB.PublicKeyJwk;

byte[] hashValClientData = _hash.ComputeHash(Encoding.Latin1.GetBytes(assertion.ClientDataJSON));

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

RSAParameters keyInfo = new RSAParameters();
keyInfo.Modulus = Encoding.Latin1.GetBytes(Base64Decode(pubKey.n));
keyInfo.Exponent = Encoding.Latin1.GetBytes(Base64Decode(pubKey.e));
rsa.ImportParameters(keyInfo);
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
byte[] sig = Encoding.Latin1.GetBytes(Base64Decode(assertion.Signature));
if (rsaDeformatter.VerifySignature(hashValClientData, sig))
{
    Console.WriteLine("The signature is valid.");
}
else
{
    Console.WriteLine("The signature is not valid.");
}

        public static string Base64Encode(string plainText)
        {
            var plainTextBytes = Encoding.Latin1.GetBytes(plainText);
            return Convert.ToBase64String(plainTextBytes);
        }
        public static string Base64Decode(string base64EncodedData)
        {
            string paddedString = base64EncodedData;
            int padding = base64EncodedData.Length % 4;
            if (padding > 0 && padding < 3)
            {
                paddedString += "==".Substring(0, padding);
            }
            var base64EncodedBytes = Convert.FromBase64String(paddedString);
            return Encoding.Latin1.GetString(base64EncodedBytes);
        }

Browser Console output: -

=== Assertion response === test.html:211 id (base64): gtCDzIXzuh0ZlblqiyMFf7f0/TS2m2a8sLvbj3CtERo= test.html:211 clientDataJSON: {"type":"webauthn.get","challenge":"ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SmxlSEFpT2pFMk1UUTFNRFV6TURJc0ltbHpjeUk2SWxSbGMzUXVZMjl0SWl3aVlYVmtJam9pVkdWemRDNWpiMjBpZlEuUXdYVUdob3FQM1RGckhGV2pOOHNyZWVadFpMM2gtaUVpZk9jTWlzbHQxVQ","origin":"https://localhost:44362","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. "} test.html:211 userHandle (base64): c29tZS51c2VyLmlk test.html:211 signature (base64): Gd0x/28tLTKba9/LRa+7riJ4XygPgfAjwdVw3h/fxisWSU8OLbcfqu6K5bIFspnPrsTyA6xD9I+5Sq/BAOalcAJCy46/39swTPF6+76F8Hx5GFNcXusMZw6PQZpEqALZkifF936hTBXCoVrYcl9NZ5/jjd9zpFhSN90Ht/WEAl4DrvgnZ/NQa2klCpm4anDaZoYDcv9SykqtUv/CHNAtpSYgcfA8XVcDGG3ieefw1rii6g6chgTNfwhctIiqSkCBrLECavVUrbT6UpF+R2nIgexCyT8dKe8gVxvNaUeFnltSSkleOo/GUHzisseFjTow+R9yo4og/tuuS9PSWTR8WA== test.html:211 authenticatorData (base64): SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAQ==

1 Answers

There may be other issues, but the biggest problem is that you are attempting to verify the signature against the hash of the clientDataJSON. It should actually be the binary concatenation of authenticatorData and the hashed clientDataJSON.

From https://www.w3.org/TR/webauthn/#sctn-verifying-assertion, step 20:

"Using credentialPublicKey, verify that sig is a valid signature over the binary concatenation of authData and hash."

Given what you have, something like this should work:

    var authData = Convert.FromBase64String(assertion.authenticatorData);
    byte[] hashValClientData = _hash.ComputeHash(assertion.ClientDataJSON);

    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);

    var rsa = RSA.Create();
    rsa.ImportParameters(
        new RSAParameters()
        {
            Modulus = Convert.FromBase64String(pubKey.n),
            Exponent = Convert.FromBase64String(pubKey.e),
        }
    );

    byte[] sig = Convert.FromBase64String(assertion.Signature);
    if (rsa.VerifyData(data, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
    {
        Console.WriteLine("The signature is valid.");
    }
    else
    {
        Console.WriteLine("The signature is not valid.");
    }

This project may have some code that is of interest to you: https://github.com/abergs/fido2-net-lib

Related