Skip to content
Open
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
4 changes: 3 additions & 1 deletion .config/forest.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
271
273
Algorand/M
API's
API/SM
Expand Down Expand Up @@ -99,6 +99,7 @@ Filecoin/M
Filfox
Filops
FilOz
Filsnap
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's is not being used anywhere, you can remove this.

FIP/S
FVM
GC
Expand All @@ -121,6 +122,7 @@ implementer/S
implementor/SM
infeasible
interop/S
interoperating
ip
IPFS
IPLD
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
### Breaking

- [#7073](https://github.com/ChainSafe/forest/pull/7073): Replaced the underlying cache engine across the node. The eviction policy is no longer strict LRU — it is now CLOCK-PRO via [`quick_cache`](https://crates.io/crates/quick_cache), which is scan-resistant and typically gives higher hit rates on chain workloads. The Prometheus metric names `lru_cache_hit_total` and `lru_cache_miss_total` are renamed to `cache_hit_total` and `cache_miss_total`. **Operators must update dashboards, alert rules, and recording rules** that reference the old names. Label set (`kind="..."`) is unchanged.
- [#6442](https://github.com/ChainSafe/forest/issues/6442): `forest-wallet sign` and `forest-wallet verify` now apply the FRC-0102 signing envelope to the message by default. Pass `--raw` on both sides to reproduce the previous raw-bytes behaviour.

### Added

Expand Down
117 changes: 110 additions & 7 deletions src/wallet/subcommands/wallet_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ use crate::{
rpc::{self, prelude::*},
};
use anyhow::{Context as _, bail};
use base64::{Engine, prelude::BASE64_STANDARD};
use clap::Subcommand;
use dialoguer::{Password, console::Term, theme::ColorfulTheme};
use directories::ProjectDirs;
Expand Down Expand Up @@ -169,17 +168,17 @@ impl WalletBackend {
}
}

async fn wallet_sign(&self, address: Address, message: String) -> anyhow::Result<Signature> {
async fn wallet_sign(&self, address: Address, message: Vec<u8>) -> anyhow::Result<Signature> {
if let Some(keystore) = &self.local {
let key = crate::key_management::try_find_key(&address, keystore)?;

Ok(crate::key_management::sign(
*key.key_info.key_type(),
key.key_info.private_key(),
&BASE64_STANDARD.decode(message)?,
&message,
)?)
} else {
Ok(WalletSign::call(&self.remote, (address, message.into_bytes())).await?)
Ok(WalletSign::call(&self.remote, (address, message)).await?)
}
}

Expand Down Expand Up @@ -262,6 +261,11 @@ pub enum WalletCommands {
/// The address to be used to sign the message
#[arg(short)]
address: StrictAddress,
/// Sign the raw message bytes without the FRC-0102 envelope. Use this
/// for interoperating with pre-FRC-0102 tooling, or when the bytes are
/// already an on-chain Filecoin message (which must not be wrapped).
#[arg(long)]
raw: bool,
},
/// Validates whether a given string can be decoded as a well-formed address
ValidateAddress {
Expand All @@ -280,6 +284,12 @@ pub enum WalletCommands {
/// The signature of the message to verify
#[arg(short)]
signature: String,
/// Verify against the raw message bytes without applying the
/// FRC-0102 envelope. Use this for signatures produced by
/// pre-FRC-0102 tooling or for on-chain Filecoin messages (which are
/// signed raw, without the envelope).
#[arg(long)]
raw: bool,
},
/// Deletes the wallet associated with the given address.
Delete {
Expand Down Expand Up @@ -446,9 +456,13 @@ impl WalletCommands {
Ok(())
}
Self::SetDefault { key } => backend.wallet_set_default(key.into()).await,
Self::Sign { address, message } => {
Self::Sign {
address,
message,
raw,
} => {
let message = hex::decode(message).context("Message has to be a hex string")?;
let message = BASE64_STANDARD.encode(message);
let message = if raw { message } else { wrap_frc0102(&message) };

let signature = backend.wallet_sign(address.into(), message).await?;
println!("{}", hex::encode(signature.to_bytes()));
Expand All @@ -463,10 +477,12 @@ impl WalletCommands {
message,
address,
signature,
raw,
} => {
let sig_bytes =
hex::decode(signature).context("Signature has to be a hex string")?;
let msg = hex::decode(message).context("Message has to be a hex string")?;
let msg = if raw { msg } else { wrap_frc0102(&msg) };

let signature = Signature::from_bytes(sig_bytes)?;
let is_valid = backend
Expand Down Expand Up @@ -615,6 +631,15 @@ fn resolve_target_address(target_address: &str) -> anyhow::Result<(Address, bool
}
}

const FRC_0102_FILECOIN_PREFIX: &[u8] = b"\x19Filecoin Signed Message:\n";

/// Wraps `msg` with the FRC-0102 envelope: `0x19 || "Filecoin Signed Message:\n" || ascii(len(msg)) || msg`
// See <https://github.com/filecoin-project/FIPs/blob/bdd5283279fd115c87c9bbf71d2e40c9d075f5aa/FRCs/frc-0102.md>.
fn wrap_frc0102(msg: &[u8]) -> Vec<u8> {
let len = msg.len().to_string();
[FRC_0102_FILECOIN_PREFIX, len.as_bytes(), msg].concat()
}

fn resolve_method_num(from: &Address, to: &Address, is_0x_recipient: bool) -> u64 {
if !is_eth_address(from) && !is_0x_recipient {
return METHOD_SEND;
Expand All @@ -635,7 +660,7 @@ mod tests {
use crate::shim::address::{Address, CurrentNetwork, Network};
use crate::shim::message::METHOD_SEND;

use super::{resolve_method_num, resolve_target_address};
use super::{SignatureType, resolve_method_num, resolve_target_address, wrap_frc0102};

#[test]
fn test_resolve_target_address_id() {
Expand Down Expand Up @@ -753,4 +778,82 @@ mod tests {
let method = resolve_method_num(&from, &to, true);
assert_eq!(method, EVMMethod::InvokeContract as u64);
}

#[test]
fn test_wrap_frc0102_empty() {
let wrapped = wrap_frc0102(&[]);
assert_eq!(wrapped, b"\x19Filecoin Signed Message:\n0");
}

#[test]
fn test_wrap_frc0102_short() {
let wrapped = wrap_frc0102(b"hello");
assert_eq!(wrapped, b"\x19Filecoin Signed Message:\n5hello");
}

#[test]
fn test_wrap_frc0102_longer() {
let msg = b"this is a longer test message";
let wrapped = wrap_frc0102(msg);
let mut expected = Vec::new();
expected.extend_from_slice(b"\x19Filecoin Signed Message:\n");
expected.extend_from_slice(msg.len().to_string().as_bytes());
expected.extend_from_slice(msg);
assert_eq!(wrapped, expected);
}

#[test]
fn test_wrap_frc0102_binary_msg() {
// Non-UTF-8 bytes must pass through unchanged.
let msg: &[u8] = &[0x00, 0xFF, 0x10, 0x80];
let wrapped = wrap_frc0102(msg);
assert_eq!(&wrapped[..26], b"\x19Filecoin Signed Message:\n");
assert_eq!(&wrapped[26..27], b"4");
assert_eq!(&wrapped[27..], msg);
}

#[test]
fn test_wrap_frc0102_length_boundaries() {
// Length is encoded as decimal ASCII; check 1-, 2-, 3- and 4-digit boundaries.
for len in [0, 9, 10, 99, 100, 999, 1000] {
let msg = vec![0xABu8; len];
let wrapped = wrap_frc0102(&msg);
let digits = len.to_string();
assert_eq!(&wrapped[..26], b"\x19Filecoin Signed Message:\n");
assert_eq!(&wrapped[26..26 + digits.len()], digits.as_bytes());
assert_eq!(&wrapped[26 + digits.len()..], msg.as_slice());
}
}

#[test]
fn test_wrap_frc0102_contains_newline() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can combine above tests into single test using the rstest and cases. Since they are all roughly checking the same thing.

// The spec does not require any escaping; embedded newlines pass through.
let msg = b"line1\nline2";
let wrapped = wrap_frc0102(msg);
assert_eq!(wrapped, b"\x19Filecoin Signed Message:\n11line1\nline2");
}

#[test]
fn test_frc0102_roundtrip_sign_verify_secp256k1() {
use crate::key_management::generate_key;
let key = generate_key(SignatureType::Secp256k1).unwrap();
let raw_msg = b"hello world";
let wrapped = wrap_frc0102(raw_msg);

// Sign the wrapped bytes (what `forest-wallet sign` does by default).
let signature = crate::key_management::sign(
*key.key_info.key_type(),
key.key_info.private_key(),
&wrapped,
)
.unwrap();

// `verify` on the wrapped bytes must succeed.
signature.verify(&wrapped, &key.address).unwrap();

assert!(
signature.verify(raw_msg, &key.address).is_err(),
"raw-bytes verify should fail when the signature was produced over the FRC-0102 envelope"
);
}
}
Loading