From ba5da0dfec86d33bb787c2b510b535eb32ef9650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 17 Jun 2019 14:46:55 +0200 Subject: [PATCH] Verify EIP191 signatures in SignatureVerifier Not really an EIP191-compliant method, as it only supports version E, but this is what we currently use in nucypher --- .../contracts/lib/SignatureVerifier.sol | 56 +++++++++++++++++++ .../eth/contracts/contracts/LibTestSet.sol | 22 ++++++++ .../contracts/lib/test_signature_verifier.py | 43 +++++++++++++- 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/nucypher/blockchain/eth/sol/source/contracts/lib/SignatureVerifier.sol b/nucypher/blockchain/eth/sol/source/contracts/lib/SignatureVerifier.sol index 8fbefd562..41b1652fa 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/lib/SignatureVerifier.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/lib/SignatureVerifier.sol @@ -87,4 +87,60 @@ library SignatureVerifier { return toAddress(_publicKey) == recover(hash(_message, _algorithm), _signature); } + /** + * @notice Hash message according to EIP191 signature specification + * @dev It always assumes Keccak256 is used as hashing algorithm + * @dev Only supports version E + * @param _message Message to sign + **/ + function hashEIP191( + bytes memory _message + ) + internal + pure + returns (bytes32 result) + { + require(_message.length > 0, "Empty message not allowed"); + + // Header for Version E as defined by EIP191. First byte ('E') is the version + bytes25 header = "Ethereum Signed Message:\n"; + + // Compute text-encoded length of message + uint256 length = _message.length; + uint256 digits = 0; + while (length != 0) { + digits++; + length /= 10; + } + bytes memory lengthAsText = new bytes(digits); + length = _message.length; + uint256 index = digits - 1; + while (length != 0) { + lengthAsText[index--] = byte(uint8(48 + length % 10)); + length /= 10; + } + + return keccak256(abi.encodePacked(byte(0x19), header, lengthAsText, _message)); + } + + /** + * @notice Verify EIP191 signature + * @dev It always assumes Keccak256 is used as hashing algorithm + * @param _message Signed message + * @param _signature Signature of message hash + * @param _publicKey secp256k1 public key in uncompressed format without prefix byte (64 bytes) + **/ + function verifyEIP191( + bytes memory _message, + bytes memory _signature, + bytes memory _publicKey + ) + internal + pure + returns (bool) + { + require(_publicKey.length == 64); + return toAddress(_publicKey) == recover(hashEIP191(_message), _signature); + } + } diff --git a/tests/blockchain/eth/contracts/contracts/LibTestSet.sol b/tests/blockchain/eth/contracts/contracts/LibTestSet.sol index 032e55ae6..25cd63d9c 100644 --- a/tests/blockchain/eth/contracts/contracts/LibTestSet.sol +++ b/tests/blockchain/eth/contracts/contracts/LibTestSet.sol @@ -43,6 +43,28 @@ contract SignatureVerifierMock { return SignatureVerifier.verify(_message, _signature, _publicKey, _algorithm); } + function verifyEIP191( + bytes memory _message, + bytes memory _signature, + bytes memory _publicKey + ) + public + pure + returns (bool) + { + return SignatureVerifier.verifyEIP191(_message, _signature, _publicKey); + } + + function hashEIP191( + bytes memory _message + ) + public + pure + returns (bytes32) + { + return SignatureVerifier.hashEIP191(_message); + } + } diff --git a/tests/blockchain/eth/contracts/lib/test_signature_verifier.py b/tests/blockchain/eth/contracts/lib/test_signature_verifier.py index 7afc97734..77f97c8c9 100644 --- a/tests/blockchain/eth/contracts/lib/test_signature_verifier.py +++ b/tests/blockchain/eth/contracts/lib/test_signature_verifier.py @@ -21,14 +21,16 @@ import os import pytest from cryptography.hazmat.backends.openssl import backend from cryptography.hazmat.primitives import hashes +from eth_account.account import Account +from eth_account.messages import encode_defunct from eth_tester.exceptions import TransactionFailed -from eth_utils import to_normalized_address +from eth_utils import to_normalized_address, to_checksum_address from umbral.keys import UmbralPrivateKey from umbral.signing import Signer -from nucypher.crypto.api import keccak_digest -from nucypher.crypto.utils import get_signature_recovery_value +from nucypher.crypto.api import keccak_digest, verify_eip_191 +from nucypher.crypto.utils import get_signature_recovery_value, canonical_address_from_umbral_key ALGORITHM_KECCAK256 = 0 ALGORITHM_SHA256 = 1 @@ -144,3 +146,38 @@ def test_verify(testerchain, signature_verifier): recoverable_signature, umbral_pubkey_bytes[1:], ALGORITHM_SHA256).call() + + +@pytest.mark.slow +def test_verify_eip191(testerchain, signature_verifier): + message = os.urandom(100) + + # Generate Umbral key + umbral_privkey = UmbralPrivateKey.gen_key() + umbral_pubkey = umbral_privkey.get_pubkey() + umbral_pubkey_bytes = umbral_pubkey.to_bytes(is_compressed=False) + + # Produce EIP191 signature + signable_message = encode_defunct(primitive=message) + signature = Account.sign_message(signable_message=signable_message, + private_key=umbral_privkey.to_bytes()) + signature = bytes(signature.signature) + + # Off-line verify, just in case + checksum_address = to_checksum_address(canonical_address_from_umbral_key(umbral_pubkey)) + assert verify_eip_191(address=checksum_address, + message=message, + signature=signature) + + # Verify signature + assert signature_verifier.functions.verifyEIP191(message, + signature, + umbral_pubkey_bytes[1:]).call() + + # Check that the hash-based method also works independently + hash = signature_verifier.functions.hashEIP191(message).call() + eip191_header = "\x19Ethereum Signed Message:\n"+str(len(message)) + assert hash == keccak_digest(eip191_header.encode() + message) + + address = signature_verifier.functions.recover(hash, signature).call() + assert address == checksum_address