Reusable Verifiable Claims using EIP 712 Signed Typed Data.
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.
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.
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.
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.
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 an address
(who the claim is about)validFrom
the time in seconds encoded as a uint256
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 a uint256
of when the validity of the claim expires. If you intend for the claim not to expire use 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
.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;
}
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);
}
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;
}
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.
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.
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.
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)
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.
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 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 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;
}
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: ...
}
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"
}
}
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");
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.
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.
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.
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 and related rights waived via CC0.