├── .soliumignore ├── .gitattributes ├── bin ├── gen-doc.sh └── test.sh ├── resources └── diagram.png ├── migrations ├── 1_initial_migration.js └── 1527388907_deploy_contracts.js ├── .soliumrc.json ├── Makefile ├── ethpm.json ├── .travis.yml ├── LICENSE ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── contracts ├── Migrations.sol └── SignatureUtils.sol ├── truffle-config.js ├── .gitignore ├── package.json ├── API.md ├── README.template.md ├── test └── TestSignatureUtils.sol └── README.md /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /bin/gen-doc.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | npx solmd contracts/SignatureUtils.sol --dest API.md 4 | -------------------------------------------------------------------------------- /resources/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsys/solidity-sigutils/HEAD/resources/diagram.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/1527388907_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const SignatureUtils = artifacts.require('SignatureUtils') 2 | 3 | module.exports = async function(deployer) { 4 | await deployer.deploy(SignatureUtils); 5 | }; 6 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install-mythril 2 | install-mythril: 3 | brew tap ethereum/ethereum 4 | brew install leveldb solidity 5 | pip3 install mythril 6 | 7 | .PHONY: myth 8 | myth: 9 | myth -x contracts/SimpleIdentityProvider.sol 10 | myth -x contracts/policies/SimpleWhitelistPolicy.sol 11 | -------------------------------------------------------------------------------- /ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_name": "sigutils", 3 | "version": "1.0.0", 4 | "description": "A solidity library for verifying message multi-signatures", 5 | "authors": [ 6 | "Alexander Kern " 7 | ], 8 | "keywords": [ 9 | "solidity", 10 | "ethereum", 11 | "security", 12 | "signatures" 13 | ], 14 | "license": "Apache-2.0" 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - "8" 7 | cache: 8 | directories: 9 | - node_modules 10 | env: 11 | - 12 | - SOLC_NIGHTLY=true 13 | # - SOLIDITY_COVERAGE=true 14 | matrix: 15 | fast_finish: true 16 | allow_failures: 17 | - env: SOLC_NIGHTLY=true 18 | # - env: SOLIDITY_COVERAGE=true 19 | before_script: 20 | - truffle version 21 | script: 22 | - bash ./bin/test.sh 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Distributed Systems, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const PrivateKeyProvider = require('truffle-privatekey-provider'); 2 | const web3 = require('web3') 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: "localhost", 8 | port: 9545, 9 | network_id: "*" // Match any network id 10 | }, 11 | rinkeby: { 12 | provider: () => new PrivateKeyProvider(process.env.INFURA_PRIVATE_KEY, `https://rinkeby.infura.io/${process.env.INFURA_API_KEY}`), 13 | network_id: 4, 14 | gas: 4612388 // Gas limit used for deploys 15 | }, 16 | mainnet: { 17 | provider: () => new PrivateKeyProvider(process.env.INFURA_PRIVATE_KEY, `https://mainnet.infura.io/${process.env.INFURA_API_KEY}`), 18 | network_id: 0, 19 | gas: 4612388, // Gas limit used for deploys, 20 | gasPrice: 9 * 1000000000 21 | } 22 | }, 23 | mocha: { 24 | reporter: 'eth-gas-reporter', 25 | reporterOptions: { 26 | currency: 'USD' 27 | } 28 | } 29 | 30 | // See 31 | // to customize your Truffle configuration! 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # build 64 | build 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-sigutils", 3 | "version": "1.0.1", 4 | "description": "A solidity library for verifying message multi-signatures", 5 | "main": "index.js", 6 | "repository": "git@github.com:dsys/solidity-sigutils.git", 7 | "author": "alex@cleargraph.com", 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "start": "truffle develop", 11 | "test": "truffle test", 12 | "watch": "watchman-make -p 'contracts/**/*.sol' 'test/**/*.sol' --run 'npm test'", 13 | "compile": "truffle compile", 14 | "migrate": "truffle migrate --reset --compile-all --network development", 15 | "migrate-rinkeby": "truffle migrate --reset --compile-all --network rinkeby", 16 | "migrate-mainnet": "truffle migrate --compile-all --network mainnet", 17 | "lint": "solium -d contracts/", 18 | "fix": "solium --fix -d contracts/", 19 | "gen-docs": "solmd contracts/SignatureUtils.sol --dest API.md && cat README.template.md API.md > README.md" 20 | }, 21 | "devDependencies": { 22 | "eth-gas-reporter": "^0.1.2", 23 | "ganache-cli": "^6.1.0", 24 | "solidity-docgen": "^0.1.0", 25 | "solium": "^1.1.7", 26 | "solmd": "^0.3.0", 27 | "truffle": "^4.1.11", 28 | "truffle-privatekey-provider": "^0.0.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # based on https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/scripts/test.sh 4 | 5 | # Exit script as soon as a command fails. 6 | set -o errexit 7 | 8 | # Executes cleanup function at script exit. 9 | trap cleanup EXIT 10 | 11 | cleanup() { 12 | # Kill the ganache instance that we started (if we started one and if it's still running). 13 | if [ -n "$ganache_pid" ] && ps -p $ganache_pid > /dev/null; then 14 | kill -9 $ganache_pid 15 | fi 16 | } 17 | 18 | if [ "$SOLIDITY_COVERAGE" = true ]; then 19 | ganache_port=8555 20 | else 21 | ganache_port=9545 22 | fi 23 | 24 | ganache_running() { 25 | nc -z localhost "$ganache_port" 26 | } 27 | 28 | start_ganache() { 29 | # We define 10 accounts with balance 1M ether, needed for high-value tests. 30 | local accounts=( 31 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" 32 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" 33 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" 34 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" 35 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" 36 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" 37 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" 38 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" 39 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" 40 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" 41 | ) 42 | 43 | if [ "$SOLIDITY_COVERAGE" = true ]; then 44 | node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null & 45 | else 46 | node_modules/.bin/ganache-cli --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null & 47 | fi 48 | 49 | ganache_pid=$! 50 | } 51 | 52 | if ganache_running; then 53 | echo "Using existing ganache instance" 54 | else 55 | echo "Starting our own ganache instance" 56 | start_ganache 57 | fi 58 | 59 | if [ "$SOLC_NIGHTLY" = true ]; then 60 | echo "Downloading solc nightly" 61 | wget -q https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/soljson-nightly.js -O /tmp/soljson.js && find . -name soljson.js -exec cp /tmp/soljson.js {} \; 62 | fi 63 | 64 | sleep 5 65 | 66 | if [ "$SOLIDITY_COVERAGE" = true ]; then 67 | node_modules/.bin/solidity-coverage 68 | 69 | if [ "$CONTINUOUS_INTEGRATION" = true ]; then 70 | cat coverage/lcov.info | node_modules/.bin/coveralls 71 | fi 72 | else 73 | npm run lint 74 | node_modules/.bin/truffle migrate --compile-all --network development 75 | node_modules/.bin/truffle test --network development "$@" 76 | fi 77 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | * [SignatureUtils](#signatureutils) 2 | * [recoverAddress](#function-recoveraddress) 3 | * [countSignatures](#function-countsignatures) 4 | * [parseSignature](#function-parsesignature) 5 | * [toEthPersonalSignedMessageHash](#function-toethpersonalsignedmessagehash) 6 | * [toEthBytes32SignedMessageHash](#function-toethbytes32signedmessagehash) 7 | * [uintToString](#function-uinttostring) 8 | * [recoverAddresses](#function-recoveraddresses) 9 | 10 | # SignatureUtils 11 | 12 | Alexander Kern 13 | 14 | ## *function* recoverAddress 15 | 16 | SignatureUtils.recoverAddress(_hash, _signatures, _pos) `pure` `1c2a15b8` 17 | 18 | **Recovers an address using a message hash and a signature in a bytes array.** 19 | 20 | 21 | Inputs 22 | 23 | | **type** | **name** | **description** | 24 | |-|-|-| 25 | | *bytes32* | _hash | The signed message hash | 26 | | *bytes* | _signatures | The signatures bytes array | 27 | | *uint256* | _pos | The signature's position in the bytes array (0 indexed) | 28 | 29 | 30 | ## *function* countSignatures 31 | 32 | SignatureUtils.countSignatures(_signatures) `pure` `33ae3ad0` 33 | 34 | **Counts the number of signatures in a signatures bytes array. Returns 0 if the length is invalid.** 35 | 36 | > Signatures are 65 bytes long and are densely packed. 37 | 38 | Inputs 39 | 40 | | **type** | **name** | **description** | 41 | |-|-|-| 42 | | *bytes* | _signatures | The signatures bytes array | 43 | 44 | 45 | ## *function* parseSignature 46 | 47 | SignatureUtils.parseSignature(_signatures, _pos) `pure` `b31d63cc` 48 | 49 | **Extracts the r, s, and v parameters to `ecrecover(...)` from the signature at position `_pos` in a densely packed signatures bytes array.** 50 | 51 | > Based on [OpenZeppelin's ECRecovery](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ECRecovery.sol) 52 | 53 | Inputs 54 | 55 | | **type** | **name** | **description** | 56 | |-|-|-| 57 | | *bytes* | _signatures | The signatures bytes array | 58 | | *uint256* | _pos | The position of the signature in the bytes array (0 indexed) | 59 | 60 | 61 | ## *function* toEthPersonalSignedMessageHash 62 | 63 | SignatureUtils.toEthPersonalSignedMessageHash(_msg) `pure` `d8a40f6b` 64 | 65 | **Converts a byte array to a personal signed message hash (result of `web3.personal.sign(...)`) by concatenating its length.** 66 | 67 | 68 | Inputs 69 | 70 | | **type** | **name** | **description** | 71 | |-|-|-| 72 | | *bytes* | _msg | The bytes array to encrypt | 73 | 74 | 75 | ## *function* toEthBytes32SignedMessageHash 76 | 77 | SignatureUtils.toEthBytes32SignedMessageHash(_msg) `pure` `e5990d20` 78 | 79 | **Converts a bytes32 to an signed message hash.** 80 | 81 | 82 | Inputs 83 | 84 | | **type** | **name** | **description** | 85 | |-|-|-| 86 | | *bytes32* | _msg | The bytes32 message (i.e. keccak256 result) to encrypt | 87 | 88 | 89 | ## *function* uintToString 90 | 91 | SignatureUtils.uintToString(v) `pure` `e9395679` 92 | 93 | **Converts a uint to its decimal string representation.** 94 | 95 | 96 | Inputs 97 | 98 | | **type** | **name** | **description** | 99 | |-|-|-| 100 | | *uint256* | v | The uint to convert | 101 | 102 | 103 | ## *function* recoverAddresses 104 | 105 | SignatureUtils.recoverAddresses(_hash, _signatures) `pure` `f0c8e969` 106 | 107 | **Recovers an array of addresses using a message hash and a signatures bytes array.** 108 | 109 | 110 | Inputs 111 | 112 | | **type** | **name** | **description** | 113 | |-|-|-| 114 | | *bytes32* | _hash | The signed message hash | 115 | | *bytes* | _signatures | The signatures bytes array | 116 | 117 | 118 | --- -------------------------------------------------------------------------------- /README.template.md: -------------------------------------------------------------------------------- 1 | # solidity-sigutils 2 | 3 | ![Travis](https://img.shields.io/travis/dsys/solidity-sigutils.svg) 4 | ![npm](https://img.shields.io/npm/v/solidity-sigutils.svg) 5 | 6 | A solidity library for verifying Ethereum message multi-signatures. 7 | 8 | These utilities make it simple to interact with Ethereum signed messages based on [EIP 191](https://github.com/ethereum/EIPs/issues/191) and [ERC 1077](https://github.com/ethereum/EIPs/pull/1077). They are a building block for Cleargraph's open and decentralized identity system, and can be used for many other purposes in decentralized applications. 9 | 10 | You can sign a transaction/message using your private key by calling [web3.personal.sign()](https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html) using MetaMask, Toshi, or another compatible web3 runtime. All signatures are 65 bytes long with the format `{bytes32 r}{bytes32 s}{uint8 v}`. Multiple signatures are stored densely (no padding) by concatenating them. 11 | 12 | [API Reference](#api-reference) · [Read the announcement.](https://medium.com/dsys/now-open-source-friendly-multi-signatures-for-ethereum-d75ca5a0dc5c) · [See tests for examples.](https://github.com/dsys/solidity-sigutils/blob/master/test/TestSignatureUtils.sol) 13 | 14 | ## Usage 15 | 16 | [![solidity-sigutils flow](https://raw.githubusercontent.com/dsys/solidity-sigutils/master/resources/diagram.png)](#working-with-ethereum-signed-messages) 17 | 18 | [Signed messages](https://medium.com/@angellopozo/ethereum-signing-and-validating-13a2d7cb0ee3) are an increasingly important tool used by decentralized applications. They enable complex access management and delegation patterns and have greater flexibility than raw transactions. Wallet applications such as MetaMask and Toshi support signing transactions via their web3 provider which contracts can verify using `ecrecover()`. 19 | 20 | In the context of identity management, signed messages play a crucial role in building more secure and accessible wallets. Conventionally, anyone with a user's private key has full control over their wallet. This is a security vulnerability: *any malicious actor with access to the user's private key can steal all funds.* 21 | 22 | To improve security, it makes sense to require multi-factor approval from more than one device for some or all transactions. A so-called "multisig identity" often involves a proxy contract that accepts signed transactions from a whitelist of keys. To perform a multisig transaction: 23 | 24 | 1. **Sign:** The user signs a transaction message with their private key from multiple devices. 25 | 2. **Concatenate:** The user concatenates the message signatures into a single multi-signature. 26 | 3. **Verify:** The user sends the transaction message and concatenated signatures to their proxy verifier contract, which verifies that enough valid signatures have been provided using *solidity-sigutils*. 27 | 4. **Execute:** The proxy contract forwards the transaction to the designated contract. 28 | 29 | Signed messages inherit the security of Ethereum's `web3.personal.sign()` and `ecrecover()`. One important benefit over raw transactions is that users can work with trust-less intermediaries without sharing their private keys. For example, signed messages enable complex transaction funding strategies like gas relays which pay for transaction costs on a user's behalf. Additionally, identity contracts may choose to use signed messages to implement advanced functionalities such as account recovery logic that does not rely on centralized authorities. 30 | 31 | ## Installation 32 | 33 | Install using npm: 34 | 35 | $ npm install --save solidity-sigutils 36 | 37 | Then, in your solidity file, use the library: 38 | 39 | ```solidity 40 | import "solidity-sigutils/contracts/SignatureUtils.sol"; 41 | 42 | contract MyContract { 43 | 44 | using SignatureUtils for *; // optional 45 | 46 | function myFunction( 47 | string _personalMessage, 48 | bytes _signatures 49 | ) public returns (address[]) { 50 | // Generate the message hash according to EIP 191 51 | bytes32 hash = SignatureUtils.toEthPersonalSignedMessageHash(_personalMessage); 52 | 53 | // Returns the array of addresses which signed hash using their private key 54 | return SignatureUtils.recoverAddresses(hash, _signatures); 55 | // or use SignatureUtils.recoverAddress(hash, _signatures, 0) for only one signature 56 | } 57 | 58 | } 59 | ``` 60 | 61 | ## Development 62 | 63 | PRs welcome. To install dependencies and start the local development server: 64 | 65 | $ npm install 66 | $ npm run migrate 67 | $ npm start 68 | 69 | ### Testing 70 | 71 | $ npm test 72 | $ npm run watch # requires watchman: brew install watchman 73 | 74 | ### Regenerate documentation 75 | 76 | $ npm run gen-docs 77 | 78 | ### Static analysis with Mythril 79 | 80 | $ make install-mythril 81 | $ make myth 82 | 83 | ## Coda 84 | 85 | Licensed under Apache 2.0. Started at [ETHBuenosAires](https://ethbuenosaires.com/). 86 | 87 | # API Reference 88 | -------------------------------------------------------------------------------- /contracts/SignatureUtils.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | /// @title A library of utilities for (multi)signatures 4 | /// @author Alexander Kern 5 | /// @dev This library can be linked to another Solidity contract to expose signature manipulation functions. 6 | library SignatureUtils { 7 | 8 | /// @notice Converts a bytes32 to an signed message hash. 9 | /// @param _msg The bytes32 message (i.e. keccak256 result) to encrypt 10 | function toEthBytes32SignedMessageHash( 11 | bytes32 _msg 12 | ) 13 | pure 14 | public 15 | returns (bytes32 signHash) 16 | { 17 | signHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _msg)); 18 | } 19 | 20 | /// @notice Converts a byte array to a personal signed message hash (result of `web3.personal.sign(...)`) by concatenating its length. 21 | /// @param _msg The bytes array to encrypt 22 | function toEthPersonalSignedMessageHash( 23 | bytes _msg 24 | ) 25 | pure 26 | public 27 | returns (bytes32 signHash) 28 | { 29 | signHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", uintToString(_msg.length), _msg)); 30 | } 31 | 32 | /// @notice Converts a uint to its decimal string representation. 33 | /// @param v The uint to convert 34 | function uintToString( 35 | uint v 36 | ) 37 | pure 38 | public 39 | returns (string) 40 | { 41 | uint w = v; 42 | bytes32 x; 43 | if (v == 0) { 44 | x = "0"; 45 | } else { 46 | while (w > 0) { 47 | x = bytes32(uint(x) / (2 ** 8)); 48 | x |= bytes32(((w % 10) + 48) * 2 ** (8 * 31)); 49 | w /= 10; 50 | } 51 | } 52 | 53 | bytes memory bytesString = new bytes(32); 54 | uint charCount = 0; 55 | for (uint j = 0; j < 32; j++) { 56 | byte char = byte(bytes32(uint(x) * 2 ** (8 * j))); 57 | if (char != 0) { 58 | bytesString[charCount] = char; 59 | charCount++; 60 | } 61 | } 62 | bytes memory resultBytes = new bytes(charCount); 63 | for (j = 0; j < charCount; j++) { 64 | resultBytes[j] = bytesString[j]; 65 | } 66 | 67 | return string(resultBytes); 68 | } 69 | 70 | /// @notice Extracts the r, s, and v parameters to `ecrecover(...)` from the signature at position `_pos` in a densely packed signatures bytes array. 71 | /// @dev Based on [OpenZeppelin's ECRecovery](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ECRecovery.sol) 72 | /// @param _signatures The signatures bytes array 73 | /// @param _pos The position of the signature in the bytes array (0 indexed) 74 | function parseSignature( 75 | bytes _signatures, 76 | uint _pos 77 | ) 78 | pure 79 | public 80 | returns (uint8 v, bytes32 r, bytes32 s) 81 | { 82 | uint offset = _pos * 65; 83 | // The signature format is a compact form of: 84 | // {bytes32 r}{bytes32 s}{uint8 v} 85 | // Compact means, uint8 is not padded to 32 bytes. 86 | assembly { // solium-disable-line security/no-inline-assembly 87 | r := mload(add(_signatures, add(32, offset))) 88 | s := mload(add(_signatures, add(64, offset))) 89 | // Here we are loading the last 32 bytes, including 31 bytes 90 | // of 's'. There is no 'mload8' to do this. 91 | // 92 | // 'byte' is not working due to the Solidity parser, so lets 93 | // use the second best option, 'and' 94 | v := and(mload(add(_signatures, add(65, offset))), 0xff) 95 | } 96 | 97 | if (v < 27) v += 27; 98 | 99 | require(v == 27 || v == 28); 100 | } 101 | 102 | /// @notice Counts the number of signatures in a signatures bytes array. Returns 0 if the length is invalid. 103 | /// @param _signatures The signatures bytes array 104 | /// @dev Signatures are 65 bytes long and are densely packed. 105 | function countSignatures( 106 | bytes _signatures 107 | ) 108 | pure 109 | public 110 | returns (uint) 111 | { 112 | return _signatures.length % 65 == 0 ? _signatures.length / 65 : 0; 113 | } 114 | 115 | /// @notice Recovers an address using a message hash and a signature in a bytes array. 116 | /// @param _hash The signed message hash 117 | /// @param _signatures The signatures bytes array 118 | /// @param _pos The signature's position in the bytes array (0 indexed) 119 | function recoverAddress( 120 | bytes32 _hash, 121 | bytes _signatures, 122 | uint _pos 123 | ) 124 | pure 125 | public 126 | returns (address) 127 | { 128 | uint8 v; 129 | bytes32 r; 130 | bytes32 s; 131 | (v, r, s) = parseSignature(_signatures, _pos); 132 | return ecrecover(_hash, v, r, s); 133 | } 134 | 135 | /// @notice Recovers an array of addresses using a message hash and a signatures bytes array. 136 | /// @param _hash The signed message hash 137 | /// @param _signatures The signatures bytes array 138 | function recoverAddresses( 139 | bytes32 _hash, 140 | bytes _signatures 141 | ) 142 | pure 143 | public 144 | returns (address[] addresses) 145 | { 146 | uint8 v; 147 | bytes32 r; 148 | bytes32 s; 149 | uint count = countSignatures(_signatures); 150 | addresses = new address[](count); 151 | for (uint i = 0; i < count; i++) { 152 | (v, r, s) = parseSignature(_signatures, i); 153 | addresses[i] = ecrecover(_hash, v, r, s); 154 | } 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /test/TestSignatureUtils.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "truffle/Assert.sol"; 4 | import "../contracts/SignatureUtils.sol"; 5 | 6 | contract TestSignatureUtils { 7 | 8 | function testToSignHash() public { 9 | Assert.equal( 10 | SignatureUtils.toEthBytes32SignedMessageHash(keccak256("foobar")), 11 | 0xdffbffe3efcc512c9b93c698d46fecc2eca5e2836e6bd604aa6d1d3c815f1537, 12 | "The signature should be correct" 13 | ); 14 | 15 | Assert.equal( 16 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 17 | 0xe01d3891dc951e1138097775e7606ec57253579693f93e9c8dc6b455e08b87ba, 18 | "The signature should be correct" 19 | ); 20 | } 21 | 22 | function testParseSignature() public { 23 | uint8 v; 24 | bytes32 r; 25 | bytes32 s; 26 | 27 | (v, r, s) = SignatureUtils.parseSignature(hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451c", 0); 28 | Assert.equal(uint(v), 28, "V should be correct"); 29 | Assert.equal(r, 0xbdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c8, "R should be correct"); 30 | Assert.equal(s, 0x44fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be73345, "S should be correct"); 31 | 32 | (v, r, s) = SignatureUtils.parseSignature(hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c", 0); 33 | Assert.equal(uint(v), 28, "V should be correct"); 34 | Assert.equal(r, 0xbdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c8, "R should be correct"); 35 | Assert.equal(s, 0x44fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be73345, "S should be correct"); 36 | 37 | (v, r, s) = SignatureUtils.parseSignature(hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c", 1); 38 | Assert.equal(uint(v), 28, "V should be correct"); 39 | Assert.equal(r, 0xa3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd, "R should be correct"); 40 | Assert.equal(s, 0x0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea99, "S should be correct"); 41 | } 42 | 43 | function testCountSignatures() public { 44 | uint count = SignatureUtils.countSignatures( 45 | hex"bdaf" 46 | ); 47 | Assert.equal(count, 0, "Signature count should be zero"); 48 | 49 | count = SignatureUtils.countSignatures( 50 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451c" 51 | ); 52 | Assert.equal(count, 1, "Signature count should be zero"); 53 | 54 | count = SignatureUtils.countSignatures( 55 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c" 56 | ); 57 | Assert.equal(count, 2, "Signature count should be correct"); 58 | } 59 | 60 | function testRecoverAddress() public { 61 | address addr = SignatureUtils.recoverAddress( 62 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 63 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451c", 64 | 0 65 | ); 66 | 67 | Assert.equal(addr, 0x730392967a8b81b067459a449C23B2DF9CFC5e28, "Address should be correct"); 68 | 69 | addr = SignatureUtils.recoverAddress( 70 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 71 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c", 72 | 0 73 | ); 74 | 75 | Assert.equal(addr, 0x730392967a8b81b067459a449C23B2DF9CFC5e28, "Address should be correct"); 76 | 77 | addr = SignatureUtils.recoverAddress( 78 | SignatureUtils.toEthPersonalSignedMessageHash("lacroixn"), 79 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c", 80 | 1 81 | ); 82 | 83 | Assert.equal(addr, 0x257555c08E8f81C6855FEE00ACA8Ef41C878D1CE, "Address should be correct"); 84 | 85 | addr = SignatureUtils.recoverAddress( 86 | SignatureUtils.toEthPersonalSignedMessageHash("wrong"), 87 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c", 88 | 0 89 | ); 90 | 91 | Assert.notEqual(addr, 0x730392967a8b81b067459a449C23B2DF9CFC5e28, "Address should be incorrect"); 92 | } 93 | 94 | function testRecoverAddresses() public { 95 | address[] memory addr = SignatureUtils.recoverAddresses( 96 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 97 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451c45d89542a4c55bff03a799749a05c779861b306bb1e5cd45415ac05b666fa7c534ae29a8c7746da72b6d8dee6c69fc52b9c0448ea08ecb54d81cf934dff7372d1c" 98 | ); 99 | 100 | Assert.equal(addr.length, 2, "Address count should be correct"); 101 | Assert.equal(addr[0], 0x730392967a8b81b067459a449C23B2DF9CFC5e28, "Address 0 should be correct"); 102 | Assert.equal(addr[1], 0x257555c08E8f81C6855FEE00ACA8Ef41C878D1CE, "Address 1 should be correct"); 103 | 104 | addr = SignatureUtils.recoverAddresses( 105 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 106 | hex"bdaf2b3265225e00f4df2a3a038bea2907c5f0194311a9449735e477971d54c844fe7bf6590dc048cd005ed6c070ca8eeda91a0636328136b5f36c656be733451ca3bee0d556bf8faa9923d33649b8dc601779c9b8f5bd0e18b243a56a696eacbd0740091f1321e33f929758645fd0bdbeea76017defc6e7742ffcefc73055ea991c" 107 | ); 108 | 109 | Assert.equal(addr.length, 2, "Address count should be correct"); 110 | Assert.equal(addr[0], 0x730392967a8b81b067459a449C23B2DF9CFC5e28, "Address 0 should be correct"); 111 | Assert.notEqual(addr[1], 0x257555c08E8f81C6855FEE00ACA8Ef41C878D1CE, "Address 1 should be incorrect"); 112 | 113 | addr = SignatureUtils.recoverAddresses( 114 | SignatureUtils.toEthPersonalSignedMessageHash("foobar"), 115 | hex"bdaf2b32652c" 116 | ); 117 | Assert.equal(addr.length, 0, "Address count should be zero"); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidity-sigutils 2 | 3 | ![Travis](https://img.shields.io/travis/dsys/solidity-sigutils.svg) 4 | ![npm](https://img.shields.io/npm/v/solidity-sigutils.svg) 5 | 6 | A solidity library for verifying Ethereum message multi-signatures. 7 | 8 | These utilities make it simple to interact with Ethereum signed messages based on [EIP 191](https://github.com/ethereum/EIPs/issues/191) and [ERC 1077](https://github.com/ethereum/EIPs/pull/1077). They are a building block for Cleargraph's open and decentralized identity system, and can be used for many other purposes in decentralized applications. 9 | 10 | You can sign a transaction/message using your private key by calling [web3.personal.sign()](https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html) using MetaMask, Toshi, or another compatible web3 runtime. All signatures are 65 bytes long with the format `{bytes32 r}{bytes32 s}{uint8 v}`. Multiple signatures are stored densely (no padding) by concatenating them. 11 | 12 | [API Reference](#api-reference) · [Read the announcement.](https://medium.com/dsys/now-open-source-friendly-multi-signatures-for-ethereum-d75ca5a0dc5c) · [See tests for examples.](https://github.com/dsys/solidity-sigutils/blob/master/test/TestSignatureUtils.sol) 13 | 14 | ## Usage 15 | 16 | [![solidity-sigutils flow](https://raw.githubusercontent.com/dsys/solidity-sigutils/master/resources/diagram.png)](#working-with-ethereum-signed-messages) 17 | 18 | [Signed messages](https://medium.com/@angellopozo/ethereum-signing-and-validating-13a2d7cb0ee3) are an increasingly important tool used by decentralized applications. They enable complex access management and delegation patterns and have greater flexibility than raw transactions. Wallet applications such as MetaMask and Toshi support signing transactions via their web3 provider which contracts can verify using `ecrecover()`. 19 | 20 | In the context of identity management, signed messages play a crucial role in building more secure and accessible wallets. Conventionally, anyone with a user's private key has full control over their wallet. This is a security vulnerability: *any malicious actor with access to the user's private key can steal all funds.* 21 | 22 | To improve security, it makes sense to require multi-factor approval from more than one device for some or all transactions. A so-called "multisig identity" often involves a proxy contract that accepts signed transactions from a whitelist of keys. To perform a multisig transaction: 23 | 24 | 1. **Sign:** The user signs a transaction message with their private key from multiple devices. 25 | 2. **Concatenate:** The user concatenates the message signatures into a single multi-signature. 26 | 3. **Verify:** The user sends the transaction message and concatenated signatures to their proxy verifier contract, which verifies that enough valid signatures have been provided using *solidity-sigutils*. 27 | 4. **Execute:** The proxy contract forwards the transaction to the designated contract. 28 | 29 | Signed messages inherit the security of Ethereum's `web3.personal.sign()` and `ecrecover()`. One important benefit over raw transactions is that users can work with trust-less intermediaries without sharing their private keys. For example, signed messages enable complex transaction funding strategies like gas relays which pay for transaction costs on a user's behalf. Additionally, identity contracts may choose to use signed messages to implement advanced functionalities such as account recovery logic that does not rely on centralized authorities. 30 | 31 | ## Installation 32 | 33 | Install using npm: 34 | 35 | $ npm install --save solidity-sigutils 36 | 37 | Then, in your solidity file, use the library: 38 | 39 | ```solidity 40 | import "solidity-sigutils/contracts/SignatureUtils.sol"; 41 | 42 | contract MyContract { 43 | 44 | using SignatureUtils for *; // optional 45 | 46 | function myFunction( 47 | string _personalMessage, 48 | bytes _signatures 49 | ) public returns (address[]) { 50 | // Generate the message hash according to EIP 191 51 | bytes32 hash = SignatureUtils.toEthPersonalSignedMessageHash(_personalMessage); 52 | 53 | // Returns the array of addresses which signed hash using their private key 54 | return SignatureUtils.recoverAddresses(hash, _signatures); 55 | // or use SignatureUtils.recoverAddress(hash, _signatures, 0) for only one signature 56 | } 57 | 58 | } 59 | ``` 60 | 61 | ## Development 62 | 63 | PRs welcome. To install dependencies and start the local development server: 64 | 65 | $ npm install 66 | $ npm run migrate 67 | $ npm start 68 | 69 | ### Testing 70 | 71 | $ npm test 72 | $ npm run watch # requires watchman: brew install watchman 73 | 74 | ### Regenerate documentation 75 | 76 | $ npm run gen-docs 77 | 78 | ### Static analysis with Mythril 79 | 80 | $ make install-mythril 81 | $ make myth 82 | 83 | ## Coda 84 | 85 | Licensed under Apache 2.0. Started at [ETHBuenosAires](https://ethbuenosaires.com/). 86 | 87 | # API Reference 88 | * [SignatureUtils](#signatureutils) 89 | * [recoverAddress](#function-recoveraddress) 90 | * [countSignatures](#function-countsignatures) 91 | * [parseSignature](#function-parsesignature) 92 | * [toEthPersonalSignedMessageHash](#function-toethpersonalsignedmessagehash) 93 | * [toEthBytes32SignedMessageHash](#function-toethbytes32signedmessagehash) 94 | * [uintToString](#function-uinttostring) 95 | * [recoverAddresses](#function-recoveraddresses) 96 | 97 | # SignatureUtils 98 | 99 | Alexander Kern 100 | 101 | ## *function* recoverAddress 102 | 103 | SignatureUtils.recoverAddress(_hash, _signatures, _pos) `pure` `1c2a15b8` 104 | 105 | **Recovers an address using a message hash and a signature in a bytes array.** 106 | 107 | 108 | Inputs 109 | 110 | | **type** | **name** | **description** | 111 | |-|-|-| 112 | | *bytes32* | _hash | The signed message hash | 113 | | *bytes* | _signatures | The signatures bytes array | 114 | | *uint256* | _pos | The signature's position in the bytes array (0 indexed) | 115 | 116 | 117 | ## *function* countSignatures 118 | 119 | SignatureUtils.countSignatures(_signatures) `pure` `33ae3ad0` 120 | 121 | **Counts the number of signatures in a signatures bytes array. Returns 0 if the length is invalid.** 122 | 123 | > Signatures are 65 bytes long and are densely packed. 124 | 125 | Inputs 126 | 127 | | **type** | **name** | **description** | 128 | |-|-|-| 129 | | *bytes* | _signatures | The signatures bytes array | 130 | 131 | 132 | ## *function* parseSignature 133 | 134 | SignatureUtils.parseSignature(_signatures, _pos) `pure` `b31d63cc` 135 | 136 | **Extracts the r, s, and v parameters to `ecrecover(...)` from the signature at position `_pos` in a densely packed signatures bytes array.** 137 | 138 | > Based on [OpenZeppelin's ECRecovery](https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ECRecovery.sol) 139 | 140 | Inputs 141 | 142 | | **type** | **name** | **description** | 143 | |-|-|-| 144 | | *bytes* | _signatures | The signatures bytes array | 145 | | *uint256* | _pos | The position of the signature in the bytes array (0 indexed) | 146 | 147 | 148 | ## *function* toEthPersonalSignedMessageHash 149 | 150 | SignatureUtils.toEthPersonalSignedMessageHash(_msg) `pure` `d8a40f6b` 151 | 152 | **Converts a byte array to a personal signed message hash (result of `web3.personal.sign(...)`) by concatenating its length.** 153 | 154 | 155 | Inputs 156 | 157 | | **type** | **name** | **description** | 158 | |-|-|-| 159 | | *bytes* | _msg | The bytes array to encrypt | 160 | 161 | 162 | ## *function* toEthBytes32SignedMessageHash 163 | 164 | SignatureUtils.toEthBytes32SignedMessageHash(_msg) `pure` `e5990d20` 165 | 166 | **Converts a bytes32 to an signed message hash.** 167 | 168 | 169 | Inputs 170 | 171 | | **type** | **name** | **description** | 172 | |-|-|-| 173 | | *bytes32* | _msg | The bytes32 message (i.e. keccak256 result) to encrypt | 174 | 175 | 176 | ## *function* uintToString 177 | 178 | SignatureUtils.uintToString(v) `pure` `e9395679` 179 | 180 | **Converts a uint to its decimal string representation.** 181 | 182 | 183 | Inputs 184 | 185 | | **type** | **name** | **description** | 186 | |-|-|-| 187 | | *uint256* | v | The uint to convert | 188 | 189 | 190 | ## *function* recoverAddresses 191 | 192 | SignatureUtils.recoverAddresses(_hash, _signatures) `pure` `f0c8e969` 193 | 194 | **Recovers an array of addresses using a message hash and a signatures bytes array.** 195 | 196 | 197 | Inputs 198 | 199 | | **type** | **name** | **description** | 200 | |-|-|-| 201 | | *bytes32* | _hash | The signed message hash | 202 | | *bytes* | _signatures | The signatures bytes array | 203 | 204 | 205 | --- --------------------------------------------------------------------------------