Repository: duo-labs/py_webauthn

Issue:
Systemic presence of extra byte for registration with eddsa
In goal to register a passkey on a website, I use a php library (https://github.com/web-auth/webauthn-framework). When I try to register a new credential with eddsa as 'prefered' algorithm, the load of passkey's response failed for presence of extra bytes in authentication data .If I comment the check then all is good.

I've tried to decompose the attestation object by hand and find that:

```
00000000: 10100011 01100011 01100110 01101101 01110100 01100100 01101110 01101111  .cfmtdno
          map    3 txt    3        f        m        t txt    4        n        o

00000008: 01101110 01100101 01100111 01100001 01110100 01110100 01010011 01110100  negattSt
                 n        e txt    7        a        t        t        S        t

00000016: 01101101 01110100 10100000 01101000 01100001 01110101 01110100 01101000  mt.hauth
                 m        t map    0 txt    8        a        u        t        h

00000024: 01000100 01100001 01110100 01100001 01011001 00000001 00000101 00010110  DataY...   ==> 25 means length is in 2 next bytes (total length 292, -261 = 31)
                 D        a        t        a byt   25               261 <rpIdHash

00000032: 10110000 00101101 11111011 11000011 11010100 11001100 10100011 01111110  .-.....~
00000040: 10111100 00101111 01100101 00010110 01100101 10011011 00010010 00100001  ./e.e..!
00000048: 00001101 10111001 11100001 00000001 10001010 10111001 11110001 00111010  .......:
00000056: 10010110 10010000 01100011 10001110 10100110 11111101 10101000 01000101  ..c....E   ==> flags UP:/:UV:/:/:/:AT:ED little-endian
                                                               rpIdHash> <flags >                        1 0  1 0 0 0  1  0

00000064: 00000000 00000000 00000000 00000001 00101111 11000000 01010111 10011111  ..../.W.   ==> signCount: 1
          <signCount                        > <aaguid
                                              <attestedCredentialData

00000072: 10000001 00010011 01000111 11101010 10110001 00010110 10111011 01011010  ..G....Z   ==> aaguid: 2fc0579f-8113-47ea-b116-bb5a8db9202a
00000080: 10001101 10111001 00100000 00101010 00000000 10000000 11101000 00101111  .. *.../   ==> credentialIdLength: 128
                                      aaguid> <credentialIdLen> <credentialId

00000088: 11100110 10111101 11100011 00000000 11100100 11101100 11001001 00111110  .......>
00000096: 00000000 00010110 01000100 10001010 11010000 00001111 10100110 11110010  ..D.....
00000104: 10001010 00000001 00011010 01101111 10000111 11111111 01111011 00001100  ...o..{.
00000112: 11111100 10100100 10011001 10111110 10101111 10000011 00110100 01001100  ......4L
00000120: 00110110 01100000 10110101 11101100 10101011 11110111 00101010 00111011  6`....*;
00000128: 00101000 00111000 10100000 11001100 01111101 10000111 11010011 11111010  (8..}...
00000136: 01011000 00101001 00101011 01010011 01000100 10011100 11111111 00010011  X)+SD...
00000144: 10101101 01101001 01110011 00101101 01110101 00100001 01100100 10011101  .is-u!d.
00000152: 00110110 01011100 11001011 11000101 11010000 10100000 11111010 01001011  6\.....K
00000160: 01001110 00001001 11101010 11101001 10010101 00110111 00100110 00011111  N....7&.
00000168: 00101111 01000100 00001001 00111111 10001111 01001111 11010100 11001111  /D.?.O..
00000176: 01010111 10010110 11100000 11111110 01011000 11111111 00000110 00010101  W...X...
00000184: 11111111 11000101 10001000 00101000 00110110 10111011 11100111 10111001  ...(6...
00000192: 10011011 00001000 10111110 00101001 10000110 01110010 00011100 00011100  ...).r..
00000200: 01011010 01101010 11000111 11110011 00101101 00110010 00100000 11011001  Zj..-2 .
00000208: 10110011 01001101 10001101 11101110 00101111 11001001 10100011 00000001  .M../...    ==> 1 -> kty: OKP
                                                  credentialId> map    3 uin    1
                                                                <credentialPublicKey

