Appearance
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.
subject
the subject of the claim as anaddress
(who the claim is about)validFrom
the time in seconds encoded as auint256
of start of validity of claim. In most cases this would be the time of issuance, but some claims may be valid in the future or past.validTo
the time in seconds encoded as auint256
of when the validity of the claim expires. If you intend for the claim not to expire use0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
.
The basic minimal claim data structure as a Solidity struct:
solidity
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:
solidity
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.
solidity
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.
solidity
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.
solidity
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:
solidity
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.
solidity
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:
solidity
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:
solidity
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
solidity
struct Name {
address issuer;
address subject;
string name;
uint256 validFrom;
uint256 validTo;
}
Average Balance
solidity
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
solidity
struct Name {
address issuer;
address subject;
bytes32 keccak256;
uint256 validFrom;
uint256 validTo;
}
Email Claim
solidity
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:
solidity
{
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:
solidity
{
"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.
solidity
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:
solidity
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
Copyright
Copyright and related rights waived via CC0.