Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add eth_signTypedData as a standard for machine-verifiable and human-readable typed data signing with Ethereum keys #712

Merged
merged 58 commits into from
Jun 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
21abe25
Add eip-signTypedData
LogvinovLeon Sep 12, 2017
69e486b
Change namespace from personal to eth
LogvinovLeon Oct 16, 2017
75904fc
Change a way schema hash is combined together with data as proposed b…
LogvinovLeon Oct 16, 2017
cfa56eb
Add a note about it being implemented in MetaMask as an experimental …
LogvinovLeon Oct 16, 2017
1dd2fbe
Add signerAddress as a parameter
LogvinovLeon Oct 16, 2017
597cdd7
Add test vectors
LogvinovLeon Oct 16, 2017
e22ba32
Fix an example
LogvinovLeon Oct 16, 2017
12696f8
Missing commas, periods
pirapira Dec 8, 2017
b4e5d70
Address the feedback
LogvinovLeon Jan 17, 2018
ec20713
Add a missing signerAddress parameter in the example
LogvinovLeon Jan 17, 2018
745963a
Change the order of parameters to have an address as a second arg
LogvinovLeon Jan 24, 2018
dab73be
Wrote motivation
recmo Mar 21, 2018
6d7d340
WIP
recmo Mar 21, 2018
c098c14
First draft of specification
recmo Mar 23, 2018
71c5765
Fixes
recmo Apr 9, 2018
0c2297b
Update to new EIP format
recmo Apr 9, 2018
0acbfa1
Assign EIP number
recmo Apr 9, 2018
56f6321
Clarify encoding of short static byte arrays
recmo Apr 9, 2018
ae5f668
Removed Solidity changes
recmo Apr 9, 2018
636f0e4
Fixup
recmo Apr 9, 2018
15cbcad
Fix typos
recmo Apr 9, 2018
b658f07
WIP EIP191
recmo Apr 11, 2018
0a00916
WIP TODO
recmo Apr 11, 2018
d3168c4
WIP Replay attacks
recmo Apr 11, 2018
d117b9b
Fixes the sorted by name example encoding
dekz Apr 6, 2018
5703264
Remove Solidity hash
recmo Apr 12, 2018
121b07a
Added note on replay protection
recmo Apr 12, 2018
73cfdec
Redesign domain separator
recmo Apr 30, 2018
e2cb963
Include images and simple motivation
dekz May 8, 2018
780bb68
Merge branch 'master' into master
dekz May 8, 2018
71d93f9
Fix up EIP metadata formatting
dekz May 18, 2018
7c35e86
Merge pull request #3 from 0xProject/updates/eip-formatting
dekz May 18, 2018
e9387c8
Merge branch 'master' into master
dekz May 18, 2018
08c3741
Merge branch 'master' into master
dekz May 30, 2018
5c307ce
Add domain separator
recmo May 30, 2018
4f74a93
Remove replay attacks from todo list
recmo May 30, 2018
6bf149b
Add Jacob Evans to authors
recmo May 30, 2018
43b838b
Clarify encodeData
recmo May 30, 2018
6d173d9
Rename Message example to Mail
recmo May 30, 2018
f5f80fa
Update mock signing screen
recmo May 30, 2018
0c57087
Rework EIP712Domain
recmo May 30, 2018
154834e
Update Solidity example
recmo May 30, 2018
948aa83
Update Javascript example
recmo May 30, 2018
bcf5c95
Relocate files
recmo May 30, 2018
7b85ba3
Rename DomainSeparator to EIP712Domain (fix)
recmo May 30, 2018
18f0248
Move examples to separate files
recmo Jun 1, 2018
dcc3b33
Remove httpOrigin domain parameter
recmo Jun 1, 2018
d179610
Update JSON-Schema
recmo Jun 1, 2018
daba29f
Add registery of version bytes
recmo Jun 4, 2018
452670e
Add eip712 to eip191 registery
recmo Jun 4, 2018
8efb50a
Add requires header
recmo Jun 4, 2018
ec327ac
Set correct language on all snipets
recmo Jun 4, 2018
bfd3187
GitHub highlighting for Solidity files
recmo Jun 4, 2018
0cabeb3
Update Web3 API specification
recmo Jun 4, 2018
39f049d
Use abi.encode where possible
recmo Jun 4, 2018
f062976
Update JSON-RPC specification
recmo Jun 8, 2018
81c4295
Asset path repo is ethereums
recmo Jun 8, 2018
997af5c
Correctly spelling of registry
recmo Jun 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GitHub highlighting for Solidity files
# See https://github.com/github/linguist/pull/3973#issuecomment-357507741
*.sol linguist-language=Solidity
11 changes: 11 additions & 0 deletions EIPS/eip-191.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ Additionally, `0x19` has been chosen because since ethereum/go-ethereum#2940 , t

Using `0x19` thus makes it possible to extend the scheme by defining a version `0x45` (`E`) to handle these kinds of signatures.

