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

signP256 Cheat #6797

Merged
merged 10 commits into from
Jan 16, 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
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ serde_json.workspace = true
base64.workspace = true
tracing.workspace = true
walkdir = "2"
p256 = "0.13.2"
22 changes: 21 additions & 1 deletion crates/cheatcodes/assets/cheatcodes.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,14 @@ interface Vm {
#[cheatcode(group = Evm, safety = Unsafe)]
function loadAllocs(string calldata pathToAllocsJson) external;

/// Signs data.
/// Signs `digest` with `privateKey` using the secp256k1 curve.
#[cheatcode(group = Evm, safety = Safe)]
wilsoncusack marked this conversation as resolved.
Show resolved Hide resolved
function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s);

/// Signs `digest` with `privateKey` using the secp256r1 curve.
#[cheatcode(group = Evm, safety = Safe)]
function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s);
mds1 marked this conversation as resolved.
Show resolved Hide resolved

// -------- Record Storage --------

/// Records all storage reads and writes.
Expand Down
7 changes: 7 additions & 0 deletions crates/cheatcodes/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ impl Cheatcode for sign_0Call {
}
}

impl Cheatcode for signP256Call {
fn apply_full<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { privateKey, digest } = self;
super::utils::sign_p256(privateKey, digest, ccx.state)
}
}

impl Cheatcode for recordCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
Expand Down
80 changes: 80 additions & 0 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use ethers_signers::{
};
use foundry_common::types::{ToAlloy, ToEthers};
use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER;
use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey as P256SigningKey};

/// The BIP32 default derivation path prefix.
const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
Expand Down Expand Up @@ -171,6 +172,22 @@ pub(super) fn sign(private_key: &U256, digest: &B256, chain_id: u64) -> Result {
Ok((sig.v, r_bytes, s_bytes).abi_encode())
}

pub(super) fn sign_p256(private_key: &U256, digest: &B256, _state: &mut Cheatcodes) -> Result {
ensure!(*private_key != U256::ZERO, "private key cannot be 0");
let n = U256::from_limbs(*p256::NistP256::ORDER.as_words());
ensure!(
*private_key < n,
format!("private key must be less than the secp256r1 curve order ({})", n),
);
let bytes = private_key.to_be_bytes();
let signing_key = P256SigningKey::from_bytes((&bytes).into())?;
let signature: Signature = signing_key.sign_prehash(digest.as_slice())?;
let r_bytes: [u8; 32] = signature.r().to_bytes().into();
let s_bytes: [u8; 32] = signature.s().to_bytes().into();

Ok((r_bytes, s_bytes).abi_encode())
}

pub(super) fn parse_private_key(private_key: &U256) -> Result<SigningKey> {
ensure!(*private_key != U256::ZERO, "private key cannot be 0");
ensure!(
Expand Down Expand Up @@ -219,3 +236,66 @@ fn derive_key<W: Wordlist>(mnemonic: &str, path: &str, index: u32) -> Result {
let private_key = U256::from_be_bytes(wallet.signer().to_bytes().into());
Ok(private_key.abi_encode())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::CheatsConfig;
use alloy_primitives::FixedBytes;
use hex::FromHex;
use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature};
use std::{path::PathBuf, sync::Arc};

fn cheats() -> Cheatcodes {
let config = CheatsConfig {
ffi: true,
root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
..Default::default()
};
Cheatcodes { config: Arc::new(config), ..Default::default() }
}

#[test]
fn test_sign_p256() {
use p256::ecdsa::VerifyingKey;

let pk_u256: U256 = "1".parse().unwrap();
let signing_key = P256SigningKey::from_bytes(&pk_u256.to_be_bytes().into()).unwrap();
let digest = FixedBytes::from_hex(
"0x44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56",
)
.unwrap();
let mut cheats = cheats();

let result = sign_p256(&pk_u256, &digest, &mut cheats).unwrap();
let result_bytes: [u8; 64] = result.try_into().unwrap();
let signature = Signature::from_bytes(&result_bytes.into()).unwrap();
let verifying_key = VerifyingKey::from(&signing_key);
assert!(verifying_key.verify_prehash(digest.as_slice(), &signature).is_ok());
}

#[test]
fn test_sign_p256_pk_too_large() {
// max n from https://neuromancer.sk/std/secg/secp256r1
let pk =
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551".parse().unwrap();
let digest = FixedBytes::from_hex(
"0x54705ba3baafdbdfba8c5f9a70f7a89bee98d906b53e31074da7baecdc0da9ad",
)
.unwrap();
let mut cheats = cheats();
let result = sign_p256(&pk, &digest, &mut cheats);
assert_eq!(result.err().unwrap().to_string(), "private key must be less than the secp256r1 curve order (115792089210356248762697446949407573529996955224135760342422259061068512044369)");
}

#[test]
fn test_sign_p256_pk_0() {
let digest = FixedBytes::from_hex(
"0x54705ba3baafdbdfba8c5f9a70f7a89bee98d906b53e31074da7baecdc0da9ad",
)
.unwrap();
let mut cheats = cheats();
let result = sign_p256(&U256::ZERO, &digest, &mut cheats);
assert_eq!(result.err().unwrap().to_string(), "private key cannot be 0");
}
}
18 changes: 18 additions & 0 deletions testdata/cheats/SignP256.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "./Vm.sol";

contract SignTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testSignP256() public {
bytes32 pk = hex"A8568B74282DCC66FF70F10B4CE5CC7B391282F5381BBB4F4D8DD96974B16E6B";
bytes32 digest = hex"54705ba3baafdbdfba8c5f9a70f7a89bee98d906b53e31074da7baecdc0da9ad";

(bytes32 r, bytes32 s) = vm.signP256(uint256(pk), digest);
wilsoncusack marked this conversation as resolved.
Show resolved Hide resolved
assertEq(r, hex"7C11C3641B19E7822DB644CBF76ED0420A013928C2FD3E36D8EF983B103BDFE1");
assertEq(s, hex"317D89879868D484810D4E508A96109F8C87617B7BE9337411348D7B786F945F");
}
}
1 change: 1 addition & 0 deletions testdata/cheats/Vm.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.