5

With the eminent demise of the u2f api, I'm trying to move to WebAuthn APIs using the AppId extension to support security keys previously registered with U2F. As best I can tell from reading the docs I think I am doing it correctly, however, when attempting to authenticate I am prompted by my browser to tap my key, and my key is blinking, but upon tapping it I get the error "You're using a security key that's not registered with this website". In comparing the existing u2f authentication request I'm using the same appid and key handle.

Example U2F sign request:

{
  "version": "U2F_V2",
  "challenge": "zSeDYPUjDVbLQ9HDle3g2QYrHEdG5vGBwAhzqdm_PAY",
  "appId": "https:\/\/subdomain.domain.net\/app-id.json",
  "keyHandle": "H-tBDjS1Fgr19cprKYUnZ9cDSE2AiX_Ld1kdPR2ruhIUbYr7jP3dflxkjZmfvqxkg5q84eXBr3ium3ETJ61Fww"
}

And here is my server response that is handed to the webauthn js library:

{
  "publicKey": {
    "challenge": "iygioh7vECe9OCQ5K0IBa0XeTD5hxX+aOBGimrJAntg=",
    "timeout": 60000,
    "rpId": "domain.net",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "SC10QkRqUzFGZ3IxOWNwcktZVW5aOWNEU0UyQWlYX0xkMWtkUFIycnVoSVViWXI3alAzZGZseGtqWm1mdnF4a2c1cTg0ZVhCcjNpdW0zRVRKNjFGd3c="
      }
    ],
    "userVerification": "discouraged",
    "extensions": {
      "appid": "https://subdomain.domain.net/app-id.json"
    }
  }
}

The base64 url decoded version of that credential id is

H-tBDjS1Fgr19cprKYUnZ9cDSE2AiX_Ld1kdPR2ruhIUbYr7jP3dflxkjZmfvqxkg5q84eXBr3ium3ETJ61Fww

I noticed in our U2F version that the appid url is escaped for some reason, so I've tried the same thing in WebAuthn version but that did not make a difference.

I've also tried the challenge and credential id without the trailing padding (=), but that also did not help.

The JS client we're using to interact with the webauthn API appears to be decoding the challenge and credential id into binary array buffers. Here is a console log of the object sent as the publicKey to the navigator.credential.get API:

{
  "challenge": { "0": 83, "1": 195, "2": 81, "3": 9, "4": 32, "5": 53, "6": 59, "7": 244, "8": 34, "9": 113, "10": 189, "11": 177, "12": 61, "13": 184, "14": 170, "15": 86, "16": 43, "17": 206, "18": 102, "19": 145, "20": 218, "21": 136, "22": 137, "23": 18, "24": 14, "25": 176, "26": 210, "27": 54, "28": 201, "29": 57, "30": 156, "31": 21},
  "timeout": 60000,
  "rpId": "domain.net",
  "allowCredentials": [
    {
      "type": "public-key",
      "id": { "0": 72, "1": 45, "2": 116, "3": 66, "4": 68, "5": 106, "6": 83, "7": 49, "8": 70, "9": 103, "10": 114, "11": 49, "12": 57, "13": 99, "14": 112, "15": 114, "16": 75, "17": 89, "18": 85, "19": 110, "20": 90, "21": 57, "22": 99, "23": 68, "24": 83, "25": 69, "26": 50, "27": 65, "28": 105, "29": 88, "30": 95, "31": 76, "32": 100, "33": 49, "34": 107, "35": 100, "36": 80, "37": 82, "38": 50, "39": 114, "40": 117, "41": 104, "42": 73, "43": 85, "44": 98, "45": 89, "46": 114, "47": 55, "48": 106, "49": 80, "50": 51, "51": 100, "52": 102, "53": 108, "54": 120, "55": 107, "56": 106, "57": 90, "58": 109, "59": 102, "60": 118, "61": 113, "62": 120, "63": 107, "64": 103, "65": 53, "66": 113, "67": 56, "68": 52, "69": 101, "70": 88, "71": 66, "72": 114, "73": 51, "74": 105, "75": 117, "76": 109, "77": 51, "78": 69, "79": 84, "80": 74, "81": 54, "82": 49, "83": 70, "84": 119, "85": 119}
    }
  ],
  "userVerification": "discouraged",
  "extensions": {
    "appid": "https://subdomain.domain.net/app-id.json"
  }
}

