EIP1484 - Digital Identity Aggregator

# Simple Summary

A protocol for aggregating digital identity information that's broadly interoperable with existing, proposed, and hypothetical future digital identity standards.

# Abstract

This EIP proposes an identity management and aggregation framework on the Ethereum blockchain. It allows entities to claim an Identity via a singular Identity Registry smart contract, associate it with Ethereum addresses in a variety of meaningful ways, and use it to interact with smart contracts. This enables arbitrarily complex identity-related functionality. Notably (among other features) ERC-1484 Identities: are self-sovereign, can natively support ERC-725 and ERC-1056 identities, are DID compliant (opens new window), and can be fully powered by meta-transactions (opens new window).

# Motivation

Emerging identity standards and related frameworks proposed by the Ethereum community (including ERCs/EIPs 725, 735 (opens new window), 780 (opens new window), 1056, etc.) define and instrumentalize digital identity in a variety of ways. As existing approaches mature, new standards emerge, and isolated, non-standard approaches to identity develop, coordinating on identity will become increasingly burdensome for blockchain users and developers, and involve the unnecessary duplication of work.

The proliferation of on-chain identity solutions can be traced back to the fact that each codifies a notion of identity and links it to specific aspects of Ethereum (claims protocols, per-identity smart contracts, signature verification schemes, etc.). This proposal eschews that approach, instead introducing a protocol layer in between the Ethereum network and individual identity applications. This solves identity management and interoperability challenges by enabling any identity-driven application to leverage an un-opinionated identity management protocol.

# Definitions

  • Identity Registry: A single smart contract which is the hub for all Identities. The primary responsibility of the Registry is to define and enforce the rules of a global namespace for Identities, which are individually denominated by Ethereum Identification Numbers (EINs).

  • Identity: A data structure containing all the core information relevant to an identity, namely: a Recovery Address, an Associated Addresses set, a Providers set, and a Resolvers set. Identities are denominated by EINs (incrementing uint identifiers starting at 1), which are unique but otherwise uninformative. Each Identity is a Solidity struct:

struct Identity {
    address recoveryAddress;
    AddressSet.Set associatedAddresses;
    AddressSet.Set providers;
    AddressSet.Set resolvers;
}
1
2
3
4
5
6
  • Associated Address: An Ethereum address publicly associated with an Identity. In order for an address to become an Associated Address, an Identity must either transact from or produce an appropriately signed message from the candidate address and an existing Associated Address, indicating intent to associate. An Associated Address can be removed from an Identity by transacting/producing a signature indicating intent to disassociate. A given address may only be an Associated Address for one Identity at any given time.

  • Provider: An Ethereum address (typically but not by definition a smart contract) authorized to act on behalf of Identities who have authorized them to do so. This includes but is not limited to managing the Associated Address, Provider, and Resolver sets for an Identity. Providers exist to facilitate user adoption by making it easier to manage Identities.

  • Resolver: A smart contract containing arbitrary information pertaining to Identities. A resolver may implement an identity standard, such as ERC-725, or may consist of a smart contract leveraging or declaring identifying information about Identities. These could be simple attestation structures or more sophisticated financial dApps, social media dApps, etc. Each Resolver added to an Identity makes the Identity more informative.

  • Recovery Address: An Ethereum address (either an account or smart contract) that can be used to recover lost Identities as outlined in the Recovery section.

  • Destruction: In the event of irrecoverable loss of control of an Identity, Destruction is a contingency measure to permanently disable the Identity. It removes all Associated Addresses, Providers, and optionally Resolvers while preserving the Identity. Evidence of the existence of the Identity persists, while control over the Identity is nullified.

# Specification

A digital identity in this proposal can be viewed as an omnibus account, containing more information about an identity than any individual identity application could. This omnibus identity is resolvable to an unlimited number of sub-identities called Resolvers. This allows an atomic entity, the Identity, to be resolvable to abstract data structures, the Resolvers. Resolvers recognize Identities by any of their Associated Addresses, or by their EIN.

