Yubico / python-fido2

Provides library functionality for FIDO 2.0, including communication with a device over USB.
BSD 2-Clause "Simplified" License
432 stars 109 forks source link

Implement the FIDO AppID extension #137

Closed gcochard closed 2 years ago

gcochard commented 2 years ago

Per #136, I am reopening this against the next branch. The original patch information stands and is stated below. Regarding the use case I am working towards, we have a CLI tool that does an SSO login using Duo for the second factor. I had some trouble with Duo Security's challenges as passed by their web service and the only way I could make it work was to change the origin in the ClientData. Changing the rp_id returns an invalid CBOR error, and this is the only way it validates the signature with duo. It's entirely possible I'm missing something else but I couldn't get a U2FClient to work either.

Here's an excerpt of the request(s) and response(s) from duo using the browser (yk, solo) and using alohomora before this patch (solohomora). The solohomora one was not passing validation with Duo.


This patch implements the appid extension behavior as described in section 10.1 of the webauthn specification. See https://www.w3.org/TR/webauthn/#sctn-appid-extension for details. Namely, this will use the appid as presented in the extensions.appid field for the ClientData.origin value, rather than the rpId. The justification for this change is due to compatibility issues with existing sites which use backward-compatible challenges even if the authenticator device was never registered as a U2F key. This change has been tested against Duo security for security keys enrolled with and without U2F support.

This also modifies the ClientDataJSON generation algorithm to use separators ':', and ',', without whitespace, to satisfy the requirements of section 5.8.1.2 of the spec (Limited Verification Algorithm), which states that the expected JSON structure should not have spaces. See https://www.w3.org/TR/webauthn/#clientdatajson-verification for further reading. In making this change, I have also used an OrderedDict for the call to ClientData.build as that guarantees ordering even in versions of python where this was not intially guaranteed for the standard dict.

gcochard commented 2 years ago

TL:DR on the issue I'm facing: The returned clientDataJSON from the browser includes the appid as the origin, rather than the rp_id. The relevant request:

{
  "allowCredentials":[{
    "transports":["usb","nfc","ble"],
    "type":"public-key",
    "id":"dipB0Q2TgTSpOINIsI9uaesA4ZrI1nGoeKc3Dx-VOvAJ1knOY46MzjY3da14KcTzLPzlIJF9p9gtqr2t6TfWeQ"
  }],
  "challenge":"gsHAM4df1mxnPcIaFcSnCy9MMMjtirjB",
  "rpId":"duosecurity.com",
  "timeout":60000,
  "sessionId":"cxWu9ywXNfO39HfJTG-B5TSg5_qQgS3Ef6o2ynfsWJY",
  "userVerification":"discouraged",
  "extensions":{
    "appid":"https://api-69267918.duosecurity.com"
  }
}

And the relevant response's clientDataJSON after decoding and pretty-printing:

{
  "type":"webauthn.get",
  "challenge":"gsHAM4df1mxnPcIaFcSnCy9MMMjtirjB",
  "origin":"https://api-69267918.duosecurity.com",
  "crossOrigin":false
}

Note that this is in happening in Chrome, so I would assume that Chrome also modifying the origin in the same way. It's possible that it's sending a CBOR request with the rpId as the origin, getting a NO_CREDENTIALS error, then retrying after modifying the request. I haven't done that here, but I could implement it in case this breaks some other workflow.

Here's another request/response from alohomora without this patch:

2022-04-15 10:31:59,764 DEBUG alohomora.req trying device <fido2.client.Fido2Client object at 0x108d07c70> with req 
{
  'allowCredentials': [{
    'transports': ['usb', 'nfc', 'ble'], 
    'type': 'public-key', 
    'id': b'v*A\xd1\r\x93\x814\xa98\x83H\xb0\x8fni\xeb\x00\xe1\x9a\xc8\xd6q\xa8x\xa77\x0f\x1f\x95:\xf0\t\xd6I\xcec\x8e\x8c\xce67u\xadx)\xc4\xf3,\xfc\xe5 \x91}\xa7\xd8-\xaa\xbd\xad\xe97\xd6y'
  }], 
  'challenge': b')\x00\xc8;\x8aK\xeb\x8e\xf7\x9e\x90\t\xd6I\x85y9\x05\xf2\x03\x91T\xb7\xd4', 
  'rpId': 'duosecurity.com', 
  'timeout': 60000, 
  'userVerification': 'discouraged', 
  'extensions': {
    'appid': 'https://api-69267918.duosecurity.com'
  }
}
2022-04-15 10:31:59,764 DEBUG alohomora.req rp: duosecurity.com, origin: duosecurity.com