What am I doing wrong? We have thousands of users using yubikeys with u2f and with such a short time frame for a migration we really need this backwards compatibility option to work.

Solution: @IAmKale was right that this was a double encoding issue, or perhaps it was a triple encoding issue. Anyway, the solution was the H-tBD... string needed to be raw url base64 decoded before sending back to browser. Thank you @IAmKale for the suggestions that led to figuring this out!

Phillip
  • 643
  • 6
  • 15

1 Answers1

3

Everything about the options you pass to navigator.credentials.get() looks correct, including how you're specifying the "appid" extension. I believe the issue is that you're double-encoding your U2F credential's credential ID. Try passing the original "keyHandle" in the options instead (you can use it as-is because it's already compatible with base64url encoding):

{
  "publicKey": {
    "challenge": "iygioh7vECe9OCQ5K0IBa0XeTD5hxX+aOBGimrJAntg=",
    "timeout": 60000,
    "rpId": "domain.net",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "H-tBDjS1Fgr19cprKYUnZ9cDSE2AiX_Ld1kdPR2ruhIUbYr7jP3dflxkjZmfvqxkg5q84eXBr3ium3ETJ61Fww"
      }
    ],
    "userVerification": "discouraged",
    "extensions": {
      "appid": "https://subdomain.domain.net/app-id.json"
    }
  }
}

I'm almost certain that's all you need to do to get WebAuthn to work with your old U2F credentials. If that still doesn't work, try it again with "appid" set to the escaped "appId" URL from the U2F sign request you posted.

IAmKale
  • 3,146
  • 1
  • 25
  • 46
  • Thanks for the quick reply @IAmKale! I'm using the [duo-labs/webauthn](https://github.com/duo-labs/webauthn) package server-side. I set the credential id to the raw non-encoded value. So the encoding must be done by the library. I just tested replacing the value in the json sent to the browser with the raw key handle `H-tBD...` but that produces a different error in in the browser: `DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.` So it seems the JS library we're using already decodes the value before sending to the webauthn api. – Phillip Nov 16 '21 at 19:36
  • I don't recall anyone adding appid extension support to that library, unless you added it yourself? – aseigler Nov 16 '21 at 20:22
  • @Phillip Aha, that's part of the problem. py_webauthn encodes `bytes()` to Base64URL, which you can't easily decode using `window.atob` in the browser. I'd recommend leveraging a front end helper library, like @simplewebauthn/browser or webauthn-json, to coordinate decoding base64url to `ArrayBuffer`'s. – IAmKale Nov 16 '21 at 20:44
  • Sorry, you said webauthn, not py_webauthn. Maybe it's a similar story, I'm not as familiar with the Go library (I maintain the latter). – IAmKale Nov 16 '21 at 20:46
  • @aseigler, there isn't explicit `appid` extension support, but there is generic support for extensions. Essentially I have: ```extensions := protocol.AuthenticationExtensions{"appid": "https://subdomain.domain.net/app-id.json"}``` and then in the call to the `BeginLogin` method I pass in `webauthn.WithAssertionExtensions(extensions)` – Phillip Nov 16 '21 at 22:01
  • Thanks @IAmKale, I'm currently using [@webauthn/client](https://www.npmjs.com/package/@webauthn/client) in the browser to handle the encoding/decoding, and I believe it is working based on the console log including the byte array of the keyHandle, which is exactly the same as what I set the credential ID to in the duo-labs/webauthn client server-side. I'll check out and hopefully try the ones you referenced though too in case there is a bug in the one I'm using now. Any chance you can share this question with your colleagues who worked on the Go library too? – Phillip Nov 16 '21 at 22:08
  • @Phillip Try decoding it with https://github.com/herrjemand/Base64URL-ArrayBuffer – Ackermann Yuriy Nov 18 '21 at 04:20