Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1. Abstract

This spec introduces a distinction between two classes of Authenticators in the World ID Protocol: Proving Authenticators and Admin Authenticators. A Proving Authenticator holds only an off-chain public key and is limited to proof generation and presentation. An Admin Authenticator additionally holds an on-chain management key (represented as an address registered in _authenticatorAddressToPackedAccountData) and can perform all management operations on the World ID. The authenticator type is encoded in the existing pubkey bitmap of the WorldIDRegistry. In addition, this spec deprecates the updateAuthenticator method.

2. Motivation

Prior to this spec, every authenticator registered for a World ID was required to have a corresponding on-chain management key. This means every authenticator has equal permissions and can perform all management operations (inserting, removing or updating other authenticators, initiating recovery agent updates, etc.). There are multiple use cases where a user may want to have an authenticator capable of generating proofs on their behalf but not managing their World ID. An example of this is when delegating proof generation to some CLI tools, agents or non fully-featured authenticators.

Requiring a management key for authenticators that don’t need it expands the attack surface (and additionally increases storage costs on-chain). Separating the two roles allows:

  1. A smaller attack surface for Proving Authenticators, limiting the blast radius in case of compromise.
  2. Reduced storage costs for authenticators that only generate proofs.
  3. Flexibility for future authenticator form factors that lack the ability to hold an Ethereum key pair.

3. Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

The terms Authenticator, Issuer and Credential are as defined in the World ID Protocol.

3.1 Terminology

This spec introduces the following explicit terminology:

  1. Signing Key: This is an EdDSA over BabyJubJub keypair which authenticators use to sign operations off-chain related to proof generation. The signatures from this key are used in ZK circuits (e.g. Query Circuit or WIP-103 Proof of Ownership). Previously this may have been referred to as off-chain signer.
  2. Management Key: An ECDSA over secp256k1 keypair (curve used by World Chain) used to sign operations on-chain that change the state of a World ID in the WorldIDRegistry. The public key is represented as an Ethereum address. Previously this may have been referred to as on-chain signer.
  3. Management Operations: Any operation which results in a state change to a user’s World ID in the WorldIDRegistry. Examples: inserting an authenticator, removing an authenticator, initiating a Recovery Agent update.

3.2 Authenticator Classes

A distinction is introduced between two classes of Authenticators:

  1. Proving Authenticator: Holds only a Signing Key and is capable of proof generation and presentation. A Proving Authenticator SHALL NOT perform any management operations on the World ID, i.e. it cannot perform any state changes on the WorldIDRegistry for the user. A Proving Authenticator has no entry in _authenticatorAddressToPackedAccountData.
  2. Admin Authenticator: Holds both an Signing Key and a Management Key. An Admin Authenticator is capable of all operations for a user’s World ID, including management operations and proof generation.

Prior to the introduction of this spec, all registered authenticators MUST be considered Admin Authenticators.

3.3 Bitmap Encoding

The WorldIDRegistry stores a 96-bit pubkey bitmap per account in _leafIndexToRecoveryAddressPacked. This spec splits the bitmap into two halves:

  • Bits [0-47]: Occupancy. Bit n is set if pubkeyId n is in use.
  • Bits [48-95]: Authenticator Class. Bit 48 + n is set if pubkeyId n is a Proving Authenticator.

An occupied slot with its class bit clear is an Admin Authenticator. An occupied slot with its class bit set is a Proving Authenticator. The state (occupancy=0, class=1) is unused and MUST NOT occur; an unoccupied slot MUST have both bits clear.

As a consequence of this encoding, the effective hard limit on authenticators per account is reduced from 96 to 48. This limit MUST be exposed as a separate constant, MAX_AUTHENTICATORS_V2_HARD_LIMIT, and the registry MUST reject setMaxAuthenticators calls with a value greater than it.

3.4 insertAuthenticator

The insertAuthenticator function MUST accept address(0) as the newAuthenticatorAddress parameter. When address(0) is provided:

  1. The occupancy bit for the given pubkeyId MUST be set.
  2. The class bit MUST be set, marking the slot as a Proving Authenticator.
  3. No entry SHALL be written to _authenticatorAddressToPackedAccountData.
  4. Address validation (_validateNewAuthenticatorAddress) MUST be skipped.

When a non-zero address is provided, the function behaves as in V1 (occupancy bit set, class bit clear, address mapping written).

The operation MUST be authorized by an existing Admin Authenticator (no changes).

3.5 removeAuthenticator

The removeAuthenticator function MUST use the bitmap class bit to determine the removal path:

  1. If the class bit is set (Proving Authenticator), the caller MUST pass address(0) as authenticatorAddress. No address mapping validation or cleanup is performed. Both the occupancy and class bits MUST be cleared.
  2. If the class bit is clear (Admin Authenticator), the caller MUST pass the correct non-zero authenticatorAddress. Full V1 validation applies: the address mapping is checked for leaf index, pubkeyId, and recovery counter consistency, then deleted. Both the occupancy and class bits MUST be cleared.

