githubwebauthnfido

Why does GitHub issue GetAssertion requests when trying to register a new security key


I'm working on a fido2 authenticator library and found some odd behavior regarding the security key registration process on Github:

I've previously registered two keys and now I want to add another one.

After the getInfo API call, Github seams to make a authenticatorGetAssertion call for every key already registered. This is probably their way to check if the key has already been registered or not (just a guess). Both GetAssertion requests fail with `CTAP2_ERR_NO_CREDENTIALS`.

YubiKey 5:

FIDODebug[11:35:56] -> {1: "packed", 2: h'3AEB002460381C6F258E8395D3026F571F0D9A76488DCD837639B13AED3165604100000000FA2B99DC9E3942578F924A30D23C41180040EDA043B6EDD805976A4E2EFFCC84381FFC601E5301C9DB1D7C887035176EC83889AC86398FDEA2A5F1A57C11AF4AE0484ED51C5C2F121B2454D6F9FAD6E187FBA5010203262001215820DBCDD8CC3C667D6776F25181BB950063B3CF99AE58D014ADD021E4F1D7E040EE225820D0E6ED4CCB2742BF0C1287CA451D94C779CED73B415BA2ACC88689D79B5444F4', 3: {"alg": -7, "sig": h'3045022100A886B4C3E3E4EC63E602C4466B614DEDA02CE7815F67DF2F15F9D45F657857A902203881D5080A3107DA0E17DA9CD9841EC55D5A470CCB01C379A416230771542F4E', "x5c": [h'308202BC308201A4A003020102020403ADF012300D06092A864886F70D01010B0500302E312C302A0603550403132359756269636F2055324620526F6F742043412053657269616C203435373230303633313020170D3134303830313030303030305A180F32303530303930343030303030305A306D310B300906035504061302534531123010060355040A0C0959756269636F20414231223020060355040B0C1941757468656E74696361746F72204174746573746174696F6E3126302406035504030C1D59756269636F205532462045452053657269616C2036313733303833343059301306072A8648CE3D020106082A8648CE3D03010703420004199E879C162DB7DC39EE4A42A04616A5B309FECA092F76BE0948F96D6E95CAE4CC65CD54A059CFBDC7C9B31B2B1D6C184479C2C061F418AA954B596A2C1CFA17A36C306A302206092B0601040182C40A020415312E332E362E312E342E312E34313438322E312E373013060B2B0601040182E51C0201010404030204303021060B2B0601040182E51C01010404120410FA2B99DC9E3942578F924A30D23C4118300C0603551D130101FF04023000300D06092A864886F70D01010B0500038201010028EBB367FED1D8F0E289EBCA9FF6D80757C60E9AE57CB1728C9B1C38CABBBD84D9237DA831AC21949F0F2DFC0C316BFDB175B36E63A22BBB580EADCA5280D079840E5A1E2572625A3BFB876033DBFB22A969C938B89CE171359400A1252D9702A91293D54519E960DD22CE8A27EB05EB7E79B750C002FED9016B711EC9AD74501BD914CBBE8ED9571281B74F44EB077CE61ECB06AB85A97255267EE8E3982BF43F0CB21A382D235EB9E4CE6DB298C405425040232B2B61E10CD70C6215BC03B7E94071B70E12D1C47F96655A2EF99D4CE55A7F1B4B1FF914EE136D9E612047148864698880443116653889B86486D9C9C9FFBC9385453569B345744B8CA0B437']}}

FIDODebug[11:35:54] <- 1 {1: h'E782C8D2E0AD4ADDF8271928D8B37746EDE1B30AD92DA8F9DEB96E8280B4AA2F', 2: {"id": "github.com", "name": "GitHub"}, 3: {"id": h'C0BBBCEA3161953E45CDB3C75D28C38C3B08D5BDEA6458CE83E62E0E1B81F1FE0B3D4FFD2D9ADCF935409365AB50DDFB122139A301C0BA081B987895EE601C77', "name": "r4gus", "displayName": "David Sugar"}, 4: [{"alg": -7, "type": "public-key"}, {"alg": -257, "type": "public-key"}]}

FIDODebug[11:35:54] -> (CTAP2 error code 46)

FIDODebug[11:35:54] <- 2 {1: "github.com", 2: h'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', 3: [{"id": h'A321BA23BFBACD2362664B5A44CC5399BC805BC66D47741D967890321F3C486BA84CA5D586B5AA5A63F6EBF7B12429756835C3930D3460678196C70EF88AD4C3', "type": "public-key"}], 5: {"up": false}}

FIDODebug[11:35:54] -> (CTAP2 error code 46)

