mirror of
https://github.com/ProvableHQ/leo.git
synced 2024-10-26 15:13:13 +03:00
Merge pull request #27627 from Meshiest/feat-cli-sign
feat(cli): add sign and verify to the account subcommand
This commit is contained in:
commit
993819516c
@ -34,6 +34,22 @@ create_messages!(
|
||||
help: None,
|
||||
}
|
||||
|
||||
/// For when the CLI is given invalid user input.
|
||||
@backtraced
|
||||
cli_invalid_input {
|
||||
args: (error: impl Display),
|
||||
msg: format!("cli input error: {error}"),
|
||||
help: None,
|
||||
}
|
||||
|
||||
/// For when the CLI fails to run something
|
||||
@backtraced
|
||||
cli_runtime_error {
|
||||
args: (error: impl Display),
|
||||
msg: format!("cli error: {error}"),
|
||||
help: None,
|
||||
}
|
||||
|
||||
/// For when the CLI could not fetch the versions.
|
||||
@backtraced
|
||||
could_not_fetch_versions {
|
||||
|
@ -48,7 +48,7 @@ enum Commands {
|
||||
#[clap(flatten)]
|
||||
command: Add,
|
||||
},
|
||||
#[clap(about = "Create a new Aleo account")]
|
||||
#[clap(about = "Create a new Aleo account, sign and verify messages")]
|
||||
Account {
|
||||
#[clap(subcommand)]
|
||||
command: Account,
|
||||
|
@ -15,13 +15,22 @@
|
||||
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
use leo_errors::UtilError;
|
||||
use leo_package::root::Env;
|
||||
use snarkvm::prelude::{Address, PrivateKey, ViewKey};
|
||||
use snarkvm::{
|
||||
cli::dotenv_private_key,
|
||||
console::program::{Signature, ToFields, Value},
|
||||
prelude::{Address, PrivateKey, ViewKey},
|
||||
};
|
||||
|
||||
use crossterm::ExecutableCommand;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Commands to manage Aleo accounts.
|
||||
#[derive(Parser, Debug)]
|
||||
@ -49,6 +58,39 @@ pub enum Account {
|
||||
#[clap(long)]
|
||||
discreet: bool,
|
||||
},
|
||||
/// Sign a message using your Aleo private key.
|
||||
Sign {
|
||||
/// Specify the account private key of the node
|
||||
#[clap(long = "private-key")]
|
||||
private_key: Option<PrivateKey<CurrentNetwork>>,
|
||||
/// Specify the path to a file containing the account private key of the node
|
||||
#[clap(long = "private-key-file")]
|
||||
private_key_file: Option<String>,
|
||||
/// Message (Aleo value) to sign
|
||||
#[clap(short = 'm', long)]
|
||||
message: String,
|
||||
/// Seed the RNG with a numeric value
|
||||
#[clap(short = 's', long)]
|
||||
seed: Option<u64>,
|
||||
/// When enabled, parses the message as bytes instead of Aleo literals
|
||||
#[clap(short = 'r', long)]
|
||||
raw: bool,
|
||||
},
|
||||
/// Verify a message from an Aleo address.
|
||||
Verify {
|
||||
/// Address to use for verification
|
||||
#[clap(short = 'a', long)]
|
||||
address: Address<CurrentNetwork>,
|
||||
/// Signature to verify
|
||||
#[clap(short = 's', long)]
|
||||
signature: String,
|
||||
/// Message (Aleo value) to verify the signature against
|
||||
#[clap(short = 'm', long)]
|
||||
message: String,
|
||||
/// When enabled, parses the message as bytes instead of Aleo literals
|
||||
#[clap(short = 'r', long)]
|
||||
raw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Command for Account {
|
||||
@ -94,6 +136,37 @@ impl Command for Account {
|
||||
write_to_env_file(private_key, &ctx)?;
|
||||
}
|
||||
}
|
||||
Self::Sign { message, seed, raw, private_key, private_key_file } => {
|
||||
let key = match (private_key, private_key_file) {
|
||||
(Some(private_key), None) => private_key,
|
||||
(None, Some(private_key_file)) => {
|
||||
let path = private_key_file
|
||||
.parse::<PathBuf>()
|
||||
.map_err(|e| CliError::cli_invalid_input(format!("invalid path - {e}")))?;
|
||||
let key_str = std::fs::read_to_string(path).map_err(UtilError::failed_to_read_file)?;
|
||||
PrivateKey::<CurrentNetwork>::from_str(key_str.trim())
|
||||
.map_err(|e| CliError::cli_invalid_input(format!("could not parse private key: {e}")))?
|
||||
}
|
||||
(None, None) => {
|
||||
// Attempt to pull private key from env, then .env file
|
||||
match dotenvy::var("PRIVATE_KEY")
|
||||
.map_or_else(|_| dotenv_private_key(), |key| PrivateKey::<CurrentNetwork>::from_str(&key))
|
||||
{
|
||||
Ok(key) => key,
|
||||
Err(_) => Err(CliError::cli_invalid_input(
|
||||
"missing the '--private-key', '--private-key-file', PRIVATE_KEY env, or .env",
|
||||
))?,
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_)) => Err(CliError::cli_invalid_input(
|
||||
"cannot specify both the '--private-key' and '--private-key-file' flags",
|
||||
))?,
|
||||
};
|
||||
println!("{}", sign_message(key, message, seed, raw)?);
|
||||
}
|
||||
Self::Verify { address, signature, message, raw } => {
|
||||
println!("{}", verify_message(address, signature, message, raw)?)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -152,3 +225,156 @@ fn wait_for_keypress() {
|
||||
let mut single_key = [0u8];
|
||||
std::io::stdin().read_exact(&mut single_key).unwrap();
|
||||
}
|
||||
|
||||
// Sign a message with an Aleo private key
|
||||
pub(crate) fn sign_message(
|
||||
private_key: PrivateKey<CurrentNetwork>,
|
||||
message: String,
|
||||
seed: Option<u64>,
|
||||
raw: bool,
|
||||
) -> Result<String> {
|
||||
// Recover the seed.
|
||||
let mut rng = match seed {
|
||||
// Recover the field element deterministically.
|
||||
Some(seed) => ChaChaRng::seed_from_u64(seed),
|
||||
// Sample a random field element.
|
||||
None => ChaChaRng::from_entropy(),
|
||||
};
|
||||
|
||||
// Sign the message
|
||||
let signature = if raw {
|
||||
private_key.sign_bytes(message.as_bytes(), &mut rng)
|
||||
} else {
|
||||
let fields = Value::<CurrentNetwork>::from_str(&message)?
|
||||
.to_fields()
|
||||
.map_err(|_| CliError::cli_invalid_input("Failed to parse a valid Aleo value"))?;
|
||||
private_key.sign(&fields, &mut rng)
|
||||
}
|
||||
.map_err(|_| CliError::cli_runtime_error("Failed to sign the message"))?
|
||||
.to_string();
|
||||
// Return the signature as a string
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
// Verify a signature with an Aleo address
|
||||
pub(crate) fn verify_message(
|
||||
address: Address<CurrentNetwork>,
|
||||
signature: String,
|
||||
message: String,
|
||||
raw: bool,
|
||||
) -> Result<String> {
|
||||
let signature = Signature::<CurrentNetwork>::from_str(&signature)
|
||||
.map_err(|e| CliError::cli_invalid_input(format!("Failed to parse a valid signature: {e}")))?;
|
||||
|
||||
// Verify the signature
|
||||
let verified = if raw {
|
||||
signature.verify_bytes(&address, message.as_bytes())
|
||||
} else {
|
||||
let fields = Value::<CurrentNetwork>::from_str(&message)?
|
||||
.to_fields()
|
||||
.map_err(|_| CliError::cli_invalid_input("Failed to parse a valid Aleo value"))?;
|
||||
signature.verify(&address, &fields)
|
||||
};
|
||||
|
||||
// Return the verification result
|
||||
match verified {
|
||||
true => Ok("✅ The signature is valid".to_string()),
|
||||
false => Err(CliError::cli_runtime_error("❌ The signature is invalid"))?,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{sign_message, verify_message};
|
||||
|
||||
#[test]
|
||||
fn test_signature_raw() {
|
||||
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".parse().unwrap();
|
||||
let message = "Hello, world!".to_string();
|
||||
assert!(sign_message(key, message, None, true).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature() {
|
||||
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".parse().unwrap();
|
||||
let message = "5field".to_string();
|
||||
assert!(sign_message(key, message, None, false).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_fail() {
|
||||
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".parse().unwrap();
|
||||
let message = "not a literal value".to_string();
|
||||
assert!(sign_message(key, message, None, false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seeded_signature_raw() {
|
||||
let seed = Some(38868010450269069);
|
||||
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".parse().unwrap();
|
||||
let message = "Hello, world!".to_string();
|
||||
let expected = "sign175pmqldmkqw2nwp7wz7tfmpyqdnvzaq06mh8t2g22frsmrdtuvpf843p0wzazg27rwrjft8863vwn5a5cqgr97ldw69cyq53l0zlwqhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkevd26g";
|
||||
let actual = sign_message(key, message, seed, true).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seeded_signature() {
|
||||
let seed = Some(38868010450269069);
|
||||
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".parse().unwrap();
|
||||
let message = "5field".to_string();
|
||||
let expected = "sign1ad29myqy8gv6xve2r6tuly39m63l2mpfpyvqkwdl2umxqek6q5qxmy63zmhjx75x90sqxq69u5ntzp25kp59e0hp4hj8l8085sg7vqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk7v46re";
|
||||
let actual = sign_message(key, message, seed, false).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_raw() {
|
||||
// test signature of "Hello, world!"
|
||||
let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".parse().unwrap();
|
||||
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
|
||||
let message = "Hello, world!".to_string();
|
||||
assert!(verify_message(address, signature, message, true).is_ok());
|
||||
|
||||
// test signature of "Hello, world!" against the message "Different Message"
|
||||
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
|
||||
let message = "Different Message".to_string();
|
||||
assert!(verify_message(address, signature, message, true).is_err());
|
||||
|
||||
// test signature of "Hello, world!" against the wrong address
|
||||
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
|
||||
let message = "Hello, world!".to_string();
|
||||
let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".parse().unwrap();
|
||||
assert!(verify_message(wrong_address, signature, message, true).is_err());
|
||||
|
||||
// test a valid signature of "Different Message"
|
||||
let signature = "sign1424ztyt9hcm77nq450gvdszrvtg9kvhc4qadg4nzy9y0ah7wdqq7t36cxal42p9jj8e8pjpmc06lfev9nvffcpqv0cxwyr0a2j2tjqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk3yrr50".to_string();
|
||||
let message = "Different Message".to_string();
|
||||
assert!(verify_message(address, signature, message, true).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify() {
|
||||
// test signature of 5u8
|
||||
let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j".parse().unwrap();
|
||||
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
|
||||
let message = "5field".to_string();
|
||||
assert!(verify_message(address, signature, message, false).is_ok());
|
||||
|
||||
// test signature of 5u8 against the message 10u8
|
||||
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
|
||||
let message = "10field".to_string();
|
||||
assert!(verify_message(address, signature, message, false).is_err());
|
||||
|
||||
// test signature of 5u8 against the wrong address
|
||||
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
|
||||
let message = "5field".to_string();
|
||||
let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".parse().unwrap();
|
||||
assert!(verify_message(wrong_address, signature, message, false).is_err());
|
||||
|
||||
// test a valid signature of 10u8
|
||||
let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
|
||||
let message = "10field".to_string();
|
||||
assert!(verify_message(address, signature, message, false).is_ok());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user