Skip to content

Commit

Permalink
signP256 Cheat (#6797)
Browse files Browse the repository at this point in the history
* started, need to test

* update lock

* track Vm.sol diff

* SignP256.t.sol

* added tests, fix tests, other cleanup

* update json

* improve tests

* add comment

* CI clippy

* typo
  • Loading branch information
wilsoncusack authored Jan 16, 2024
1 parent 04ccec7 commit 36044da
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 2 deletions.
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)]
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);

// -------- 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);
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.

0 comments on commit 36044da

Please sign in to comment.