FIDODebug[11:35:54] <- 2 {1: "github.com", 2: h'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', 3: [{"id": h'182F151E610E171BBD5F7579E7908F0EBFE3AF8D7217BF6812234AA7021F4304656CD7E128C28B8509C0636F57EC69FA867884BAD906A2E53552F6DBE6ED1120', "type": "public-key"}], 5: {"up": false}}

FIDODebug[11:35:54] -> (CTAP2 error code 46)

FIDODebug[11:35:54] <- 2 {1: "https://github.com/u2f/trusted_facets", 2: h'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', 3: [{"id": h'A321BA23BFBACD2362664B5A44CC5399BC805BC66D47741D967890321F3C486BA84CA5D586B5AA5A63F6EBF7B12429756835C3930D3460678196C70EF88AD4C3', "type": "public-key"}], 5: {"up": false}}

FIDODebug[11:35:54] -> (CTAP2 error code 46)

FIDODebug[11:35:54] <- 2 {1: "https://github.com/u2f/trusted_facets", 2: h'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', 3: [{"id": h'182F151E610E171BBD5F7579E7908F0EBFE3AF8D7217BF6812234AA7021F4304656CD7E128C28B8509C0636F57EC69FA867884BAD906A2E53552F6DBE6ED1120', "type": "public-key"}], 5: {"up": false}}

FIDODebug[11:35:54] The device supports the CTAP2 protocol.

FIDODebug[11:35:54] -> {1: ["U2F_V2", "FIDO_2_0"], 2: ["hmac-secret"], 3: h'FA2B99DC9E3942578F924A30D23C4118', 4: {"rk": true, "up": true, "plat": false, "clientPin": false}, 5: 1200, 6: [1]}

FIDODebug[11:35:53] Discovery session started.

FIDODebug[11:35:53] Sending CTAP2 AuthenticatorGetInfo request to authenticator.

FIDODebug[11:35:53] BLE adapter address 6C:6A:77:A0:F9:FF

FIDODebug[11:35:53] Android accessory discovery started

FIDODebug[11:35:53] Found 0 caBLEv2 devices

The problem I see is that this doesn't reflect the [spec](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorMakeCredential) as far as I understand it.

This leads to an error if I enforce user verification, even when setting the alwaysUv flag, because the authenticatorGetAssertion request omits the pinUvAuthParam parameter.

custom:

FIDODebug[11:59:22] -> (CTAP2 error code 49)

FIDODebug[11:59:18] <- 2 {1: "https://github.com/u2f/trusted_facets", 2: h'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', 3: [{"id": h'182F151E610E171BBD5F7579E7908F0EBFE3AF8D7217BF6812234AA7021F4304656CD7E128C28B8509C0636F57EC69FA867884BAD906A2E53552F6DBE6ED1120', "type": "public-key"}], 5: {"up": false}}

FIDODebug[11:59:18] The device supports the CTAP2 protocol.

FIDODebug[11:59:18] -> {1: ["FIDO_2_0", "FIDO_2_1"], 3: h'6F158274AAB6443D9BCF8A3F69297C88', 4: {"rk": false, "up": true, "uv": false, "plat": true, "alwaysUv": true, "clientPin": true, "pinUvAuthToken": true, "makeCredUvNotRqd": false, "noMcGaPermissionsWithClientPin": false}, 6: [2], 9: ["usb"], 10: [{"alg": -7, "type": "public-key"}], 14: 51966}

After receiving the first CTAP2_ERR_PIN_INVALID no further assertion requests are issued and Github will display Security key registration failed.

In comparison, the impl works for example with Codeberg.

For both authenticatorMakeCredential and authenticatorGetAssertion the spec says:

1. If authenticator supports either pinUvAuthToken or clientPin features and the platform sends a zero length pinUvAuthParam:

    1. Request evidence of user interaction in an authenticator-specific way (e.g., flash the LED light).

    2. If the user declines permission, or the operation times out, then end the operation by returning CTAP2_ERR_OPERATION_DENIED.

    3. If evidence of user interaction is provided in this step then return either CTAP2_ERR_PIN_NOT_SET if PIN is not set or CTAP2_ERR_PIN_INVALID if PIN has been set.

So a implementation that adheres to the spec will return CTAP2_ERR_PIN_INVALID before we can check if the given credential exists or not.

Does anyone know if this behavior is common with other implementations and/ or reflects an older ctap spec?

I would expect the following behavior:

