Repository: duo-labs/py_webauthn

Issue:
Support for Chrome caBLE v2
In chrome one can now use an Android phone as security key (see https://blog.millerti.me/2021/06/18/previewing-chromes-cable-v2-support-for-webauthn/ - the functionality has now made its way into the regular release of Chrome). 

This methods returns the transport method name 'cable'. For this to work properly with py_webauthn, `CABLE = 'cable'` would have to be added to the class AuthenticatorTransport in structs.py

Discussion:

Patch:
diff --git a/webauthn/helpers/structs.py b/webauthn/helpers/structs.py
index 74274ab..16800c4 100644
--- a/webauthn/helpers/structs.py
+++ b/webauthn/helpers/structs.py
@@ -74,6 +74,7 @@ class AuthenticatorTransport(str, Enum):
         `NFC`: Near Field Communication
         `BLE`: Bluetooth Low Energy
         `INTERNAL`: Direct connection (read: a platform authenticator)
+        `CABLE`: Cloud Assisted Bluetooth Low Energy
 
     https://www.w3.org/TR/webauthn-2/#enum-transport
     """
@@ -82,6 +83,7 @@ class AuthenticatorTransport(str, Enum):
     NFC = "nfc"
     BLE = "ble"
     INTERNAL = "internal"
+    CABLE = "cable"
 
 
 class AuthenticatorAttachment(str, Enum):
@@ -221,7 +223,7 @@ class PublicKeyCredentialRpEntity(WebAuthnBaseModel):
 
     Attributes:
         `name`: A user-readable name for the Relying Party
-        `id`: A unique, constant value assigned to the Relying Party. Authenticators use this value to associate a credential with a particular Relying Party user
+        (optional) `id`: A unique, constant value assigned to the Relying Party. Authenticators use this value to associate a credential with a particular Relying Party user
 
     https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrpentity
     """
@@ -373,7 +375,7 @@ class RegistrationCredential(WebAuthnBaseModel):
         `raw_id`: A byte sequence representing the credential's unique identifier
         `response`: The authenticator's attesation data
         `type`: The literal string `"public-key"`
-        `transports`: The authenticator's supported methods of communication with a client/browser
+        (optional) `transports`: The authenticator's supported methods of communication with a client/browser
 
     https://www.w3.org/TR/webauthn-2/#publickeycredential
     """


Test patch:
diff --git a/tests/test_verify_registration_response.py b/tests/test_verify_registration_response.py
index 60b7077..d9a0115 100644
--- a/tests/test_verify_registration_response.py
+++ b/tests/test_verify_registration_response.py
@@ -166,3 +166,35 @@ class TestVerifyRegistrationResponse(TestCase):
                     AttestationFormat.PACKED: [globalsign_r2]
                 },
             )
+
+    def test_verifies_registration_over_cable(self) -> None:
+        credential = RegistrationCredential.parse_raw(
+            """{
+            "id": "9y1xA8Tmg1FEmT-c7_fvWZ_uoTuoih3OvR45_oAK-cwHWhAbXrl2q62iLVTjiyEZ7O7n-CROOY494k7Q3xrs_w",
+            "rawId": "9y1xA8Tmg1FEmT-c7_fvWZ_uoTuoih3OvR45_oAK-cwHWhAbXrl2q62iLVTjiyEZ7O7n-CROOY494k7Q3xrs_w",
+            "response": {
+                "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAFwAAAAAAAAAAAAAAAAAAAAAAQPctcQPE5oNRRJk_nO_371mf7qE7qIodzr0eOf6ACvnMB1oQG165dqutoi1U44shGezu5_gkTjmOPeJO0N8a7P-lAQIDJiABIVggSFbUJF-42Ug3pdM8rDRFu_N5oiVEysPDB6n66r_7dZAiWCDUVnB39FlGypL-qAoIO9xWHtJygo2jfDmHl-_eKFRLDA",
+                "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVHdON240V1R5R0tMYzRaWS1xR3NGcUtuSE00bmdscXN5VjBJQ0psTjJUTzlYaVJ5RnRya2FEd1V2c3FsLWdrTEpYUDZmbkYxTWxyWjUzTW00UjdDdnciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
+            },
+            "type": "public-key",
+            "clientExtensionResults": {},
+            "transports": [
+                "cable"
+            ]
+        }"""
+        )
+
+        challenge = base64url_to_bytes(
+            "TwN7n4WTyGKLc4ZY-qGsFqKnHM4nglqsyV0ICJlN2TO9XiRyFtrkaDwUvsql-gkLJXP6fnF1MlrZ53Mm4R7Cvw"
+        )
+        rp_id = "localhost"
+        expected_origin = "http://localhost:5000"
+
+        verification = verify_registration_response(
+            credential=credential,
+            expected_challenge=challenge,
+            expected_origin=expected_origin,
+            expected_rp_id=rp_id,
+        )
+
+        assert verification.fmt == AttestationFormat.NONE