>>Hydro’s ERC-1484
Hydro’s ERC-14842019-03-03T19:25:37+00:00

Hydro’s ERC-1484

Hydro's ERC-1484 Logo

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, and can be fully powered by meta-transactions.

Motivation

Emerging identity standards and related frameworks proposed by the Ethereum community (including ERCs/EIPs 7257357801056, 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 enforce a global namespace for Identities, which are individually denominated by Ethereum Identification Numbers (EINs).
  • Identity: A data structure containing all the information relevant to a particular identity. They are denominated by EINs (incrementing uints), which are unique but uninformative.
  • Associated Address: An Ethereum address publicly associated with an Identity. In order for an address to become an Associated Address for an Identity, the Identity must produce signed messages from the candidate address and an existing Associated Address indicating this intent. Identities can remove an Associated Address by producing a signed message indicating intent to disassociate itself from the Identity. Signatures must include a timestamp within a rolling lagged window of the current block.timestamp to prevent replay attacks. An 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 add and remove Associated AddressesProviders, and Resolvers from Identities who have authorized the Provider to act on their behalf. Providers exist to facilitate user adoption, and make 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.
  • Poison Pill: In the event of irrecoverable control of an Identity, the Poison Pill offers a contingency measure to permanently disable the Identity. It removes all Associated Addresses and Providers while preserving the Identity (and optionally, Resolvers). 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 ResolversResolvers recognize identities by any of their Associated Addresses. The protocol allows for an atomic entity, the Identity, to be resolvable to abstract data structures, the Resolvers.

The protocol revolves around claiming an Identity and managing Associated Addresses and Resolvers. Identities delegate much of this responsibility to one or more ProvidersProvider smart contracts or addresses may add and remove Resolvers indiscriminately, but may only add and remove Associated Addresses or other Providers with the appropriate permissions.

Identity Registry

The Identity Registry contains functionality to mint new Identities and for existing Identities to manage their ProvidersAssociated Addresses, 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.

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

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 specification to identity applications built on top of the protocol – for instance, managementactionclaim 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 flexibility 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 signature and verification scheme that requires two signatures — one from an address already within the registry and one from the address to be claimed. Importantly, the transaction need not come from the original user, which allows entities, governments, etc. to bear the overhead of creating a core identity. To prevent a compromised Associated Address from unilaterally removing other Associated Addresses, removal of an Associated Address also requires a signature from the address to be removed.

Provider Management

While the protocol allows for users to directly call identity management functions, it also aims to be more robust and future-proof by allowing arbitrary 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. In order to prevent Identities from adding an initial Provider that does not implement the functionality to add other Providers, identities may add Providers directly from the Identity Registry.

Resolver Management

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 applicationResolvers resolve abstract data in smart contracts to an atomic entity, the Identity.

Recovery

The specification includes a Recovery Address to account for instances when users lose control over an Associated Address. Upon Identity creation, the public Recovery Address is passed as a parameter by a provider. Recovery functionality is triggered in three scenarios:

1. Changing Recovery Address: If a recovery key is lost, a provider can initiateRecoveryAddressChange through a Provider. 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 triggers a 14 day challenge period during which the old Recovery Address may reject the change. If the Recovery Address does not reject the change within 14 days, the Recovery Address is changed. However, during the fourteen day period, the Recovery Address can dispute the change request by calling triggerRecovery.

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 a 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.

3. Poison Pill The Recovery scheme offers considerable power to a Recovery Address; accordingly, the Poison Pill 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 triggerPoisonPill 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 Resolveror 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 AddressProvider, 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 Providers to remove an Associated Address 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.

Importantly, the Recovery Address can be a user-controlled wallet or another address such as a multisig wallet or smart contract. This allows for sophisticated recovery structures that can be compliant, e.g. with standards such as DID.

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 arbitrarily robust Identity rather than just an address.

Implementation

The reference implementation for ERC-1484 may be found in hydrogen-dev/ERC-1484.

identityExists

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

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

hasIdentity

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

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

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);

isAddressFor

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

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

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);

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);

getDetails

Returns the recoveryAddressassociatedAddressesproviders and resolvers of the passed EIN.

function getDetails(uint ein) public view returns (
  address recoveryAddress, address[] associatedAddresses, address[] providers, address[] resolvers
);

mintIdentity

Mints an Identity, setting the Provider to the msg.sender. Returns the EINof the new Identity.

function mintIdentity(address recoveryAddress, address provider, address[] resolvers) public returns (uint ein);

Triggers event: IdentityMinted

mintIdentityDelegated