FIDODebug[12:23:46] -> {1: "packed", 2: h'54B06FBA8D07757301B093071D8CDF891BCDE3D7F13612FEB3FB1FD86D21FA9345000000006F158274AAB6443D9BCF8A3F69297C880040F95BEBC0A6EE527B85ED1FBA12B7C8E6960BB1DADC5FD7DF768BA5684E449A8E3DBE4782333F37168C83FAFF3B2147F376BBAB17E40FB3DD8A8A35061B932C3EA60102032620012158203F4AAB317DD060BF7399F1D19DC43C423038F61604C438D2DD84A5FB6FA7296E2258202CC58E3AB2AF86A225A75DF3F3D43D496BC1C2F08E4BF0D23E85233C4094321C2358205BA74E18264AEA3E049099B3CE49352E916400A495BB0F3DA34E2E2B66C73B1F', 3: {"alg": "Es256", "sig": h'304402206275A1022E010833818B9F3A52A629D3FFE0C721DEDF3882CD4EDFBBF0A6EF190220607FD3C4423108DC6BC1D91A4A7A1018F3094B0DD221B4379F141866F3F07089'}}

FIDODebug[12:23:41] <- 1 {1: h'F9B272A975C33E3E6D01DA61A020462BD7A4F5895F95D2F90259081778DCEC55', 2: {"id": "codeberg.org", "name": "Codeberg.org"}, 3: {"id": h'B4A7080000000000', "name": "r4gus", "displayName": "David Sugar"}, 4: [{"alg": -7, "type": "public-key"}, {"alg": -35, "type": "public-key"}, {"alg": -36, "type": "public-key"}, {"alg": -257, "type": "public-key"}, {"alg": -258, "type": "public-key"}, {"alg": -259, "type": "public-key"}, {"alg": -37, "type": "public-key"}, {"alg": -38, "type": "public-key"}, {"alg": -39, "type": "public-key"}, {"alg": -8, "type": "public-key"}], 8: h'52F1621659FC1E4918F7E9D1B5F36DCA0E932E1F0EFAE6E7CE9A1F861A865D35', 9: 2}

FIDODebug[12:23:41] -> {2: h'15CFD3213A708811FECB288A782720D1E5BBA1E1C9486FC4ABE130522B7875A2935834B42F2161A9ABBE1DEDCB076A0E'}

FIDODebug[12:23:41] <- 6 {1: 2, 2: 9, 3: {1: 2, 3: -25, -1: 1, -2: h'A31362152EC002890873DDC70CC546D86C2D6925296F3679E3A1D5A798E89361', -3: h'1D557E224D36218759EF5B6F25525FF31A907F7AB97BF634A6A22EF03A818616'}, 6: h'0174E6862BCF871ABD26F9AC56BA05804A433D06B39A7C13A965B07DE6D52D78', 9: 3, 10: "codeberg.org"}

FIDODebug[12:23:41] -> {1: {1: 2, 3: -25, -1: 1, -2: h'B0972D6924BCEE5A6DAD51254DB0BE9A7D99D51515B9066A7D40EA4CA120DBA6', -3: h'37F40038A01A1DAAB56216C8D26E79E7113CB7E3430ADC077AF58FDAC767996E'}}

FIDODebug[12:23:41] <- 6 {1: 2, 2: 2}

FIDODebug[12:23:39] -> {3: 8, 4: false}

FIDODebug[12:23:39] <- 6 {1: 2, 2: 1}

FIDODebug[12:23:39] The device supports the CTAP2 protocol.

FIDODebug[12:23:39] -> {1: ["FIDO_2_0", "FIDO_2_1"], 3: h'6F158274AAB6443D9BCF8A3F69297C88', 4: {"rk": false, "up": true, "uv": false, "plat": true, "alwaysUv": true, "clientPin": true, "pinUvAuthToken": true, "makeCredUvNotRqd": false, "noMcGaPermissionsWithClientPin": false}, 6: [2], 9: ["usb"], 10: [{"alg": -7, "type": "public-key"}], 14: 51966}

Solution

  • Probing the excludeList with getAssertions is done because authenticators have a limit to the maximum message size that they can handle. Thus sending the excludeList (with can be quite large) in a single makeCredential request will fail in some cases and there's no way to split an excludeList in the makeCredential call.

    So the excludeList in CTAP2 is often unused in practice.

    GitHub is setting appIdExclude, so the probe needs to check the AppID first. A pinUvAuthToken cannot be used for this because PUATs become specific to the RP ID that you use them with, and the PUAT is needed for the final makeCredential, thus cannot be used for AppID probing. That should be fine because credentials created with AppIDs must have been created using the old U2F web API and that never supported credProtect, and never worked for alwaysUv authenticators.

    From the spec for getAssertion:

    If the alwaysUv option ID is present and true and the "up" option is present and true then:

    But the up option is not present and true here, and there are no other mentions of alwaysUv in the getAssertion processing steps.

    You quote the section with "If authenticator supports either pinUvAuthToken or clientPin features and the platform sends a zero length pinUvAuthParam:", but that doesn't apply either because the pinUvAuthToken is not zero length, it's omitted. (Those are different things.)