The protocol revolves around claiming an Identity and managing Associated Addresses, Providers and Resolvers. Identities can delegate much or all of this responsibility to one or more Providers, or perform it directly from an Associated Address. Associated Addresses/Providers may add and remove Resolvers and Providers indiscriminately. Associated Addresses may only be added or removed with the appropriate permission.

# Identity Registry

The Identity Registry contains functionality to create new Identities and for existing Identities to manage their Associated Addresses, Providers, and Resolvers. It is important to note that this registry fundamentally requires transactions for every aspect of building out an Identity. However, recognizing the importance of accessibility to dApps and identity applications, we empower Providers to build out Identities on the behalf of users, without requiring users to pay gas costs. An example of this pattern, often referred to as a meta transactions, can be seen in the reference implementation (opens new window).

Due to the fact that multiple addresses can be associated with a given identity (though not the reverse), Identities are denominated by EIN. This uint identifier can be encoded in QR format or mapped to more user-friendly formats, such as a string, in registries at the Provider or Resolver level.

# Address Management

The address management function consists of trustlessly connecting multiple user-owned Associated Addresses to an Identity. It does not give special status to any particular Associated Address, rather leaving this (optional) specification to identity applications built on top of the protocol - for instance, management, action, claim and encryption keys denominated in the ERC-725 standard, or identifiers and delegates as denominated in ERC-1056. This allows a user to access common identity data from multiple wallets while still:

  • retaining the ability to interact with contracts outside of their identity
  • taking advantage of address-specific permissions established at the application layer of a user's identity.

Trustlessness in the address management function is achieved through a robust permissioning scheme. To add an Associated Address to an Identity, implicit permission from a transaction sender or explicit permission from a signature is required from 1) an address already within the registry and 2) an address to be claimed. Importantly, the transaction need not come from any particular address, as long as permission is established, which allows not only users but third parties (companies, governments, etc.) to bear the overhead of managing identities. To prevent a compromised Associated Address from unilaterally removing other Associated Addresses, it's only possible to remove an Associated Address by transacting or producing a signature from it.

All signatures required in ERC-1484 are designed per the ERC-191 v0 specification. To avoid replay attacks, all signatures must include a timestamp within a rolling lagged window of the current block.timestamp. For more information, see this best practices document (opens new window) in the reference implementation.

# Provider Management

While the protocol allows users to directly call identity management functions, it also aims to be more robust and future-proof by allowing Providers, typically smart contracts, to perform identity management functions on a user's behalf. A Provider set by an Identity can perform address management and resolver management functions by passing a user's EIN in function calls.

# Resolver Management

A Resolver is any smart contract that encodes information which resolves to an Identity. We remain agnostic about the specific information that can be encoded in a resolver and the functionality that this enables. The existence of Resolvers is primarily what makes this ERC an identity protocol rather than an identity application. Resolvers resolve abstract data in smart contracts to an atomic entity, the Identity.

# Recovery

If users lose control over an Associated Address, the Recovery Address provides a fallback mechanism. Upon Identity creation, a Recovery Address is passed as a parameter by the creator. Recovery functionality is triggered in three scenarios:

1. Changing Recovery Address: If a recovery key is lost, an Associated Address/Provider can triggerRecoveryAddressChange/triggerRecoveryAddressChangeFor. To prevent malicious behavior from someone who has gained control of an Associated Address or Provider and is changing the Recovery Address to one under their control, this action triggers a 14 day challenge period during which the old Recovery Address may reject the change by triggering recovery. If the Recovery Address does not reject the change within 14 days, the Recovery Address is changed.

2. Recovery: Recovery occurs when a user recognizes that an Associated Address or the Recovery Address belonging to the user is lost or stolen. In this instance the Recovery Address must call triggerRecovery. This removes all Associated Addresses and Providers from the corresponding Identity and replaces them with an address passed in the function call. The Identity and associated Resolvers maintain integrity. The user is now responsible for adding the appropriate un-compromised addresses back to their Identity.