Performs the same logic as mintIdentity, but is called by a Provider. This function requires a signature for the associatedAddress to confirm their consent.

function mintIdentityDelegated(
    address recoveryAddress, address associatedAddress, address[] resolvers,
    uint8 v, bytes32 r, bytes32 s, uint timestamp
) public returns (uint ein);

Triggers event: IdentityMinted

addAddress

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

function addAddress(
    address approvingAddress, address addressToAdd, uint8[2] v, bytes32[2] r, bytes32[2] s, uint timestamp
) public;

Triggers event: AddressAdded

removeAddress

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

function removeAddress(address addressToRemove, uint8 v, bytes32 r, bytes32 s, uint timestamp) public;

Triggers event: AddressRemoved

addProviders

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

function addProviders(address[] providers) public;

Triggers event: ProviderAdded

addProviders

Adds an array of Providers to the Identity referenced by the passed EIN. This must be called by a Provider.

function addProviders(uint ein, address[] providers) public;

Triggers event: ProviderAdded

removeProviders

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

function removeProviders(address[] providers) public;

Triggers event: ProviderRemoved

removeProviders

Removes an array of Providers to the Identity referenced by the passed EIN. This must be called by a Provider.

function addProviders(uint ein, address[] providers) public;

Triggers event: ProviderRemoved

addResolvers

Adds an array of Resolvers to the passed EIN. This must be called by a Provider.

function addResolvers(uint ein, address[] resolvers) public;

Triggers event: ResolverAdded

removeResolvers

Removes an array of Resolvers from the passed EIN. This must be called by a Provider.

function removeResolvers(uint ein, address[] resolvers) public;

Triggers event: ResolverRemoved

initiateRecoveryAddressChange

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

function initiateRecoveryAddressChange(uint ein, address newRecoveryAddress) public;

Triggers event: RecoveryAddressChangeInitiated

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) public;

Triggers event: RecoveryTriggered

triggerPoisonPill

Triggers the poison pill on an EIN. This renders the Identity permanently unusable.

function triggerPoisonPill(uint ein, address[] firstChunk, address[] lastChunk, bool clearResolvers) public;

Triggers event: Poisoned

Events

IdentityMinted

MUST be triggered when an Identity is minted.

event IdentityMinted(
    uint indexed ein,
    address recoveryAddress,
    address associatedAddress,
    address provider,
    address[] resolvers,
    bool delegated
);

AddressAdded

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

event AddressAdded(uint indexed ein, address addedAddress, address approvingAddress, address provider);

AddressRemoved

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

event AddressRemoved(uint indexed ein, address removedAddress, address provider);

ProviderAdded

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

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

ProviderRemoved

MUST be triggered when a provider is removed.

emit ProviderRemoved(uint indexed ein, address provider, bool delegated);

ResolverAdded

MUST be triggered when a resolver is added.

event ResolverAdded(uint indexed ein, address resolvers, address provider);

ResolverRemoved

MUST be triggered when a resolver is removed.

event ResolverRemoved(uint indexed ein, address resolvers, address provider);

RecoveryAddressChangeInitiated

MUST be triggered when a recovery address change is initiated.

event RecoveryAddressChangeInitiated(uint indexed ein, address oldRecoveryAddress, address newRecoveryAddress);

RecoveryTriggered

MUST be triggered when recovery is initiated.

event RecoveryTriggered(
    uint indexed ein, address recoveryAddress, address[] oldAssociatedAddresses, address newAssociatedAddress
);

Poisoned

MUST be triggered when an Identity is poisoned.

event Poisoned(uint indexed ein, address recoveryAddress, address poisoner, bool resolversCleared);

Solidity Interface