00000216: 01100011 01001111 01001011 01010000 00000011 00100111 00100000 01100111  cOKP.' g    ==> 3 -> alg: -8 (eddsa)
          txt    3        O        K        P uin    3 nin    7 nin    0 txt    7

00000224: 01000101 01100100 00110010 00110101 00110101 00110001 00111001 00100001  Ed25519!    ==> -1 -> crv: Ed25519
                 E        d        2        5        5        1        9 nin    1              ==> -2 -> should be public key (bstr)
                                                    credentialPublicKey> <extensions? (should be a map 101 not a negative integer 001)
                                                 attestedCredentialData>

00000232: 10011000 00100000 00010110 00011000 11110110 00011000 01011001 00011000  . ....Y.
          arr   24       32 uin   22 uin   24      246 uin   24       89 uin   24

00000240: 11111010 00011000 00101110 00010100 00011000 01110101 00011000 00111010  .....u.:
               250 uin   24       46 uin   20 uin   24      117 uin   24       58

00000248: 00011000 10000100 00010111 00011000 01010010 00011000 01110100 00011000  ....R.t.
          uin   24      132 uin   23 uin   24       82 uin   24      116 uin   24

00000256: 01111010 00011000 11000101 00010011 00011000 11011001 00011000 11000101  z.......
               122 uin   24      197 uin   19 uin   24      217 uin   24      197

00000264: 00011000 10000011 00011000 00101101 00011000 11101101 00011000 00011000  ...-....
          uin   24      131 uin   24       45 uin   24      237 uin   24       24

00000272: 00011000 11101010 00011000 10001111 00011000 00101110 00011000 01110100  .......t
          uin   24      234 uin   24      143 uin   24       46 uin   24      116

00000280: 00000111 00011000 01011110 00011000 11110100 00010101 00011000 11001100  ..^.....
          uin    7 uin   24       94 uin   24      244 uin   21 uin   24      204

00000288: 00011000 11001001 00011000 01101101                                      ...m
          uin   24      201 uin   24      109
```

If we follow strictly the webauthn doc, yes, there is some extra bytes. So to check if the problem comes from the library I use, I decide to try another one (yours).

I've write this little script:

```python
import sys
from webauthn.helpers import (parse_attestation_object, base64url_to_bytes)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Expect one argument (filename)")
        exit(1)

    filename = sys.argv[1]
    print("decode", filename)
    file = open(filename, "r")
    base64 = file.read()
    bytes = base64url_to_bytes(base64)
    parse_attestation_object(bytes)
    
    pass