Importantly, the Recovery Address can be a user-controlled wallet or another address, such as a multisig wallet or smart contract. This allows for arbitrarily sophisticated recovery logic! This includes the potential for recovery to be fully compliant with standards such as DID (opens new window).

3. Destruction The Recovery scheme offers considerable power to a Recovery Address; accordingly, Destruction is a nuclear option to combat malicious control over an Identity when a Recovery Address is compromised. If a malicious actor compromises a user's Recovery Address and triggers recovery, any address removed in the Recovery process can call triggerDestruction within 14 days to permanently disable the Identity. The user would then need to create a new Identity, and would be responsible for engaging in recovery schemes for any identity applications built in the Resolver or Provider layers.

# Alternative Recovery Considerations

We considered many possible alternatives when devising the Recovery process outlined above. We ultimately selected the scheme that was most un-opinionated, modular, and consistent with the philosophy behind the Associated Address, Provider, and Resolver components. Still, we feel that it is important to highlight some of the other recovery options we considered, to provide a rationale as to how we settled on what we did.

High Level Concerns Fundamentally, a Recovery scheme needs to be resilient to a compromised address taking control of a user's Identity. A secondary concern is preventing a compromised address from maliciously destroying a user's identity due to off-chain utility, which is not an optimal scenario, but is strictly better than if they've gained control.

Alternative 1: Nuclear Option This approach would allow any Associated Address to destroy an Identity whenever another Associated Address is compromised. While this may seem severe, we strongly considered it because this ERC is an identity protocol, not an identity application. This means that though a user's compromised Identity is destroyed, they should still have recourse to whatever restoration mechanisms are available in each of their actual identities at the Resolver and/or Provider level. We ultimately dismissed this approach for two main reasons:

  • It is not robust in cases where a user has only one Associated Address
  • It would increase the frequency of recovery requests to identity applications due to its unforgiving nature.

Alternative 2: Unilateral Address Removal via Providers This would allow Associated Addresses/Providers to remove Associated Addresses without a signature from said address. This implementation would allow Providers to include arbitrarily sophisticated schemes for removing a rogue address - for instance, multi-sig requirements, centralized off-chain verification, user controlled master addresses, deferral to a jurisdictional contract, and more. To prevent a compromised Associated Address from simply setting a malicious Provider to remove un-compromised addresses, it would have required a waiting period between when a Provider is set and when they would be able to remove an Associated Address. We dismissed this approach because we felt it placed too high of a burden on Providers. If a Provider offered a sophisticated range of functionality to a user, but post-deployment a threat was found in the Recovery logic of the provider, Provider-specific infrastructure would need to be rebuilt. We also considered including a flag that would allow a user to decide whether or not a Provider may remove Associated Addresses unilaterally. Ultimately, we concluded that only allowing removal of Associated Addresses via the Recovery Address enables equally sophisticated recovery logic while separating the functionality from Providers, leaving less room for users to relinquish control to potentially flawed implementations.

# Rationale

We find that at a protocol layer, identities should not rely on specific claim or attestation structures, but should instead be a part of a trustless framework upon which arbitrarily sophisticated claim and attestation structures may be built.

The main criticism of existing identity solutions is that they're overly restrictive. We aim to limit requirements, keep identities modular and future-proof, and remain un-opinionated regarding any functionality a particular identity component may have. This proposal gives users the option to interact on the blockchain using an robust Identity rather than just an address.

# Implementation

The reference implementation for ERC-1484 may be found in NoahZinsmeister/ERC-1484 (opens new window).

# identityExists

Returns a bool indicating whether or not an Identity denominated by the passed EIN exists.

function identityExists(uint ein) public view returns (bool);
1

# hasIdentity

Returns a bool indicating whether or not the passed _address is associated with an Identity.

function hasIdentity(address _address) public view returns (bool);
1

# getEIN

Returns the EIN associated with the passed _address. Throws if the address is not associated with an EIN.

function getEIN(address _address) public view returns (uint ein);
1

# isAssociatedAddressFor