pragma solidity ^0.4.24;
contract ERC1484 {
    event IdentityMinted(
        uint indexed ein,
        address recoveryAddress,
        address associatedAddress,
        address provider,
        address[] resolvers,
        bool delegated
    );
    event AddressAdded(uint indexed ein, address addedAddress, address approvingAddress, address provider);
    event AddressRemoved(uint indexed ein, address removedAddress, address provider);
    event ProviderAdded(uint indexed ein, address provider, bool delegated);
    event ProviderRemoved(uint indexed ein, address provider, bool delegated);
    event ResolverAdded(uint indexed ein, address resolvers, address provider);
    event ResolverRemoved(uint indexed ein, address resolvers, address provider);
    event RecoveryAddressChangeInitiated(uint indexed ein, address oldRecoveryAddress, address newRecoveryAddress);
    event RecoveryTriggered(
        uint indexed ein, address recoveryAddress, address[] oldAssociatedAddresses, address newAssociatedAddress
    );
    event Poisoned(uint indexed ein, address recoveryAddress, address poisoner, bool resolversCleared);
    function identityExists(uint ein) public view returns (bool);
    function hasIdentity(address _address) public view returns (bool);
    function getEIN(address _address) public view returns (uint ein);
    function isAddressFor(uint ein, address _address) public view returns (bool);
    function isProviderFor(uint ein, address provider) public view returns (bool);
    function isResolverFor(uint ein, address resolver) public view returns (bool);
    function getDetails(uint ein) public view returns (
      address recoveryAddress, address[] associatedAddresses, address[] providers, address[] resolvers
    );
    function mintIdentity(address recoveryAddress, address provider, address[] resolvers) public returns (uint ein);
    function mintIdentityDelegated(
        address recoveryAddress, address associatedAddress, address[] resolvers,
        uint8 v, bytes32 r, bytes32 s, uint timestamp
    ) public returns (uint ein);
    function addAddress(
        address approvingAddress, address addressToAdd, uint8[2] v, bytes32[2] r, bytes32[2] s, uint timestamp
    ) public;
    function removeAddress(address addressToRemove, uint8 v, bytes32 r, bytes32 s, uint timestamp) public;
    function addProviders(address[] providers) public;
    function addProviders(uint ein, address[] providers) public;
    function removeProviders(address[] providers) public;
    function removeProviders(uint ein, address[] providers) public;
    function addResolvers(uint ein, address[] resolvers) public;
    function removeResolvers(uint ein, address[] resolvers) public;
    function initiateRecoveryAddressChange(uint ein, address newRecoveryAddress) public;
    function triggerRecovery(uint ein, address newAssociatedAddress, uint8 v, bytes32 r, bytes32 s) public;
    function triggerPoisonPill(uint ein, address[] firstChunk, address[] lastChunk, bool clearResolvers) public;
}

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 Resolversfor Identities will need to write wrapper contracts that resolve addresses to EIN-denominated Identities.

Best Practices

This sections will describe best practices developers must follow when building over ERC-1484 protocol.

Building Providers

A crucial role that Providers play in the ERC-1484 ecosystem is permissioning access to IdentityRegistry functionality. In addition, since Resolvers are also encouraged to allow Providers to interact with their smart contracts on behalf of users (see BuildingResolvers.md), Providers should also be written to accommodate these use cases.

Without further ado, here are some best practices around building a robust resolver

Permissioning

There are several routes a Provider could take when choosing how to let Identities interact with their smart contracts. Providers are recommended to implement one or more of the best practices outlined below.

Before diving in, let’s first sketch out a wrappedAddResolversFor function that we want to call. Say it is wrapping the addResolversFor function of the IdentityRegistry.

function wrappedAddResolversFor(...) {
  ...
  IdentityRegistry.addResolversFor(ein, ...);
}

Obviously, we have to be very careful about who can call this function, and how calls are permissioned to affect the data of Identities.

1. Allow Identities to call functions directly.

The first and simplest option is to allow any associatedAddress of an identity to call wrappedAddResolversFor by simply looking up their EIN from the 1484 registry via getEIN. All further operations can now use that EIN.

function wrappedAddResolversFor(...) {
  uint ein = identityRegistry.getEIN(msg.sender);
  ...
  IdentityRegistry.addResolversFor(ein, ...);
}
2.Allow third parties to submit permission signatures on behalf of Identities

In some cases, users of your Provider will be unable or unwilling to submit and manage transactions on their own behalf. To alleviate this issue, Providers are encouraged to gather signatures from Identities and use these to manage user Identites on their behalf. This technique is know as meta transactions, and a sample Provider implementing this pattern can be found here.

So, we want to allow a Provider to call updateInformation on behalf of an EIN. In order to ensure that allowing someone other than an associatedAddress to call functions pertaining to an EIN is not an anti-pattern, we must:

  • Garner an appropriate permission signature from an associatedAddress.
  • Be thoughtful about the identity of the third parties that may submit signatures. In most cases, public functions will be fine, but onlyOwner or other permission schemes may be appropriate on a case-by-case basis.
  • Take care to avoid replay attacks under such a scheme (see VerifyingSignatures.md for more information)

After the above have been taken care of, a Provider has 3 signature verification options:

Only Address (Recommended)

One solution is to simply accept the address that generated the passed signature as an argument, and get the EIN from that address.