If an incorrect authenticatorAddress is passed for the authenticator class being removed (non-zero for a Proving Authenticator, or zero for an Admin Authenticator) the call MUST revert with AuthenticatorClassMismatch.

The operation MUST be authorized by an existing Admin Authenticator (no changes).

3.6 updateAuthenticator

The updateAuthenticator function is deprecated in this version. Calls MUST revert with MethodUnsupported. Key rotation can be accomplished with a removeAuthenticator followed by insertAuthenticator.

3.7 Account Creation

Account creation (_registerAccount) is unaffected. _registerAccount rejects address(0), so every new account starts with at least one Admin Authenticator.

3.8 Manageability Invariant

An Admin Authenticator MUST NOT be removed if it is the only Admin Authenticator remaining on the account. Such a call MUST revert with UnmanageableNotAllowed. This guarantees that an account always retains at least one Admin Authenticator capable of managing it.

4. Rationale

Introducing two classes of authenticators allows users to have more control over the permissions they assign to different authenticators. This follows common security practices of least privilege.

4.1 Split bitmap over separate storage

The authenticator class could be stored in a separate mapping (e.g. mapping(uint64 => uint256)), but this would add a new cold SSTORE (20k gas) per account on first use. The split-bitmap approach reuses the existing 96-bit bitmap with no additional storage cost. The trade-off is halving the maximum authenticator count from 96 to 48, which is acceptable given practical account sizes.

4.2 updateAuthenticator disabled

Supporting all type of transitions in a single function adds significant surface area. The same outcomes are achievable via removeAuthenticator + insertAuthenticator. Furthermore, these operations are expected to seldom occur.

4.3 Authorization model

Proving Authenticators cannot sign management operations because they have no entry in _authenticatorAddressToPackedAccountData, which is required by _recoverAccountDataFromSignature. This is enforced structurally rather than with an explicit permission check, and the structural check covers every call site that resolves a signer through _recoverAccountDataFromSignature (e.g. insertAuthenticator, removeAuthenticator, initiateRecoveryAgentUpdate, cancelRecoveryAgentUpdate).

5. Security

  1. Proving Authenticators cannot escalate privileges. The lack of an _authenticatorAddressToPackedAccountData entry means _recoverAccountDataFromSignature will always revert for a Proving Authenticator’s address. There is no code path through which a Proving Authenticator can authorize a management operation.
  2. Class enforcement on removal. The bitmap class bit determines the required removal path. A caller cannot use address(0) to remove an Admin Authenticator (which would leave its mapping stale) or a non-zero address to remove a Proving Authenticator.
  3. Bitmap backward compatibility. V1 accounts have all type bits at zero, correctly classifying existing authenticators as Admin. This holds because _maxAuthenticators was never set above 48 in V1. The setMaxAuthenticators override enforces this new hard limit going forward.
  4. Account creation unchanged. The _registerAccount function retains its V1 check that rejects address(0), ensuring every new account starts with at least one Admin Authenticator.

6. Backwards Compatibility

  • All existing authenticators are classified as Admin Authenticators (class bit defaults to 0). No migration is required.
  • The maximum authenticator count per account is reduced from 96 to 48. Accounts with authenticators at pubkeyId >= 48 are incompatible.
  • updateAuthenticator is disabled. Callers relying on this function MUST switch to removeAuthenticator + insertAuthenticator.

7. Alternatives Considered

With the introduction of this spec, we also considered completely decoupling Signing Keys from Management Keys, which means that each key is treated independently and there is no explicit concept of an Authenticator encoded in the WorldIDRegistry. This was eventually dismissed. Implementing such a system requires one of two options.

A general note applies to both options: practical accounts are expected to hold a small number of authenticators (the V1 default for _maxAuthenticators is 7 and has never been raised), so the 48-slot cap imposed by the chosen encoding is not a binding constraint. While at this time the use of Management-only keys does not have an application it is quite likely the the future direction is for Management Keys to exist on alternative constructs (examples: passkeys, zkLogin alternatives, etc). These constructs could provide recovery alternatives to users. Not pursued with this spec because at this time there are off-protocol mechanisms (e.g. authenticator-managed backups) which address most of the same user pain points.

Option 1: New storage layout. This would truly reflect the separation between each key, but changing the storage layout requires the introduction of at least a new mapping (to store the separate Management Keys) which would cost at least 20,000 additional gas per account on first use, plus a migration path or lazy-init logic for V1 accounts, both of which expand the surface area. The runtime gain (decoupled lifecycles) does not justify these costs given the manageability invariant already binds at least one Management Key to each account.

Option 2: Re-structured bitmap. This entails reusing the same 96-bit bitmap but claiming the unused (occupancy=0, class=1) state to mean a Management-only Authenticator (a slot with a Management Key but no Signing Key). The main concrete benefit is that inserting or removing a Management Key would not require updating the Merkle tree, but this operation is rarely expected to occur. In exchange, the bitmap encoding becomes a four-state enum (instead of a flag on an occupied slot) and removeAuthenticator must distinguish three classes. The added complexity is not justified.