From 7a205511a98011e4fc0cffb7a3652d435b191cd7 Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:19:51 -0500 Subject: [PATCH] feat: EIP-1271 support (#55) --- justfile | 15 +++ relay_rpc/Cargo.toml | 15 ++- relay_rpc/src/auth/cacao.rs | 20 +++- relay_rpc/src/auth/cacao/signature.rs | 74 -------------- .../cacao/signature/eip1271/blockchain_api.rs | 59 +++++++++++ .../cacao/signature/eip1271/get_rpc_url.rs | 5 + .../src/auth/cacao/signature/eip1271/mod.rs | 98 +++++++++++++++++++ relay_rpc/src/auth/cacao/signature/eip191.rs | 39 ++++++++ relay_rpc/src/auth/cacao/signature/mod.rs | 63 ++++++++++++ relay_rpc/src/auth/cacao/tests.rs | 28 ++++-- 10 files changed, 328 insertions(+), 88 deletions(-) delete mode 100644 relay_rpc/src/auth/cacao/signature.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/mod.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip191.rs create mode 100644 relay_rpc/src/auth/cacao/signature/mod.rs diff --git a/justfile b/justfile index 78c20d3..4cb6e46 100644 --- a/justfile +++ b/justfile @@ -72,6 +72,21 @@ fmt: echo ' ^^^^^^ To install `rustup component add rustfmt`, see https://github.com/rust-lang/rustfmt for details' fi +fmt-imports: + #!/bin/bash + set -euo pipefail + + if command -v cargo-fmt >/dev/null; then + echo '==> Running rustfmt' + cargo +nightly fmt -- --config group_imports=StdExternalCrate,imports_granularity=One + else + echo '==> rustfmt not found in PATH, skipping' + fi + +unit: lint test test-all + +devloop: unit fmt-imports + # Run commit checker commit-check: #!/bin/bash diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index cf7ba3d..ce393a4 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -22,4 +22,17 @@ once_cell = "1.16" jsonwebtoken = "8.1" k256 = { version = "0.13", optional = true } sha3 = { version = "0.10", optional = true } -sha2 = { version = "0.10.6" } \ No newline at end of file +sha2 = { version = "0.10.6" } +reqwest = { version = "0.11", features = ["default-tls"] } +url = "2" +alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" } +alloy-json-abi = "0.6.2" +alloy-sol-types = "0.6.2" +alloy-primitives = "0.6.2" + +[dev-dependencies] +tokio = { version = "1.35.1", features = ["test-util", "macros"] } diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 246243b..52bd759 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -1,7 +1,12 @@ use { - self::{header::Header, payload::Payload, signature::Signature}, + self::{ + header::Header, + payload::Payload, + signature::{eip1271::get_rpc_url::GetRpcUrl, Signature}, + }, core::fmt::Debug, serde::{Deserialize, Serialize}, + serde_json::value::RawValue, std::fmt::{Display, Write}, }; @@ -21,11 +26,20 @@ pub enum CacaoError { #[error("Invalid payload resources")] PayloadResources, + #[error("Invalid address")] + AddressInvalid, + #[error("Unsupported signature type")] UnsupportedSignature, + #[error("Provider not available for that chain")] + ProviderNotAvailable, + #[error("Unable to verify")] Verification, + + #[error("Internal EIP-1271 resolution error: {0}")] + Eip1271Internal(alloy_json_rpc::RpcError>), } impl From for CacaoError { @@ -77,10 +91,10 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub fn verify(&self) -> Result { + pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result { self.p.validate()?; self.h.validate()?; - self.s.verify(self) + self.s.verify(self, provider).await } pub fn siwe_message(&self) -> Result { diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs deleted file mode 100644 index 90aad4f..0000000 --- a/relay_rpc/src/auth/cacao/signature.rs +++ /dev/null @@ -1,74 +0,0 @@ -use { - super::{Cacao, CacaoError}, - serde::{Deserialize, Serialize}, -}; - -pub const EIP191: &str = "eip191"; - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] -pub struct Signature { - pub t: String, - pub s: String, -} - -impl Signature { - pub fn verify(&self, cacao: &Cacao) -> Result { - match self.t.as_str() { - EIP191 => Eip191.verify(&cacao.s.s, &cacao.p.address()?, &cacao.siwe_message()?), - // "eip1271" => Eip1271.verify(), TODO: How to accces our RPC? - _ => Err(CacaoError::UnsupportedSignature), - } - } -} - -pub struct Eip191; - -impl Eip191 { - pub fn eip191_bytes(&self, message: &str) -> Vec { - format!( - "\u{0019}Ethereum Signed Message:\n{}{}", - message.as_bytes().len(), - message - ) - .into() - } - - fn verify(&self, signature: &str, address: &str, message: &str) -> Result { - use { - k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}, - sha3::{Digest, Keccak256}, - }; - - let signature_bytes = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification)?; - - let sig = Sig::try_from(&signature_bytes[..64]).map_err(|_| CacaoError::Verification)?; - let recovery_id = RecoveryId::try_from(&signature_bytes[64] % 27) - .map_err(|_| CacaoError::Verification)?; - - let recovered_key = VerifyingKey::recover_from_digest( - Keccak256::new_with_prefix(self.eip191_bytes(message)), - &sig, - recovery_id, - ) - .map_err(|_| CacaoError::Verification)?; - - let add = &Keccak256::default() - .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) - .finalize()[12..]; - - let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); - - if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { - Err(CacaoError::Verification) - } else { - Ok(true) - } - } -} - -/// 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/eip1271/blockchain_api.rs b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs new file mode 100644 index 0000000..ac8dcc7 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs @@ -0,0 +1,59 @@ +use {super::get_rpc_url::GetRpcUrl, crate::domain::ProjectId, url::Url}; + +// https://github.com/WalletConnect/blockchain-api/blob/master/SUPPORTED_CHAINS.md +const SUPPORTED_CHAINS: [&str; 26] = [ + "eip155:1", + "eip155:5", + "eip155:11155111", + "eip155:10", + "eip155:420", + "eip155:42161", + "eip155:421613", + "eip155:137", + "eip155:80001", + "eip155:1101", + "eip155:42220", + "eip155:1313161554", + "eip155:1313161555", + "eip155:56", + "eip155:56", + "eip155:43114", + "eip155:43113", + "eip155:324", + "eip155:280", + "near", + "eip155:100", + "solana:4sgjmw1sunhzsxgspuhpqldx6wiyjntz", + "eip155:8453", + "eip155:84531", + "eip155:7777777", + "eip155:999", +]; + +#[derive(Debug, Clone)] +pub struct BlockchainApiProvider { + project_id: ProjectId, +} + +impl BlockchainApiProvider { + pub fn new(project_id: ProjectId) -> Self { + Self { project_id } + } +} + +impl GetRpcUrl for BlockchainApiProvider { + fn get_rpc_url(&self, chain_id: String) -> Option { + if SUPPORTED_CHAINS.contains(&chain_id.as_str()) { + Some( + format!( + "https://rpc.walletconnect.com/v1?chainId={chain_id}&projectId={}", + self.project_id + ) + .parse() + .expect("Provider URL should be valid"), + ) + } else { + None + } + } +} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs new file mode 100644 index 0000000..9639c17 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs @@ -0,0 +1,5 @@ +use url::Url; + +pub trait GetRpcUrl { + fn get_rpc_url(&self, chain_id: String) -> Option; +} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs new file mode 100644 index 0000000..5c6184b --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs @@ -0,0 +1,98 @@ +use { + super::CacaoError, + alloy_primitives::{Address, FixedBytes}, + alloy_providers::provider::{Provider, TempProvider}, + alloy_rpc_types::{CallInput, CallRequest}, + alloy_sol_types::{sol, SolCall}, + alloy_transport_http::Http, + url::Url, +}; + +pub mod blockchain_api; +pub mod get_rpc_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 { + let provider = Provider::new(Http::new(provider)); + + let call_request = CallRequest { + to: Some(address), + input: CallInput::new( + isValidSignatureCall { + _hash: FixedBytes::from(hash), + _signature: signature, + } + .abi_encode() + .into(), + ), + ..Default::default() + }; + + let result = provider.call(call_request, None).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) + } + })?; + + if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { + Ok(true) + } else { + Err(CacaoError::Verification) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix}, + alloy_primitives::address, + sha3::{Digest, Keccak256}, + }; + + // Manual test. Paste address, signature, message, and project ID to verify + // function + #[tokio::test] + #[ignore] + async fn test_eip1271() { + 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(); + assert!(verify_eip1271(signature, address, hash, provider) + .await + .unwrap()); + } +} diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs new file mode 100644 index 0000000..4cb180d --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -0,0 +1,39 @@ +use { + super::CacaoError, + crate::auth::cacao::signature::strip_hex_prefix, + sha3::{Digest, Keccak256}, +}; + +pub const EIP191: &str = "eip191"; + +pub fn eip191_bytes(message: &str) -> Vec { + format!( + "\u{0019}Ethereum Signed Message:\n{}{}", + message.as_bytes().len(), + message + ) + .into() +} + +pub fn verify_eip191(signature: &[u8], address: &str, hash: Keccak256) -> Result { + use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; + + let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; + let recovery_id = + RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; + + let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) + .map_err(|_| CacaoError::Verification)?; + + let add = &Keccak256::default() + .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) + .finalize()[12..]; + + let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); + + if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { + Err(CacaoError::Verification) + } else { + Ok(true) + } +} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs new file mode 100644 index 0000000..bd2c4b2 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -0,0 +1,63 @@ +use { + self::{ + eip1271::{get_rpc_url::GetRpcUrl, verify_eip1271, EIP1271}, + eip191::{eip191_bytes, verify_eip191, EIP191}, + }, + super::{Cacao, CacaoError}, + alloy_primitives::Address, + serde::{Deserialize, Serialize}, + sha3::{Digest, Keccak256}, + std::str::FromStr, +}; + +pub mod eip1271; +pub mod eip191; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] +pub struct Signature { + pub t: String, + pub s: String, +} + +impl Signature { + pub async fn verify( + &self, + cacao: &Cacao, + get_provider: &impl GetRpcUrl, + ) -> Result { + 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()?)); + + match self.t.as_str() { + EIP191 => verify_eip191(&signature, &address, hash), + EIP1271 => { + let chain_id = cacao.p.chain_id_reference()?; + let provider = get_provider.get_rpc_url(chain_id); + 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) + } + } + _ => 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/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 2c8c8b8..74c01d4 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,8 +1,16 @@ -use crate::auth::cacao::Cacao; +use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; + +struct MockGetRpcUrl; + +impl GetRpcUrl for MockGetRpcUrl { + fn get_rpc_url(&self, _: String) -> Option { + None + } +} /// Test that we can verify a deprecated Cacao. -#[test] -fn cacao_verify_success() { +#[tokio::test] +async fn cacao_verify_success() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -24,7 +32,7 @@ fn cacao_verify_success() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -37,8 +45,8 @@ fn cacao_verify_success() { } /// Test that we can verify a updated Cacao. -#[test] -fn cacao_verify_success_identity_in_audience() { +#[tokio::test] +async fn cacao_verify_success_identity_in_audience() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -61,7 +69,7 @@ fn cacao_verify_success_identity_in_audience() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_ok()); assert!(result.map_err(|_| false).unwrap()); @@ -74,8 +82,8 @@ fn cacao_verify_success_identity_in_audience() { } /// Test that we can verify a Cacao -#[test] -fn cacao_verify_failure() { +#[tokio::test] +async fn cacao_verify_failure() { let cacao_serialized = r#"{ "h": { "t": "eip4361" @@ -97,6 +105,6 @@ fn cacao_verify_failure() { } }"#; let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); - let result = cacao.verify(); + let result = cacao.verify(&MockGetRpcUrl).await; assert!(result.is_err()); }