ERC-1812 - Ethereum Verifiable Claims

Created 2019-03-03
Status Stagnant
Category ERC
Type Standards Track
Authors
Requires

Ethereum Verifiable Claims

Simple Summary

Reusable Verifiable Claims using EIP 712 Signed Typed Data.

Abstract

A new method for Off-Chain Verifiable Claims built on EIP-712. These Claims can be issued by any user with a EIP 712 compatible web3 provider. Claims can be stored off chain and verified on-chain by Solidity Smart Contracts, State Channel Implementations or off-chain libraries.

Motivation

Reusable Off-Chain Verifiable Claims provide an important piece of integrating smart contracts with real world organizational requirements such as meeting regulatory requirements such as KYC, GDPR, Accredited Investor rules etc.

ERC-735 and ERC-780 provide methods of making claims that live on chain. This is useful for some particular use cases, where some claim about an address must be verified on chain.

In most cases though it is both dangerous and in some cases illegal (according to EU GDPR rules for example) to record Identity Claims containing Personal Identifying Information (PII) on an immutable public database such as the Ethereum blockchain.

The W3C Verifiable Claims Data Model and Representations as well as uPorts Verification Message Spec are proposed off-chain solutions.

While built on industry standards such as JSON-LD and JWT neither of them are easy to integrate with the Ethereum ecosystem.

EIP-712 introduces a new method of signing off chain Identity data. This provides both a data format based on Solidity ABI encoding that can easily be parsed on-chain an a new JSON-RPC call that is easily supported by existing Ethereum wallets and Web3 clients.

This format allows reusable off-chain Verifiable Claims to be cheaply issued to users, who can present them when needed.

Prior Art

Verified Identity Claims such as those proposed by uPort and W3C Verifiable Claims Working Group form an important part of building up reusable identity claims.

ERC-735 and ERC-780 provide on-chain storage and lookups of Verifiable Claims.

Specification

Claims

Claims can be generalized like this:

Issuer makes the claim that Subject is something or has some attribute and value.

Claims should be deterministic, in that the same claim signed multiple times by the same signer.

Claims data structure

Each claim should be typed based on its specific use case, which EIP 712 lets us do effortlessly. But there are 3 minimal attributes required of the claims structure.

The basic minimal claim data structure as a Solidity struct:

struct [CLAIM TYPE] {
    address subject;
    uint256 validFrom;
    uint256 validTo;
}

The CLAIM TYPE is the actual name of the claim. While not required, in most cases use the taxonomy developed by schema.org which is also commonly used in other Verifiable Claims formats.

Example claim that issuer knows a subject:

struct Know {
    address subject;
    uint256 validFrom;
    uint256 validTo;
}

Presenting a Verifiable Claim

Verifying Contract

When defining Verifiable Claims formats a Verifying Contract should be created with a public verify() view function. This makes it very easy for other smart contracts to verify a claim correctly.

It also provides a convenient interface for web3 and state channel apps to verify claims securely.

function verifyIssuer(Know memory claim, uint8 v, bytes32 r, bytes32 s) public returns (address) {
    bytes32 digest = keccak256(
      abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        hash(claim)
      )
    );
    require(
        (claim.validFrom >= block.timestamp) && (block.timestamp < claim.validTo)
, "invalid issuance timestamps");
    return ecrecover(digest, v, r, s);
}

Calling a SmartContract function

Verifiable Claims can be presented to a solidity function call as it’s struct together with the v, r and s signature components.

function vouch(Know memory claim, uint8 v, bytes32 r, bytes32 s) public returns (bool) {
    address issuer = verifier.verifyIssuer(claim, v, r, s);
    require(issuer !== '0x0');
    knows[issuer][claim.subject] = block.number;
    return true;
}

Embedding a Verifiable Claim in another Signed Typed Data structure

The Claim struct should be embedded in another struct together with the v, r and s signature parameters.

struct Know {
    address subject;
    uint256 validFrom;
    uint256 validTo;
}

struct VerifiableReference {
    Know delegate;
    uint8 v;
    bytes32 r;
    bytes32 s;
}

struct Introduction {
    address recipient;
    VerifiableReference issuer;
}

Each Verifiable Claim should be individually verified together with the parent Signed Typed Data structure.

