Skip to content

Commit

Permalink
feat: Add sign-message and verify-signed-message commands
Browse files Browse the repository at this point in the history
  • Loading branch information
sanity committed Aug 9, 2024
1 parent 97caf66 commit 56ec336
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 1 deletion.
110 changes: 109 additions & 1 deletion rust/cli/src/bin/ghostkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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::<String>("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::<String>("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::<String>("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::<String>("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::<String>("signed_message").unwrap());
let master_verifying_key = if let Some(key_file) = sub_matches.get_one::<String>("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::<String>("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
Expand Down
80 changes: 80 additions & 0 deletions rust/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<VerifyingKey>,
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,
Expand Down
10 changes: 10 additions & 0 deletions rust/cli/src/signed_message.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
pub signature: Signature,
}
22 changes: 22 additions & 0 deletions rust/cli/test_ghostkey.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 56ec336

Please sign in to comment.