### Registry of version bytes

| Version byte | EIP | Description
| ------------ | -------------- | -----------
| `0x00` | [191][eip-191] | Data with intended validator
| `0x01` | [712][eip-712] | Structured data
| `0x45` | [191][eip-191] | `personal_sign` messages

[eip-191]: https://eips.ethereum.org/EIPS/eip-191
[eip-712]: https://eips.ethereum.org/EIPS/eip-712

### Example

function submitTransactionPreSigned(address destination, uint value, bytes data, uint nonce, uint8 v, bytes32 r, bytes32 s)
Expand Down
446 changes: 446 additions & 0 deletions EIPS/eip-712.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions assets/eip-712/Example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const ethUtil = require('ethereumjs-util');
const abi = require('ethereumjs-abi');
const chai = require('chai');

const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
],
},
primaryType: 'Mail',
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
from: {
name: 'Cow',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
contents: 'Hello, Bob!',
},
};

const types = typedData.types;

// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {
if (found.includes(primaryType)) {
return found;
}
if (types[primaryType] === undefined) {
return found;
}
found.push(primaryType);
for (let field of types[primaryType]) {
for (let dep of dependencies(field.type, found)) {
if (!found.includes(dep)) {
found.push(dep);
}
}
}
return found;
}

function encodeType(primaryType) {
// Get dependencies primary first, then alphabetical
let deps = dependencies(primaryType);
deps = deps.filter(t => t != primaryType);
deps = [primaryType].concat(deps.sort());

// Format as a string with fields
let result = '';
for (let type of deps) {
result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`;
}
return result;
}

function typeHash(primaryType) {
return ethUtil.sha3(encodeType(primaryType));
}

function encodeData(primaryType, data) {
let encTypes = [];
let encValues = [];

// Add typehash
encTypes.push('bytes32');
encValues.push(typeHash(primaryType));

// Add field contents
for (let field of types[primaryType]) {
let value = data[field.name];
if (field.type == 'string' || field.type == 'bytes') {
encTypes.push('bytes32');
value = ethUtil.sha3(value);
encValues.push(value);
} else if (types[field.type] !== undefined) {
encTypes.push('bytes32');
value = ethUtil.sha3(encodeData(field.type, value));
encValues.push(value);
} else if (field.type.lastIndexOf(']') === field.type.length - 1) {
throw 'TODO: Arrays currently unimplemented in encodeData';
} else {
encTypes.push(field.type);
encValues.push(value);
}
}

return abi.rawEncode(encTypes, encValues);
}

function structHash(primaryType, data) {
return ethUtil.sha3(encodeData(primaryType, data));
}

function signHash() {
return ethUtil.sha3(
Buffer.concat([
Buffer.from('1901', 'hex'),
structHash('EIP712Domain', typedData.domain),
structHash(typedData.primaryType, typedData.message),
]),
);
}

const privateKey = ethUtil.sha3('cow');
const address = ethUtil.privateToAddress(privateKey);
const sig = ethUtil.ecsign(signHash(), privateKey);

const expect = chai.expect;
expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal(
'0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
);
expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal(
'0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
);
expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal(
'0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
);
expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal(
'0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
);
expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
expect(sig.v).to.equal(28);
expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');
106 changes: 106 additions & 0 deletions assets/eip-712/Example.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
pragma solidity ^0.4.24;

contract Example {

struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}

struct Person {
string name;
address wallet;
}

struct Mail {
Person from;
Person to;
string contents;
}

bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

bytes32 constant PERSON_TYPEHASH = keccak256(
"Person(string name,address wallet)"
);

bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
);

bytes32 DOMAIN_SEPARATOR;

constructor () public {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: "Ether Mail",
version: '1',
chainId: 1,
// verifyingContract: this
verifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC
}));
}

function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}

function hash(Person person) internal pure returns (bytes32) {
return keccak256(abi.encode(
PERSON_TYPEHASH,
keccak256(bytes(person.name)),
person.wallet
));
}

function hash(Mail mail) internal pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
hash(mail.from),
hash(mail.to),
keccak256(bytes(mail.contents))
));
}

function verify(Mail mail, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
// Note: we need to use `encodePacked` here instead of `encode`.
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(mail)
));
return ecrecover(digest, v, r, s) == mail.from.wallet;
}

function test() public view returns (bool) {
// Example signed message
Mail memory mail = Mail({
from: Person({
name: "Cow",
wallet: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
}),
to: Person({
name: "Bob",
wallet: 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB
}),
contents: "Hello, Bob!"
});
uint8 v = 28;
bytes32 r = 0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d;
bytes32 s = 0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562;

assert(DOMAIN_SEPARATOR == 0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f);
assert(hash(mail) == 0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e);
assert(verify(mail, v, r, s));
return true;
}
}
Binary file added assets/eip-712/eth_sign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/eip-712/eth_signTypedData.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.