For the complete documentation index, see llms.txt. This page is also available as Markdown.

22. Add and remove keys on an ONCHAINID

The keys of an ONCHAINID decide who controls it and who acts for it. The Identity Owner adds a key to enroll a new device, a passkey, an operator or a signing service, and removes a key when a device is lost or a person leaves. Both are management operations: they change who holds authority over the identity, and only a MANAGEMENT key can make them.

At a glance

Triggered by

Identity Owner

Actors

Identity Owner, Platform

Contracts

ONCHAINID, ERC734Validator

Preconditions

The identity exists (see 02); the owner controls a MANAGEMENT key of the identity; for a new signer, its signer data is known

Result

The identity's key set reflects the change: a signer holds a new purpose, or no longer holds one

Actors and roles

Actor
Role in this flow
What they hold

Identity Owner

Decides the change and authorizes it.

A MANAGEMENT key of the identity

Platform

Encodes the signer, relays the call and verifies the result.

Whatever keys the owner has authorized

Sequence

Step by step

Step 1. Prepare the change

Who: Platform, on behalf of the Identity Owner

Call: Off-chain, using read calls

Before the call: The owner knows which signer gains or loses which purpose.

What happens: For a new signer, the Platform builds its signer data: the 20 bytes of an address for an externally owned account or a smart-contract wallet, or the address of a verifier followed by the public key for a passkey or another scheme. It computes keyHash as keccak256(signerData). It reads Identity.getKey(keyHash) to see whether the signer is already registered and with which purposes, and Identity.getKeysByPurpose(purpose) to see who already holds the purpose. Before removing a MANAGEMENT purpose, it reads Identity.getKeysByPurpose(1) and confirms that another manager remains.

What changes: Nothing on-chain.

Step 2. Add a key, or a purpose to a key

Who: Identity Owner (MANAGEMENT key)

Call: Identity.addKeyWithData(keyHash, purpose, keyType, signerData, clientData) for a signer that is not registered yet. Identity.addKey(keyHash, purpose, keyType) to give another purpose to a signer that is already registered.

Before the call: Step 1 is done. The call is made by a MANAGEMENT key directly on the identity, or by the identity itself in a user operation signed by a MANAGEMENT key.

What the contract checks:

  1. The caller holds MANAGEMENT on the identity, or is the identity itself. Otherwise the call reverts with SenderDoesNotHaveManagementKey.

  2. For addKeyWithData: keyHash equals keccak256(signerData). For addKey: the key is already registered. Otherwise InvalidSignerData.

  3. The signer data is at least 20 bytes and within the maximum length, and the client data is within its maximum. Otherwise InvalidSignerLength or ClientDataTooLong.

  4. The purpose is one of the six. Otherwise InvalidPurpose.

  5. The signer is not the key registry module itself. Otherwise ModuleCannotBeKey.

  6. For a signer already registered: keyType is the type it was registered with. Otherwise KeyTypeMismatch.

  7. The signer does not already hold this purpose. Otherwise KeyAlreadyRegistered.

What changes: The registry records the signer if it is new, and adds the purpose to it. It emits KeyAdded(identity, keyHash, purpose, keyType, signerData, clientData, caller). From the next call, the signer has the authority of that purpose.

Step 3. Remove a purpose from a key

Who: Identity Owner (MANAGEMENT key)

Call: Identity.removeKey(keyHash, purpose)

Before the call: Step 1 is done. To strip a signer entirely, the owner removes each of its purposes.

What the contract checks:

  1. The caller holds MANAGEMENT on the identity, or is the identity itself. Otherwise SenderDoesNotHaveManagementKey.

  2. The key is registered. Otherwise KeyNotRegistered.

  3. When the purpose is MANAGEMENT and the key is a signer: another MANAGEMENT signer remains. Otherwise CannotRemoveLastManagementKey.

  4. The key holds the purpose. Otherwise KeyDoesNotHavePurpose.

