Repository: pypi/warehouse

Issue:
2FA: Enrolling a TouchID sensor as a webauthn security key fails (Chrome, Mac)
<!--
    NOTE: This issue should be for problems with PyPI itself, including:
    * pypi.org
    * test.pypi.org
    * files.pythonhosted.org

    This issue should NOT be for a project installed from PyPI. If you are
    having an issue with a specific package, you should reach out to the
    maintainers of that project directly instead.

    Furthermore, this issue should NOT be for any non-PyPI properties (like
    python.org, docs.python.org, etc.)
-->

**Describe the bug**
I'm trying to enroll a TouchID sensor as a webauthn device. PyPI and Chrome do let me select the sensor, and I do get prompted for a touch, but then PyPI throws an error: "Registration rejected. Error: Self attestation is not permitted.."

**Expected behavior**
I expect to be able to enroll a TouchID sensor. 

**To Reproduce**
- PyPI --> Account Settings
- Click "Add 2FA With Security Key"
- Type a key name, click "Provision Key"
- Chrome prompts to choose between a USB security key and a built-in sensor. Choose "Built-in sensor"
- MacOS prompts to hit the TouchID sensor. Do so.
- Chrome prompts, "Allow this site to see your security key?" Click "Allow"
- PyPI displays an error: "Registration rejected. Error: Self attestation is not permitted.."

**My Platform**
- MacOS 10.14.5
- MacBook Air (2018 edition, with TouchID)
- Chrome "75.0.3770.100 (Official Build) (64-bit)"



Discussion:
I can reproduce this with the same macOS/Chrome versions.
Thanks! I'll take a look today.

Resolving patch:
diff --git a/warehouse/utils/webauthn.py b/warehouse/utils/webauthn.py
--- a/warehouse/utils/webauthn.py
+++ b/warehouse/utils/webauthn.py
@@ -110,8 +110,9 @@ def verify_registration_response(response, challenge, *, rp_id, origin):
     # response's clientData.challenge is encoded twice:
     # first for the entire clientData payload, and then again
     # for the individual challenge.
+    encoded_challenge = _webauthn_b64encode(challenge.encode()).decode()
     response = pywebauthn.WebAuthnRegistrationResponse(
-        rp_id, origin, response, _webauthn_b64encode(challenge.encode()).decode()
+        rp_id, origin, response, encoded_challenge, self_attestation_permitted=True
     )
     try:
         return response.verify()
@@ -129,12 +130,13 @@ def verify_assertion_response(assertion, *, challenge, user, origin, icon_url, r
     """
     webauthn_users = _get_webauthn_users(user, icon_url=icon_url, rp_id=rp_id)
     cred_ids = [cred.credential_id for cred in webauthn_users]
+    encoded_challenge = _webauthn_b64encode(challenge.encode()).decode()
 
     for webauthn_user in webauthn_users:
         response = pywebauthn.WebAuthnAssertionResponse(
             webauthn_user,
             assertion,
-            _webauthn_b64encode(challenge.encode()).decode(),
+            encoded_challenge,
             origin,
             allow_credentials=cred_ids,
         )


Test patch:
diff --git a/tests/unit/utils/test_webauthn.py b/tests/unit/utils/test_webauthn.py
--- a/tests/unit/utils/test_webauthn.py
+++ b/tests/unit/utils/test_webauthn.py
@@ -46,6 +46,7 @@ def test_verify_registration_response(monkeypatch):
             "fake_origin",
             {},
             webauthn._webauthn_b64encode("not_a_real_challenge".encode()).decode(),
+            self_attestation_permitted=True,
         )
     ]
     assert resp == "not a real object"