diff --git a/rust/cli/src/bin/ghostkey.rs b/rust/cli/src/bin/ghostkey.rs index 9ee6c27e..305ca065 100644 --- a/rust/cli/src/bin/ghostkey.rs +++ b/rust/cli/src/bin/ghostkey.rs @@ -5,19 +5,22 @@ use ed25519_dalek::*; use ghostkey_lib::armorable::Armorable; use ghostkey::commands::{ generate_delegate_cmd, generate_ghost_key_cmd, generate_master_key_cmd, verify_delegate_cmd, - verify_ghost_key_cmd, + verify_ghost_key_cmd, sign_message_cmd, verify_signed_message_cmd, }; use ghostkey_lib::delegate_certificate::DelegateCertificateV1; use ghostkey_lib::ghost_key_certificate::GhostkeyCertificateV1; use log::info; use std::path::Path; use std::process; +use std::fs; const CMD_GENERATE_MASTER_KEY: &str = "generate-master-key"; const CMD_GENERATE_DELEGATE: &str = "generate-delegate"; const CMD_VERIFY_DELEGATE: &str = "verify-delegate"; const CMD_GENERATE_GHOST_KEY: &str = "generate-ghost-key"; const CMD_VERIFY_GHOST_KEY: &str = "verify-ghost-key"; +const CMD_SIGN_MESSAGE: &str = "sign-message"; +const CMD_VERIFY_SIGNED_MESSAGE: &str = "verify-signed-message"; const ARG_OUTPUT_DIR: &str = "output-dir"; const ARG_IGNORE_PERMISSIONS: &str = "ignore-permissions"; @@ -140,6 +143,63 @@ fn run() -> i32 { .value_name("DIR"), ), ) + .subcommand( + Command::new(CMD_SIGN_MESSAGE) + .about("Signs a message using a ghost key") + .arg( + Arg::new("ghost_certificate") + .long("ghost-certificate") + .help("The file containing the ghost key certificate") + .required(true) + .value_name("FILE"), + ) + .arg( + Arg::new("ghost_signing_key") + .long("ghost-signing-key") + .help("The file containing the ghost signing key") + .required(true) + .value_name("FILE"), + ) + .arg( + Arg::new("message") + .long("message") + .help("The message to sign (either a file path or a string)") + .required(true) + .value_name("MESSAGE"), + ) + .arg( + Arg::new("output") + .long("output") + .help("The file to output the signed message") + .required(true) + .value_name("FILE"), + ), + ) + .subcommand( + Command::new(CMD_VERIFY_SIGNED_MESSAGE) + .about("Verifies a signed message") + .arg( + Arg::new("signed_message") + .long("signed-message") + .help("The file containing the signed message") + .required(true) + .value_name("FILE"), + ) + .arg( + Arg::new("master_verifying_key") + .long("master-verifying-key") + .help("The file containing the master verifying key") + .required(false) + .value_name("FILE"), + ) + .arg( + Arg::new("output") + .long("output") + .help("The file to output the verified message (if not provided, the message will be printed to stdout)") + .required(false) + .value_name("FILE"), + ), + ) .get_matches(); match matches.subcommand() { @@ -277,6 +337,54 @@ fn run() -> i32 { }; verify_ghost_key_cmd(&master_verifying_key, &ghost_certificate) } + Some((CMD_SIGN_MESSAGE, sub_matches)) => { + let ghost_certificate_file = Path::new(sub_matches.get_one::("ghost_certificate").unwrap()); + let ghost_certificate = match GhostkeyCertificateV1::from_file(ghost_certificate_file) { + Ok(cert) => cert, + Err(e) => { + eprintln!("{} to read ghost key certificate: {}", "Failed".red(), e); + return 1; + } + }; + let ghost_signing_key_file = Path::new(sub_matches.get_one::("ghost_signing_key").unwrap()); + let ghost_signing_key = match SigningKey::from_file(ghost_signing_key_file) { + Ok(key) => key, + Err(e) => { + eprintln!("{} to read ghost signing key: {}", "Failed".red(), e); + return 1; + } + }; + let message = sub_matches.get_one::("message").unwrap(); + let message_content = if Path::new(message).is_file() { + match fs::read(message) { + Ok(content) => content, + Err(e) => { + eprintln!("{} to read message file: {}", "Failed".red(), e); + return 1; + } + } + } else { + message.as_bytes().to_vec() + }; + let output_file = Path::new(sub_matches.get_one::("output").unwrap()); + sign_message_cmd(&ghost_certificate, &ghost_signing_key, &message_content, output_file) + } + Some((CMD_VERIFY_SIGNED_MESSAGE, sub_matches)) => { + let signed_message_file = Path::new(sub_matches.get_one::("signed_message").unwrap()); + let master_verifying_key = if let Some(key_file) = sub_matches.get_one::("master_verifying_key") { + match VerifyingKey::from_file(Path::new(key_file)) { + Ok(key) => Some(key), + Err(e) => { + eprintln!("{} to read master verifying key: {}", "Failed".red(), e); + return 1; + } + } + } else { + None + }; + let output_file = sub_matches.get_one::("output").map(|s| Path::new(s)); + verify_signed_message_cmd(signed_message_file, &master_verifying_key, output_file) + } _ => { info!("No valid subcommand provided. Use --help for usage information."); 0 diff --git a/rust/cli/src/commands.rs b/rust/cli/src/commands.rs index ceef1fa9..dd2e9e0d 100644 --- a/rust/cli/src/commands.rs +++ b/rust/cli/src/commands.rs @@ -8,9 +8,11 @@ use colored::Colorize; use ed25519_dalek::*; use log::info; use std::fs; +use std::io::Read; use std::os::unix::fs::PermissionsExt; use std::path::Path; use rand_core::OsRng; +use crate::signed_message::SignedMessage; pub fn generate_master_key_cmd(output_dir: &Path, ignore_permissions: bool) -> i32 { let (signing_key, verifying_key) = match create_keypair(&mut OsRng) { @@ -172,6 +174,84 @@ pub fn verify_delegate_cmd( } } +pub fn sign_message_cmd( + ghost_certificate: &GhostkeyCertificateV1, + ghost_signing_key: &SigningKey, + message: &[u8], + output_file: &Path, +) -> i32 { + let signature = ghost_signing_key.sign(message); + let signed_message = SignedMessage { + certificate: ghost_certificate.clone(), + message: message.to_vec(), + signature, + }; + + match signed_message.to_file(output_file) { + Ok(_) => { + println!( + "{} written {}", + "Signed message", + "successfully".green() + ); + 0 + } + Err(e) => { + eprintln!("{} to write signed message: {}", "Failed".red(), e); + 1 + } + } +} + +pub fn verify_signed_message_cmd( + signed_message_file: &Path, + master_verifying_key: &Option, + output_file: Option<&Path>, +) -> i32 { + let signed_message = match SignedMessage::from_file(signed_message_file) { + Ok(sm) => sm, + Err(e) => { + eprintln!("{} to read signed message: {}", "Failed".red(), e); + return 1; + } + }; + + match signed_message.certificate.verify(master_verifying_key) { + Ok(info) => { + println!("Ghost certificate {}", "verified".green()); + println!("Info: {}", info.blue()); + + let verifying_key = signed_message.certificate.verifying_key; + match verifying_key.verify(&signed_message.message, &signed_message.signature) { + Ok(_) => { + println!("Signature {}", "verified".green()); + match output_file { + Some(file) => { + if let Err(e) = fs::write(file, &signed_message.message) { + eprintln!("{} to write message to file: {}", "Failed".red(), e); + return 1; + } + println!("Message written to {}", file.display()); + } + None => { + println!("Message: {}", String::from_utf8_lossy(&signed_message.message)); + } + } + 0 + } + Err(e) => { + eprintln!("{} to verify signature: {}", "Failed".red(), e); + 1 + } + } + } + Err(e) => { + eprintln!("{} to verify ghost certificate: {}", "Failed".red(), e); + 1 + } + } +} + pub fn generate_ghost_key_cmd( delegate_certificate: &DelegateCertificateV1, delegate_signing_key: &RSASigningKey, diff --git a/rust/cli/src/signed_message.rs b/rust/cli/src/signed_message.rs new file mode 100644 index 00000000..593956a7 --- /dev/null +++ b/rust/cli/src/signed_message.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use ghostkey_lib::ghost_key_certificate::GhostkeyCertificateV1; +use ed25519_dalek::Signature; + +#[derive(Serialize, Deserialize)] +pub struct SignedMessage { + pub certificate: GhostkeyCertificateV1, + pub message: Vec, + pub signature: Signature, +} diff --git a/rust/cli/test_ghostkey.sh b/rust/cli/test_ghostkey.sh index 313cc782..c70e58d7 100755 --- a/rust/cli/test_ghostkey.sh +++ b/rust/cli/test_ghostkey.sh @@ -84,6 +84,28 @@ run_test "Verify delegate with wrong master key (should fail)" "cargo run --bin # Test verify-ghost-key with wrong master key (should fail) run_test "Verify ghost key with wrong master key (should fail)" "cargo run --bin ghostkey -- verify-ghost-key --master-verifying-key $temp_dir/master-2/master_verifying_key.pem --ghost-certificate $temp_dir/ghost-1/ghost_key_certificate.pem" 1 +# Test sign-message +echo "Test message" > $temp_dir/test_message.txt +run_test "Sign message" "cargo run --bin ghostkey -- sign-message --ghost-certificate $temp_dir/ghost-1/ghost_key_certificate.pem --ghost-signing-key $temp_dir/ghost-1/ghost_key_signing_key.pem --message $temp_dir/test_message.txt --output $temp_dir/signed_message.pem" 0 + +# Test verify-signed-message +run_test "Verify signed message" "cargo run --bin ghostkey -- verify-signed-message --signed-message $temp_dir/signed_message.pem --master-verifying-key $temp_dir/master-1/master_verifying_key.pem" 0 + +# Test verify-signed-message with wrong master key (should fail) +run_test "Verify signed message with wrong master key (should fail)" "cargo run --bin ghostkey -- verify-signed-message --signed-message $temp_dir/signed_message.pem --master-verifying-key $temp_dir/master-2/master_verifying_key.pem" 1 + +# Test verify-signed-message with output to file +run_test "Verify signed message with output to file" "cargo run --bin ghostkey -- verify-signed-message --signed-message $temp_dir/signed_message.pem --master-verifying-key $temp_dir/master-1/master_verifying_key.pem --output $temp_dir/verified_message.txt" 0 + +# Verify the content of the output file +if cmp -s "$temp_dir/test_message.txt" "$temp_dir/verified_message.txt"; then + echo -e "${GREEN}Verified message matches original message${NC}" + ((pass_count++)) +else + echo -e "${RED}Verified message does not match original message${NC}" + ((fail_count++)) +fi + # Clean up echo "Cleaning up temporary directory" rm -rf "$temp_dir"