2022-04-15 10:32:00,778 DEBUG alohomora.req wa_resp: AuthenticatorAssertionResponse(client_data={"type": "webauthn.get", "origin": "duosecurity.com", "challenge": "KQDIO4pL6473npAJ1kmFeTkF8gORVLfU", "clientExtensions": {}}, authenticator_data=AuthenticatorData(rp_id_hash=b'\xe4j\xbb\xc0\xc1\x02&\xa4\xa4\xd6\x1c\xd9\xaa\xc0\xdd$\xfecS\xc5{\xf4t\xed\xb8\xc2)\xbb\xaf\xe5k\x05', flags=1, counter=3452, credential_data=None, extensions=None), signature=b'0E\x02 -\x7f\x19-VYN\xe3r7uM\xe7\xc0\xc5pr\xa8\x0ep\x1d9-\xf7\r\x8fA\xa0x\x88Z\xb0\x02!\x00\xf0`\xdfMX\x06d\xd8J\xa56\xbe\x19\x1d\x174\xc9\x8a\t\x84\x19\xb2\xeeEl\xc0M\x02\xaat\x190', user_handle=None, credential_id=b'v*A\xd1\r\x93\x814\xa98\x83H\xb0\x8fni\xeb\x00\xe1\x9a\xc8\xd6q\xa8x\xa77\x0f\x1f\x95:\xf0\t\xd6I\xcec\x8e\x8c\xce67u\xadx)\xc4\xf3,\xfc\xe5 \x91}\xa7\xd8-\xaa\xbd\xad\xe97\xd6y', extension_results={})
2022-04-15 10:32:00,779 DEBUG alohomora.req wa_resp.client_data: {"type": "webauthn.get", "origin": "duosecurity.com", "challenge": "KQDIO4pL6473npAJ1kmFeTkF8gORVLfU", "clientExtensions": {}}
...snip...
2022-04-15 10:32:00,779 DEBUG alohomora.req 
{
  'signature': '304502202d7f192d56594ee37237754de7c0c57072a80e701d392df70d8f41a078885ab0022100f060df4d580664d84aa536be191d1734c98a098419b2ee456cc04d02aa741930', 
  'authenticatorData': '5Gq7wMECJqSk1hzZqsDdJP5jU8V79HTtuMIpu6_lawUBAAANfA==', 
  'clientDataJSON': 'eyJ0eXBlIjogIndlYmF1dGhuLmdldCIsICJvcmlnaW4iOiAiZHVvc2VjdXJpdHkuY29tIiwgImNoYWxsZW5nZSI6ICJLUURJTzRwTDY0NzNucEFKMWttRmVUa0Y4Z09SVkxmVSIsICJjbGllbnRFeHRlbnNpb25zIjoge319', 
  'extensionResults': {'appid': False}, 
  'rawId': 'dipB0Q2TgTSpOINIsI9uaesA4ZrI1nGoeKc3Dx-VOvAJ1knOY46MzjY3da14KcTzLPzlIJF9p9gtqr2t6TfWeQ', 
  'id': 'dipB0Q2TgTSpOINIsI9uaesA4ZrI1nGoeKc3Dx-VOvAJ1knOY46MzjY3da14KcTzLPzlIJF9p9gtqr2t6TfWeQ', 
  'sessionId': 'R2by5n9w_VO4tlK2KumrFHaIeYikTdjkjiwIugmByeI', 
  'type': 'public-key'
}
...snip...
2022-04-15 10:32:01,066 INFO  alohomora.req 
{
  'stat': 'OK', 'response': {
    'status': 'Login request denied.', 'status_code': 'deny', 'result': 'FAILURE', 'reason': 'Error'
  }
}

Given that the browser is returning a ClientDataJSON with the type set to webauthn.get I'm very certain that it's not falling back to U2F here. Additionally, the yubikey was re-registered as a WebAuthN authenticator after duo dropped support for U2F authenticators. I have also tested with a new key that was only WebAuthN from the beginning and I see the same behavior.

dainnilsson commented 2 years ago

Thanks for the additional information!

The origin is provided by the Client and for a browser request corresponds to the location of the page making the request, in this case https://api-69267918.duosecurity.com. For the Fido2Client in python-fido2, since it doesn't implement a browser, this value is passed in to the constructor by the caller. Note that using the RP ID as origin as in your example is invalid, as the format isn't valid (eg. an RP ID doesn't contain "https://" while an origin does).

If you just create the instance as: Fido2Client(device, "https://api-69267918.duosecurity.com"), doesn't this achieve your desired result?

gcochard commented 2 years ago

If you just create the instance as: Fido2Client(device, "https://api-69267918.duosecurity.com"), doesn't this achieve your desired result?

Huzzuh! It worked this time. I believe I was having unrelated encoding issues that may have masked the issue during my debugging. Thanks for pointing out my mistake! What's the ETA for the 1.0.0 release? I'd like to use it instead of the 0.9.x version.

dainnilsson commented 2 years ago

Great, glad you got it working! No exact ETA for 1.0 yet, but I would expect it in about 1-2 months, with an RC release out sooner. Oh, and thanks for pointing out https://www.w3.org/TR/webauthn/#clientdatajson-verification. We'll probably add the changes to be compliant with that as well!