forc_wallet/
sign.rs

1use crate::account;
2use anyhow::{Context, Result, bail};
3use clap::{Args, Subcommand};
4use fuels::crypto::{Message, SecretKey, Signature};
5use fuels::types::Bytes32;
6use rpassword::prompt_password;
7use std::{
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12/// Sign some data (e.g. a transaction ID, a file, a string, or a hex-string)
13/// using either a wallet account or a private key.
14#[derive(Debug, Args)]
15pub struct Sign {
16    /// Sign using the wallet account at the given index.
17    /// Uses a discrete interactive prompt for password input.
18    #[clap(long, value_name = "ACCOUNT_INDEX")]
19    pub account: Option<usize>,
20    /// Sign using a private key.
21    /// Uses a discrete interactive prompt for collecting the private key.
22    #[clap(long)]
23    pub private_key: bool,
24    /// Sign by passing the private key directly.
25    ///
26    /// WARNING: This is primarily provided for non-interactive testing. Using this flag is
27    /// prone to leaving your private key exposed in your shell command history!
28    #[clap(long)]
29    pub private_key_non_interactive: Option<SecretKey>,
30    /// Directly provide the wallet password when signing with an account.
31    ///
32    /// WARNING: This is primarily provided for non-interactive testing. Using this flag is
33    /// prone to leaving your password exposed in your shell command history!
34    #[clap(long)]
35    pub password_non_interactive: Option<String>,
36    #[clap(subcommand)]
37    pub data: Data,
38}
39
40/// The data that is to be signed.
41#[derive(Debug, Subcommand)]
42pub enum Data {
43    /// Sign a transaction ID.
44    ///
45    /// The tx ID is signed directly, i.e. it is not re-hashed before signing.
46    ///
47    /// Previously `tx`, though renamed in anticipation of support for signing transaction files.
48    TxId { tx_id: Bytes32 },
49    /// Read the file at the given path into bytes and sign the raw data.
50    File { path: PathBuf },
51    /// Sign the given string as a slice of bytes.
52    String { string: String },
53    /// Parse the given hex-encoded byte string and sign the raw bytes.
54    ///
55    /// All characters must be within the range '0'..='f'. Each character pair
56    /// represents a single hex-encoded byte.
57    ///
58    /// The string may optionally start with the `0x` prefix which will be
59    /// discarded before decoding and signing the remainder of the string.
60    Hex { hex_string: String },
61}
62
63pub fn cli(ctx: &crate::CliContext, sign: Sign) -> Result<()> {
64    let Sign {
65        account,
66        private_key,
67        private_key_non_interactive,
68        password_non_interactive,
69        data,
70    } = sign;
71    match (
72        account,
73        password_non_interactive,
74        private_key,
75        private_key_non_interactive,
76    ) {
77        // Provided an account index, so we'll request the password.
78        (Some(acc_ix), None, false, None) => wallet_account_cli(ctx, acc_ix, data)?,
79        // Provided the password as a flag, so no need for interactive step.
80        (Some(acc_ix), Some(pw), false, None) => {
81            let msg = msg_from_data(data)?;
82            let sig = sign_msg_with_wallet_account(&ctx.wallet_path, acc_ix, &msg, &pw)?;
83            println!("Signature: {sig}");
84        }
85        // Provided the private key to sign with directly.
86        (None, None, _, Some(priv_key)) => {
87            let msg = msg_from_data(data)?;
88            let sig = Signature::sign(&priv_key, &msg);
89            println!("Signature: {sig}");
90        }
91        // Sign with a private key interactively.
92        (None, None, true, None) => private_key_cli(data)?,
93        // TODO: If the user provides neither account or private flags, ask in interactive mode?
94        _ => bail!(
95            "Unexpected set of options passed to `forc wallet sign`.\n  \
96                 To sign with a wallet account, use `forc wallet sign --account <index> <data>`\n  \
97                 To sign with a private key, use `forc wallet sign --private <data>`",
98        ),
99    }
100    Ok(())
101}
102
103pub(crate) fn wallet_account_cli(
104    ctx: &crate::CliContext,
105    account_ix: usize,
106    data: Data,
107) -> Result<()> {
108    let msg = msg_from_data(data)?;
109    sign_msg_with_wallet_account_cli(&ctx.wallet_path, account_ix, &msg)
110}
111
112pub(crate) fn private_key_cli(data: Data) -> Result<()> {
113    sign_msg_with_private_key_cli(&msg_from_data(data)?)
114}
115
116fn sign_msg_with_private_key_cli(msg: &Message) -> Result<()> {
117    let secret_key_input = prompt_password("Please enter the private key you wish to sign with: ")?;
118    let signature = sign_with_private_key_str(msg, &secret_key_input)?;
119    println!("Signature: {signature}");
120    Ok(())
121}
122
123fn sign_with_private_key_str(msg: &Message, priv_key_input: &str) -> Result<Signature> {
124    let secret_key = SecretKey::from_str(priv_key_input)?;
125    Ok(Signature::sign(&secret_key, msg))
126}
127
128fn sign_msg_with_wallet_account_cli(
129    wallet_path: &Path,
130    account_ix: usize,
131    msg: &Message,
132) -> Result<()> {
133    let password = prompt_password("Please enter your wallet password: ")?;
134    let signature = sign_msg_with_wallet_account(wallet_path, account_ix, msg, &password)?;
135    println!("Signature: {signature}");
136    Ok(())
137}
138
139fn sign_msg_with_wallet_account(
140    wallet_path: &Path,
141    account_ix: usize,
142    msg: &Message,
143    pw: &str,
144) -> Result<Signature> {
145    let secret_key = account::derive_secret_key(wallet_path, account_ix, pw)?;
146    Ok(Signature::sign(&secret_key, msg))
147}
148
149/// Cast the `Bytes32` directly to a message without normalizing it.
150/// We don't renormalize as a hash is already a normalized representation.
151fn msg_from_hash32(hash: Bytes32) -> Message {
152    Message::from_bytes(hash.into())
153}
154
155fn msg_from_file(path: &Path) -> Result<Message> {
156    let bytes = std::fs::read(path).context("failed to read bytes from path")?;
157    Ok(Message::new(bytes))
158}
159
160fn msg_from_hex_str(hex_str: &str) -> Result<Message> {
161    let bytes = bytes_from_hex_str(hex_str)?;
162    Ok(Message::new(bytes))
163}
164
165fn msg_from_data(data: Data) -> Result<Message> {
166    let msg = match data {
167        Data::TxId { tx_id } => msg_from_hash32(tx_id),
168        Data::File { path } => msg_from_file(&path)?,
169        Data::Hex { hex_string } => msg_from_hex_str(&hex_string)?,
170        Data::String { string } => Message::new(string),
171    };
172    Ok(msg)
173}
174
175fn bytes_from_hex_str(mut hex_str: &str) -> Result<Vec<u8>> {
176    // Check for the prefix.
177    const PREFIX: &str = "0x";
178    if hex_str.starts_with(PREFIX) {
179        hex_str = &hex_str[PREFIX.len()..];
180    } else {
181        bail!("missing 0x at the beginning of hex string")
182    }
183    hex::decode(hex_str).context("failed to decode bytes from hex string")
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::utils::test_utils::{TEST_PASSWORD, with_tmp_dir_and_wallet};
190    use fuels::crypto::Message;
191
192    #[test]
193    fn sign_tx_id() {
194        with_tmp_dir_and_wallet(|_dir, wallet_path| {
195            let tx_id = Bytes32::from_str(
196                "0x6c226b276bd2028c0582229b6396f91801c913973487491b0262c5c7b3cd6e39",
197            )
198            .unwrap();
199            let msg = msg_from_hash32(tx_id);
200            let account_ix = 0;
201            let sig =
202                sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
203            assert_eq!(
204                sig.to_string(),
205                "bcf4651f072130aaf8925610e1d719b76e25b19b0a86779d3f4294964f1607cc95eb6c58eb37bf0510f618bd284decdf936c48ec6722df5472084e4098d54620"
206            );
207        });
208    }
209
210    const TEST_STR: &str = "Blah blah blah";
211    const EXPECTED_SIG: &str = "b0b2f29b52d95c1cba47ea7c7edeec6c84a0bd196df489e219f6f388b69d760479b994f4bae2d5f2abef7d5faf7d9f5ee3ea47ada4d15b7a7ee2777dcd7b36bb";
212
213    #[test]
214    fn sign_string() {
215        with_tmp_dir_and_wallet(|_dir, wallet_path| {
216            let msg = Message::new(TEST_STR);
217            let account_ix = 0;
218            let sig =
219                sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
220            assert_eq!(sig.to_string(), EXPECTED_SIG);
221        });
222    }
223
224    #[test]
225    fn sign_file() {
226        with_tmp_dir_and_wallet(|dir, wallet_path| {
227            let path = dir.join("data");
228            std::fs::write(&path, TEST_STR).unwrap();
229            let msg = msg_from_file(&path).unwrap();
230            let account_ix = 0;
231            let sig =
232                sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
233            assert_eq!(sig.to_string(), EXPECTED_SIG);
234        });
235    }
236
237    #[test]
238    fn sign_hex() {
239        with_tmp_dir_and_wallet(|_dir, wallet_path| {
240            let hex_encoded = format!("0x{}", hex::encode(TEST_STR));
241            let msg = msg_from_hex_str(&hex_encoded).unwrap();
242            let account_ix = 0;
243            let sig =
244                sign_msg_with_wallet_account(wallet_path, account_ix, &msg, TEST_PASSWORD).unwrap();
245            assert_eq!(sig.to_string(), EXPECTED_SIG);
246        });
247    }
248}