This specification describes an Ethereum EIP712 Signature Suite created in 2021 for the Linked Data Proof specification. The Signature Suite utilizes EIP712 signatures.

This is an experimental specification and is undergoing regular revisions. It is not fit for production deployment.

Introduction

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 Linked Data Proofs [[LD-PROOFS]] 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 linked data document matches the EIP712 JSON schema that will be provided to the EIP712 signature function.

Terminology

The following terms are used to describe concepts involved in the generation and verification of the Linked Data Proof signature suite.

signature suite
A specified set of cryptographic primitives typically consisting of a canonicalization algorithm, a message digest algorithm, and a signature algorithm that are bundled together by cryptographers for developers for the purposes of safety and convenience.
canonicalization algorithm
An algorithm that takes an input document that has more than one possible representation and always transforms it into a canonical form. This process is sometimes also called normalization.
message digest algorithm
An algorithm that takes a message, prefferably in some canonical form and produces a cryptographic output called a digest that is often many orders of magnitude smaller than the input message. These algorithms are often 1) very fast, 2) non-reversible, 3) cause the output to change significantly when even one bit of the input message changes, and 4) make it infeasible to find two different inputs for the same output.
types generation algorithm
An algorithm that takes an input document and produces an EIP712 compatible types JSON object containing the types for the input document being signed.
canonical form
The output of applying a canonicalization algorithm to an input document.
signature algorithm
An algorithm that takes an input message and produces an output value where the receiver of the message can mathematically verify that the message has not been modified in transit and came from someone possessing a particular secret.
linked data document
A document comprised of linked data.
linked data proof
An object or mechanism for proving integrity of linked data documents, in the form specified by [[LD-PROOFS]].
EcdsaSecp256k1VerificationKey2019
A type of the verification method for the signature suite EcdsaSecp256k1Signature2019. See also EcdsaSecp256k1VerificationKey2019 in W3C CCG Security Vocabularity.
EcdsaSecp256k1RecoveryMethod2020
A type of the verification method for the signature suite EcdsaSecp256k1RecoverySignature2020. See also EcdsaSecp256k1RecoveryMethod2020 in W3C CCG Security Vocabularity.
JsonWebKey2020
A type of the verification method for the signature suite JSON Web Signature 2020. See also JsonWebKey2020 in W3C CCG Security Vocabularity.
EthereumEip712Signature2021
The type of the linked data proof for the signature suite EthereumEip712Signature2021.
@context
The property of a linked data document used to reference JSON-LD context files. See also the Contexts section of the VC Data Model, and the definition of context in JSON-LD 1.1.

Suite Definition

The Ethereum EIP 712 signature suite 2021 MUST be used in conjunction with the signing and verification algorithms in the Linked Data Proofs [[LD-PROOFS]] 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 Generation

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:

  1. Creates a mapping output from string to TypedDataField[] types, where TypedDataField is an object consisting of two string properties - name and type
  2. Creates an empty array types of TypedDataField to collect all the fields
  3. Canonicalizes the input document using the canonicalization algorithm
  4. If `primaryType` is not provided, set `primaryType = "Document"` else use the provided value.
  5. For each property in the canonicalized input document, iterated in lexicographic order of property name according to RFC 8785 Section 3.2.3, the algorithm checks the type of the value specific to the implementation language.
  6. If the type of the value is a primitive boolean, 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
    1. boolean - maps to bool
    2. number - maps to uint256
    3. string - maps to string
  7. If the type of the value is an array, ensure each element of the array has the same primitive type. Push an object to types with the name set to the property name of the input document and type set to the corresponding EIP712 array type
    1. boolean[] - maps to bool[]
    2. number[] - maps to uint256[]
    3. 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.

  8. If the type of the value is an object, call the function recursively on the inner object, and set the return value equal to _recursiveOutput.
    1. Set _recursiveTypes = _recursiveOutput[primaryType]
    2. Push an object to types with the name set to the property name of the input document and type set to the CapitalCased property name - propertyType
    3. Set output[propertyType] = _recursiveTypes
    4. Loop over _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.
  9. Finally, set output[primaryType] to the types array that was generated. Return output
The following is an example of the autogenerated schema. Given the following input document and no `primaryType`:
          {
            "@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" }
          ]
        }
        

Verification Method

The cryptographic material used to verify a linked data 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:

Proof Representation

The cryptographic material used to represent a linked data proof is called the proof type.

This specification relies on the output of the EIP712 signature function.

Ethereum EIP712 Signature 2021

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"
                }
              }
           }
           

JSON-LD Context

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 .

Linked Data Canonicalization Hash

IRI
https://w3id.org/security/suites/eip712sig-2021#canonicalizationHash
Status
unstable
Domain
sec:Signature
Range
xsd:string

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.

Linked Data Canonicalization Hash Algorithm

Given an input linked data document document which includes a proof property with object value proof, the value for canonicalizationHash is computed as follows:

  1. Convert document from JSON-LD to an RDF dataset, dataset, according to JSON-LD 1.1 Processing Algorithms and API ยง 8.1 Deserialize JSON-LD to RDF Algorithm.
  2. Canonicalize dataset according to [[RDF-DATASET-CANONICALIZATION]].
  3. Serialize dataset in N-Quads format as nquads.
  4. Sort the lines of nquads lexicographically (comparing each N-Quad line as a UTF-8 byte string, sorting in ascending order).
  5. Compute the SHA-256 digest of nquads (including the trailing newline) as digest (encoded as a lowercase hexadecimal value).
  6. Return digest.

Test Vectors

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"
          }
        }
      

Basic Document - Types Generation - No Embedding

This test vector has not yet been verified by more than one independent implementation.

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",
          }
        

Nested Document - TypedData Provided - Embedded EIP712 Properties

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",
          }
        

Nested Document - Types Generation - TypedData Schema as URI

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.

Nested Document - Types Generation - Types Embedded

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.

Security Considerations

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

TODO: We need to add a complete list of security considerations, e.g., what happens if EIP712 JSON schema does not match the message to be signed.

Signing over JSON-LD expanded terms is optional.

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 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 document as a linked data document. When using this signature suite with JSON-LD documents, SHOULD be used.