diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index c8fa18b..4b5ce5d 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -15,6 +15,7 @@ cacao = [ "dep:alloy-json-abi", "dep:alloy-sol-types", "dep:alloy-primitives", + "dep:alloy-node-bindings" ] [dependencies] @@ -49,6 +50,7 @@ alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } +alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } alloy-json-abi = { version = "0.6.2", optional = true } alloy-sol-types = { version = "0.6.2", optional = true } alloy-primitives = { version = "0.6.2", optional = true } diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/TestContract.sol b/relay_rpc/src/auth/cacao/signature/eip1271/TestContract.sol new file mode 100644 index 0000000..856fe2f --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/TestContract.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.8.25; + +// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation + +contract TestContract { + address owner; + + constructor() { + owner = msg.sender; + } + + /** + * @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) { + 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/src/auth/cacao/signature/eip1271/mod.rs b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs index 50125ba..3b72abb 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs @@ -56,8 +56,13 @@ pub async fn verify_eip1271( } })?; - if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(true) + let magic = result.get(..4); + if let Some(magic) = magic { + if magic == MAGIC_VALUE.to_be_bytes().to_vec() { + Ok(true) + } else { + Err(CacaoError::Verification) + } } else { Err(CacaoError::Verification) } @@ -68,15 +73,20 @@ mod test { use { super::*, crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix}, + alloy_node_bindings::{Anvil, AnvilInstance}, alloy_primitives::address, + k256::{ecdsa::SigningKey, elliptic_curve::SecretKey, Secp256k1}, + regex::Regex, sha3::{Digest, Keccak256}, + std::process::Stdio, + tokio::process::Command, }; // Manual test. Paste address, signature, message, and project ID to verify // function #[tokio::test] #[ignore] - async fn test_eip1271() { + async fn test_eip1271_manual() { let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); let signature = "xxx"; let signature = data_encoding::HEXLOWER_PERMISSIVE @@ -94,4 +104,121 @@ mod test { .await .unwrap()); } + + async fn spawn_anvil() -> (AnvilInstance, Url, SecretKey) { + let anvil = Anvil::new().spawn(); + let provider = anvil.endpoint().parse().unwrap(); + let private_key = anvil.keys().first().unwrap().clone(); + (anvil, provider, private_key) + } + + async fn deploy_contract(rpc_url: &Url, private_key: &SecretKey) -> Address { + let key_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(&private_key.to_bytes()); + let output = Command::new("forge") + .args([ + "create", + "--contracts", + "relay_rpc/src/auth/cacao/signature/eip1271", + "TestContract", + "--rpc-url", + rpc_url.as_str(), + "--private-key", + &key_encoded, + "--cache-path", + "target/.forge/cache", + "--out", + "target/.forge/out", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .await + .unwrap(); + let output = String::from_utf8(output.stdout).unwrap(); + let (_, [contract_address]) = Regex::new("Deployed to: (0x[0-9a-fA-F]+)") + .unwrap() + .captures(&output) + .unwrap() + .extract(); + contract_address.parse().unwrap() + } + + fn sign_message(message: &str, private_key: &SecretKey) -> Vec { + let (signature, recovery): (k256::ecdsa::Signature, _) = + SigningKey::from_bytes(&private_key.to_bytes()) + .unwrap() + .sign_digest_recoverable(Keccak256::new_with_prefix(eip191_bytes(message))) + .unwrap(); + let signature = signature.to_bytes(); + // need for +27 is mentioned in EIP-1271 reference implementation + [&signature[..], &[recovery.to_byte() + 27]].concat() + } + + fn message_hash(message: &str) -> [u8; 32] { + Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] + .try_into() + .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).await; + + let message = "xxx"; + let signature = sign_message(message, &private_key); + + assert!( + verify_eip1271(signature, contract_address, &message_hash(message), rpc_url) + .await + .unwrap() + ); + } + + #[tokio::test] + async fn test_eip1271_fail() { + let (_anvil, rpc_url, private_key) = spawn_anvil().await; + let contract_address = deploy_contract(&rpc_url, &private_key).await; + + let message = "xxx"; + let mut signature = sign_message(message, &private_key); + signature[0] = signature[0].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).await; + + let message = "xxx"; + let signature = sign_message(message, &anvil.keys()[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_contract_address() { + let (_anvil, rpc_url, private_key) = spawn_anvil().await; + let mut contract_address = deploy_contract(&rpc_url, &private_key).await; + + contract_address.0[0] = contract_address.0[0].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) + )); + } }