Returns a bool indicating whether or not the passed _address is associated with the passed EIN.

function isAssociatedAddressFor(uint ein, address _address) public view returns (bool);
1

# isProviderFor

Returns a bool indicating whether or not the passed provider has been set by the passed EIN.

function isProviderFor(uint ein, address provider) public view returns (bool);
1

# isResolverFor

Returns a bool indicating whether or not the passed resolver has been set by the passed EIN.

function isResolverFor(uint ein, address resolver) public view returns (bool);
1

# getIdentity

Returns the recoveryAddress, associatedAddresses, providers and resolvers of the passed EIN.

function getIdentity(uint ein) public view
    returns (
        address recoveryAddress,
        address[] memory associatedAddresses, address[] memory providers, address[] memory resolvers
    );
1
2
3
4
5

# createIdentity

Creates an Identity, setting the msg.sender as the sole Associated Address. Returns the EIN of the new Identity.

function createIdentity(address recoveryAddress, address[] memory providers, address[] memory resolvers)
    public returns (uint ein);
1
2

Triggers event: IdentityCreated

# createIdentityDelegated

Performs the same logic as createIdentity, but can be called by any address. This function requires a signature from the associatedAddress to ensure their consent.

function createIdentityDelegated(
    address recoveryAddress, address associatedAddress, address[] memory providers, address[] memory resolvers,
    uint8 v, bytes32 r, bytes32 s, uint timestamp
)
    public returns (uint ein);
1
2
3
4
5

Triggers event: IdentityCreated

# addAssociatedAddress

Adds the addressToAdd to the EIN of the approvingAddress. The msg.sender must be either of the approvingAddress or the addressToAdd, and the signature must be from the other one.

function addAssociatedAddress(
    address approvingAddress, address addressToAdd, uint8 v, bytes32 r, bytes32 s, uint timestamp
)
    public
1
2
3
4

Triggers event: AssociatedAddressAdded

# addAssociatedAddressDelegated

Adds the addressToAdd to the EIN of the approvingAddress. Requires signatures from both the approvingAddress and the addressToAdd.

function addAssociatedAddressDelegated(
    address approvingAddress, address addressToAdd,
    uint8[2] memory v, bytes32[2] memory r, bytes32[2] memory s, uint[2] memory timestamp
)
    public
1
2
3
4
5

Triggers event: AssociatedAddressAdded

# removeAssociatedAddress

Removes the msg.sender as an Associated Address from its EIN.

function removeAssociatedAddress() public;
1

Triggers event: AssociatedAddressRemoved

# removeAssociatedAddressDelegated

Removes the addressToRemove from its associated EIN. Requires a signature from the addressToRemove.

function removeAssociatedAddressDelegated(address addressToRemove, uint8 v, bytes32 r, bytes32 s, uint timestamp)
    public;
1
2

Triggers event: AssociatedAddressRemoved

# addProviders

Adds an array of Providers to the Identity of the msg.sender.

function addProviders(address[] memory providers) public;
1

Triggers event: ProviderAdded

# addProvidersFor

Performs the same logic as addProviders, but must be called by a Provider.

function addProvidersFor(uint ein, address[] memory providers) public;
1

Triggers event: ProviderAdded

# removeProviders

Removes an array of Providers from the Identity of the msg.sender.

function removeProviders(address[] memory providers) public;
1

Triggers event: ProviderRemoved

# removeProvidersFor

Performs the same logic as removeProviders, but is called by a Provider.

function removeProvidersFor(uint ein, address[] memory providers) public;
1

Triggers event: ProviderRemoved

# addResolvers

Adds an array of Resolvers to the EIN of the msg.sender.

function addResolvers(address[] memory resolvers) public;
1

Triggers event: ResolverAdded

# addResolversFor

Performs the same logic as addResolvers, but must be called by a Provider.

function addResolversFor(uint ein, address[] memory resolvers) public;
1

Triggers event: ResolverAdded

# removeResolvers

Removes an array of Resolvers from the EIN of the msg.sender.

