Skip to content

Commit 7a20551

Browse files
authored
feat: EIP-1271 support (#55)
1 parent b4683d7 commit 7a20551

File tree

10 files changed

+328
-88
lines changed

10 files changed

+328
-88
lines changed

justfile

+15
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ fmt:
7272
echo ' ^^^^^^ To install `rustup component add rustfmt`, see https://github.com/rust-lang/rustfmt for details'
7373
fi
7474

75+
fmt-imports:
76+
#!/bin/bash
77+
set -euo pipefail
78+
79+
if command -v cargo-fmt >/dev/null; then
80+
echo '==> Running rustfmt'
81+
cargo +nightly fmt -- --config group_imports=StdExternalCrate,imports_granularity=One
82+
else
83+
echo '==> rustfmt not found in PATH, skipping'
84+
fi
85+
86+
unit: lint test test-all
87+
88+
devloop: unit fmt-imports
89+
7590
# Run commit checker
7691
commit-check:
7792
#!/bin/bash

relay_rpc/Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,17 @@ once_cell = "1.16"
2222
jsonwebtoken = "8.1"
2323
k256 = { version = "0.13", optional = true }
2424
sha3 = { version = "0.10", optional = true }
25-
sha2 = { version = "0.10.6" }
25+
sha2 = { version = "0.10.6" }
26+
reqwest = { version = "0.11", features = ["default-tls"] }
27+
url = "2"
28+
alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" }
29+
alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" }
30+
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" }
31+
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" }
32+
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1" }
33+
alloy-json-abi = "0.6.2"
34+
alloy-sol-types = "0.6.2"
35+
alloy-primitives = "0.6.2"
36+
37+
[dev-dependencies]
38+
tokio = { version = "1.35.1", features = ["test-util", "macros"] }

relay_rpc/src/auth/cacao.rs

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
use {
2-
self::{header::Header, payload::Payload, signature::Signature},
2+
self::{
3+
header::Header,
4+
payload::Payload,
5+
signature::{eip1271::get_rpc_url::GetRpcUrl, Signature},
6+
},
37
core::fmt::Debug,
48
serde::{Deserialize, Serialize},
9+
serde_json::value::RawValue,
510
std::fmt::{Display, Write},
611
};
712

@@ -21,11 +26,20 @@ pub enum CacaoError {
2126
#[error("Invalid payload resources")]
2227
PayloadResources,
2328

29+
#[error("Invalid address")]
30+
AddressInvalid,
31+
2432
#[error("Unsupported signature type")]
2533
UnsupportedSignature,
2634

35+
#[error("Provider not available for that chain")]
36+
ProviderNotAvailable,
37+
2738
#[error("Unable to verify")]
2839
Verification,
40+
41+
#[error("Internal EIP-1271 resolution error: {0}")]
42+
Eip1271Internal(alloy_json_rpc::RpcError<alloy_transport::TransportErrorKind, Box<RawValue>>),
2943
}
3044

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

80-
pub fn verify(&self) -> Result<bool, CacaoError> {
94+
pub async fn verify(&self, provider: &impl GetRpcUrl) -> Result<bool, CacaoError> {
8195
self.p.validate()?;
8296
self.h.validate()?;
83-
self.s.verify(self)
97+
self.s.verify(self, provider).await
8498
}
8599

86100
pub fn siwe_message(&self) -> Result<String, CacaoError> {

relay_rpc/src/auth/cacao/signature.rs

-74
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use {super::get_rpc_url::GetRpcUrl, crate::domain::ProjectId, url::Url};
2+
3+
// https://github.com/WalletConnect/blockchain-api/blob/master/SUPPORTED_CHAINS.md
4+
const SUPPORTED_CHAINS: [&str; 26] = [
5+
"eip155:1",
6+
"eip155:5",
7+
"eip155:11155111",
8+
"eip155:10",
9+
"eip155:420",
10+
"eip155:42161",
11+
"eip155:421613",
12+
"eip155:137",
13+
"eip155:80001",
14+
"eip155:1101",
15+
"eip155:42220",
16+
"eip155:1313161554",
17+
"eip155:1313161555",
18+
"eip155:56",
19+
"eip155:56",
20+
"eip155:43114",
21+
"eip155:43113",
22+
"eip155:324",
23+
"eip155:280",
24+
"near",
25+
"eip155:100",
26+
"solana:4sgjmw1sunhzsxgspuhpqldx6wiyjntz",
27+
"eip155:8453",
28+
"eip155:84531",
29+
"eip155:7777777",
30+
"eip155:999",
31+
];
32+
33+
#[derive(Debug, Clone)]
34+
pub struct BlockchainApiProvider {
35+
project_id: ProjectId,
36+
}
37+
38+
impl BlockchainApiProvider {
39+
pub fn new(project_id: ProjectId) -> Self {
40+
Self { project_id }
41+
}
42+
}
43+
44+
impl GetRpcUrl for BlockchainApiProvider {
45+
fn get_rpc_url(&self, chain_id: String) -> Option<Url> {
46+
if SUPPORTED_CHAINS.contains(&chain_id.as_str()) {
47+
Some(
48+
format!(
49+
"https://rpc.walletconnect.com/v1?chainId={chain_id}&projectId={}",
50+
self.project_id
51+
)
52+
.parse()
53+
.expect("Provider URL should be valid"),
54+
)
55+
} else {
56+
None
57+
}
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use url::Url;
2+
3+
pub trait GetRpcUrl {
4+
fn get_rpc_url(&self, chain_id: String) -> Option<Url>;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use {
2+
super::CacaoError,
3+
alloy_primitives::{Address, FixedBytes},
4+
alloy_providers::provider::{Provider, TempProvider},
5+
alloy_rpc_types::{CallInput, CallRequest},
6+
alloy_sol_types::{sol, SolCall},
7+
alloy_transport_http::Http,
8+
url::Url,
9+
};
10+
11+
pub mod blockchain_api;
12+
pub mod get_rpc_url;
13+
14+
pub const EIP1271: &str = "eip1271";
15+
16+
// https://eips.ethereum.org/EIPS/eip-1271
17+
const MAGIC_VALUE: u32 = 0x1626ba7e;
18+
sol! {
19+
function isValidSignature(
20+
bytes32 _hash,
21+
bytes memory _signature)
22+
public
23+
view
24+
returns (bytes4 magicValue);
25+
}
26+
27+
pub async fn verify_eip1271(
28+
signature: Vec<u8>,
29+
address: Address,
30+
hash: &[u8; 32],
31+
provider: Url,
32+
) -> Result<bool, CacaoError> {
33+
let provider = Provider::new(Http::new(provider));
34+
35+
let call_request = CallRequest {
36+
to: Some(address),
37+
input: CallInput::new(
38+
isValidSignatureCall {
39+
_hash: FixedBytes::from(hash),
40+
_signature: signature,
41+
}
42+
.abi_encode()
43+
.into(),
44+
),
45+
..Default::default()
46+
};
47+
48+
let result = provider.call(call_request, None).await.map_err(|e| {
49+
if let Some(error_response) = e.as_error_resp() {
50+
if error_response.message.starts_with("execution reverted:") {
51+
CacaoError::Verification
52+
} else {
53+
CacaoError::Eip1271Internal(e)
54+
}
55+
} else {
56+
CacaoError::Eip1271Internal(e)
57+
}
58+
})?;
59+
60+
if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() {
61+
Ok(true)
62+
} else {
63+
Err(CacaoError::Verification)
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod test {
69+
use {
70+
super::*,
71+
crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix},
72+
alloy_primitives::address,
73+
sha3::{Digest, Keccak256},
74+
};
75+
76+
// Manual test. Paste address, signature, message, and project ID to verify
77+
// function
78+
#[tokio::test]
79+
#[ignore]
80+
async fn test_eip1271() {
81+
let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
82+
let signature = "xxx";
83+
let signature = data_encoding::HEXLOWER_PERMISSIVE
84+
.decode(strip_hex_prefix(signature).as_bytes())
85+
.map_err(|_| CacaoError::Verification)
86+
.unwrap();
87+
let message = "xxx";
88+
let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..]
89+
.try_into()
90+
.unwrap();
91+
let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx"
92+
.parse()
93+
.unwrap();
94+
assert!(verify_eip1271(signature, address, hash, provider)
95+
.await
96+
.unwrap());
97+
}
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use {
2+
super::CacaoError,
3+
crate::auth::cacao::signature::strip_hex_prefix,
4+
sha3::{Digest, Keccak256},
5+
};
6+
7+
pub const EIP191: &str = "eip191";
8+
9+
pub fn eip191_bytes(message: &str) -> Vec<u8> {
10+
format!(
11+
"\u{0019}Ethereum Signed Message:\n{}{}",
12+
message.as_bytes().len(),
13+
message
14+
)
15+
.into()
16+
}
17+
18+
pub fn verify_eip191(signature: &[u8], address: &str, hash: Keccak256) -> Result<bool, CacaoError> {
19+
use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey};
20+
21+
let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?;
22+
let recovery_id =
23+
RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?;
24+
25+
let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id)
26+
.map_err(|_| CacaoError::Verification)?;
27+
28+
let add = &Keccak256::default()
29+
.chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..])
30+
.finalize()[12..];
31+
32+
let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add);
33+
34+
if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() {
35+
Err(CacaoError::Verification)
36+
} else {
37+
Ok(true)
38+
}
39+
}

0 commit comments

Comments
 (0)