Verifiable Claims issued to different EIP 712 Domains can be embedded within each other.

State Channels

This proposal will not show how to use Eth Verifiable Claims as part of a specific State Channel method.

Any State Channel based on EIP712 should be able to include the embeddable Verifiable Claims as part of its protocol. This could be useful for exchanging private Identity Claims between the parties for regulatory reasons, while ultimately not posting them to the blockchain on conclusion of a channel.

Key Delegation

In most simple cases the issuer of a Claim is the signer of the data. There are cases however where signing should be delegated to an intermediary key.

KeyDelegation can be used to implement off chain signing for smart contract based addresses, server side key rotation as well as employee permissions in complex business use cases.

ERC1056 Signing Delegation

ERC-1056 provides a method for addresses to assign delegate signers. One of the primary use cases for this is that a smart contract can allow a key pair to sign on its behalf for a certain period. It also allows server based issuance tools to institute key rotation.

To support this an additional issuer attribute can be added to the Claim Type struct. In this case the verification code should lookup the EthereumDIDRegistry to see if the signer of the data is an allowed signing delegate for the issuer

The following is the minimal struct for a Claim containing an issuer:

struct [CLAIM TYPE] {
    address subject;
  address issuer;
    uint256 validFrom;
    uint256 validTo;
}

If the issuer is specified in the struct In addition to performing the standard ERC712 verification the verification code MUST also verify that the signing address is a valid veriKey delegate for the address specified in the issuer.

registry.validDelegate(issuer, 'veriKey', recoveredAddress)

Embedded Delegation Proof

There may be applications, in particularly where organizations want to allow delegates to issue claims about specific domains and types.

For this purpose instead of the issuer we allow a special claim to be embedded following this same format:

struct Delegate {
    address issuer;
    address subject;
    uint256 validFrom;
    uint256 validTo;
}

struct VerifiableDelegate {
    Delegate delegate;
    uint8 v;
    bytes32 r;
    bytes32 s;
}


struct [CLAIM TYPE] {
    address subject;
    VerifiedDelegate issuer;
    uint256 validFrom;
    uint256 validTo;
}

Delegates should be created for specific EIP 712 Domains and not be reused across Domains.

Implementers of new EIP 712 Domains can add further data to the Delegate struct to allow finer grained application specific rules to it.

Claim Types

Binary Claims

A Binary claim is something that doesn’t have a particular value. It either is issued or not.

Examples: * subject is a Person * subject is my owner (eg. Linking an ethereum account to an owner identity)

Example:

struct Person {
    address issuer;
    address subject;
    uint256 validFrom;
    uint256 validTo;
}

This is exactly the same as the minimal claim above with the CLAIM TYPE set to Person.

Value Claims

Value claims can be used to make a claim about the subject containing a specific readable value.

WARNING: Be very careful about using Value Claims as part of Smart Contract transactions. Identity Claims containing values could be a GDPR violation for the business or developer encouraging a user to post it to a public blockchain.

Examples: * subject’s name is Alice * subjects average account balance is 1234555

Each value should use the value field to indicate the value.

A Name Claim

struct Name {
    address issuer;
    address subject;
    string name;
    uint256 validFrom;
    uint256 validTo;
}

Average Balance

struct AverageBalance {
    address issuer;
    address subject;
    uint256 value;
    uint256 validFrom;
    uint256 validTo;
}

Hashed Claims

Hashed claims can be used to make a claim about the subject containing the hash of a claim value. Hashes should use ethereum standard keccak256 hashing function.

WARNING: Be very careful about using Hashed Claims as part of Smart Contract transactions. Identity Claims containing hashes of known values could be a GDPR violation for the business or developer encouraging a user to post it to a public blockchain.

Examples: - [ ] hash of subject’s name is keccak256(“Alice Torres”) - [ ] hash of subject’s email is keccak256(“alice@example.com”)

Each value should use the keccak256 field to indicate the hashed value. Question. The choice of using this name is that we can easily add support for future algorithms as well as maybe zkSnark proofs.

A Name Claim

struct Name {
    address issuer;
    address subject;
    bytes32 keccak256;
    uint256 validFrom;
    uint256 validTo;
}

Email Claim