function removeResolvers(address[] memory resolvers) public;
1

Triggers event: ResolverRemoved

# removeResolversFor

Performs the same logic as removeResolvers, but must be called by a Provider.

function removeResolversFor(uint ein, address[] memory resolvers) public;
1

Triggers event: ResolverRemoved

# triggerRecoveryAddressChange

Initiates a change in the current recoveryAddress for the EIN of the msg.sender.

function triggerRecoveryAddressChange(address newRecoveryAddress) public;
1

Triggers event: RecoveryAddressChangeTriggered

# triggerRecoveryAddressChangeFor

Initiates a change in the current recoveryAddress for a given EIN.

function triggerRecoveryAddressChangeFor(uint ein, address newRecoveryAddress) public;
1

Triggers event: RecoveryAddressChangeTriggered

# triggerRecovery

Triggers EIN recovery from the current recoveryAddress, or the old recoveryAddress if changed within the last 2 weeks.

function triggerRecovery(uint ein, address newAssociatedAddress, uint8 v, bytes32 r, bytes32 s, uint timestamp) public;
1

Triggers event: RecoveryTriggered

# triggerDestruction

Triggers destruction of an EIN. This renders the Identity permanently unusable.

function triggerDestruction(uint ein, address[] memory firstChunk, address[] memory lastChunk, bool clearResolvers)
  public;
1
2

Triggers event: IdentityDestroyed

# Events

# IdentityCreated

MUST be triggered when an Identity is created.

event IdentityCreated(
    address indexed initiator, uint indexed ein,
    address recoveryAddress, address associatedAddress, address[] providers, address[] resolvers, bool delegated
);
1
2
3
4

# AssociatedAddressAdded

MUST be triggered when an address is added to an Identity.

event AssociatedAddressAdded(
    address indexed initiator, uint indexed ein, address approvingAddress, address addedAddress, bool delegated
);
1
2
3

# AssociatedAddressRemoved

MUST be triggered when an address is removed from an Identity.

event AssociatedAddressRemoved(address indexed initiator, uint indexed ein, address removedAddress, bool delegated);
1

# ProviderAdded

MUST be triggered when a provider is added to an Identity.

event ProviderAdded(address indexed initiator, uint indexed ein, address provider, bool delegated);
1

# ProviderRemoved

MUST be triggered when a provider is removed.

event ProviderRemoved(address indexed initiator, uint indexed ein, address provider, bool delegated);
1

# ResolverAdded

MUST be triggered when a resolver is added.

event ResolverAdded(address indexed initiator, uint indexed ein, address resolvers, bool delegated);
1

# ResolverRemoved

MUST be triggered when a resolver is removed.

event ResolverRemoved(address indexed initiator, uint indexed ein, address resolvers, bool delegated);
1

# RecoveryAddressChangeTriggered

MUST be triggered when a recovery address change is triggered.

event RecoveryAddressChangeTriggered(
    address indexed initiator, uint indexed ein,
    address oldRecoveryAddress, address newRecoveryAddress, bool delegated
);
1
2
3
4

# RecoveryTriggered

MUST be triggered when recovery is triggered.

event RecoveryTriggered(
    address indexed initiator, uint indexed ein, address[] oldAssociatedAddresses, address newAssociatedAddress
);
1
2
3

# IdentityDestroyed

MUST be triggered when an Identity is destroyed.

event IdentityDestroyed(address indexed initiator, uint indexed ein, address recoveryAddress, bool resolversReset);
1

# Solidity Interface

interface IdentityRegistryInterface {
    function isSigned(address _address, bytes32 messageHash, uint8 v, bytes32 r, bytes32 s)
        external pure returns (bool);

    // Identity View Functions /////////////////////////////////////////////////////////////////////////////////////////
    function identityExists(uint ein) external view returns (bool);
    function hasIdentity(address _address) external view returns (bool);
    function getEIN(address _address) external view returns (uint ein);
    function isAssociatedAddressFor(uint ein, address _address) external view returns (bool);
    function isProviderFor(uint ein, address provider) external view returns (bool);
    function isResolverFor(uint ein, address resolver) external view returns (bool);
    function getIdentity(uint ein) external view returns (
        address recoveryAddress,
        address[] memory associatedAddresses, address[] memory providers, address[] memory resolvers
    );

