This specification describes an Ethereum EIP712 Signature Suite created in 2021 for use with Verifiable Credentials and data integrity proofs. This signature suite utilizes [[EIP712]] signatures.
This is an experimental specification and is undergoing regular revisions. It is not fit for production deployment.
This specification defines a cryptographic suite for the purpose of creating, verifying proofs for EIP712 signatures in conformance with the Ethereum Improvement Proposal #712 [[EIP712]] as well as the Data Integrity [[DATA-INTEGRITY]] specification.
In general the suites implicitly follows the JCS Algorithm [[JCS]] in the steps it follows to transform an input document into its canonical form. The canonical representation is then provided to the EIP712 signature function. Implementations that auto-generate a `types` object will also need to follow JCS carefully in the process of that generation.
A signature scheme consists of a hashing algorithm and a signing algorithm. The signing algorithm of choice in Ethereum is secp256k1. The hashing algorithm of choice is keccak256.
EIP712 defines a standard API for Web3 Provider (e.g., wallets) to generate signatures over human-readable data where the signature can be verified either by a Smart Contract on the Ethereum Blockchain, or completely offchain.
The rational is to use existing Web3 Providers and their secure key management system to produce signatures that are compliant with the JSON-LD and more specifically, the Linked Data Signatures (LDS) data model.
Since the EIP712 signature function relies on JSON schemas, implementers need to ensure that the unsigned data document matches the EIP712 JSON schema that will be provided to the EIP712 signature function.
The following terms are used to describe concepts involved in the generation and verification of the Linked Data Proof signature suite.
types
JSON object
containing the types for the input document being signed.
type
of the verification method for the signature
suite EcdsaSecp256k1Signature2019.
See also EcdsaSecp256k1VerificationKey2019
in W3C CCG Security Vocabularity.
type
of the verification method for the signature
suite EcdsaSecp256k1RecoverySignature2020.
See also EcdsaSecp256k1RecoveryMethod2020
in W3C CCG Security Vocabularity.
type
of the verification method for the signature
suite JSON Web Signature 2020.
See also JsonWebKey2020
in W3C CCG Security Vocabularity.
type
of the data integrity proof
for the signature suite EthereumEip712Signature2021.
The Ethereum EIP 712 signature suite 2021 MUST be used in conjunction with the signing and verification algorithms in the Data Integrity [[DATA-INTEGRITY]] specification. The suite consists of the following algorithms:
Parameter | Value | Specification |
---|---|---|
canonicalization algorithm (if automatically-generated `types` object is present) |
JCS | [[JCS]] |
message digest algorithm | EIP712 uses Keccak-256 | [[EIP712]] |
signature algorithm | EIP712 uses ECDSA K-256 | [[EIP712]] |
To generate the EIP712 signature, EIP712 requires TypedData
which is a JSON object
containing type information, domain separator parameters and the message object.
TypedData
MUST be a JSON object according to the EIP712 specification and
contains properties types
, domain
, primaryType
and
message
. The types
property of TypedData
can be generated by the types generation algorithm
if not provided as input. Note: in the case of this generation, elements
will be normalized according to [JCS].
types
MUST be a JSON array with at least two entries. The first entry refers to the
EIP712Domain
property that contains the JSON schema according to the EIP712
specification. Remaining entries MUST be the JSON schemas of the message to be
signed in the EIP712 format. types
MUST contain a property proof
of type
Proof
which contains the JSON schema for all properties of the final
data integrity proof
proof
property
that need to be signed.
message
MUST be the
unsigned data document
that contains the message to be signed. The message MUST
contain a proof
property with values set to the values of
the properties in the resulting
data-cite="DATA-INTEGRITY#dfn-signed-data-document">signed data document's
proof
property that are expected to be
signed.
domain
MUST have the value as defined in EIP712
. If domain
is not
provided, a default object is applied with the only property being name
with value
EthereumEip712Signature2021
primaryType
MUST have the value as defined in EIP712
. primaryType
represents the top-level type of the object in the EIP712 message
but does not have to correspond
to any of
the types in the message
.
If TypedData
's types
object is not provided to the signature suite, the suite MUST
generate the JSON object by
inferring types from the input document, and optionally a provided `primaryType`.
In case of ambiguous types, the algorithm SHOULD defer to using the most liberal option. For example, a number
should be inferred as the uint256
type even if that specific number can fit in a uint8
type.
The types generation algorithm is defined as follows:
output
from string
to TypedDataField[]
types, where
TypedDataField
is an object consisting of two string properties - name
and
type
types
of TypedDataField
to collect all the fieldsboolean
, number
or string
,
push an object to types
with the name
set to the property name of the input
document, and type
set to the corresponding EIP712 primitive type
boolean
- maps to bool
number
- maps to uint256
string
- maps to string
types
with the name
set to the property name of the input document and
type
set to the corresponding EIP712 array type
boolean[]
- maps to bool[]
number[]
- maps to uint256[]
string[]
- maps to string[]
WARNING: The current algorithm definition does not support auto generating types for arrays of structs. We need to work on that.
_recursiveOutput
.
_recursiveTypes = _recursiveOutput[primaryType]
types
with the name
set to the property name of the input
document and type
set to the CapitalCased property name - propertyType
output[propertyType] = _recursiveTypes
_recursiveOutput
, and if any keys other than primaryType
are
present,
add them directly to output
. If any such key already has an entry in output
,
raise an error.output[primaryType]
to the types
array that was generated. Return
output
{ "@context": ["https://schema.org", "https://w3id.org/security/v2"], "@type": "Person", "name": { "first": "Jane", "last": "Doe" }, "otherData": { "jobTitle": "Professor", "school": "University of ExampleLand" }, "telephone": "(425) 123-4567", "email": "jane.doe@example.com" }It will generate the following schema:
{ "Document": [ { "name": "@context", type: "string[]" }, { "name": "@type", type: "string" }, { "name": "email", type: "string" }, { "name": "name", type: "Name" }, { "name": "otherData", type: "OtherData" }, { "name": "telephone", type: "string" } ], "Name": [ { "name": "first", type: "string" }, { "name": "last", type: "string" } ], "OtherData": [ { "name": "jobTitle", type: "string" }, { "name": "school", type: "string" } ] }
The cryptographic material used to verify a data integrity proof is called the verification method.
This signature suite does not define a new verification method type. The following verification method types can be used with Ethereum EIP712 Signature 2021:
The cryptographic material used to represent a data integrity proof is called the proof type.
This specification relies on the output of the EIP712 signature function.
The verificationMethod
property of the proof SHOULD be a URI. Dereferencing
the verificationMethod
SHOULD result in an object of type
EcdsaSecp256k1VerificationKey2019
,
EcdsaSecp256k1RecoveryMethod2020
, or
JsonWebKey2020
.
If the dereferenced verification method object is of type
JsonWebKey2020
, it MUST contain a property
publicKeyJwk
,
containing a secp256k1 public key represented as a JSON Web Key (JWK)
according to
RFC 8812 Section 3.1.
The type
property of the proof MUST be EthereumEip712Signature2021
.
The created
property of the proof MUST be an [ISO_8601] formated date string.
The proofPurpose
property of the proof MUST be a string, and SHOULD match the
verification relationship expressed by the verification method controller.
The proofValue
property of the proof MUST be the hex encoded output of the
EIP712 signature function according [EIP712].
The eip712
property MUST contain meta-information about the signature generation process
that can be used when the signature is verified. It MUST contain the following properties:
types
MUST be a URI that results in an object that contains the JSON schema
that describes the message to be signed according to EIP712, or an object that contains the JSON schema
itself.
domain
MUST be the domain
property of the EIP712 TypedData
object.
primaryType
MUST be the primaryType
property of the EIP712 TypedData
object.
The canonicalizationHash
property of the proof, if
present, MUST contain a value computed from the input document and
proof as specified in
Linked Data Canonicalization Hash.
The following is a non-normative example of an EthereumEip712Signature2021
proof:
{ "proof": { "type": "EthereumEip712Signature2021", "created": "2019-12-11T03:50:55Z", "proofPurpose": "assertionMethod", "proofValue": "0xc565d38982e1a5004efb5ee390fba0a08bb5e72b3f3e91094c66bc395c324f785425d58d5c1a601372d9c16164e380c63e89f1e0ea95fdefdf7b2854c4f938e81b", "verificationMethod": "did:example:aaaabbbb#issuerKey-1", "eip712": { "types": "https://example.com/schemas/v1", "primaryType": "VerifiableCredential" } } }
The @context
property of a JSON-LD document using
this signature suite SHOULD include the following URI string:
https://w3id.org/security/suites/eip712sig-2021/v1
.
To ensure the integrity of a JSON-LD document's RDF data model, use
the proof canonicalizationHash
property described in
.
https://w3id.org/security/suites/eip712sig-2021#canonicalizationHash
Linked data proof suites conventionally create a proof
verification hash
for signing and verifying the document and proof as JSON-LD/RDF, using JSON-LD
context expansion,
RDF Deserialization,
and [[RDF-DATASET-CANONICALIZATION]]. This signature suite instead
signs a structured TypedData
object derived from the
JSON-LD document, without JSON-LD or RDF processing. For use cases
where JSON-LD and RDF processing is needed, this document specifies an
optional property of a
EthereumEip712Signature2021
proof, canonicalizationHash
, for a verification hash
computed with JSON-LD/RDF processing and URDNA2015. Signing over this
property and verifying it during proof verification secures the RDF
data model of the signed document. Construction of the
canonicalizationHash
value is defined in
.
The input document provided to
MUST be the document
as it would be returned after proof creation (with the
eip712
proof property) except without the
proofValue
proof property.
When preparing the TypedData
structure for signing or
verification, the canonicalizationHash
value MUST be
included in the message's proof object.
Note that although the proof object in the input document provided to
includes the eip712
property, the proof object in the
message in the TypedData
structure does not include the
eip712
property. That is because the properties of the
eip712
object are instead included in non-message parts
of the TypedData
structure; but for the purpose of linked
data integrity, it is desired to include the eip712
properties as they would be in the returned document.
After signing, proof properties
eip712
, proofValue
and
canonicalizationHash
are inserted into the proof object
before returning the document.
Given an input unsigned data document document which includes a
proof
property with object value proof,
the value for canonicalizationHash
is computed as
follows:
The following test vectors are provided to assist implementers. Some of the given test vectors specify
inputOptions
which are options to be passed when creating a proof.
These can include options specifying the domain
, types
, primaryType
,
verificationMethod
, date
, embedAsURI
, and embed
.
The following is an example Ethereum-compatible hexadecimal private key, and corresponding did:pkh
verificationMethod
that can be used to assist with test vectors:
{ "privateKey": "0x149195a4059ac8cafe2d56fc612f613b6b18b9265a73143c9f6d7cfbbed76b7e", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId" }
The following are some example input documents that will be provided to the Ethereum EIP712 Signature Suite, to generate various type of output proofs:
{ "testBasicDocument": { "@context": ["https://schema.org", "https://w3id.org/security/v2"], "@type": "Person", "firstName": "Jane", "lastName": "Does", "jobTitle": "Professor", "telephone": "(425) 123-4567", "email": "jane.doe@example.com" }, "testNestedDocument": { "@context": ["https://schema.org", "https://w3id.org/security/v2"], "@type": "Person", "data": { "name": { "firstName": "John", "lastName": "Doe" }, "job": { "jobTitle": "Professor", "employer": "University of Waterloo" } }, "telephone": "(425) 123-4567" } }
With the following inputOptions
provided to the signature suite along with the
testBasicDocument
input document:
{ "date": "2021-08-30T13:28:02Z", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", "domain": { "name": "Test" } }
The following is the resulting proof
object:
{ "created": "2021-08-30T13:28:02Z", "proofPurpose": "assertionMethod", "proofValue": "0xbbdf2914c7572185bbc263e066dfb43f3136e4441fddb3fe3ea4541bbf7fd1f00d8e5af3ce4fbb1f2ebd5256f39b22cef7f285189df2976ea0c385c77f0a42791b", "type": "EthereumEip712Signature2021", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", }
With the following inputOptions
provided to the signature suite along with the
testNestedDocument
input document:
{ "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", "types": { "Data": [ { "name": "job", "type": "Job" }, { "name": "name", "type": "Name" } ], "Document": [ { "name": "@context", "type": "string[]" }, { "name": "@type", "type": "string" }, { "name": "data", "type": "Data" }, { "name": "telephone", "type": "string" }, { "name": "proof", "type": "Proof" } ], "Job": [ { "name": "employer", "type": "string" }, { "name": "jobTitle", "type": "string" } ], "Proof": [ { "name": "created", "type": "string" }, { "name": "proofPurpose", "type": "string" }, { "name": "type", "type": "string" }, { "name": "verificationMethod", "type": "string" } ], "Name": [ { "name": "firstName", "type": "string" }, { "name": "lastName", "type": "string" } ] }, "domain": { "name": "Test" }, "date": "2021-08-30T13:28:02Z", "embed": true }
The following is the resulting proof
object:
{ "created": "2021-08-30T13:28:02Z", "eip712": { "domain": { "name": "Test", }, "primaryType": "Document", "types": { "Data": [ { "name": "job", "type": "Job", }, { "name": "name", "type": "Name", }, ], "Document": [ { "name": "@context", "type": "string[]", }, { "name": "@type", "type": "string", }, { "name": "data", "type": "Data", }, { "name": "telephone", "type": "string", }, { "name": "proof", "type": "Proof", }, ], "Job": [ { "name": "employer", "type": "string", }, { "name": "jobTitle", "type": "string", }, ], "Name": [ { "name": "firstName", "type": "string", }, { "name": "lastName", "type": "string", }, ], "Proof": [ { "name": "created", "type": "string", }, { "name": "proofPurpose", "type": "string", }, { "name": "type", "type": "string", }, { "name": "verificationMethod", "type": "string", }, ], }, }, "proofPurpose": "assertionMethod", "proofValue": "0xcf5844be1f1a5c1a083565d492ab4bee93bd0e24a4573bd8ff47331ad225b9d11c4831aade8d071f4abb8c9e266aaaf30612c582c2bc8f082b8788448895fa4a1b", "type": "EthereumEip712Signature2021", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", }
With the following inputOptions
provided to the signature suite along with the
testNestedDocument
input document:
{ "embedAsURI": true, "date": "2021-08-30T13:28:02Z", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", "domain": { "name": "Test" } }
The following is the resulting proof
object:
{ "created": "2021-08-30T13:28:02Z", "proofPurpose": "assertionMethod", "type": "EthereumEip712Signature2021", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", "proofValue": "0x8327ad5e4b2426eac7626400c75f000c3e04caf2a863b888988e4e85533880183d4b9cc6870183e55dabfa96b9486624f45ef849bb146257d123f297a2dbf3a11c", "eip712": { "domain": { "name": "Test" }, "types": "https://example.org/types.json", "primaryType": "Document" } }
Dereferencing the types
URI should result in the following object:
{ "Data": [ { "name": "job", "type": "Job" }, { "name": "name", "type": "Name" } ], "Job": [ { "name": "employer", "type": "string" }, { "name": "jobTitle", "type": "string" } ], "Name": [ { "name": "firstName", "type": "string" }, { "name": "lastName", "type": "string" } ], "Document": [ { "name": "@context", "type": "string[]" }, { "name": "@type", "type": "string" }, { "name": "data", "type": "Data" }, { "name": "proof", "type": "Proof" }, { "name": "telephone", "type": "string" } ], "Proof": [ { "name": "created", "type": "string" }, { "name": "proofPurpose", "type": "string" }, { "name": "type", "type": "string" }, { "name": "verificationMethod", "type": "string" } ] }
The example URI provided above is not a real URI that would dereference, but outlines the expected behaviour.
With the following inputOptions
provided to the signature suite along with the
testNestedDocument
input document:
{ "date": "2021-08-30T13:28:02Z", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", "domain": { "name": "Test" }, "embed": true }
The following is the resulting proof
object:
{ "created": "2021-08-30T13:28:02Z", "eip712": { "domain": { "name": "EthereumEip712Signature2021", }, "primaryType": "Document", "types": { "Data": [ { "name": "job", "type": "Job", }, { "name": "name", "type": "Name", }, ], "Document": [ { "name": "@context", "type": "string[]", }, { "name": "@type", "type": "string", }, { "name": "data", "type": "Data", }, { "name": "proof", "type": "Proof", }, { "name": "telephone", "type": "string", }, ], "Job": [ { "name": "employer", "type": "string", }, { "name": "jobTitle", "type": "string", }, ], "Name": [ { "name": "firstName", "type": "string", }, { "name": "lastName", "type": "string", }, ], "Proof": [ { "name": "created", "type": "string", }, { "name": "proofPurpose", "type": "string", }, { "name": "type", "type": "string", }, { "name": "verificationMethod", "type": "string", }, ], }, }, "proofPurpose": "assertionMethod", "proofValue": "0x7d57ace2be9cc3944aac023f66130935e489bbb1c9b469a4a5b4f16e5c298b57291bc80d52c6f873b11f4bf45c97c6e2506419af7506eaac5374e9ed381fcc5b1b", "type": "EthereumEip712Signature2021", "verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId", }
A conforming document is any concrete expression of the data model that complies with the normative statements in this specification. Specifically, all relevant normative statements in Sections and of this document MUST be enforced.
A conforming processor is any algorithm realized as software and/or hardware that generates or consumes a conforming document. Conforming processors MUST produce errors when non-conforming documents are consumed.
This document also contains examples that contain JSON and JSON-LD
content. Some of these examples contain characters that are invalid
JSON, such as inline comments (//
) and the use of ellipsis
(...
) to denote information that adds little value to the
example. Implementers are cautioned to remove this content if they
desire to use the information as valid JSON or JSON-LD.
The following section describes security considerations that developers implementing this specification should be aware of in order to create secure software.
This specification relies on JCS, which is used to generate the `types` object deterministically if not provided. please review [[JCS]] for details.
This specification relies on EIP712, please review [[EIP712]].
Linked data signatures suites typically use JSON-LD to RDF Deserialization, RDF Dataset Canonicalization and serialization as N-Quads, as part of constructing the data to sign. This signature suite differs by instead signing based on the JSON document structure more directly, without conversion to RDF. This is supposed to enable a more human-readable signing input. However, it means that information from the JSON-LD context is not included in the signing input that otherwise would be. If the referenced JSON-LD context files are changed, changing the definition of some terms, it is possible that the proof signature may remain valid but the underlying JSON-LD/RDF data could be different.
In some cases, this could create security issues if unmitigated, because the semantic disambiguation information is not included in the signing method's integrity guarantees. One common method for additionally securing those linked data documents is to add an additional, but optional, "semantic integrity" hash to the proof object before URDNA canonicalization. This digest then acts as a kind of checksum that the verifier can use to check the integrity of the expanded context. The algorithms for generating this digest (and implicitly, the algorithm for how to verify it) can be found in the Linked Data Proof specification.
To prevent this kind of issue, this specification defines a
mechanism for including a cryptographic digest of the RDF data in the
proof property, which is included in the signing input:
(canonicalizationHash
proof property).
While it is not expected that EIP-712 signers will be able to natively
understand this canonicalization hash, signers and verifiers of this
proof suite using JSON-LD processing can use it to ensure the integrity
of the signed data document
as a linked data document. When using this signature
suite with JSON-LD documents, SHOULD be used.