diff --git a/Cargo.toml b/Cargo.toml index 7857ebc..f53c374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,6 @@ required-features = ["client", "rpc"] [lints.clippy] indexing_slicing = "deny" + +# [patch."https://github.com/WalletConnect/erc6492.git"] +# erc6492 = { path = "../erc6492-rs" } diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index c9ff0d7..677a92a 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -5,19 +5,12 @@ edition = "2021" license = "Apache-2.0" [features] +default = ["cacao"] cacao = [ "dep:k256", - "dep:sha3", "dep:alloy-provider", - "dep:alloy-transport", - "dep:alloy-transport-http", - "dep:alloy-rpc-types", - "dep:alloy-json-rpc", - "dep:alloy-json-abi", - "dep:alloy-sol-types", "dep:alloy-primitives", - "dep:alloy-node-bindings", - "dep:alloy-contract" + "dep:erc6492", ] [dependencies] @@ -43,27 +36,15 @@ regex = "1.7" once_cell = "1.16" jsonwebtoken = "8.1" k256 = { version = "0.13", optional = true } -sha3 = { version = "0.10", optional = true } sha2 = { version = "0.10.6" } url = "2" alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-contract = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-json-abi = { version = "0.7.0", optional = true } -alloy-sol-types = { version = "0.7.0", optional = true } alloy-primitives = { version = "0.7.0", optional = true } +erc6492 = { git = "https://github.com/WalletConnect/erc6492.git", optional = true } strum = { version = "0.26", features = ["strum_macros", "derive"] } [dev-dependencies] tokio = { version = "1.35.1", features = ["test-util", "macros"] } -[build-dependencies] -serde_json = "1.0" -hex = "0.4.3" - [lints.clippy] indexing_slicing = "deny" diff --git a/relay_rpc/build.rs b/relay_rpc/build.rs deleted file mode 100644 index 6d8b1a1..0000000 --- a/relay_rpc/build.rs +++ /dev/null @@ -1,98 +0,0 @@ -use { - serde_json::Value, - std::process::{Command, Stdio}, -}; - -fn main() { - #[cfg(feature = "cacao")] - build_contracts(); -} - -fn build_contracts() { - println!("cargo::rerun-if-changed=contracts"); - install_foundry(); - compile_contracts(); - extract_bytecodes(); -} - -fn format_foundry_dir(path: &str) -> String { - format!( - "{}/../../../../.foundry/{}", - std::env::var("OUT_DIR").unwrap(), - path - ) -} - -fn install_foundry() { - let bin_folder = format_foundry_dir("bin"); - std::fs::remove_dir_all(&bin_folder).ok(); - std::fs::create_dir_all(&bin_folder).unwrap(); - let output = Command::new("bash") - .args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .unwrap(); - println!("foundryup status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("foundryup stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("foundryup stderr: {stderr:?}"); - assert!(output.status.success()); -} - -fn compile_contracts() { - let output = Command::new(format_foundry_dir("bin/forge")) - .args([ - "build", - "--contracts=relay_rpc/contracts", - "--cache-path", - &format_foundry_dir("forge/cache"), - "--out", - &format_foundry_dir("forge/out"), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .unwrap(); - println!("forge status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("forge stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("forge stderr: {stderr:?}"); - assert!(output.status.success()); -} - -const EIP6492_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.json"; -const EIP6492_BYTECODE_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; -const EIP1271_MOCK_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.json"; -const EIP1271_MOCK_BYTECODE_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode"; -fn extract_bytecodes() { - extract_bytecode( - &format_foundry_dir(EIP6492_FILE), - &format_foundry_dir(EIP6492_BYTECODE_FILE), - ); - extract_bytecode( - &format_foundry_dir(EIP1271_MOCK_FILE), - &format_foundry_dir(EIP1271_MOCK_BYTECODE_FILE), - ); -} - -fn extract_bytecode(input_file: &str, output_file: &str) { - let contents = serde_json::from_slice::(&std::fs::read(input_file).unwrap()).unwrap(); - let bytecode = contents - .get("bytecode") - .unwrap() - .get("object") - .unwrap() - .as_str() - .unwrap() - .strip_prefix("0x") - .unwrap(); - let bytecode = hex::decode(bytecode).unwrap(); - std::fs::write(output_file, bytecode).unwrap(); -} diff --git a/relay_rpc/contracts/Create2.sol b/relay_rpc/contracts/Create2.sol deleted file mode 100644 index 062722e..0000000 --- a/relay_rpc/contracts/Create2.sol +++ /dev/null @@ -1,49 +0,0 @@ -// https://github.com/Genesis3800/CREATE2Factory/blob/b202029eadc0299e6e5923dd90db4200c2f7955a/src/Create2.sol - -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract Create2 { - - error Create2InsufficientBalance(uint256 received, uint256 minimumNeeded); - - error Create2EmptyBytecode(); - - error Create2FailedDeployment(); - - function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr) { - - if (msg.value < amount) { - revert Create2InsufficientBalance(msg.value, amount); - } - - if (bytecode.length == 0) { - revert Create2EmptyBytecode(); - } - - assembly { - addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) - } - - if (addr == address(0)) { - revert Create2FailedDeployment(); - } - } - - function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address addr) { - - address contractAddress = address(this); - - assembly { - let ptr := mload(0x40) - - mstore(add(ptr, 0x40), bytecodeHash) - mstore(add(ptr, 0x20), salt) - mstore(ptr, contractAddress) - let start := add(ptr, 0x0b) - mstore8(start, 0xff) - addr := keccak256(start, 85) - } - } - -} diff --git a/relay_rpc/contracts/Eip1271Mock.sol b/relay_rpc/contracts/Eip1271Mock.sol deleted file mode 100644 index 35fa8b9..0000000 --- a/relay_rpc/contracts/Eip1271Mock.sol +++ /dev/null @@ -1,81 +0,0 @@ -pragma solidity ^0.8.25; - -// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation - -contract Eip1271Mock { - address owner_eoa; - - constructor(address _owner_eoa) { - owner_eoa = _owner_eoa; - } - - /** - * @notice Verifies that the signer is the owner of the signing contract. - */ - function isValidSignature( - bytes32 _hash, - bytes calldata _signature - ) external view returns (bytes4) { - // Validate signatures - if (recoverSigner(_hash, _signature) == owner_eoa) { - return 0x1626ba7e; - } else { - return 0xffffffff; - } - } - - /** - * @notice Recover the signer of hash, assuming it's an EOA account - * @dev Only for EthSign signatures - * @param _hash Hash of message that was signed - * @param _signature Signature encoded as (bytes32 r, bytes32 s, uint8 v) - */ - function recoverSigner( - bytes32 _hash, - bytes memory _signature - ) internal pure returns (address signer) { - require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length"); - - // Variables are not scoped in Solidity. - uint8 v = uint8(_signature[64]); - bytes32 r; - bytes32 s; - assembly { - // Slice the signature into r and s components - r := mload(add(_signature, 32)) - s := mload(add(_signature, 64)) - } - - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - // - // Source OpenZeppelin - // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol - - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - revert("SignatureValidator#recoverSigner: invalid signature 's' value"); - } - - if (v != 27 && v != 28) { - revert("SignatureValidator#recoverSigner: invalid signature 'v' value"); - } - - // Recover ECDSA signer - signer = ecrecover(_hash, v, r, s); - - // Prevent signer from being 0x0 - require( - signer != address(0x0), - "SignatureValidator#recoverSigner: INVALID_SIGNER" - ); - - return signer; - } -} diff --git a/relay_rpc/contracts/Eip6492.sol b/relay_rpc/contracts/Eip6492.sol deleted file mode 100644 index 41586fe..0000000 --- a/relay_rpc/contracts/Eip6492.sol +++ /dev/null @@ -1,111 +0,0 @@ -// As per ERC-1271 -interface IERC1271Wallet { - function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); -} - -error ERC1271Revert(bytes error); -error ERC6492DeployFailed(bytes error); - -contract UniversalSigValidator { - bytes32 private constant ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; - bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e; - - function isValidSigImpl( - address _signer, - bytes32 _hash, - bytes calldata _signature, - bool allowSideEffects, - bool tryPrepare - ) public returns (bool) { - uint contractCodeLen = address(_signer).code.length; - bytes memory sigToValidate; - // The order here is strictly defined in https://eips.ethereum.org/EIPS/eip-6492 - // - ERC-6492 suffix check and verification first, while being permissive in case the contract is already deployed; if the contract is deployed we will check the sig against the deployed version, this allows 6492 signatures to still be validated while taking into account potential key rotation - // - ERC-1271 verification if there's contract code - // - finally, ecrecover - bool isCounterfactual = bytes32(_signature[_signature.length-32:_signature.length]) == ERC6492_DETECTION_SUFFIX; - if (isCounterfactual) { - address create2Factory; - bytes memory factoryCalldata; - (create2Factory, factoryCalldata, sigToValidate) = abi.decode(_signature[0:_signature.length-32], (address, bytes, bytes)); - - if (contractCodeLen == 0 || tryPrepare) { - (bool success, bytes memory err) = create2Factory.call(factoryCalldata); - if (!success) revert ERC6492DeployFailed(err); - } - } else { - sigToValidate = _signature; - } - - // Try ERC-1271 verification - if (isCounterfactual || contractCodeLen > 0) { - try IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate) returns (bytes4 magicValue) { - bool isValid = magicValue == ERC1271_SUCCESS; - - // retry, but this time assume the prefix is a prepare call - if (!isValid && !tryPrepare && contractCodeLen > 0) { - return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true); - } - - if (contractCodeLen == 0 && isCounterfactual && !allowSideEffects) { - // if the call had side effects we need to return the - // result using a `revert` (to undo the state changes) - assembly { - mstore(0, isValid) - revert(31, 1) - } - } - - return isValid; - } catch (bytes memory err) { - // retry, but this time assume the prefix is a prepare call - if (!tryPrepare && contractCodeLen > 0) { - return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true); - } - - revert ERC1271Revert(err); - } - } - - // ecrecover verification - require(_signature.length == 65, 'SignatureValidator#recoverSigner: invalid signature length'); - bytes32 r = bytes32(_signature[0:32]); - bytes32 s = bytes32(_signature[32:64]); - uint8 v = uint8(_signature[64]); - if (v != 27 && v != 28) { - revert('SignatureValidator: invalid signature v value'); - } - return ecrecover(_hash, v, r, s) == _signer; - } - - function isValidSigWithSideEffects(address _signer, bytes32 _hash, bytes calldata _signature) - external returns (bool) - { - return this.isValidSigImpl(_signer, _hash, _signature, true, false); - } - - function isValidSig(address _signer, bytes32 _hash, bytes calldata _signature) - external returns (bool) - { - try this.isValidSigImpl(_signer, _hash, _signature, false, false) returns (bool isValid) { return isValid; } - catch (bytes memory error) { - // in order to avoid side effects from the contract getting deployed, the entire call will revert with a single byte result - uint len = error.length; - if (len == 1) return error[0] == 0x01; - // all other errors are simply forwarded, but in custom formats so that nothing else can revert with a single byte in the call - else assembly { revert(error, len) } - } - } -} - -// this is a helper so we can perform validation in a single eth_call without pre-deploying a singleton -contract ValidateSigOffchain { - constructor (address _signer, bytes32 _hash, bytes memory _signature) { - UniversalSigValidator validator = new UniversalSigValidator(); - bool isValidSig = validator.isValidSigWithSideEffects(_signer, _hash, _signature); - assembly { - mstore(0, isValidSig) - return(31, 1) - } - } -} diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 179077f..9f888bc 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -4,10 +4,10 @@ use { payload::Payload, signature::{get_rpc_url::GetRpcUrl, Signature}, }, - alloy_primitives::hex::FromHexError, + alloy_primitives::AddressError, core::fmt::Debug, + erc6492::RpcError, serde::{Deserialize, Serialize}, - serde_json::value::RawValue, std::{ fmt::{Display, Write}, sync::Arc, @@ -31,16 +31,10 @@ pub enum CacaoError { PayloadResources, #[error("Invalid address")] - AddressInvalid, + AddressInvalid(AddressError), - #[error("Address not EIP-191")] - AddressNotEip191(FromHexError), - - #[error("EIP-1271 signatures not supported")] - Eip1271NotSupported, - - #[error("EIP-6492 signatures not supported")] - Eip6492NotSupported, + #[error("Provider address verification not supported")] + ProviderAddressVerificationNotSupported, #[error("Unsupported signature type")] UnsupportedSignature, @@ -51,11 +45,8 @@ pub enum CacaoError { #[error("Unable to verify")] Verification, - #[error("Internal EIP-1271 resolution error: {0}")] - Eip1271Internal(alloy_json_rpc::RpcError>), - - #[error("Internal EIP-6492 resolution error: {0}")] - Eip6492Internal(alloy_json_rpc::RpcError>), + #[error("Internal verification error: {0}")] + Rpc(RpcError), } impl From for CacaoError { diff --git a/relay_rpc/src/auth/cacao/signature/eip1271.rs b/relay_rpc/src/auth/cacao/signature/eip1271.rs deleted file mode 100644 index 6deddf7..0000000 --- a/relay_rpc/src/auth/cacao/signature/eip1271.rs +++ /dev/null @@ -1,224 +0,0 @@ -use { - super::CacaoError, - alloy_primitives::Address, - alloy_provider::{network::Ethereum, Provider, ReqwestProvider}, - alloy_rpc_types::{TransactionInput, TransactionRequest}, - alloy_sol_types::{sol, SolCall}, - url::Url, -}; - -pub const EIP1271: &str = "eip1271"; - -// https://eips.ethereum.org/EIPS/eip-1271 -const MAGIC_VALUE: u32 = 0x1626ba7e; -sol! { - function isValidSignature( - bytes32 _hash, - bytes memory _signature) - public - view - returns (bytes4 magicValue); -} - -pub async fn verify_eip1271( - signature: Vec, - address: Address, - hash: &[u8; 32], - provider: Url, -) -> Result<(), CacaoError> { - let provider = ReqwestProvider::::new_http(provider); - - let call_request = TransactionRequest::default() - .to(address) - .input(TransactionInput::new( - isValidSignatureCall { - _hash: hash.into(), - _signature: signature.into(), - } - .abi_encode() - .into(), - )); - - let result = provider - .call(&call_request, Default::default()) - .await - .map_err(|e| { - if let Some(error_response) = e.as_error_resp() { - if error_response.message.starts_with("execution reverted:") { - CacaoError::Verification - } else { - CacaoError::Eip1271Internal(e) - } - } else { - CacaoError::Eip1271Internal(e) - } - })?; - - let magic = result.get(..4); - if let Some(magic) = magic { - if magic == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(()) - } else { - Err(CacaoError::Verification) - } - } else { - Err(CacaoError::Verification) - } -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::auth::cacao::signature::{ - eip191::eip191_bytes, - strip_hex_prefix, - test_helpers::{ - deploy_contract, - message_hash, - sign_message, - spawn_anvil, - EIP1271_MOCK_CONTRACT, - }, - }, - alloy_primitives::address, - k256::ecdsa::SigningKey, - sha3::{Digest, Keccak256}, - }; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip1271_manual() { - let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - let signature = "xxx"; - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification) - .unwrap(); - let message = "xxx"; - let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] - .try_into() - .unwrap(); - let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" - .parse() - .unwrap(); - verify_eip1271(signature, address, hash, provider) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let mut contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - *contract_address.0.first_mut().unwrap() = - contract_address.0.first().unwrap().wrapping_add(1); - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - let message2 = "yyy"; - assert!(matches!( - verify_eip1271( - signature, - contract_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs index 2287ef5..33ad441 100644 --- a/relay_rpc/src/auth/cacao/signature/eip191.rs +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -1,8 +1,6 @@ use { super::CacaoError, - crate::auth::cacao::signature::strip_hex_prefix, - alloy_primitives::Address, - sha3::{Digest, Keccak256}, + alloy_primitives::{Address, FixedBytes}, }; pub const EIP191: &str = "eip191"; @@ -19,7 +17,7 @@ pub fn eip191_bytes(message: &str) -> Vec { pub fn verify_eip191( signature: &[u8], address: &Address, - hash: Keccak256, + hash: FixedBytes<32>, ) -> Result<(), CacaoError> { use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; @@ -28,47 +26,42 @@ pub fn verify_eip191( let recovery_id = RecoveryId::try_from(signature.get(64).ok_or(CacaoError::Verification)? % 27) .map_err(|_| CacaoError::Verification)?; - let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) + let recovered_key = VerifyingKey::recover_from_prehash(hash.as_slice(), &sig, recovery_id) .map_err(|_| CacaoError::Verification)?; - let hash = Keccak256::default() - .chain_update( - recovered_key - .to_encoded_point(false) - .as_bytes() - .get(1..) - .ok_or(CacaoError::Verification)?, - ) - .finalize(); - let add = hash.get(12..).ok_or(CacaoError::Verification)?; + let add = Address::from_public_key(&recovered_key); - let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); - - if address_encoded.to_lowercase() != strip_hex_prefix(&address.to_string()).to_lowercase() { - Err(CacaoError::Verification) - } else { + if &add == address { Ok(()) + } else { + Err(CacaoError::Verification) } } #[cfg(test)] mod tests { use { - crate::auth::cacao::signature::{ - eip191::verify_eip191, - test_helpers::{message_hash_internal, sign_message}, - }, - alloy_primitives::Address, + crate::auth::cacao::signature::eip191::verify_eip191, + alloy_primitives::{eip191_hash_message, Address}, k256::ecdsa::SigningKey, }; + pub fn sign_message(message: &str, private_key: &SigningKey) -> Vec { + let (signature, recovery): (k256::ecdsa::Signature, _) = private_key + .sign_prehash_recoverable(eip191_hash_message(message).as_slice()) + .unwrap(); + let signature = signature.to_bytes(); + // need for +27 is mentioned in EIP-1271 reference implementation + [&signature[..], &[recovery.to_byte() + 27]].concat() + } + #[test] fn test_eip191() { let private_key = SigningKey::random(&mut rand::thread_rng()); let message = "xxx"; let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); - verify_eip191(&signature, &address, message_hash_internal(message)).unwrap(); + verify_eip191(&signature, &address, eip191_hash_message(message)).unwrap(); } #[test] @@ -78,7 +71,7 @@ mod tests { let mut signature = sign_message(message, &private_key); *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); let address = Address::from_private_key(&private_key); - assert!(verify_eip191(&signature, &address, message_hash_internal(message)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); } #[test] @@ -88,7 +81,7 @@ mod tests { let signature = sign_message(message, &private_key); let mut address = Address::from_private_key(&private_key); *address.0.first_mut().unwrap() = address.0.first().unwrap().wrapping_add(1); - assert!(verify_eip191(&signature, &address, message_hash_internal(message)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); } #[test] @@ -98,6 +91,6 @@ mod tests { let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); let message2 = "yyy"; - assert!(verify_eip191(&signature, &address, message_hash_internal(message2)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message2)).is_err()); } } diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs deleted file mode 100644 index 0fc5117..0000000 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ /dev/null @@ -1,482 +0,0 @@ -use { - crate::auth::cacao::CacaoError, - alloy_primitives::Address, - alloy_provider::{network::Ethereum, Provider, ReqwestProvider}, - alloy_rpc_types::{TransactionInput, TransactionRequest}, - alloy_sol_types::{sol, SolConstructor}, - url::Url, -}; - -pub const EIP6492: &str = "eip6492"; - -// https://eips.ethereum.org/EIPS/eip-6492 -const MAGIC_VALUE: u8 = 0x01; -sol! { - contract ValidateSigOffchain { - constructor (address _signer, bytes32 _hash, bytes memory _signature); - } -} -const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), - "/../../../../.foundry/forge/out/Eip6492.sol/ValidateSigOffchain.bytecode" -)); - -pub async fn verify_eip6492( - signature: Vec, - address: Address, - hash: &[u8; 32], - provider: Url, -) -> Result<(), CacaoError> { - let provider = ReqwestProvider::::new_http(provider); - - let call = ValidateSigOffchain::constructorCall { - _signer: address, - _hash: hash.into(), - _signature: signature.into(), - }; - let bytes = VALIDATE_SIG_OFFCHAIN_BYTECODE - .iter() - .cloned() - .chain(call.abi_encode()) - .collect::>(); - let transaction_request = - TransactionRequest::default().input(TransactionInput::new(bytes.into())); - - let result = provider - .call(&transaction_request, Default::default()) - .await - .map_err(|e| { - if let Some(error_response) = e.as_error_resp() { - if error_response.message == "execution reverted" { - CacaoError::Verification - } else { - CacaoError::Eip6492Internal(e) - } - } else { - CacaoError::Eip6492Internal(e) - } - })?; - - let magic = result.first(); - if let Some(magic) = magic { - if magic == &MAGIC_VALUE { - Ok(()) - } else { - Err(CacaoError::Verification) - } - } else { - Err(CacaoError::Verification) - } -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::auth::cacao::signature::{ - strip_hex_prefix, - test_helpers::{ - deploy_contract, - message_hash, - sign_message, - spawn_anvil, - CREATE2_CONTRACT, - EIP1271_MOCK_CONTRACT, - }, - }, - alloy_primitives::{address, b256, Uint}, - alloy_sol_types::{SolCall, SolValue}, - k256::ecdsa::SigningKey, - }; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip6492_manual() { - let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - let message = "xxx"; - let signature = "xxx"; - - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification) - .unwrap(); - let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" - .parse() - .unwrap(); - verify_eip6492(signature, address, &message_hash(message), provider) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip191_pass() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let address = Address::from_private_key(&private_key); - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip191_wrong_signature() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - let address = Address::from_private_key(&private_key); - assert!( - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip191_wrong_address() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let mut address = Address::from_private_key(&private_key); - *address.0.first_mut().unwrap() = address.0.first().unwrap().wrapping_add(1); - assert!( - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip191_wrong_message() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let address = Address::from_private_key(&private_key); - let message2 = "yyy"; - assert!( - verify_eip6492(signature, address, &message_hash(message2), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip1271_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let mut contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - *contract_address.0.first_mut().unwrap() = - contract_address.0.first().unwrap().wrapping_add(1); - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - let message2 = "yyy"; - assert!(matches!( - verify_eip6492( - signature, - contract_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - const EIP1271_MOCK_BYTECODE: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), - "/../../../../.foundry/forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode" - )); - const EIP6492_MAGIC_BYTES: [u16; 16] = [ - 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, - 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, - ]; - sol! { - contract Eip1271Mock { - address owner_eoa; - - constructor(address owner_eoa) { - owner_eoa = owner_eoa; - } - } - } - - sol! { - contract Create2 { - function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr); - } - } - - fn predeploy_signature( - owner_eoa: Address, - create2_factory_address: Address, - signature: Vec, - ) -> (Address, Vec) { - let salt = b256!("7c5ea36004851c764c44143b1dcb59679b11c9a68e5f41497f6cf3d480715331"); - let contract_bytecode = EIP1271_MOCK_BYTECODE; - let contract_constructor = Eip1271Mock::constructorCall { owner_eoa }; - - let bytecode = contract_bytecode - .iter() - .cloned() - .chain(contract_constructor.abi_encode()) - .collect::>(); - let predeploy_address = create2_factory_address.create2_from_code(salt, bytecode.clone()); - let signature = ( - create2_factory_address, - Create2::deployCall { - amount: Uint::ZERO, - salt, - bytecode: bytecode.into(), - } - .abi_encode(), - signature, - ) - .abi_encode_sequence() - .into_iter() - .chain( - EIP6492_MAGIC_BYTES - .iter() - .flat_map(|&x| x.to_be_bytes().into_iter()), - ) - .collect::>(); - (predeploy_address, signature) - } - - #[tokio::test] - async fn test_eip6492_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip6492_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (mut predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - *predeploy_address.0.first_mut().unwrap() = - predeploy_address.0.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url, - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - let message2 = "yyy"; - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index 4eb0ae1..9b133b6 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -1,99 +1,67 @@ use { self::{ - eip1271::{verify_eip1271, EIP1271}, - eip191::{eip191_bytes, verify_eip191, EIP191}, - eip6492::{verify_eip6492, EIP6492}, + eip191::{verify_eip191, EIP191}, get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, - alloy_primitives::Address, + alloy_primitives::{eip191_hash_message, hex::FromHex, Address, Bytes}, + alloy_provider::{network::Ethereum, ReqwestProvider}, + erc6492::verify_signature, serde::{Deserialize, Serialize}, - sha3::{Digest, Keccak256}, - std::str::FromStr, }; -pub mod eip1271; pub mod eip191; -pub mod eip6492; pub mod get_rpc_url; -#[cfg(test)] -mod test_helpers; - #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct Signature { pub t: String, pub s: String, } +pub const EIP1271: &str = "eip1271"; +pub const EIP6492: &str = "eip6492"; + impl Signature { pub async fn verify( &self, cacao: &Cacao, provider: Option<&impl GetRpcUrl>, ) -> Result<(), CacaoError> { + let chain_id = cacao.p.chain_id_reference()?; let address = cacao.p.address()?; - - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) - .map_err(|_| CacaoError::Verification)?; - - let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); + let address = + Address::parse_checksummed(address, None).map_err(CacaoError::AddressInvalid)?; + let signature = Bytes::from_hex(&cacao.s.s).map_err(|_| CacaoError::Verification)?; + let message = cacao.siwe_message()?; match self.t.as_str() { - EIP191 => verify_eip191( - &signature, - &address.parse().map_err(CacaoError::AddressNotEip191)?, - hash, - ), - EIP1271 => { - if let Some(provider) = provider { - let chain_id = cacao.p.chain_id_reference()?; - let provider = provider.get_rpc_url(chain_id).await; - if let Some(provider) = provider { - verify_eip1271( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider, - ) - .await - } else { - Err(CacaoError::ProviderNotAvailable) - } - } else { - Err(CacaoError::Eip1271NotSupported) - } + EIP191 => { + // Technically we can use EIP-6492 to verify EIP-191 signatures as well, + // but since we know the signature type we can avoid an RPC request. + verify_eip191(&signature, &address, eip191_hash_message(message)) } - EIP6492 => { + EIP1271 | EIP6492 => { if let Some(provider) = provider { - let chain_id = cacao.p.chain_id_reference()?; - let provider = provider.get_rpc_url(chain_id).await; - if let Some(provider) = provider { - verify_eip6492( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider, - ) + let provider = ReqwestProvider::::new_http( + provider + .get_rpc_url(chain_id) + .await + .ok_or(CacaoError::ProviderNotAvailable)?, + ); + let result = verify_signature(signature, address, message, provider) .await + .map_err(CacaoError::Rpc)?; + if result.is_valid() { + Ok(()) } else { - Err(CacaoError::ProviderNotAvailable) + Err(CacaoError::Verification) } } else { - Err(CacaoError::Eip6492NotSupported) + Err(CacaoError::ProviderNotAvailable) } } _ => Err(CacaoError::UnsupportedSignature), } } } - -/// Remove the "0x" prefix from a hex string. -fn strip_hex_prefix(s: &str) -> &str { - s.strip_prefix("0x").unwrap_or(s) -} diff --git a/relay_rpc/src/auth/cacao/signature/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs deleted file mode 100644 index f7f305c..0000000 --- a/relay_rpc/src/auth/cacao/signature/test_helpers.rs +++ /dev/null @@ -1,102 +0,0 @@ -use { - super::eip191::eip191_bytes, - alloy_node_bindings::{Anvil, AnvilInstance}, - alloy_primitives::Address, - k256::ecdsa::SigningKey, - regex::Regex, - sha2::Digest, - sha3::Keccak256, - std::process::Stdio, - tokio::process::Command, - url::Url, -}; - -fn format_foundry_dir(path: &str) -> String { - format!( - "{}/../../../../.foundry/{}", - std::env::var("OUT_DIR").unwrap(), - path - ) -} - -pub async fn spawn_anvil() -> (AnvilInstance, Url, SigningKey) { - let anvil = Anvil::at(format_foundry_dir("bin/anvil")).spawn(); - let provider = anvil.endpoint().parse().unwrap(); - let private_key = anvil.keys().first().unwrap().clone(); - ( - anvil, - provider, - SigningKey::from_bytes(&private_key.to_bytes()).unwrap(), - ) -} - -pub const EIP1271_MOCK_CONTRACT: &str = "Eip1271Mock"; -pub const CREATE2_CONTRACT: &str = "Create2"; - -pub async fn deploy_contract( - rpc_url: &Url, - private_key: &SigningKey, - contract_name: &str, - constructor_arg: Option<&str>, -) -> Address { - let key_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(&private_key.to_bytes()); - let cache_folder = format_foundry_dir("forge/cache"); - let out_folder = format_foundry_dir("forge/out"); - let mut args = vec![ - "create", - "--contracts=relay_rpc/contracts", - contract_name, - "--rpc-url", - rpc_url.as_str(), - "--private-key", - &key_encoded, - "--cache-path", - &cache_folder, - "--out", - &out_folder, - ]; - if let Some(arg) = constructor_arg { - args.push("--constructor-args"); - args.push(arg); - } - let output = Command::new(format_foundry_dir("bin/forge")) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .await - .unwrap(); - println!("forge status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("forge stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("forge stderr: {stderr:?}"); - assert!(output.status.success()); - let (_, [contract_address]) = Regex::new("Deployed to: (0x[0-9a-fA-F]+)") - .unwrap() - .captures(&stdout) - .unwrap() - .extract(); - contract_address.parse().unwrap() -} - -pub fn sign_message(message: &str, private_key: &SigningKey) -> Vec { - let (signature, recovery): (k256::ecdsa::Signature, _) = private_key - .sign_digest_recoverable(message_hash_internal(message)) - .unwrap(); - let signature = signature.to_bytes(); - // need for +27 is mentioned in EIP-1271 reference implementation - [&signature[..], &[recovery.to_byte() + 27]].concat() -} - -pub fn message_hash_internal(message: &str) -> Keccak256 { - Keccak256::new_with_prefix(eip191_bytes(message)) -} - -pub fn message_hash(message: &str) -> [u8; 32] { - message_hash_internal(message).finalize()[..] - .try_into() - .unwrap() -}