What changes: The purpose is removed from the key. When it was the key's last purpose, the key's record is deleted. The registry emits KeyRemoved(identity, keyHash, purpose, caller). From the next call, the signer no longer has that authority. Requests it had queued in the execution queue can no longer be approved.

State after the flow

  • Identity.keyHasPurpose(keyHash, purpose) reflects the change.

  • Identity.getKeysByPurpose(purpose) includes or no longer includes the key.

  • Identity.getKey(keyHash) returns the key's purposes, type and signer data, or nothing if the key was deleted.

  • The identity always has at least one MANAGEMENT key that can sign.

  • The identity's claims, linked wallets, positions and eligibility are unchanged.

Failure modes

Symptom
Cause
Fix

Reverts with SenderDoesNotHaveManagementKey

The caller holds ACTION or another purpose, not MANAGEMENT (check 1)

Use a MANAGEMENT key

A user operation that adds a key is rejected

It was signed by an ACTION key. Calls to the identity itself are management operations

Sign with a MANAGEMENT key

Reverts with InvalidSignerData

keyHash is not the hash of signerData, or addKey was used for a signer that is not registered (step 2, check 2)

Recompute the hash. Use addKeyWithData for a new signer

Reverts with InvalidSignerLength

The signer data is shorter than 20 bytes or too long (step 2, check 3)

Encode an address as its 20 bytes. Encode other signers as verifier then key

Reverts with InvalidPurpose

The purpose is not between 1 and 6 (step 2, check 4)

Use a defined purpose

Reverts with KeyTypeMismatch

The type given differs from the one the key was registered with (step 2, check 6)

Pass the registered type, read with getKey

Reverts with KeyAlreadyRegistered

The signer already holds this purpose (step 2, check 7)

Nothing to do

Reverts with KeyNotRegistered

The key is not on the identity (step 3, check 2)

Check the hash. For an address, it is the hash of the 20 address bytes

Reverts with CannotRemoveLastManagementKey

The key is the identity's last manager that can sign (step 3, check 3)

Add another MANAGEMENT key first

Reverts with KeyDoesNotHavePurpose

The key does not hold the purpose (step 3, check 4)

Read getKeyPurposes(keyHash)

A passkey was added and cannot call the identity directly

A direct call is authorized from the caller's address, and a passkey has none

A passkey acts through user operations

Common misunderstandings

Wrong: "Adding a key links a wallet."

Right: Keys and links are separate. A key gives a signer authority over the identity. A link attributes an address, and what it holds, to the identity. An address can be a key and not a linked wallet, or the reverse.

Wrong: "An ACTION key can add another ACTION key."

Right: Every change to the key set is a management operation. An ACTION key acts toward other contracts, and has no route to the identity's own keys, through a user operation, a direct call or the execution queue.

Wrong: "Removing a key removes the signer."

Right: removeKey removes one purpose. A signer that holds MANAGEMENT and ACTION keeps the other after one call. The record is deleted when the last purpose goes.

Wrong: "The key hash of an address is the hash of its ABI encoding."

Right: A key hash is keccak256 of the signer data. For an address, the signer data is the 20 address bytes, not the 32-byte ABI encoding that earlier versions hashed.

Integration notes

  • Enroll a second MANAGEMENT signer early, on another device. An identity with one manager and no guardians is lost with that key.

  • Use addKeyWithData for every new signer. addKey only adds a purpose to a signer the identity already knows.

  • Give each key the least purpose it needs: ACTION for daily use, CLAIM_SIGNER for a signing service, PROPOSER for an automated system.

  • When a device is lost, remove each purpose of its key. Then review requests pending in the execution queue.

  • Index KeyAdded and KeyRemoved on the ERC734Validator, filtered by identity. They carry the caller, so every change is attributable.

  • A Claim Issuer rotates its signing keys with this flow. Removing CLAIM_SIGNER from a key invalidates every claim that key signed, at once.

Last updated