There are many sources that say FIDO2/CTAP2 is backward compatible with U2F:
...all previously certified FIDO U2F Security Keys and YubiKeys will continue to work as a second-factor authentication login experience with web browsers and online services supporting WebAuthn. - Yubico
But after looking at the specifications, I'm having trouble seeing how that actually works in practice. Specifically, it seems like there is a mismatch between FIDO2's relying party identifier and U2F's application identity.
In U2F, the application identity is a URL, like https://example.com
. SHA-256 of the application identity is called the application parameter. The application parameter is what is actually sent to the authenticator during registration and authentication.
In FIDO2, the equivalent seems to be the relying party identifier, which is defined to be a domain name, like example.com
.
The relying party identifier and the application identity serve the same purpose in both FIDO2/CTAP2 and U2F. However, CTAP2 authenticators get the relying party identifier directly as an UTF8 string, whereas U2F authenticators only get a SHA-256 hash of the application identity (the application parameter).
The FIDO documentation for CTAP describes how CTAP2 maps onto CTAP1/U2F. In it, they simply treat the relying party identifier directly as the application identity:
Let rpIdHash be a byte array of size 32 initialized with SHA-256 hash of rp.id parameter as CTAP1/U2F application parameter (32 bytes)
This seems inconsistent. Let's say I were example.com
, and I adopted U2F second-factor authentication early on. My application id would be https://example.com
, so my original U2F application parameter would be SHA256("https://example.com")
:
100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9
But if I then switched over to using Webauthn, my relying party identifier would just be example.com
. When that is converted to a U2F application parameter as described in section 7 of fido-client-to-authenticator-protocol-v2.0, the resulting value should be SHA256("example.com")
:
a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947
That is obviously different. Anyone who had previously set up their U2F keys for use with my website would no longer be able to use them after I switched to WebAuthn: unless they re-registered with their keys. And, of course, they would need to be able to log-in to do that.
Digging deeper, I noticed that the example they gave in the documentation had a relying party identifier of example.com
, but the hash they gave in the example was...
1194228DA8FDBDEEFD261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE
Which isn't either of the above two options. It remains unclear to me what string hashes to that value.
So what am I getting wrong here? How could services that deployed 2FA using U2F ever switch to FIDO2/Webauthn without requiring their users to re-register their security keys? I must be missing something.
WebAuthn supports backward compatibility with U2F via the AppID Extension documented in the W3C WebAuthn spec. The Relying Party (RP) passes the U2F application identity to the browser via this extension.