struct Email {
    address issuer;
    address subject;
    bytes32 keccak256;
    uint256 validFrom;
    uint256 validTo;
}

EIP 712 Domain

The EIP 712 Domain specifies what kind of message that is to be signed and is used to differentiate between signed data types. The content MUST contain the following:

{
  name: "EIP1???Claim",
  version: 1,
  chainId: 1, // for mainnet
  verifyingContract: 0x // TBD
  salt: ...
}

Full Combined format for EIP 712 signing:

Following the EIP 712 standard we can combine the Claim Type with the EIP 712 Domain and the claim itself (in the message) attribute.

Eg:

  {
    "types": {
      "EIP712Domain": [
        {
          "name": "name",
          "type": "string"
        },
        {
          "name": "version",
          "type": "string"
        },
        {
          "name": "chainId",
          "type": "uint256"
        },
        {
          "name": "verifyingContract",
          "type": "address"
        }
      ],
      "Email": [
        { 
          "name": "subject",
          "type": "address"
        },
        {
          "name": "keccak256",
          "type": "bytes32"
        },
        {
          "name": "validFrom",
          "type": "uint256"
        },
        {
          "name": "validTo",
          "type": "uint256"
        }
      ]
    },
    "primaryType": "Email",
    "domain": {
      "name": "EIP1??? Claim",
      "version": "1",
      "chainId": 1,
      "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
    },
    "message": {
      "subject": "0x5792e817336f41de1d8f54feab4bc200624a1d9d",
      "value": "9c8465d9ae0b0bc167dee7f62880034f59313100a638dcc86a901956ea52e280",
      "validFrom": "0x0000000000000000000000000000000000000000000000000001644b74c2a0",
      "validTo": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
    }
  }

Revocation

Both Issuers and Subjects should be allowed to revoke Verifiable Claims. Revocations can be handled through a simple on-chain registry.

The ultimate rules of who should be able to revoke a claim is determined by the Verifying contract.

The digest used for revocation is the EIP712 Signed Typed Data digest.

contract RevocationRegistry {
  mapping (bytes32 => mapping (address => uint)) public revocations;

  function revoke(bytes32 digest) public returns (bool) {
    revocations[digest][msg.sender] = block.number;
    return true;
  }

  function revoked(address party, bytes32 digest) public view returns (bool) {
    return revocations[digest][party] > 0;
  }
}

A verifying contract can query the Revocation Registry as such:

bytes32 digest = keccak256(
  abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR,
    hash(claim)
  )
);
require(valid(claim.validFrom, claim.validTo), "invalid issuance timestamps");
address issuer = ecrecover(digest, v, r, s);
require(!revocations.revoked(issuer, digest), "claim was revoked by issuer");
require(!revocations.revoked(claim.subject, digest), "claim was revoked by subject");

Creation of Verifiable Claims Domains

Creating specific is Verifiable Claims Domains is out of the scope of this EIP. The Example Code has a few examples.

EIP’s or another process could be used to standardize specific important Domains that are universally useful across the Ethereum world.

Rationale

Signed Typed Data provides a strong foundation for Verifiable Claims that can be used in many different kinds of applications built on both Layer 1 and Layer 2 of Ethereum.

Rationale for using not using a single EIP 712 Domain

EIP712 supports complex types and domains in itself, that we believe are perfect building blocks for building Verifiable Claims for specific purposes.

The Type and Domain of a Claim is itself an important part of a claim and ensures that Verifiable Claims are used for the specific purposes required and not misused.

EIP712 Domains also allow rapid experimentation, allowing taxonomies to be built up by the community.

Test Cases

There is a repo with a few example verifiers and consuming smart contracts written in Solidity:

Example Verifiers * Verifier for very simple IdVerification Verifiable Claims containing minimal Personal Data * Verifier for OwnershipProofs signed by a users wallet

Example Smart Contracts * KYCCoin.sol - Example Token allows reusable IdVerification claims issued by trusted verifiers and users to whitelist their own addresses using OwnershipProofs * ConsortiumAgreement.sol - Example Consortium Agreement smart contract. Consortium Members can issue Delegated Claims to employees or servers to interact on their behalf.

Shared Registries * RevocationRegistry.sol

Copyright

Copyright and related rights waived via CC0.