NOTE: This is a draft and not implementation ready. Security analysis: https://eprint.iacr.org/2020/1004 (published at https://sigsac.org/ccs/CCS2020/).
Authors: Emil Lundberg, Dain Nilsson
Web Authentication solves many problems in secure online authentication, but also introduces some new challenges. One of the greatest challenges is loss of an authenticator - what can the user do to prevent being locked out of their account if they lose an authenticator?
The current workaround to this problem is to have the user register more than one authenticator, for example a roaming USB authenticator and a platform authenticator integrated into a smartphone. That way the user can still use the other authenticator to log in if they lose one of the two.
However, this approach has drawbacks. What we would like to enable is for the user to have a separate backup authenticator which they could leave in a safe place and not keep with them day-to-day. This is not really feasible with the aforementioned workaround, since the user would have to register the backup authenticator with each new RP where they register their daily-use authenticator. This effectively means that the user must keep the backup authenticator with them, or in an easily accessible location, to not risk forgetting to register the backup authenticator, which largely defeats the purpose of the backup authenticator.
Under the restriction that we don't want to share any secrets or private keys between authenticators, one simple way to solve this would be to import a public key from the backup authenticator to the primary authenticator, so that the primary authenticator can also register that public key with each RP. Then the backup authenticator can later prove possession of the private key and recover access to the account. This has a big drawback, however: a static public key would be easily correlatable between RPs or accounts, undermining much of the privacy protections in WebAuthn.
In this document we propose a key agreement scheme which allows a pair of authenticators to agree on an EC key pair in such a way that the primary authenticator can generate nondeterministic public keys, but only the backup authenticator can derive the corresponding private keys. We present the scheme in the context of a practical application as a WebAuthn extension for account recovery. This enables the use case of storing the backup authenticator in a secure location, while maintaining WebAuthn's privacy protection of non-correlatable public keys.
The following terms are used throughout this document:
LEFT(X, n)
is the firstn
bytes of the byte arrayX
.DROP_LEFT(X, n)
is the byte arrayX
without the firstn
bytes.- CTAP2_ERR_XXX represents some not yet specified error code.
The scheme has three participants:
- PA: the primary authenticator
- BA: the backup authenticator
- RP: the WebAuthn Relying Party
The goal is that PA will generate public keys for RP to store. At a later time, BA will request the public keys from RP and derive the corresponding private keys without further communication with PA.
The scheme is divided into three stages ordered in this forward sequence:
-
In stage 1, only PA and BA may communicate.
PA <-> BA RP
This corresponds to the initial setup done to pair the primary authenticator with the backup authenticator.
-
In stage 2, only PA and RP may communicate.
PA BA ^ | v RP
This corresponds to using the primary authenticator for day-to-day authentication while the backup authenticator is stored away in a safe place.
-
In stage 3, only BA and RP may communicate.
PA BA ^ | | RP <---+
This corresponds to the primary authenticator being lost and no longer available, and the backup authenticator having been retrieved from storage.
This procedure is performed once to set up the parameters for the key agreement scheme.
- PA and BA agree on a choice of two key derivation functions
KDF1
andKDF2
, and one message authentication code (MAC) functionMAC
.KDF1
outputs integers andKDF2
outputs values suitable as key inputs forMAC
. - BA generates a new P-256 EC key pair with private key
s
and public keyS
. - BA sends
S
to PA. - RP chooses a unique public identifier
rp_id
. This is effectively a protocol constant and implicitly available to all parties at all times.
The following steps are performed by PA, the primary authenticator.
-
Generate an ephemeral EC P-256 key pair:
e
,E
. -
Let
cred_key = KDF1(ECDH(e, S))
andmac_key = KDF2(ECDH(e, S))
. -
If
cred_key >= n
, wheren
is the order of the P-256 curve, start over from 1. -
Calculate
P = (cred_key * G) + S
, where * and + are EC point multiplication and addition, andG
is the generator of the P-256 curve. -
If
P
is the point at infinity, start over from 1. -
Let
credential_id = E || LEFT(MAC(mac_key, E || rp_id), 16)
. -
Send the pair
(P, credential_id)
to RP for storage.
The following steps are performed by BA, the backup authenticator.
-
Retrieve a set of
credential_id
s from RP. Perform the following steps for eachcredential_id
. -
Let
E = LEFT(credential_id, 65)
. Verify thatE
is not the point at infinity. -
Let
cred_key = KDF1(ECDH(s, E))
andmac_key = KDF2(ECDH(s, E))
. -
Verify that
credential_id == E || LEFT(MAC(mac_key, E || rp_id), 16)
. If not, thiscredential_id
was generated for a different backup authenticator than BA or a different relying party than RP, and is not processed further. -
Calculate
p = cred_key + s mod n
, wheren
is the order of the P-256 curve. -
The private key is
p
, which BA can now use to create a signature.
As a result of these procedures, BA will have derived p
such that
p * G = (cred_key + s) * G =
= cred_key * G + s * G =
= cred_key * G + S = P.
Although it was shown by Frymann et al. [1] that derived public keys P
are
unlinkable, it was brought to our attention by Wilson [2] that a weakness exists
in this generic protocol: a user who has multiple accounts can be tricked by a
malicous RP into revealing their ownership of both accounts.
The attack proceeds as follows:
-
The user performs the procedure defined above in "public key creation" twice, resulting in two distinct public keys
P1
andP2
, with respective credential IDcredential_id1
andcredential_id2
. The user registers (P1
,credential_id1
) as a recovery key for accountA1
at the RP, and registers (P2
,credential_id2
) as a recovery key for accountA2
at the same RP. -
The user initiates the recovery procedure for account
A1
, expecting the RP to respond with an authentication challenge withcredential_id1
to be signed byP1
. -
The RP instead responds with an authentication challenge with
credential_id2
. Since this is also a valid credential ID for the same RP ID, the user's backup authenticator successfully produces an authentication signature signed byP2
. -
Since
P2
is registered to accountA2
, the RP can conclude that the user most likely owns bothA1
andA2
.
This account linking attack is possible in the WebAuthn protocol without the
recovery
extension proposed below, so this weakness does not introduce any new
weakness when used in the WebAuthn context. However, this weakness should be
taken into account should this protocol be applied in other contexts where it
would introduce a new weakness. In that case, this weakness could possibly be
mitigated by including some form of account identifier in the MAC embedded in
the credential_id
; this way the client and authenticator could cooperate to
detect if the RP responds with credential_id
s for a different account than the
user requested. See [2] for additional detail.
- [1]: Frymann et al., "Asynchronous Remote Key Generation: An Analysis of Yubico's Proposal for W3C WebAuthn". Proceedings of the 2020 ACM SIGSAC Conference on Computer and Communications Security, 2020. https://doi.org/10.1145/3372297.3417292
- [2]: Wilson, Spencer MacLaren, "Post-Quantum Account Recovery for Passwordless Authentication". Master Thesis, University of Waterloo, 2023. https://uwspace.uwaterloo.ca/handle/10012/19316
This section proposes an application of the above key agreement scheme as a WebAuthn extension for recovery credentials. The second subsection proposes the CTAP2 commands used to export and import the recovery seed.
This extension allows for recovery credentials to be registered with an RP, which can be used for account recovery in the case of a lost or destroyed primary authenticator. This is done by associating one or more backup authenticators with the primary authenticator, after which the latter can provide additional credentials for account recovery to the RP without involving the backup authenticators.
In summary, the extension works like this:
-
The primary authenticator first generates public keys and credential IDs for recovery credentials. These are stored by the RP and associated with the primary authenticator's credential, the primary credential. These are delivered through the authenticator data, and therefore signed by the primary credential.
-
After losing the primary authenticator, account recovery can be done by creating a new credential with the backup authenticator. The backup authenticator receives the recovery credential IDs from the RP, and can use one of them to derive the private key corresponding to the recovery public key. The backup authenticator uses this private key to sign the new credential public key, thus creating a signature chain from the primary credential to the new credential.
-
Upon verifying the recovery signature, the RP invalidates the primary credential and all recovery credentials associated with it, and replaces it with the new credential. The backup authenticator is thus "promoted" and replaces the primary authenticator.
In order for the RP to detect when recovery credentials can be registered, or
need to be updated, the primary authenticator keeps a recovery credentials
state counter defined as follows. Let state
be initialized to 0. Performing a
device reset resets state
to 0. When the set of registered backup
authenticators for the device changes (e.g., on adding or removing a backup
authenticator, including adding the first backup authenticator) state
is
incremented by one.
NOTE: The choice to make registration of recovery credentials explicit is deliberate, in an attempt to ensure that the user deliberately intends to do so and understands the implications.
The authenticator operations are governed by an alg
parameter,
an unsigned 8-bit integer identifying the key agreement scheme to be used.
Credential IDs for recovery credentials are always on the form
alg || <key agreement data>
,
where the format and meaning of <key agreement data>
depends on the value of alg
.
This allows for new key agreement schemes to be added in the future
without changes to the WebAuthn-facing interface;
clients and RPs are automatically compatible with any new key agreement schemes.
Currently the only valid value for alg
is alg=0
.
recovery
Registration and Authentication
partial dictionary AuthenticationExtensionsClientInputs {
RecoveryExtensionInput recovery;
}
dictionary RecoveryExtensionInput {
required RecoveryExtensionAction action;
sequence<PublicKeyCredentialDescriptor> allowCredentials;
}
enum RecoveryExtensionAction {
"state",
"generate",
"recover"
}
The values of action
have the following meanings. X
indicates that the value
is applicable for the given WebAuthn operation:
Value | create() | get() | Description |
---|---|---|---|
state | X | X | Get the recovery credentials state counter value from the primary authenticator. |
generate | X | Regenerate recovery credentials from the primary authenticator. | |
recover | X | Get a recovery signature from a backup authenticator, to replace the primary credential with a new one. |
None required, except creating the authenticator extension input from the client extension input.
If the client implements support for this extension, then when action
is
"generate"
, the client SHOULD notify the user of the number of recovery
credentials in the response.
None.
The client extension input encoded as a CBOR map.
If action
is
-
"state"
,-
Let
state
be the current value of the recovery credentials state counter. -
Set the extension output to the CBOR encoding of
{"action": "state", "state": state}
.
-
-
"generate"
,-
If the current operation is not an
authenticatorGetAssertion
operation, return CTAP2_ERR_XXX. -
Let
creds
be an empty list. -
For each recovery seed tuple
(alg, aaguid, S)
stored in this authenticator:-
If
alg
equals-
0:
-
Generate an ephemeral EC P-256 key pair:
e, E
.E
MUST NOT be the point at infinity. -
Let
ikm = ECDH(e, S)
. Letikm_x
be the X coordinate ofikm
, encoded as a byte string of length 32 as described in SEC 1, section 2.3.7. -
Let
credKey
be the 32 bytes of output keying material from HKDF-SHA-256 with the arguments:salt
: Not set.IKM
:ikm_x
.info
: The stringwebauthn.recovery.cred_key
encoded as a UTF-8 byte string.L
: 32.
Parse
credKey
as a big-endian unsigned 256-bit number. -
Let
macKey
be the 32 bytes of output keying material from HKDF-SHA-256 with the arguments:salt
: Not set.IKM
:ikm_x
.info
: The stringwebauthn.recovery.mac_key
encoded as a UTF-8 byte string.L
: 32.
-
If
credKey >= n
, wheren
is the order of the P-256 curve, start over from 1. -
Let
P = (credKey * G) + S
, where * and + are EC point multiplication and addition, andG
is the generator of the P-256 curve. -
If
P
is the point at infinity, start over from 1. -
Let
rpIdHash
be the SHA-256 hash ofrpId
. -
Let
E_enc
beE
encoded as described in SEC 1, section 2.3.3, without point compression. -
Set
credentialId = alg || E_enc || LEFT(HMAC-SHA-256(macKey, alg || E_enc || rpIdHash), 16)
.
-
-
anything else:
-
Return CTAP2_ERR_XXX.
Note: This should never happen, since the Import recovery seed operation should never store a recovery seed with an unknown
alg
value.
-
-
-
Let
attCredData
be a new attested credential data structure with the following member values:- aaguid:
aaguid
. - credentialIdLength: The byte length of
credentialId
. - credentialId:
credentialId
. - credentialPublicKey:
P
.
- aaguid:
-
Add
attCredData
tocreds
.
-
-
Let
state
be the current value of the recovery credentials state counter. -
Set the extension output to the CBOR encoding of
{"action": "generate", "state": state, "creds": creds}
.
-
-
"recover"
,-
If the current operation is not an
authenticatorMakeCredential
operation, return CTAP2_ERR_XXX. -
If the recovery seed key pair
s, S
has not been initialized, return CTAP2_ERR_XXX. -
For each
cred
inallowCredentials
:-
Let
alg = LEFT(cred.id, 1)
. -
If
alg
equals-
0:
-
Let
E_enc = LEFT(DROP_LEFT(cred.id, 1), 65)
. -
Let
E
be the P-256 public key decoded from the uncompressed pointE_enc
as described in SEC 1, section 2.3.4. If invalid, return CTAP2_ERR_XXX. -
If
E
is the point at infinity, return CTAP2_ERR_XXX. -
Let
ikm = ECDH(s, E)
. Letikm_x
be the X coordinate ofikm
, encoded as a byte string of length 32 as described in SEC 1, section 2.3.7. -
Let
credKey
be the 32 bytes of output keying material from HKDF-SHA-256 with the arguments:salt
: Not set.IKM
:ikm_x
.info
: The stringwebauthn.recovery.cred_key
encoded as a UTF-8 byte string.L
: 32.
Parse
credKey
as a big-endian unsigned 256-bit number. -
Let
macKey
be the 32 bytes of output keying material from HKDF-SHA-256 with the arguments:salt
: Not set.IKM
:ikm_x
.info
: The stringwebauthn.recovery.mac_key
encoded as a UTF-8 byte string.L
: 32.
-
Let
rpIdHash
be the SHA-256 hash ofrp.id
. -
If
cred.id
is not exactly equal toalg || E || LEFT(HMAC-SHA-256(macKey, alg || E || rpIdHash), 16)
, continue. -
Let
p = credKey + s (mod n)
, wheren
is the order of the P-256 curve. -
Let
authenticatorDataWithoutExtensions
be the authenticator data that will be returned from this registration operation, but without theextensions
part. TheED
flag inauthenticatorDataWithoutExtensions
MUST be set to 1 even thoughauthenticatorDataWithoutExtensions
does not include extension data. -
Let
sig
be a signature overauthenticatorDataWithoutExtensions || clientDataHash
usingp
.sig
is DER encoded as described in RFC 3279.
-
-
anything else:
- Continue.
-
-
Let
state
be the current value of the recovery credentials state counter. -
Set the extension output to the CBOR encoding of
{"action": "recover", "credId": cred.id, "sig": sig, "state": state}
and end extension processing.
-
-
Return an error code equivalent to ERR_XXX.
-
-
anything else,
- Return CTAP2_ERR_XXX.
A CBOR map with contents as defined above.
dictionary RecoveryExtensionOutput {
required RecoveryExtensionAction action;
required int state;
sequence<ArrayBuffer> creds;
ArrayBuffer credId;
ArrayBuffer sig;
}
-
The RP MUST be very explicit in notifying the user when recovery credentials are registered, and how many, to avoid any credentials being registered without the user's knowledge. If possible, the client SHOULD also display the number of backup authenticators associated with the primary authenticator.
-
The RP SHOULD clearly display information about registered recovery credentials, just as it does with standard credentials. For example, the RP MAY use the AAGUIDs of recovery credentials to indicate the (alleged) model of the corresponding managing authenticator.
-
All security and privacy considerations for standard credentials also apply to recovery credentials.
-
Although recovery credentials are issued by the primary authenticator, they can only ever be used by the backup authenticator.
-
Recovery credentials are scoped to a specific RP ID, and the RP SHOULD also associate each recovery credential with a specific primary credential.
-
Recovery credentials can only be used in registration ceremonies where the recovery extension is present, with
action == "recover"
. -
A primary authenticator MAY refuse to import a recovery seed without a trusted attestation signature, to reduce the risk that an RP rejects the recovery credential that would later be generated by the backup authenticator.
-
Recovery credentials cannot be used as resident credentials, since they by definition cannot be stored in the backup authenticator.
The CTAP2 command authenticatorRecovery
is added. It is not exposed via any
browser API.
This command is used to export a recovery seed from a backup authenticator and then to import the seed to another authenticator, so that the latter can issue recovery credentials on behalf of the backup authenticator.
It takes the following input parameters:
Parameter name | Data type | Required? | Definition |
---|---|---|---|
subCommand (0x01) | Unsigned integer | Required | Identifier for the subcommand to execute. |
allowAlgs (0x02) | Array of unsigned integers | Optional | Required if subCommand = exportSeed (0x02). List of acceptable key agreement schemes for seed export. |
seed (0x03) | RecoverySeed | Optional | Required if subCommand = importSeed (0x03). Recovery seed to import. |
pinUvAuthProtocol (0x04) | Unsigned integer | Required | PIN/UV protocol version chosen by the client. |
pinUvAuthParam (0x05) | Byte array | Required | First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken |
The list of sub commands for recovery seeds is:
subCommand Name | subCommand Number |
---|---|
getAllowAlgs | 0x01 |
exportSeed | 0x02 |
importSeed | 0x03 |
The RecoverySeed type is a CBOR map with the following structure:
Member name | Data type | Required? | Definition |
---|---|---|---|
alg (0x01) | Unsigned integer | Required | Identifier for the key agreement scheme. |
aaguid (0x02) | Byte array | Required | AAGUID of the authenticator that exported the payload . |
x5c (0x03) | Array of byte arrays | Required | Sequence of DER encoded X.509 attestation certificates |
sig (0x04) | Byte array | Required | DER encoded ECDSA signature. |
S_enc (0xFF) | Byte array | Optional | Required if alg = 0x00. P-256 public key encoded as described in SEC 1, section 2.3.4, without point compression. |
On success, authenticator returns the following structure in its response:
Parameter name | Data type | Required? | Definition |
---|---|---|---|
allowAlgs (0x02) | Array of unsigned integers | Optional | List of key agreement schemes the authenticator supports. |
seed (0x03) | RecoverySeed | Optional | Recovery seed to be imported by another authenticator. |
TODO
Used by the platform to get a suitable value for the allowAlgs (0x02) parameter of the exportSeed (0x02) subcommand.
Following operations are performed to get the list of recovery key agreement schemes an authenticator supports:
- Platform sends authenticatorRecovery command with following parameters:
- subCommand (0x01): getAllowAlgs (0x01)
- Authenticator returns authenticatorRecovery response with following
parameters:
- allowAlgs (0x02): An array containing the integer 0 as the only element.
Exports a seed which can be imported into other authenticators, enabling them to register credentials on behalf of the exporting authenticator, for the purpose of account recovery.
Following operations are performed to get a recovery seed:
-
Platform gets pinUvAuthToken from the authenticator.
-
Platform sends authenticatorRecovery command with following parameters:
-
subCommand (0x01): exportSeed (0x02)
-
allowAlgs (0x02): Output from getAllowAlgs (0x01) subcommand on a different authenticator
-
pinUvAuthProtocol (0x04): Pin Protocol used. Currently this is 0x01.
-
pinUvAuthParam (0x05):
LEFT(HMAC-SHA-256(pinUvAuthToken, exportSeed (0x02)), 16)
.
-
-
Authenticator verifies pinUvAuthParam by generating
LEFT(HMAC-SHA-256(pinUvAuthToken, exportSeed (0x02)), 16)
and matching against input pinUvAuthParam parameter.- If pinUvAuthParam verification fails, authenticator returns CTAP2_ERR_PIN_AUTH_INVALID error.
- If authenticator sees 3 consecutive mismatches, it returns CTAP2_ERR_PIN_AUTH_BLOCKED indicating that power recycle is needed for further operations. This is done so that malware running on the platform should not be able to block the device without user interaction.
-
Authenticator performs following steps:
-
For each
alg
inallowAlgs
:-
If
alg
equals:-
0:
-
If the recovery functionality is uninitialized, generate a new EC P-256 key pair and store it as
s, S
. TheauthenticatorReset
command MUST erases
andS
. -
Let
S_enc
beS
encoded as described in SEC 1, section 2.3.3, without point compression. -
Let
sig
be a signature over the dataalg || aaguid || S_enc
using the authenticator's attestation key and the SHA-256 hash algorithm.sig
is DER encoded as described in RFC 3279. -
Authenticator returns authenticatorRecovery response with following parameters:
- seed (0x03): RecoverySeed structure with following parameters:
- alg (0x01):
alg
- aaguid (0x02): Authenticator's AAGUID
- x5c (0x03): Authenticator's attestation certificate chain as DER encoded X.509 certificates, with leaf attestation certificate as the first element
- sig (0x04):
sig
as computed above - S_enc (0xFF):
S_enc
as computed above
- alg (0x01):
- seed (0x03): RecoverySeed structure with following parameters:
-
-
anything else:
- Continue.
-
-
-
Return CTAP2_ERR_XXX.
-
Imports a recovery seed, enabling this authenticator to issue recovery
credentials on behalf of a backup authenticator. Multiple recovery seeds can be
imported into an authenticator, limited by storage space. Resetting the
authenticator removes all stored recovery seeds, and resets the state
counter
to 0.
Following operations are performed to get a recovery seed:
-
Platform gets pinUvAuthToken from the authenticator.
-
Platform sends authenticatorRecovery command with following parameters:
-
subCommand (0x01): importSeed (0x03)
-
seed (0x03): Output from exportSeed (0x02) subcommand on a different authenticator, containing following parameters:
- alg (0x01): Identifier for key agreement scheme
- aaguid (0x02): AAGUID of the authenticator that exported the seed
- x5c (0x03): Attestation certificate chain of the authenticator that exported the seed
- sig (0x04): Attestation signature over the seed contents
- S_enc (0xFF): Required if alg = 0x00. EC public key encoded without point compression.
-
pinUvAuthProtocol (0x04): Pin Protocol used. Currently this is 0x01.
-
pinUvAuthParam (0x05):
LEFT(HMAC-SHA-256(pinUvAuthToken, importSeed (0x03)), 16)
.
-
-
Authenticator verifies pinUvAuthParam by generating
LEFT(HMAC-SHA-256(pinUvAuthToken, importSeed (0x03)), 16)
and matching against input pinUvAuthParam parameter.- If pinUvAuthParam verification fails, authenticator returns CTAP2_ERR_PIN_AUTH_INVALID error.
- If authenticator sees 3 consecutive mismatches, it returns CTAP2_ERR_PIN_AUTH_BLOCKED indicating that power recycle is needed for further operations. This is done so that malware running on the platform should not be able to block the device without user interaction.
-
Authenticator performs following steps:
-
If the authenticator has no storage space available to import a recovery seed, return CTAP2_ERR_XXX.
-
If
alg
equals:-
0:
-
Let
S
be the P-256 public key decoded from the uncompressed pointS_enc
as described in SEC 1, section 2.3.4. If invalid, return CTAP2_ERR_XXX. -
Let
attestation_cert
be the first element ofx5c
. -
Extract the public key from
attestation_cert
and use it to verify the signaturesig
against the signed dataalg || aaguid || S_enc
. If invalid, return CTAP2_ERR_XXX. -
If
attestation_cert
contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid
), verify that the value of this extension equalsaaguid
. -
OPTIONALLY, perform this sub-step:
- Using a vendor-specific store of trusted attestation CA
certificates, verify the signature chain
x5c
. If invalid or untrusted, OPTIONALLY return CTAP2_ERR_XXX.
- Using a vendor-specific store of trusted attestation CA
certificates, verify the signature chain
-
Store
(alg, aaguid, S)
internally.
-
-
anything else:
- Return CTAP2_ERR_XXX.
-
-
Increment the
state
counter by one (the counter's initial value is 0). -
Return CTAP2_OK.
-
An RP supporting this extension SHOULD include the extension with action: "state"
whenever performing a registration or authentication ceremony. There
are two cases where the response indicates that the RP SHOULD initiate recovery
credential registration (action: "generate"
), which are:
- Upon successful
create()
, ifstate
> 0. - Upon successful
get()
, ifstate
>old_state
, whereold_state
is the previous value forstate
that the RP has seen for the used credential.
The following operations assume that each user account contains a
recoveryStates
field, which is a map with credential IDs as keys.
recoveryStates
is initialized to an empty map.
To detect when the user's authenticator has updated its recovery seed settings, the RP SHOULD add the following steps to all registration and authentication ceremonies:
-
When initiating any
create()
orget()
operation, set the extension"recovery": {"action": "state"}
. -
Let
pkc
be the PublicKeyCredential response from the client. -
In step 14 of the RP Operation to Register a New Credential, or 15 of the RP Operation to Verify an Authentication Assertion, perform the following steps:
-
Let
extOutput
be the recovery extension output, or null if not present. Forcreate()
ceremonies, this isextOutput = pkc.response.attestationObject["authData"].extensions["recovery"]
; forget()
ceremonies it isextOutput = pkc.response.authenticatorData.extensions["recovery"]
. -
If
extOutput
is not null:-
If
extOutput.action
does not equal"state"
, orextOutput.state
is not present, abort this extension processing and OPTIONALLY show a user-visible warning. -
If
extOutput.state > 0
:-
Let
recoveryState = recoveryStates[pkc.id]
, or null if not present. -
If
recoveryState
is null orextOutput.state > recoveryState.state
:- If the ceremony finishes successfully, prompt the user that
their recovery credentials need to be updated and ask to
initiate a Registering recovery credentials ceremony as
described below. It is RECOMMENDED to set
allowCredentials
to contain onlypkc.id
in this authentication ceremony.
- If the ceremony finishes successfully, prompt the user that
their recovery credentials need to be updated and ask to
initiate a Registering recovery credentials ceremony as
described below. It is RECOMMENDED to set
-
-
-
-
Continue with the remaining steps of the standard registration or authentication ceremony.
To register new recovery credentials for a given primary credential, or replace the existing recovery credentials with updated ones, the RP performs the following procedure:
-
Initiate a
get()
operation and set the extension"recovery": {"action": "generate"}
.If this ceremony was triggered as described in Detecting changes to recovery seeds, it is RECOMMENDED to set
allowCredentials
to contain only the credential that was used in that preceding ceremony. -
Let
pkc
be the PublicKeyCredential response from the client. If the operation fails, abort the ceremony with an error. -
In step 15 of the RP Operation to Verify an Authentication Assertion, perform the following steps:
-
Let
extOutput = pkc.response.authenticatorData.extensions["recovery"]
, or null if not present. -
If
extOutput
is null,extOutput.action
does not equal"generate"
,extOutput.state
is not present, orextOutput.creds
is not present, abort the ceremony with an error. -
Let
acceptedCreds
be a new empty list. -
Let
rejectedCreds
be a new empty list. -
For each
cred
inextOutput.creds
:- If
cred.aaguid
identifies an authenticator model accepted by the RP's policy, addcred
toacceptedCreds
. Otherwise, addcred
torejectedCreds
.
- If
-
Set
recoveryStates[pkc.id] = (extOutput.state, acceptedCreds)
. -
Show the user a confirmation message containing the length of
acceptedCreds
. -
If
rejectedCreds
is not empty, show the user a warning message. The warning message SHOULD contain the length ofrejectedCreds
and, if possible, descriptions of the AAGUIDs that were rejected.
-
-
Continue with the remaining steps of the standard authentication ceremony.
To authenticate the user with a recovery credential and create a new primary credential, the RP performs the following procedure:
-
Identify the user, for example by asking for a username.
-
Let
allowCredentials
be a new empty list. -
For each
(state, creds)
value in therecoveryStates
map stored in the user's account:-
For each
cred
increds
:-
Let
credDesc
be a PublicKeyCredentialDescriptor structure with the following member values:- type:
"public-key"
. - id:
cred.credentialId
.
- type:
-
Add
credDesc
toallowCredentials
.
-
-
-
If
allowCredentials
is empty, abort this procedure with an error. -
Initiate a
create()
operation with the extension input:"recovery": { "action": "recover", "allowCredentials": <allowCredentials as computed above> }
-
Let
pkc
be the PublicKeyCredential response from the client. If the operation fails, abort the ceremony with an error. -
In step 14 of the RP Operation to Register a New Credential, perform the following steps:
-
Let
extOutput = pkc.response.authenticatorData.extensions["recovery"]
, or null if not present. -
If
extOutput
is null,extOutput.action
does not equal"recover"
,extOutput.state
is not present,extOutput.credId
is not present, orextOutput.sig
is not present, abort the ceremony with an error. -
Let
revokedCredId
be null. -
For each
primaryCredId
in the keys ofrecoveryStates
:-
Let
(state, creds) = recoveryCreds[primaryCredId]
. -
For each
cred
increds
:-
If
cred.credentialId
equalsextOutput.credId
:-
Verify that
credentialId
equals theid
member of some element ofallowCredentials
. -
Let
publicKey
be the decoded public keycred.credentialPublicKey
. -
Let
authenticatorDataWithoutExtensions
bepkc.response.authenticatorData
, but without theextensions
part. TheED
flag inauthenticatorDataWithoutExtensions
MUST be set to 1 even thoughauthenticatorDataWithoutExtensions
does not include the extension outputs. -
Using
publicKey
, verify thatextOutput.sig
is a valid signature overauthenticatorDataWithoutExtensions || clientDataHash
. If the signature is invalid, fail the registration ceremony. -
Set
revokedCredId = primaryCredId
. -
Break.
-
-
If
revokedCredId
is not null, break.
-
-
-
If
revokedCredId
is null, abort the ceremony with an error.
-
-
Continue with the remaining steps of the standard registration ceremony. This means a new credential has now been registered using the backup authenticator.
-
Invalidate the credential identified by
revokedCredId
and all recovery credentials associated with it (i.e., deleterecoveryStates[revokedCredId]
). This step and the registration of the new credential SHOULD be performed as an atomic operation. -
It is RECOMMENDED to send the user an e-mail or similar notification about this change to their account.
-
If
extOutput.state
is greater than 0, the RP SHOULD initiate recovery credential registration (action = "generate"
) for the newly registered credential.
When identifying the user and building the allowCredentials
list, please
consider the risk of privacy leak via Credential IDs.
As an alternative to proceeding to register a new credential for the backup authenticator, the RP MAY choose to not replace the lost credential with the new one, and instead disable 2FA or provide some other means for the user to access their account. In either case, the associated primary credential SHOULD be revoked and no longer usable.