    // Identity Management Functions ///////////////////////////////////////////////////////////////////////////////////
    function createIdentity(address recoveryAddress, address[] calldata providers, address[] calldata resolvers)
        external returns (uint ein);
    function createIdentityDelegated(
        address recoveryAddress, address associatedAddress, address[] calldata providers, address[] calldata resolvers,
        uint8 v, bytes32 r, bytes32 s, uint timestamp
    ) external returns (uint ein);
    function addAssociatedAddress(
        address approvingAddress, address addressToAdd, uint8 v, bytes32 r, bytes32 s, uint timestamp
    ) external;
    function addAssociatedAddressDelegated(
        address approvingAddress, address addressToAdd,
        uint8[2] calldata v, bytes32[2] calldata r, bytes32[2] calldata s, uint[2] calldata timestamp
    ) external;
    function removeAssociatedAddress() external;
    function removeAssociatedAddressDelegated(address addressToRemove, uint8 v, bytes32 r, bytes32 s, uint timestamp)
        external;
    function addProviders(address[] calldata providers) external;
    function addProvidersFor(uint ein, address[] calldata providers) external;
    function removeProviders(address[] calldata providers) external;
    function removeProvidersFor(uint ein, address[] calldata providers) external;
    function addResolvers(address[] calldata resolvers) external;
    function addResolversFor(uint ein, address[] calldata resolvers) external;
    function removeResolvers(address[] calldata resolvers) external;
    function removeResolversFor(uint ein, address[] calldata resolvers) external;

    // Recovery Management Functions ///////////////////////////////////////////////////////////////////////////////////
    function triggerRecoveryAddressChange(address newRecoveryAddress) external;
    function triggerRecoveryAddressChangeFor(uint ein, address newRecoveryAddress) external;
    function triggerRecovery(uint ein, address newAssociatedAddress, uint8 v, bytes32 r, bytes32 s, uint timestamp)
        external;
    function triggerDestruction(
        uint ein, address[] calldata firstChunk, address[] calldata lastChunk, bool resetResolvers
    ) external;

    // Events //////////////////////////////////////////////////////////////////////////////////////////////////////////
    event IdentityCreated(
        address indexed initiator, uint indexed ein,
        address recoveryAddress, address associatedAddress, address[] providers, address[] resolvers, bool delegated
    );
    event AssociatedAddressAdded(
        address indexed initiator, uint indexed ein, address approvingAddress, address addedAddress
    );
    event AssociatedAddressRemoved(address indexed initiator, uint indexed ein, address removedAddress);
    event ProviderAdded(address indexed initiator, uint indexed ein, address provider, bool delegated);
    event ProviderRemoved(address indexed initiator, uint indexed ein, address provider, bool delegated);
    event ResolverAdded(address indexed initiator, uint indexed ein, address resolvers);
    event ResolverRemoved(address indexed initiator, uint indexed ein, address resolvers);
    event RecoveryAddressChangeTriggered(
        address indexed initiator, uint indexed ein, address oldRecoveryAddress, address newRecoveryAddress
    );
    event RecoveryTriggered(
        address indexed initiator, uint indexed ein, address[] oldAssociatedAddresses, address newAssociatedAddress
    );
    event IdentityDestroyed(address indexed initiator, uint indexed ein, address recoveryAddress, bool resolversReset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# Backwards Compatibility

Identities established under this standard consist of existing Ethereum addresses; accordingly, there are no backwards compatibility issues. Deployed, non-upgradeable smart contracts that wish to become Resolvers for Identities will need to write wrapper contracts that resolve addresses to EIN-denominated Identities.

# Additional References

Copyright and related rights waived via CC0 (opens new window).

▲ Powered by Vercel