Skip to content

Commit 2ce3a45

Browse files
authored
fix: string slice panic & automated smart contract tests (#76)
1 parent b4c8f4c commit 2ce3a45

File tree

13 files changed

+385
-21
lines changed

13 files changed

+385
-21
lines changed

.github/workflows/ci.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
rust: nightly
3636
- name: "Tests"
3737
cmd: nextest
38-
args: run --workspace --all-features --features cacao --retries 3
38+
args: run --workspace --all-features
3939
rust: stable
4040
- name: "Documentation Tests"
4141
cmd: test
@@ -54,6 +54,13 @@ jobs:
5454
profile: default
5555
override: true
5656

57+
- name: Install Foundry
58+
uses: foundry-rs/foundry-toolchain@v1
59+
60+
# pre-build contracts to avoid race condition installing solc during `forge create` in tests
61+
- name: Build contracts
62+
run: forge build -C contracts --cache-path=target/.forge/cache --out=target/.forge/out
63+
5764
- uses: Swatinem/rust-cache@v2
5865

5966
- uses: taiki-e/install-action@v1

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ required-features = ["client", "rpc"]
4141
[[example]]
4242
name = "webhook"
4343
required-features = ["client", "rpc"]
44+
45+
[lints.clippy]
46+
indexing_slicing = "deny"

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ The core Relay client. Provides access to all available Relay RPC methods to bui
1515

1616
Provides all of the Relay domain types (e.g. `ClientId`, `ProjectId` etc.) as well as auth token generation and validation functionality.
1717

18+
### Test dependencies
19+
20+
Foundry is required to be installed to your system for testing: <https://book.getfoundry.sh/getting-started/installation>
21+
1822
# License
1923

2024
[Apache License (Version 2.0)](LICENSE)

blockchain_api/Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
relay_rpc = { path = "../relay_rpc" }
7+
relay_rpc = { path = "../relay_rpc", features = ["cacao"] }
88
reqwest = { version = "0.12.2", features = ["json"] }
99
serde = "1.0"
1010
tokio = { version = "1.0", features = ["test-util", "macros"] }
1111
tracing = "0.1.40"
1212
url = "2"
13+
14+
[lints.clippy]
15+
indexing_slicing = "deny"

contracts/Eip1271Mock.sol

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
pragma solidity ^0.8.25;
2+
3+
// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation
4+
5+
contract Eip1271Mock {
6+
address owner;
7+
8+
constructor() {
9+
owner = msg.sender;
10+
}
11+
12+
/**
13+
* @notice Verifies that the signer is the owner of the signing contract.
14+
*/
15+
function isValidSignature(
16+
bytes32 _hash,
17+
bytes calldata _signature
18+
) external view returns (bytes4) {
19+
// Validate signatures
20+
if (recoverSigner(_hash, _signature) == owner) {
21+
return 0x1626ba7e;
22+
} else {
23+
return 0xffffffff;
24+
}
25+
}
26+
27+
/**
28+
* @notice Recover the signer of hash, assuming it's an EOA account
29+
* @dev Only for EthSign signatures
30+
* @param _hash Hash of message that was signed
31+
* @param _signature Signature encoded as (bytes32 r, bytes32 s, uint8 v)
32+
*/
33+
function recoverSigner(
34+
bytes32 _hash,
35+
bytes memory _signature
36+
) internal pure returns (address signer) {
37+
require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");
38+
39+
// Variables are not scoped in Solidity.
40+
uint8 v = uint8(_signature[64]);
41+
bytes32 r;
42+
bytes32 s;
43+
assembly {
44+
// Slice the signature into r and s components
45+
r := mload(add(_signature, 32))
46+
s := mload(add(_signature, 64))
47+
}
48+
49+
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
50+
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
51+
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
52+
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
53+
//
54+
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
55+
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
56+
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
57+
// these malleable signatures as well.
58+
//
59+
// Source OpenZeppelin
60+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol
61+
62+
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
63+
revert("SignatureValidator#recoverSigner: invalid signature 's' value");
64+
}
65+
66+
if (v != 27 && v != 28) {
67+
revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
68+
}
69+
70+
// Recover ECDSA signer
71+
signer = ecrecover(_hash, v, r, s);
72+
73+
// Prevent signer from being 0x0
74+
require(
75+
signer != address(0x0),
76+
"SignatureValidator#recoverSigner: INVALID_SIGNER"
77+
);
78+
79+
return signer;
80+
}
81+
}

relay_client/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ tokio-tungstenite = "0.21.0"
2828
futures-channel = "0.3"
2929
tokio-stream = "0.1"
3030
tokio-util = "0.7"
31+
32+
[lints.clippy]
33+
indexing_slicing = "deny"

relay_rpc/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ cacao = [
1515
"dep:alloy-json-abi",
1616
"dep:alloy-sol-types",
1717
"dep:alloy-primitives",
18+
"dep:alloy-node-bindings"
1819
]
1920

2021
[dependencies]
@@ -48,10 +49,14 @@ alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e
4849
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
4950
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
5051
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
52+
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true }
5153
alloy-json-abi = { version = "0.6.2", optional = true }
5254
alloy-sol-types = { version = "0.6.2", optional = true }
5355
alloy-primitives = { version = "0.6.2", optional = true }
5456
strum = { version = "0.26", features = ["strum_macros", "derive"] }
5557