```

The file contains:

```
o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBBRawLfvD1MyjfrwvZRZlmxIhDbnhAYq58TqWkGOOpv2oRQAAAAEvwFefgRNH6rEWu1qNuSAqAIDoL-a94wDk7Mk-ABZEitAPpvKKARpvh_97DPykmb6vgzRMNmC17Kv3KjsoOKDMfYfT-lgpK1NEnP8TrWlzLXUhZJ02XMvF0KD6S04J6umVNyYfL0QJP49P1M9XluD-WP8GFf_FiCg2u-e5mwi-KYZyHBxaasfzLTIg2bNNje4vyaMBY09LUAMnIGdFZDI1NTE5IZggFhj2GFkY-hguFBh1GDoYhBcYUhh0GHoYxRMY2RjFGIMYLRjtGBgY6hiPGC4YdAcYXhj0FRjMGMkYbQ
```

But your lib also raised an error `webauthn.helpers.exceptions.InvalidAuthenticatorDataStructure: Leftover bytes detected while parsing authenticator data`.

So, did we really need to check that ?

I use a Yubikey 5 NFC (firmware 5.4.3) and have also opened an issue on the php lib https://github.com/web-auth/webauthn-framework/issues/436

Discussion:

Patch:
diff --git a/webauthn/helpers/parse_authenticator_data.py b/webauthn/helpers/parse_authenticator_data.py
index c9d12ea..5792def 100644
--- a/webauthn/helpers/parse_authenticator_data.py
+++ b/webauthn/helpers/parse_authenticator_data.py
@@ -57,6 +57,23 @@ def parse_authenticator_data(val: bytes) -> AuthenticatorData:
         credential_id = val[pointer : pointer + credential_id_len]
         pointer += credential_id_len
 
+        """
+        Some authenticators incorrectly compose authData when using EdDSA for their public keys.
+        A CBOR "Map of 3 items" (0xA3) should be "Map of 4 items" (0xA4), and if we manually adjust
+        the single byte there's a good chance the authData can be correctly parsed. Let's try to
+        detect when this happens and gracefully handle it.
+        """
+        # Decodes to `{1: "OKP", 3: -8, -1: "Ed25519"}` (it's missing key -2 a.k.a. COSEKey.X)
+        bad_eddsa_cbor = bytearray.fromhex("a301634f4b500327206745643235353139")
+        # If we find the bytes here then let's fix the bad data
+        if val[pointer : pointer + len(bad_eddsa_cbor)] == bad_eddsa_cbor:
+            # Make a mutable copy of the bytes...
+            _val = bytearray(val)
+            # ...Fix the bad byte...
+            _val[pointer] = 0xA4
+            # ...Then replace `val` with the fixed bytes
+            val = bytes(_val)
+
         # Load the next CBOR-encoded value
         credential_public_key = cbor2.loads(val[pointer:])
         credential_public_key_bytes = cbor2.dumps(credential_public_key)


Test patch:
diff --git a/tests/test_parse_authenticator_data.py b/tests/test_parse_authenticator_data.py
index c6e284c..f4e3d0d 100644
--- a/tests/test_parse_authenticator_data.py
+++ b/tests/test_parse_authenticator_data.py
@@ -182,3 +182,29 @@ class TestWebAuthnParseAuthenticatorData(TestCase):
 
         assert output.flags.be is True
         assert output.flags.be is True
+
+    def test_parses_bad_eddsa_auth_data(self) -> None:
+        """
+        Help out particular YubiKeys that incorrectly CBOR-encode authData when they use Ed25519
+        for their public key.
+
+        See https://github.com/duo-labs/py_webauthn/issues/160
+        """
+        auth_data = bytearray.fromhex(
+            "16B02DFBC3D4CCA37EBC2F6516659B12210DB9E1018AB9F13A9690638EA6FDA845000000012FC0579F811347EAB116BB5A8DB9202A0080E82FE6BDE300E4ECC93E0016448AD00FA6F28A011A6F87FF7B0CFCA499BEAF83344C3660B5ECABF72A3B2838A0CC7D87D3FA58292B53449CFF13AD69732D7521649D365CCBC5D0A0FA4B4E09EAE99537261F2F44093F8F4FD4CF5796E0FE58FF0615FFC5882836BBE7B99B08BE2986721C1C5A6AC7F32D3220D9B34D8DEE2FC9A301634F4B5003272067456432353531392198201618F6185918FA182E141875183A18841718521874187A18C51318D918C51883182D18ED181818EA188F182E187407185E18F41518CC18C9186D"
+        )
+
+        output = parse_authenticator_data(auth_data)
+
+        cred_data = output.attested_credential_data
+        self.assertIsNotNone(cred_data)
+        assert cred_data  # Make mypy happy
+
+        self.assertEqual(
+            cred_data.credential_id.hex(),
+            "e82fe6bde300e4ecc93e0016448ad00fa6f28a011a6f87ff7b0cfca499beaf83344c3660b5ecabf72a3b2838a0cc7d87d3fa58292b53449cff13ad69732d7521649d365ccbc5d0a0fa4b4e09eae99537261f2f44093f8f4fd4cf5796e0fe58ff0615ffc5882836bbe7b99b08be2986721c1c5a6ac7f32d3220d9b34d8dee2fc9"
+        )
+        self.assertEqual(
+            cred_data.credential_public_key.hex(),
+            "a401634f4b5003272067456432353531392198201618f6185918fa182e141875183a18841718521874187a18c51318d918c51883182d18ed181818ea188f182e187407185e18f41518cc18c9186d",
+        )