Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: EIP-1271 support #55

Merged
merged 10 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion relay_rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
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"] }
20 changes: 17 additions & 3 deletions relay_rpc/src/auth/cacao.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand All @@ -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<alloy_transport::TransportErrorKind, Box<RawValue>>),
}

impl From<std::fmt::Error> for CacaoError {
Expand Down Expand Up @@ -77,10 +91,10 @@ pub struct Cacao {
impl Cacao {
const ETHEREUM: &'static str = "Ethereum";

pub fn verify(&self) -> Result<bool, CacaoError> {
pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result<bool, CacaoError> {
self.p.validate()?;
self.h.validate()?;
self.s.verify(self)
self.s.verify(self, provider).await
}

pub fn siwe_message(&self) -> Result<String, CacaoError> {
Expand Down
74 changes: 0 additions & 74 deletions relay_rpc/src/auth/cacao/signature.rs

This file was deleted.

59 changes: 59 additions & 0 deletions relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs
Original file line number Diff line number Diff line change
@@ -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] = [
Copy link
Member Author

@chris13524 chris13524 Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered checking the response of Blockchain API to determine support or not, but this would make the CACAO verification function more coupled to Blockchain API so decided to hardcode the supported list here for now.

In the future we should either pull the list dynamically, or refactor the CACAO to use a more generic error handling and be able to interpret the response code. I slightly prefer the check-before-request approach to avoid redundant requests.

Captured in this issue: WalletConnect/notify-server#345 (comment)

"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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using NotifyServer and Keys Server project_ids? Might be good to add it to allowlist to not have rate limiting there

Copy link
Member Author

@chris13524 chris13524 Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out!

This should be fine for now, as 1 request per second is 86k/day. Limit/day is 100k. Notify Server does 1-2 msgs/s and Keys Server does 0.1/s.

Also most people have EOAs not smart accounts. We'll add metrics soon for this so we can see more details.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we have metrics in Cloud already for RPC usage, so we should see this too as-is.

}

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<Url> {
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
}
}
}
5 changes: 5 additions & 0 deletions relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use url::Url;

pub trait GetRpcUrl {
fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
}
98 changes: 98 additions & 0 deletions relay_rpc/src/auth/cacao/signature/eip1271/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
address: Address,
hash: &[u8; 32],
provider: Url,
) -> Result<bool, CacaoError> {
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());
}
}
39 changes: 39 additions & 0 deletions relay_rpc/src/auth/cacao/signature/eip191.rs
Original file line number Diff line number Diff line change
@@ -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<u8> {
format!(
"\u{0019}Ethereum Signed Message:\n{}{}",
message.as_bytes().len(),
message
)
.into()
}

pub fn verify_eip191(signature: &[u8], address: &str, hash: Keccak256) -> Result<bool, CacaoError> {
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)
}
}
Loading
Loading