#![forbid(unsafe_code)]
use anubis_age::pqc::mldsa::SigningKey;
use anubis_age::pqc::signed;
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use clap::Parser;
#[derive(Debug, Parser)]
#[command(
name = "anubis-rage-sign",
about = "Sign encrypted age files with post-quantum ML-DSA-87 signatures",
version
)]
struct Opts {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Parser)]
enum Command {
Keygen {
#[arg(short, long, value_name = "OUTPUT")]
output: PathBuf,
},
Extract {
#[arg(short, long, value_name = "IDENTITY")]
identity: PathBuf,
},
Sign {
#[arg(short = 'k', long, value_name = "KEY")]
signing_key: PathBuf,
#[arg(short, long, value_name = "INPUT")]
input: Option<PathBuf>,
#[arg(short, long, value_name = "OUTPUT")]
output: Option<PathBuf>,
},
Verify {
#[arg(short = 'k', long, value_name = "KEY")]
verification_key: String,
#[arg(short, long, value_name = "INPUT")]
input: Option<PathBuf>,
#[arg(short, long, value_name = "OUTPUT")]
output: Option<PathBuf>,
},
}
fn main() -> io::Result<()> {
let opts = Opts::parse();
match opts.command {
Command::Keygen { output } => {
let signing_key = SigningKey::generate();
let key_str = signing_key.to_string();
let content = format!(
"# created: {}\n# verification key: {}\n{}\n",
chrono::Local::now().to_rfc3339(),
signing_key.to_public(),
key_str
);
fs::write(&output, content)?;
eprintln!("Generated ML-DSA-87 signing key: {}", output.display());
eprintln!("Verification key: {}", signing_key.to_public());
Ok(())
}
Command::Extract { identity } => {
let key_contents = fs::read_to_string(&identity)?;
let signing_key: SigningKey = key_contents
.lines()
.find(|line| line.starts_with("ANUBIS-MLDSA-87-SECRET"))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no signing key found"))?
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
println!("{}", signing_key.to_public());
Ok(())
}
Command::Sign {
signing_key: key_path,
input,
output,
} => {
let key_contents = fs::read_to_string(&key_path)?;
let signing_key: SigningKey = key_contents
.lines()
.find(|line| line.starts_with("ANUBIS-MLDSA-87-SECRET"))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no signing key found"))?
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let encrypted_data = if let Some(input_path) = input {
fs::read(input_path)?
} else {
let mut data = Vec::new();
io::stdin().read_to_end(&mut data)?;
data
};
let mut signed_output = Vec::new();
signed::sign_encrypted_file(&encrypted_data, &signing_key, &mut signed_output)?;
if let Some(output_path) = output {
fs::write(output_path, signed_output)?;
} else {
io::stdout().write_all(&signed_output)?;
}
eprintln!("File signed with ML-DSA-87");
Ok(())
}
Command::Verify {
verification_key: key_str,
input,
output,
} => {
let verification_key = if let Ok(vk) = key_str.parse() {
vk
} else {
let key_contents = fs::read_to_string(&key_str)?;
key_contents
.lines()
.find(|line| line.starts_with("anubis1mldsa87"))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "no verification key found")
})?
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
};
let signed_data = if let Some(input_path) = input {
fs::read(input_path)?
} else {
let mut data = Vec::new();
io::stdin().read_to_end(&mut data)?;
data
};
let encrypted_data = signed::verify_and_extract(&signed_data, &verification_key)?;
if let Some(output_path) = output {
fs::write(output_path, encrypted_data)?;
} else {
io::stdout().write_all(&encrypted_data)?;
}
eprintln!("Signature verified successfully");
Ok(())
}
}
}