function wrappedAddResolversFor(address approvingAddress, uint8 v, bytes32 r, bytes32 s, ...) public {
  bytes32 messageHash = keccak256(abi.encodePacked(...));
  require(identityRegistry.isSigned(approvingAddress, messageHash, v, r, s);
  uint ein = identityRegistry.getEIN(approvingAddress);
  ...
  IdentityRegistry.addResolversFor(ein, ...);
}

This is a nice solution, because we know that the EIN is automatically valid once we know the signature checks out!

(Not Recommended) Passing the EIN

Another (perhaps more ‘obvious’) solution is to pass the EIN of the user in question.

function wrappedAddResolversFor(uint ein, uint8 v, bytes32 r, bytes32 s, ...) public {
  bytes32 messageHash = keccak256(abi.encodePacked(...));
  address signingAddress = ecrecover(messageHash, v, r, s);
  require(identityRegistry.isAddressFor(ein, signingAddress));
  ...
  IdentityRegistry.addResolversFor(ein, ...);
}

This solution has one big drawback. Recovering the address from the signature, and checking that it belongs to the passed EIN, only works for signatures of the specific form above. It notably does not work for signatures prefixed with the somewhat-standard \x19Ethereum Signed Message:\n32. So, this method cannot be agnostic between prefixed and un-prefixed signatures unless it recovers 2 addresses and checks that either one belongs to the EIN, which is somewhat unwieldy.

(Not Recommended) Both

To be extra safe, one could pass both the EIN and the approvingAddress.

function wrappedAddResolversFor(uint ein, address approvingAddress, uint8 v, bytes32 r, bytes32 s, ...) public {
  require(identityRegistry.isAddressFor(ein, approvingAddress));
  bytes32 messageHash = keccak256(abi.encodePacked(...));
  require(identityRegistry.isSigned(approvingAddress, messageHash, v, r, s);
  ...
  IdentityRegistry.addResolversFor(ein, ...);
}
3. Allow Provider owners to call onlyOwner functions on behalf of Identities

The easiest but most centralized/potentially insecure solution is to simply allow only the owner of the Provider contract to make calls for EINs.

function wrappedAddResolversFor(uint ein, ...) public onlyOwner {
  IdentityRegistry.addResolversFor(ein, ...);
}

Building Resolvers

Because of the open-ended nature of resolvers, it can be daunting to begin writing one. Below are some hints to get you started.

Enforcing isResolver

While not strictly necessary, resolvers are strongly recommended to enforce that calling EINs have set your Resolver via code that looks something like the following:

  require(identityRegistry.isResolverFor(ein, address(this)), "The EIN has not set this resolver.");

While Resolvers are of course free to let anyone they wish interact with their smart contract, they should, in most cases, restrict use to Identities who currently have their address set as a Resolver. This is helpful so that front-ends can know which Resolvers a given entity has set, and can also facilitate inter-Resolver logic.

Permissioning

There are several routes a Resolver could take when choosing how to let Identities interact with their smart contracts. Resolvers are recommended to implement one or more of the best practices outlined below.

Before diving in, let’s first sketch out an updateInformation function that we want to call. Say it modifies data associated with an Identity.

// perform the desired operations on the EIN's Identity
function updateInformation(...) ... {
  ...
}

Obviously, we have to be careful about who can call this function, and how calls are permissioned to affect the data of Identities.

1. Allow Identities to call functions directly

The first and simplest option is to allow any associatedAddress of an identity to call updateInformation by simply looking up their EIN from the 1484 registry via getEIN. All further operations can now use that EIN.

function updateInformation(...) public {
  uint ein = identityRegistry.getEIN(msg.sender);
  ...
}
2. Allow Providers to call functions on behalf of Identities

Some users, though, might not want to interact with our Resolver directly. They might instead prefer to do so through a Provider. In this case, we want to allow a Provider to call updateInformation on behalf of an EIN. For this not to be an anti-pattern, we must:

  • Ensure that any time we receive a call from a Provider on behalf of an EIN, the EIN in question has actually set this Provider. This is easily achieved by code that looks something like the following:
function updateInformation(uint ein, ...) public {
  require(identityRegistry.isProviderFor(ein, msg.sender));
  ...;
}
  • Be convinced that Providers have appropriately permissioned their smart contracts so that we can trust that the calls we receive actually represent the intent of the EIN. (For example, a public function that triggers EIN-specific logic on your Resolver will probably be abused!) To this point, it is often enough to trust that Providers are treating permissions appropriately (see BuildingProviders.md for Provider best practices around this issue). Cases where Providers may not be operating as securely as possibly can be chalked up to user error, and plausibly don’t have to be handled by individual Resolvers. However, if a Resolver wishes to use only a trusted subset of Providers, or utilize the functionality of one particular Provider, they are of course free to do so with code that looks something like the following:
function updateInformation(uint ein, ...) public {
  require(msg.sender == 0x...); // ensure that the calling address is e.g. a pre-determined whitelisted Provider
  require(identityRegistry.isProviderFor(ein, msg.sender));
  ...;
}
3. (Not Recommended) Allow third parties to submit permission signatures on behalf of Identities

This is an anti-pattern for a Resolver, and is not recommended. For cases in which it’s absolutely necessary, refer to the BuildingProviders.md best practices.

4. (Not Recommended) Allow Resolver owners to call onlyOwner functions on behalf of Identities

This is an anti-pattern for a Resolver. For cases in which it’s absolutely necessary, onlyOwner calls may be made like so:

function updateInformation(uint ein, ...) public onlyOwner() {
  ...;
}

Verifying Signatures

Background on Signatures in Ethereum

There has been a lot of discussion around optimal signature schemes in Ethereum. This has included the controversial and confusing inclusion/non-inclusion of the notorious \x19Ethereum Signed Message:\n32 prefix in signatures, discussions over whether to sign data or the hash of data, inconsistent signature implementations across software packages. So, before diving in, some clarifications. As far as this document/ERC-1484 are concerned:

  • Only message hashes should be signed, not raw messages. The downside is that the user is typically signing what looks like gibberish. The upside is that these signatures can be directly verified in the EVM. For example:
  bytes32 messageHash = keccak256(abi.encodePacked(msg.sender, "123", 100))
  address signingAddress = ecrecover(messageHash, v, r, s);  
  • To ensure that users are not signing an RLP-encoded transaction, signatures should be constructed according to the commonly used ERC191 signature scheme. This also ensures that signatures include data specific to the contract being interacted with, i.e. address(this). ERC1484 uses 191 signatures.
  • To improve the UX of signing message hashes, a trusted UI could show users the data which hashes to what they are signing. This would be a big improvement, with the only remaining problem being that the names of the data fields could not be verified (leading to potential confusion/misdirection in cases when users sign data that includes multiple addresses, a to and a from, for example). Recent efforts such as ERC-712) aim to solve this problem by hard-coding values in smart contracts to enable complex front-end signature verification.
  • The ‘official’ Ethereum signed message prefix is not encouraged, should at best be optional, and if included, be prepended to the hash of the data, hashed again, and then signed. For example:
  bytes prefix = "\x19Ethereum Signed Message:\n32";
  bytes32 innerHash = keccak256(abi.encodePacked(...));
  bytes32 messageHash = keccak256(abi.encodePacked(prefix, innerHash));
Preventing Replay Attacks

Often, Providers want to be able to permission calls on their registry with signatures (see BuildingProviders.md). Care must be taken to prevent replay attacks, and one/a combination of the 4 strategies below is highly recommended.

Before diving in, note that care must be taken that signatures cannot be replayed across networks. If a user signs permission with any of the methods below on Rinkeby, this same signature can be used on any other network, including mainnet, if the contract address and user address are the same! The easiest way to prevent this issue, absent including a hard-coded chain id in the signature, is to ensure that common contracts across networks do not share the same address, and to include address(this) in all signatures per ERC191.

Now that that’s out of the way, let’s dive into specific strategies for ensuring that signatures can’t be replayed!

1. Designed signature uniqueness

The technically hardest but conceptually easiest solution is to simply ensure that a given signature can, by design, only be used once. This is ideal for one-time sign-up situations, where what is being signed precludes the signature from ever being used again.

2. Enforced Signature Uniqueness

If uniqueness by design isn’t possible, it can be enforced in two ways:

Timeouts

Every time an address calls a permissioned function, included in the message they sign must be a timestamp that is within some lagged window of the current block’s timestamp.

  • Pros: Does not require on-chain storage or on-chain read/writes.
  • Cons: Signatures can be replayed within short windows, can introduce fragility around transaction timing, block timestamps are slightly manipulable by miners.
Nonces

Every time an address calls a permissioned function, included in the message they sign must be a nonce that increments every call.

  • Pros: Relatively light on gas costs (only ~5k gas to update an existing storage variable)
  • Cons: Requires an on-chain read and write for every transaction. Can introduce fragility around having >1 pending transaction.

The topics discussed above can be seen implemented in the SignatureVerifier contract that the registry inherits from and in code throughout the IdentityRegistry.

Do NOT follow this link or you will be banned from the site!