5658
[dev-dependencies]
5759
tokio = { version = "1.35.1", features = ["test-util", "macros"] }
60+
61+
[lints.clippy]
62+
indexing_slicing = "deny"

relay_rpc/src/auth/cacao.rs

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use {
44
payload::Payload,
55
signature::{eip1271::get_rpc_url::GetRpcUrl, Signature},
66
},
7+
alloy_primitives::hex::FromHexError,
78
core::fmt::Debug,
89
serde::{Deserialize, Serialize},
910
serde_json::value::RawValue,
@@ -32,6 +33,9 @@ pub enum CacaoError {
3233
#[error("Invalid address")]
3334
AddressInvalid,
3435

36+
#[error("Address not EIP-191")]
37+
AddressNotEip191(FromHexError),
38+
3539
#[error("EIP-1271 signatures not supported")]
3640
Eip1271NotSupported,
3741

relay_rpc/src/auth/cacao/signature/eip1271/mod.rs

+99-4
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ pub async fn verify_eip1271(
5656
}
5757
})?;
5858

59-
if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() {
60-
Ok(true)
59+
let magic = result.get(..4);
60+
if let Some(magic) = magic {
61+
if magic == MAGIC_VALUE.to_be_bytes().to_vec() {
62+
Ok(true)
63+
} else {
64+
Err(CacaoError::Verification)
65+
}
6166
} else {
6267
Err(CacaoError::Verification)
6368
}
@@ -67,16 +72,21 @@ pub async fn verify_eip1271(
6772
mod test {
6873
use {
6974
super::*,
70-
crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix},
75+
crate::auth::cacao::signature::{
76+
eip191::eip191_bytes,
77+
strip_hex_prefix,
78+
test_helpers::{deploy_contract, message_hash, sign_message, spawn_anvil},
79+
},
7180
alloy_primitives::address,
81+
k256::ecdsa::SigningKey,
7282
sha3::{Digest, Keccak256},
7383
};
7484

7585
// Manual test. Paste address, signature, message, and project ID to verify
7686
// function
7787
#[tokio::test]
7888
#[ignore]
79-
async fn test_eip1271() {
89+
async fn test_eip1271_manual() {
8090
let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
8191
let signature = "xxx";
8292
let signature = data_encoding::HEXLOWER_PERMISSIVE
@@ -94,4 +104,89 @@ mod test {
94104
.await
95105
.unwrap());
96106
}
107+
108+
#[tokio::test]
109+
async fn test_eip1271_pass() {
110+
let (_anvil, rpc_url, private_key) = spawn_anvil().await;
111+
let contract_address = deploy_contract(&rpc_url, &private_key).await;
112+
113+
let message = "xxx";
114+
let signature = sign_message(message, &private_key);
115+
116+
assert!(
117+
verify_eip1271(signature, contract_address, &message_hash(message), rpc_url)
118+
.await
119+
.unwrap()
120+
);
121+
}
122+
123+
#[tokio::test]
124+
async fn test_eip1271_wrong_signature() {
125+
let (_anvil, rpc_url, private_key) = spawn_anvil().await;
126+
let contract_address = deploy_contract(&rpc_url, &private_key).await;
127+
128+
let message = "xxx";
129+
let mut signature = sign_message(message, &private_key);
130+
*signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1);
131+
132+
assert!(matches!(
133+
verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await,
134+
Err(CacaoError::Verification)
135+
));
136+
}
137+
138+
#[tokio::test]
139+
async fn test_eip1271_fail_wrong_signer() {
140+
let (anvil, rpc_url, private_key) = spawn_anvil().await;
141+
let contract_address = deploy_contract(&rpc_url, &private_key).await;
142+
143+
let message = "xxx";
144+
let signature = sign_message(
145+
message,
146+
&SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(),
147+
);
148+
149+
assert!(matches!(
150+
verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await,
151+
Err(CacaoError::Verification)
152+
));
153+
}
154+
155+
#[tokio::test]
156+
async fn test_eip1271_fail_wrong_contract_address() {
157+
let (_anvil, rpc_url, private_key) = spawn_anvil().await;
158+
let mut contract_address = deploy_contract(&rpc_url, &private_key).await;
159+
160+
*contract_address.0.first_mut().unwrap() =
161+
contract_address.0.first().unwrap().wrapping_add(1);
162+
163+
let message = "xxx";
164+
let signature = sign_message(message, &private_key);
165+
166+
assert!(matches!(
167+
verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await,
168+
Err(CacaoError::Verification)
169+
));
170+
}
171+
172+
#[tokio::test]
173+
async fn test_eip1271_wrong_message() {
174+
let (_anvil, rpc_url, private_key) = spawn_anvil().await;
175+
let contract_address = deploy_contract(&rpc_url, &private_key).await;
176+
177+
let message = "xxx";
178+
let signature = sign_message(message, &private_key);
179+
180+
let message2 = "yyy";
181+
assert!(matches!(
182+
verify_eip1271(
183+
signature,
184+
contract_address,
185+
&message_hash(message2),
186+
rpc_url
187+
)
188+
.await,
189+
Err(CacaoError::Verification)
190+
));
191+
}
97192
}

0 commit comments

Comments
 (0)